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 UILabel
s 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 UILabel
s inside a text view won't get 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?
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.