// // KMPDFSynchronizer.swift // PDF Reader Pro // // Created by lizhe on 2024/2/28. // import Cocoa enum KMPDFSynchronizerType: Int { case defaultOptions = 0 case showReadingBarMask = 1 case flippedMask = 2 } let PDFSYNC_TO_PDF: (CGFloat) -> CGFloat = { coord in return CGFloat(coord) / 65536.0 } let SKPDFSynchronizerPdfsyncExtension = "pdfsync" var SKPDFSynchronizerTexExtensions: [String] = ["tex", "ltx", "latex", "ctx", "lyx", "rnw"] let STACK_BUFFER_SIZE = 256 func caseInsensitiveStringEqual(_ item1: UnsafeRawPointer, _ item2: UnsafeRawPointer, _ size: ((UnsafeRawPointer) -> Int)?) -> Bool { return CFStringCompare(item1 as! CFString, item2 as! CFString, CFStringCompareFlags.compareCaseInsensitive) == CFComparisonResult.compareEqualTo } //func caseInsensitiveStringHash(item: UnsafeRawPointer, size: @escaping (UnsafeRawPointer) -> UInt) -> UInt { // guard let item = item else { return 0 } // // var hash: UInt = 0 // let allocator = CFGetAllocator(item) // let len = CFStringGetLength(item as! CFString) // // let STACK_BUFFER_SIZE = 1024 // 设置STACK_BUFFER_SIZE的值,以便与C代码匹配 // // var buffer = [UniChar](repeating: 0, count: STACK_BUFFER_SIZE) // let bufferPtr: UnsafeMutablePointer // if len + 10 >= STACK_BUFFER_SIZE { // bufferPtr = UnsafeMutablePointer(OpaquePointer(CFAllocatorAllocate(allocator, (len + 10) * MemoryLayout.size, 0))) // } else { // bufferPtr = UnsafeMutablePointer(mutating: buffer) // } // // CFStringGetCharacters(item as! CFString, CFRange(location: 0, length: len), bufferPtr) // // let mutableString = CFStringCreateMutableWithExternalCharactersNoCopy(allocator, bufferPtr, len, len + 10, (bufferPtr != buffer ? allocator : kCFAllocatorNull)) // CFStringLowercase(mutableString, nil) // hash = (mutableString as AnyObject).hash // CFRelease(mutableString) // // if bufferPtr != buffer { // CFAllocatorDeallocate(allocator, bufferPtr) // } // // return hash //} protocol KMPDFSynchronizerDelegate: AnyObject { func synchronizer(_ synchronizer: KMPDFSynchronizer, foundLine line: Int, inFile file: String) func synchronizer(_ synchronizer: KMPDFSynchronizer, foundLocation point: NSPoint, atPageIndex pageIndex: UInt, options: Int) } class KMPDFSynchronizer: NSObject { weak var delegate: KMPDFSynchronizerDelegate? var fileName: String = "" var shouldKeepRunning: Bool { OSMemoryBarrier() return shouldKeepRunningFlag == 1 } private var queue: DispatchQueue = DispatchQueue(label: "net.sourceforge.queue.KMPDFSynchronizer") private var lockQueue: DispatchQueue = DispatchQueue(label: "net.sourceforge.lockQueue.KMPDFSynchronizer") private var syncFileName: String = "" { didSet { if syncFileName.count == 0 { lastModDate = nil } else { do { let attributes = try FileManager.default.attributesOfItem(atPath: syncFileName) lastModDate = attributes[.modificationDate] as? Date } catch { } } } } private var lastModDate: Date? private var isPdfsync: Bool = true private var fileManager: FileManager = FileManager() private var pages: [Any] = [] private var lines: NSMapTable? private var filenames: NSMapTable? private var scanner: synctex_scanner_p? private var shouldKeepRunningFlag: Int32 = 1 func setFileName(_ newFileName: String) { // We compare filenames in canonical form throughout, so we need to make sure fileName also is in canonical form if let canonicalFileName = NSURL(fileURLWithPath: newFileName).resolvingSymlinksInPath?.standardizedFileURL.path { DispatchQueue.global().async { [weak self] in guard let self = self else { return } self.lockQueue.sync { if self.fileName != canonicalFileName { if self.fileName != newFileName { self.syncFileName = "" self.lastModDate = nil } self.fileName = canonicalFileName } } } } } func terminate() { delegate = nil let originalValue = OSAtomicCompareAndSwap32Barrier(1, 0, &shouldKeepRunningFlag) // originalValue 为原来的值,如果原来的值是 1,表示已经交换成功,如果是 0,表示没有成功交换 // 如果需要对返回值进行处理,可以在这里添加逻辑 } func findFileAndLine(forLocation point: NSPoint, inRect rect: NSRect, pageBounds bounds: NSRect, atPageIndex pageIndex: UInt) { // Implement this method } func findPageAndLocation(forLine line: Int, inFile file: String, options: Int) { // Implement this method } } //MARK: Support extension KMPDFSynchronizer { func sourceFile(forFileName file: String, isTeX: Bool, removeQuotes: Bool) -> String { var fileName = file if removeQuotes && fileName.count > 2 && fileName.first == "\"" && fileName.last == "\"" { fileName = String(fileName.dropFirst().dropLast()) } if !(fileName as NSString).isAbsolutePath { fileName = (self.fileName as NSString).deletingLastPathComponent + "/" + fileName } if isTeX && !FileManager.default.fileExists(atPath: fileName) && !SKPDFSynchronizerTexExtensions.contains((fileName as NSString).pathExtension.lowercased()) { for ext in SKPDFSynchronizerTexExtensions { let tryFile = fileName + "." + ext if FileManager.default.fileExists(atPath: tryFile) { fileName = tryFile break } } } // Swift's `standardizedFileURL` property does both `resolvingSymlinksInPath` and `standardizingPath` return URL(fileURLWithPath: fileName).standardizedFileURL.path } func defaultSourceFile() -> String { let file = (self.fileName as NSString).deletingPathExtension for `extension` in SKPDFSynchronizerTexExtensions { let tryFile = file + "." + `extension` if FileManager.default.fileExists(atPath: tryFile) { return tryFile } } return file + "." + SKPDFSynchronizerTexExtensions.first! } } //MARK: PDFSync extension KMPDFSynchronizer{ func recordForIndex(_ records: NSMapTable, _ recordIndex: Int) -> KMPDFSyncRecord { if let record = records.object(forKey: recordIndex as AnyObject) as? KMPDFSyncRecord { return record } else { let record = KMPDFSyncRecord(recordIndex: recordIndex) records.setObject(record, forKey: recordIndex as AnyObject) return record } } func loadPdfsyncFile(_ theFileName: String) -> Bool { pages.removeAll() if lines != nil { lines?.removeAllObjects() } else { // let keyPointerFunctions = NSPointerFunctions(options: [.strongMemory, .objectPersonality]) // keyPointerFunctions.isEqualFunction = { (a, b) in // caseInsensitiveStringEqual(a, b) { <#UnsafeRawPointer#> in // <#code#> // } // guard let strA = a as? String, let strB = b as? String else { return false } // return strA.caseInsensitiveCompare(strB) == .orderedSame // } // keyPointerFunctions.hashFunction = { (ptr) in // guard let str = ptr as? String else { return 0 } // return str.hash // } // let valuePointerFunctions = NSPointerFunctions(options: [.strongMemory, .objectPersonality]) // lines = NSMapTable(keyPointerFunctions: keyPointerFunctions, valuePointerFunctions: valuePointerFunctions,capacity: 0) } syncFileName = theFileName isPdfsync = true guard let pdfsyncString = try? String(contentsOfFile: theFileName, encoding: .utf8) else { return false } var rv = false let records = NSMapTable.strongToStrongObjects() let files = NSMutableArray() var recordIndex = 0, line = 0, pageIndex = 0 var x = 0.0, y = 0.0 var record: KMPDFSyncRecord? var array: NSMutableArray? var ch: unichar = 0 let sc = Scanner(string: pdfsyncString) let newlines = CharacterSet.newlines sc.charactersToBeSkipped = CharacterSet.whitespaces var file: NSString? var tryFile: String let scanString = sc.scanUpToCharacters(from: newlines) let scanCharactersString = sc.scanCharacters(from: newlines) if scanString?.count != 0 && scanCharactersString?.count != 0 { file = sourceFile(forFileName: scanString! as String, isTeX: true, removeQuotes: true) as NSString files.add(file!) array = NSMutableArray() lines!.setObject(array!, forKey: file!) sc.scanString("version", into: nil) sc.scanInt(nil) sc.scanCharacters(from: newlines, into: nil) // while sc.shouldKeepRunning() && sc.scanCharacter(&ch) { // switch ch { // case "l": // if sc.scanInt(&recordIndex) && sc.scanInt(&line) { // // we ignore the column // sc.scanInt(nil) // record = recordForIndex(records, recordIndex) // record!.file = file! as String // record!.line = line // lines!.object(forKey: file! as NSString)!.add(record!) // } // case "p": // // we ignore * and + modifiers // if !sc.scanString("*", into: nil) { // sc.scanString("+", into: nil) // } // if sc.scanInt(&recordIndex) && sc.scanDouble(&x) && sc.scanDouble(&y) { // record = recordForIndex(records, recordIndex) // record!.pageIndex = pages.count - 1 // record!.point = NSMakePoint(CGFloat(PDFSYNC_TO_PDF(x) + pdfOffset.x), CGFloat(PDFSYNC_TO_PDF(y) + pdfOffset.y)) // (pages.lastObject as! NSMutableArray).add(record!) // } // case "s": // // start of a new page, the scanned integer should always equal [pages count]+1 // var tempPageIndex = 0 // if !sc.scanInt(&tempPageIndex) { // pageIndex = pages.count + 1 // } else { // pageIndex = tempPageIndex // } // while pageIndex > pages.count { // array = NSMutableArray() // pages.add(array!) // } // case "(": // // start of a new source file // var tempFile: NSString? // if sc.scanUpToCharacters(from: newlines, into: &tempFile as? String) { // file = sourceFile(forFileName: tempFile! as String, isTeX: true, removeQuotes: true) as NSString // files.add(file!) // if lines!.object(forKey: file!) == nil { // array = NSMutableArray() // lines!.setObject(array!, forKey: file!) // } // } // case ")": // // closing of a source file // if files.count > 0 { // files.removeLastObject() // file = files.lastObject as? NSString // } // default: // // shouldn't reach // break // } // // sc.scanUpToCharacters(from: newlines, into: nil) // sc.scanCharacters(from: newlines, into: nil) // } let lineSortDescriptor = NSSortDescriptor(key: "line", ascending: true) let lineSortDescriptors = [lineSortDescriptor] // for array in lines!.objectEnumerator() { // (array as! NSMutableArray).sort(using: lineSortDescriptors) // } for array in pages { (array as! NSMutableArray).sort(using: [NSSortDescriptor(key: "y", ascending: false), NSSortDescriptor(key: "x", ascending: true)]) } // rv = sc.shouldKeepRunning } return rv } func pdfsyncFindFileLine(_ linePtr: inout Int, file filePtr: inout String?, forLocation point: NSPoint, inRect rect: NSRect, pageBounds bounds: NSRect, atPageIndex pageIndex: Int) -> Bool { var rv = false if pageIndex < pages.count { var record: KMPDFSyncRecord? var beforeRecord: KMPDFSyncRecord? var afterRecord: KMPDFSyncRecord? var atRecords = [Double: KMPDFSyncRecord]() // for case let tempRecord as KMPDFSyncRecord in pages[pageIndex] { // if tempRecord.line == 0 { // continue // } // let p = tempRecord.point // if p.y > NSMaxY(rect) { // beforeRecord = tempRecord // } else if p.y < NSMinY(rect) { // afterRecord = tempRecord // break // } else if p.x < NSMinX(rect) { // beforeRecord = tempRecord // } else if p.x > NSMaxX(rect) { // afterRecord = tempRecord // break // } else { // atRecords[abs(p.x - point.x)] = tempRecord // } // } var nearestRecord: KMPDFSyncRecord? if !atRecords.isEmpty { let nearest = atRecords.keys.sorted()[0] nearestRecord = atRecords[nearest] } else if let beforeRecord = beforeRecord, let afterRecord = afterRecord { let beforePoint = beforeRecord.point let afterPoint = afterRecord.point if beforePoint.y - point.y < point.y - afterPoint.y { nearestRecord = beforeRecord } else if beforePoint.y - point.y > point.y - afterPoint.y { nearestRecord = afterRecord } else if beforePoint.x - point.x < point.x - afterPoint.x { nearestRecord = beforeRecord } else if beforePoint.x - point.x > point.x - afterPoint.x { nearestRecord = afterRecord } else { nearestRecord = beforeRecord } } else if let beforeRecord = beforeRecord { nearestRecord = beforeRecord } else if let afterRecord = afterRecord { nearestRecord = afterRecord } if let record = nearestRecord { linePtr = record.line filePtr = record.file rv = true } } if !rv { print("PDFSync was unable to find file and line.") } return rv } func pdfsyncFindPage(_ pageIndexPtr: inout Int, location pointPtr: inout NSPoint, forLine line: Int, inFile file: String) -> Bool { var rv = false if let theLines = lines!.object(forKey: file as NSString) as? [KMPDFSyncRecord] { var record: KMPDFSyncRecord? var beforeRecord: KMPDFSyncRecord? var afterRecord: KMPDFSyncRecord? var atRecord: KMPDFSyncRecord? for tempRecord in theLines { if tempRecord.pageIndex == NSNotFound { continue } let l = tempRecord.line if l < line { beforeRecord = tempRecord } else if l > line { afterRecord = tempRecord break } else { atRecord = tempRecord break } } if let atRecord = atRecord { record = atRecord } else if let beforeRecord = beforeRecord, let afterRecord = afterRecord { let beforeLine = beforeRecord.line let afterLine = afterRecord.line if beforeLine - line > line - afterLine { record = afterRecord } else { record = beforeRecord } } else if let beforeRecord = beforeRecord { record = beforeRecord } else if let afterRecord = afterRecord { record = afterRecord } if let record = record { pageIndexPtr = record.pageIndex pointPtr = record.point rv = true } } if !rv { print("PDFSync was unable to find location and page.") } return rv } } //MARK: SyncTeX extension KMPDFSynchronizer { func loadSynctexFile(forFile theFileName: String) -> Bool { var rv = false if let scanner = scanner { synctex_scanner_free(scanner) } scanner = synctex_scanner_new_with_output_file(theFileName.cString(using: .utf8), nil, 1) // if let scanner = scanner { // let fileRep = synctex_scanner_get_synctex(scanner) // syncFileName = sourceFile(forFileName: String(cString: fileRep!), isTeX: false, removeQuotes: false) // if let filenames = filenames { // NSResetMapTable(filenames) // } else { // let keyPointerFunctions = NSPointerFunctions(options: [.strongMemory, .objectPersonality]) // keyPointerFunctions.isEqualFunction = { (a, b) in // guard let strA = a as? String, let strB = b as? String else { return false } // return strA.caseInsensitiveCompare(strB) == .orderedSame // } // keyPointerFunctions.hashFunction = { (ptr) in // guard let str = ptr as? String else { return 0 } // return str.hash // } // let valuePointerFunctions = NSPointerFunctions(options: [.mallocMemory, .cstringPersonality, .copyIn]) // filenames = NSMapTable(keyOptions: keyPointerFunctions, valueOptions: valuePointerFunctions) // } // var node = synctex_scanner_input(scanner) // repeat { // if let fileRep = synctex_scanner_get_name(scanner, synctex_node_tag(node)) { // filenames!.setObject(String(cString: fileRep), forKey: sourceFile(forFileName: String(cString: fileRep), isTeX: true, removeQuotes: false)) // } // } while ((node = synctex_node_next(node)) != nil) // isPdfsync = false // rv = shouldKeepRunning // } return rv } func synctexFindFileLine(_ linePtr: inout Int, file filePtr: inout String?, forLocation point: NSPoint, inRect rect: NSRect, pageBounds bounds: NSRect, atPageIndex pageIndex: Int) -> Bool { var rv = false if synctex_edit_query(scanner, Int32(pageIndex + 1), Float(Double(point.x)), Float(Double(NSMaxY(bounds) - point.y))) > 0 { var node: synctex_node_p? var file: UnsafePointer? while rv == false && (node = synctex_scanner_next_result(scanner)) != nil { if let tempFile = synctex_scanner_get_name(scanner, synctex_node_tag(node)) { linePtr = Int(max(synctex_node_line(node), 1) - 1) filePtr = sourceFile(forFileName: String(cString: tempFile), isTeX: true, removeQuotes: false) rv = true } } } if rv == false { NSLog("SyncTeX was unable to find file and line.") } return rv } func synctexFindPage(pageIndexPtr: UnsafeMutablePointer, pointPtr: UnsafeMutablePointer, forLine line: Int, inFile file: String) -> Bool { guard let filenames = filenames else { return false } var rv = false // var filename: UnsafeMutableRawPointer = NSMapGet(filenames, file) ?? UnsafeMutableRawPointer(<#Builtin.RawPointer#>) //?? NSMapGet(filenames, (file as NSString).resolvingSymlinksInPath.standardizingPath) // if filename == nil { // for fn in filenames.allKeys { // if let fnString = fn as? String, fnString.lastPathComponent.caseInsensitiveCompare(file.lastPathComponent) == .orderedSame { // filename = NSMapGet(filenames, fn) as? UnsafePointer // break // } // } // // if filename == nil { // filename = (file as NSString).lastPathComponent.utf8String // } // } // if synctex_display_query(scanner, filename, Int32(line) + 1, 0, -1) > 0 { // if let node = synctex_scanner_next_result(scanner) { // let page = UInt(synctex_node_page(node)) // pageIndexPtr.pointee = max(page, 1) - 1 // pointPtr.pointee = CGPoint(x: CGFloat(synctex_node_visible_h(node)), y: CGFloat(synctex_node_visible_v(node))) // rv = true // } // } if !rv { NSLog("SyncTeX was unable to find location and page.") } return rv } } //MARK: Generic extension KMPDFSynchronizer { func loadSyncFileIfNeeded() -> Bool { let theFileName = fileName var rv = false if theFileName.count != 0 { var theSyncFileName = self.syncFileName var modDate: NSDate? if theSyncFileName.count != 0 && fileManager.fileExists(atPath: theSyncFileName) { do { let attributes = try FileManager.default.attributesOfItem(atPath: theFileName) modDate = attributes[.modificationDate] as? NSDate } catch { } let currentModDate = self.lastModDate if (currentModDate != nil) && modDate?.compare(currentModDate!) != ComparisonResult.orderedDescending { rv = true } else if (isPdfsync) { rv = self.loadPdfsyncFile(theSyncFileName) } else { rv = self.loadSynctexFile(forFile: theFileName) } } else { rv = self.loadSynctexFile(forFile: theFileName) if rv == false { theSyncFileName = (theFileName as NSString).deletingPathExtension.stringByAppendingPathExtension("pdfsync") if fileManager.fileExists(atPath: theSyncFileName) { rv = self.loadPdfsyncFile(theSyncFileName) } } } } if rv == false { print("Unable to find or load synctex or pdfsync file.") } return rv } } //MARK: Finding API extension KMPDFSynchronizer { func findFileAndLine(forLocation point: NSPoint, inRect rect: NSRect, pageBounds bounds: NSRect, atPageIndex pageIndex: Int) { queue.async { guard self.shouldKeepRunning, self.loadSyncFileIfNeeded() else { return } var foundLine = 0 var foundFile: String? var success = false if self.isPdfsync { success = self.pdfsyncFindFileLine(&foundLine, file: &foundFile, forLocation: point, inRect: rect, pageBounds: bounds, atPageIndex: pageIndex) } else { success = self.synctexFindFileLine(&foundLine, file: &foundFile, forLocation: point, inRect: rect, pageBounds: bounds, atPageIndex: pageIndex) } if success, self.shouldKeepRunning { DispatchQueue.main.async { self.delegate?.synchronizer(self, foundLine: foundLine, inFile: foundFile ?? "") } } } } func findPageAndLocation(forLine line: Int, inFile file: String?, options: Int) { let fixedFile = file ?? defaultSourceFile() // queue.async { // guard let fixedFile = fixedFile, self.shouldKeepRunning, self.loadSyncFileIfNeeded() else { return } // // var foundPageIndex = NSNotFound // var foundPoint = NSZeroPoint // var foundOptions = options // // if self.isPdfsync { // self.pdfsyncFindPage(&foundPageIndex, location: &foundPoint, forLine: line, inFile: fixedFile) // } else { // self.synctexFindPage(&foundPageIndex, location: &foundPoint, forLine: line, inFile: fixedFile) // } // // if self.shouldKeepRunning() { // if self.isPdfsync { // foundOptions &= ~KMPDFSynchronizerType.flippedMask // } else { // foundOptions |= KMPDFSynchronizerType.flippedMask // } // DispatchQueue.main.async { // self.delegate?.synchronizer(self, foundLocation: foundPoint, atPageIndex: foundPageIndex, options: foundOptions) // } // } // } } }