#!/usr/bin/env swift import AppKit import Foundation import WebKit enum ExportError: Error, CustomStringConvertible { case invalidArguments case snapshotFailed case pngEncodingFailed(String) case clockRegionMissing var description: String { switch self { case .invalidArguments: return "用法:export-kindle-background.swift " case .snapshotFailed: return "网页截图失败" case let .pngEncodingFailed(path): return "PNG 编码失败:\(path)" case .clockRegionMissing: return "页面里没有找到 data-clock-region 标记" } } } final class SnapshotExporter: NSObject, WKNavigationDelegate { private let url: URL private let pngOutputURL: URL private let regionOutputURL: URL private let completion: (Result) -> Void private let targetSize = CGSize(width: 1024, height: 600) private lazy var window: NSWindow = { let window = NSWindow( contentRect: CGRect(origin: .zero, size: targetSize), styleMask: [.borderless], backing: .buffered, defer: false ) window.isReleasedWhenClosed = false window.backgroundColor = .white return window }() private lazy var webView: WKWebView = { let config = WKWebViewConfiguration() let view = WKWebView(frame: CGRect(origin: .zero, size: targetSize), configuration: config) view.navigationDelegate = self return view }() init(url: URL, pngOutputURL: URL, regionOutputURL: URL, completion: @escaping (Result) -> Void) { self.url = url self.pngOutputURL = pngOutputURL self.regionOutputURL = regionOutputURL self.completion = completion super.init() } func start() { window.contentView = webView window.orderBack(nil) webView.load(URLRequest(url: url)) } func webView(_ webView: WKWebView, didFinish _: WKNavigation!) { // 等页面数据与图片稳定下来,避免只截到骨架。 DispatchQueue.main.asyncAfter(deadline: .now() + 4) { self.capture() } } private func capture() { let regionScript = """ (() => { const node = document.querySelector('[data-clock-region="true"]'); if (!node) return null; const rect = node.getBoundingClientRect(); return { x: Math.round(rect.left), y: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height) }; })(); """ webView.evaluateJavaScript(regionScript) { value, error in if let error { self.finish(.failure(error)) return } guard let region = value as? [String: NSNumber] else { self.finish(.failure(ExportError.clockRegionMissing)) return } let snapshotConfig = WKSnapshotConfiguration() snapshotConfig.rect = CGRect(origin: .zero, size: self.targetSize) snapshotConfig.snapshotWidth = NSNumber(value: Float(self.targetSize.width)) self.webView.takeSnapshot(with: snapshotConfig) { image, snapshotError in if let snapshotError { self.finish(.failure(snapshotError)) return } guard let image else { self.finish(.failure(ExportError.snapshotFailed)) return } do { try self.savePNG(image: image, to: self.pngOutputURL) try self.saveRegion(region: region) self.finish(.success(())) } catch { self.finish(.failure(error)) } } } } private func savePNG(image: NSImage, to url: URL) throws { let normalizedImage = NSImage(size: NSSize(width: targetSize.width, height: targetSize.height)) normalizedImage.lockFocus() image.draw(in: NSRect(origin: .zero, size: targetSize)) normalizedImage.unlockFocus() guard let tiffRepresentation = normalizedImage.tiffRepresentation, let bitmap = NSBitmapImageRep(data: tiffRepresentation), let pngData = bitmap.representation(using: .png, properties: [:]) else { throw ExportError.pngEncodingFailed(url.path) } try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) try pngData.write(to: url) } private func saveRegion(region: [String: NSNumber]) throws { let jsonObject: [String: Int] = [ "x": region["x"]?.intValue ?? 0, "y": region["y"]?.intValue ?? 0, "width": region["width"]?.intValue ?? 0, "height": region["height"]?.intValue ?? 0, ] let data = try JSONSerialization.data(withJSONObject: jsonObject, options: [.prettyPrinted, .sortedKeys]) try FileManager.default.createDirectory(at: regionOutputURL.deletingLastPathComponent(), withIntermediateDirectories: true) try data.write(to: regionOutputURL) } private func finish(_ result: Result) { completion(result) NSApplication.shared.terminate(nil) } } guard CommandLine.arguments.count >= 4 else { fputs("\(ExportError.invalidArguments)\n", stderr) exit(1) } let url = URL(string: CommandLine.arguments[1])! let pngOutputURL = URL(fileURLWithPath: CommandLine.arguments[2]) let regionOutputURL = URL(fileURLWithPath: CommandLine.arguments[3]) let app = NSApplication.shared app.setActivationPolicy(.prohibited) let exporter = SnapshotExporter(url: url, pngOutputURL: pngOutputURL, regionOutputURL: regionOutputURL) { result in switch result { case .success: print("Exported \(pngOutputURL.path)") print("Region saved to \(regionOutputURL.path)") case let .failure(error): fputs("\(error)\n", stderr) exit(1) } } exporter.start() app.run()