120 lines
3.9 KiB
JavaScript
120 lines
3.9 KiB
JavaScript
#!/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();
|