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. 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 NSTextLayoutFragment
s?
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.