|
|
@@ -63,7 +63,7 @@ final class CVFilledPreviewPageView: NSView {
|
|
63
|
63
|
editCheckbox.target = self
|
|
64
|
64
|
editCheckbox.action = #selector(didToggleEditMode)
|
|
65
|
65
|
|
|
66
|
|
- let subtitle = NSTextField(wrappingLabelWithString: "Layout matches the CV Maker thumbnail for this template. Turn on editing to adjust wording, then export a vector PDF.")
|
|
|
66
|
+ let subtitle = NSTextField(wrappingLabelWithString: "Layout matches the CV Maker thumbnail for this template. Turn on editing to adjust wording, then export a PDF that matches what you see here (fonts, columns, colours, and rules).")
|
|
67
|
67
|
subtitle.font = .systemFont(ofSize: 12, weight: .regular)
|
|
68
|
68
|
subtitle.textColor = Self.secondaryText
|
|
69
|
69
|
subtitle.maximumNumberOfLines = 0
|
|
|
@@ -165,7 +165,10 @@ final class CVFilledPreviewPageView: NSView {
|
|
165
|
165
|
doc.layoutSubtreeIfNeeded()
|
|
166
|
166
|
let bounds = doc.bounds
|
|
167
|
167
|
guard !bounds.isEmpty else { return }
|
|
168
|
|
- let data = doc.dataWithPDF(inside: bounds)
|
|
|
168
|
+ // `dataWithPDF` uses the print pipeline and often drops layer-backed chrome (card fill,
|
|
|
169
|
+ // borders, hairlines, tinted sidebars) while keeping text — so the PDF looks like plain
|
|
|
170
|
+ // text. Rasterising what is actually drawn on screen preserves the full layout.
|
|
|
171
|
+ let data = doc.pdfDataMatchingScreenAppearance() ?? doc.dataWithPDF(inside: bounds)
|
|
169
|
172
|
guard !data.isEmpty else {
|
|
170
|
173
|
presentExportError("The résumé could not be rendered to PDF (empty output). Try scrolling the preview so it lays out, then export again.")
|
|
171
|
174
|
return
|
|
|
@@ -215,3 +218,36 @@ final class CVFilledPreviewPageView: NSView {
|
|
215
|
218
|
}
|
|
216
|
219
|
}
|
|
217
|
220
|
}
|
|
|
221
|
+
|
|
|
222
|
+// MARK: - PDF export (screen-accurate)
|
|
|
223
|
+
|
|
|
224
|
+private extension NSView {
|
|
|
225
|
+
|
|
|
226
|
+ /// Single-page PDF that matches the composited on-screen appearance, including
|
|
|
227
|
+ /// `CALayer` backgrounds, borders, and rules. Callers fall back to `dataWithPDF` when this returns nil.
|
|
|
228
|
+ func pdfDataMatchingScreenAppearance() -> Data? {
|
|
|
229
|
+ layoutSubtreeIfNeeded()
|
|
|
230
|
+ let rect = bounds
|
|
|
231
|
+ guard rect.width > 1, rect.height > 1 else { return nil }
|
|
|
232
|
+
|
|
|
233
|
+ guard let rep = bitmapImageRepForCachingDisplay(in: rect) else { return nil }
|
|
|
234
|
+ cacheDisplay(in: rect, to: rep)
|
|
|
235
|
+ guard let cgImage = rep.cgImage else { return nil }
|
|
|
236
|
+
|
|
|
237
|
+ let data = NSMutableData()
|
|
|
238
|
+ guard let consumer = CGDataConsumer(data: data as CFMutableData) else { return nil }
|
|
|
239
|
+ var mediaBox = CGRect(origin: .zero, size: NSSize(width: rect.width, height: rect.height))
|
|
|
240
|
+ guard let ctx = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else { return nil }
|
|
|
241
|
+
|
|
|
242
|
+ ctx.beginPDFPage(nil)
|
|
|
243
|
+ ctx.interpolationQuality = .high
|
|
|
244
|
+ // Draw without an extra Y-flip: `cacheDisplay`’s bitmap + `cgImage` already match PDF user space
|
|
|
245
|
+ // on macOS here; a translate/scale(-1) was inverting the whole page in Preview.
|
|
|
246
|
+ ctx.draw(cgImage, in: mediaBox)
|
|
|
247
|
+ ctx.endPDFPage()
|
|
|
248
|
+ ctx.closePDF()
|
|
|
249
|
+
|
|
|
250
|
+ let out = data as Data
|
|
|
251
|
+ return out.isEmpty ? nil : out
|
|
|
252
|
+ }
|
|
|
253
|
+}
|