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. In the image below, the paragraph is a bitmap, while the heading is real text.

This hinted that the actual text fragments are the ones that get rasterized when drawing, not the whole view, and I was right.

I 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
})

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). I can get around this, but my text attachments won't get drawn this way either, but display a skewed placeholder icon:

I can render them correctly on the context manually:

if fragment.textAttachmentViewProviders.count > 0 {
    if let view = fragment.textAttachmentViewProviders.first?.view {
        view.layer.render(in: cgContext)
        return true
    }
}

This renders them at 0, 0 though.
Edit: Changing bounds or frame for view/layer has no effect.

I'm wondering if there is a way to make UITextView and NSTextLayoutManager to draw their contents this way automatically, so the fragments would remain as text in the PDF, rather than become a bitmap? It seems weird that the text view doesn't do this by default like it does on macOS, especially as it appears to be entirely possible.

Or, if that's not possible, what's the correct way of drawing my text attachments when enumerating NSTextLayoutFragments?

Accepted Reply

Oh well. 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 display 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 make this the default behavior in UITextView, I doubt most people would want their text views to be rasterized in PDFs.

Replies

Oh well. 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 display 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 make this the default behavior in UITextView, I doubt most people would want their text views to be rasterized in PDFs.