Files
kindle-calendar/calendar/scripts/export-kindle-background.swift
2026-03-15 15:58:14 +08:00

187 lines
5.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 <url> <png-output> <region-output>"
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, Error>) -> 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, Error>) -> 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<Void, Error>) {
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()