Search code examples
iosobjective-cpdf-generationquartz-2d

How do I rotate text in Quartz running on iOS so the origin does not move?


I am trying to create a PDF that will run on iOS devices (iPad only). I have created my PDF and I am able to write text and graphics into it. But I am stumped on writing rotated text such that the text will rotate but the origin will remain the same. Here is an image of the output I am trying to achieve:

example of desired output - rotated text

I create my PDF like this:

  -(void)drawPDF:(NSString*)fileName
{
    self.pageSize = CGSizeMake(792, 612);

    // Create the PDF context using the default page size of 612 x 792.
    UIGraphicsBeginPDFContextToFile(fileName, CGRectZero, nil);

    // Mark the beginning of a new page.
    UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, pageSize.width, pageSize.height), nil);

    // Get the graphics context.
    context = UIGraphicsGetCurrentContext();

    CGContextSetTextMatrix(context, CGAffineTransformIdentity);    
    CGContextTranslateCTM(context, 0, 654);
    CGContextScaleCTM(context, 1.0, -1.0);

    UIFont *font = [UIFont fontWithName:@"Helvetica-Light" size:8];

    [self drawText:@"here is a really long line of text so we can see" withFrame:CGRectMake(400, 100, 50, 50) withFont:font rotation:-50];

    // Close the PDF context and write the contents out.
    UIGraphicsEndPDFContext();
}

Then I have this function for drawing the text, that I've tried many variations of:

  -(void)drawText:(NSString *)text withFrame:(CGRect)rect withFont:(UIFont *)font rotation:(float)degrees
{
    float radians = [PDFMaker DegreesToRadians:degrees];

    NSDictionary *attribs = @{NSFontAttributeName:[UIFont fontWithName:@"Helvetica-Light" size:10.0]};

    CGContextSaveGState(context);

    CGPoint pos = CGPointMake(rect.origin.x, -50 - rect.origin.y);

    // Translate so we're drawing in the right coordinate space
    CGContextScaleCTM(context, 1.0, -1.0);

    CGSize stringSize = [text sizeWithAttributes:attribs];
    CGContextTranslateCTM(context, 0-stringSize.width/2, 0-stringSize.height/2);

    CGContextRotateCTM(context, radians);

    CGContextTranslateCTM(context, stringSize.width/2, stringSize.height/2);

    [text drawAtPoint:CGPointMake(pos.x, pos.y) withAttributes:attribs];

    CGContextRestoreGState(context);
}

But no matter how much I play with the translates, the text jumps around as if being rotated around an axis at the origin. I've read in other answers to rotate the text on its center at the origin and have tried that but that doesn't seem to work (or, probably more accurately, I can't get it to work). I suspect the problem may be something related to using a PDF context but really don't know. Any help would be greatly appreciated.


Solution

  • After a lot of research and experimentation I figured it out. I'm not sure if it's OK to answer my own question so, if not, will an op please let me know and I'll delete it. I do think the answer and the reason behind the answer might be valuable for other community members.

    Here is the working code:

    -(void)drawText:(NSString *)text withFrame:(CGRect)rect withFont:(UIFont *)font
       rotation:(float)degrees alignment:(int)alignment center:(BOOL)center
    

    { // Pivot rect rect = CGRectMake(rect.origin.x, rect.origin.y*-1, rect.size.width, rect.size.height);

    float radians = [PDFMaker DegreesToRadians:degrees];
    
    NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
    paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
    paragraphStyle.alignment = alignment;
    
    NSDictionary *attributes = @{ NSFontAttributeName: font,
                                  NSParagraphStyleAttributeName: paragraphStyle,
                                  NSForegroundColorAttributeName: [Util ColorFromHexString:@"FFFFFF"] };
    
    CGContextSaveGState(context);
    
    // Translate so we're drawing in the right coordinate space
    CGContextScaleCTM(context, 1.0, -1.0);
    
    // Save this is we want/need to offset by size
    CGRect textSize = [text boundingRectWithSize:rect.size
                                              options:NSStringDrawingUsesLineFragmentOrigin
                                           attributes:attributes
                                              context:nil];
    
    // Translate to origin, rotate, translate back
    CGContextTranslateCTM(context, rect.origin.x, rect.origin.y);
    CGContextRotateCTM(context, radians);
    CGContextTranslateCTM(context, -rect.origin.x, -rect.origin.y);
    
    // Center if necessary
    if(center==YES)
    {
        CGContextTranslateCTM(context, 0, (rect.size.height/2)-(textSize.size.height/2));
    }
    
    [text drawInRect:rect withAttributes:attributes];
    
    CGContextRestoreGState
    

    }

    What's as interesting or more interesting is the why behind it. All this has been written in other questions and answers, though usually in pieces and many of the code the code samples in other Q&A's have since been deprecated.

    So the why: iOS starts with a matrix with the origin at the top-left, like Windows. But the text rendering functions of iOS are carried over from OSX (which is carried over from NEXT), and the origin is at the bottom-left. PDF is normally written from the bottom left but it's standard practice to write a transform so it runs from the top-left. But since these are text rendering functions that use Core Text we flip again. So you need to take into account and get the transforms correct or your text will print upside down and backwards or off-screen.

    After that we need to rotate at the origin which, at least the way I've set things up, is now at the bottom left. If we don't rotate at the origin our text will seem to fly around: the further away from the origin the more it will fly. So we translate to the origin, rotate, then translate back. This is not unique to PDF and applies to any rotation in Quartz.

    Finally one editorial comment on making PDF on an iOS device that should not be overlooked: it can be slow and consume a lot of memory. I think it's fine to do for a small number of pages when you want high-quality vector output for printing or embedding into things like Office docs; charts, graphs, maybe game instructions, printable tickets .. small things. But if you're printing something like a large report or a book it's probably best to produce PDF on a server.