-
-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Consent PDF export truncates at 1 page #49
Comments
Example of current PDF export from SpeziOnboarding: Example of expected PDF export (generated by ResearchKit): |
Thanks for creating the issue @vishnuravi! |
I spent quite some time working on a solution for this. I came up with something that works, but I am not sure if it covers all edge cases (see code below) In short, I added some logic for manual pagination, creating individual PDF pages if the text overflows to the next page. I tested with one, two, and three pages and it worked well. I attached some PDF examples at the end of this comment. The crucial part in the code is the split function, which took me some tries to get right :D If you think this can be a suitable solution, I will be happy to clean up the code and do a PR :) On a side note, I also tried different approaches and got some findings I think are worth sharing: An alternative to generating the PDF would be to use a library like Ink to convert the markdown text to HTML code, and then use a WebView to render the PDF. However, WebView's createPDF() function also does not include automatic pagination but instead puts all the text in one big PDF file. It might be possible, however, to split that PDF file into smaller individual pages. I did not pursue this approach further. Here is my current solution. You can also check out the complete code in my forked repo @MainActor
func export() async -> PDFDocument?
{
let markdown = await asyncMarkdown()
let markdownString = (try? AttributedString(
markdown: markdown,
options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
)) ?? AttributedString(String(localized: "MARKDOWN_LOADING_ERROR", bundle: .module))
let pageSize = CGSize(
width: exportConfiguration.paperSize.dimensions.width,
height: exportConfiguration.paperSize.dimensions.height
)
let pages = paginatedViews(markdown: markdownString)
print("NumPages: \(pages.count)")
return await withCheckedContinuation { continuation in
guard let mutableData = CFDataCreateMutable(kCFAllocatorDefault, 0),
let consumer = CGDataConsumer(data: mutableData),
let pdf = CGContext(consumer: consumer, mediaBox: nil, nil) else {
continuation.resume(returning: nil)
return
}
for page in pages {
pdf.beginPDFPage(nil)
let hostingController = UIHostingController(rootView: page)
hostingController.view.frame = CGRect(origin: .zero, size: pageSize)
let renderer = UIGraphicsImageRenderer(bounds: hostingController.view.bounds)
let image = renderer.image { ctx in
hostingController.view.drawHierarchy(in: hostingController.view.bounds, afterScreenUpdates: true)
}
// Correct text being rendered 180° rotated due to coordinate system mismatch.
pdf.saveGState()
pdf.translateBy(x: 0, y: pageSize.height)
pdf.scaleBy(x: 1.0, y: -1.0)
hostingController.view.layer.render(in: pdf)
pdf.restoreGState()
pdf.endPDFPage()
}
pdf.closePDF()
continuation.resume(returning: PDFDocument(data: mutableData as Data))
}
}
private func paginatedViews(markdown: AttributedString) -> [AnyView]
{
var pages = [AnyView]()
var remainingMarkdown = markdown
let pageSize = CGSize(width: exportConfiguration.paperSize.dimensions.width, height: exportConfiguration.paperSize.dimensions.height)
let headerHeight: CGFloat = 150
let footerHeight: CGFloat = 150
while !remainingMarkdown.unicodeScalars.isEmpty {
let (currentPageContent, nextPageContent) = split(markdown: remainingMarkdown, pageSize: pageSize, headerHeight: headerHeight, footerHeight: footerHeight)
let currentPage: AnyView = AnyView(
VStack {
if pages.isEmpty { // First page
OnboardingTitleView(title: exportConfiguration.consentTitle)
}
Text(currentPageContent)
.padding()
Spacer()
if nextPageContent.unicodeScalars.isEmpty { // Last page
ZStack(alignment: .bottomLeading) {
SignatureViewBackground(name: name, backgroundColor: .clear)
#if !os(macOS)
Image(uiImage: blackInkSignatureImage)
#else
Text(signature)
.padding(.bottom, 32)
.padding(.leading, 46)
.font(.custom("Snell Roundhand", size: 24))
#endif
}
.padding(.bottom, footerHeight)
}
}
.frame(width: pageSize.width, height: pageSize.height)
)
pages.append(currentPage)
remainingMarkdown = nextPageContent
}
return pages
}
private func split(markdown: AttributedString, pageSize: CGSize, headerHeight: CGFloat, footerHeight: CGFloat) -> (AttributedString, AttributedString)
{
let contentHeight = pageSize.height - headerHeight - footerHeight
var currentPage = AttributedString()
var remaining = markdown
let textStorage = NSTextStorage(attributedString: NSAttributedString(markdown))
let layoutManager = NSLayoutManager()
let textContainer = NSTextContainer(size: CGSize(width: pageSize.width, height: contentHeight))
layoutManager.addTextContainer(textContainer)
textStorage.addLayoutManager(layoutManager)
var accumulatedHeight: CGFloat = 0
let maximumRange = layoutManager.glyphRange(for: textContainer)
currentPage = AttributedString(textStorage.attributedSubstring(from: maximumRange))
remaining = AttributedString(textStorage.attributedSubstring(from: NSRange(location: maximumRange.length, length: textStorage.length - maximumRange.length)))
return (currentPage, remaining)
} And here are some successful examples: |
Thanks a lot @RealLast for the deep-dive into that topic! 🚀 |
…le pages, if footer, header and text do not fit on a single page. Addresses StanfordSpezi#49.
Thanks Philipp! I just created a PR for this. |
Description
The PDF export of the consent form is truncated if the text exceeds 1 page.
Reproduction
A reproducible example can be seen in the LifeSpace StrokeCog study application. See the comment below for a PDF produced by this application.
Expected behavior
The PDF export is expected to contain all of the text in the markdown file provided, followed by the signature.
Additional context
No response
Code of Conduct
The text was updated successfully, but these errors were encountered: