Core Text Layout Issues

Discussion in 'Mac Programming' started by printf, May 30, 2010.

  1. printf macrumors regular

    Joined:
    Aug 27, 2008
    #1
    I originally posted this in the iphone programming section, but having thought about it, it probably makes more sense to post this here since core text isn't exlusive to the iphone, and more importantly, it probably isn't used as much by iphone developers as os x developers, so here goes:


    First off, i'm going to explain my approach, but if you're an expert at core text, go straight to the bottom where I explain exactly what I'm after.

    Now, maybe I'm the only one who's struggling with this, because I'm not seeing much else about it in google land, but how can i easily layout text using top and left as the origin?

    The following is usually what I see recommended, but there are issues that creep up almost immediately when going this route:
    Code:
    CGContextTranslateCTM(context, 0.0f, rect.size.height);
    CGContextScaleCTM(context, 1.0f, -1.0f);
    
    for example, the only way to position text down is by making the y value negative as shown here:
    Code:
    CGPathAddRect(path, NULL, CGRectMake(0,-50,100,rect.size.height));
    
    that might be forgivable, but if i'm forced then to use the height of the view/context. if not, it pushes the text down toward the bottom:
    Code:
    CGPathAddRect(path, NULL, CGRectMake(0,-50,100,200));
    
    I tried playing with the text matrix, but it had no effect on it:
    Code:
    CGContextSetTextMatrix(context, CGAffineTransformMakeTranslation(0, 200));
    



    OK, so all i really want is to specify a rectangle and for core text to display the text at the exact pixel location regardless of any width/height combination. How can I accomplish this?
     
  2. kainjow Moderator emeritus

    kainjow

    Joined:
    Jun 15, 2000
    #2
    "Core Text" is an actual API that doesn't exist publicly on the iPhone until I believe 3.2. That API uses the CT prefix.

    On the iPhone prior to 3.2 you're limited pretty much to the NSString additions in UIKit for drawing and sizing text and the few CG functions. Take a look a UIStringDrawing.h - you may find what you're after there.
     
  3. printf thread starter macrumors regular

    Joined:
    Aug 27, 2008
    #3
    hey kainjow, you are correct, it was not available until 3.2, but i've downloaded the sdk 4 beta, so i've got it working, just not the layout issues i mentioned above.

    i re-read my original post and realized my actual problem might not be clear. i guess it really has to do with the bottom-left origin when drawing text - ANY text.. core text, nsstring, or cgcontext text api's. I've worked with them all, but the two you mention have limitations of not being able to measure text and/or format it in a paragraph, set the letter spacing, etc, so that's why i chose core text.

    anyway, i guess i was just curious if anyone knew any 'tricks' to avoid having to invert the context and set the matrix transform to get it to display top-left, because when you've played with it for a while you realize that it's not intuitive and rather difficult to do something as simple as drawing text at an arbitrary point.
     
  4. printf thread starter macrumors regular

    Joined:
    Aug 27, 2008
    #4
    here's something you can try yourself just put this in your custom view's drawRect method:

    Code:
    	CGContextRef context = UIGraphicsGetCurrentContext();
    	
    	CGContextTranslateCTM(context, 0.0f, rect.size.height); //seems to work better by translating, then scaling
    	CGContextScaleCTM(context, 1.0f, -1.0f);
    	
    
    	CFMutableAttributedStringRef maString = CFAttributedStringCreateMutable(NULL, 0);
    	CFAttributedStringBeginEditing(maString);	
    	
    	CFStringRef str = CFSTR("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");
    	CFAttributedStringReplaceString(maString, CFRangeMake(0, 0), str);
    	
    	CTFontRef font = CTFontCreateWithName(CFSTR("Helvetica"), 11, NULL);
    	CFAttributedStringSetAttribute(maString, CFRangeMake(0, 12), kCTFontAttributeName, font);
    	CFRelease(font);	
    	
    	CFAttributedStringEndEditing(maString);	
    
    	CTFramesetterRef frs = CTFramesetterCreateWithAttributedString(maString);
    	CGMutablePathRef path = CGPathCreateMutable();
    	
    	CGPathAddRect(path, NULL, CGRectMake(5,5,300,200));
    	
    	CTFrameRef ctframe = CTFramesetterCreateFrame(frs, CFRangeMake(0, 0), path, NULL);	
    	CGPathRelease(path);	
    	CTFrameDraw(ctframe, context);
    	CFRelease(frs);
    	
    	CFRelease(maString);
    
    note the line where i set the path using CGRectMake(5,5,300,200). i would expect a paragraph of text at x:5, y:5, and the text to extend to width:300, height:200, but that's not the case.

    as an interesting experiment, change the height arg from 200 to 100 and watch the paragraph drop even closer to the bottom of the view. this does not make logical sense to me and therefore this is why i'm looking for alternatives to using CGContextTranslateCTM and CGContextScaleCTM as these are a really bad workaround.

    any ideas?
     
  5. kainjow Moderator emeritus

    kainjow

    Joined:
    Jun 15, 2000
    #5
    I'm still a little unclear. Are you trying to draw the text upside down? That is what that code does for me, at least when put into an NSView.

    I did have to add this for it to make any sense:

    Code:
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    Maybe you could post some screenshots of what you're seeing and what you're wanting to get.

    EDIT: Ok, now I see what you're referring to. NSView != UIView when it comes to this code for some reason... I'll try a few things out.
     
  6. kainjow Moderator emeritus

    kainjow

    Joined:
    Jun 15, 2000
    #6
    I think I got it working like you'd want. Maybe I missed something obvious, but it appears you have to calculate the height of the text to offset it from the top properly.

    I haven't tested it recently in the simulator but it was working before I cleaned up the code (don't have iPhone SDK installed atm). See what you think.

    Notes:
    - Helvetica doesn't work properly, I don't know why.
    - measureFrameHeight: was adopted from http://lists.apple.com/archives/quartz-dev/2008/Mar/msg00079.html

    // View.h
    Code:
    #if TARGET_OS_IPHONE
    #import <UIKit/UIKit.h>
    #import <CoreText/CoreText.h>
    #else
    #import <AppKit/AppKit.h>
    #endif
    
    
    #if TARGET_OS_IPHONE
    @interface View : UIView
    #else
    @interface View : NSView
    #endif
    {
        CTFramesetterRef frs;
    }
    
    @end
    // View.m
    Code:
    #import "View.h"
    
    
    @implementation View
    
    - (void)dealloc
    {
        if (frs)
            CFRelease(frs);
        [super dealloc];
    }
    
    - (CGFloat)measureFrameHeight:(CTFrameRef)frame
    {
        CFArrayRef lines = CTFrameGetLines(frame);
        CGFloat height = 0.0;
        CGFloat ascent, descent, leading;
        for (CFIndex index = 0; index < CFArrayGetCount(lines); index++) {
            CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, index);
            CTLineGetTypographicBounds(line, &ascent,  &descent, &leading);
            height += (ascent + fabsf(descent) + leading);
        }
        return ceil(height);
    }
    
    - (void)createFramesetter
    {
        if (frs)
            return;
    
        const CFStringRef str = CFSTR("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");
        const CFStringRef fontName = CFSTR("Arial");
    
        CFMutableAttributedStringRef maString = NULL;
        CTFontRef font1 = NULL;
        CTFontRef font2 = NULL;
    
        maString = CFAttributedStringCreateMutable(NULL, 0);
        if (!maString)
            goto cleanup;
        CFAttributedStringBeginEditing(maString);
        CFAttributedStringReplaceString(maString, CFRangeMake(0, 0), str);
        font1 = CTFontCreateWithName(fontName, 11.0, NULL);
        if (!font1)
            goto cleanup;
        CFAttributedStringSetAttribute(maString, CFRangeMake(0, CFAttributedStringGetLength(maString)), kCTFontAttributeName, font1);
        font2 = CTFontCreateWithName(fontName, 32.0, NULL);
        if (!font2)
            goto cleanup;
        CFAttributedStringSetAttribute(maString, CFRangeMake(0, 12), kCTFontAttributeName, font2);
        CFAttributedStringEndEditing(maString);
    
        frs = CTFramesetterCreateWithAttributedString(maString);
    
    cleanup:
        if (maString)
            CFRelease(maString);
        if (font1)
            CFRelease(font1);
        if (font2)
            CFRelease(font2);
    }
    
    - (void)drawInContext:(CGContextRef)ctx bounds:(CGRect)bounds
    {
        [self createFramesetter];
    
        const CGFloat border = 10.0;
        CGRect frameRect = bounds;
        CGMutablePathRef path = NULL;
        CTFrameRef ctframe = NULL;
    
        frameRect.size.height = FLT_MAX;
        frameRect.size.width -= (border * 2);
    
        path = CGPathCreateMutable();
        if (path) {
            CGPathAddRect(path, NULL, frameRect);
            ctframe = CTFramesetterCreateFrame(frs, CFRangeMake(0, 0), path, NULL);
            if (ctframe) {
                frameRect.size.height = [self measureFrameHeight:ctframe];
                CFRelease(ctframe);
            }
            CFRelease(path);
        }
    	
        frameRect.origin.y = CGRectGetMaxY(bounds) - frameRect.size.height - border;
        frameRect.origin.x += border;
    
        path = CGPathCreateMutable();
        if (path) {
            CGPathAddRect(path, NULL, frameRect);
            ctframe = CTFramesetterCreateFrame(frs, CFRangeMake(0, 0), path, NULL);
            if (ctframe) {
                CGContextAddPath(ctx, path);
                CGContextSetRGBFillColor(ctx, 1.0, 0.0, 0.0, 0.5);
                CGContextFillPath(ctx);
    
                CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);			
                CTFrameDraw(ctframe, ctx);
                CFRelease(ctframe);
            }
            CGPathRelease(path);
        }
    }
    
    #if TARGET_OS_IPHONE
    - (void)drawRect:(CGRect)rect
    #else
    - (void)drawRect:(NSRect)rect
    #endif
    {
    #if TARGET_OS_IPHONE
        CGRect bounds = self.bounds;
        CGContextRef ctx = UIGraphicsGetCurrentContext();
        CGContextTranslateCTM(ctx, 0.0, bounds.size.height);
        CGContextScaleCTM(ctx, 1.0, -1.0);
    #else
        CGRect bounds = NSRectToCGRect([self bounds]);
        CGContextRef ctx = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
    #endif
        [self drawInContext:ctx bounds:bounds];
    }
    
    @end
     
  7. printf thread starter macrumors regular

    Joined:
    Aug 27, 2008
    #7
    woah, just last night i practically came up with the same thing - though admittedly, yours is a bit more thorough. fantastic work. it's interesting how you're initially setting the frame rect height to FLT_MAX and then recomputing that rect with that first call to CTFramesetterCreateFrame. that's very clever compared to my arbitrary height i was using. i hope the documentation eventually provides more complete examples like this to help people as it's very different from ATSUI!

    also, the thing i was hung up on for so long was the helvetica font! it's not measuring correctly - but until i read your post i was still thinking my frame height calculation math was off. i wonder if this is a bug that needs to be reported..

    anyway, thanks again!
     
  8. arnieterm macrumors regular

    Joined:
    Aug 28, 2008
    #8
    Will this solution of drawing an attributed string at a particular location will work in case of multiple paragraphs? This is important for me as the page I want to render not only contains text [being drawn using CoreText] but also includes images [either centered, left or right aligned]
    . Let say the attributedString is a combination of three sub attributedString each of which has paragraph settings.After second attributedString I need to draw the image and then the remaining attributedString.
    Do I need to create an array of NSAttributedString and inside the drawRect draw each of these strings separately and when I reach at the image location then draw the image and then the remaining attributed string
    Please correct me wherever appropriate. The paragraph styles are different for each attributed string.

    Thanks
    Arnieterm
     
  9. arnieterm macrumors regular

    Joined:
    Aug 28, 2008
    #9
    Anyone out there?
    Does there exists any solution for my problem?
    THanks
    Arnieterm
     
  10. kainjow Moderator emeritus

    kainjow

    Joined:
    Jun 15, 2000
    #10
    Why not just use an NSTextView, make it transparent and turn off editing/selecting?
     
  11. arnieterm macrumors regular

    Joined:
    Aug 28, 2008
    #11
    My problem refers to iphone [using iphone os sdk 4]. My problem is to render a document that contains both text and images [not too complicated document]
    Thanks
    Arnieterm
     
  12. jared_kipe macrumors 68030

    jared_kipe

    Joined:
    Dec 8, 2003
    Location:
    Seattle
    #12
    I may not be understanding exactly what you're looking for, but even before CoreText was available CoreGraphics seems to have what you're looking fore.

    Namely,
    CGAffineTransform myTextTransform = CGAffineTransformMake(1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f);
    CGContextSetTextMatrix(myContext, myTextTransform);
    CGContextShowTextAtPoint(myContext, (float)x, (float)y, "string", (int)stringLength);
     
  13. kainjow Moderator emeritus

    kainjow

    Joined:
    Jun 15, 2000
    #13
    Have you considered just using UIWebView?
     
  14. jared_kipe macrumors 68030

    jared_kipe

    Joined:
    Dec 8, 2003
    Location:
    Seattle
    #14
    Ooo good one. Probably easy to layout a document in HTML/css than actually laying it out in CoreGraphics/CoreText. Hell, a webView might even be able to render the document all by itself depending on type.
     
  15. arnieterm macrumors regular

    Joined:
    Aug 28, 2008
    #15
    My first choice is obviously the UIWebView but my app requires no vertical scrolling and when we do a font increase then the document needs a vertical scrolling but instead i require the app to have more number of pages to adjust the completed document. CoreGraphics is a good choice but as CoreText is now available on iphone so I am giving it a try.

    Thanks
    Arnieterm
     

Share This Page