|
@@ -1,4 +1,5 @@
|
|
|
import Cocoa
|
|
import Cocoa
|
|
|
|
|
+import CoreText
|
|
|
import PDFKit
|
|
import PDFKit
|
|
|
import UniformTypeIdentifiers
|
|
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)
|
|
showUnsupportedAlert(for: url)
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
|
|
+ printPDF(document, title: title, from: window ?? NSApp.keyWindow)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static func contentType(for url: URL) -> UTType? {
|
|
private static func contentType(for url: URL) -> UTType? {
|
|
@@ -149,9 +133,45 @@ enum PrintService {
|
|
|
return UTType(filenameExtension: ext)
|
|
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?) {
|
|
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()
|
|
let printInfo = configuredPrintInfo()
|
|
|
- guard let operation = document.printOperation(
|
|
|
|
|
|
|
+ guard let operation = printable.printOperation(
|
|
|
for: printInfo,
|
|
for: printInfo,
|
|
|
scalingMode: .pageScaleToFit,
|
|
scalingMode: .pageScaleToFit,
|
|
|
autoRotate: false
|
|
autoRotate: false
|
|
@@ -170,17 +190,6 @@ enum PrintService {
|
|
|
printPDF(document, title: title, from: window)
|
|
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?) {
|
|
private static func runPrintOperation(_ operation: NSPrintOperation, title: String, from window: NSWindow?) {
|
|
|
operation.showsPrintPanel = true
|
|
operation.showsPrintPanel = true
|
|
|
operation.showsProgressPanel = false
|
|
operation.showsProgressPanel = false
|
|
@@ -278,10 +287,6 @@ enum PrintService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static func pdfDocument(from text: String) -> PDFDocument? {
|
|
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()
|
|
let paragraphStyle = NSMutableParagraphStyle()
|
|
|
paragraphStyle.alignment = .left
|
|
paragraphStyle.alignment = .left
|
|
|
paragraphStyle.lineBreakMode = .byWordWrapping
|
|
paragraphStyle.lineBreakMode = .byWordWrapping
|
|
@@ -291,81 +296,196 @@ enum PrintService {
|
|
|
.foregroundColor: NSColor.black,
|
|
.foregroundColor: NSColor.black,
|
|
|
.paragraphStyle: paragraphStyle,
|
|
.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 textWidth = pageSize.width - printInfo.leftMargin - printInfo.rightMargin
|
|
|
let textHeight = pageSize.height - printInfo.topMargin - printInfo.bottomMargin
|
|
let textHeight = pageSize.height - printInfo.topMargin - printInfo.bottomMargin
|
|
|
let containerSize = NSSize(width: textWidth, height: textHeight)
|
|
let containerSize = NSSize(width: textWidth, height: textHeight)
|
|
|
|
|
|
|
|
|
|
+ let textStorage = NSTextStorage(attributedString: attributed)
|
|
|
|
|
+ let layoutManager = NSLayoutManager()
|
|
|
|
|
+ textStorage.addLayoutManager(layoutManager)
|
|
|
|
|
+
|
|
|
let pdfData = NSMutableData()
|
|
let pdfData = NSMutableData()
|
|
|
var mediaBox = CGRect(origin: .zero, size: pageSize)
|
|
var mediaBox = CGRect(origin: .zero, size: pageSize)
|
|
|
-
|
|
|
|
|
guard let consumer = CGDataConsumer(data: pdfData as CFMutableData),
|
|
guard let consumer = CGDataConsumer(data: pdfData as CFMutableData),
|
|
|
let context = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
|
|
let context = CGContext(consumer: consumer, mediaBox: &mediaBox, nil) else {
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ var pageCount = 0
|
|
|
while true {
|
|
while true {
|
|
|
let textContainer = NSTextContainer(size: containerSize)
|
|
let textContainer = NSTextContainer(size: containerSize)
|
|
|
textContainer.lineFragmentPadding = 0
|
|
textContainer.lineFragmentPadding = 0
|
|
|
layoutManager.addTextContainer(textContainer)
|
|
layoutManager.addTextContainer(textContainer)
|
|
|
|
|
+ layoutManager.ensureLayout(for: textContainer)
|
|
|
|
|
|
|
|
let glyphRange = layoutManager.glyphRange(for: textContainer)
|
|
let glyphRange = layoutManager.glyphRange(for: textContainer)
|
|
|
guard glyphRange.length > 0 else { break }
|
|
guard glyphRange.length > 0 else { break }
|
|
|
|
|
|
|
|
let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
|
|
let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
|
|
|
let pageText = attributed.attributedSubstring(from: charRange)
|
|
let pageText = attributed.attributedSubstring(from: charRange)
|
|
|
-
|
|
|
|
|
- context.beginPDFPage(nil)
|
|
|
|
|
- drawTextPage(
|
|
|
|
|
- pageText,
|
|
|
|
|
- in: context,
|
|
|
|
|
|
|
+ let pageImage = renderTextPageImage(
|
|
|
|
|
+ pageText: pageText,
|
|
|
pageSize: pageSize,
|
|
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()
|
|
context.endPDFPage()
|
|
|
|
|
+ pageCount += 1
|
|
|
|
|
|
|
|
if NSMaxRange(glyphRange) >= layoutManager.numberOfGlyphs { break }
|
|
if NSMaxRange(glyphRange) >= layoutManager.numberOfGlyphs { break }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
context.closePDF()
|
|
context.closePDF()
|
|
|
|
|
+ guard pageCount > 0 else { return nil }
|
|
|
return PDFDocument(data: pdfData as Data)
|
|
return PDFDocument(data: pdfData as Data)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private static func drawTextPage(
|
|
|
|
|
- _ text: NSAttributedString,
|
|
|
|
|
- in context: CGContext,
|
|
|
|
|
|
|
+ private static func renderTextPageImage(
|
|
|
|
|
+ pageText: NSAttributedString,
|
|
|
pageSize: NSSize,
|
|
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,
|
|
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]
|
|
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? {
|
|
private static func pdfDocument(from image: NSImage) -> PDFDocument? {
|