// // KMSnapshotPDFView.swift // PDF Reader Pro // // Created by tangchao on 2023/12/12. // import Cocoa class KMSnapshotPDFView: CPDFListView { private var _scalePopUpButton: NSPopUpButton? var scalePopUpButton: NSPopUpButton? { get { if (self._scalePopUpButton == nil) { let scrollView = self.documentView() scrollView?.hasHorizontalScroller = true // create it let scalePopUpButton_ = NSPopUpButton(frame: NSMakeRect(0.0, 0.0, 1.0, 1.0), pullsDown: false) scalePopUpButton_.cell?.controlSize = .small self._scalePopUpButton = scalePopUpButton_ scalePopUpButton_.isBordered = false scalePopUpButton_.isEnabled = true scalePopUpButton_.refusesFirstResponder = true (scalePopUpButton_.cell as? NSPopUpButtonCell)?.usesItemFromMenu = true // set a suitable font, the control size is 0, 1 or 2 scalePopUpButton_.font = .toolTipsFont(ofSize: CONTROL_FONT_SIZE) var cnt = 0 var numberOfDefaultItems = self._SKDefaultScaleMenuFactorsCount var curItem: AnyObject? var label = "" var width: CGFloat = 0.0 var maxWidth: CGFloat = 0.0 let size = NSMakeSize(1000.0, 1000.0) let attrs = [NSAttributedString.Key.font : scalePopUpButton_.font ?? .systemFont(ofSize: 13)] var maxIndex = 0 // fill it for i in 0 ..< numberOfDefaultItems { label = Bundle.main.localizedString(forKey: self._SKDefaultScaleMenuLabels[i], value: "", table: "ZoomValues") width = NSWidth(label.boundingRect(with: size, options: NSString.DrawingOptions(rawValue: 0), attributes: attrs)) if (width > maxWidth) { maxWidth = width maxIndex = i } scalePopUpButton_.addItem(withTitle: label) curItem = scalePopUpButton_.item(at: i) // [curItem setRepresentedObject:(SKDefaultScaleMenuFactors[cnt] > 0.0 ? [NSNumber numberWithDouble:SKDefaultScaleMenuFactors[cnt]] : nil)]; let fac = self._SKDefaultScaleMenuFactors[i] if fac > 0.0 { (curItem as? NSMenuItem)?.representedObject = NSNumber(value: fac) } else { (curItem as? NSMenuItem)?.representedObject = nil } } // Make sure the popup is big enough to fit the largest cell scalePopUpButton_.selectItem(at: maxIndex) scalePopUpButton_.sizeToFit() scalePopUpButton_.setFrameSize(NSMakeSize(NSWidth(scalePopUpButton_.frame) - CONTROL_WIDTH_OFFSET, CONTROL_HEIGHT)) // // // select the appropriate item, adjusting the scaleFactor if necessary if self.autoFits { self._setScaleFactor(0, adjustPopup: true) } else { self._setScaleFactor(self.scaleFactor, adjustPopup: true) } // // // hook it up scalePopUpButton_.target = self scalePopUpButton_.action = #selector(_scalePopUpAction) // // // don't let it become first responder scalePopUpButton_.refusesFirstResponder = true } return _scalePopUpButton } } var autoFitPage: CPDFPage? var autoFitRect: NSRect = .zero var autoFits = false var startScale: CGFloat = 0 var minHistoryIndex = 0 private let CONTROL_FONT_SIZE = 10.0 private let CONTROL_HEIGHT = 15.0 private let CONTROL_WIDTH_OFFSET = 20.0 private let _SKDefaultScaleMenuLabels = ["Auto", "10%", "20%", "25%", "35%", "50%", "60%", "71%", "85%", "100%", "120%", "141%", "170%", "200%", "300%", "400%", "600%", "800%", "1000%", "1200%", "1400%", "1700%", "2000%"] private let _SKDefaultScaleMenuFactors = [0.0, 0.1, 0.2, 0.25, 0.35, 0.5, 0.6, 0.71, 0.85, 1.0, 1.2, 1.41, 1.7, 2.0, 3.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 17.0, 20.0] private let _SKMinDefaultScaleMenuFactor = 0.1 private let _SKDefaultScaleMenuFactorsCount = 23 private let SKPDFContentViewChangedNotification = "SKPDFContentViewChangedNotification" deinit { KMPrint("KMSnapshotPDFView deinit.") NotificationCenter.default.removeObserver(self) } override init(frame frameRect: NSRect) { super.init(frame: frameRect) self._commonInitialization() } required init?(coder: NSCoder) { super.init(coder: coder) self._commonInitialization() } override func layout() { super.layout() let scrollView = self.documentView() let clipView = scrollView?.contentView // KMPrint(scrollView?.frame) // KMPrint(clipView?.frame) scrollView?.frame = self.bounds } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) // Drawing code here. } @objc func handlePDFViewFrameChangedNotification(_ notification: NSNotification) { if self.autoFits { let clipView = self.documentView().contentView let clipRect = self.convert(clipView.visibleRect, from: clipView) var rect = self.convert(self.autoFitRect, from: self.autoFitPage) let factor = fmin(NSWidth(clipRect) / NSWidth(rect), NSHeight(clipRect) / NSHeight(rect)) rect = self.convert(NSInsetRect(rect, 0.5 * (NSWidth(rect) - NSWidth(clipRect) / factor), 0.5 * (NSHeight(rect) - NSHeight(clipRect) / factor)), to: self.autoFitPage) super.scaleFactor = factor * self.scaleFactor self.go(to: rect, on: self.autoFitPage) } } @objc func handlePDFContentViewFrameChangedDelayedNotification(_ notification: NSNotification) { if self.inLiveResize == false && (self.window?.isZoomed ?? false) == false { self._resetAutoFitRectIfNeeded() } } @objc func handlePDFContentViewFrameChangedNotification(_ notification: NSNotification) { if self.inLiveResize == false && (self.window?.isZoomed ?? false) == false { let note = Notification(name: Notification.Name(SKPDFContentViewChangedNotification), object: self) NotificationQueue.default.enqueue(note, postingStyle: .whenIdle, coalesceMask: .onName, forModes: nil) } } @objc func handlePDFViewScaleChangedNotification(_ notification: NSNotification) { if self.autoFits == false { self._setScaleFactor(fmax(self.scaleFactor, _SKMinDefaultScaleMenuFactor), adjustPopup: true) } } func setAutoScales() { } @IBAction override func zoomIn(_ sender: Any?) { if self.autoFits { super.zoomIn(sender) self._setAutoFits(false, adjustPopup: true) } else { let numberOfDefaultItems = _SKDefaultScaleMenuFactorsCount var i = self._lowerIndex(for: self.scaleFactor) if i < numberOfDefaultItems-1 { i += 1 } self._setScaleFactor(_SKDefaultScaleMenuFactors[Int(i)]) } } @IBAction override func zoomOut(_ sender: Any?) { if self.autoFits { super.zoomOut(sender) self._setAutoFits(false, adjustPopup: true) } else { var i = self._upperIndex(for: self.scaleFactor) if i > 1 { i -= 1 } self._setScaleFactor(_SKDefaultScaleMenuFactors[Int(i)]) } } override var canZoomIn: Bool { if super.canZoomIn == false { return false } let numberOfDefaultItems = _SKDefaultScaleMenuFactorsCount let i = self._lowerIndex(for: self.scaleFactor) return i < numberOfDefaultItems - 1 } override var canZoomOut: Bool { if super.canZoomOut == false { return false } let i = self._upperIndex(for: self.scaleFactor) return i > 1 } override func go(to page: CPDFPage!) { super.go(to: page) self._resetAutoFitRectIfNeeded() } @objc func doAutoFit(_ sender: Any?) { self.autoFits = true } @objc func doActualSize(_ sender: Any?) { self.scaleFactor = 1.0 } override func menu(for event: NSEvent) -> NSMenu? { let selectionActions: NSSet = NSSet(objects: ["copy:", "_searchInSpotlight:", "_searchInGoogle:", "_searchInDictionary:", "_revealSelection:"]) var menu = super.menu(for: event) if menu == nil { menu = NSMenu() } menu?.insertItem(.separator(), at: 0) menu?.insertItem(withTitle: KMLocalizedString("Print"), action: #selector(menuItemClick_Print), target: self, at: 0) var item = menu?.insertItem(withTitle: KMLocalizedString("Export"), action: nil, target: self, at: 0) let subMenu = NSMenu() subMenu.addItem(title: KMLocalizedString("PNG"), action: #selector(menuItemClick_ExportPNG), target: self) subMenu.addItem(title: KMLocalizedString("JPG"), action: #selector(menuItemClick_ExportJPG), target: self) subMenu.addItem(title: KMLocalizedString("PDF"), action: #selector(menuItemClick_ExportPDF), target: self) item?.submenu = subMenu menu?.insertItem(.separator(), at: 0) menu?.insertItem(withTitle: KMLocalizedString("Copy"), action: #selector(menuItemClick_Copy), target: self, at: 0) // [self setCurrentSelection:RUNNING_AFTER(10_11) ? [[[PDFSelection alloc] initWithDocument:[self document]] autorelease] : nil]; self.currentSelection = CPDFSelection(document: self.document) if let _menu = menu { while (_menu.numberOfItems > 0) { if let item = _menu.item(at: 0), item.action != nil { if item.isSeparatorItem || self.validate(item) == false || selectionActions.contains(NSStringFromSelector(item.action!)) { menu?.removeItem(at: 0) } else { break } } else { break } } } if let i = menu?.indexOfItem(withTarget: self, andAction: NSSelectorFromString("_setAutoSize:")), i != -1 { menu?.item(at: i)?.action = #selector(doAutoFit) } if let i = menu?.indexOfItem(withTarget: self, andAction: NSSelectorFromString("_setActualSize:")), i != -1 { menu?.item(at: i)?.action = #selector(doActualSize) var item = menu?.insertItem(withTitle: KMLocalizedString("Physical Size"), action: #selector(doPhysicalSize), target: self, at: i + 1) // item?.keyEquivalentModifierMask = [.alternate] // [item setKeyEquivalentModifierMask:NSAlternateKeyMask]; // [item setAlternate:YES]; item?.isAlternate = true } return menu } @objc func doPhysicalSize(_ sender: Any?) { // [self setPhysicalScaleFactor:1.0]; } override func magnify(with event: NSEvent) { if KMDataManager.ud_bool(forKey: SKDisablePinchZoomKey) == false && event.responds(to: NSSelectorFromString("magnification")) { if event.phase == .began { self.startScale = self.scaleFactor } let magnifyFactor = (1.0 + fmax(-0.5, fmin(1.0 , event.magnification))) super.scaleFactor = magnification * self.scaleFactor if event.phase == .ended || event.phase == .cancelled && (fabs(self.startScale-self.scaleFactor) > 0.001) { self._setScaleFactor(fmax(self.scaleFactor, _SKMinDefaultScaleMenuFactor), adjustPopup: true) } } } /* - (BOOL)canGoBack { if ([self respondsToSelector:@selector(currentHistoryIndex)] && minHistoryIndex > 0) return minHistoryIndex < [self currentHistoryIndex]; else return [super canGoBack]; } // we don't want to steal the printDocument: action from the responder chain - (void)printDocument:(id)sender{} - (BOOL)respondsToSelector:(SEL)aSelector { return aSelector != @selector(printDocument:) && [super respondsToSelector:aSelector]; } #pragma mark Gestures - (void)beginGestureWithEvent:(NSEvent *)theEvent { if ([[SKSnapshotPDFView superclass] instancesRespondToSelector:_cmd]) [super beginGestureWithEvent:theEvent]; startScale = [self scaleFactor]; } - (void)endGestureWithEvent:(NSEvent *)theEvent { if (fabs(startScale - [self scaleFactor]) > 0.001) [self setScaleFactor:fmax([self scaleFactor], SKMinDefaultScaleMenuFactor) adjustPopup:YES]; if ([[SKSnapshotPDFView superclass] instancesRespondToSelector:_cmd]) [super endGestureWithEvent:theEvent]; } - (void)magnifyWithEvent:(NSEvent *)theEvent { } #pragma mark Dragging - (void)mouseDown:(NSEvent *)theEvent{ [[self window] makeFirstResponder:self]; if ([theEvent standardModifierFlags] == (NSCommandKeyMask | NSShiftKeyMask)) { [self doPdfsyncWithEvent:theEvent]; } else { [self doDragWithEvent:theEvent]; } } - (void)setCursorForAreaOfInterest:(PDFAreaOfInterest)area { if ([NSEvent standardModifierFlags] == (NSCommandKeyMask | NSShiftKeyMask)) [[NSCursor arrowCursor] set]; else [[NSCursor openHandCursor] set]; } */ func thumbnailWithSize(_ size: CGFloat) -> NSImage? { // NSView *clipView = [[[self documentView] enclosingScrollView] contentView]; let clipView = self.documentView().contentView var bounds = self.convert(clipView.bounds, from: clipView) guard let imageRep = self.bitmapImageRepForCachingDisplay(in: bounds) else { return nil } var transform: NSAffineTransform? var thumbnailSize = bounds.size var shadowBlurRadius = 0.0 var shadowOffset = 0.0 var image: NSImage? self.cacheDisplay(in: bounds, to: imageRep) bounds.origin = .zero if (size > 0.0) { shadowBlurRadius = round(size / 32.0) shadowOffset = -ceil(shadowBlurRadius * 0.75) if (NSHeight(bounds) > NSWidth(bounds)) { thumbnailSize = NSMakeSize(round((size - 2.0 * shadowBlurRadius) * NSWidth(bounds) / NSHeight(bounds) + 2.0 * shadowBlurRadius), size) } else { thumbnailSize = NSMakeSize(size, round((size - 2.0 * shadowBlurRadius) * NSHeight(bounds) / NSWidth(bounds) + 2.0 * shadowBlurRadius)) } transform = NSAffineTransform() transform?.translateX(by: shadowBlurRadius, yBy: shadowBlurRadius - shadowOffset) transform?.scaleX(by: (thumbnailSize.width - 2.0 * shadowBlurRadius) / NSWidth(bounds), yBy: (thumbnailSize.height - 2.0 * shadowBlurRadius) / NSHeight(bounds)) } image = NSImage(size: thumbnailSize) image?.lockFocus() NSGraphicsContext.current?.imageInterpolation = .high transform?.concat() NSGraphicsContext.saveGraphicsState() // [[PDFView defaultPageBackgroundColor] set]; // if (shadowBlurRadius > 0.0) { // [NSShadow setShadowWithColor:[NSColor colorWithCalibratedWhite:0.0 alpha:0.5] blurRadius:shadowBlurRadius yOffset:shadowOffset]; // } __NSRectFill(bounds) NSGraphicsContext.current?.imageInterpolation = .default NSGraphicsContext.restoreGraphicsState() imageRep.draw(in: bounds) image?.unlockFocus() return image } func resetHistory() { // if ([self respondsToSelector:@selector(currentHistoryIndex)]) // minHistoryIndex = [self currentHistoryIndex]; if self.responds(to: NSSelectorFromString("currentHistoryIndex")) { // self.current } } // MARK: - Menu @objc func menuItemClick_Print(_ sender: AnyObject?) { // NSImage * image = [self thumbnailWithSize:0.0]; // // PDFPage *page = [[[PDFPage alloc] initWithImage:image] autorelease]; // // PDFDocument *pdfDocument = [[[PDFDocument alloc] init] autorelease]; // [pdfDocument insertPage:page atIndex:0]; // // NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo]; // // NSPrintOperation *printOperation = nil; // if ([pdfDocument respondsToSelector:@selector(printOperationForPrintInfo:scalingMode:autoRotate:)]){ // printOperation = [pdfDocument printOperationForPrintInfo:printInfo scalingMode:kPDFPrintPageScaleNone autoRotate:YES]; // } // // [printOperation runOperationModalForWindow:self.window delegate:self didRunSelector:nil contextInfo:NULL]; } @objc func menuItemClick_ExportPDF(_ sender: NSMenuItem) { guard let image = self.thumbnailWithSize(0) else { return } let document = CPDFDocument() document?.km_insert(image: image, at: 0) NSPanel.savePanel_pdf_success(self.window!, document: document) { url in NSWorkspace.shared.selectFile(url.path, inFileViewerRootedAtPath: "") } } @objc func menuItemClick_ExportJPG(_ sender: NSMenuItem) { guard let image = self.thumbnailWithSize(0) else { return } NSPanel.savePanel_data_success(self.window!, imageData: image.jpgData(), allowedTypes: ["jpg"]) { url in NSWorkspace.shared.selectFile(url.path, inFileViewerRootedAtPath: "") } } @objc func menuItemClick_Copy(_ sender: AnyObject?) { guard let image = self.thumbnailWithSize(0) else { return } if let tiffData = image.tiffRepresentation { let pasteboardItem = NSPasteboardItem() pasteboardItem.setData(tiffData, forType: .tiff) let pboard = NSPasteboard.general pboard.clearContents() pboard.writeObjects([pasteboardItem]) } } @objc func menuItemClick_ExportPNG(_ sender: NSMenuItem) { guard let image = self.thumbnailWithSize(0) else { return } NSPanel.savePanel_data_success(self.window!, imageData: image.pngData(), allowedTypes: ["png"]) { url in NSWorkspace.shared.selectFile(url.path, inFileViewerRootedAtPath: "") } } } // MARK: - Private Methods extension KMSnapshotPDFView { private func _commonInitialization() { self._scalePopUpButton = nil self.autoFitPage = nil self.autoFitRect = .zero NotificationCenter.default.addObserver(self, selector: #selector(handlePDFViewFrameChangedNotification), name: NSView.frameDidChangeNotification, object: self) NotificationCenter.default.addObserver(self, selector: #selector(handlePDFViewFrameChangedNotification), name: NSView.boundsDidChangeNotification, object: self) NotificationCenter.default.addObserver(self, selector: #selector(handlePDFContentViewFrameChangedNotification), name: NSView.boundsDidChangeNotification, object: self.documentView()?.contentView) NotificationCenter.default.addObserver(self, selector: #selector(handlePDFContentViewFrameChangedDelayedNotification), name: NSNotification.Name(SKPDFContentViewChangedNotification), object: self) // if ([PDFView instancesRespondToSelector:@selector(magnifyWithEvent:)] == NO || [PDFView instanceMethodForSelector:@selector(magnifyWithEvent:)] == [NSView instanceMethodForSelector:@selector(magnifyWithEvent:)]) // [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handlePDFViewScaleChangedNotification:) // name:PDFViewScaleChangedNotification object:self]; } private func _setScaleFactor(_ newScaleFactor: CGFloat) { self._setScaleFactor(newScaleFactor, adjustPopup: true) } private func _setScaleFactor(_ newScaleFactor: CGFloat, adjustPopup flag: Bool) { var newValue = newScaleFactor if flag { let i = self._index(for: newScaleFactor) self.scalePopUpButton?.selectItem(at: Int(i)) newValue = self._SKDefaultScaleMenuFactors[Int(i)] } if self.autoFits { self._setAutoFits(false, adjustPopup: false) } super.scaleFactor = newValue } private func _lowerIndex(for scaleFactor: CGFloat) -> UInt { var count = self._SKDefaultScaleMenuFactorsCount for i in 0 ..< count { let index = count-i-1 if (scaleFactor * 1.01 > self._SKDefaultScaleMenuFactors[index]) { return UInt(index) } } return 1 } private func _upperIndex(for scaleFactor: CGFloat) -> UInt { var count = self._SKDefaultScaleMenuFactorsCount for i in 1 ..< count { if (scaleFactor * 0.99 < self._SKDefaultScaleMenuFactors[i]) { return UInt(i) } } return UInt(count - 1) } private func _index(for scaleFactor: CGFloat) -> UInt { let lower = self._lowerIndex(for: scaleFactor) let upper = self._upperIndex(for: scaleFactor) if (upper > lower && scaleFactor < 0.5 * (self._SKDefaultScaleMenuFactors[Int(lower)] + self._SKDefaultScaleMenuFactors[Int(upper)])) { return lower } return upper } private func _setAutoFits(_ newAuto: Bool) { self._setAutoFits(newAuto, adjustPopup: true) } private func _setAutoFits(_ newAuto: Bool, adjustPopup flag: Bool) { if (self.autoFits != newAuto) { self.autoFits = newAuto; if (self.autoFits) { super.autoScales = false self._resetAutoFitRectIfNeeded() if (flag) { self.scalePopUpButton?.selectItem(at: 0) } } else { self.autoFitPage = nil self.autoFitRect = NSZeroRect if (flag) { self._setScaleFactor(self.scaleFactor, adjustPopup: flag) } } } } private func _resetAutoFitRectIfNeeded() { if self.autoFits { if let clipView = self.documentView()?.contentView { self.autoFitPage = self.currentPage() let rect = self.convert(clipView.visibleRect, from: clipView) self.autoFitRect = self.convert(rect, to: self.autoFitPage) } } } @objc private func _scalePopUpAction(_ sender: AnyObject?) { // NSNumber *selectedFactorObject = [[sender selectedItem] representedObject]; let selectedFactorObject = self.scalePopUpButton?.selectedItem?.representedObject if let data = selectedFactorObject as? NSNumber { self._setScaleFactor(data.doubleValue, adjustPopup: false) } else { self._setAutoFits(true, adjustPopup: false) } } } extension KMSnapshotPDFView { override func validate(_ menuItem: NSMenuItem) -> Bool { if (menuItem.action == #selector(doAutoFit)) { menuItem.state = self.autoFits ? .on : .off return true } else if (menuItem.action == #selector(doActualSize)) { menuItem.state = fabs(self.scaleFactor - 1.0) < 0.1 ? .on : .off return true } // else if (menuItem.action == #selector(doPhysicalSize)) { // [menuItem setState:([self autoScales] || fabs([self physicalScaleFactor] - 1.0 ) > 0.01) ? NSOffState : NSOnState]; // return true // } // } else if ([[SKSnapshotPDFView superclass] instancesRespondToSelector:_cmd]) { // return [super validateMenuItem:menuItem]; // } return true } }