Fix extremely oversized text in PDF export

Three defensive fixes for WKWebView.createPDF() rendering text far
larger than the source HTML's actual font sizes specify:

- Added a viewport meta tag and explicit -webkit-text-size-adjust:
  100% — WebKit's shared engine has a text-autosizing heuristic
  (normally associated with mobile Safari, but can trigger in WKWebView
  contexts too, especially for content loaded via loadHTMLString with
  no base URL) that boosts font sizes for perceived readability; this
  disables it explicitly rather than relying on it not firing.
- Explicit body font-size (was previously unset, relying on WebKit's
  default) and webView.pageZoom = 1.0.
- Resize the webview to the actual rendered content height
  (via document.body.scrollHeight) before calling createPDF, instead
  of relying on its undocumented auto-sizing against the arbitrary
  initial frame — a bounds mismatch here is a known cause of
  blown-up/rescaled PDF output.
This commit is contained in:
2026-07-29 09:01:12 +02:00
parent 6fb49083d8
commit a3fd2c0eab
+13 -2
View File
@@ -56,6 +56,7 @@ enum ConversationExportService {
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>\(htmlEscape(name))</title> <title>\(htmlEscape(name))</title>
<style>\(css)</style> <style>\(css)</style>
</head> </head>
@@ -69,9 +70,10 @@ enum ConversationExportService {
nonisolated private static let css = """ nonisolated private static let css = """
:root { color-scheme: light dark; } :root { color-scheme: light dark; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body { font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; \ body { font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; \
color: #1a1a1a; background: #ffffff; max-width: 820px; margin: 40px auto; padding: 0 24px; \ font-size: 15px; color: #1a1a1a; background: #ffffff; max-width: 820px; margin: 40px auto; \
line-height: 1.5; } padding: 0 24px; line-height: 1.5; }
h1 { font-size: 24px; border-bottom: 1px solid #ddd; padding-bottom: 12px; } h1 { font-size: 24px; 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; }
.message.user { background: #f5f7fa; border-left-color: #4a90d9; } .message.user { background: #f5f7fa; border-left-color: #4a90d9; }
@@ -112,6 +114,7 @@ enum ConversationExportService {
static func pdfData(name: String, messages: [Message]) async throws -> Data { static func pdfData(name: String, messages: [Message]) async throws -> Data {
let htmlString = html(name: name, messages: messages) let htmlString = html(name: name, messages: messages)
let webView = WKWebView(frame: NSRect(x: 0, y: 0, width: 816, height: 1056)) let webView = WKWebView(frame: NSRect(x: 0, y: 0, width: 816, height: 1056))
webView.pageZoom = 1.0
// PDF content is baked at export time, so it can't respond to prefers-color-scheme // PDF content is baked at export time, so it can't respond to prefers-color-scheme
// live like the HTML export does match whatever appearance the app is in right now. // live like the HTML export does match whatever appearance the app is in right now.
webView.appearance = NSApp.effectiveAppearance webView.appearance = NSApp.effectiveAppearance
@@ -119,6 +122,14 @@ enum ConversationExportService {
webView.navigationDelegate = delegate webView.navigationDelegate = delegate
try await delegate.load(htmlString, in: webView) try await delegate.load(htmlString, in: webView)
// Resize to the actual rendered content height so createPDF's capture bounds match
// reality, rather than relying on its undocumented auto-sizing against the arbitrary
// initial frame a mismatch here is a common cause of blown-up/rescaled PDF output.
if let heightValue = try? await webView.evaluateJavaScript("document.body.scrollHeight"),
let height = (heightValue as? NSNumber)?.doubleValue, height > 0 {
webView.frame = NSRect(x: 0, y: 0, width: webView.frame.width, height: height)
}
return try await withCheckedThrowingContinuation { continuation in return try await withCheckedThrowingContinuation { continuation in
webView.createPDF(configuration: WKPDFConfiguration()) { result in webView.createPDF(configuration: WKPDFConfiguration()) { result in
continuation.resume(with: result) continuation.resume(with: result)