// // KMTools.swift // PDF Master // // Created by tangchao on 2023/3/7. // import Cocoa @objc class KMTools: NSObject { // MARK: - 获取已打开的文件 @objc class func getOpenDocumentURLs() -> [URL] { var files:[URL] = [] for window in NSApp.windows { if ((window.windowController is KMBrowserWindowController) == false) { continue } let controller: KMBrowserWindowController = window.windowController as! KMBrowserWindowController let model = controller.browser?.tabStripModel guard let count = model?.count() else { continue } if (count <= 0) { continue } for i in 0 ..< count { let document = model?.tabContents(at: Int32(i)) // if (document?.windowControllers == nil || document?.windowControllers.count == 0) { // continue // } if (document?.fileURL == nil) { continue } if (document?.isHome == nil || document!.isHome) { continue } files.append((document?.fileURL)!) } } return files } // MARK: - 无法区分 [权限+开启] [开启] 这两种情况 请不要使用 private class func isDocumentHasPermissionsPassword(_ url: URL) -> Bool { let document = PDFDocument(url: url) if (document == nil) { return false } if (document?.permissionsStatus == .user) { return true } // document?.permissionsStatus == .none if (document!.isLocked == false) { // 没有加锁 return false } // 已加锁 [权限+开启] [开启] if (KMTools.hasPermissionsLimit(document!)) { // 有权限限制 return true } return false } // MARK: - 暂时只处理了复制和打印两项(后续项目需求有新增时,可以再此方法里扩展) @objc class func hasPermissionsLimit(_ document: PDFDocument) -> Bool { if (document.allowsCopying == false) { return true } if (document.allowsPrinting == false) { return true } return false } // MARK: - 打开网页 @objc class func openURL(url: URL?) { guard let _url = url else { KMPrint("url invalid.") return } NSWorkspace.shared.open(_url) } @objc class func openURL(urlString: String?) { guard let _urlString = urlString else { KMPrint("url invalid.") return } KMTools.openURL(url: URL(string: _urlString)) } // MARK: - 获取 App 版本号 @objc class func getAppVersion() -> String { let infoDictionary = Bundle.main.infoDictionary if (infoDictionary == nil) { return "1.0.0" } var version = infoDictionary!["CFBundleShortVersionString"] if (version != nil && (version is String) && (version as! String).isEmpty == false) { return version as! String } version = infoDictionary!["CFBundleVersion"] if (version != nil && (version is String) && (version as! String).isEmpty == false) { return version as! String } return "1.0.0" } class func getSystemVersion() -> (Int, Int, Int) { let versionInfo = ProcessInfo.processInfo.operatingSystemVersion return (versionInfo.majorVersion, versionInfo.minorVersion, versionInfo.patchVersion) } @objc class func isDefaultPDFReader() -> Bool { let app = LSCopyDefaultRoleHandlerForContentType("pdf" as CFString, LSRolesMask.all)?.takeUnretainedValue() if (app == nil) { return false } return (app! as String) == Bundle.main.bundleIdentifier! } @objc class func setDefaultPDFReader(_ isOrNo: Bool) -> Bool { var bid = "com.apple.Preview" if (isOrNo) { bid = Bundle.main.bundleIdentifier! } let status: OSStatus = LSSetDefaultRoleHandlerForContentType(KMTools.UTIforFileExtension("pdf") as CFString, LSRolesMask.all, bid as CFString) if (status == 0) { return true } return false } @objc class func UTIforFileExtension(_ exn: String) -> String { return (UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, exn as CFString, nil)?.takeUnretainedValue())! as String } // MARK: - 是否全屏 @objc class func isFullScreen(_ window: NSWindow) -> Bool { return window.styleMask.contains(.fullScreen) } // MARK: - 文件类型 static let imageExtensions = ["jpg","cur","bmp","jpeg","gif","png","tiff","tif",/*@"pic",*/"ico","icns","tga","psd","eps","hdr","jp2","jpc","pict","sgi","heic"] static let pdfExtensions = ["pdf"] static let officeExtensions = ["doc", "docx", "xls", "xlsx", "ppt", "pptx"] @objc class func isImageType(_ exn: String) -> Bool { return KMTools.imageExtensions.contains(exn.lowercased()) } @objc class func isPDFType(_ exn: String) -> Bool { return KMTools.pdfExtensions.contains(exn.lowercased()) } @objc class func isOfficeType(_ exn: String) -> Bool { return KMTools.officeExtensions.contains(exn.lowercased()) } @objc class func getUniqueFilePath(filePath: String) -> String { var isDirectory: ObjCBool = false var uniqueFilePath = filePath let fileManager = FileManager.default fileManager.fileExists(atPath: uniqueFilePath, isDirectory: &isDirectory) var i = 0 if (isDirectory.boolValue) { while fileManager.fileExists(atPath: uniqueFilePath) { i += 1 uniqueFilePath = "\(filePath)(\(i))" } } else { let fileURL = URL(fileURLWithPath: filePath) let path = fileURL.deletingPathExtension().path while fileManager.fileExists(atPath: uniqueFilePath) { i += 1 uniqueFilePath = "\(path)(\(i).\(fileURL.pathExtension)" } } return uniqueFilePath } @objc class func getTempFloderPath() -> String? { return NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.applicationSupportDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last?.stringByAppendingPathComponent(Bundle.main.bundleIdentifier!).stringByAppendingPathComponent("KMTemp") } // MARK: - Document isDocumentEdited @objc class func setDocumentEditedState(window: NSWindow) { guard let _document = NSDocumentController.shared.document(for: window) else { return } self.setDocumentEditedState(document: _document) } @objc class func setDocumentEditedState(url: URL) { guard let _document = NSDocumentController.shared.document(for: url) else { return } self.setDocumentEditedState(document: _document) } @objc class func setDocumentEditedState(document: NSDocument) { km_synchronized(document) { document.updateChangeCount(.changeDone) } } @objc class func clearDocumentEditedState(window: NSWindow) { guard let _document = NSDocumentController.shared.document(for: window) else { return } self.clearDocumentEditedState(document: _document) } @objc class func clearDocumentEditedState(url: URL) { guard let _document = NSDocumentController.shared.document(for: url) else { return } self.clearDocumentEditedState(document: _document) } @objc class func clearDocumentEditedState(document: NSDocument) { km_synchronized(document) { document.updateChangeCount(.changeCleared) } } } // MARK: - PDFMaster let kKMPurchaseProductURLString = "https://www.pdfreaderpro.com/store" extension KMTools { // 打开 [快速教学] @objc class func openQuickStartStudy() { // MARK: - // MARK: 内嵌文档需要替换 var fileName = "PDF Master User Guide" let fileType = "pdf" let path = Bundle.main.path(forResource: fileName, ofType: fileType) if (path == nil || FileManager.default.fileExists(atPath: path!) == false) { KMTools.openURL(url: URL(string: "https://www.pdfreaderpro.com/help")) return } let version = KMTools.getAppVersion() fileName.append(" v\(version).\(fileType)") let folderPath = NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true).last?.appending("/\(Bundle.main.bundleIdentifier!)") if (FileManager.default.fileExists(atPath: folderPath!) == false) { try?FileManager.default.createDirectory(atPath: folderPath!, withIntermediateDirectories: false) } let toPath = "\(folderPath!)/\(fileName)" if (FileManager.default.fileExists(atPath: toPath)) { try?FileManager.default.removeItem(atPath: toPath) } try?FileManager.default.copyItem(atPath: path!, toPath: toPath) NSDocumentController.shared.km_safe_openDocument(withContentsOf: URL(fileURLWithPath: toPath), display: true) { _, _, _ in } } // 打开 [FAQ] 网站 @objc class func openFAQWebsite() { // KMTools.openURL(URL(string: "")!) } // 打开 [更多产品] 网站 @objc class func openMoreProductWebsite() { KMTools.openURL(url: URL(string: "https://www.pdfreaderpro.com/product?utm_source=MacApp&utm_campaign=ProductLink&utm_medium=PdfProduct")) } // 打开 [免费 PDF 模板] 网站 @objc class func openFreePDFTemplatesWebsite() { KMTools.openURL(url: URL(string: "https://www.pdfreaderpro.com/templates?utm_source=MacApp&utm_campaign=TemplatesLink&utm_medium=PdfTemplates")) } // 打开 [ComPDFKit 授权] 网站 @objc class func openComPDFKitPowerWebsite() { KMTools.openURL(url: URL(string: "https://www.compdf.com/?utm_source=macapp&utm_medium=pdfmac&utm_campaign=compdfkit-promp")) } // 打开 [官网 下载页] 网站 // 测试环境 http://test-pdf-pro.kdan.cn:3021/pdf-master-mac-download @objc class func openDownloadDMGWebsite() { KMTools.openURL(urlString: "https://www.pdfreaderpro.com/pdf-master-mac-download") } @objc class func openPurchaseProductWebsite() { KMTools.openURL(urlString: kKMPurchaseProductURLString) } // 意见反馈 @objc class func feekback() { let (major, minor, bugFix) = KMTools.getSystemVersion() let versionInfoString = "\(KMTools.getRawSystemInfo()) - \(major).\(minor).\(bugFix)" let appVersion = KMTools.getAppVersion() let appName = KMTools.getAppName() let subjects = "\(appName) - \(appVersion);\(NSLocalizedString("Propose a New Feature", comment: ""));\(versionInfoString)" // MARK: - // MARK TODO: 邮箱域名需要替换 let email = "support@pdfreaderpro.com" // MARK: - // MARK TODO: 邮箱域名需要替换 KMMailHelper.newEmail(withContacts: email, andSubjects: subjects) } @objc class func getRawSystemInfo() -> String { let info = GBDeviceInfo.deviceInfo().rawSystemInfoString if (info == nil) { return "" } return info! } @objc class func getAppName() -> String { #if VERSION_PRO return "PDF Master Pro" #endif return "PDF Master" } @objc class func pageRangeTypeString(pageRange: KMPageRange) -> String { switch pageRange { case .all: return NSLocalizedString("All Pages", comment: "") case .current: return NSLocalizedString("Current Page", comment: "") case .odd: return NSLocalizedString("Odd Pages", comment: "") case .even: return NSLocalizedString("Even Pages", comment: "") case .custom: return NSLocalizedString("Customize", comment: "") case .horizontal: return NSLocalizedString("Horizontal Pages", comment: "") case .vertical: return NSLocalizedString("Vertical Pages", comment: "") } } @objc class func pageRangePlaceholderString() -> String { return NSLocalizedString("e.g. 1,3-5,10", comment: "") } @objc class func saveWatermarkDocumentToTemp(document: CPDFDocument, secureOptions: [CPDFDocumentWriteOption : Any]? = nil, removePWD: Bool = false) -> URL? { // 将文档存入临时目录 if let data = self.getTempFloderPath(), !FileManager.default.fileExists(atPath: data) { try?FileManager.default.createDirectory(atPath: data, withIntermediateDirectories: false) } guard let filePath = self.getTempFloderPath()?.stringByAppendingPathComponent("temp_saveDocumentFor_temp.pdf") else { return nil } // 清除临时数据 if (FileManager.default.fileExists(atPath: filePath)) { try?FileManager.default.removeItem(atPath: filePath) } return self.saveWatermarkDocument(document: document, to: URL(fileURLWithPath: filePath), secureOptions: secureOptions, removePWD: removePWD) } @objc class func saveWatermarkDocument(document: CPDFDocument, to url: URL, secureOptions: [CPDFDocumentWriteOption : Any]? = nil, removePWD: Bool = false) -> URL? { guard let _document = self._saveDocumentForWatermark(document: document) else { return nil } // 保存文档 if let data = secureOptions, !data.isEmpty { _document.write(to: url, withOptions: data) } else if (removePWD) { _document.writeDecrypt(to: url) } else { _document.write(to: url) } // 清除临时数据 if let _fileUrl = _document.documentURL, FileManager.default.fileExists(atPath: _fileUrl.path) { try?FileManager.default.removeItem(atPath: _fileUrl.path) } return url } @objc class func saveWatermarkDocumentForCompress(document: CPDFDocument, to url: URL, imageQuality: Int) -> URL? { guard let _document = self._saveDocumentForWatermark(document: document) else { return nil } // _document.write(to: _document.documentURL) // 保存文档 let result = _document.writeOptimize(to: url, withOptions: [.imageQualityOption : imageQuality]) // 清除临时数据 if let _fileUrl = _document.documentURL, FileManager.default.fileExists(atPath: _fileUrl.path) { try?FileManager.default.removeItem(atPath: _fileUrl.path) } if (result) { return url } return nil } @objc class func saveWatermarkDocumentForFlatten(document: CPDFDocument, to url: URL) -> URL? { guard let _document = self._saveDocumentForWatermark(document: document) else { return nil } // 保存文档 let result = _document.writeFlatten(to: url) // 清除临时数据 if let _fileUrl = _document.documentURL, FileManager.default.fileExists(atPath: _fileUrl.path) { try?FileManager.default.removeItem(atPath: _fileUrl.path) } if (result) { return url } return nil } @objc class func saveDocumentToTemp(document: CPDFDocument, fileID: String, needUnlock: Bool = true) -> URL? { // 将文档存入临时目录 if let data = self.getTempFloderPath(), !FileManager.default.fileExists(atPath: data) { try?FileManager.default.createDirectory(atPath: data, withIntermediateDirectories: false) } guard let filePath = self.getTempFloderPath()?.stringByAppendingPathComponent("temp_saveDocumentFor\(fileID).pdf") else { return nil } // 清除临时数据 if (FileManager.default.fileExists(atPath: filePath)) { try?FileManager.default.removeItem(atPath: filePath) } document.write(toFile: filePath) if (!FileManager.default.fileExists(atPath: filePath)) { return nil } guard let _document = CPDFDocument(url: URL(fileURLWithPath: filePath)) else { return nil } if (!needUnlock) { return _document.documentURL } // 如果加锁,则去解锁 if let pwd = document.password, !pwd.isEmpty, _document.isLocked { _document.unlock(withPassword: document.password) } if (_document.isLocked) { return nil } return _document.documentURL } @objc class func trackEvent(type: KMSubscribeWaterMarkType) -> Void { if (type == .stamp) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Stamp", parameters: nil, appTarget: .all) } else if (type == .link) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Link", parameters: nil, appTarget: .all) } else if (type == .sign) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Sign", parameters: nil, appTarget: .all) } else if (type == .editText) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_EditText", parameters: nil, appTarget: .all) } else if (type == .editImage) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_EditImage", parameters: nil, appTarget: .all) } else if (type == .insert) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Insert", parameters: nil, appTarget: .all) } else if (type == .extract) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Extract", parameters: nil, appTarget: .all) } else if (type == .replace) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Replace", parameters: nil, appTarget: .all) } else if (type == .split) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Split", parameters: nil, appTarget: .all) } else if (type == .delete) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Delete", parameters: nil, appTarget: .all) } else if (type == .rotate) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Rotate", parameters: nil, appTarget: .all) } else if (type == .copy) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Copy", parameters: nil, appTarget: .all) } else if (type == .toWord) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_ToWord", parameters: nil, appTarget: .all) } else if (type == .toExcel) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_ToExcel", parameters: nil, appTarget: .all) } else if (type == .toPPT) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_ToPPT", parameters: nil, appTarget: .all) } else if (type == .toRTF) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_ToRTF", parameters: nil, appTarget: .all) } else if (type == .toCSV) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_ToCSV", parameters: nil, appTarget: .all) } else if (type == .toHTML) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_ToHTML", parameters: nil, appTarget: .all) } else if (type == .toText) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_ToText", parameters: nil, appTarget: .all) } else if (type == .toImage) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_ToImage", parameters: nil, appTarget: .all) } else if (type == .compress) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Compress", parameters: nil, appTarget: .all) } else if (type == .merge) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Merge", parameters: nil, appTarget: .all) } else if (type == .setPassword) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_SetPassword", parameters: nil, appTarget: .all) } else if (type == .removePassword) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_RemovePassword", parameters: nil, appTarget: .all) } else if (type == .crop) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_Crop", parameters: nil, appTarget: .all) } else if (type == .aiTranslate) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_AITranslate", parameters: nil, appTarget: .all) } else if (type == .aiRewrite) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_AIRewrite", parameters: nil, appTarget: .all) } else if (type == .aiCorrect) { KMAnalytics.trackEvent(eventName: "PDFMaster_Subscribe_AICorrect", parameters: nil, appTarget: .all) } } // MARK: - Private Methods @objc fileprivate class func _documentAddWatermark(document: CPDFDocument) -> CPDFDocument? { // 添加水印 let watermark = CPDFWatermark(document: document, type: .image) watermark?.image = NSImage(named: "KMImageNameWatermark") watermark?.horizontalPosition = .left watermark?.verticalPosition = .top watermark?.scale = 0.5 document.addWatermark(watermark) // 添加 link注释 var watermarkAnnoBounds = NSMakeRect(0, 0, 120, 32) for i in 0 ..< document.pageCount { guard let page = document.page(at: i) else { continue } // 水印注释 frame watermarkAnnoBounds.origin.y = page.bounds.size.height-watermarkAnnoBounds.size.height // 找到需要删除的水印注释(之前添加) var flagAnnos: [CPDFAnnotation] = [] for anno in page.annotations { if let anno_link = anno as? CPDFLinkAnnotation, anno_link.url() == kKMPurchaseProductURLString, anno_link.bounds.equalTo(watermarkAnnoBounds) { flagAnnos.append(anno_link) } } // 删除之前的水印注释 for anno in flagAnnos { page.removeAnnotation(anno) } // 新增新的水印注释 let anno = CPDFLinkAnnotation(document: document) anno?.bounds = watermarkAnnoBounds anno?.setURL(kKMPurchaseProductURLString) page.addAnnotation(anno) } return document } @objc fileprivate class func _saveDocumentForWatermark(document: CPDFDocument) -> CPDFDocument? { // 将文档存入临时目录 guard let _fileUrl = self.saveDocumentToTemp(document: document, fileID: "Watermark") else { return nil } guard let _document = CPDFDocument(url: _fileUrl) else { return nil } // 如果加锁,则去解锁 if let pwd = document.password, !pwd.isEmpty, _document.isLocked { _document.unlock(withPassword: pwd) } // 添加水印 return self._documentAddWatermark(document: _document) } }