Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
9.7 kB · 231 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232#!/usr/bin/env node// Verifies the output of the epub-optimize CLI run against the fixtures// produced by test/make-fixture.js.//// Usage: node test/verify.js [outputDir]// outputDir defaults to test/fixtures/optimized (the CLI's default --out// for an <input> of test/fixtures), resolved relative to process.cwd() —// run from the project root.//// Prints PASS/FAIL for each named check, a final "X passed, Y failed"// summary, and exits 1 if any check failed.
import { existsSync, readFileSync } from 'node:fs';import path from 'node:path';import JSZip from 'jszip';import sharp from 'sharp';
const outDir = process.argv[2] || path.join('test', 'fixtures', 'optimized');const fixturesDir = path.join('test', 'fixtures');
const sampleOutPath = path.join(outDir, 'sample.epub');const brokenOutPath = path.join(outDir, 'broken-image.epub');const brokenFixturePath = path.join(fixturesDir, 'broken-image.epub');const notesOutPath = path.join(outDir, 'notes.txt');
let passed = 0;let failed = 0;
function assert(cond, msg) { if (!cond) throw new Error(msg);}
async function check(name, fn) { try { await fn(); passed++; console.log(`PASS ${name}`); } catch (err) { failed++; console.log(`FAIL ${name}`); console.log(` ${err && err.message ? err.message : err}`); }}
/** Extract all <img ...> opening tags from an XHTML/HTML string. */function imgTags(text) { return text.match(/<img\b[^>]*>/gi) || [];}
/** * Read the FIRST local file header directly from raw zip bytes. This is the * most reliable way to confirm "mimetype" is the first entry and is stored * uncompressed — reading it back through a zip library would obscure both * ordering and the raw on-disk compression method. */function readFirstLocalFileHeader(buf) { assert(buf.length >= 30, 'buffer too small to contain a zip local file header'); const sig = buf.readUInt32LE(0); assert(sig === 0x04034b50, `first bytes are not a zip local file header (got 0x${sig.toString(16)})`); const compression = buf.readUInt16LE(8); const nameLen = buf.readUInt16LE(26); const name = buf.subarray(30, 30 + nameLen).toString('utf8'); return { compression, name };}
async function loadZip(filePath) { const buf = readFileSync(filePath); const zip = await JSZip.loadAsync(buf); return { buf, zip };}
function requireEntry(zip, entryPath) { const entry = zip.file(entryPath); if (!entry) throw new Error(`missing zip entry: ${entryPath}`); return entry;}
async function main() { console.log(`Verifying converted fixtures in: ${outDir}\n`);
// Load sample.epub once up front; individual checks below assert on the // shared load result rather than re-reading the file each time. let sampleBuf = null; let sampleZip = null; let sampleLoadErr = null; if (existsSync(sampleOutPath)) { try { const loaded = await loadZip(sampleOutPath); sampleBuf = loaded.buf; sampleZip = loaded.zip; } catch (err) { sampleLoadErr = err; } } else { sampleLoadErr = new Error(`missing file: ${sampleOutPath}`); }
function requireSampleZip() { if (sampleLoadErr) throw sampleLoadErr; if (!sampleZip) throw new Error('sample.epub zip was not loaded'); return sampleZip; }
await check('mimetype is the first zip entry and stored uncompressed (STORE)', async () => { if (sampleLoadErr) throw sampleLoadErr; assert(sampleBuf, 'sample.epub bytes were not read'); const header = readFirstLocalFileHeader(sampleBuf); assert(header.name === 'mimetype', `first zip entry is "${header.name}", expected "mimetype"`); assert(header.compression === 0, `mimetype compression method is ${header.compression}, expected 0 (STORE)`); });
await check('OEBPS/images/pic.jpg exists (pic.png removed), jpeg, <=480x800, grayscale', async () => { const zip = requireSampleZip(); assert(!zip.file('OEBPS/images/pic.png'), 'OEBPS/images/pic.png should have been removed'); const entry = requireEntry(zip, 'OEBPS/images/pic.jpg'); const buf = Buffer.from(await entry.async('nodebuffer')); const meta = await sharp(buf).metadata(); assert(meta.format === 'jpeg', `expected format "jpeg", got "${meta.format}"`); assert(meta.width <= 480, `width ${meta.width} exceeds 480`); assert(meta.height <= 800, `height ${meta.height} exceeds 800`); const isGrayscale = meta.space === 'b-w' || meta.channels === 1; assert( isGrayscale, `expected grayscale output (space "b-w" or 1 channel), got space="${meta.space}" channels=${meta.channels}` ); });
await check('OEBPS/images/cover.jpg exists and is jpeg', async () => { const zip = requireSampleZip(); const entry = requireEntry(zip, 'OEBPS/images/cover.jpg'); const buf = Buffer.from(await entry.async('nodebuffer')); const meta = await sharp(buf).metadata(); assert(meta.format === 'jpeg', `expected format "jpeg", got "${meta.format}"`); });
await check('OEBPS/ch1.xhtml: no img width/height, references pic.jpg, has defensive style, no svg', async () => { const zip = requireSampleZip(); const entry = requireEntry(zip, 'OEBPS/ch1.xhtml'); const text = await entry.async('text'); for (const tag of imgTags(text)) { assert(!/\s(width|height)\s*=/i.test(tag), `img tag still has width/height attribute: ${tag}`); } assert(text.includes('images/pic.jpg'), 'does not reference images/pic.jpg'); assert(!text.includes('images/pic.png'), 'still references images/pic.png'); assert(text.includes('img,svg{max-width:100%'), 'missing defensive style block (DEFENSIVE_STYLE)'); assert(!/<svg[\s>]/i.test(text), 'svg tag still present'); });
await check('OEBPS/cover.xhtml: no svg, contains img referencing the cover jpg', async () => { const zip = requireSampleZip(); const entry = requireEntry(zip, 'OEBPS/cover.xhtml'); const text = await entry.async('text'); assert(!/<svg[\s>]/i.test(text), 'svg tag still present in cover.xhtml'); const hasCoverImg = imgTags(text).some((tag) => /src=["'][^"']*cover\.jpg["']/i.test(tag)); assert(hasCoverImg, 'no <img> referencing a cover .jpg found'); });
await check('OEBPS/content.opf: jpeg media-types, no svg property, cover meta present', async () => { const zip = requireSampleZip(); const entry = requireEntry(zip, 'OEBPS/content.opf'); const text = await entry.async('text'); assert(text.includes('media-type="image/jpeg"'), 'no media-type="image/jpeg" found'); assert(!text.includes('media-type="image/png"'), 'media-type="image/png" still present'); assert(!text.includes('properties="svg"'), 'properties="svg" still present'); assert(/<meta\b[^>]*\bname="cover"/.test(text), 'missing <meta name="cover" element'); });
await check('OEBPS/toc.ncx: dtb:uid matches the OPF dc:identifier value', async () => { const zip = requireSampleZip(); const opfText = await requireEntry(zip, 'OEBPS/content.opf').async('text'); const ncxText = await requireEntry(zip, 'OEBPS/toc.ncx').async('text');
const idMatch = opfText.match(/<dc:identifier[^>]*id=["']uid["'][^>]*>([^<]+)<\/dc:identifier>/); assert(idMatch, 'could not locate dc:identifier with id="uid" in OPF'); const opfId = idMatch[1].trim();
const ncxMatch = ncxText.match(/<meta\s+name=["']dtb:uid["']\s+content=["']([^"']*)["']/i) || ncxText.match(/<meta\s+content=["']([^"']*)["']\s+name=["']dtb:uid["']/i); assert(ncxMatch, 'could not locate dtb:uid meta in NCX'); const ncxId = ncxMatch[1];
assert(ncxId !== 'WRONG-ID', 'toc.ncx dtb:uid still reads the fixture sentinel "WRONG-ID"'); assert(ncxId === opfId, `NCX dtb:uid "${ncxId}" does not match OPF identifier "${opfId}"`); });
await check('OEBPS/style.css references pic.jpg', async () => { const zip = requireSampleZip(); const text = await requireEntry(zip, 'OEBPS/style.css').async('text'); assert(text.includes('pic.jpg'), 'style.css does not reference pic.jpg'); assert(!text.includes('pic.png'), 'style.css still references pic.png'); });
await check( 'broken-image.epub: bad.png unchanged at its original path, no bad.jpg, xhtml still references bad.png', async () => { assert(existsSync(brokenOutPath), `missing file: ${brokenOutPath}`); assert( existsSync(brokenFixturePath), `missing source fixture: ${brokenFixturePath} (run make-fixture.js first)` );
const { zip: outZip } = await loadZip(brokenOutPath); const { zip: origZip } = await loadZip(brokenFixturePath);
const origEntry = requireEntry(origZip, 'OEBPS/images/bad.png'); const origBytes = Buffer.from(await origEntry.async('nodebuffer'));
const outEntry = requireEntry(outZip, 'OEBPS/images/bad.png'); const outBytes = Buffer.from(await outEntry.async('nodebuffer')); assert(outBytes.equals(origBytes), 'OEBPS/images/bad.png content changed; expected a byte-identical fallback');
assert(!outZip.file('OEBPS/images/bad.jpg'), 'unexpected OEBPS/images/bad.jpg entry present');
const pageText = await requireEntry(outZip, 'OEBPS/page.xhtml').async('text'); assert(pageText.includes('images/bad.png'), 'OEBPS/page.xhtml no longer references bad.png'); } );
await check('notes.txt was not copied to the output dir', async () => { assert(!existsSync(notesOutPath), `unexpected file present: ${notesOutPath}`); });
console.log(`\n${passed} passed, ${failed} failed`); process.exit(failed > 0 ? 1 : 0);}
main().catch((err) => { console.error('verify.js crashed unexpectedly:', err); process.exit(1);});