Use CATextLayer with CGContext - iphone

I need to draw text into a CGContext and want to use CATextLayer to set the text. Can anyone give me some steps as to how it'll be done. I've written some code for it, but don't know the correctness of it (since I'm new to iPhone Development).
Some basic questions :
1. How to obtain position and size of text for setting the CATextLayer frame
2. The text is somehow coming out inverted which I've fixed using CGAffineTransform, but its a hacky fix and I want to know the actual reason why the text is coming out inverted.
3. I also need to set the text border width and color for which I'm using NSAttributedString, but setting the text size (increasing or decreasing) does not reflect on the screen.
EDIT 1 :
I've updated the code as per your recommendations, but the text is not resizing even after using CTFontRef. Also, on the iPhone the text is getting truncated a little bit from the top. Any idea what's being set incorrectly? When I checked the values I'm getting from getTypographicBounds, they're all 0 (origin.x, origin.y, width and height).
The getTypographicBounds method :
width = CTLineGetTypographicBounds(m_line, &ascent, &descent, &leading);
// Return text bounds
return CGRectMake(0, 0, width, ascent + descent);
The modified code :
CATextLayer* text = [[CATextLayer alloc] init];
CGColorSpaceRef rgbColor = CGColorSpaceCreateDeviceRGB();
CGColorRef rgb = CGColorCreate(rgbColor, m_fill_color);
text.foregroundColor = rgb;
text.frame = CGRectMake([self getTypographicBounds].origin.x, [self getTypographicBounds].origin.y, [self getTypographicBounds].size.width + 2*m_padding, [self getTypographicBounds].size.height + 2*m_padding);
text.wrapped = YES;
NSMutableDictionary *stringAttributes = [NSMutableDictionary dictionary];
CTFontRef aCFFont = CTFontCreateWithName ((CFStringRef)m_font_name, 30.0, NULL);
// Set a negative width so we get both stroke and fill to show
[stringAttributes setObject: (NSObject*)aCFFont forKey: (id)kCTFontNameAttribute];
[stringAttributes setObject: [NSNumber numberWithFloat: -3 ] forKey: (id)kCTStrokeWidthAttributeName];
[stringAttributes setObject: [UIColor whiteColor] forKey: (id)kCTStrokeColorAttributeName];
NSAttributedString *stringToDraw = [[NSAttributedString alloc] initWithString:m_text
attributes:stringAttributes];
text.string = (NSString*)stringToDraw;
[text drawInContext:ctx];
EDIT 2 : The wiki example uses CTLine, I'm using CATextLayer. Reason :: CATextLayer lets me modify text during runtime to show Chinese, Japanese, Hindi, Russian and all languages with different scripts. Also, CATextLayer seems to render with more clarity than CoreText.
The issue I'm facing right now is that the text being displayed by [text drawInContext:ctx] line is truncating from the top. I checked the frame and text font size. Font size is 16 px and frame height is 17.768, so I don't know why it's truncating.
// Ensure CTLine is up-to-date
if (m_is_dirty)
[self recomputeLine];
// Get text metrics
CGFloat width;
CGFloat ascent;
CGFloat descent;
CGFloat leading;
width = CTLineGetTypographicBounds(m_line, &ascent, &descent, &leading);
// Position text so that top of text aligns with top of layer
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity );
// Setup text drawing style
CGContextSetRGBFillColor (ctx, m_fill_color [0], m_fill_color [1], m_fill_color [2], 1.0);
CGContextSetRGBStrokeColor (ctx, m_stroke_color[0], m_stroke_color[1], m_stroke_color[2], 1.0);
CGContextSetFont (ctx, m_font );
CGContextSetFontSize (ctx, m_font_size );
CGContextSetLineWidth (ctx, m_stroke_width);
CATextLayer* text = [[CATextLayer alloc] init];
CGColorSpaceRef rgbColor = CGColorSpaceCreateDeviceRGB();
CGColorRef rgb = CGColorCreate(rgbColor, m_fill_color);
text.foregroundColor = rgb;
text.fontSize = m_font_size;
text.font = m_font;
text.frame = CGRectMake([self getTypographicBounds].origin.x, [self getTypographicBounds].origin.y, [self getTypographicBounds].size.width, [self getTypographicBounds].size.height);
text.wrapped = YES;
NSMutableDictionary *stringAttributes = [NSMutableDictionary dictionary];
// Set a negative width so we get both stroke and fill to show
[stringAttributes setObject: [UIFont fontWithName:m_font_name size:m_font_size] forKey: (id)kCTFontNameAttribute];
[stringAttributes setObject: [NSNumber numberWithFloat: -3 ] forKey: (id)kCTStrokeWidthAttributeName];
[stringAttributes setObject: [UIColor whiteColor] forKey: (id)kCTStrokeColorAttributeName];
NSAttributedString *stringToDraw = [[NSAttributedString alloc] initWithString:m_text
attributes:stringAttributes];
text.string = (NSString*)stringToDraw;
[text drawInContext:ctx];
Thanks in advance
Nick

It is because of the flipped coordinate space used for CGContexts. For a full explanation (with diagrams) see here.
EDIT:
To control the position translate your context before drawing the text:
CGContextTranslateCTM(context, x, y);
EDIT 2:
Looks like you need to use a CTFontRef, not a UIFont. For an example, see here (see post by Gordon Apple Wed Jun 23 10:40:23 2010).
EDIT 3:
Unless you specifically need to use NSAttributedString, I would recommend using the much simpler NSString drawInRect:withFont:lineBreakMode:alignment: method. It creates a CATextLayer for you, but is much simpler to use:
UIFont *font = [UIFont fontWithName:m_font_name size:30.f];
[m_text drawInRect:text.frame withFont:font lineBreakMode:UILineBreakModeWordWrap alignment:UITextAlignmentLeft];
EDIT 4:
Try setting your dictionary like this:
CTFontRef ctFont = CTFontCreateWithName((__bridge CFStringRef)fontName, pointSize,NULL);
CGColorRef color = textColor.CGColor;
NSDictionary *attributesDict = [NSDictionary dictionaryWithObjectsAndKeys:
(__bridge id)ctFont, (id)kCTFontAttributeName,
color, (id)kCTForegroundColorAttributeName,nil];

Related

UILabel auto size error on iOS 7

I'm making my app transition to iOS 7 and have this method (already modified for iOS 7, using boundingRectWithSize...):
+ (CGSize)messageSize:(NSString*)message {
NSDictionary *attributes = #{NSFontAttributeName : [UIFont fontWithName:#"Futura-Medium" size:13]};
CGRect frame = [message boundingRectWithSize:CGSizeMake([PTSMessagingCell maxTextWidth], CGFLOAT_MAX) options:NSStringDrawingUsesFontLeading attributes:attributes context:nil];
return frame.size;
}
I am getting this appearance:
The message UILabel is being cut. It feels like line spacing is too big. It tried many other answers I found but none of them work.
If someone knows how to help me, I appreciate! ;)
Thanks!
Try changing NSStringDrawingUsesFontLeading as your option to NSStringDrawingUsesLineFragmentOrigin.
If you were only supporting iOS 6 and iOS 7, then I would definitely change all of your NSString's sizeWithFont:... to the NSAttributeString's boundingRectWithSize. Starting in iOS 6, the NSAttributedString's NSStringDrawing functions were introduced and they're threadsafe unlike the old NSString+UIKit methods we're used to (eg. sizeWithFont:..., etc), which were UIStringDrawing functions (and act unpredictably when you use them from a non-main thread. It'll save you a lot of headache if you happen to have a weird multi-threading corner case! Here's how I converted NSString's sizeWithFont:constrainedToSize::
What used to be:
NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
CGSize size = [text sizeWithFont:font
constrainedToSize:(CGSize){width, CGFLOAT_MAX}];
Can be replaced with:
NSString *text = ...;
CGFloat width = ...;
UIFont *font = ...;
NSAttributedString *attributedText =
[[NSAttributedString alloc]
initWithString:text
attributes:#
{
NSFontAttributeName: font
}];
CGRect rect = [attributedText boundingRectWithSize:(CGSize){width, CGFLOAT_MAX}
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
CGSize size = rect.size;
Please note the documentation mentions:
In iOS 7 and later, this method returns fractional sizes (in the size
component of the returned CGRect); to use a returned size to size
views, you must use raise its value to the nearest higher integer
using the ceil function.
So to pull out the calculated height or width to be used for sizing views, I would use:
CGFloat height = ceilf(size.height);
CGFloat width = ceilf(size.width);
I think you are updating your label frame from either viewDidLoad or viewWillAppear, so it is not working.
if you will update frame of label from viewDidAppear method then you will get updated frame of label.
I am not sure why this is happened, I think it is iOS 7 bug.
Try this
+ (CGSize)messageSize:(NSString*)message {
CGSize nameSize = [message sizeWithFont:[UIFont fontWithName:#"Futura-Medium" size:13]
constrainedToSize:CGSizeMake(maxWidth, maxHeight) lineBreakMode:NSLineBreakByWordWrapping];
NSLog(#"width = %f, height = %f", nameSize.width, nameSize.height);
return nameSize;
}

Display NSAttributedString using CoreText

I have heard that I can display a NSAttributedString using CoreText, can anyone say me how (The simplest way)?
Please, don't answer with CATextLayer or OHAttributedLabel.
I know that there are a lot of questions about this in this forum, but I haven't find the answer
Thanks!!
Simplest way? Something like this:
CGContextRef context = UIGraphicsGetCurrentContext();
// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
// Create a path to render text in
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds );
// An attributed string containing the text to render
NSAttributedString* attString = [[NSAttributedString alloc]
initWithString:...];
// create the framesetter and render text
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter,
CFRangeMake(0, [attString length]), path, NULL);
CTFrameDraw(frame, context);
// Clean up
CFRelease(frame);
CFRelease(path);
CFRelease(framesetter);
I think that the simplest way (using Core Text) is:
// Create the CTLine with the attributed string
CTLineRef line = CTLineCreateWithAttributedString(attrString);
// Set text position and draw the line into the graphics context called context
CGContextSetTextPosition(context, x, y);
CTLineDraw(line, context);
// Clean up
CFRelease(line);
Using a Framesetter is more efficient IF you are drawing lots of text but this is the method recommended by Apple if you just need to display a small amount of text (like a label) and doesn't require you to create a path or frame (since it is done for you automatically by CTLineDraw).
Starting from ios 6 you can do the following :
NSMutableParagraphStyle *paragrahStyle = [[NSMutableParagraphStyle alloc] init];
[paragrahStyle setLineSpacing:40];
[attributedString addAttribute:NSParagraphStyleAttributeName value:paragrahStyle range:NSMakeRange(0, [labelText length])];
cell.label.attributedText = attributedString ;

Set image colors from core graphics

I have image in png file like this. I load it and draw in drawRect method. Can I change colors of image in Core Graphics or Quartz on iphone? I want to have red football ball, not black. Can I do it?
The image you linked is an image with a white background which makes it a bit tricker (though there might be a way of making a specific color clear that I couldn't find). One way to do it is to fetch the bitmap representation of the image and go through each pixel to change the colors.
These examples won't work straight off on the iPhone but might serve as a starting point for what you want to do.
In the first one it simply iterates over the pixels and change all non-white pixels to red. Unless the color you want to change is always black you likely want to tint the color rather than to just set it to full red.
NSImage *image = [NSImage imageNamed:#"football.jpg"];
NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithData:[image TIFFRepresentation]];
NSSize imageSize = [bitmap size];
int samples = imageSize.height * [bitmap bytesPerRow];
unsigned char *bitmapData = [bitmap bitmapData];
int samplesPerPixel = [bitmap samplesPerPixel];
int startSample = [bitmap bitmapFormat] & NSAlphaFirstBitmapFormat ? 1 : 0;
for (int i = startSample; i < samples; i = i + samplesPerPixel) {
if (bitmapData[i] < 255.0 && bitmapData[i + 1] < 255.0 && bitmapData[i + 2] < 255.0) {
bitmapData[i] = 255.0;
}
}
NSImage *newImage = [[NSImage alloc] initWithSize:[bitmap size]];
[newImage addRepresentation:bitmap];
[bitmap release];
If you have control over the source image it will likely be easier if you create them with transparent background and saves them as PNG (or other format that supports an alpha channel). With at least AppKit you can then do a much simpler solution.
NSImage *image = [NSImage imageNamed:#"football-transparent.png"];
NSSize size = [anImage size];
NSRect imageBounds = NSMakeRect(0, 0, size.width, size.height);
NSImage *newImage = [anImage copy];
[newImage lockFocus];
[[NSColor redColor] set];
NSRectFillUsingOperation(imageBounds, NSCompositeSourceAtop);
[newImage unlockFocus];

Drop Shadow on UITextField text

Is it possible to add a shadow to the text in a UITextField?
As of 3.2, you can use the CALayer shadow properties.
_textField.layer.shadowOpacity = 1.0;
_textField.layer.shadowRadius = 0.0;
_textField.layer.shadowColor = [UIColor blackColor].CGColor;
_textField.layer.shadowOffset = CGSizeMake(0.0, -1.0);
I have a slightly different problem - I want a blurred shadow on a UILabel. Luckily, the solution to this turned out to be number (2) from Tyler
Here's my code :
- (void) drawTextInRect:(CGRect)rect {
CGSize myShadowOffset = CGSizeMake(4, -4);
CGFloat myColorValues[] = {0, 0, 0, .8};
CGContextRef myContext = UIGraphicsGetCurrentContext();
CGContextSaveGState(myContext);
CGColorSpaceRef myColorSpace = CGColorSpaceCreateDeviceRGB();
CGColorRef myColor = CGColorCreate(myColorSpace, myColorValues);
CGContextSetShadowWithColor (myContext, myShadowOffset, 5, myColor);
[super drawTextInRect:rect];
CGColorRelease(myColor);
CGColorSpaceRelease(myColorSpace);
CGContextRestoreGState(myContext);
}
This is in a class that extends from UILabel and draws the text with a shadow down and to the right 4px, the shadow is grey at 80% opacity and is sightly blurred.
I think that Tyler's solution number 2 is a little better for performance than Tyler's number 1 - you're only dealing with one UILabel in the view and, assuming that you're not redrawing every frame, it's not a hit in rendering performance over a normal UILabel.
PS This code borrowed heavily from the Quartz 2D documentation
I don't think you get built-in support for text shadows here, the way you do with UILabel.
Two ideas:
(1) [Moderately tricky to code.] Add a second UITextField behind the original, at a very small offset (maybe by (0.2,0.8)? ). You can listen to every text change key-by-key by implementing the textField:shouldChangeCharactersInRange:replacementString: method in the UITextFieldDelegate protocol. Using that, you can update the lower text simultaneously. You could also make the lower text (the shadow text) gray, and even slightly blurry using the fact that fractionally-offset text rects appear blurry. Added: Oh yea, don't forget to set the top text field's background color to [UIColor clearColor] if you go with this idea.
(2) [Even more fun to code.] Subclass UITextField and override the drawRect: method. I haven't done this before, so I'll mention up front that this depends on this being the designated drawing method, and it may turn out that you have to override another drawing function, such as drawTextInRect:, which is specific to UITextField. Now set up the drawing context to draw shadows via the CGContextSetShadow functions, and call [super drawRect:rect];. Hopefully that works -- in case the original UITextField code clears the drawing context's shadow parameters, that idea is hosed, and you'll have to write the whole drawing code yourself, which I anti-recommend because of all the extras that come with UITextFields like copy-and-paste and kanji input in Japanese.
Although the method of applying the shadow directly to the UITextView will work, it's the wrong way to do this. By adding the shadow directly with a clear background color, all subviews will get the shadow, even the cursor.
The approach that should be used is with NSAttributedString.
NSMutableAttributedString* attString = [[NSMutableAttributedString alloc] initWithString:textView.text];
NSRange range = NSMakeRange(0, [attString length]);
[attString addAttribute:NSFontAttributeName value:textView.font range:range];
[attString addAttribute:NSForegroundColorAttributeName value:textView.textColor range:range];
NSShadow* shadow = [[NSShadow alloc] init];
shadow.shadowColor = [UIColor whiteColor];
shadow.shadowOffset = CGSizeMake(0.0f, 1.0f);
[attString addAttribute:NSShadowAttributeName value:shadow range:range];
textView.attributedText = attString;
However textView.attributedText is for iOS6. If you must support lower versions, you could use the following approach. (Dont forget to add #import <QuartzCore/QuartzCore.h>)
CALayer *textLayer = (CALayer *)[textView.layer.sublayers objectAtIndex:0];
textLayer.shadowColor = [UIColor whiteColor].CGColor;
textLayer.shadowOffset = CGSizeMake(0.0f, 1.0f);
textLayer.shadowOpacity = 1.0f;
textLayer.shadowRadius = 0.0f;

iPhone: Draw rotated text?

I want to draw some text in a view, rotated 90°. I'm pretty new to iPhone development, and poking around the web reveals a number of different solutions. I've tried a few and usually end up with my text getting clipped.
What's going on here? I am drawing in a fairly small space (a table view cell), but there has to be a "right" way to do this… right?
Edit: Here are a couple of examples. I'm trying to display the text "12345" along the black bar at the left.
First attempt, from RJShearman on the Apple Discussions
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSelectFont (context, "Helvetica-Bold", 16.0, kCGEncodingMacRoman);
CGContextSetTextDrawingMode (context, kCGTextFill);
CGContextSetRGBFillColor(context, 1.0, 0.0, 0.0, 1.0);
CGContextSetTextMatrix (context, CGAffineTransformRotate(CGAffineTransformScale(CGAffineTransformIdentity, 1.f, -1.f ), M_PI/2));
CGContextShowTextAtPoint (context, 21.0, 55.0, [_cell.number cStringUsingEncoding:NSUTF8StringEncoding], [_cell.number length]);
CGContextRestoreGState(context);
(source: deeptechinc.com)
Second attempt, from zgombosi on iPhone Dev SDK. Identical results (the font was slightly smaller here, so there's less clipping).
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint point = CGPointMake(6.0, 50.0);
CGContextSaveGState(context);
CGContextTranslateCTM(context, point.x, point.y);
CGAffineTransform textTransform = CGAffineTransformMakeRotation(-1.57);
CGContextConcatCTM(context, textTransform);
CGContextTranslateCTM(context, -point.x, -point.y);
[[UIColor redColor] set];
[_cell.number drawAtPoint:point withFont:[UIFont fontWithName:#"Helvetica-Bold" size:14.0]];
CGContextRestoreGState(context);
Attempt two. There is almost identical clipping http://dev.deeptechinc.com/sidney/share/iphonerotation/attempt2.png
It turns out that the my table cell was always initialized 44px high regardless of the row height, so all of my drawing was getting clipped 44px from the top of the cell.
To draw larger cells it was necessary to set the content view's autoresizingMask with
cellContentView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
or
cellContentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
…and drawRect is called with the correct size. In a way, this makes sense, because UITableViewCell's initWithStyle:reuseIdentifier: makes no mention of the size of the cell, and only the table view actually knows how big each row is going to be, based on its own size and its delegate's response to tableView:heightForRowAtIndexPath:.
I read the Quartz 2D Programming Guide until the drawing model and functions started to make sense, and the code to draw my rotated text became simple and obvious:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextRotateCTM(context, -(M_PI/2));
[_cell.number drawAtPoint:CGPointMake(-57.0, 5.5) withFont:[UIFont fontWithName:#"Helvetica-Bold" size:16.0]];
CGContextRestoreGState(context);
Thanks for the tips, it looks like I'm all set.
Use :-
label.transform = CGAffineTransformMakeRotation(- 90.0f * M_PI / 180.0f);
where label is the object of UILabel.
Here's a tip. I presume you're doing this drawing in drawRect. Why don't you draw a frame around drawRect to see how big the rect is and if that is why you get clipping.
An alternative is to put your text in a UILabel, and then rotate that 90 degrees when you make your cells in cellForRowAtIndexPath.
You know about the UITableViewDelegate method heightForRowAtIndexPath right?
Here's a simple tutorial on various graphics level methods. Presuming you know how big your text is you should be able to size your table view row size appropriately.
Also, I'd check to make sure that the bounds after any transform actually meet your expectations. (Either use a debugger or log statement to verify this).
to what #Sidnicious said, and what i collected through out stack overflow, i want to give a usage example - appended my code to completely draw a ruler to the left screen side, with numbers rotated:
RulerView : UIView
// simple testing for iPhones (check for device descriptions to get all iPhones + iPads)
- (float)getPPI
{
switch ((int)[UIScreen mainScreen].bounds.size.height) {
case 568: // iPhone 5*
case 667: // iPhone 6
return 163.0;
break;
case 736: // iPhone 6+
return 154.0;
break;
default:
return -1.0;
break;
}
}
- (void)drawRect:(CGRect)rect
{
[[UIColor blackColor] setFill];
float ppi = [self getPPI];
if (ppi == -1.0) // unable to draw, maybe an ipad.
return;
float linesDist = ppi/25.4; // ppi/mm per inch (regular size iPad would be 132.0, iPhone6+ 154.0)
float linesWidthShort = 15.0;
float linesWidthMid = 20.0;
float linesWidthLong = 25.0;
for (float i = 0, c = 0; i <= self.bounds.size.height; i = i + linesDist, c = c +1.0)
{
bool isMid = (int)c % 5 == 0;
bool isLong = (int)c % 10 == 0;
float linesWidth = isLong ? linesWidthLong : isMid ? linesWidthMid : linesWidthShort;
UIRectFillUsingBlendMode( (CGRect){0, i, linesWidth, .5} , kCGBlendModeNormal);
/* FONT: Numbers without rotation (yes, is short)
if (isLong && i > 0 && (int)c % 10 == 0)
[[NSString stringWithFormat:#"%d", (int)(c/10)] drawAtPoint:(CGPoint){linesWidthLong +2, i -5} withAttributes:#{
NSFontAttributeName: [UIFont systemFontOfSize:9],
NSBaselineOffsetAttributeName: [NSNumber numberWithFloat:1.0]
}];
*/
// FONT: Numbers with rotation (yes, requires more effort)
if (isLong && i > 0 && (int)c % 10 == 0)
{
NSString *str = [NSString stringWithFormat:#"%d", (int)(c/10)];
NSDictionary *attrs = #{
NSFontAttributeName: [UIFont systemFontOfSize:9],
NSBaselineOffsetAttributeName: [NSNumber numberWithFloat:0.0]
};
CGSize textSize = [str sizeWithAttributes:attrs];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextRotateCTM(context, +(M_PI/2));
[str drawAtPoint:(CGPoint){i - (textSize.width/2), -(linesWidthLong + textSize.height +2)} withAttributes:attrs];
CGContextRestoreGState(context);
}
}
}
After I discovered that I needed to add the following to the top of my file I liked Matt's approach. Very simple.
#define degreesToRadian(x) (M_PI * (x) / 180.0)
mahboudz's suggestion will probably be your path of least resistance. You can rotate the UILabel 90deg with this: [label setTransform:CGAffineTransformMakeRotation(DegreesToRadians(-90.0f))]; You'll just have to calculate your cell height based upon the label width. -Matt – Matt Long Nov 10 at 0:09