// // KMWatermarkPDFView.swift // PDF Reader Pro // // Created by tangchao on 2022/12/19. // import Cocoa class KMWatermarkPDFView: CPDFView { // var watermarkModel: KMWatermarkModel! var watermark: KMWatermarkModel? var background: KMBackgroundModel? var headerFooter: KMHeaderFooterObject? override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) } override func draw(_ page: CPDFPage!, to context: CGContext!) { if let watermark = self.watermark { if watermark.isFront { super.draw(page, to: context) if (self.needDraw(page)) { drawWatermarkPage(page, to: context) } } else { if (self.needDraw(page)) { drawWatermarkPage(page, to: context) } super.draw(page, to: context) } } else if let background = self.background { // if let context = NSGraphicsContext.current?.cgContext { drawBackgroundPage(page, to: context) super.draw(page, to: context) // } } else if let headerFooter = self.headerFooter { super.draw(page, to: context) // if let context = NSGraphicsContext.current?.cgContext { drawHeaderFooterPage(page, to: context) // } } else { super.draw(page, to: context) } } func needDraw(_ page: CPDFPage) -> Bool { guard let watermark = watermark else { return false} if (watermark.pageRangeType.rawValue == 0) { return true } // else if (self.watermarkModel.pageRangeType == 1) { // let array = self.watermarkModel.pagesString.components(separatedBy: ",") // let index: Int = Int(self.document.index(for: page)) // if (!array.contains("\(index+1)")) { // return false // } // } else if (watermark.pageRangeType == .odd) { let index: Int = Int(self.document.index(for: page)) return (index % 2 == 0) } else if (watermark.pageRangeType == .even) { let index: Int = Int(self.document.index(for: page)) return (index % 2 == 1) } else if (watermark.pageRangeType == .other) { if (watermark.pagesString.isEmpty) { return false } let array = watermark.pagesString.components(separatedBy: ",") let index: Int = Int(self.document.index(for: page)) if (!array.contains("\(index+1)")) { return false } } return true } func drawWatermarkPage(_ page: CPDFPage, to context: CGContext) { guard let watermark = watermark else { return} let pageBounds = page.bounds(for: .cropBox) let w: CGFloat = NSWidth(pageBounds) let h: CGFloat = NSHeight(pageBounds) let width: CGFloat = sqrt(w*w+h*h) let newRect: CGRect = CGRect(x: -(width-w)*0.5, y: -(width-h)*0.5, width: width, height: width) let new_w: CGFloat = NSWidth(newRect) let new_h: CGFloat = NSHeight(newRect) // NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: false) NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: false) page.transform(context, for: .cropBox) if (watermark.pagesString.count > 0) { let array = watermark.pagesString.components(separatedBy: ",") let index: Int = Int(self.document.index(for: page)) if (!array.contains("\(index)")) { return } } if (!watermark.text.isEmpty) { var font = watermark.getTextFont() var color = watermark.getTextColor() var red: CGFloat = 0 var green: CGFloat = 0 var blue: CGFloat = 0 color.usingColorSpaceName(NSColorSpaceName.calibratedRGB)?.getRed(&red, green: &green, blue: &blue, alpha: nil) color = NSColor(red: red, green: green, blue: blue, alpha: watermark.opacity) var size = NSZeroSize let style = NSMutableParagraphStyle() style.alignment = watermark.textAligement style.lineBreakMode = .byCharWrapping let dict = [ NSAttributedString.Key.paragraphStyle : style, NSAttributedString.Key.foregroundColor : color, NSAttributedString.Key.font : font as Any ] as [NSAttributedString.Key : Any] size = watermark.text.boundingRect(with: NSSize(width: 1000, height: 1000), options: NSString.DrawingOptions(rawValue: 3), attributes: dict).size let radian: CGFloat = watermark.rotation*(Double.pi/180.0) let t: CGAffineTransform = CGAffineTransform(rotationAngle: radian) var rect:CGRect = CGRect(x: 0, y: 0, width: size.width, height: size.height) if (watermark.isTilePage) { context.translateBy(x: w * 0.5, y: h * 0.5) context.concatenate(t) context.translateBy(x: -(w/2), y: -(h/2)) let verticalWidth: CGFloat = size.width + watermark.tileHorizontalSpace let horizontalHeight: CGFloat = size.height + watermark.tileVerticalSpace let line: Int = Int(((new_h-watermark.tileVerticalSpace)/horizontalHeight)+1) let row: Int = Int(((new_w-watermark.tileHorizontalSpace)/verticalWidth)+1) let point: CGPoint = CGPoint(x: w*0.5-size.width*0.5+watermark.horizontalSpace, y: h*0.5-size.height*0.5+watermark.verticalSpace) for i in 0 ..< line { for j in 0 ..< row { watermark.text.draw(in: NSRect(x: point.x+CGFloat(j)*verticalWidth, y: point.y+CGFloat(i)*horizontalHeight, width: size.width, height: size.height), withAttributes: dict) } } for i in 1 ..< line { for j in 0 ..< row { watermark.text.draw(in: NSRect(x: point.x+CGFloat(j)*verticalWidth, y: point.y-CGFloat(i)*horizontalHeight, width: size.width, height: size.height), withAttributes: dict) } } for i in 0 ..< line { for j in 1 ..< row { watermark.text.draw(in: NSRect(x: point.x-CGFloat(j)*verticalWidth, y: point.y+CGFloat(i)*horizontalHeight, width: size.width, height: size.height), withAttributes: dict) } } for i in 1 ..< line { for j in 1 ..< row { watermark.text.draw(in: NSRect(x: point.x-CGFloat(j)*verticalWidth, y: point.y-CGFloat(i)*horizontalHeight, width: size.width, height: size.height), withAttributes: dict) } } } else { if (watermark.verticalMode == 0) { rect.origin.y = pageBounds.size.height-rect.size.height } else if (watermark.verticalMode == 1) { rect.origin.y = (pageBounds.size.height-rect.size.height) * 0.5 } else { rect.origin.y = 0 } if (watermark.horizontalMode == 0) { rect.origin.x = 0 } else if (watermark.horizontalMode == 1) { rect.origin.x = (pageBounds.size.width-rect.size.width) * 0.5 } else { rect.origin.x = pageBounds.size.width-rect.size.width } rect.origin.x += watermark.horizontalSpace rect.origin.y += watermark.verticalSpace let contextCenter = CGPoint(x: rect.midX, y: rect.midY) context.translateBy(x: contextCenter.x, y: contextCenter.y) context.rotate(by: radian) context.translateBy(x: -contextCenter.x, y: -contextCenter.y) watermark.text.draw(in: rect, withAttributes: dict) } } else if (watermark.image != nil) { let tiffData = watermark.image.tiffRepresentation let bitmap: NSBitmapImageRep! bitmap = NSBitmapImageRep(data: tiffData!) let ciImage = CIImage(bitmapImageRep: bitmap) var size: NSSize = (ciImage?.extent.size)! size.width *= watermark.scale size.height *= watermark.scale let radian = watermark.rotation * (Double.pi / 180.0) let t: CGAffineTransform = CGAffineTransform(rotationAngle: radian) var rect = NSMakeRect(0, 0, size.width, size.height) if (watermark.isTilePage) { context.translateBy(x: w/2,y: h/2) context.concatenate(t) context.translateBy(x: -(w/2), y: -(h/2)) let verticalWidth: CGFloat = size.width + watermark.tileHorizontalSpace let horizontalHeight: CGFloat = size.height + watermark.tileVerticalSpace let line: Int = Int((new_h - watermark.tileVerticalSpace)/horizontalHeight + 1) let row: Int = Int((new_w - watermark.tileHorizontalSpace) / verticalWidth + 1) let point = NSPoint(x: w/2 - size.width/2, y: h/2 - size.height/2) for i in 0 ..< (line/2+1) { for j in 0 ..< row { let area = CGRect(x: point.x + CGFloat(j) * verticalWidth, y: point.y + CGFloat(i)*horizontalHeight, width: size.width, height: size.height) watermark.image.draw(in: area, from: NSZeroRect, operation: .overlay, fraction: watermark.opacity) } } for i in 1 ..< (line/2+1) { for j in 0 ..< row { let area = CGRect(x: point.x + CGFloat(j) * verticalWidth, y: point.y - CGFloat(i)*horizontalHeight, width: size.width, height: size.height) watermark.image.draw(in: area, from: NSZeroRect, operation: .overlay, fraction: watermark.opacity) } } for i in 0 ..< (line/2+1) { for j in 1 ..< row { let area = CGRect(x: point.x - CGFloat(j) * verticalWidth, y: point.y + CGFloat(i)*horizontalHeight, width: size.width, height: size.height) watermark.image.draw(in: area, from: NSZeroRect, operation: .overlay, fraction: watermark.opacity) } } for i in 1 ..< (line/2+1) { for j in 1 ..< row { let area = CGRect(x: point.x - CGFloat(j) * verticalWidth, y: point.y - CGFloat(i)*horizontalHeight, width: size.width, height: size.height) watermark.image.draw(in: area, from: NSZeroRect, operation: .overlay, fraction: watermark.opacity) } } } else { if (watermark.verticalMode == 0) { rect.origin.y = pageBounds.size.height-rect.size.height } else if (watermark.verticalMode == 1) { rect.origin.y = (pageBounds.size.height-rect.size.height) * 0.5 } else { rect.origin.y = 0 } if (watermark.horizontalMode == 0) { rect.origin.x = 0 } else if (watermark.horizontalMode == 1) { rect.origin.x = (pageBounds.size.width-rect.size.width) * 0.5 } else { rect.origin.x = pageBounds.size.width-rect.size.width } rect.origin.x += watermark.horizontalSpace rect.origin.y += watermark.verticalSpace let contextCenter = CGPoint(x: rect.midX, y: rect.midY) context.translateBy(x: contextCenter.x, y: contextCenter.y) context.rotate(by: radian) context.translateBy(x: -contextCenter.x, y: -contextCenter.y) watermark.image.draw(in: rect, from: NSZeroRect, operation: .overlay, fraction: watermark.opacity) } } NSGraphicsContext.restoreGraphicsState() } func drawBackgroundPage(_ page: CPDFPage, to context: CGContext) { guard let background = background else { return} let pageBounds = page.bounds(for: .cropBox) if let currentContext = NSGraphicsContext.current { NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: false) } NSGraphicsContext.saveGraphicsState() page.transform(context, for: .cropBox) if let backgroundColor = background.color, backgroundColor != NSColor.clear { var size = pageBounds.size size.width *= background.scale size.height *= background.scale let radian: CGFloat = background.rotation * (Double.pi/180.0) var transform = CGAffineTransform(rotationAngle: radian) var rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) rect = rect.applying(transform) if background.verticalMode == 0 { rect.origin.y = pageBounds.size.height - rect.size.height } else if background.verticalMode == 1 { rect.origin.y = (pageBounds.size.height - rect.size.height) / 2.0 } else { rect.origin.y = 0 } if background.horizontalMode == 0 { rect.origin.x = 0 } else if background.horizontalMode == 1 { rect.origin.x = (pageBounds.size.width - rect.size.width) / 2.0 } else { rect.origin.x = pageBounds.size.width - rect.size.width } rect.origin.y += background.verticalSpace rect.origin.x += background.horizontalSpace let contextCenter = CGPoint(x: rect.midX, y: rect.midY) context.translateBy(x: contextCenter.x, y: contextCenter.y) context.rotate(by: CGFloat(radian)) context.translateBy(x: -contextCenter.x, y: -contextCenter.y) var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 if let color = background.color?.usingColorSpaceName(NSColorSpaceName.calibratedRGB) { color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) } context.setFillColor(red: red, green: green, blue: blue, alpha: background.opacity ?? 1.0) context.fill(CGRect(x: rect.origin.x + (rect.size.width - size.width) / 2.0, y: rect.origin.y + (rect.size.height - size.height) / 2.0, width: size.width, height: size.height)) } else if let backgroundImage = background.image { var size = backgroundImage.size if background.imagePath.extension.lowercased() == ".pdf", FileManager.default.fileExists(atPath: background.imagePath) { // Handle PDF background image case } else { size = CGSize(width: size.width * 2, height: size.height * 2) } size.width *= background.scale size.height *= background.scale let radian: CGFloat = background.rotation * (Double.pi/180.0) var transform = CGAffineTransform(rotationAngle: radian) var rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) rect = rect.applying(transform) if background.verticalMode == 0 { rect.origin.y = pageBounds.size.height - rect.size.height } else if background.verticalMode == 1 { rect.origin.y = (pageBounds.size.height - rect.size.height) / 2.0 } else { rect.origin.y = 0 } if background.horizontalMode == 0 { rect.origin.x = 0 } else if background.horizontalMode == 1 { rect.origin.x = (pageBounds.size.width - rect.size.width) / 2.0 } else { rect.origin.x = pageBounds.size.width - rect.size.width } rect.origin.y += background.verticalSpace rect.origin.x += background.horizontalSpace let contextCenter = CGPoint(x: rect.midX, y: rect.midY) context.translateBy(x: contextCenter.x, y: contextCenter.y) context.rotate(by: radian) context.translateBy(x: -contextCenter.x, y: -contextCenter.y) backgroundImage.draw(in: CGRect(x: rect.origin.x + (rect.size.width - size.width) / 2.0, y: rect.origin.y + (rect.size.height - size.height) / 2.0, width: size.width, height: size.height), from: NSZeroRect, operation: .sourceOver, fraction: background.opacity) } NSGraphicsContext.restoreGraphicsState() } func drawHeaderFooterPage(_ page: CPDFPage, to context: CGContext) { guard let headerFooter = headerFooter else { return } let pageBounds = page.bounds(for: .cropBox) let xMargin = 0.0//pageBounds.origin.x * 2 let yMargin = 0.0//pageBounds.origin.y * 2 NSGraphicsContext.saveGraphicsState() if context != nil { NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: false) } page.transform(context, for: .cropBox) let color = headerFooter.getTextColor() let font = NSFont.boldSystemFont(ofSize: CGFloat(headerFooter.getTextFontSize())) var style = NSMutableParagraphStyle() style.alignment = .center style.lineBreakMode = .byCharWrapping var size = NSZeroSize var attributes: [NSAttributedString.Key: Any] = [ .paragraphStyle: style, .foregroundColor: color, .font: font ] let index = self.document?.index(for: page) ?? 0 var topOffset: CGFloat = 0 var bottomOffset: CGFloat = 0 let topLeftString = headerFooter.topLeftString var tString = replacingOldString(topLeftString, currentPage: Int(index)) size = tString.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attributes).size var posY = min(pageBounds.size.height - CGFloat((headerFooter.topMargin)), pageBounds.size.height - size.height) tString.draw(in: CGRect(x: Double(headerFooter.leftMargin) + xMargin, y: posY + yMargin, width: size.width, height: size.height), withAttributes: attributes) let topCenterString = headerFooter.topCenterString tString = replacingOldString(topCenterString, currentPage: Int(index)) size = tString.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attributes).size posY = min(pageBounds.size.height - CGFloat((headerFooter.topMargin)), pageBounds.size.height - size.height) tString.draw(in: CGRect(x: pageBounds.size.width/2 - size.width/2 + xMargin, y: posY + yMargin, width: size.width, height: size.height), withAttributes: attributes) let topRightString = headerFooter.topRightString tString = replacingOldString(topRightString, currentPage: Int(index)) size = tString.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attributes).size posY = min(pageBounds.size.height - CGFloat((headerFooter.topMargin)), pageBounds.size.height - size.height) tString.draw(in: CGRect(x: pageBounds.size.width - CGFloat((headerFooter.rightMargin)) - size.width + xMargin, y: posY + yMargin, width: size.width, height: size.height), withAttributes: attributes) let bottomLeftString = headerFooter.bottomLeftString tString = replacingOldString(bottomLeftString, currentPage: Int(index)) size = tString.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attributes).size posY = max(CGFloat((headerFooter.bottomMargin)) - size.height, 0) tString.draw(in: CGRect(x: Double(headerFooter.leftMargin) + xMargin, y: posY + yMargin, width: size.width, height: size.height), withAttributes: attributes) let bottomCenterString = headerFooter.bottomCenterString tString = replacingOldString(bottomCenterString, currentPage: Int(index)) size = tString.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attributes).size posY = max(CGFloat((headerFooter.bottomMargin)) - size.height, 0) tString.draw(in: CGRect(x: pageBounds.size.width/2 - size.width/2 + xMargin, y: posY + yMargin, width: size.width, height: size.height), withAttributes: attributes) let bottomRightString = headerFooter.bottomRightString tString = replacingOldString(bottomRightString, currentPage: Int(index)) size = tString.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: attributes).size posY = max(CGFloat((headerFooter.bottomMargin)) - size.height, 0) tString.draw(in: CGRect(x: pageBounds.size.width - CGFloat((headerFooter.rightMargin)) - size.width + xMargin, y: posY + yMargin, width: size.width, height: size.height), withAttributes: attributes) context.setStrokeColor(NSColor(red: 51.0/255.0, green: 186.0/255.0, blue: 234.0/255.0, alpha: 1.0).cgColor) context.setLineWidth(1.0) context.move(to: CGPoint(x: xMargin, y: CGFloat(headerFooter.bottomMargin) + bottomOffset)) context.addLine(to: CGPoint(x: xMargin + pageBounds.size.width, y: CGFloat(headerFooter.bottomMargin) + bottomOffset)) context.move(to: CGPoint(x: Int(xMargin) + headerFooter.leftMargin, y: 0)) context.addLine(to: CGPoint(x: xMargin + Double(headerFooter.leftMargin), y: pageBounds.size.height)) context.move(to: CGPoint(x: Int(xMargin + pageBounds.size.width) - (headerFooter.rightMargin), y: 0)) context.addLine(to: CGPoint(x: xMargin + pageBounds.size.width - Double((headerFooter.rightMargin)), y: pageBounds.size.height)) context.move(to: CGPoint(x: xMargin, y: pageBounds.size.height - CGFloat((headerFooter.topMargin)) - topOffset)) context.addLine(to: CGPoint(x: xMargin + pageBounds.size.width, y: pageBounds.size.height - CGFloat((headerFooter.topMargin)) - topOffset)) let arr: [CGFloat] = [8, 1] context.setLineDash(phase: 0, lengths: arr) context.drawPath(using: .stroke) NSGraphicsContext.restoreGraphicsState() } func replacingOldString(_ oldString: String?, currentPage page: Int) -> String { guard let oldString = oldString else { return "" } var newString = oldString let startString = self.headerFooter?.startString ?? "" var newPage = "" if let isBates = self.headerFooter?.isBates, isBates { let pattern = "<<#\\d*?#\\d*#.*?>>|<<#\\d*?#\\d*?>>" if let dates = match(pattern, in: oldString) { for result in dates { let resultStr = (oldString as NSString).substring(with: result.range) let newResultStr = (resultStr as NSString).substring(with: NSMakeRange(2, resultStr.count - 4)) let array = newResultStr.components(separatedBy: "#") if array.count > 1 { let batesDigits = array[1] if array.count > 2 { let firstString = array[2] newPage = String(format: "%0*ld", batesDigits.count, page + (firstString as NSString).integerValue) } if array.count > 3 { newPage = "\(array[3])\(newPage)" } if array.count > 4 { newPage += array[4] } newString = newString.replacingOccurrences(of: resultStr, with: newPage) } } } } else { newPage = "\(page + (startString as NSString).integerValue)" if !startString.isEmpty { newString = KMWatermarkPDFView.convertViewPageFormat(newString, currentPage: newPage, pageCount: "\(self.document?.pageCount ?? UInt(0 + (startString as NSString).integerValue - 1))") } } newString = KMWatermarkPDFView.convertDateFormat(newString) return newString } func match(_ pattern: String, in testString: String) -> [NSTextCheckingResult]? { do { let regex = try NSRegularExpression(pattern: pattern, options: []) let results = regex.matches(in: testString, options: [], range: NSRange(location: 0, length: testString.count)) return results } catch { return nil } } static func convertDateFormat(_ oldString: String) -> String { var newString = oldString for dateFormat in KMHeaderFooterManager.defaultManager.dateFormatArray { if newString.contains(dateFormat) { let formatString = dateFormat.replacingOccurrences(of: "m", with: "M") let replace = "<<\(dateFormat)>>" let date = Date() let dateFormatter = DateFormatter() dateFormatter.dateFormat = formatString let dateString = dateFormatter.string(from: date) newString = newString.replacingOccurrences(of: replace, with: dateString) } } return newString } static func convertViewPageFormat(_ oldString: String, currentPage: String, pageCount: String) -> String { var newString = oldString let pageFormatArray = ["1", "1 of n", "1/n", "Page 1", "Page 1 of n"] for pageFormat in pageFormatArray { let pageFormat = "<<\(pageFormat)>>" if newString.contains(pageFormat) { var tString: String? switch pageFormat { case "<<1>>": tString = currentPage case "<<1 of n>>": tString = "\(currentPage) of \(pageCount)" case "<<1/n>>": tString = "\(currentPage)/\(pageCount)" case "<>": tString = "Page \(currentPage)" case "<>": tString = "Page \(currentPage) of \(pageCount)" default: break } if let tString = tString { newString = newString.replacingOccurrences(of: pageFormat, with: tString) } } } return newString } }