| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- 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 }
- }
|