finance-tools/scripts/repack-pattern-images.mjs

120 lines
3.9 KiB
JavaScript
Raw Normal View History

2026-06-21 20:28:06 +00:00
#!/usr/bin/env node
// 將各書頁圖片整理到 pages/005/img_0_.jpg避免全書共用 3 張圖被覆寫。
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..', '..');
const PATTERNS_DIR = path.join(ROOT, 'content', 'raw', 'patterns');
const META_PATH = path.join(PATTERNS_DIR, 'metadata.json');
function fileMd5(filePath) {
return crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex');
}
function loadSharedHashes() {
const flatDir = path.join(PATTERNS_DIR, 'images');
const hashes = new Map();
if (!fs.existsSync(flatDir)) return hashes;
for (const fname of fs.readdirSync(flatDir)) {
if (!/\.(jpe?g|png|webp)$/i.test(fname)) continue;
hashes.set(fname, fileMd5(path.join(flatDir, fname)));
}
return hashes;
}
function copyIfExists(src, dest, { allowShared = false, sharedHashes = null } = {}) {
if (!fs.existsSync(src)) return false;
const fname = path.basename(dest);
if (!allowShared && sharedHashes?.has(fname) && fileMd5(src) === sharedHashes.get(fname)) {
return false;
}
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(src, dest);
return true;
}
function candidateSourceDirs() {
const dirs = [];
if (fs.existsSync(META_PATH)) {
try {
const meta = JSON.parse(fs.readFileSync(META_PATH, 'utf8'));
if (meta.output_dir) {
dirs.push(path.resolve(PATTERNS_DIR, meta.output_dir));
dirs.push(path.resolve(ROOT, meta.output_dir));
}
} catch { /* ignore */ }
}
dirs.push(path.join(PATTERNS_DIR, 'output'));
dirs.push(path.join(ROOT, '短線交易日線圖大全'));
return [...new Set(dirs)].filter((d) => fs.existsSync(d));
}
function imagesFromMd(mdPath) {
const raw = fs.readFileSync(mdPath, 'utf8');
return [...raw.matchAll(/!\[\]\((images\/[^)]+)\)/g)].map((m) => path.basename(m[1]));
}
function main() {
const force = process.argv.includes('--force');
const sources = candidateSourceDirs();
const sharedHashes = loadSharedHashes();
let copied = 0;
let missing = 0;
let skippedShared = 0;
for (let i = 1; i <= 72; i++) {
const pageId = String(i).padStart(3, '0');
const mdPath = path.join(PATTERNS_DIR, `${pageId}.md`);
if (!fs.existsSync(mdPath)) continue;
const names = imagesFromMd(mdPath);
if (!names.length) continue;
const destDir = path.join(PATTERNS_DIR, 'pages', pageId);
fs.mkdirSync(destDir, { recursive: true });
for (const name of names) {
const dest = path.join(destDir, name);
if (fs.existsSync(dest)) {
const isSharedArtifact = sharedHashes.get(name) === fileMd5(dest);
if (isSharedArtifact) fs.unlinkSync(dest);
else if (!force) continue;
}
let hit = false;
for (const srcRoot of sources) {
const tries = [
path.join(srcRoot, pageId, 'images', name),
path.join(srcRoot, `${pageId}.images`, name),
path.join(srcRoot, 'images', pageId, name),
path.join(srcRoot, pageId, name),
];
for (const src of tries) {
if (copyIfExists(src, dest, { sharedHashes })) {
copied += 1;
hit = true;
break;
}
}
if (hit) break;
}
if (!hit) {
// flat images/ 是擷取器跨頁共用的檔案,不能當成該頁教材圖。
skippedShared += 1;
missing += 1;
}
}
}
console.log(`[repack-pattern-images] copied=${copied} missing=${missing} shared_fallback=${skippedShared}`);
if (sources.length) console.log(`來源目錄:${sources.join(', ')}`);
if (missing > 0 || skippedShared > 0) {
console.log('缺少各頁專屬圖片;已略過擷取器產生的全書共用圖,避免教材顯示錯圖。');
}
}
main();