update at 2026-03-15 15:58:14

This commit is contained in:
douboer@gmail.com
2026-03-15 15:58:14 +08:00
parent b38d932a05
commit 4b280073d4
24 changed files with 1509 additions and 596 deletions

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env sh
set -eu
ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)"
CALENDAR_DIR="$ROOT_DIR/calendar"
DIST_DIR="$CALENDAR_DIR/dist"
PORT=${PORT:-4173}
URL=${1:-"http://127.0.0.1:$PORT/?mode=background"}
OUT_PNG=${2:-"$DIST_DIR/kindlebg.png"}
OUT_REGION=${3:-"$DIST_DIR/clock-region.json"}
cd "$CALENDAR_DIR"
npm run build >/dev/null
python3 -m http.server "$PORT" -d "$DIST_DIR" >/tmp/kindle-calendar-http.log 2>&1 &
SERVER_PID=$!
trap 'kill "$SERVER_PID" 2>/dev/null || true' EXIT INT TERM
sleep 1
/usr/bin/swift "$CALENDAR_DIR/scripts/export-kindle-background.swift" "$URL" "$OUT_PNG" "$OUT_REGION"
node "$CALENDAR_DIR/scripts/generate-dashboard-manifest.mjs" >/dev/null
cat "$OUT_REGION"

View File

@@ -0,0 +1,186 @@
#!/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()

View File

@@ -0,0 +1,50 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const currentDir = path.dirname(fileURLToPath(import.meta.url));
const distDir = path.resolve(currentDir, '../dist');
const manifestPath = path.join(distDir, 'dashboard-manifest.json');
const clockRegionPath = path.join(distDir, 'clock-region.json');
const defaultClockRegion = {
x: 313,
y: 0,
width: 220,
height: 220,
};
const clockRegion = fs.existsSync(clockRegionPath)
? {
...defaultClockRegion,
...JSON.parse(fs.readFileSync(clockRegionPath, 'utf8')),
}
: defaultClockRegion;
const manifest = {
background: {
path: 'kindlebg.png',
url: 'https://shell.biboer.cn:20001/kindlebg.png',
updatedAt: new Date().toISOString(),
refreshIntervalMinutes: 120,
},
clockRegion,
clockFace: {
path: 'assets/clock-face.png',
managedOnKindle: true,
designWidth: 220,
designHeight: 220,
},
clockHands: {
hourPattern: 'assets/hour-hand/%03d.png',
minutePattern: 'assets/minute-hand/%02d.png',
refreshIntervalMinutes: 1,
networkRequired: false,
anchorMode: 'baked-into-patch',
scaleWithClockFace: false,
},
};
fs.mkdirSync(distDir, { recursive: true });
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
console.log(`Wrote ${manifestPath}`);