Pārlūkot izejas kodu

Fix document preview and printing for DOCX, MD, and other text formats.

Add Quick Look preview and a unified in-memory PDF print pipeline so office and text files render content reliably in the print dialog.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 mēnesi atpakaļ
vecāks
revīzija
d496c50dd6

+ 235 - 0
smart_printer/DocumentContentService.swift

@@ -0,0 +1,235 @@
+import Cocoa
+import PDFKit
+import QuickLookUI
+import UniformTypeIdentifiers
+
+enum DocumentContentService {
+    static let textExtensions: Set<String> = ["txt", "md", "csv", "json", "xml", "html", "htm"]
+    static let wordProcessorExtensions: Set<String> = ["doc", "docx", "odt"]
+    static let officeExtensions: Set<String> = [
+        "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pages", "numbers", "key", "odt",
+    ]
+
+    static func contentType(for url: URL) -> UTType? {
+        if let values = try? url.resourceValues(forKeys: [.contentTypeKey]),
+           let type = values.contentType {
+            return type
+        }
+        let ext = url.pathExtension.lowercased()
+        guard !ext.isEmpty else { return nil }
+        return UTType(filenameExtension: ext)
+    }
+
+    static func makePreviewView(for url: URL) -> NSView? {
+        let ext = url.pathExtension.lowercased()
+        let type = contentType(for: url)
+
+        if type?.conforms(to: .pdf) == true, let document = PDFDocument(url: url) {
+            let pdfView = PDFView()
+            pdfView.document = document
+            pdfView.autoScales = true
+            pdfView.displayMode = .singlePageContinuous
+            pdfView.backgroundColor = .clear
+            return pdfView
+        }
+
+        if type?.conforms(to: .image) == true, let image = NSImage(contentsOf: url) {
+            return makeImagePreview(image)
+        }
+        if let image = NSImage(contentsOf: url), image.isValid {
+            return makeImagePreview(image)
+        }
+
+        if ext == "rtf" || type?.conforms(to: .rtf) == true,
+           let data = try? Data(contentsOf: url),
+           let attributed = NSAttributedString(rtf: data, documentAttributes: nil) {
+            return makeScrollableTextPreview(attributed)
+        }
+
+        if type?.conforms(to: .html) == true,
+           let data = try? Data(contentsOf: url),
+           let attributed = NSAttributedString(html: data, documentAttributes: nil) {
+            return makeScrollableTextPreview(attributed)
+        }
+
+        if textExtensions.contains(ext) || type?.conforms(to: .plainText) == true || type?.conforms(to: .text) == true,
+           let text = readText(at: url) {
+            return makeScrollableTextPreview(NSAttributedString(string: text))
+        }
+
+        return QuickLookPreviewHostView(url: url)
+    }
+
+    static func attributedString(for url: URL) -> NSAttributedString? {
+        let ext = url.pathExtension.lowercased()
+        let type = contentType(for: url)
+
+        if ext == "rtf" || type?.conforms(to: .rtf) == true,
+           let data = try? Data(contentsOf: url) {
+            return NSAttributedString(rtf: data, documentAttributes: nil)
+        }
+
+        if type?.conforms(to: .html) == true,
+           let data = try? Data(contentsOf: url) {
+            return NSAttributedString(html: data, documentAttributes: nil)
+        }
+
+        if wordProcessorExtensions.contains(ext),
+           let rtfData = rtfData(from: url) {
+            return NSAttributedString(rtf: rtfData, documentAttributes: nil)
+        }
+
+        if let text = readText(at: url) {
+            return NSAttributedString(string: text)
+        }
+
+        if officeExtensions.contains(ext),
+           let text = plainText(from: url) {
+            return NSAttributedString(string: text)
+        }
+
+        return nil
+    }
+
+    static func plainTextForPrinting(from url: URL) -> String? {
+        let ext = url.pathExtension.lowercased()
+        if officeExtensions.contains(ext) || wordProcessorExtensions.contains(ext) {
+            return plainText(from: url)
+        }
+        return readText(at: url)
+    }
+
+    static func readText(at url: URL) -> String? {
+        let encodings: [String.Encoding] = [.utf8, .utf16, .windowsCP1252, .isoLatin1, .macOSRoman]
+        for encoding in encodings {
+            if let text = try? String(contentsOf: url, encoding: encoding),
+               !text.isEmpty {
+                return text
+            }
+        }
+        return nil
+    }
+
+    static func rtfData(from url: URL) -> Data? {
+        convert(from: url, to: "rtf")
+    }
+
+    static func plainText(from url: URL) -> String? {
+        guard let data = convert(from: url, to: "txt") else { return nil }
+        return String(data: data, encoding: .utf8)
+    }
+
+    /// Reads the file in-process (security-scoped access) and pipes bytes to textutil via stdin.
+    private static func convert(from url: URL, to format: String) -> Data? {
+        guard let inputData = try? Data(contentsOf: url), !inputData.isEmpty else { return nil }
+
+        var arguments = ["-convert", format, "-stdin", "-stdout"]
+        let ext = url.pathExtension.lowercased()
+        if !ext.isEmpty {
+            arguments.append(contentsOf: ["-format", ext])
+        }
+
+        let process = Process()
+        process.executableURL = URL(fileURLWithPath: "/usr/bin/textutil")
+        process.arguments = arguments
+
+        let inputPipe = Pipe()
+        let outputPipe = Pipe()
+        process.standardInput = inputPipe
+        process.standardOutput = outputPipe
+        process.standardError = Pipe()
+
+        do {
+            try process.run()
+            let inputHandle = inputPipe.fileHandleForWriting
+            inputHandle.write(inputData)
+            try inputHandle.close()
+            process.waitUntilExit()
+            guard process.terminationStatus == 0 else { return nil }
+            let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
+            return outputData.isEmpty ? nil : outputData
+        } catch {
+            return nil
+        }
+    }
+
+    static func makeScrollableTextPreview(_ text: NSAttributedString) -> NSView {
+        let scrollView = NSScrollView()
+        scrollView.hasVerticalScroller = true
+        scrollView.hasHorizontalScroller = false
+        scrollView.autohidesScrollers = true
+        scrollView.borderType = .noBorder
+        scrollView.drawsBackground = false
+
+        let textView = NSTextView()
+        textView.isEditable = false
+        textView.isSelectable = true
+        textView.drawsBackground = false
+        textView.textStorage?.setAttributedString(text)
+        if textView.font == nil {
+            textView.font = AppTheme.regularFont(size: 13)
+        }
+        textView.textColor = AppTheme.textPrimary
+
+        scrollView.documentView = textView
+        return scrollView
+    }
+
+    private static func makeImagePreview(_ image: NSImage) -> NSView {
+        let scrollView = NSScrollView()
+        scrollView.hasVerticalScroller = true
+        scrollView.hasHorizontalScroller = true
+        scrollView.autohidesScrollers = true
+        scrollView.borderType = .noBorder
+        scrollView.drawsBackground = false
+
+        let imageView = NSImageView()
+        imageView.image = image
+        imageView.imageScaling = .scaleProportionallyDown
+        imageView.imageAlignment = .alignCenter
+        imageView.translatesAutoresizingMaskIntoConstraints = false
+
+        let container = NSView()
+        container.translatesAutoresizingMaskIntoConstraints = false
+        container.addSubview(imageView)
+
+        let width = max(image.size.width, 1)
+        let height = max(image.size.height, 1)
+        NSLayoutConstraint.activate([
+            imageView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+            imageView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+            imageView.topAnchor.constraint(equalTo: container.topAnchor),
+            imageView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
+            container.widthAnchor.constraint(equalToConstant: width),
+            container.heightAnchor.constraint(equalToConstant: height),
+        ])
+
+        scrollView.documentView = container
+        return scrollView
+    }
+}
+
+final class QuickLookPreviewHostView: NSView {
+    private let previewView: QLPreviewView
+
+    init(url: URL) {
+        previewView = QLPreviewView(frame: .zero, style: .normal)!
+        super.init(frame: .zero)
+        translatesAutoresizingMaskIntoConstraints = false
+
+        previewView.translatesAutoresizingMaskIntoConstraints = false
+        previewView.autostarts = true
+        previewView.previewItem = url as NSURL
+
+        addSubview(previewView)
+        NSLayoutConstraint.activate([
+            previewView.leadingAnchor.constraint(equalTo: leadingAnchor),
+            previewView.trailingAnchor.constraint(equalTo: trailingAnchor),
+            previewView.topAnchor.constraint(equalTo: topAnchor),
+            previewView.bottomAnchor.constraint(equalTo: bottomAnchor),
+        ])
+    }
+
+    @available(*, unavailable)
+    required init?(coder: NSCoder) { nil }
+}

+ 3 - 44
smart_printer/FilePreviewView.swift

@@ -185,53 +185,12 @@ final class FilePreviewOverlayView: NSView, AppearanceRefreshable {
     }
 
     private func makePreview(for url: URL) -> NSView {
-        let ext = url.pathExtension.lowercased()
-
-        if ext == "pdf", let document = PDFDocument(url: url) {
-            let pdfView = PDFView()
-            pdfView.document = document
-            pdfView.autoScales = true
-            pdfView.displayMode = .singlePageContinuous
-            pdfView.backgroundColor = .clear
-            return pdfView
+        if let preview = DocumentContentService.makePreviewView(for: url) {
+            return preview
         }
-
-        if ext == "rtf",
-           let data = try? Data(contentsOf: url),
-           let attributed = NSAttributedString(rtf: data, documentAttributes: nil) {
-            return makeTextPreview(attributed)
-        }
-
-        if ["txt", "md", "csv", "json", "xml", "html", "htm"].contains(ext),
-           let text = try? String(contentsOf: url, encoding: .utf8) {
-            return makeTextPreview(NSAttributedString(string: text))
-        }
-
         return makePlaceholderPreview(for: url)
     }
 
-    private func makeTextPreview(_ text: NSAttributedString) -> NSView {
-        let scrollView = NSScrollView()
-        scrollView.hasVerticalScroller = true
-        scrollView.hasHorizontalScroller = false
-        scrollView.autohidesScrollers = true
-        scrollView.borderType = .noBorder
-        scrollView.drawsBackground = false
-
-        let textView = NSTextView()
-        textView.isEditable = false
-        textView.isSelectable = true
-        textView.drawsBackground = false
-        textView.textStorage?.setAttributedString(text)
-        if textView.font == nil {
-            textView.font = AppTheme.regularFont(size: 13)
-        }
-        textView.textColor = AppTheme.textPrimary
-
-        scrollView.documentView = textView
-        return scrollView
-    }
-
     private func makePlaceholderPreview(for url: URL) -> NSView {
         let container = NSView()
 
@@ -272,7 +231,7 @@ final class FilePreviewOverlayView: NSView, AppearanceRefreshable {
 
     private func printCurrentFile() {
         guard let url = currentURL() else { return }
-        PrintService.print(urls: [url], from: window)
+        PrintService.printFile(at: url, from: window)
 
         if currentIndex < urls.count - 1 {
             showFile(at: currentIndex + 1)

+ 192 - 72
smart_printer/PrintService.swift

@@ -1,4 +1,5 @@
 import Cocoa
+import CoreText
 import PDFKit
 import UniformTypeIdentifiers
 
@@ -113,30 +114,13 @@ enum PrintService {
         }
     }
 
-    private static func printFile(at url: URL, from window: NSWindow?) {
-        let type = contentType(for: url) ?? UTType(filenameExtension: url.pathExtension) ?? .data
-
-        if type.conforms(to: .pdf), let document = PDFDocument(url: url) {
-            printPDF(document, title: url.lastPathComponent, from: window)
-        } else if type.conforms(to: .image), let image = NSImage(contentsOf: url) {
-            printImage(image, title: url.lastPathComponent, from: window)
-        } else if let image = NSImage(contentsOf: url) {
-            printImage(image, title: url.lastPathComponent, from: window)
-        } else if type.conforms(to: .plainText) || type.conforms(to: .text),
-                  let text = try? String(contentsOf: url, encoding: .utf8),
-                  let document = pdfDocument(from: text) {
-            printPDF(document, title: url.lastPathComponent, from: window)
-        } else if type.conforms(to: .rtf),
-                  let data = try? Data(contentsOf: url),
-                  let attributed = NSAttributedString(rtf: data, documentAttributes: nil) {
-            printAttributedText(attributed, title: url.lastPathComponent, from: window)
-        } else if type.conforms(to: .html),
-                  let data = try? Data(contentsOf: url),
-                  let attributed = NSAttributedString(html: data, documentAttributes: nil) {
-            printAttributedText(attributed, title: url.lastPathComponent, from: window)
-        } else {
+    static func printFile(at url: URL, from window: NSWindow? = nil) {
+        let title = url.lastPathComponent
+        guard let document = printablePDFDocument(from: url) else {
             showUnsupportedAlert(for: url)
+            return
         }
+        printPDF(document, title: title, from: window ?? NSApp.keyWindow)
     }
 
     private static func contentType(for url: URL) -> UTType? {
@@ -149,9 +133,45 @@ enum PrintService {
         return UTType(filenameExtension: ext)
     }
 
+    private static func printablePDFDocument(from url: URL) -> PDFDocument? {
+        let type = contentType(for: url) ?? UTType(filenameExtension: url.pathExtension) ?? .data
+
+        if type.conforms(to: .pdf), let source = PDFDocument(url: url) {
+            return flattenedPDF(from: source)
+        }
+
+        if type.conforms(to: .image), let image = NSImage(contentsOf: url) {
+            return pdfDocument(from: image)
+        }
+        if let image = NSImage(contentsOf: url), image.isValid {
+            return pdfDocument(from: image)
+        }
+
+        if let text = DocumentContentService.plainTextForPrinting(from: url)?
+            .trimmingCharacters(in: .whitespacesAndNewlines),
+           !text.isEmpty,
+           let document = pdfDocument(from: text) {
+            return document
+        }
+
+        if let attributed = DocumentContentService.attributedString(for: url),
+           !attributed.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
+           let document = pdfDocument(from: attributed) {
+            return document
+        }
+
+        return nil
+    }
+
     private static func printPDF(_ document: PDFDocument, title: String, from window: NSWindow?) {
+        guard let data = document.dataRepresentation(),
+              let printable = PDFDocument(data: data) else {
+            showPrintFailedAlert()
+            return
+        }
+
         let printInfo = configuredPrintInfo()
-        guard let operation = document.printOperation(
+        guard let operation = printable.printOperation(
             for: printInfo,
             scalingMode: .pageScaleToFit,
             autoRotate: false
@@ -170,17 +190,6 @@ enum PrintService {
         printPDF(document, title: title, from: window)
     }
 
-    private static func printAttributedText(_ text: NSAttributedString, title: String, from window: NSWindow?) {
-        let printInfo = configuredPrintInfo()
-        let pageSize = printInfo.paperSize
-        let textView = NSTextView(frame: NSRect(origin: .zero, size: pageSize))
-        textView.textStorage?.setAttributedString(text)
-        textView.isEditable = false
-
-        let operation = NSPrintOperation(view: textView, printInfo: printInfo)
-        runPrintOperation(operation, title: title, from: window)
-    }
-
     private static func runPrintOperation(_ operation: NSPrintOperation, title: String, from window: NSWindow?) {
         operation.showsPrintPanel = true
         operation.showsProgressPanel = false
@@ -278,10 +287,6 @@ enum PrintService {
     }
 
     private static func pdfDocument(from text: String) -> PDFDocument? {
-        let printInfo = configuredPrintInfo()
-        let pageSize = printInfo.paperSize
-        guard pageSize.width > 0, pageSize.height > 0 else { return nil }
-
         let paragraphStyle = NSMutableParagraphStyle()
         paragraphStyle.alignment = .left
         paragraphStyle.lineBreakMode = .byWordWrapping
@@ -291,81 +296,196 @@ enum PrintService {
             .foregroundColor: NSColor.black,
             .paragraphStyle: paragraphStyle,
         ]
-        let attributed = NSAttributedString(string: text, attributes: attributes)
+        return pdfDocument(from: NSAttributedString(string: text, attributes: attributes))
+    }
 
-        let textStorage = NSTextStorage(attributedString: attributed)
-        let layoutManager = NSLayoutManager()
-        textStorage.addLayoutManager(layoutManager)
+    private static func pdfDocument(from attributed: NSAttributedString) -> PDFDocument? {
+        let normalized = attributedStringForPrinting(attributed)
+        guard normalized.length > 0 else { return nil }
+
+        if let document = pdfDocumentUsingRenderedImages(from: normalized) {
+            return document
+        }
+        return pdfDocumentUsingCoreText(from: normalized)
+    }
+
+    private static func attributedStringForPrinting(_ source: NSAttributedString) -> NSAttributedString {
+        let mutable = NSMutableAttributedString(attributedString: source)
+        let fullRange = NSRange(location: 0, length: mutable.length)
+        guard fullRange.length > 0 else { return mutable }
+
+        mutable.addAttribute(.foregroundColor, value: NSColor.black, range: fullRange)
+        mutable.enumerateAttribute(.font, in: fullRange) { value, range, _ in
+            if value == nil {
+                mutable.addAttribute(.font, value: NSFont.systemFont(ofSize: 12), range: range)
+            }
+        }
+        return mutable
+    }
+
+    private static func pdfDocumentUsingCoreText(from attributed: NSAttributedString) -> PDFDocument? {
+        let printInfo = configuredPrintInfo()
+        let pageSize = printInfo.paperSize
+        guard pageSize.width > 0, pageSize.height > 0 else { return nil }
+
+        let frameRect = CGRect(
+            x: printInfo.leftMargin,
+            y: printInfo.bottomMargin,
+            width: pageSize.width - printInfo.leftMargin - printInfo.rightMargin,
+            height: pageSize.height - printInfo.topMargin - printInfo.bottomMargin
+        )
+
+        let pdfData = NSMutableData()
+        var mediaBox = CGRect(origin: .zero, size: pageSize)
+        guard let consumer = CGDataConsumer(data: pdfData as CFMutableData),
+              let context = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
+            return nil
+        }
+
+        let framesetter = CTFramesetterCreateWithAttributedString(attributed as CFAttributedString)
+        var currentIndex = 0
+        var pageCount = 0
+
+        while currentIndex < attributed.length {
+            context.beginPDFPage(nil)
+
+            let path = CGPath(rect: frameRect, transform: nil)
+            let range = CFRange(location: currentIndex, length: attributed.length - currentIndex)
+            let frame = CTFramesetterCreateFrame(framesetter, range, path, nil)
+            CTFrameDraw(frame, context)
+
+            let visibleRange = CTFrameGetVisibleStringRange(frame)
+            context.endPDFPage()
+            pageCount += 1
+
+            guard visibleRange.length > 0 else { break }
+            currentIndex += visibleRange.length
+        }
+
+        context.closePDF()
+        guard pageCount > 0 else { return nil }
+        return PDFDocument(data: pdfData as Data)
+    }
+
+    /// Renders each text page to a bitmap and embeds it in the PDF, matching the photo print pipeline.
+    private static func pdfDocumentUsingRenderedImages(from attributed: NSAttributedString) -> PDFDocument? {
+        let printInfo = configuredPrintInfo()
+        let pageSize = printInfo.paperSize
+        guard pageSize.width > 0, pageSize.height > 0 else { return nil }
 
         let textWidth = pageSize.width - printInfo.leftMargin - printInfo.rightMargin
         let textHeight = pageSize.height - printInfo.topMargin - printInfo.bottomMargin
         let containerSize = NSSize(width: textWidth, height: textHeight)
 
+        let textStorage = NSTextStorage(attributedString: attributed)
+        let layoutManager = NSLayoutManager()
+        textStorage.addLayoutManager(layoutManager)
+
         let pdfData = NSMutableData()
         var mediaBox = CGRect(origin: .zero, size: pageSize)
-
         guard let consumer = CGDataConsumer(data: pdfData as CFMutableData),
               let context = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
             return nil
         }
 
+        var pageCount = 0
         while true {
             let textContainer = NSTextContainer(size: containerSize)
             textContainer.lineFragmentPadding = 0
             layoutManager.addTextContainer(textContainer)
+            layoutManager.ensureLayout(for: textContainer)
 
             let glyphRange = layoutManager.glyphRange(for: textContainer)
             guard glyphRange.length > 0 else { break }
 
             let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
             let pageText = attributed.attributedSubstring(from: charRange)
-
-            context.beginPDFPage(nil)
-            drawTextPage(
-                pageText,
-                in: context,
+            let pageImage = renderTextPageImage(
+                pageText: pageText,
                 pageSize: pageSize,
-                printInfo: printInfo,
-                textWidth: textWidth,
-                textHeight: textHeight
+                printInfo: printInfo
             )
+
+            context.beginPDFPage(nil)
+            if let cgImage = pageImage.cgImage(forProposedRect: nil, context: nil, hints: nil) {
+                context.draw(cgImage, in: CGRect(origin: .zero, size: pageSize))
+            }
             context.endPDFPage()
+            pageCount += 1
 
             if NSMaxRange(glyphRange) >= layoutManager.numberOfGlyphs { break }
         }
 
         context.closePDF()
+        guard pageCount > 0 else { return nil }
         return PDFDocument(data: pdfData as Data)
     }
 
-    private static func drawTextPage(
-        _ text: NSAttributedString,
-        in context: CGContext,
+    private static func renderTextPageImage(
+        pageText: NSAttributedString,
         pageSize: NSSize,
-        printInfo: NSPrintInfo,
-        textWidth: CGFloat,
-        textHeight: CGFloat
-    ) {
-        context.saveGState()
-        context.translateBy(x: 0, y: pageSize.height)
-        context.scaleBy(x: 1, y: -1)
-
-        let drawRect = CGRect(
+        printInfo: NSPrintInfo
+    ) -> NSImage {
+        let textRect = NSRect(
             x: printInfo.leftMargin,
-            y: printInfo.topMargin,
-            width: textWidth,
-            height: textHeight
+            y: printInfo.bottomMargin,
+            width: pageSize.width - printInfo.leftMargin - printInfo.rightMargin,
+            height: pageSize.height - printInfo.topMargin - printInfo.bottomMargin
         )
 
-        NSGraphicsContext.saveGraphicsState()
-        NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: true)
-        text.draw(
-            with: drawRect,
+        let image = NSImage(size: pageSize)
+        image.lockFocus()
+        NSColor.white.setFill()
+        NSRect(origin: .zero, size: pageSize).fill()
+        pageText.draw(
+            with: textRect,
             options: [.usesLineFragmentOrigin, .usesFontLeading]
         )
-        NSGraphicsContext.restoreGraphicsState()
+        image.unlockFocus()
+        return image
+    }
+
+    private static func flattenedPDF(from source: PDFDocument) -> PDFDocument? {
+        guard source.pageCount > 0 else { return nil }
+
+        let printInfo = configuredPrintInfo()
+        let pageSize = printInfo.paperSize
+        guard pageSize.width > 0, pageSize.height > 0 else { return nil }
 
-        context.restoreGState()
+        let pdfData = NSMutableData()
+        var mediaBox = CGRect(origin: .zero, size: pageSize)
+
+        guard let consumer = CGDataConsumer(data: pdfData as CFMutableData),
+              let context = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
+            return nil
+        }
+
+        for index in 0..<source.pageCount {
+            guard let page = source.page(at: index) else { continue }
+            let pageBounds = page.bounds(for: .mediaBox)
+            guard pageBounds.width > 0, pageBounds.height > 0 else { continue }
+
+            context.beginPDFPage(nil)
+            context.saveGState()
+
+            let scale = min(pageSize.width / pageBounds.width, pageSize.height / pageBounds.height)
+            let drawWidth = pageBounds.width * scale
+            let drawHeight = pageBounds.height * scale
+            let offsetX = (pageSize.width - drawWidth) / 2
+            let offsetY = (pageSize.height - drawHeight) / 2
+
+            context.translateBy(x: offsetX, y: offsetY)
+            context.scaleBy(x: scale, y: scale)
+            context.translateBy(x: -pageBounds.origin.x, y: -pageBounds.origin.y)
+            page.draw(with: .mediaBox, to: context)
+
+            context.restoreGState()
+            context.endPDFPage()
+        }
+
+        context.closePDF()
+        guard !(pdfData as Data).isEmpty else { return nil }
+        return PDFDocument(data: pdfData as Data)
     }
 
     private static func pdfDocument(from image: NSImage) -> PDFDocument? {