3

Possible Duplicate:
Cannot create PDF document with 400+ pages on iOS

My app generates PDF files. These PDf's can be large due to the user being able to add pages, potentially unlimited, although usual is approx. 10. Im having problems with iPhone 4 users experiencing a crash at the PDF genration stage. Some investigative work shows the app is running out of memory during the PDF generation stage, I get low memory warnings then final crash. I can reproduce the problem If I add 50 plus pages on iPhone 5, a lot less on iPhone 4, and not on the simulator which is expected.

Can anyone suggest how I can reduce this aculamitive memory climb and ultimate crash when generating the PDf file.

Ive researched SO: iPhone App Crashes due to Low Memory but works fine in simulator and Quartz PDF API Causing Out of Memory Crashes

@interface ICPDFPreviewController ()
@property (nonatomic, strong) Certificate *certificate;
@property (nonatomic, strong) NSData *pdfData;
@property (nonatomic) BOOL viewHasUnloaded;
- (void)generatePdf;
- (void)pdfDone:(NSData *)data;
- (NSData *)createPdfWithPages:(NSArray *)pages;
@end

@implementation ICPDFPreviewController
@synthesize certificate=_certificate;
@synthesize scrollView=_scrollView;
@synthesize webView=_webView;
@synthesize pdfData=_pdfData;
@synthesize viewHasUnloaded=_viewHasUnloaded;



- (void)generatePdf
 {
 NSMutableArray *pagesArray = [NSMutableArray array];

 if ([self.certificate.certificateType.title isEqualToString:@"Minor Works"]) {
[pagesArray addObject:[[ICPDFMinorWorksPage1 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFMinorWorksPage2 alloc] initWithCertificate:self.certificate]];

} else if ([self.certificate.certificateType.title isEqualToString:@"EIC"]) {
[pagesArray addObject:[[ICPDFEICPage1 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICPage2 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICPage3 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICPage4 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICPage5 alloc] initWithCertificate:self.certificate]];
[self addDistributionBoardsToPagesArray:pagesArray];
ICPDFEICPageFinal *pageFinal = [[ICPDFEICPageFinal alloc] initWithCertificate:self.certificate];
pageFinal.pageNumber.text = [NSString stringWithFormat:@"%d", pagesArray.count+1];
[pagesArray addObject:pageFinal];

} else if ([self.certificate.certificateType.title isEqualToString:@"Domestic EIC"]) {
[pagesArray addObject:[[ICPDFDomesticEICPage1 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFDomesticEICPage2 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFDomesticEICPage3 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFDomesticEICPage4 alloc] initWithCertificate:self.certificate]];
[self addDistributionBoardsToPagesArray:pagesArray];
[pagesArray addObject:[[ICPDFDomesticEICPageFinal alloc] initWithCertificate:self.certificate]];

} else if ([self.certificate.certificateType.title isEqualToString:@"EICR"]) {
[pagesArray addObject:[[ICPDFEICRPage1 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICRPage2 alloc] initWithCertificate:self.certificate]];
[self addObservationsToPagesArray:pagesArray];
[self addDistributionBoardsToPagesArray:pagesArray];
[pagesArray addObject:[[ICPDFEICRInspection alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICRInspectionPage1 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICRInspectionPage2 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICRInspectionPage3 alloc] initWithCertificate:self.certificate]];
[pagesArray addObject:[[ICPDFEICRPageFinal alloc] initWithCertificate:self.certificate]];
 }

// Set page count on all pages
int pageNumber = 0;
for (ICCertificateComponent *page in pagesArray) {
page.pageNumber.text = [NSString stringWithFormat:@"%d", ++pageNumber];
page.pageCount.text = [NSString stringWithFormat:@"%d", pagesArray.count];
}

NSData *pdfData = [self createPdfWithPages:pagesArray];
[self performSelectorOnMainThread:@selector(pdfDone:) withObject:pdfData waitUntilDone:YES];

 }

- (void)pdfDone:(NSData *)data
{
self.pdfData = data;
[self.webView loadData:self.pdfData MIMEType:@"application/pdf" textEncodingName:@"utf-8"   baseURL:nil];
[ICUtils removeProgressView];
}

- (NSData *)createPdfWithPages:(NSArray *)pages
 {
// Creates a mutable data object for updating with binary data, like a byte array
NSMutableData *pdfData = [NSMutableData data];

 ICCertificateComponent *firstPage = [pages objectAtIndex:0];

UIGraphicsBeginPDFContextToData(pdfData, firstPage.contentView.bounds, nil);

 for (int i = 0; i < pages.count; i++) {
ICCertificateComponent *thisPage = [pages objectAtIndex:i];
UIGraphicsBeginPDFPageWithInfo(thisPage.contentView.bounds, nil);
    ////////////////////////////////////////////////////////////////////
   //tried adding this after research on SO, did not stop app crash
  // CGContextSetInterpolationQuality((__bridge CGContextRef)(thisPage), kCGInterpolationHigh);    CGContextSetRenderingIntent((__bridge CGContextRef)(thisPage), kCGRenderingIntentDefault);
 /////////////////////////////////////////////////////////////////////

CGContextRef pdfContext = UIGraphicsGetCurrentContext();
[thisPage.contentView.layer renderInContext:pdfContext];
}

UIGraphicsEndPDFContext();

return pdfData;
}

- (void)addDistributionBoardsToPagesArray:(NSMutableArray *)pagesArray
{
int pageCount = pagesArray.count;
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"createdAt"    ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil]; 
NSArray *boards = [self.certificate.distributionBoards   sortedArrayUsingDescriptors:sortDescriptors];
for (DistributionBoard *thisBoard in boards) {
DebugLog(@"Creating a board page");
ICPDFDistributionBoard *boardPage = [[ICPDFDistributionBoard alloc]   initWithDistributionBoard:thisBoard];
boardPage.pageNumber.text = [NSString stringWithFormat:@"%d", ++pageCount];
DebugLog(@"Page number is %d", pageCount);
[pagesArray addObject:boardPage];

NSSortDescriptor *circuitDescriptor = [[NSSortDescriptor alloc] initWithKey:@"createdAt" ascending:YES];
NSArray *circuitDescriptors = [[NSArray alloc] initWithObjects:circuitDescriptor, nil]; 
NSArray *circuits = [thisBoard.circuits sortedArrayUsingDescriptors:circuitDescriptors];

//int circuitCount = circuits.count;
ICPDFCircuitDetails *circuitDetails = boardPage.circuitDetails;

int circuitCount = 0;
for (Circuit *thisCircuit in circuits) {
    circuitCount++;
    if (circuitCount > 16) {
        // Add an extension page
        DebugLog(@"Adding an extension sheet");
        circuitCount = 1;
        ICPDFDistributionBoardExtension *boardExtension = [[ICPDFDistributionBoardExtension  alloc] initWithDistributionBoard:thisBoard];
        [pagesArray addObject:boardExtension];
        boardExtension.pageNumber.text = [NSString stringWithFormat:@"%d", ++pageCount];
        circuitDetails = boardExtension.circuitDetails;
     }
    NSString *key = [NSString stringWithFormat:@"circuitRow%d", circuitCount];
    ICCircuitRow *circuitRow = [circuitDetails valueForKey:key];
    [circuitRow populateFromCircuit:thisCircuit];
    }
  }
  }
Community
  • 1
  • 1
JSA986
  • 5,870
  • 9
  • 45
  • 91

2 Answers2

3

In general, memory is finite and your generated output isn't, so the way to make it work is to ensure that:

  • you're not accumulating the entire PDF in memory as you generate it
  • you're not unnecessarily keeping around byproducts of the rendering for each page

In your case, using UIGraphicsBeginPDFContextToData means that you're rendering the whole PDF to an ever-expanding NSData. When that data gets too big, you'll get killed. Instead, try UIGraphicsBeginPDFContextToFile. Also in your inner loop for rendering pages, consider inserting an @autoreleasepool { ... } block to prevent objects from building up unnecessarily during a long run. I'm not sure how big your pagesArray bunch of stuff actually is, and whether that's something you might consider "paging in" one page at a time as you generate.

Ben Zotto
  • 70,108
  • 23
  • 141
  • 204
  • Thank you for your clear and detailed explainantion and advice as to whats going on here, it makes a bit more sense now! when changing from `UIGraphicsBeginPDFContextToData` to `UIGraphicsBeginPDFContextToFile` will I need to change ` NSMutableData *pdfData = [NSMutableData data]` to an NSMutableString? – JSA986 Feb 04 '13 at 17:21
  • @JSA986: Presumably, yes. The flow depends on what you want to do with the data. Looks like you want to load it into a web view. In this case, create a temporary file, generate to that file, then load the web view with the path to it. You might design your `-createPdfWithPages:` method to also accept the path to the desired output file, so you maintain symmetric ownership of the destination and can delete it when done. – Ben Zotto Feb 04 '13 at 17:52
  • I dont think this is going to work in my case, I do want to load it into a webview correct but if I chage it from `data` to `file` I get all sorts of yellow warning triangles and no pdf render. Ive added @autoreleasepool { ... } as advised but obviouldy based on your comments I need to do more, is there anything else I can do while keeping `UIGraphicsBeginPDFContextToData` I have not written this part of the app and im not having any luck with `writing to file` or memery management on this – JSA986 Feb 04 '13 at 23:56
  • @JSA986: You can't simply drop in the new function. You'll need to create a reasonable path to a temporary file, give that to the function, and then give the file to the webview to load (and delete the file when you're done). If you're not sure how to do those things, please ask new questions here, as they're beyond the scope of this particular question/answer/comment thread. – Ben Zotto Feb 05 '13 at 02:00
  • Ah ok, I will ask new question based on that thanks for your for your advice – JSA986 Feb 05 '13 at 02:04
2

EDIT: I think Ben Zotto's solution is the way to go: use UIGraphicsBeginPDFContextToFile.


Someone asked this months ago. I wrote a memory mapped data consumer that might help:

https://gist.github.com/3748250

It uses a memory mapped PDF context instead of normal memory.

nielsbot
  • 15,922
  • 4
  • 48
  • 73
  • Looks good! thank you for that link and for sharing your code – JSA986 Feb 04 '13 at 18:56
  • It occurs to me you might even try making a data consumer that just writes to a file and check your memory usage then. Using a memory mapped file might be overkill... – nielsbot Feb 04 '13 at 21:22
  • Ok, I have no idea what a data consumer is though. – JSA986 Feb 04 '13 at 23:58
  • a data consumer is a CGDataConsumerRef. It's an abstract class in Core Graphics. I think you can just switch to `UIGraphicsBeginPDFContextToFile` and you'll be fine. If not, you can revisit this answer. – nielsbot Feb 05 '13 at 00:50
  • I tried switching to `UIGraphicsBeginPDFContextToFile` but my pdf gets error in context error in debugger, and I get about 20 warnings saying incompatible pointer types returning string _strong from a function nsdata type. – JSA986 Feb 05 '13 at 00:55