Converting an Attributed String to a PDF

Attributed strings are a powerful way to stylize textual content, as they contain the string alongside the visual representation of said text. As such, they are great candidates for converting into graphical outputs, like an image or a PDF.

In this article, I’ll cover how an attributed string can be converted to a PDF on iOS. There are multiple ways a PDF can be generated from NSAttributedString via system APIs, with various ways to handle the conversion. I’ll provide an overview of two of these methods, but ultimately, you should choose the option that best fits your needs.

Let’s assume we create an NSAttributedString to work with and that we want to convert it to a PDF, like what’s shown in this Gist.

Using a Graphics Context

One solution for converting an attributed string to a PDF would be to use a PDF graphics context via UIGraphicsPDFRenderer. This method is mostly suitable for short strings that should only be converted into a single-page PDF. However, this solution is advanced, since it is possible to literally draw anything on the PDF in the graphics context and it also requires more specific handling. It isn’t straightforward to create multiple pages from a long string, so you will need to manually handle splitting up the string into multiple parts for each page.

This is how an attributed string can be converted into a single-page PDF that has the size of a DIN A4 page:

Copy
1
2
3
4
5
6
7
8
9
10
11
let documentURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("Converted.pdf")
let dinA4PaperRect = CGRect(origin: .zero, size: CGSize(width: 595, height: 842))
let graphicsRenderer = UIGraphicsPDFRenderer(bounds: dinA4PaperRect)
try? graphicsRenderer.writePDF(to: documentURL) { context in
    context.beginPage()
    let pageMargin: CGFloat = 60
    let drawRect = dinA4PaperRect.inset(by: UIEdgeInsets(top: pageMargin, left: pageMargin, bottom: pageMargin, right: pageMargin))
    attributedString.draw(in: drawRect)
}

// Use the generated PDF stored at documentURL.
Copy
1
2
3
4
5
6
7
8
9
10
11
NSURL *documentURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"Converted.pdf"]];
CGRect dinA4PaperRect = (CGRect){.size = CGSizeMake(595.f, 842.f)};
UIGraphicsPDFRenderer *graphicsRenderer = [[UIGraphicsPDFRenderer alloc] initWithBounds:dinA4PaperRect];
[graphicsRenderer writePDFToURL:documentURL withActions:^(UIGraphicsPDFRendererContext *context) {
    [context beginPage];
    CGFloat pageMargin = 60.f;
    CGRect drawRect = UIEdgeInsetsInsetRect(dinA4PaperRect, UIEdgeInsetsMake(pageMargin, pageMargin, pageMargin, pageMargin));
    [attributedString drawInRect:drawRect];
} error:nil];

// Use the generated PDF stored at documentURL.

This results in a single-page PDF being created, as shown below.

Any text that flows off the first page is, by default, cut off and not rendered. In such cases, it is necessary to manually handle content fitting on the page and take care of adding new pages as needed.

Using a Print Formatter

The easiest way to handle conversion is using a print formatter. If you want something flexible and customizable that also provides high-quality output and support for automatic page wrapping of strings, this is the way to go. UIPrintPageRenderer can be used to create a PDF from a print formatter like UISimpleTextPrintFormatter.

UISimpleTextPrintFormatter takes an attributed string and handles drawing it into one page or wrapping it into multiple pages automatically. You also have the ability to customize various properties — like content insets — on the print formatter.

To utilize this, we can create a subclass of UIPrintPageRenderer that provides paperRect and printableRect — along with a custom method to generate the PDF — from the print formatter to a URL on disk:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class PDFDINA4PrintRenderer: UIPrintPageRenderer {

    let pageSize = CGSize(width: 595, height: 842)

    override var paperRect: CGRect {
        return CGRect(origin: .zero, size: pageSize)
    }

    override var printableRect: CGRect {
        let pageMargin: CGFloat = 60
        let margins = UIEdgeInsets(top: pageMargin, left: pageMargin, bottom: pageMargin, right: pageMargin)
        return paperRect.inset(by: margins)
    }

    func renderPDF(to url: URL) throws {
        prepare(forDrawingPages: NSMakeRange(0, numberOfPages))

        let graphicsRenderer = UIGraphicsPDFRenderer(bounds: paperRect)
        try graphicsRenderer.writePDF(to: url) { context in
            for pageIndex in 0..<numberOfPages {
                context.beginPage()
                drawPage(at: pageIndex, in: context.pdfContextBounds)
            }
        }
    }
}
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@interface PSPDFDINA4PrintRenderer: UIPrintPageRenderer
- (BOOL)renderPDFToURL:(NSURL *)url error:(NSError *__autoreleasing *)error;
@end

@implementation PSPDFDINA4PrintRenderer

- (CGSize)pageSize {
    return CGSizeMake(595.f, 842.f);
}

- (CGRect)paperRect {
    return (CGRect){.size = self.pageSize};
}

- (CGRect)printableRect {
    CGFloat pageMargin = 60.f;
    UIEdgeInsets margins = UIEdgeInsetsMake(pageMargin, pageMargin, pageMargin, pageMargin);
    return UIEdgeInsetsInsetRect(self.paperRect, margins);
}

- (BOOL)renderPDFToURL:(NSURL *)url error:(NSError *__autoreleasing *)error {
    [self prepareForDrawingPages:NSMakeRange(0, self.numberOfPages)];

    UIGraphicsPDFRenderer *graphicsRenderer = [[UIGraphicsPDFRenderer alloc] initWithBounds:self.paperRect];
    return [graphicsRenderer writePDFToURL:url withActions:^(UIGraphicsPDFRendererContext *context) {
        for (NSInteger pageIndex = 0; pageIndex < self.numberOfPages; pageIndex++) {
            [context beginPage];
            [self drawPageAtIndex:pageIndex inRect:context.pdfContextBounds];
        }
    } error:error];
}

@end

Now we can use our custom print renderer and provide it with a print formatter. The print formatter handles splitting up the string into multiple pages in case it doesn’t fit on one page:

Copy
1
2
3
4
5
6
7
8
9
let pdfRenderer = PDFDINA4PrintRenderer()
let printFormatter = UISimpleTextPrintFormatter(attributedText: attributedString)
pdfRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0)

let documentURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("Converted from Attributed String.pdf")

try pdfRenderer.renderPDF(to: documentURL)

// Use the generated PDF stored at documentURL.
Copy
1
2
3
4
5
6
7
8
9
PSPDFDINA4PrintRenderer *pdfRenderer = [PSPDFDINA4PrintRenderer new];
UISimpleTextPrintFormatter *printFormatter = [[UISimpleTextPrintFormatter alloc] initWithAttributedText:attributedString];
[pdfRenderer addPrintFormatter:printFormatter startingAtPageAtIndex:0];

NSURL *documentURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"Converted from Attributed String.pdf"]];
NSError *error;
if ([pdfRenderer renderPDFToURL:documentURL error:&error]) {
    // Use the generated PDF stored at documentURL.
}

Converting a long, attributed string with a few different attributes will generate a PDF with multiple pages that looks like the one below.

You can even add multiple print formatters to a single print renderer to append differently formatted strings. For example, you can render an attributed string on the first few pages and append pages containing a converted HTML string afterward.

Work with the Generated PDF

Now that we’ve explored different ways of how an attributed string can be converted into a PDF, you might want to continue working with the document. Depending on your use case, you might want to display, modify, or mark up the PDF. All of this can be done with PSPDFKit. Head over to our guides to get an overview of how PDFs can be utilized in your app.

PSPDFKit for iOS

Download the free 60-day trial and add it to your app today.

iOS