// // KMTools.swift // PDF Reader Pro // // 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: - 查看文件 @objc class func viewFile(at filepath: String) { let ws = NSWorkspace.shared let url = URL(fileURLWithPath: filepath) ws.activateFileViewerSelecting([url]) } // 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 self.getTempRootPath()?.stringByAppendingPathComponent("KMTemp") } @objc class func getTempRootPath() -> String? { return NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.applicationSupportDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).last?.stringByAppendingPathComponent(Bundle.main.bundleIdentifier!) } // 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: - 解析 [1-3,5-7] @objc class func parseIndexSet(indexSet: IndexSet) -> String { return self.parseIndexs(indexs: indexSet.sorted()) } @objc class func parseIndexs(indexs: [Int]) -> String { if (indexs.isEmpty) { return "" } if (indexs.count == 1) { return "\(indexs.first!+1)" } var sortArray: [Int] = [] for i in indexs { sortArray.append(i) } /// 排序 (升序) sortArray.sort(){$0 < $1} var a: Int = 0 var b: Int = 0 var result: String? for i in sortArray { if (result == nil) { a = i b = i result = "" continue } if (i == b+1) { b = i if (i == sortArray.last) { result?.append(String(format: "%d-%d", a+1,b+1)) } } else { if (a == b) { result?.append(String(format: "%d,", a+1)) } else { result?.append(String(format: "%d-%d,", a+1,b+1)) } a = i b = i if (i == sortArray.last) { result?.append(String(format: "%d", a+1)) } } } return result ?? "" } } // MARK: - PDFReaderPro let kKMPurchaseProductURLString = "https://www.pdfreaderpro.com/store/pdftecheditor" extension KMTools { // 打开 [快速教学] @objc class func openQuickStartStudy() { // MARK: - // MARK: 内嵌文档需要替换 var fileName = "Quick Start 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() { var tStrUrl: String? tStrUrl = "https://www.pdfreaderpro.com/help?utm_source=lynxdmg&utm_medium=menubar&utm_campaign=online_help" KMTools.openURL(urlString: tStrUrl) } // 打开 [更多产品] 网站 @objc class func openMoreProductWebsite() { var tStrUrl: String? tStrUrl = NSLocalizedString("https://www.pdfreaderpro.com/product?utm_source=lynxdmg&utm_medium=menubar&utm_campaign=moreproduct", comment: "") KMTools.openURL(urlString: tStrUrl) } // 打开 [免费 PDF 模板] 网站 @objc class func openFreePDFTemplatesWebsite() { var tStrUrl: String? tStrUrl = "https://www.pdfreaderpro.com/templates?utm_source=lynxdmg&utm_medium=menubar&utm_campaign=pdf_templates" KMTools.openURL(urlString: tStrUrl) } // 打开 [ComPDFKit 授权] 网站 @objc class func openComPDFKitPowerWebsite() { KMTools.openURL(url: URL(string: NSLocalizedString("https://www.compdf.com?utm_source=lynxdmg&utm_medium=menubar&utm_campaign=compdfkit-promp", comment: ""))) } // 打开 [官网 下载页] 网站 // 测试环境 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.getAppNameForSupportEmail() let subjects = "\(appName) - \(appVersion);\(NSLocalizedString("Feedback", comment: ""));\(versionInfoString)" let email = "support@pdfreaderpro.com" KMMailHelper.newEmail(withContacts: email, andSubjects: subjects) } // @objc class func reportBug() { let (major, minor, bugFix) = KMTools.getSystemVersion() let versionInfoString = "\(KMTools.getRawSystemInfo()) - \(major).\(minor).\(bugFix)" let appVersion = KMTools.getAppVersion() let appName = KMTools.getAppNameForSupportEmail() let subjects = "\(appName) - \(appVersion);\(NSLocalizedString("Report a Bug", comment: ""));\(versionInfoString)" let email = "support@pdfreaderpro.com" KMMailHelper.newEmail(withContacts: email, andSubjects: subjects) } // @objc class func proposeNewFeature() { let (major, minor, bugFix) = KMTools.getSystemVersion() let versionInfoString = "\(KMTools.getRawSystemInfo()) - \(major).\(minor).\(bugFix)" let appVersion = KMTools.getAppVersion() let appName = KMTools.getAppNameForSupportEmail() let subjects = "\(appName) - \(appVersion);\(NSLocalizedString("Propose a New Feature", comment: ""));\(versionInfoString)" let email = "support@pdfreaderpro.com" KMMailHelper.newEmail(withContacts: email, andSubjects: subjects) } // @objc class func reportGeneralQuestions() { let (major, minor, bugFix) = KMTools.getSystemVersion() let versionInfoString = "\(KMTools.getRawSystemInfo()) - \(major).\(minor).\(bugFix)" let appVersion = KMTools.getAppVersion() let appName = KMTools.getAppNameForSupportEmail() let subjects = "\(appName) - \(appVersion);\(NSLocalizedString("General Questions", comment: ""));\(versionInfoString)" let email = "support@pdfreaderpro.com" KMMailHelper.newEmail(withContacts: email, andSubjects: subjects) } @objc class func rateUs() { #if VERSION_FREE iRate.sharedInstance().appStoreID = 919472673 #else iRate.sharedInstance().appStoreID = 825459243 #endif if UserDefaults.standard.bool(forKey: "kUserHaveClickRateUsKey") == false { UserDefaults.standard.set(true, forKey: "kUserHaveClickRateUsKey") UserDefaults.standard.synchronize() NotificationCenter.default.post(name: NSNotification.Name(rawValue: "kUserHaveClickRateUsNotification"), object: self) } iRate.sharedInstance().openRatingsPageInAppStore() } @objc class func getAppNameForSupportEmail() -> String { var tAppName = "PDF Reader Pro" #if VERSION_FREE #if VERSION_DMG tAppName = "LynxPDF Editor" #if VERSION_BETA tAppName = "PDF Reader Pro Beta" #endif // 桌机版 if let tManager = VerificationManager.default() { let status = tManager.status if status == ActivityStatusTrial { tAppName = "\(tAppName) Trial" } else if status == ActivityStatusVerification { tAppName = "\(tAppName) Verification" } else if status == ActivityStatusTrialExpire { tAppName = "\(tAppName) TrialExpire" } else if status == ActivityStatusVerifExpire { tAppName = "\(tAppName) VerifExpire" } } #else // AppStore 免费版本 tAppName = "PDF Reader Pro Lite" #endif #else // AppStore 付费版 tAppName = "PDF Reader Pro Edition" #endif return tAppName } @objc class func getRawSystemInfo() -> String { let info = GBDeviceInfo.deviceInfo().rawSystemInfoString if (info == nil) { return "" } return info! } @objc class func getAppName() -> String { return "LynxPDF Editor" } @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, documentAttribute: [CPDFDocumentAttribute : Any]? = nil, removePWD: Bool = false) -> URL? { guard let _document = self._saveDocumentForWatermark(document: document) else { return nil } // 保存文档 if let data = secureOptions, !data.isEmpty { _document.setDocumentAttributes(documentAttribute) _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) { if let rootPath = self.getTempRootPath(), !FileManager.default.fileExists(atPath: rootPath) { try?FileManager.default.createDirectory(atPath: rootPath, withIntermediateDirectories: false) } 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 { } private static var dateFormatter_: DateFormatter? @objc class func timeString(timeDate date: Date) -> String { if dateFormatter_ == nil { dateFormatter_ = DateFormatter() } let calendar = Calendar.current let nowCmps = calendar.dateComponents([.day, .month, .year], from: Date()) let currentCmps = calendar.dateComponents([.day, .month, .year], from: date) if (currentCmps.year == nowCmps.year) { if (currentCmps.month == nowCmps.month && currentCmps.day == nowCmps.day) { dateFormatter_?.dateFormat = "HH:mm" } else { dateFormatter_?.dateFormat = "MM-dd, HH:mm" } } else { dateFormatter_?.dateFormat = "yyyy-MM-dd, HH:mm" } return dateFormatter_?.string(from: date) ?? "" } @objc class func timeString(timeDate date: Date, formatString: String) -> String { if dateFormatter_ == nil { dateFormatter_ = DateFormatter() } let calendar = Calendar.current let nowCmps = calendar.dateComponents([.day, .month, .year], from: Date()) let currentCmps = calendar.dateComponents([.day, .month, .year], from: date) dateFormatter_?.dateFormat = formatString return dateFormatter_?.string(from: date) ?? "" } @objc class func isFileGreaterThan10MB(atPath filePath: String) -> Bool { let fileManager = FileManager.default do { let fileAttributes = try fileManager.attributesOfItem(atPath: filePath) if let fileSize = fileAttributes[.size] as? UInt64 { let megabyteSize = fileSize / (1024 * 1024) return megabyteSize >= 10 } } catch { KMPrint("Error: \(error)") } return false } // 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) } }