Getting information about a glyph in Core Text - ios

I just saw this recent question on SO, which gets somewhat close to what I'm asking here.
So I'm trying to typeset a mathematical equation using Core Text and NSAttributedString. It was working pretty well for expressions like x2, but then I came across a problem when I use y. Namely, 'y' has a descender which I don't know how to take into account in my drawing code:
CGContextSaveGState(context);
CGContextTranslateCTM(context, x, sizeOfGlyph.height / 2.0);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributedSubstring);
CGContextSetTextPosition(context, 0.0, 0.0);
CTLineDraw(line, context);
CFRelease(line);
CGContextRestoreGState(context);
Basically, I translate the context by half the glyph's height (as calculated here) to start the text at the baseline of the glyph. As you can imagine, this runs into problems with glyphs like 'y', because half the glyph's height isn't the baseline of the glyph. I don't think it's sufficient to use the font's descender, because I have no way of knowing whether this glyph has a descender or not. So what I'd like to find out is whether there is a way to find the descender (or any desired metrics) for a particular glyph?
Any know-how on the topic would be appreciated, I'm just getting started with Core Text.

What I ended up doing was to use the origin of the glyph returned from the image bounds function. I calculated the bounds of the glyph using the method outlined in this answer. The origin of the resulting CGRect gives the y-position of the glyph's bottom-left corner, which in the case of 'y' would be negative. So by adding together glyphRect.origin.y + glyphRect.size.height, you get the distance from the top of the glyph to the baseline.

Related

How to reset a Rectangle's x,y,width,height, after transform

I have an app that uses konvajs, where I set rectangles to be resizable. I have it set such that after I transform the rectangle I set the scaleX and scaleY to 1 so I can just use x, y, width, and height. I do this with the following code:
myRectangle.on('transformend', function() {
myRectangle.width(Math.round(myRectangle.width() * myRectangle.scaleX()));
myRectangle.height(Math.round(myRectangle.height() * myRectangle.scaleY()));
myRectangle.scaleX(1);
myRectangle.scaleY(1);
});
However, sometimes after I resize (usually if I "flip" the rectangle by dragging up or to the left), the x, y, width and height are strange values. Sometimes the width or height is negative, sometimes it seems like the x and y positions do not refer to the top left of the rectangle. I want to be able to extract information about the rectangle, so I would like position to be top left of the rectangle with positive width and height values. I don't mind resetting these values after the rectangle is tranformed, but I am not quite sure how konvajs is calculating the x,y,width, and height so I can't properly reset them. Is there some metric indicating when a tranform "flips" a rectangle? Or some other way to reset it?
It seems that setting flipEnabled and rotationEnabled to false on the transformer prevents rotations from happening.
To get a visual sense of what is happening to the attrs during the transform, take a look at the demo in the official docs here and pay special attention to width/height, rotation and scale as you resize by dragging the right edge first, then repeat with the bottom edge.
It will help to understand that dragging a Transformer handle changes the scale of the rectangle - not the width or height. However this is not the end of the story - if you 'flip' the shape in the horizontal axis then you will see that the rotation is changed from zero to 180 degrees and the scaleX remains positive. But if you drag and flip the shape in the vertical axis then there is no rotation effect and the scaleY switches to negative.
Long story short - at the moment I can't think of a useful use-case that requires trying to redraw the rectangle without scale or rotation affects, which I will refer to as the 'plain' rect versus the 'exotic' rect you get after using the Transformer.
If the use-case is hit detection via your own math then you have everything you need to know in the rects x & y, width & height, rotation and scaleX & scaleY. Even if you could get the attrs for a plain rect you would still have the same params to plug into your math, so recomputing the plain rect is wasted effort.
If the use-case is storage (serialization) of the rect's attrs then again the same point as above - you need to store the position, rotation, size, and scale so as to be able to redraw it later.
A legitimate use-case for resetting scale to 1 would be if your app's business case requires it. But this only covers resetting:
rect.seAttrs({
width: rect.width() * scaleX,
height: rect.height() * scaleY,
scaleX: 1,
scaleY: 1
}
and leaves the rect at the same position and rotation.
Conclusion: attempting to recompute a plain rect from an exotic rect may not be worth the effort in some cases.

CATransform3D - understanding the transform values

The picture shows a simple UIView after applying the following transform:
- (CATransform3D) transformForOpenedMenu
{
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 /450;
transform = CATransform3DRotate(transform, D2R(40), 0, 1, 0);
transform = CATransform3DTranslate(transform, 210, 150, -500);
return transform;
}
I'm trying to make the distances highlighted with black to have equal length. Could you please help me understand the logic behind the values and calculations?
Cheers
UPD Sept 13
Looks like removing 3DTranslate keeps distances equal. I see I can use layer's frame property to reposition rotated view to the bottom left of the screen. Not yet sure, but this might actually work.
The .m34 value you are setting is best set on the sublayerTransform of the containing view rather than the view you are transforming.
I don't fully understand the maths behind affine transforms so I made this project which allows me to play around with the transform values to achieve the effect I want. You can plug in the values from your code above and see what it looks like, though note that there is already a perspective value applied using the sublayerTransform property mentioned above.
For your specific case, I think you want to adjust the anchor point of the layer to (0.0,0.5) and apply the rotation transform only. This assumes you want the menu to swing back like a door, with the hinges on the left edge.
The problem you're seeing is caused by your CATransform3DTranslate call. You're essentially setting the Y Axis off center, and hence seeing a different perspective view of the frame.
Think of it this way;
You're standing in the center of a long narrow field stretching off into the horizon. The edge of the field appears as if it is converges to a center point somewhere off in the distance. The angle of each edge to the converging point will appear equal if you are at the center of the field. If, on the other hand, you move either to the left or the right, the angles change and one will seem greater than the other (inversely opposite of course).
This is essentially what is happening with your view; As your converging points are to the right, changing the Y axis away from 0 will have the same effect as moving to the left or right in my example above. You're no longer looking at the parallel lines from the center.
so in your code above Setting the ty in CATransform3DTranslate to 0 Should fix your problem I.E.
transform = CATransform3DTranslate(transform, 210, 0, -500);
You may also need to alter the tz and tx Value to make it fit.
OK, so what eventually solved my question is this:
3D transform on Y axis to swing the view like a door transform = CATransform3DRotate(transform, D2R(40), 0, 1, 0);
set Z anchor point on a layer, to move it back targetView.layer.anchorPointZ = 850;
adjust layer position so that the view is located slightly to the bottom left of the parent view:
newPosition.x += 135 * positionDirection;
newPosition.y += 70 * positionDirection;
This sequence adjusts position without CATransform3DTranslate and keeps the 'swinged' effect not distorted.
Thanks everybody!

iOS: Draw text on UIView with LineWidth 1px

I have a scenario where I will have to draw a text on a UIView. For which my code is
- (void)drawRect:(CGRect)iRect {
[super drawRect:iRect];
CGContextRef aContext = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(aContext, [[UIColor greenColor] CGColor]);
[#"s" drawInRect:CGRectMake(0, 0, iRect.size.width, iRect.size.height) withFont:[UIFont systemFontOfSize:500] lineBreakMode:NSLineBreakByCharWrapping alignment:NSTextAlignmentCenter];
}
With this code I can achieve:
However, since I'm using a FontSize of 500, the text lineWidth is big.
I would like to reduce the lineWidth to 1px.
Please advice on how to achieve this.
Is there another method apart from the conventional drawInRect.
Will CoreText help me get the solution
Thanks.
EDIT
Using UIBezierPath for the Glyph, I was able to get the Path of the Character. But the path that is achieved is through the border of the character. hence, I get the text as:
Can this be made a single line ?
Even though it looks as if the letter S is drawn as a line of a certain width, that's not the case. If you look closer, then you'll see that the line width varies. It's in fact a complex graphical construct defined by it's outline. It's even more obvious if you use a font with serifs. As a consequence, there is no such thing as a text line width.
The best solution probably is to use a different font that has the look as if it was drawn using a narrow pen. I'm not sure if such a font is preinstalled on iOS.

Line spacing in multi-language layout with Core Text

The attributed string has only one attribute - 17 point Helvetica NeueUI font - covering the whole string. Line 1~3 are purely English, line 4~6 are mixtures of English and Chinese, line 7~8 are purely Chinese.
It is then layouted with CTFramesetter and resultant frame is drawn with CTFrameDraw.
// UIView subclass
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor whiteColor];
CTFontRef font = CTFontCreateWithName(CFSTR("Helvetica NeueUI"), 17.f, NULL);
_text = [[NSAttributedString alloc] initWithString:string attributes:
[NSDictionary dictionaryWithObject:(id)font forKey:(id)kCTFontAttributeName]];
CFRelease(font);
_framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)_text);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
_frame = CTFramesetterCreateFrame(_framesetter, CFRangeMake(0, 0), path, NULL);
CGPathRelease(path);
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
// Flip the coordinate system.
CGContextTranslateCTM(context, 0.f, self.bounds.size.height);
CGContextScaleCTM(context, 1.f, -1.f);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CTFrameDraw(_frame, context);
CGContextRestoreGState(context);
}
The problem is that the space between line 7 and 8 (purely Chinese lines) is much smaller than others.
For contrast, I put a UILabel below it, with the same font and text. Ignoring the rendering difference of single characters, you can see the line spaces in UILabel are uniform including the last one between two Chinese lines.
(source: skitch.com)
Notice: this results above is got on a real device. If you run the same code on the simulator, you'll get very different result for the Core Text view — spaces between lines containing Chinese are much larger than others.
Why is the space between Chinese lines smaller(larger)?
How can I layout multi-language text with uniform line height using Core Text?
PS: here is the sample project you can try yourself.
Similarly to the space between each letter in European languages, Chinese writing uses a narrow space between characters, though it does not observe the equivalent to the wider space between words, except in rare occasions. (In this respect, it may somewhat resemble a form of scriptio continua.)
When space is used, it is also fullwidth. One instance of its usage is as an honorific marker. A modern example in Taiwan, is found in the reference to Chiang Kai-shek as 先總統 蔣公 (Former President, Lord Chiang), in which the preceding space serves as an honorific marker for 蔣公. This use is also still current in very formal letters or other old-style documents.
When Chinese is written in transliterated form (as in Hanyu Pinyin), spaces are used to assist in reading. source:http://en.wikipedia.org/wiki/Chinese_punctuation
So its because in chinese as space is used a bit differently than in english.
The reason for the space is depending on the characters, as it could be found when querying parts of the text for its dimension, but this does not declare why a label simply ignores this problem.
However the space is in fact introduced by the FrameSetter in order to achieve even space you could use the FrameSetter to get the line breaks and use a TypeSetter (CTTypesetterCreateWithAttributedString) to produce the lines and point them at a desired line distance, as shown in the Manual Line Break from Apple. This also works for your given sample, as i have briefly checked.
Anyway I could only agree that the behaviour of the label is somehow odd, but not this bad as it looks better.
I finally got an official answer from Apple engineer:
CTLine metrics are based on the maximum of each of {ascent, descent,
leading}; this has not changed, nor will it. Clients may override the
line metrics using paragraph style specifiers or iOS 7 text styles,
for example [UIFont preferredFontForTextStyle:UIFontTextStyleBody].

How do I draw a point using Core Graphics?

I see APIs in Quartz for drawing lines and circles. But all I want to do is to specify the (x,y) cartesian coordinate to color a pixel a particular value. How do I do that?
CGContextFillRect(context, CGRectMake(x,y,1,1));
Quartz is not a pixel-oriented API, and its contexts aren’t necessarily pixel buffers. If you want to draw pixmaps, create a bitmap context with CGBitmapContextCreate(). You provide a buffer, which you can manipulate directly, and can copy to another context by creating a CGImage from the same buffer using CGImageCreate() and drawing that.
Draw a very small ellipse/circle and fill it!
CGContextAddEllipseInRect(Context,(CGRectMake (x_dot, y_dot, 3.0, 3.0));
CGContextDrawPath(Context, kCGPathFill);
CGContextStrokePath(Context);
I am using this code to create a small dot (3x3 pixels) in a dotted music note.
I got a point (zero length line) to draw after setting the line-caps to kCGLineCapRound. The default line-cap has no length so can't be drawn.
The argument that a point has no size is silly. The lines have no width but we can draw those (by using the "line width" from the draw state). A point should draw in exactly the same way, and with different line caps I believe it does.
Maybe this behavior is new?
You can draw a 1-pixel-length line at the coordinate in question; that should accomplish what you want.
I'm having the same issue - i find the best solution is similar to the last, but at least it doesn't leave something that looks like a "dash"... of course, should ensure x/y are both > 0.
CGContextFillRect(context, CGRectMake(x - 0.5, y - 0.5, 1.0 , 1.0));
Swift 3 :
UIRectFill(CGRect(x: x, y: y, width: 1, height: 1))
UIRectFill()

Resources