NSPrintOperation PDFOperationWithView

Folks;


macOS 10.15 - Xcode 11.3.1

I've been somewhat successful getting this method to create a PDFDocument from the contents of an NSTextView.


However, the contents of this NSTextView is an NSAttributedString (set using TextStorage).

The view behaves as expected and there is .pdf document created.


The contents of the .pdf file appears to be the .string value of the NSTextView.


How do I use this NSPrintOperation to produce a PDFDocument which retains the attributed string?


Thanks for any thoughts!

Steve


BTW: If anyone has a means of producing a PDFDocument from NSAttributedString for macOS that does not involve NSPrintOperation I would appreciate a quick example or a link... I used to use 'cupsfilter' but it has been marked deprecated AND it also yields this same result... It logs

'cupsfilter: No filter to convert from text/rtf to application/pdf.' but creates the file...(but loses the attributed string)

Answered by SwampDog in 409486022

If anyone is still following along...

Thanks go to janabanan for staying with this thread!!


Here's simplified code which produces a .pdf file which retains the links embedded in the NSAttributedString which is displayed in 'sourceTextView'. This .pdf file is also appropriately paginated.


[savePanel beginSheetModalForWindow:self.windowController.window completionHandler:^(NSInteger result){

if (result == NSFileHandlingPanelOKButton) {

NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo];

[printInfo setHorizontalPagination:NSFitPagination];

[printInfo setVerticallyCentered:NO];

[printInfo setHorizontallyCentered:NO];

[printInfo setLeftMargin:67.0];

[printInfo setRightMargin:67.0];

[printInfo setTopMargin:72.0];

[printInfo setBottomMargin:72.0];

[printInfo dictionary] [NSPrintJobDisposition] = NSPrintSaveJob;

[printInfo dictionary] [NSPrintJobSavingURL] = [savePanel URL];

NSPrintOperation *op = [NSPrintOperation printOperationWithView:self.windowController.sourceSourceTextView printInfo:printInfo];

[op setShowsPrintPanel:NO];

[op setShowsProgressPanel:NO];

[op runOperation];

}

[savePanel orderOut:self];

}];

To create a PDF without using NSPrintOperation, you have to use Quartz. If you have a lot of text in the PDF, using Core Text will also help you calculate the amount of text that fits on a page.


Apple's Quartz2D Programming Guide has a section on creating PDF files with Quartz. The article at the following link goes into more detail on what you have to do to create a PDF with Core Text and Quartz:


meandmark.com/blog/2016/08/creating-pdfs-with-core-text-and-quartz/

szymczyk


Thanks for taking the time to reply.

I followed up on your pointers to Apple Quartz 2D and the link you provided.

The code below is based on both of those source and this code functions as expected.

BUT it yields exactly the same output - the resulting .pdf file does not show an attributed string - it simply shows the .string value.

Surely I'm missing something!

Can someone provide any guidance?


My suspicions center on 'pageDictionary' and 'CTFrameDraw(). [but what do I know]


- (PDFDocument *) pdfForAttributedString:(NSAttributedString *)thisAtrributedString  {
  NSString *tDir = self.tempDirectoryURL.path;
  NSString *globUniq = [[NSProcessInfo processInfo] globallyUniqueString];
  NSString *newFileName =  [tDir stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.pdf", globUniq]];
  const char *filename = [newFileName UTF8String];
  CFStringRef path = CFStringCreateWithCString (NULL, filename, kCFStringEncodingUTF8);
  CFURLRef url = CFURLCreateWithFileSystemPath (NULL, path, kCFURLPOSIXPathStyle, 0);
  CFRelease (path);

  CFMutableDictionaryRef myDictionary = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
  CFDictionarySetValue(myDictionary, kCGPDFContextCreator, CFSTR(“XYZ - a macOS app"));
  CGContextRef pdfContext = CGPDFContextCreateWithURL (url, nil, myDictionary); // Quartz assumes 8.5 x 11
  CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(thiscfAttr);
  CFRange textRange = CFRangeMake(0, CFAttributedStringGetLength(thiscfAttr));
  CFRange pageRange;
  CGSize pageSize = CGSizeMake(612.0, 792.0); 

  CFAttributedStringRef thiscfAttr = (__bridge CFAttributedStringRef)(thisAtrributedString);
  CTFramesetterSuggestFrameSizeWithConstraints(framesetter, textRange, nil, pageSize, &pageRange);
  CGRect pageRect = CGRectMake(72.0, 72.0, 468.0, 648.0);// 1” margins
  CGPathRef framePath = CGPathCreateWithRect(pageRect, nil);
  CTFrameRef frame = CTFramesetterCreateFrame(framesetter, pageRange, framePath, nil);
  CFMutableDictionaryRef pageDictionary = NULL;
  CGPDFContextBeginPage (pdfContext, pageDictionary); 
  CTFrameDraw(frame, pdfContext);
  CGPDFContextEndPage (pdfContext);
  CGPDFContextClose(pdfContext);

  NSURL *printURL = (__bridge NSURL *)(url);
  PDFDocument *doc = [[PDFDocument alloc] initWithURL:printURL];
  return doc;
}

I am wondering if line 18 should come before lines 13 and 14?


Do you need to use quartz? If it was me, I would using printing and print it to a pdf. I can post code to do that if you want.


I posted some code but it is being moderated (for three days now).

I have not created a PDF using the C API. I don't know why you're getting plain text instead of rich text in the PDF. I don't see anything in your code that would create a PDF with a plain text string instead of an attributed string.


I noticed two things with your code. First, why do you need the variable thiscfAttr? Can't you pass the argument thisAttributedString to CTFramesetterCreateWithAttributedString?


Second, your code will create a single page PDF no matter how long the text is. You create one page, draw the frame, and stop. You must create pages as long as there is text remaining in the attributed string.

How do I use this NSPrintOperation to produce a PDFDocument which retains the attributed string?

What is precisely your problem ?


If you use draw, you keep the attributes

let sAttributed =  NSAttributedString(string: "MyString", attributes: myAttributes)
sAttributed.draw(in: rect))

I am interested in looking at the solution you propose! Please post!

I don't understand how your 2 lines of code produce a .pdf document.


You ask 'what precisely is your problem'?


I have posted 2 different sets of code which DO produce .pdf files. My problem is is that these file do not produce the source attributedString -- they appear to produce the .string value of the NSAttributedString.


My precise problem is that I am trying to produce a .pdf file which is an attributed string.

any code you might show about how to 'create pages as long as there is text remaining' would be appreciated!

For creating a PDF from a text view's contents, you're better off going with NSPrintOperation.


To answer your question on creating pages as long there is text remaining, you have to keep track of how much text remains to be drawn. Create a variable of type CFRange to keep track of how much text remains. You will use the pageRange that line 19 of your code returns to update the remaining text range using code similar to the following:


remainingTextRange.location = pageRange.location + pageRange.length


Create a while loop. While the location (a CFRange struct has a location and a length) of the remaining text range is less than the length of the attributed string, you will run lines 19-27 of your code: calculate the page range, create the frame, and draw the frame in the PDF page. After doing all that, update the remaining text range using the pageRange from line 19.

// This is in an NSDocument class
//------------------------------------------------------------------------------------------------ 
-(IBAction)saveMyAttStringToPDF:(id)sender { 
// if you need to do some stuff, like put an attributed string in place, do it here. 
    [super saveDocumentToPDF:sender]; 
// you could also connect your menuItem directly to the saveDocumentToPDF: action and eliminate this piece of code.  
} 
//------------------------------------------------------------------------------------------------ 
-(NSPrintInfo *)thePrintInfo { 
    NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo];  // get the shared 
    [printInfo setHorizontalPagination:NSFitPagination]; 
    [printInfo setVerticallyCentered:NO]; 
    [printInfo setHorizontallyCentered:NO]; 
    [printInfo setLeftMargin:72.0]; 
    [printInfo setRightMargin:72.0]; 
    [printInfo setTopMargin:72.0]; 
    [printInfo setBottomMargin:72.0]; 
    return printInfo; 
} 
//------------------------------------------------------------------------------------------------ 
-(NSPrintOperation *)printOperationWithSettings:(NSDictionary *)printSettings error:(NSError **)outError { 
    NSAttributedString *attString = myAttributedString; 
    NSPrintInfo *aPrintInfo = [self thePrintInfo]; 
  // the rect doesn’t really matter—the textView is used to assemble the pieces 
    NSRect theRect = NSMakeRect (0, 0, 468.0, 300.0); 
    NSTextView *printTextView = [[NSTextView alloc] initWithFrame:theRect]; 
    NSTextStorage *printStorage = [[NSTextStorage alloc] initWithAttributedString:attString]; 
    [[printTextView layoutManager] replaceTextStorage:printStorage]; 
    NSPrintOperation *op = [NSPrintOperation printOperationWithView:printTextView printInfo:aPrintInfo]; 
    [op setShowsPrintPanel:NO]; 
    [op setShowsProgressPanel:YES]; 
    return op; 
}

symczyk;


Thanks for the traction. Your contributions here are appreciated!!


You say above: '...For creating a PDF from a text view's contents, you're better off going with NSPrintOperation...'


Yeah, that's where I started this thread. Below is that code.


I'm back to the original question: This code produces a .pdf file BUT that .pdf does not contain the NSAttributedString shown in the NSTextView. The .pdf file only contains the .string value of the source NSAttributedString.


- (PDFDocument *) pdfForAttributedString:(NSView *)thisView  {
  NSString *globUniq = [[NSProcessInfo processInfo] globallyUniqueString];
  NSURL *printURL = [NSURL URLWithString:[[self.tempDirectoryURL path] stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.pdf", globUniq]]];
  NSMutableDictionary *dict = [[NSPrintInfo sharedPrintInfo] dictionary];
  [dict setObject:NSPrintSaveJob forKey:NSPrintJobDisposition];
  [dict setObject:printURL forKey:NSPrintJobSavingURL];
//set 1 in margins
  [dict setObject:[NSNumber numberWithFloat:72.0] forKey:NSTopMarginDocumentAttribute];
  [dict setObject:[NSNumber numberWithFloat:72.0] forKey:NSBottomMarginDocumentAttribute];
  [dict setObject:[NSNumber numberWithFloat:72.0] forKey:NSLeftMarginDocumentAttribute];
  [dict setObject:[NSNumber numberWithFloat:72.0] forKey:NSRightMarginDocumentAttribute];
//centering
  [dict setObject:[NSNumber numberWithBool:0] forKey:NSPrintHorizontallyCentered];
  [dict setObject:[NSNumber numberWithBool:0] forKey:NSPrintVerticallyCentered];
  NSPrintInfo *pix = [[NSPrintInfo alloc] initWithDictionary:dict];
  [pix setHorizontalPagination:NSPrintingPaginationModeFit];//NSPrintingPaginationModeFit  NSAutoPagination
  [pix setVerticalPagination:NSPrintingPaginationModeFit];//NSAutoPagination
//setup dataObject
  NSMutableData *output = [[NSMutableData alloc] init];
  NSPrintOperation *op = [NSPrintOperation PDFOperationWithView:thisView insideRect:thisView.frame toData:output printInfo:pix];
//hide UI
  [op setShowsPrintPanel:NO];
  [op setShowsProgressPanel:NO];
//do the thing
  if ([op runOperation]) {
  PDFDocument * doc = [[PDFDocument alloc] initWithData:output];
  return doc;
  } else {
  return nil;
  }
}

To Anyone Tagging Along;

OMG It appears to me now that this is the expected behavior!


Create a new Word document or TextEdit..

Enter some text and assign a link to this text.

Now use 'Print' and then 'Save as PDF' to save a document.

Now open that newly created .pdf file.

No hyperlinks!! You may see text that has link color but the underlying hyperlink is not embedded in the .pdf file!

BTW this is tru for Catalina and High Sierra (10.13.6)

Has it always been like this? It doesn't seem so to me but I can't point to anything.


Steve

I was curious so I just tested it in my app. I created a document, copied the link from this forum, pasted it into the document, and saved it as a pdf. When I open the pdf with preview, the link works--pops up Safari and opens this forum when I click on it.

janabanana


Thank-You for tagging along in this voyage!


However, your comment confuses me on several points:

first you make no comment as to my Word or TextEdit suggested actions... can you confirm the behavior I have observed? (what OS?)

you say 'tested in my app' -- so you wrote all the code involved?

you say 'saved it as a pdf' - this is using your code or using the standard macOS 'Print' - 'Save As'?

in your app are you using NSPrintOperation to render the attributed string within an NSTextView or are you doing something different?

Sorry for being a bit vague.


I just tested a link in TextEdit and it works. This is on 10.14.6. I do my development on 10.15.3 / XCode 11.3 which is where I tested the code sample above.


Yes, I wrote all the code involved including what I pasted above. This is a much simpler version of the code I use in my app. I trimmed it down to just what is needed to do the pdf print and tested it before I pasted it. I would not post code that I had not verfied or had not written myself. I'm sure at some point I got the code ideas from someplace else but that was years ago.


My app is a text editor of sorts. It uses NSDocument, windowController, textViews, the usual stuff. It has multiple pages with multiple textViews and the layout of the display and printing is quite involved. But, again, I did test the code above. I pulled the attributed string directly from the document's textStorage in NSDocument.


No, this is not using the Print, save as method. My code has an Export to PDF menu item. That menu item is connected to the saveDocumentToPDF action in NSDocument. When I export to a pdf in this method, a little window pops up asking for a filename and that's it. No printing panel stuff.


I was thinking about your problems and I am wondering if maybe there is something further up the "food chain" causing your issues. Have you verified that yes, indeed, your attributed string really is an attributed string? Is there the possibility that your textView(s) are doing plain text and you don't know it?


If you use the above code I posted in an NSDocument class, you should get a pdf file with the attributes in it if indeed your textStorage/attributed string really does have the attributes in it.

NSPrintOperation PDFOperationWithView
 
 
Q