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();
|