Switch PDF export from createPDF() to real print pagination
Rune compared our PDF against a normal reference PDF and the difference was structural, not just a font-size tweak: the reference was 6 standard A4 pages (595x842pt); ours was ONE continuous page 850x6886pt — nearly 8 feet tall. That's what createPDF() actually does when content overflows its frame (confirmed in the prior commit) — it auto-grows to fit everything as a single non-standard-sized page, never paginating. There was no page of a familiar size to judge the "normal" font size against, which is what actually read as "huge" even after the sizing fixes. Replaced with WKWebView's real print pipeline: build an NSPrintInfo for US Letter (612x792pt) with normal margins, get a print operation via webView.printOperation(with:), and run it silently (no panel) to a temp file, dispatched off the main actor since NSPrintOperation.run() blocks synchronously. This is genuine multi-page pagination — same mechanism any app's real print-to-PDF uses. Also added break-inside/page-break-inside: avoid on .message so a single message doesn't get split awkwardly across a page boundary. Verified with a standalone reproduction script (same method as the prior fix) using the actual exported CSS and realistic multi-message content: clean 2-page US Letter output, message boundaries respected across the page break, normal-looking document proportions.
This commit is contained in:
@@ -76,7 +76,8 @@ enum ConversationExportService {
|
||||
font-size: 14px; color: #1a1a1a; background: #ffffff; max-width: 820px; margin: 40px auto; \
|
||||
padding: 0 24px; line-height: 1.5; }
|
||||
h1 { font-size: 22px; border-bottom: 1px solid #ddd; padding-bottom: 12px; }
|
||||
.message { margin: 20px 0; padding: 12px 16px; border-radius: 8px; border-left: 4px solid transparent; }
|
||||
.message { margin: 20px 0; padding: 12px 16px; border-radius: 8px; border-left: 4px solid transparent; \
|
||||
break-inside: avoid; page-break-inside: avoid; }
|
||||
.message.user { background: #f5f7fa; border-left-color: #4a90d9; }
|
||||
.message.assistant { background: #fafafa; border-left-color: #8a8a8a; }
|
||||
.role { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; \
|
||||
@@ -115,16 +116,22 @@ enum ConversationExportService {
|
||||
|
||||
// MARK: - PDF
|
||||
|
||||
enum PDFError: LocalizedError {
|
||||
case generationFailed
|
||||
var errorDescription: String? { "Failed to generate PDF" }
|
||||
}
|
||||
|
||||
/// Uses WKWebView's real print pipeline (NSPrintOperation), not createPDF(). Confirmed
|
||||
/// empirically by comparing against a normal reference PDF: createPDF() never paginates —
|
||||
/// it captures the webview's frame verbatim, or auto-grows to ONE continuous page matching
|
||||
/// full content height for overflow (a single page many thousands of points tall for a long
|
||||
/// conversation). That's a fundamentally different, non-standard document shape from any
|
||||
/// normal PDF, which is what actually made ordinarily-sized text read as "huge" — there was
|
||||
/// no page of a familiar size to judge it against. Printing through NSPrintOperation with a
|
||||
/// real US Letter paper size gives genuine multi-page pagination, matching what any other
|
||||
/// app's "export/print to PDF" produces.
|
||||
static func pdfData(name: String, messages: [Message]) async throws -> Data {
|
||||
let htmlString = html(name: name, messages: messages)
|
||||
// Confirmed empirically (not guessed): with WKPDFConfiguration.rect left at its
|
||||
// default, createPDF captures exactly the webview's frame size verbatim for content
|
||||
// that fits within it, and auto-grows to a single tall page matching the full
|
||||
// scrollable content when it overflows — it does not paginate. Manually resizing the
|
||||
// frame to match content height beforehand (a previous attempt at this) actively
|
||||
// breaks that: the captured page ends up NOT matching the resized frame at all,
|
||||
// producing a small, oddly-proportioned page that made ordinary-sized text look
|
||||
// enormous relative to it. So: set a sane starting frame and leave it alone.
|
||||
let webView = WKWebView(frame: NSRect(x: 0, y: 0, width: 850, height: 1100))
|
||||
webView.pageZoom = 1.0
|
||||
// PDF content is baked at export time, so it can't respond to prefers-color-scheme
|
||||
@@ -134,11 +141,34 @@ enum ConversationExportService {
|
||||
webView.navigationDelegate = delegate
|
||||
try await delegate.load(htmlString, in: webView)
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
webView.createPDF(configuration: WKPDFConfiguration()) { result in
|
||||
continuation.resume(with: result)
|
||||
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".pdf")
|
||||
defer { try? FileManager.default.removeItem(at: tempURL) }
|
||||
|
||||
let printInfo = NSPrintInfo()
|
||||
printInfo.paperSize = NSSize(width: 612, height: 792) // US Letter
|
||||
printInfo.topMargin = 36
|
||||
printInfo.bottomMargin = 36
|
||||
printInfo.leftMargin = 36
|
||||
printInfo.rightMargin = 36
|
||||
printInfo.horizontalPagination = .fit
|
||||
printInfo.jobDisposition = .save
|
||||
printInfo.dictionary()[NSPrintInfo.AttributeKey.jobSavingURL] = tempURL
|
||||
|
||||
let printOp = webView.printOperation(with: printInfo)
|
||||
printOp.showsPrintPanel = false
|
||||
printOp.showsProgressPanel = false
|
||||
|
||||
// NSPrintOperation.run() blocks synchronously, so it's dispatched off the main actor
|
||||
// to avoid freezing the UI while a long conversation paginates.
|
||||
let success = await withCheckedContinuation { (continuation: CheckedContinuation<Bool, Never>) in
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
continuation.resume(returning: printOp.run())
|
||||
}
|
||||
}
|
||||
guard success, let data = try? Data(contentsOf: tempURL) else {
|
||||
throw PDFError.generationFailed
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private final class PDFLoadDelegate: NSObject, WKNavigationDelegate {
|
||||
|
||||
Reference in New Issue
Block a user