I needed a similar functionality like Rob Napier suggested when laying out text without a framesetter. however Rob's answer didn't exactly look the same as when laid out with a framesetter, so I slightly reworked it:
https://gist.github.com/jpiringer/75ed6e666832d1f8201a6b2c79610736
#include "CustomLayouter.hpp"
#import <Foundation/Foundation.h>
#include <CoreText/CoreText.h>
#include <malloc/malloc.h>
static const CFRange kRangeZero = {0, 0};
CFIndex layoutTextInRectangle(CGContextRef context, CGRect rect, CFAttributedStringRef attributedString, CFIndex startLocation, float justificationWidth, float baselineGrid, float baselineGridOffsetY) {
// Calculate the lines
CFIndex start = startLocation;
CGFloat boundsWidth = rect.size.width;
CGPoint textPosition = CGPointMake(rect.origin.x, rect.origin.y+rect.size.height);
CTTypesetterRef typesetter;
CGPoint *positionsBuffer = nullptr;
CGGlyph *glyphsBuffer = nullptr;
typesetter = CTTypesetterCreateWithAttributedString(attributedString);
NSUInteger length = CFAttributedStringGetLength(attributedString);
while (start < length && textPosition.y > rect.origin.y) {
CFIndex count = CTTypesetterSuggestLineBreak(typesetter, start, boundsWidth);
CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(start, count));
CGFloat ascent;
CGFloat descent;
CGFloat leading;
double lineWidth = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat lineHeight = ascent+descent+leading;
if (textPosition.y-lineHeight <= rect.origin.y) {
break;
}
if (justificationWidth > 0) {
// Full-justify if the text isn't too short.
if ((lineWidth / boundsWidth) > justificationWidth) {
CTLineRef justifiedLine = CTLineCreateJustifiedLine(line, 1.0, boundsWidth);
CFRelease(line);
line = justifiedLine;
}
}
CGContextSetTextPosition(context, textPosition.x, ceilf(textPosition.y-ascent));
// Get the CTRun list
CFArrayRef glyphRuns = CTLineGetGlyphRuns(line);
CFIndex runCount = CFArrayGetCount(glyphRuns);
for (CFIndex runIndex = 0; runIndex < runCount; ++runIndex) {
CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(glyphRuns, runIndex);
CTFontRef runFont = (CTFontRef)CFDictionaryGetValue(CTRunGetAttributes(run),
kCTFontAttributeName);
//CGFloat lineHeight = getLineHeight(runFont);
//NSLog(@"lineHeight: %f == %f", lineHeight, ascent+descent+leading);
// FIXME: We could optimize this by caching fonts we know we use.
CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, nullptr);
CGContextSetFont(context, cgFont);
CGContextSetFontSize(context, CTFontGetSize(runFont));
CFRelease(cgFont);
CFIndex glyphCount = CTRunGetGlyphCount(run);
// This is slightly dangerous. We're getting a pointer to the internal
// data, and yes, we're modifying it. But it avoids copying the memory
// in most cases, which can get expensive.
CGPoint *positions = (CGPoint*)CTRunGetPositionsPtr(run);
if (positions == nullptr) {
size_t positionsBufferSize = sizeof(CGPoint) * glyphCount;
if (malloc_size(positionsBuffer) < positionsBufferSize) {
positionsBuffer = (CGPoint *)realloc(positionsBuffer, positionsBufferSize);
}
CTRunGetPositions(run, kRangeZero, positionsBuffer);
positions = positionsBuffer;
}
// This one is less dangerous since we don't modify it, and we keep the const
// to remind ourselves that it's not to be modified lightly.
const CGGlyph *glyphs = CTRunGetGlyphsPtr(run);
if (glyphs == nullptr) {
size_t glyphsBufferSize = sizeof(CGGlyph) * glyphCount;
if (malloc_size(glyphsBuffer) < glyphsBufferSize) {
glyphsBuffer = (CGGlyph *)realloc(glyphsBuffer, glyphsBufferSize);
}
CTRunGetGlyphs(run, kRangeZero, (CGGlyph*)glyphs);
glyphs = glyphsBuffer;
}
CGContextShowGlyphsAtPositions(context, glyphs, positions, glyphCount);
}
// Move the index beyond the line break.
start += count;
textPosition.y -= floorf(descent + leading + ascent);
CFRelease(line);
}
free(positionsBuffer);
free(glyphsBuffer);
CFRelease(typesetter);
if (start > length) {
return 0;
}
return start;
}