| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 |
- import Cocoa
- import PDFKit
- import UniformTypeIdentifiers
- enum PrintService {
- static func printContacts(_ contacts: [PrintableContact], from window: NSWindow? = nil) {
- guard !contacts.isEmpty else {
- showNoContactsAlert()
- return
- }
- let text = formattedContactsText(contacts)
- guard let document = pdfDocument(from: text) else {
- showPrintFailedAlert()
- return
- }
- printPDF(document, title: "Print Contacts", from: window ?? NSApp.keyWindow)
- }
- static func printCanvas(_ image: NSImage, from window: NSWindow? = nil) {
- guard image.size.width > 0, image.size.height > 0 else {
- showEmptyCanvasAlert()
- return
- }
- printImage(image, title: "Blank Canvas", from: window)
- }
- static func printPhoto(_ image: NSImage, title: String = "Photo", from window: NSWindow? = nil) {
- guard image.size.width > 0, image.size.height > 0 else {
- showPrintFailedAlert()
- return
- }
- printImage(image, title: title, from: window)
- }
- static func saveCanvasPDF(_ image: NSImage) {
- guard image.size.width > 0, image.size.height > 0 else {
- showEmptyCanvasAlert()
- return
- }
- guard let document = pdfDocument(from: image) else {
- showSaveFailedAlert()
- return
- }
- let panel = NSSavePanel()
- panel.title = "Save as PDF"
- panel.message = "Choose where to save your canvas."
- panel.prompt = "Save"
- panel.nameFieldStringValue = "Blank Canvas.pdf"
- panel.allowedContentTypes = [.pdf]
- panel.canCreateDirectories = true
- guard panel.runModal() == .OK, let url = panel.url else { return }
- guard document.write(to: url) else {
- showSaveFailedAlert()
- return
- }
- }
- static func printText(_ text: String, title: String = "Print Text", from window: NSWindow? = nil) {
- let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !trimmed.isEmpty else {
- showEmptyTextAlert()
- return
- }
- guard let document = pdfDocument(from: trimmed) else {
- showPrintFailedAlert()
- return
- }
- printPDF(document, title: title, from: window ?? NSApp.keyWindow)
- }
- static func print(urls: [URL], from window: NSWindow? = nil) {
- guard !urls.isEmpty else { return }
- let hostWindow = window ?? NSApp.keyWindow
- for url in urls {
- let accessed = url.startAccessingSecurityScopedResource()
- defer {
- if accessed { url.stopAccessingSecurityScopedResource() }
- }
- printFile(at: url, from: hostWindow)
- }
- }
- 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 {
- showUnsupportedAlert(for: url)
- }
- }
- private 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)
- }
- private static func printPDF(_ document: PDFDocument, title: String, from window: NSWindow?) {
- let printInfo = configuredPrintInfo()
- guard let operation = document.printOperation(
- for: printInfo,
- scalingMode: .pageScaleToFit,
- autoRotate: false
- ) else {
- showPrintFailedAlert()
- return
- }
- runPrintOperation(operation, title: title, from: window)
- }
- private static func printImage(_ image: NSImage, title: String, from window: NSWindow?) {
- guard let document = pdfDocument(from: image) else {
- showPrintFailedAlert()
- return
- }
- 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
- operation.jobTitle = title
- operation.printPanel.options.insert([
- .showsCopies,
- .showsPageRange,
- .showsPaperSize,
- .showsOrientation,
- .showsScaling,
- ])
- NSApp.activate(ignoringOtherApps: true)
- let parentWindow = window ?? NSApp.keyWindow ?? NSApp.mainWindow
- parentWindow?.makeKeyAndOrderFront(nil)
- let observer = observePrintPanelAppearance(relativeTo: parentWindow)
- defer { NotificationCenter.default.removeObserver(observer) }
- if let parentWindow {
- operation.runModal(for: parentWindow, delegate: nil, didRun: nil, contextInfo: nil)
- } else {
- _ = operation.run()
- }
- }
- private static func observePrintPanelAppearance(relativeTo parentWindow: NSWindow?) -> NSObjectProtocol {
- NotificationCenter.default.addObserver(
- forName: NSWindow.didBecomeKeyNotification,
- object: nil,
- queue: .main
- ) { notification in
- guard let panelWindow = notification.object as? NSWindow,
- isPrintPanelWindow(panelWindow) else { return }
- configurePrintPanelWindow(panelWindow, relativeTo: parentWindow)
- DispatchQueue.main.async {
- configurePrintPanelWindow(panelWindow, relativeTo: parentWindow)
- }
- }
- }
- private static func configurePrintPanelWindow(_ panelWindow: NSWindow, relativeTo parentWindow: NSWindow?) {
- hideTrafficLights(on: panelWindow)
- if let parentWindow {
- center(panelWindow, relativeTo: parentWindow)
- }
- }
- private static func center(_ panelWindow: NSWindow, relativeTo parentWindow: NSWindow) {
- let parentFrame = parentWindow.frame
- var panelFrame = panelWindow.frame
- panelFrame.origin.x = parentFrame.midX - panelFrame.width / 2
- panelFrame.origin.y = parentFrame.midY - panelFrame.height / 2
- panelWindow.setFrame(panelFrame, display: true)
- }
- private static func isPrintPanelWindow(_ window: NSWindow) -> Bool {
- let className = String(describing: type(of: window))
- if className.localizedCaseInsensitiveContains("printpanel") {
- return true
- }
- return window.title == "Print"
- }
- private static func hideTrafficLights(on window: NSWindow) {
- for buttonType: NSWindow.ButtonType in [.closeButton, .miniaturizeButton, .zoomButton] {
- window.standardWindowButton(buttonType)?.isHidden = true
- }
- }
- private static func configuredPrintInfo() -> NSPrintInfo {
- let printInfo = (NSPrintInfo.shared.copy() as? NSPrintInfo) ?? NSPrintInfo()
- let printerName = AppSettings.effectiveDefaultPrinter
- if let printer = NSPrinter(name: printerName),
- NSPrinter.printerNames.contains(printerName) {
- printInfo.printer = printer
- }
- switch AppSettings.defaultPaperSize {
- case .a4: printInfo.paperName = NSPrinter.PaperName("iso-a4")
- case .letter: printInfo.paperName = NSPrinter.PaperName("na-letter")
- case .legal: printInfo.paperName = NSPrinter.PaperName("na-legal")
- }
- printInfo.horizontalPagination = .fit
- printInfo.verticalPagination = .fit
- printInfo.isHorizontallyCentered = true
- printInfo.isVerticallyCentered = false
- printInfo.topMargin = 36
- printInfo.bottomMargin = 36
- printInfo.leftMargin = 36
- printInfo.rightMargin = 36
- return printInfo
- }
- 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
- let attributes: [NSAttributedString.Key: Any] = [
- .font: NSFont.systemFont(ofSize: 12),
- .foregroundColor: NSColor.black,
- .paragraphStyle: paragraphStyle,
- ]
- let attributed = NSAttributedString(string: text, attributes: attributes)
- let textStorage = NSTextStorage(attributedString: attributed)
- let layoutManager = NSLayoutManager()
- textStorage.addLayoutManager(layoutManager)
- let textWidth = pageSize.width - printInfo.leftMargin - printInfo.rightMargin
- let textHeight = pageSize.height - printInfo.topMargin - printInfo.bottomMargin
- let containerSize = NSSize(width: textWidth, height: textHeight)
- 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
- }
- while true {
- let textContainer = NSTextContainer(size: containerSize)
- textContainer.lineFragmentPadding = 0
- layoutManager.addTextContainer(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,
- pageSize: pageSize,
- printInfo: printInfo,
- textWidth: textWidth,
- textHeight: textHeight
- )
- context.endPDFPage()
- if NSMaxRange(glyphRange) >= layoutManager.numberOfGlyphs { break }
- }
- context.closePDF()
- return PDFDocument(data: pdfData as Data)
- }
- private static func drawTextPage(
- _ text: NSAttributedString,
- in context: CGContext,
- 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(
- x: printInfo.leftMargin,
- y: printInfo.topMargin,
- width: textWidth,
- height: textHeight
- )
- NSGraphicsContext.saveGraphicsState()
- NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: true)
- text.draw(
- with: drawRect,
- options: [.usesLineFragmentOrigin, .usesFontLeading]
- )
- NSGraphicsContext.restoreGraphicsState()
- context.restoreGState()
- }
- private static func pdfDocument(from image: NSImage) -> PDFDocument? {
- let printInfo = configuredPrintInfo()
- let pageSize = printInfo.paperSize
- guard pageSize.width > 0, pageSize.height > 0 else { return nil }
- 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
- }
- context.beginPDFPage(nil)
- let imageSize = image.size
- if imageSize.width > 0, imageSize.height > 0,
- let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) {
- let scale = min(pageSize.width / imageSize.width, pageSize.height / imageSize.height)
- let drawSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale)
- let drawRect = CGRect(
- x: (pageSize.width - drawSize.width) / 2,
- y: (pageSize.height - drawSize.height) / 2,
- width: drawSize.width,
- height: drawSize.height
- )
- context.draw(cgImage, in: drawRect)
- }
- context.endPDFPage()
- context.closePDF()
- return PDFDocument(data: pdfData as Data)
- }
- private static func formattedContactsText(_ contacts: [PrintableContact]) -> String {
- contacts.map { contact in
- var lines = [contact.displayName]
- if let organization = contact.organization, !organization.isEmpty,
- organization.caseInsensitiveCompare(contact.displayName) != .orderedSame {
- lines.append("Organization: \(organization)")
- }
- for phone in contact.phoneNumbers {
- lines.append("Phone: \(phone)")
- }
- for email in contact.emailAddresses {
- lines.append("Email: \(email)")
- }
- for address in contact.postalAddresses {
- lines.append("Address:\n\(address)")
- }
- return lines.joined(separator: "\n")
- }
- .joined(separator: "\n\n" + String(repeating: "─", count: 40) + "\n\n")
- }
- private static func showNoContactsAlert() {
- let alert = NSAlert()
- alert.messageText = "No Contacts Selected"
- alert.informativeText = "Select at least one contact to print."
- alert.alertStyle = .informational
- alert.addButton(withTitle: "OK")
- alert.runModal()
- }
- private static func showPrintFailedAlert() {
- let alert = NSAlert()
- alert.messageText = "Print Failed"
- alert.informativeText = "The text could not be prepared for printing."
- alert.alertStyle = .warning
- alert.addButton(withTitle: "OK")
- alert.runModal()
- }
- private static func showSaveFailedAlert() {
- let alert = NSAlert()
- alert.messageText = "Save Failed"
- alert.informativeText = "The canvas could not be saved as a PDF."
- alert.alertStyle = .warning
- alert.addButton(withTitle: "OK")
- alert.runModal()
- }
- static func showEmptyCanvasAlert() {
- let alert = NSAlert()
- alert.messageText = "Nothing to Print"
- alert.informativeText = "Add a drawing, text, or image to the canvas before printing."
- alert.alertStyle = .informational
- alert.addButton(withTitle: "OK")
- alert.runModal()
- }
- private static func showEmptyTextAlert() {
- let alert = NSAlert()
- alert.messageText = "No Text to Print"
- alert.informativeText = "Enter some text before printing."
- alert.alertStyle = .informational
- alert.addButton(withTitle: "OK")
- alert.runModal()
- }
- private static func showUnsupportedAlert(for url: URL) {
- let alert = NSAlert()
- alert.messageText = "Unsupported File"
- alert.informativeText = "\"\(url.lastPathComponent)\" cannot be printed."
- alert.alertStyle = .warning
- alert.addButton(withTitle: "OK")
- alert.runModal()
- }
- }
|