| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948 |
- import Cocoa
- import CoreText
- 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 saveAttributedTextAsPDF(
- _ attributed: NSAttributedString,
- defaultName: String = "Scan",
- from window: NSWindow? = nil
- ) {
- guard hasPrintableContent(attributed) else {
- showEmptyTextAlert()
- return
- }
- guard let document = pdfDocument(from: attributed) else {
- showSaveFailedAlert()
- return
- }
- let panel = NSSavePanel()
- panel.title = "Save as PDF"
- panel.message = "Choose where to save your document."
- panel.prompt = "Save"
- panel.nameFieldStringValue = "\(defaultName).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 printAttributedText(
- _ attributed: NSAttributedString,
- title: String = "Scan",
- from window: NSWindow? = nil
- ) {
- guard hasPrintableContent(attributed) else {
- showEmptyTextAlert()
- return
- }
- guard let document = pdfDocument(from: attributed) else {
- showPrintFailedAlert()
- return
- }
- printPDF(document, title: title, from: window ?? NSApp.keyWindow)
- }
- static func saveParsedScansAsPDF(
- pages: [ParsedScanPage],
- defaultName: String = "Scan",
- from window: NSWindow? = nil
- ) {
- guard !pages.isEmpty else {
- showEmptyTextAlert()
- return
- }
- guard let document = pdfDocument(from: pages) else {
- showSaveFailedAlert()
- return
- }
- let panel = NSSavePanel()
- panel.title = "Save as PDF"
- panel.message = "Choose where to save your document."
- panel.prompt = "Save"
- panel.nameFieldStringValue = "\(defaultName).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 printParsedScans(
- pages: [ParsedScanPage],
- title: String = "Scan",
- from window: NSWindow? = nil
- ) {
- guard !pages.isEmpty else {
- showEmptyTextAlert()
- return
- }
- guard let document = pdfDocument(from: pages) else {
- showPrintFailedAlert()
- return
- }
- printPDF(document, title: title, from: window ?? NSApp.keyWindow)
- }
- static func saveTextAsPDF(
- _ text: String,
- defaultName: String = "Scan",
- from window: NSWindow? = nil
- ) {
- let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !trimmed.isEmpty else {
- showEmptyTextAlert()
- return
- }
- guard let document = pdfDocument(from: trimmed) else {
- showSaveFailedAlert()
- return
- }
- let panel = NSSavePanel()
- panel.title = "Save as PDF"
- panel.message = "Choose where to save your document."
- panel.prompt = "Save"
- panel.nameFieldStringValue = "\(defaultName).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 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)
- }
- }
- 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? {
- 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 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 = printable.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 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 {
- let callback = PrintRatingCallback(window: parentWindow)
- operation.runModal(
- for: parentWindow,
- delegate: callback,
- didRun: #selector(PrintRatingCallback.printOperationDidRun(_:success:contextInfo:)),
- contextInfo: nil
- )
- withExtendedLifetime(callback) {}
- } else {
- operation.run()
- }
- }
- private final class PrintRatingCallback: NSObject {
- private let window: NSWindow?
- init(window: NSWindow?) {
- self.window = window
- }
- @objc func printOperationDidRun(
- _ printOperation: NSPrintOperation,
- success: Bool,
- contextInfo: UnsafeMutableRawPointer?
- ) {
- guard success else { return }
- Task { @MainActor in
- AppRatingManager.shared.recordSuccessfulPrint(from: window)
- }
- }
- }
- 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 hasPrintableContent(_ attributed: NSAttributedString) -> Bool {
- if !attributed.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
- return true
- }
- var hasAttachment = false
- attributed.enumerateAttribute(.attachment, in: NSRange(location: 0, length: attributed.length)) { value, _, stop in
- if value != nil {
- hasAttachment = true
- stop.pointee = true
- }
- }
- return hasAttachment
- }
- private static let scanFigureFraction: CGFloat = 0.52
- private static func pdfDocument(from pages: [ParsedScanPage]) -> PDFDocument? {
- guard !pages.isEmpty else { return nil }
- let combined = PDFDocument()
- for page in pages {
- guard let pdfPage = pdfPage(from: page) else { continue }
- combined.insert(pdfPage, at: combined.pageCount)
- }
- return combined.pageCount > 0 ? combined : nil
- }
- private static func pdfPage(from page: ParsedScanPage) -> PDFPage? {
- let bodyEmpty = page.body.plainText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
- let captionEmpty = page.caption?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true
- if bodyEmpty && captionEmpty {
- return PDFPage(image: page.picture)
- }
- let printInfo = configuredPrintInfo()
- let pageSize = printInfo.paperSize
- guard pageSize.width > 0, pageSize.height > 0 else { return PDFPage(image: page.picture) }
- let contentWidth = pageSize.width - printInfo.leftMargin - printInfo.rightMargin
- let contentHeight = 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 PDFPage(image: page.picture)
- }
- appendParsedScanPage(
- page,
- to: context,
- pageSize: pageSize,
- printInfo: printInfo,
- contentWidth: contentWidth,
- contentHeight: contentHeight
- )
- context.closePDF()
- guard let document = PDFDocument(data: pdfData as Data) else {
- return PDFPage(image: page.picture)
- }
- return document.page(at: 0)
- }
- private static func appendParsedScanPage(
- _ page: ParsedScanPage,
- to context: CGContext,
- pageSize: NSSize,
- printInfo: NSPrintInfo,
- contentWidth: CGFloat,
- contentHeight: CGFloat
- ) {
- let bodyText = page.body.plainText.trimmingCharacters(in: .whitespacesAndNewlines)
- let captionText = page.caption?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
- let hasCaption = !captionText.isEmpty
- let hasBody = !bodyText.isEmpty
- let showsImage = page.showsPicture
- let imageFraction: CGFloat
- let captionFraction: CGFloat
- let textFraction: CGFloat
- if showsImage {
- imageFraction = hasBody || hasCaption ? 0.48 : 1.0
- captionFraction = hasCaption ? 0.05 : 0
- textFraction = hasBody ? max(0, 1 - imageFraction - captionFraction) : 0
- } else {
- imageFraction = 0
- captionFraction = hasCaption ? 0.05 : 0
- textFraction = hasBody ? max(0, 1 - captionFraction) : 0
- }
- let imageHeight = contentHeight * imageFraction
- let captionHeight = contentHeight * captionFraction
- let textHeight = contentHeight * textFraction
- context.beginPDFPage(nil)
- var cursorY = printInfo.bottomMargin + captionHeight + textHeight
- if showsImage {
- let imageRect = CGRect(
- x: printInfo.leftMargin,
- y: cursorY,
- width: contentWidth,
- height: imageHeight
- )
- drawScanImage(page.picture, in: imageRect, context: context)
- }
- if hasCaption {
- cursorY = printInfo.bottomMargin + textHeight
- let captionRect = CGRect(
- x: printInfo.leftMargin,
- y: cursorY,
- width: contentWidth,
- height: captionHeight
- )
- drawScanText(captionText, in: captionRect, context: context, pageSize: pageSize, alignment: .center)
- }
- if hasBody {
- switch page.body {
- case .single(let text):
- let textRect = CGRect(
- x: printInfo.leftMargin,
- y: printInfo.bottomMargin,
- width: contentWidth,
- height: textHeight
- )
- drawScanAttributedText(text, in: textRect, context: context, pageSize: pageSize)
- case .twoColumn(let left, let right):
- let columnGap: CGFloat = 14
- let columnWidth = (contentWidth - columnGap) / 2
- let leftRect = CGRect(
- x: printInfo.leftMargin,
- y: printInfo.bottomMargin,
- width: columnWidth,
- height: textHeight
- )
- let rightRect = CGRect(
- x: printInfo.leftMargin + columnWidth + columnGap,
- y: printInfo.bottomMargin,
- width: columnWidth,
- height: textHeight
- )
- drawScanAttributedText(left, in: leftRect, context: context, pageSize: pageSize)
- drawScanAttributedText(right, in: rightRect, context: context, pageSize: pageSize)
- }
- }
- context.endPDFPage()
- }
- private static func drawScanImage(_ image: NSImage, in rect: CGRect, context: CGContext) {
- var proposedRect = NSRect(origin: .zero, size: image.size)
- guard image.size.width > 0, image.size.height > 0,
- let cgImage = image.cgImage(forProposedRect: &proposedRect, context: nil, hints: nil) else { return }
- let scale = min(rect.width / image.size.width, rect.height / image.size.height)
- let drawSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
- let drawRect = CGRect(
- x: rect.midX - drawSize.width / 2,
- y: rect.midY - drawSize.height / 2,
- width: drawSize.width,
- height: drawSize.height
- )
- context.draw(cgImage, in: drawRect)
- }
- private static func drawScanText(
- _ text: String,
- in rect: CGRect,
- context: CGContext,
- pageSize: NSSize,
- alignment: NSTextAlignment = .left
- ) {
- guard !text.isEmpty else { return }
- let paragraphStyle = NSMutableParagraphStyle()
- paragraphStyle.alignment = alignment
- paragraphStyle.lineBreakMode = .byWordWrapping
- let attributes: [NSAttributedString.Key: Any] = [
- .font: NSFont.systemFont(ofSize: 11),
- .foregroundColor: NSColor.black,
- .paragraphStyle: paragraphStyle,
- ]
- drawScanAttributedText(
- NSAttributedString(string: text, attributes: attributes),
- in: rect,
- context: context,
- pageSize: pageSize
- )
- }
- private static func drawScanAttributedText(
- _ attributed: NSAttributedString,
- in rect: CGRect,
- context: CGContext,
- pageSize: NSSize
- ) {
- guard attributed.length > 0 else { return }
- let normalized = scanAttributedStringForPrinting(attributed)
- context.saveGState()
- context.translateBy(x: 0, y: pageSize.height)
- context.scaleBy(x: 1, y: -1)
- let flippedY = pageSize.height - rect.maxY
- let drawRect = CGRect(x: rect.origin.x, y: flippedY, width: rect.width, height: rect.height)
- NSGraphicsContext.saveGraphicsState()
- NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: true)
- normalized.draw(with: drawRect, options: [.usesLineFragmentOrigin, .usesFontLeading])
- NSGraphicsContext.restoreGraphicsState()
- context.restoreGState()
- }
- private static func scanAttributedStringForPrinting(_ 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
- let font = (value as? NSFont) ?? NSFont.systemFont(ofSize: 11)
- let scaled = NSFontManager.shared.convert(font, toSize: 11)
- mutable.addAttribute(.font, value: scaled, range: range)
- }
- return mutable
- }
- private static func pdfDocument(from text: String) -> PDFDocument? {
- let paragraphStyle = NSMutableParagraphStyle()
- paragraphStyle.alignment = .left
- paragraphStyle.lineBreakMode = .byWordWrapping
- let attributes: [NSAttributedString.Key: Any] = [
- .font: NSFont.systemFont(ofSize: 12),
- .foregroundColor: NSColor.black,
- .paragraphStyle: paragraphStyle,
- ]
- return pdfDocument(from: NSAttributedString(string: text, attributes: attributes))
- }
- 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)
- let pageImage = renderTextPageImage(
- pageText: pageText,
- pageSize: pageSize,
- 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 renderTextPageImage(
- pageText: NSAttributedString,
- pageSize: NSSize,
- printInfo: NSPrintInfo
- ) -> NSImage {
- let textRect = NSRect(
- x: printInfo.leftMargin,
- y: printInfo.bottomMargin,
- width: pageSize.width - printInfo.leftMargin - printInfo.rightMargin,
- height: pageSize.height - printInfo.topMargin - printInfo.bottomMargin
- )
- let image = NSImage(size: pageSize)
- image.lockFocus()
- NSColor.white.setFill()
- NSRect(origin: .zero, size: pageSize).fill()
- pageText.draw(
- with: textRect,
- options: [.usesLineFragmentOrigin, .usesFontLeading]
- )
- 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 }
- 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? {
- 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()
- }
- }
|