// // KMWatermarkAdjectiveTools.swift // PDF Reader Pro // // Created by tangchao on 2022/12/27. // import Cocoa enum KMWatermarkAdjectiveType: Int { case watermark = 1 case background = 2 case headerfooter = 3 case bates = 4 } @objc class KMWatermarkAdjectiveTools: NSObject { class func fetchAvailableFonts(_ size: CGFloat) -> [NSAttributedString] { let fonts = NSFontManager.shared.availableFontFamilies var result: Array = [] for fontName in fonts { let font = NSFont(name: fontName, size: size) let attribute = [NSAttributedString.Key.font : font] let string = NSAttributedString(string: fontName, attributes: attribute as [NSAttributedString.Key : Any]) result.append(string) } return result } class func fontNameToAttribute(_ name: String, _ size: CGFloat) -> NSAttributedString? { let names = NSFontManager.shared.availableFontFamilies if (!names.contains(name)) { return nil } let font = NSFont(name: name, size: size) if (font == nil) { return nil } let attribute = [NSAttributedString.Key.font : font] return NSAttributedString(string: name, attributes: attribute as [NSAttributedString.Key : Any]) } class func parseColor(color: NSColor) -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) { var red: CGFloat = 0.0 var green: CGFloat = 0.0 var blue: CGFloat = 0.0 var alpha: CGFloat = 0.0 color.usingColorSpaceName(NSColorSpaceName.calibratedRGB)?.getRed(&red, green: &green, blue: &blue, alpha: &alpha) return (red, green, blue, alpha) } @objc class func getDateFormats() -> [String] { return ["m/d", "m/d/yy", "m/d/yyyy", "mm/dd/yy", "mm/dd/yyyy", "d/m/yy", "d/m/yyyy", "dd/mm/yy", "dd/mm/yyyy", "mm/yy", "mm/yyyy", "m.d.yy", "m.d.yyyy", "mm.dd.yy", "mm.dd.yyyy", "mm.yy", "mm.yyyy", "d.m.yy", "d.m.yyyy", "dd.mm.yy", "dd.mm.yyyy", "yy-mm-dd", "yyyy-mm-dd"] } class func getPageFormats() -> [String] { return ["1", "1 of n", "1/n", "Page 1", "Page 1 of n"] } class func parsePageFormat(formatString: String, startPage: String, pageCount: String) -> String { var result = formatString for pageFormat in self.getPageFormats() { let string = "<<\(pageFormat)>>" if (result.contains(string)) { var tempString = "" if (string == "<<1>>") { tempString.append("<<\(startPage)>>") } else if (string == "<<1 of n>>") { tempString.append("<<\(startPage)>>") tempString.append(" of \(pageCount)") } else if (string == "<<1/n>>") { tempString.append("<<\(startPage)>>") tempString.append("/\(pageCount)") } else if (string == "<>") { tempString.append("Page \(startPage)") } else if (string == "<>") { tempString.append("Page \(startPage)") tempString.append("of \(pageCount)") } result = result.replacingOccurrences(of: string, with: tempString) } } return result } class func parseDateFormat(formatString: String) -> String { var result: String = formatString for dateFormat in self.getDateFormats() { if (result.contains(dateFormat)) { var formatString: String = dateFormat.replacingOccurrences(of: "m", with: "M") var replace = "<<\(dateFormat)>>" let date = Date() let dateFormatter = DateFormatter() dateFormatter.dateFormat = formatString var dateString = dateFormatter.string(from: date) result = result.replacingOccurrences(of: replace, with: dateString) } } return result } class func KMWatermarkAdjectiveType(from toolBarType: KMToolbarType) -> KMWatermarkAdjectiveType { if (toolBarType == .bates) { return .bates } else if (toolBarType == .headerAndFooter) { return .headerfooter } else if (toolBarType == .background) { return .background } else if (toolBarType == .watermark) { return .watermark } return .watermark } class func KMToolBarTypeToRightSubViewType(_ type: KMToolbarType) -> RightSubViewType { if (type == .bates) { return .Bates } else if (type == .headerAndFooter) { return .Headerfooter } else if (type == .background) { return .Background } else if (type == .watermark) { return .Watermark } return .None } // MARK: Apply class func apply(_ model: AnyObject, _ pdfView: CPDFView, _ toPath: String, completion: @escaping (_ result: Bool) -> ()) { if (model.isKind(of: KMHeaderFooterObject.self)) { KMWatermarkAdjectiveTools.applyBates(model as! KMHeaderFooterObject, pdfView, toPath, completion: completion) } else if (model.isKind(of: KMHeaderFooterObject.self)) { KMWatermarkAdjectiveTools.applyHeaderFooter(model as! KMHeaderFooterObject, pdfView, toPath, completion: completion) } else if (model.isKind(of: KMBackgroundModel.self)) { KMWatermarkAdjectiveTools.applyBackground(model as! KMBackgroundModel, pdfView, toPath, completion: completion) } else if (model.isKind(of: KMWatermarkModel.self)) { KMWatermarkAdjectiveTools.applyWatermark(model as! KMWatermarkModel, pdfView, toPath, completion: completion) } } class func delete(_ type: KMWatermarkAdjectiveType, _ pdfView: CPDFView, _ toPath: String, completion: @escaping (_ result: Bool) -> ()) { if (pdfView.document.allowsPrinting == false || pdfView.document.allowsCopying == false) { let alert = NSAlert() alert.alertStyle = .critical alert.messageText = NSLocalizedString("This PDF document's user permissions does not allow modifying, content copying and printing.", comment: "") alert.runModal() completion(false) return } DispatchQueue.global().async { let document: CPDFDocument = CPDFDocument(url: pdfView.document.documentURL) if (type == .bates) { let property = document.bates() property?.clear() } else if (type == .headerfooter) { let property = document.headerFooter() property?.clear() } else if (type == .background) { let property = document.background() property?.clear() } else if (type == .watermark) { guard let tempArr = document.watermarks() else { return } let array: Array = document.watermarks() for model in array { document.removeWatermark(model) } } /// 保存到临时路径 let documentPath = NSTemporaryDirectory() let tempPath: String = "\(documentPath)/\(toPath.lastPathComponent)" if (FileManager.default.fileExists(atPath: tempPath)) { try?FileManager.default.removeItem(atPath: tempPath) } let result = document.write(to: URL(fileURLWithPath: tempPath)) if (result) { if (FileManager.default.fileExists(atPath: toPath)) { try?FileManager.default.removeItem(atPath: toPath) } try?FileManager.default.moveItem(atPath: tempPath, toPath: toPath) } else { try?FileManager.default.removeItem(atPath: tempPath) } DispatchQueue.main.async { completion(result) } } } private class func applyBates(_ model: KMHeaderFooterObject, _ pdfView: CPDFView, _ toPath: String, completion: @escaping (_ result: Bool) -> ()) { DispatchQueue.global().async { let document: CPDFDocument = pdfView.document var property = document.bates() var fontSize = 0.0 var fontName: String = "" switch model.textFont { case .font(name: let name, size: let size): fontName = name fontSize = size break default: break } let font = NSFont.boldSystemFont(ofSize:fontSize) let style = NSMutableParagraphStyle() style.alignment = .center style.lineBreakMode = .byCharWrapping let size: NSSize = "text".boundingRect(with: NSSize(width: 1000, height: 1000), options: NSString.DrawingOptions(rawValue: 3), attributes: [NSAttributedString.Key.font : font, NSAttributedString.Key.paragraphStyle : style]).size property?.margin = NSEdgeInsetsMake(max(CGFloat(model.topMargin)-size.height, 0), CGFloat(model.leftMargin), max(CGFloat(model.bottomMargin)-size.height, 0), CGFloat(model.rightMargin)) let strings = [model.topLeftString, model.topCenterString, model.topRightString, model.bottomLeftString, model.bottomCenterString, model.bottomRightString] var count: Int = 0 var color: NSColor! switch model.textColor { case .color(red: let red, green: let green, blue: let blue, alpha: let alpha): color = NSColor(red: red, green: green, blue: blue, alpha: alpha) default: break } if (color == nil) { color = NSColor.black } for text in strings { property?.setText(text, at: UInt(count)) property?.setTextColor(color, at: UInt(count)) property?.setFontSize(fontSize, at: UInt(count)) property?.setFontName(fontName, at: UInt(count)) count += 1 } let pagesString = KMWatermarkAdjectiveTools.findPagesString(model) if (pagesString.isEmpty) { property?.pageString = "0-\(document.pageCount-1)" } else { property?.pageString = pagesString } property?.update() /// 保存到临时路径 let documentPath = NSTemporaryDirectory() let tempPath: String = "\(documentPath)/\(toPath.lastPathComponent)" if (FileManager.default.fileExists(atPath: tempPath)) { try?FileManager.default.removeItem(atPath: tempPath) } let result = document.write(to: URL(fileURLWithPath: tempPath)) if (result) { if (FileManager.default.fileExists(atPath: toPath)) { try?FileManager.default.removeItem(atPath: toPath) } try?FileManager.default.moveItem(atPath: tempPath, toPath: toPath) } else { try?FileManager.default.removeItem(atPath: tempPath) } DispatchQueue.main.async { completion(result) } } } private class func applyHeaderFooter(_ model: KMHeaderFooterObject, _ pdfView: CPDFView, _ toPath: String, completion: @escaping (_ result: Bool) -> ()) { DispatchQueue.global().async { let document: CPDFDocument = pdfView.document var property = document.headerFooter() var fontSize = 0.0 var fontName: String = "" switch model.textFont { case .font(name: let name, size: let size): fontSize = size fontName = name break default: break } let font = NSFont.boldSystemFont(ofSize:fontSize) let style = NSMutableParagraphStyle() style.alignment = .center style.lineBreakMode = .byCharWrapping let size: NSSize = "text".boundingRect(with: NSSize(width: 1000, height: 1000), options: NSString.DrawingOptions(rawValue: 3), attributes: [NSAttributedString.Key.font : font, NSAttributedString.Key.paragraphStyle : style]).size property?.margin = NSEdgeInsetsMake(max(CGFloat(model.topMargin)-size.height, 0), CGFloat(model.leftMargin), max(CGFloat(model.bottomMargin)-size.height, 0), CGFloat(model.rightMargin)) let strings = KMWatermarkAdjectiveTools.parseModel(model: model, pdfView.document.pageCount) var count: Int = 0 var color: NSColor! switch model.textColor { case .color(red: let red, green: let green, blue: let blue, alpha: let alpha): color = NSColor(red: red, green: green, blue: blue, alpha: alpha) default: break } if (color == nil) { color = NSColor.black } for text in strings { property?.setText(text, at: UInt(count)) property?.setTextColor(color, at: UInt(count)) property?.setFontSize(fontSize, at: UInt(count)) property?.setFontName(fontName, at: UInt(count)) count += 1 } let pagesString = KMWatermarkAdjectiveTools.findPagesString(model) if (pagesString.isEmpty) { property?.pageString = "0-\(document.pageCount-1)" } else { property?.pageString = pagesString } property?.update() /// 保存到临时路径 let documentPath = NSTemporaryDirectory() let tempPath: String = "\(documentPath)/\(toPath.lastPathComponent)" if (FileManager.default.fileExists(atPath: tempPath)) { try?FileManager.default.removeItem(atPath: tempPath) } let result = document.write(to: URL(fileURLWithPath: tempPath)) if (result) { if (FileManager.default.fileExists(atPath: toPath)) { try?FileManager.default.removeItem(atPath: toPath) } try?FileManager.default.moveItem(atPath: tempPath, toPath: toPath) } else { try?FileManager.default.removeItem(atPath: tempPath) } DispatchQueue.main.async { completion(result) } } } private class func applyBackground(_ model: KMBackgroundModel, _ pdfView: CPDFView, _ toPath: String, completion: @escaping (_ result: Bool) -> ()) { DispatchQueue.global().async { let document: CPDFDocument = pdfView.document var property = document.background() property!.scale = model.scale property!.rotation = CGFloat(-model.rotation) property!.opacity = model.opacity property?.xOffset = model.horizontalSpace property?.yOffset = model.verticalSpace property?.horizontalAlignment = UInt(model.horizontalMode) property?.verticalAlignment = UInt(model.verticalMode) if (model.type == .color) { property?.color = model.color property?.type = .color } else if (model.type == .file) { property?.setImage(NSImage(contentsOfFile: model.imagePath)) // property?.setImage(model.image) property?.type = .image } property?.pageString = "0-\(document.pageCount-1)" property?.update() /// 保存到临时路径 let documentPath = NSTemporaryDirectory() let tempPath: String = "\(documentPath)/\(toPath.lastPathComponent)" if (FileManager.default.fileExists(atPath: tempPath)) { try?FileManager.default.removeItem(atPath: tempPath) } let result = document.write(to: URL(fileURLWithPath: tempPath)) if (result) { if (FileManager.default.fileExists(atPath: toPath)) { try?FileManager.default.removeItem(atPath: toPath) } try?FileManager.default.moveItem(atPath: tempPath, toPath: toPath) } else { try?FileManager.default.removeItem(atPath: tempPath) } DispatchQueue.main.async { completion(result) } } } private class func applyWatermark(_ model: KMWatermarkModel, _ pdfView: CPDFView, _ toPath: String, completion: @escaping (_ result: Bool) -> ()) { DispatchQueue.global().async { var property: CPDFWatermark! var scale: CGFloat = model.scale if (!model.text.isEmpty) { property = CPDFWatermark(document: pdfView.document, type: .text) property.text = model.text property.textColor = model.getTextColor() scale = model.getTextFontSize() / 24.0 } else { property = CPDFWatermark(document: pdfView.document, type: .image) property.image = model.image } property.scale = scale property.rotation = -model.rotation property.opacity = model.opacity property.tx = model.horizontalSpace property.ty = model.verticalSpace property.isFront = model.isFront var pageString: String = "" if (model.pageRangeType == .all) { for i in 0 ..< pdfView.document.pageCount { pageString.append("\(i)") if (i != pdfView.document.pageCount-1) { pageString.append(",") } } } else if (model.pageRangeType == .odd) { for i in 0 ..< pdfView.document.pageCount { if (i % 2 == 0) { pageString.append("\(i)") } else { continue } if (i != pdfView.document.pageCount-1) { pageString.append(",") } } } else if (model.pageRangeType == .even) { for i in 0 ..< pdfView.document.pageCount { if (i % 2 == 1) { pageString.append("\(i)") } else { continue } if (i != pdfView.document.pageCount-1) { pageString.append(",") } } } else { pageString = model.pagesString } property.pageString = pageString property.isTilePage = model.isTilePage property.horizontalSpacing = model.tileHorizontalSpace / scale property.verticalSpacing = model.tileVerticalSpace / scale if (model.verticalMode == 0) { property.verticalPosition = .top } else if (model.verticalMode == 1) { property.verticalPosition = .center } else if (model.verticalMode == 2) { property.verticalPosition = .bottom } if (model.horizontalMode == 0) { property.horizontalPosition = .left } else if (model.horizontalMode == 1) { property.horizontalPosition = .center } else if (model.horizontalMode == 2) { property.horizontalPosition = .right } model.watermark = property pdfView.document.addWatermark(property) /// 保存到临时路径 let documentPath = NSTemporaryDirectory() let tempPath: String = "\(documentPath)/\(toPath.lastPathComponent)" if (FileManager.default.fileExists(atPath: tempPath)) { try?FileManager.default.removeItem(atPath: tempPath) } let result = pdfView.document.write(to: URL(fileURLWithPath: tempPath)) if (result) { if (FileManager.default.fileExists(atPath: toPath)) { try?FileManager.default.removeItem(atPath: toPath) } try?FileManager.default.moveItem(atPath: tempPath, toPath: toPath) } else { try?FileManager.default.removeItem(atPath: tempPath) } DispatchQueue.main.async { completion(result) } } } // MARK: Add class func add(_ model: AnyObject, _ pdfView: CPDFView, completion: @escaping (_ result: Bool) -> ()) { if (model.isKind(of: KMHeaderFooterObject.self)) { // KMWatermarkAdjectiveTools.applyBates(model as! KMHeaderFooterObject, pdfView, toPath, completion: completion) } else if (model.isKind(of: KMHeaderFooterObject.self)) { // KMWatermarkAdjectiveTools.applyHeaderFooter(model as! KMHeaderFooterObject, pdfView, toPath, completion: completion) } else if (model.isKind(of: KMBackgroundModel.self)) { // KMWatermarkAdjectiveTools.applyBackground(model as! KMBackgroundModel, pdfView, toPath, completion: completion) } else if (model.isKind(of: KMWatermarkModel.self)) { KMWatermarkAdjectiveTools.addWatermark(model as! KMWatermarkModel, pdfView, completion: completion) } } private class func addWatermark(_ model: KMWatermarkModel, _ pdfView: CPDFView, completion: @escaping (_ result: Bool) -> ()) { DispatchQueue.global().async { var property: CPDFWatermark! var scale: CGFloat = model.scale if (!model.text.isEmpty) { property = CPDFWatermark(document: pdfView.document, type: .text) property.text = model.text property.textColor = model.getTextColor() scale = model.getTextFontSize() / 24.0 } else { property = CPDFWatermark(document: pdfView.document, type: .image) property.image = model.image } property.scale = scale property.rotation = -model.rotation property.opacity = model.opacity property.tx = model.horizontalSpace property.ty = model.verticalSpace property.isFront = model.isFront var pageString: String = "" if (model.pageRangeType == .all) { for i in 0 ..< pdfView.document.pageCount { pageString.append("\(i)") if (i != pdfView.document.pageCount-1) { pageString.append(",") } } } else if (model.pageRangeType == .odd) { for i in 0 ..< pdfView.document.pageCount { if (i % 2 == 0) { pageString.append("\(i)") } else { continue } if (i != pdfView.document.pageCount-1) { pageString.append(",") } } } else if (model.pageRangeType == .even) { for i in 0 ..< pdfView.document.pageCount { if (i % 2 == 1) { pageString.append("\(i)") } else { continue } if (i != pdfView.document.pageCount-1) { pageString.append(",") } } } else { pageString = model.pagesString } property.pageString = pageString property.isTilePage = model.isTilePage property.horizontalSpacing = model.tileHorizontalSpace / scale property.verticalSpacing = model.tileVerticalSpace / scale if (model.verticalMode == 0) { property.verticalPosition = .top } else if (model.verticalMode == 1) { property.verticalPosition = .center } else if (model.verticalMode == 2) { property.verticalPosition = .bottom } if (model.horizontalMode == 0) { property.horizontalPosition = .left } else if (model.horizontalMode == 1) { property.horizontalPosition = .center } else if (model.horizontalMode == 2) { property.horizontalPosition = .right } model.watermark = property let result = pdfView.document.addWatermark(property) DispatchQueue.main.async { completion(result) } } } // MARK: Update class func update(_ model: AnyObject, _ pdfView: CPDFView, completion: @escaping (_ result: Bool) -> ()) { if (model.isKind(of: KMHeaderFooterObject.self)) { // KMWatermarkAdjectiveTools.applyBates(model as! KMHeaderFooterObject, pdfView, toPath, completion: completion) } else if (model.isKind(of: KMHeaderFooterObject.self)) { // KMWatermarkAdjectiveTools.applyHeaderFooter(model as! KMHeaderFooterObject, pdfView, toPath, completion: completion) } else if (model.isKind(of: KMBackgroundModel.self)) { // KMWatermarkAdjectiveTools.applyBackground(model as! KMBackgroundModel, pdfView, toPath, completion: completion) } else if (model.isKind(of: KMWatermarkModel.self)) { KMWatermarkAdjectiveTools.updateWatermark(model as! KMWatermarkModel, pdfView, completion: completion) } } private class func updateWatermark(_ model: KMWatermarkModel, _ pdfView: CPDFView, completion: @escaping (_ result: Bool) -> ()) { DispatchQueue.global().async { var property: CPDFWatermark! var scale: CGFloat = model.scale if (!model.text.isEmpty) { property = CPDFWatermark(document: pdfView.document, type: .text) property.text = model.text property.textColor = model.getTextColor() scale = model.getTextFontSize() / 24.0 } else { property = CPDFWatermark(document: pdfView.document, type: .image) property.image = model.image } property.scale = scale property.rotation = -model.rotation property.opacity = model.opacity property.tx = model.horizontalSpace property.ty = model.verticalSpace property.isFront = model.isFront var pageString: String = "" if (model.pageRangeType == .all) { for i in 0 ..< pdfView.document.pageCount { pageString.append("\(i)") if (i != pdfView.document.pageCount-1) { pageString.append(",") } } } else if (model.pageRangeType == .odd) { for i in 0 ..< pdfView.document.pageCount { if (i % 2 == 0) { pageString.append("\(i)") } else { continue } if (i != pdfView.document.pageCount-1) { pageString.append(",") } } } else if (model.pageRangeType == .even) { for i in 0 ..< pdfView.document.pageCount { if (i % 2 == 1) { pageString.append("\(i)") } else { continue } if (i != pdfView.document.pageCount-1) { pageString.append(",") } } } else { pageString = model.pagesString } property.pageString = pageString property.isTilePage = model.isTilePage property.horizontalSpacing = model.tileHorizontalSpace / scale property.verticalSpacing = model.tileVerticalSpace / scale if (model.verticalMode == 0) { property.verticalPosition = .top } else if (model.verticalMode == 1) { property.verticalPosition = .center } else if (model.verticalMode == 2) { property.verticalPosition = .bottom } if (model.horizontalMode == 0) { property.horizontalPosition = .left } else if (model.horizontalMode == 1) { property.horizontalPosition = .center } else if (model.horizontalMode == 2) { property.horizontalPosition = .right } model.watermark = property let watermarks = pdfView.document.watermarks() if (watermarks != nil) { for i in 0 ..< watermarks!.count { let watermark = watermarks![i] pdfView.document.removeWatermark(watermark) } } let result = pdfView.document.addWatermark(property) DispatchQueue.main.async { completion(result) } } } private class func parseModel(model: KMHeaderFooterObject, _ pageCount: UInt) -> [String] { var topLeftString: String = "" if (!model.topLeftString.isEmpty) { var string = KMWatermarkAdjectiveTools.parsePageFormat(formatString: model.topLeftString, startPage: model.startString, pageCount: "\(pageCount)") string = KMWatermarkAdjectiveTools.parseDateFormat(formatString: string) topLeftString = string } var topCenterString: String = "" if (!model.topCenterString.isEmpty) { var string = KMWatermarkAdjectiveTools.parsePageFormat(formatString: model.topCenterString, startPage: model.startString, pageCount: "\(pageCount)") string = KMWatermarkAdjectiveTools.parseDateFormat(formatString: string) topCenterString = string } var topRightString: String = "" if (!model.topRightString.isEmpty) { var string = KMWatermarkAdjectiveTools.parsePageFormat(formatString: model.topRightString, startPage: model.startString, pageCount: "\(pageCount)") string = KMWatermarkAdjectiveTools.parseDateFormat(formatString: string) topRightString = string } var bottomLeftString: String = "" if (!model.bottomLeftString.isEmpty) { var string = KMWatermarkAdjectiveTools.parsePageFormat(formatString: model.bottomLeftString, startPage: model.startString, pageCount: "\(pageCount)") string = KMWatermarkAdjectiveTools.parseDateFormat(formatString: string) bottomLeftString = string } var bottomCenterString: String = "" if (!model.bottomCenterString.isEmpty) { var string = KMWatermarkAdjectiveTools.parsePageFormat(formatString: model.bottomCenterString, startPage: model.startString, pageCount: "\(pageCount)") string = KMWatermarkAdjectiveTools.parseDateFormat(formatString: string) bottomCenterString = string } var bottomRightString: String = "" if (!model.bottomRightString.isEmpty) { var string = KMWatermarkAdjectiveTools.parsePageFormat(formatString: model.bottomRightString, startPage: model.startString, pageCount: "\(pageCount)") string = KMWatermarkAdjectiveTools.parseDateFormat(formatString: string) bottomRightString = string } return [topLeftString, topCenterString, topRightString, bottomLeftString, bottomCenterString, bottomRightString] } class func findPagesString(_ model: KMWatermarkAdjectiveBaseModel) -> String{ if (model.pageRangeType == .all) { /// 全部页面 return "0-\(model.pageCount-1)" } else if (model.pageRangeType == .odd) { /// 奇数页面 var string: String = "" for i in 0 ..< model.pageCount { if (i % 2 == 1) { continue } string.append("\(i)") if (i != model.pageCount-1) { string.append(",") } } return string } else if (model.pageRangeType == .even) { /// 偶数页面 var string: String = "" for i in 0 ..< model.pageCount { if (i % 2 == 0) { continue } string.append("\(i)") if (i != model.pageCount-1) { string.append(",") } } return string } else { /// 自定义 return model.pageRangeString } } }