Reputation: 1624
I am trying to draw slightly rotated text with Core Graphics on the iOS platform. Text renders fine when not rotating but the rendering system tries to lock onto pixels for rotated text.
For example: If you rotate a Core Graphics context by some small amount (like 2 degrees) and then draw text the individual characters will appear to jump up and down as Core Graphics locks the characters to the pixels (font hinting). I know that the text may become blurry if it would not lock onto the pixel grid but that's acceptable. Jumping characters are not. So how can I disable vertical font hinting? Hinting horizontal would be ok, but turning it all off is ok too.
Code for custom UIView:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
[self.backgroundColor setFill];
CGContextFillRect(context, rect);
CGContextSaveGState(context);
// rotate context
CGContextTranslateCTM(context, self.bounds.size.width / 2.0, self.bounds.size.width / 2.0);
CGContextRotateCTM(context, 2.0 * M_PI / 180.0);
CGContextTranslateCTM(context, -self.bounds.size.width / 2.0, -self.bounds.size.width / 2.0);
[[UIColor blackColor] setFill];
[self.title drawInRect:[self bounds] withFont:[UIFont systemFontOfSize:15.0]];
CGContextRestoreGState(context);
}
Result (not exactly of this code but similar, red lines inserted to guide to the "error"):
Upvotes: 2
Views: 1777
Reputation: 1379
The only solution I have found is to get the actual Bezier paths of the glyphs with Core Text and draw those, which circumvents any vertical hinting. The following code excerpt is rather lengthy:
CGRect textRect = CGRectMake(0.0, 0.0, 300.0, 190.0);
CGContextRef context = UIGraphicsGetCurrentContext();
// Flip coordinate system vertically.
CGContextSaveGState(context);
CGFloat rectHeight = textRect.size.height;
CGContextTranslateCTM(context, 0.0, rectHeight);
CGContextScaleCTM(context, 1.0f, -1.0f);
// Positive degrees because of flip.
CGAffineTransform rotationTransform = CGAffineTransformMakeRotation(2.0 * M_PI/180.0);
CGContextConcatCTM(context, rotationTransform);
CGFloat pointSize = 15.0;
CTFontRef font = CTFontCreateUIFontForLanguage(kCTFontSystemFontType,
pointSize,
NULL);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
NSDictionary *initialAttributes = (
@{
(NSString *)kCTFontAttributeName : (__bridge id)font,
(NSString *)kCTForegroundColorAttributeName : (__bridge id)[UIColor blackColor].CGColor
}
);
NSMutableAttributedString *attributedString =
[[NSMutableAttributedString alloc] initWithString:[self string]
attributes:initialAttributes];
//
// For typesetting a frame, we should create a paragraph style.
// Includes fix for CTFramesetter’s wrong line spacing behavior.
// See Technical Q&A QA1698: “How do I work-around an issue where some lines
// in my Core Text output have extra line spacing?”
//
// Center alignment looks best when filling an ellipse.
CTTextAlignment alignment = kCTLeftTextAlignment;
CTLineBreakMode lineBreakMode = kCTLineBreakByWordWrapping;
// This is the leading in the historical sense, which is added to the point
// size but does not include it like the line height does.
CGFloat leading = 2.0;
// Still, for the fix we do need the line height.
CGFloat lineHeight = pointSize + leading;
CTParagraphStyleSetting paragraphStyleSettings[] =
{
{
kCTParagraphStyleSpecifierAlignment,
sizeof(alignment),
&alignment
},
{
kCTParagraphStyleSpecifierLineBreakMode,
sizeof(lineBreakMode),
&lineBreakMode
},
// These two specifiers fix the line spacing when set to line height.
{
kCTParagraphStyleSpecifierMinimumLineHeight,
sizeof(lineHeight),
&lineHeight
},
{
kCTParagraphStyleSpecifierMaximumLineHeight,
sizeof(lineHeight),
&lineHeight
}
// Very important: Do not set kCTParagraphStyleSpecifierLineSpacing too,
// or it will be added again!
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(
paragraphStyleSettings,
sizeof(paragraphStyleSettings) / sizeof(paragraphStyleSettings[0])
);
// Apply paragraph style to entire string. This cannot be done when the
// string is empty, by the way, because attributes can only be applied to
// existing characters.
NSRange stringRange = NSMakeRange(0, [attributedString length]);
[attributedString addAttribute:(NSString *)kCTParagraphStyleAttributeName
value:(__bridge id)(paragraphStyle)
range:stringRange];
// Create bezier path to contain our text.
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, textRect);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)(attributedString));
// Range with length 0 indicates that we want to typeset until we run out of
// text or space.
CTFrameRef frame = CTFramesetterCreateFrame(
framesetter,
CFRangeMake(0, 0),
path,
NULL
);
CFArrayRef lines = CTFrameGetLines(frame);
CFIndex lineCount = CFArrayGetCount(lines);
CFRange range = CFRangeMake(0, 0);
CGPoint lineOrigins[lineCount];
CTFrameGetLineOrigins(frame, range, lineOrigins);
for (NSUInteger lineIndex = 0; lineIndex < lineCount; ++lineIndex)
{
CTLineRef line = CFArrayGetValueAtIndex(lines, lineIndex);
CGPoint lineOrigin = lineOrigins[lineIndex];
CFArrayRef runs = CTLineGetGlyphRuns(line);
CFIndex runCount = CFArrayGetCount(runs);
for (NSUInteger runIndex = 0; runIndex < runCount; ++runIndex)
{
CTRunRef run = CFArrayGetValueAtIndex(runs, runIndex);
CFIndex glyphCount = CTRunGetGlyphCount(run);
CGGlyph glyphBuffer[glyphCount];
CTRunGetGlyphs(run, range, glyphBuffer);
CGPoint positionsBuffer[glyphCount];
CTRunGetPositions(run, range, positionsBuffer);
for (NSUInteger glyphIndex = 0; glyphIndex < glyphCount; ++glyphIndex)
{
CGGlyph glyph = glyphBuffer[glyphIndex];
CGPoint position = positionsBuffer[glyphIndex];
CGAffineTransform positionTransform = CGAffineTransformMakeTranslation(lineOrigin.x + position.x,
lineOrigin.y + position.y);
CGPathRef glyphPath = CTFontCreatePathForGlyph(font, glyph, &positionTransform);
CGContextAddPath(context, glyphPath);
}
}
}
CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
CGContextFillPath(context);
CFRelease(font);
CFRelease(framesetter);
// Use specialized release function when it exists.
CGPathRelease(path);
CGContextRestoreGState(context);
Upvotes: 1