iOS 8 API printing PDFs: broken when drawing text? - objective-c

I have an app that has been happily generating PDFs using quartz/UIKit since iOS 4, but since upgrading the project to iOS 8, crashes whenever it tries to render text into the PDF context. Drawing lines & rectangles is fine, but any permutation of string rendering fails with an exception in one of the low level libraries.
Rather than posting my own source, I tried working backwards from Apple's documentation. Granted it is out of date, but if it's no longer supposed to work, they ought to have fixed it.
https://developer.apple.com/library/ios/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/GeneratingPDF/GeneratingPDF.html
Adapted source code:
- (void)producePDF
{
NSString *text=#"Bzorg blarf gloop foo!";
CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, (CFStringRef)text, NULL);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);
NSString *pdfFileName = fullPath;
// Create the PDF context using the default page size of 612 x 792.
UIGraphicsBeginPDFContextToFile(pdfFileName, CGRectZero, nil);
CFRange currentRange = CFRangeMake(0, 0);
NSInteger currentPage = 0;
BOOL done = NO;
do {
// Mark the beginning of a new page.
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
// Draw a page number at the bottom of each page.
currentPage++;
//[self drawPageNumber:currentPage];
// Render the current page and update the current range to
// point to the beginning of the next page.
//currentRange = [self renderPageWithTextRange:currentRange andFramesetter:framesetter];
currentRange=[self renderPage:currentPage withTextRange:currentRange andFramesetter:framesetter];
// If we're at the end of the text, exit the loop.
if (currentRange.location == CFAttributedStringGetLength((CFAttributedStringRef)currentText))
done = YES;
} while (!done);
// Close the PDF context and write the contents out.
UIGraphicsEndPDFContext();
// Release the framewetter.
CFRelease(framesetter);
// Release the attributed string.
CFRelease(currentText);
}
- (CFRange)renderPage:(NSInteger)pageNum withTextRange:(CFRange)currentRange
andFramesetter:(CTFramesetterRef)framesetter
{
// Get the graphics context.
CGContextRef currentContext = UIGraphicsGetCurrentContext();
// Put the text matrix into a known state. This ensures
// that no old scaling factors are left in place.
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
// Create a path object to enclose the text. Use 72 point
// margins all around the text.
CGRect frameRect = CGRectMake(72, 72, 468, 648);
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);
// Get the frame that will do the rendering.
// The currentRange variable specifies only the starting point. The framesetter
// lays out as much text as will fit into the frame.
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
CGPathRelease(framePath);
// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
CGContextTranslateCTM(currentContext, 0, 792);
CGContextScaleCTM(currentContext, 1.0, -1.0);
// Draw the frame.
CTFrameDraw(frameRef, currentContext);
// Update the current range based on what was drawn.
currentRange = CTFrameGetVisibleStringRange(frameRef);
currentRange.location += currentRange.length;
currentRange.length = 0;
CFRelease(frameRef);
return currentRange;
}
I've tried numerous permutations, and they all seem to fail at the exact point of rendering text. The Apple-derived example above dies at the line:
CTFrameDraw(frameRef, currentContext);
Other code attempts to get the minimum working:
NSMutableParagraphStyle* textStyle = NSMutableParagraphStyle.defaultParagraphStyle.mutableCopy;
textStyle.alignment = NSTextAlignmentLeft;
NSDictionary* textFontAttributes = #{
NSFontAttributeName: [UIFont fontWithName: #"Helvetica" size: 12], NSForegroundColorAttributeName: UIColor.redColor,
NSParagraphStyleAttributeName: textStyle};
[#"Hello, World!" drawAtPoint:CGPointZero withAttributes:textFontAttributes];
... crashes at the "drawAtPoint" call.

For what it's worth, if I execute the app on a device without the debugger attached (i.e. run/kill/launch from springboard), the PDF creation works just fine. Presumably whatever bogus exception was getting thrown just gets ignored in real life.

Related

Get graphics context after saving in PDF - Objective C

How to get the core graphics context from PDF after saving? For example I am creating a rectangle inside PDF pages, I need to edit, update or delete that rectangle inside PDF pages after saving it.
Here is the code I used to save rectangle inside PDF pages in objective c.
CGPDFDocumentRef pdf = CGPDFDocumentCreateWithURL((CFURLRef)[[NSBundle mainBundle] URLForResource:#"tutorial" withExtension:#"pdf"]);
const size_t numberOfPages = CGPDFDocumentGetNumberOfPages(pdf);
NSMutableData* data = [NSMutableData data];
UIGraphicsBeginPDFContextToData(data, CGRectZero, nil);
for(size_t page = 1; page <= numberOfPages; page++)
{
// Get the current page and page frame
CGPDFPageRef pdfPage = CGPDFDocumentGetPage(pdf, page);
const CGRect pageFrame = CGPDFPageGetBoxRect(pdfPage, kCGPDFMediaBox);
UIGraphicsBeginPDFPageWithInfo(pageFrame, nil);
// Draw the page (flipped)
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
CGContextScaleCTM(ctx, 1, -1);
CGContextTranslateCTM(ctx, 0, -pageFrame.size.height);
CGContextDrawPDFPage(ctx, pdfPage);
CGContextRestoreGState(ctx);
// Draw a red box
[[UIColor redColor] set];
UIRectFill(CGRectMake(20, 20, 100, 100));
}
UIGraphicsEndPDFContext();
CGPDFDocumentRelease(pdf);
pdf = nil;
Now I need to edit or delete the rectangle created above. Please advice !!!
It is not possible to remove content from the PDF file using the CoreGraphics API.
You need to redesign your code to draw or not the rectangle in the PDF file based on some condition.

CTFrame dropping glyphs at the edge

I am trying to render Arabic text in my iOS app with custom TTF font (scheherazade) using core-text, which works for the most part - however certain glyphs at the edge of the CTFrame are dropped.
When I adjust the frame-size to make the dropped-glyphs appear in the interior of the frame, they display corretly, which leads me believe something is going wrong in inside CTFrameDraw. Below is the code I'm using to render the Arabic-text:
CGContextRef context = UIGraphicsGetCurrentContext();
// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, v.textFrame.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGMutablePathRef path = CGPathCreateMutable(); //1
CGPathAddRect(path, NULL, v.textFrame );
CGFloat minLineHeight = 60.0;
CGFloat maxLineHeight = 60.0;
CTTextAlignment paragraphAlignment = kCTRightTextAlignment;
CTLineBreakMode lineBrkMode = kCTLineBreakByWordWrapping;
CTParagraphStyleSetting setting[4] = {
{kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment), &paragraphAlignment},
{kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &minLineHeight},
{kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &maxLineHeight},
{kCTParagraphStyleSpecifierLineBreakMode, sizeof(CTLineBreakMode), &lineBrkMode}
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(setting, 4);
NSDictionary *attr = [NSDictionary dictionaryWithObjectsAndKeys:
(id)v.arabicFont, (id)kCTFontAttributeName,
paragraphStyle, (id)kCTParagraphStyleAttributeName,
nil];
CFRelease(paragraphStyle);
NSAttributedString* attString = [[[NSAttributedString alloc]
initWithString:v.verseText attributes:attr] autorelease]; //2
CTFramesetterRef framesetter =
CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attString); //3
CTFrameRef frame =
CTFramesetterCreateFrame(framesetter,
CFRangeMake(0, [attString length]), path, NULL);
CTFrameDraw(frame, context); //4
CFRelease(frame); //5
CFRelease(path);
CFRelease(framesetter);
Also attached are the screenshots showing the problem I face. Any help would be much appreciated. Thanks.
invalid: http://stellarbeacon.com.au/invalid.png
valid : http://stellarbeacon.com.au/valid.png
There are some bugs in CoreText related to determining the correct frame size. Some of these where fixed in iOS 6, e.g. http://www.cocoanetics.com/2012/02/radar-coretext-line-spacing-bug/
If your problem still exists there then you should file a Radar with the specifics. Also you should open a call with Apple DTS which can probably provide you with a workaround. Often - if your problem is indeed a bug - then you get your DTS call credited back.
PS: try to display your text with my DTCoreText views which do manual layouting and display and see if the problem can be reproduced there. If not then you have your workaround.

CTFrame clipping first line of text

I am running into a problem while using Core Text, where the first line of the text I display in a CTFrame is cut off at the top, as seen in the screenshot below, with the character "B":
I think I'm doing something wrong while setting the leading in the CTFrame. My code is below:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
NSAttributedString *myString;
//Create the rectangle into which we'll draw the text
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, self.bounds);
//Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
//Setup leading (line height)
CGFloat lineHeight = 25;
CTParagraphStyleSetting settings[] = {
{ kCTParagraphStyleSpecifierMinimumLineHeight, sizeof(CGFloat), &lineHeight },
{ kCTParagraphStyleSpecifierMaximumLineHeight, sizeof(CGFloat), &lineHeight },
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings) / sizeof(settings[0]));
NSDictionary * attrs = [[NSDictionary alloc] initWithObjectsAndKeys:
(__bridge id)CTFontCreateWithName((__bridge CFStringRef) font.fontName, font.pointSize, NULL) ,
(NSString*)kCTFontAttributeName,
(id)textColor.CGColor,
(NSString*)kCTForegroundColorAttributeName,
(__bridge id) paragraphStyle,
kCTParagraphStyleAttributeName,
nil];
myString = [[NSAttributedString alloc] initWithString:text attributes:attrs];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)myString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0,[myString length]), path, NULL);
CTFrameDraw(frame, context);
CFRelease(frame);
CFRelease(framesetter);
CFRelease(path);
}
Other SO posts (this one and this one) are not really helpful.
How can I prevent the CTFrame from clipping the first line?
--EDIT--
Reducing lineheight to 20:
lineheight from the second line onwards is respected, but the baseline of the first line of text is less than 20 below the top.
Here CGFloat lineHeight = 25; I am sure (from your screen shot) its less than font size you are currently using (font.pointSize).
This is the only reason of clipping top of text, as text size not able to fit in 25 points of height.
You have two options:
1) Remove paragraphStyle and its related code. Replace your NSDictionary * attrs = … by following code.
NSDictionary * attrs = [[NSDictionary alloc] initWithObjectsAndKeys:
(__bridge id)CTFontCreateWithName((__bridge CFStringRef) font.fontName, font.pointSize, NULL) ,
(NSString*)kCTFontAttributeName,
(id)textColor.CGColor, (NSString*)kCTForegroundColorAttributeName,
nil];
CTFrameDraw() will take care for line height automatically.
2) Or always provide lineHeight greater than your max font size. Like lineHeight = font.pointSize + somemargine;
To understand above explanation try this:
In your current code set your lineHeight = 20;; you can see more text will be clipped from top line and second line text will be more closed to bottom of its top line.
I managed to fix the problem. In the end it turned out to be the location at which I flipped the coordinate system, and by flipping it earlier on in the code the problem fixed itself.

ctframe not updating ctlines

Im using core text to display some json text from the web.
The contents are pulled from a nsdictionary that is popuplated to a UITABLEVIEW.
My problem is that the ctframe doesnt want to update the latest content, it keeps redrawing the old text. Ive debugged it and im pretty sure the ctframesettercreateframe is using the latest NSAttributeString
here is my code,
I call this function at every point to display a new text. The markup parser just returns an NSAtributeString
- (void)assign_text:(NSString*)text{
self.text = text;
CGMutablePathRef path = CGPathCreateMutable(); //1
CGPathAddRect(path, NULL, self.bounds );
MarkupParser *markup = [[MarkupParser alloc]init];
NSAttributedString* attString = [markup attrStringFromMarkup: self.text];
CFAttributedStringRef a = (__bridge_retained CFAttributedStringRef)attString;
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(a); //3
ctFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, [attString length]), path, NULL);
CFRelease(a);
CFRelease(path);
CFRelease(framesetter);
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CTFrameDraw((CTFrameRef)ctFrame, context);
}
After you modify your ctFrame instance variable, you need to send yourself the setNeedsDisplay message:
ctFrame = CTFramesetterCreateFrame(...);
[self setNeedsDisplay];
Read about the view drawing cycle in the View Programming Guide for iOS.

Porting creation of a PDF document from iOS to Mac OS X

I am porting my code from iPhone to Mac and I have no idea how I can do this in Mac. Here's my code that I am trying to convert and I know that there's no UIGraphic in Mac. Can someone point me to a guide or give me a quick hint? Thanks.
NSString *newFilePath = #"path/to/your/newfile.pdf";
NSString *templatePath = #"path/to/your/template.pdf";
//create empty pdf file;
UIGraphicsBeginPDFContextToFile(newFilePath, CGRectMake(0, 0, 792, 612), nil);
CFURLRef url = CFURLCreateWithFileSystemPath (NULL, (CFStringRef)templatePath, kCFURLPOSIXPathStyle, 0);
//open template file
CGPDFDocumentRef templateDocument = CGPDFDocumentCreateWithURL(url);
CFRelease(url);
//get amount of pages in template
size_t count = CGPDFDocumentGetNumberOfPages(templateDocument);
//for each page in template
for (size_t pageNumber = 1; pageNumber <= count; pageNumber++) {
//get bounds of template page
CGPDFPageRef templatePage = CGPDFDocumentGetPage(templateDocument, pageNumber);
CGRect templatePageBounds = CGPDFPageGetBoxRect(templatePage, kCGPDFCropBox);
//create empty page with corresponding bounds in new document
UIGraphicsBeginPDFPageWithInfo(templatePageBounds, nil);
CGContextRef context = UIGraphicsGetCurrentContext();
//flip context due to different origins
CGContextTranslateCTM(context, 0.0, templatePageBounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
//copy content of template page on the corresponding page in new file
CGContextDrawPDFPage(context, templatePage);
//flip context back
CGContextTranslateCTM(context, 0.0, templatePageBounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
/* Here you can do any drawings */
[#"Test" drawAtPoint:CGPointMake(200, 300) withFont:[UIFont systemFontOfSize:20]];
}
CGPDFDocumentRelease(templateDocument);
UIGraphicsEndPDFContext();
Use CGPDFContextCreateWithURL instead of UIGraphicsBeginPDFContextToFile (the parameters are very similar). To begin/end pages, use CGPDFContextBeginPage and CGPDFContextEndPage. When you're done, call CGPDFContextClose instead of UIGraphicsEndPDFContext.
The rest can remain the same – Core Graphics exists on both iOS and Mac OS X – which also means that you could use the functions I've mentioned above on iOS as well if you want to use the same code on both platforms.
Swift 4, macOS High Sierra Update
func generatePdfWithFilePath(thefilePath: String)
{
let url = URL(fileURLWithPath: thefilePath) as CFURL
guard let currentContext = CGContext(url, mediaBox: nil, documentInfo as CFDictionary) else {
return
}
self.context = currentContext
self.context!.beginPDFPage(pageInfo as CFDictionary)
drawReport()
self.context!.endPDFPage()
// Close the PDF context and write the contents out.
self.context!.closePDF()
self.context = nil
//DebugLog("generatePdfWithFilePath() completed")
}