ソースを参照

Enable sandboxed printing with the macOS print dialog.

Add the print entitlement and route photos through PDF print operations so the system print panel opens reliably from the photo preview.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 時間 前
コミット
904d792357
共有4 個のファイルを変更した102 個の追加26 個の削除を含む
  1. 1 1
      smart_printer/DocumentPickerService.swift
  2. 1 1
      smart_printer/PhotoPreviewView.swift
  3. 98 24
      smart_printer/PrintService.swift
  4. 2 0
      smart_printer/smart_printer.entitlements

+ 1 - 1
smart_printer/DocumentPickerService.swift

@@ -131,7 +131,7 @@ enum DocumentPickerService {
131 131
                 PhotoPreviewService.present(urls: photos, from: window)
132 132
             }
133 133
             if !documents.isEmpty {
134
-                PrintService.print(urls: documents)
134
+                PrintService.print(urls: documents, from: window)
135 135
             }
136 136
         case .importFile:
137 137
             let added = ImportedFilesStore.add(urls: matched)

+ 1 - 1
smart_printer/PhotoPreviewView.swift

@@ -207,7 +207,7 @@ final class PhotoPreviewOverlayView: NSView, AppearanceRefreshable {
207 207
 
208 208
     private func printCurrentPhoto() {
209 209
         guard let url = currentURL() else { return }
210
-        PrintService.print(urls: [url])
210
+        PrintService.print(urls: [url], from: window)
211 211
 
212 212
         if currentIndex < urls.count - 1 {
213 213
             showPhoto(at: currentIndex + 1)

+ 98 - 24
smart_printer/PrintService.swift

@@ -3,7 +3,8 @@ import PDFKit
3 3
 import UniformTypeIdentifiers
4 4
 
5 5
 enum PrintService {
6
-    static func print(urls: [URL]) {
6
+    static func print(urls: [URL], from window: NSWindow? = nil) {
7
+        _ = window
7 8
         guard !urls.isEmpty else { return }
8 9
 
9 10
         for url in urls {
@@ -16,72 +17,145 @@ enum PrintService {
16 17
     }
17 18
 
18 19
     private static func printFile(at url: URL) {
19
-        let type = UTType(filenameExtension: url.pathExtension) ?? .data
20
+        let type = contentType(for: url) ?? UTType(filenameExtension: url.pathExtension) ?? .data
20 21
 
21 22
         if type.conforms(to: .pdf), let document = PDFDocument(url: url) {
22
-            printPDF(document)
23
+            printPDF(document, title: url.lastPathComponent)
23 24
         } else if type.conforms(to: .image), let image = NSImage(contentsOf: url) {
24
-            printImage(image)
25
+            printImage(image, title: url.lastPathComponent)
26
+        } else if let image = NSImage(contentsOf: url) {
27
+            printImage(image, title: url.lastPathComponent)
25 28
         } else if type.conforms(to: .plainText) || type.conforms(to: .text),
26 29
                   let text = try? String(contentsOf: url, encoding: .utf8) {
27
-            printText(text)
30
+            printText(text, title: url.lastPathComponent)
28 31
         } else if type.conforms(to: .rtf),
29 32
                   let data = try? Data(contentsOf: url),
30 33
                   let attributed = NSAttributedString(rtf: data, documentAttributes: nil) {
31
-            printAttributedText(attributed)
34
+            printAttributedText(attributed, title: url.lastPathComponent)
32 35
         } else if type.conforms(to: .html),
33 36
                   let data = try? Data(contentsOf: url),
34 37
                   let attributed = NSAttributedString(html: data, documentAttributes: nil) {
35
-            printAttributedText(attributed)
38
+            printAttributedText(attributed, title: url.lastPathComponent)
36 39
         } else {
37 40
             showUnsupportedAlert(for: url)
38 41
         }
39 42
     }
40 43
 
41
-    private static func printPDF(_ document: PDFDocument) {
44
+    private static func contentType(for url: URL) -> UTType? {
45
+        if let values = try? url.resourceValues(forKeys: [.contentTypeKey]),
46
+           let type = values.contentType {
47
+            return type
48
+        }
49
+        let ext = url.pathExtension.lowercased()
50
+        guard !ext.isEmpty else { return nil }
51
+        return UTType(filenameExtension: ext)
52
+    }
53
+
54
+    private static func printPDF(_ document: PDFDocument, title: String) {
42 55
         let printInfo = configuredPrintInfo()
43 56
         guard let operation = document.printOperation(
44 57
             for: printInfo,
45 58
             scalingMode: .pageScaleToFit,
46 59
             autoRotate: true
47 60
         ) else { return }
48
-        operation.run()
61
+        runPrintOperation(operation, title: title)
49 62
     }
50 63
 
51
-    private static func printImage(_ image: NSImage) {
52
-        let size = image.size
53
-        guard size.width > 0, size.height > 0 else { return }
54
-
55
-        let imageView = NSImageView(frame: NSRect(origin: .zero, size: size))
56
-        imageView.image = image
57
-        imageView.imageScaling = .scaleProportionallyUpOrDown
58
-
59
-        let operation = NSPrintOperation(view: imageView, printInfo: configuredPrintInfo())
60
-        operation.run()
64
+    private static func printImage(_ image: NSImage, title: String) {
65
+        guard let document = pdfDocument(from: image) else { return }
66
+        printPDF(document, title: title)
61 67
     }
62 68
 
63
-    private static func printText(_ text: String) {
64
-        printAttributedText(NSAttributedString(string: text))
69
+    private static func printText(_ text: String, title: String) {
70
+        printAttributedText(NSAttributedString(string: text), title: title)
65 71
     }
66 72
 
67
-    private static func printAttributedText(_ text: NSAttributedString) {
68
-        let textView = NSTextView(frame: NSRect(x: 0, y: 0, width: 612, height: 792))
73
+    private static func printAttributedText(_ text: NSAttributedString, title: String) {
74
+        let printInfo = configuredPrintInfo()
75
+        let pageSize = printInfo.paperSize
76
+        let textView = NSTextView(frame: NSRect(origin: .zero, size: pageSize))
69 77
         textView.textStorage?.setAttributedString(text)
70 78
         textView.isEditable = false
71 79
 
72
-        let operation = NSPrintOperation(view: textView, printInfo: configuredPrintInfo())
80
+        let operation = NSPrintOperation(view: textView, printInfo: printInfo)
81
+        runPrintOperation(operation, title: title)
82
+    }
83
+
84
+    private static func runPrintOperation(_ operation: NSPrintOperation, title: String) {
85
+        operation.showsPrintPanel = true
86
+        operation.showsProgressPanel = true
87
+        operation.jobTitle = title
88
+        operation.printPanel.options.insert([
89
+            .showsCopies,
90
+            .showsPageRange,
91
+            .showsPaperSize,
92
+            .showsOrientation,
93
+            .showsScaling,
94
+        ])
73 95
         operation.run()
74 96
     }
75 97
 
76 98
     private static func configuredPrintInfo() -> NSPrintInfo {
77 99
         let printInfo = NSPrintInfo.shared.copy() as! NSPrintInfo
100
+
78 101
         let printerName = AppSettings.effectiveDefaultPrinter
79 102
         if let printer = NSPrinter(name: printerName) {
80 103
             printInfo.printer = printer
81 104
         }
105
+
106
+        switch AppSettings.defaultPaperSize {
107
+        case .a4: printInfo.paperName = NSPrinter.PaperName("iso-a4")
108
+        case .letter: printInfo.paperName = NSPrinter.PaperName("na-letter")
109
+        case .legal: printInfo.paperName = NSPrinter.PaperName("na-legal")
110
+        }
111
+
112
+        printInfo.horizontalPagination = .fit
113
+        printInfo.verticalPagination = .fit
114
+        printInfo.isHorizontallyCentered = true
115
+        printInfo.isVerticallyCentered = true
116
+        printInfo.topMargin = 36
117
+        printInfo.bottomMargin = 36
118
+        printInfo.leftMargin = 36
119
+        printInfo.rightMargin = 36
120
+
82 121
         return printInfo
83 122
     }
84 123
 
124
+    private static func pdfDocument(from image: NSImage) -> PDFDocument? {
125
+        let printInfo = configuredPrintInfo()
126
+        let pageSize = printInfo.paperSize
127
+        guard pageSize.width > 0, pageSize.height > 0 else { return nil }
128
+
129
+        let pdfData = NSMutableData()
130
+        var mediaBox = CGRect(origin: .zero, size: pageSize)
131
+
132
+        guard let consumer = CGDataConsumer(data: pdfData as CFMutableData),
133
+              let context = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
134
+            return nil
135
+        }
136
+
137
+        context.beginPDFPage(nil)
138
+
139
+        let imageSize = image.size
140
+        if imageSize.width > 0, imageSize.height > 0,
141
+           let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) {
142
+            let scale = min(pageSize.width / imageSize.width, pageSize.height / imageSize.height)
143
+            let drawSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
144
+            let drawRect = CGRect(
145
+                x: (pageSize.width - drawSize.width) / 2,
146
+                y: (pageSize.height - drawSize.height) / 2,
147
+                width: drawSize.width,
148
+                height: drawSize.height
149
+            )
150
+            context.draw(cgImage, in: drawRect)
151
+        }
152
+
153
+        context.endPDFPage()
154
+        context.closePDF()
155
+
156
+        return PDFDocument(data: pdfData as Data)
157
+    }
158
+
85 159
     private static func showUnsupportedAlert(for url: URL) {
86 160
         let alert = NSAlert()
87 161
         alert.messageText = "Unsupported File"

+ 2 - 0
smart_printer/smart_printer.entitlements

@@ -8,5 +8,7 @@
8 8
 	<true/>
9 9
 	<key>com.apple.security.network.client</key>
10 10
 	<true/>
11
+	<key>com.apple.security.print</key>
12
+	<true/>
11 13
 </dict>
12 14
 </plist>