iosswiftuitextviewtextkit

Avoiding rasterizing UITextView when exporting to PDF


I'm trying to render a UIView to PDF, while maintaining text elements as actual text and not rasterizing them.

I'm using TextKit 2 backed UITextView and NSTextLayoutManager to provide the contents. However, no matter what I do, UITextView gets rasterized into a bitmap.

Here's the basic code:

let renderer = UIGraphicsPDFRenderer(bounds: pageRect, format: format)

let data = renderer.pdfData { (context) in
    for page in self.pageViews {
        context.beginPage()
        let cgContext = context.cgContext               
        page.layer.render(in: cgContext)
    }
}

A page view usually contains one UITextView, which uses somewhat advanced layout, so unfortunately I can't just toss it into a single NSAttributedString and draw into a CoreText context.

There's a hack to get UILabels to render themselves as non-rasterized text, and if I understand correctly, it works by just actually drawing them on the current context.

Interestingly, when providing UILabel view via NSTextAttachmentViewProvider to a text view using TextKit 2, the UILabels inside a text view won't get rasterized:

labels are not rasterized

This hinted that the actual text fragments are the ones that get rasterized when drawing, not the whole view, and I was right. You can enumerate the text fragments and draw them directly on the context, which makes them remain as text rather than become a bitmap:

page.textView?.textLayoutManager?.enumerateTextLayoutFragments(from: location, options: [.ensuresLayout, .estimatesSize, .ensuresExtraLineFragment], using: { fragment in
    let frame = fragment.layoutFragmentFrame
    fragment.draw(at: frame.origin, in: cgContext)
    return true
})

However, this causes other issues, because layout coordinates will be all over the place and not confined to the text view itself (and even more so for my custom NSTextLayoutFragment class), and text attachments won't get drawn this way either, but display a skewed placeholder icon.

I'm wondering if there is a way to make UITextView and NSTextLayoutManager to draw their contents similar to UILabel, so the fragments would remain as text in the PDF, rather than become a bitmap?


Solution

  • Here's a simple way to render a UITextView as real text into a PDF using TextKit 2. It currently supports only one text attachment per paragraph, because you apparently can't use fragment.draw(at:origin:) to draw attachments.

    let renderer = UIGraphicsPDFRenderer(bounds: pageRect, format: format)
    let data = renderer.pdfData { (context) in
    
        let cgContext = context.cgContext
    
        textView.textLayoutManager.enumerateTextLayoutFragments(from: location, options: [.ensuresLayout, .estimatesSize, .ensuresExtraLineFragment], using: { fragment in
            
            let frame = fragment.layoutFragmentFrame
            let origin = page.textView?.frame.origin ?? CGPointZero
            
            var actualFrame = frame
            actualFrame.origin.x += origin.x
            actualFrame.origin.y += origin.y
                                    
            
            if let provider = fragment.textAttachmentViewProviders.first, let view = provider.view {
                // Draw a text attachment
                let attachmentFrame = fragment.frameForTextAttachment(at: fragment.rangeInElement.location)
                actualFrame.origin.y += attachmentFrame.origin.y
                
                cgContext.saveGState()
                cgContext.translateBy(x: actualFrame.origin.x, y: actualFrame.origin.y)
                view.layer.render(in: cgContext)
                cgContext.restoreGState()
                
                return true
            } else {
                // Draw a normal paragraph
                fragment.draw(at: origin, in: cgContext)
            }
    
            return true
        })
    
    
    }
    

    I have no idea why Apple decided not to include this as default behavior in UITextView, because it seems entirely possible.