@gheop/tojiru 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/cli.js +30 -15
- package/dist/cli.js.map +1 -1
- package/dist/convert.js +16 -2
- package/dist/convert.js.map +1 -1
- package/dist/extractors/pdf.js +36 -3
- package/dist/extractors/pdf.js.map +1 -1
- package/dist/output/single-file.js +69 -0
- package/dist/output/single-file.js.map +1 -0
- package/package.json +1 -1
- package/reader/reader.js +53 -6
package/README.md
CHANGED
|
@@ -79,6 +79,17 @@ Generation shows per-page progress on stderr (e.g. `Converting 12/30`).
|
|
|
79
79
|
| `-t, --title <title>` | Document title shown in the reader |
|
|
80
80
|
| `-f, --force` | Overwrite a non-empty output folder |
|
|
81
81
|
| `--serve` | Start a preview server on the output folder after converting |
|
|
82
|
+
| `--single-file [file]` | Output a single portable HTML file instead of a folder |
|
|
83
|
+
|
|
84
|
+
### Single file
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
tojiru book.pdf --single-file book.html
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Produces one `.html` file with all pages, thumbnails, and the reader bundled in. Double-click it to read offline — no server, no extra files. Works on any OS that has a browser.
|
|
91
|
+
|
|
92
|
+
Capped at 30 MB of page data. For large documents, use the folder output and host it.
|
|
82
93
|
|
|
83
94
|
### Preview server
|
|
84
95
|
|
|
@@ -134,6 +145,18 @@ MIT — see [LICENSE](LICENSE).
|
|
|
134
145
|
|
|
135
146
|
## Changelog
|
|
136
147
|
|
|
148
|
+
### v0.4.0 — Single-file output (2026-06-26)
|
|
149
|
+
|
|
150
|
+
- `--single-file [file]` option: bundles all pages, thumbnails, and the reader into one portable HTML file
|
|
151
|
+
- Double-click the file to read offline — no server, no folder needed
|
|
152
|
+
- Size guard: rejects documents whose page data exceeds 30 MB and tells you to use folder output instead
|
|
153
|
+
- Folder mode and all existing tests unchanged
|
|
154
|
+
|
|
155
|
+
### v0.3.0 — Smaller PDF bundles (2026-06-26)
|
|
156
|
+
|
|
157
|
+
- Vector PDF page coordinates are rounded to 2 decimals — ~25% smaller pages, no visible change
|
|
158
|
+
- Image-only PDF pages (scans, comic PDFs) are auto-detected and rendered as WebP instead of SVG-wrapped bitmaps — e.g. a comic PDF dropped from ×34 to ×2.7
|
|
159
|
+
|
|
137
160
|
### v0.2.0 — Preview server and progress (2026-06-25)
|
|
138
161
|
|
|
139
162
|
- Built-in `tojiru serve <dir>` command previews a bundle locally — uses Node built-ins only, no extra install needed
|
package/dist/cli.js
CHANGED
|
@@ -27,33 +27,48 @@ program
|
|
|
27
27
|
.option('-t, --title <title>', 'document title')
|
|
28
28
|
.option('-f, --force', 'overwrite a non-empty output folder')
|
|
29
29
|
.option('--serve', 'start a preview server after converting')
|
|
30
|
+
.option('--single-file [file]', 'output a single portable HTML file (double-click to read offline)')
|
|
30
31
|
.action(async (input, opts) => {
|
|
31
32
|
try {
|
|
32
33
|
if (!existsSync(input))
|
|
33
34
|
throw new Error(`File not found: ${input}`);
|
|
34
|
-
const outDir = opts.out ?? basename(input).replace(/\.[^.]+$/, '');
|
|
35
|
-
if (existsSync(outDir) && (await readdir(outDir)).length > 0 && !opts.force) {
|
|
36
|
-
throw new Error(`Folder ${outDir} is not empty. Use --force to overwrite.`);
|
|
37
|
-
}
|
|
38
35
|
const isTTY = process.stderr.isTTY === true;
|
|
39
36
|
const onProgress = isTTY
|
|
40
37
|
? (done, total, label) => {
|
|
41
38
|
process.stderr.write(`\r${label} ${done}/${total}`);
|
|
42
39
|
}
|
|
43
40
|
: undefined;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
41
|
+
if (opts.singleFile !== undefined) {
|
|
42
|
+
// Single-file mode: bundle into a temp dir, write one HTML, remove temp dir.
|
|
43
|
+
const htmlPath = typeof opts.singleFile === 'string'
|
|
44
|
+
? opts.singleFile
|
|
45
|
+
: basename(input).replace(/\.[^.]+$/, '') + '.html';
|
|
46
|
+
const r = await convert(input, { outDir: '', title: opts.title, onProgress, singleFile: htmlPath });
|
|
47
|
+
if (isTTY)
|
|
48
|
+
process.stderr.write('\x1b[2K\r');
|
|
49
|
+
console.log(`✓ ${r.pageCount} pages → ${htmlPath}`);
|
|
50
|
+
process.stderr.write(` Double-click the file to read it offline.\n`);
|
|
54
51
|
}
|
|
55
52
|
else {
|
|
56
|
-
|
|
53
|
+
// Folder mode
|
|
54
|
+
const outDir = opts.out ?? basename(input).replace(/\.[^.]+$/, '');
|
|
55
|
+
if (existsSync(outDir) && (await readdir(outDir)).length > 0 && !opts.force) {
|
|
56
|
+
throw new Error(`Folder ${outDir} is not empty. Use --force to overwrite.`);
|
|
57
|
+
}
|
|
58
|
+
const r = await convert(input, { outDir, title: opts.title, onProgress });
|
|
59
|
+
// Clear progress line before success message
|
|
60
|
+
if (isTTY)
|
|
61
|
+
process.stderr.write('\x1b[2K\r');
|
|
62
|
+
console.log(`✓ ${r.pageCount} pages → ${r.outDir}/`);
|
|
63
|
+
if (opts.serve) {
|
|
64
|
+
const server = await serve(outDir);
|
|
65
|
+
process.stderr.write(`Serving ${outDir} at ${server.url}\n`);
|
|
66
|
+
openBrowser(server.url);
|
|
67
|
+
// Keep running — http server holds the event loop open
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
process.stderr.write(` Preview: tojiru serve ${outDir}\n`);
|
|
71
|
+
}
|
|
57
72
|
}
|
|
58
73
|
}
|
|
59
74
|
catch (e) {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAElC,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAA;IAC/D,IAAI,CAAC;QAAC,KAAK,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,CAAA;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,iBAAiB,CAAC,CAAC;AACpG,CAAC;AAED,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;AAC7B,OAAO;KACJ,IAAI,CAAC,QAAQ,CAAC;KACd,WAAW,CAAC,qDAAqD,CAAC;KAClE,OAAO,CAAC,OAAO,CAAC,CAAA;AAEnB,4DAA4D;AAC5D,OAAO;KACJ,OAAO,CAAC,iBAAiB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;KAC/C,WAAW,CAAC,2CAA2C,CAAC;KACxD,MAAM,CAAC,iBAAiB,EAAE,eAAe,CAAC;KAC1C,MAAM,CAAC,qBAAqB,EAAE,gBAAgB,CAAC;KAC/C,MAAM,CAAC,aAAa,EAAE,qCAAqC,CAAC;KAC5D,MAAM,CAAC,SAAS,EAAE,yCAAyC,CAAC;KAC5D,MAAM,CAAC,KAAK,EAAE,KAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/convert.js
CHANGED
|
@@ -10,6 +10,7 @@ import { djvuExtractor } from './extractors/djvu.js';
|
|
|
10
10
|
import { processPages } from './pages.js';
|
|
11
11
|
import { buildManifest } from './manifest.js';
|
|
12
12
|
import { writeFolder } from './output/folder.js';
|
|
13
|
+
import { writeSingleFile } from './output/single-file.js';
|
|
13
14
|
const EXTRACTORS = [pdfExtractor, cbzExtractor, cb7Extractor, cbrExtractor, djvuExtractor];
|
|
14
15
|
export async function convert(input, opts) {
|
|
15
16
|
const kind = await detectKind(input);
|
|
@@ -18,18 +19,31 @@ export async function convert(input, opts) {
|
|
|
18
19
|
throw new Error(kind ? `Format not yet supported: ${kind}` : 'Unrecognised format');
|
|
19
20
|
}
|
|
20
21
|
const work = await mkdtemp(join(tmpdir(), 'tojiru-'));
|
|
22
|
+
// In single-file mode, pages are staged in a temporary bundle dir that gets
|
|
23
|
+
// cleaned up after the HTML is written. In folder mode, outDir is the final output.
|
|
24
|
+
const bundleDir = opts.singleFile
|
|
25
|
+
? await mkdtemp(join(tmpdir(), 'tojiru-bundle-'))
|
|
26
|
+
: opts.outDir;
|
|
21
27
|
try {
|
|
22
28
|
const doc = await extractor.extract(input, work, opts.onProgress);
|
|
23
29
|
if (opts.title)
|
|
24
30
|
doc.title = opts.title;
|
|
25
31
|
if (doc.pages.length === 0)
|
|
26
32
|
throw new Error('No pages extracted.');
|
|
27
|
-
const pages = await processPages(doc,
|
|
28
|
-
|
|
33
|
+
const pages = await processPages(doc, bundleDir, {}, opts.onProgress);
|
|
34
|
+
const manifest = buildManifest(doc.title, doc.kind, pages);
|
|
35
|
+
if (opts.singleFile) {
|
|
36
|
+
await writeSingleFile(manifest, bundleDir, opts.singleFile);
|
|
37
|
+
return { outDir: opts.singleFile, pageCount: doc.pages.length };
|
|
38
|
+
}
|
|
39
|
+
await writeFolder(manifest, bundleDir);
|
|
29
40
|
return { outDir: opts.outDir, pageCount: doc.pages.length };
|
|
30
41
|
}
|
|
31
42
|
finally {
|
|
32
43
|
await rm(work, { recursive: true, force: true });
|
|
44
|
+
if (opts.singleFile) {
|
|
45
|
+
await rm(bundleDir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
33
47
|
}
|
|
34
48
|
}
|
|
35
49
|
//# sourceMappingURL=convert.js.map
|
package/dist/convert.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"convert.js","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"convert.js","sourceRoot":"","sources":["../src/convert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAGzD,MAAM,UAAU,GAAgB,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,CAAC,CAAA;AAcvG,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,KAAa,EAAE,IAAoB;IAC/D,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,CAAA;IACpC,MAAM,SAAS,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;IACzD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,6BAA6B,IAAI,EAAE,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAA;IACrF,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,CAAC,CAAC,CAAA;IACrD,4EAA4E;IAC5E,oFAAoF;IACpF,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU;QAC/B,CAAC,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC,CAAC;QACjD,CAAC,CAAC,IAAI,CAAC,MAAM,CAAA;IAEf,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;QACjE,IAAI,IAAI,CAAC,KAAK;YAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAA;QACtC,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAA;QAClE,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;QACrE,MAAM,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QAC1D,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,MAAM,eAAe,CAAC,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;YAC3D,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,CAAA;QACjE,CAAC;QACD,MAAM,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;QACtC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,CAAA;IAC7D,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAChD,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACpB,MAAM,EAAE,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACvD,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/dist/extractors/pdf.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
1
|
+
import { readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, extname, join } from 'node:path';
|
|
3
3
|
import { detectKind } from './detect.js';
|
|
4
4
|
import { findPdfConverter } from '../tools.js';
|
|
5
5
|
import { run } from '../run.js';
|
|
6
|
+
import sharp from 'sharp';
|
|
7
|
+
import { imageDims } from './images.js';
|
|
6
8
|
function pad(n, width) {
|
|
7
9
|
return String(n).padStart(width, '0');
|
|
8
10
|
}
|
|
@@ -33,6 +35,19 @@ function viewBox(svg) {
|
|
|
33
35
|
return { w: Math.round(Number(w[1])), h: Math.round(Number(h[1])) };
|
|
34
36
|
throw new Error('SVG has no usable dimensions');
|
|
35
37
|
}
|
|
38
|
+
// Rounds floats with ≥3 decimal places to `decimals` places.
|
|
39
|
+
// Integers and short floats (≤2 decimals) are unchanged.
|
|
40
|
+
// Safe for glyph outlines and <use> positions at 2 decimals (0.01 pt precision).
|
|
41
|
+
export function roundCoords(svg, decimals = 2) {
|
|
42
|
+
return svg.replace(/-?\d+\.\d{3,}/g, (m) => String(Number(parseFloat(m).toFixed(decimals))));
|
|
43
|
+
}
|
|
44
|
+
// A page is raster-dominated when pdftocairo wrapped a full-page bitmap in SVG:
|
|
45
|
+
// at least one <image> element and fewer than 50 <use> elements (vector glyphs).
|
|
46
|
+
function isRasterDominated(svg) {
|
|
47
|
+
const imageCount = (svg.match(/<image/g) ?? []).length;
|
|
48
|
+
const useCount = (svg.match(/<use/g) ?? []).length;
|
|
49
|
+
return imageCount >= 1 && useCount < 50;
|
|
50
|
+
}
|
|
36
51
|
export const pdfExtractor = {
|
|
37
52
|
name: 'pdf',
|
|
38
53
|
async canHandle(file) {
|
|
@@ -47,7 +62,8 @@ export const pdfExtractor = {
|
|
|
47
62
|
const width = Math.max(4, String(count).length);
|
|
48
63
|
const pages = [];
|
|
49
64
|
for (let i = 1; i <= count; i++) {
|
|
50
|
-
const
|
|
65
|
+
const stem = pad(i, width);
|
|
66
|
+
const svgPath = join(workdir, `${stem}.svg`);
|
|
51
67
|
if (conv === 'pdftocairo') {
|
|
52
68
|
await run('pdftocairo', ['-svg', '-f', String(i), '-l', String(i), file, svgPath]);
|
|
53
69
|
}
|
|
@@ -55,7 +71,24 @@ export const pdfExtractor = {
|
|
|
55
71
|
await run('mutool', ['draw', '-F', 'svg', '-o', svgPath, file, String(i)]);
|
|
56
72
|
}
|
|
57
73
|
const svg = await readFile(svgPath, 'utf8');
|
|
58
|
-
|
|
74
|
+
if (conv === 'pdftocairo' && isRasterDominated(svg)) {
|
|
75
|
+
// Full-page bitmap wrapped in SVG: re-render to a raster image, encode it
|
|
76
|
+
// as WebP (far smaller than the lossless PNG render), and drop the SVG.
|
|
77
|
+
const stemPath = join(workdir, stem);
|
|
78
|
+
await run('pdftocairo', ['-png', '-singlefile', '-r', '150', '-f', String(i), '-l', String(i), file, stemPath]);
|
|
79
|
+
const pngPath = `${stemPath}.png`;
|
|
80
|
+
const webpPath = `${stemPath}.webp`;
|
|
81
|
+
await sharp(pngPath).webp({ quality: 82, effort: 6 }).toFile(webpPath);
|
|
82
|
+
await unlink(svgPath);
|
|
83
|
+
await unlink(pngPath);
|
|
84
|
+
pages.push({ type: 'raster', imagePath: webpPath, ...(await imageDims(webpPath)) });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Vector page: round coordinates to shrink SVG, then store.
|
|
88
|
+
const rounded = roundCoords(svg);
|
|
89
|
+
await writeFile(svgPath, rounded, 'utf8');
|
|
90
|
+
pages.push({ type: 'vector', svgPath, ...viewBox(rounded) });
|
|
91
|
+
}
|
|
59
92
|
onProgress?.(i, count, 'Converting');
|
|
60
93
|
}
|
|
61
94
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pdf.js","sourceRoot":"","sources":["../../src/extractors/pdf.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"pdf.js","sourceRoot":"","sources":["../../src/extractors/pdf.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { readerDir } from './folder.js';
|
|
4
|
+
const MB = 1024 * 1024;
|
|
5
|
+
const SIZE_LIMIT = 30 * MB;
|
|
6
|
+
function escapeScript(s) {
|
|
7
|
+
return s.replace(/<\/script>/gi, '<\\/script>');
|
|
8
|
+
}
|
|
9
|
+
function escapeHtml(s) {
|
|
10
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
11
|
+
}
|
|
12
|
+
export async function writeSingleFile(manifest, bundleDir, outFile) {
|
|
13
|
+
// Collect all page + thumb file paths
|
|
14
|
+
const allFiles = [];
|
|
15
|
+
for (const p of manifest.pages) {
|
|
16
|
+
allFiles.push(p.file);
|
|
17
|
+
allFiles.push(p.thumb);
|
|
18
|
+
}
|
|
19
|
+
// Size guard: compute total bytes before building the output
|
|
20
|
+
const buffers = new Map();
|
|
21
|
+
let totalBytes = 0;
|
|
22
|
+
for (const f of allFiles) {
|
|
23
|
+
const buf = await readFile(join(bundleDir, f));
|
|
24
|
+
buffers.set(f, buf);
|
|
25
|
+
totalBytes += buf.length;
|
|
26
|
+
}
|
|
27
|
+
if (totalBytes > SIZE_LIMIT) {
|
|
28
|
+
const mb = Math.round(totalBytes / MB);
|
|
29
|
+
throw new Error(`Single-file output would be ~${mb} MB — too large to be practical (browsers choke on huge inline HTML). Use the folder output instead.`);
|
|
30
|
+
}
|
|
31
|
+
// Build __TOJIRU_PAGES: file path → base64
|
|
32
|
+
const pagesMap = {};
|
|
33
|
+
for (const [f, buf] of buffers) {
|
|
34
|
+
pagesMap[f] = buf.toString('base64');
|
|
35
|
+
}
|
|
36
|
+
// Read reader assets from package
|
|
37
|
+
const rd = readerDir();
|
|
38
|
+
const css = await readFile(join(rd, 'reader.css'), 'utf8');
|
|
39
|
+
const js = await readFile(join(rd, 'reader.js'), 'utf8');
|
|
40
|
+
const html = [
|
|
41
|
+
'<!doctype html>',
|
|
42
|
+
'<html lang="en">',
|
|
43
|
+
'<head>',
|
|
44
|
+
'<meta charset="utf-8" />',
|
|
45
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
46
|
+
`<title>${escapeHtml(manifest.title)}</title>`,
|
|
47
|
+
'<style>',
|
|
48
|
+
css,
|
|
49
|
+
'</style>',
|
|
50
|
+
'</head>',
|
|
51
|
+
'<body>',
|
|
52
|
+
'<div id="reduce" title="Collapse / expand thumbnails">☰</div>',
|
|
53
|
+
'<nav id="menu" aria-label="Pages"></nav>',
|
|
54
|
+
'<div id="resize" title="Drag to resize"></div>',
|
|
55
|
+
'<main id="page"></main>',
|
|
56
|
+
`<script type="application/json" id="tojiru-manifest">${escapeScript(JSON.stringify(manifest))}</script>`,
|
|
57
|
+
'<script>',
|
|
58
|
+
`window.__TOJIRU_PAGES = ${escapeScript(JSON.stringify(pagesMap))};`,
|
|
59
|
+
'</script>',
|
|
60
|
+
'<script type="module">',
|
|
61
|
+
escapeScript(js),
|
|
62
|
+
'</script>',
|
|
63
|
+
'</body>',
|
|
64
|
+
'</html>',
|
|
65
|
+
'',
|
|
66
|
+
].join('\n');
|
|
67
|
+
await writeFile(outFile, html);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=single-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"single-file.js","sourceRoot":"","sources":["../../src/output/single-file.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/reader/reader.js
CHANGED
|
@@ -8,9 +8,39 @@ async function loadManifest() {
|
|
|
8
8
|
return res.json()
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// Returns raw bytes for a page or thumb file.
|
|
12
|
+
// When window.__TOJIRU_PAGES contains the key, decodes from base64 (single-file
|
|
13
|
+
// mode). Otherwise falls back to a network fetch (folder mode).
|
|
14
|
+
async function getPageBytes(key) {
|
|
15
|
+
const inline = window.__TOJIRU_PAGES
|
|
16
|
+
if (inline && Object.prototype.hasOwnProperty.call(inline, key)) {
|
|
17
|
+
const b64 = inline[key]
|
|
18
|
+
const bin = atob(b64)
|
|
19
|
+
const bytes = new Uint8Array(bin.length)
|
|
20
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
|
|
21
|
+
return bytes
|
|
22
|
+
}
|
|
23
|
+
const res = await fetch(key)
|
|
24
|
+
return new Uint8Array(await res.arrayBuffer())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// UTF-8-safe base64 of an SVG string (btoa is Latin1-only; accented text needs this).
|
|
28
|
+
function svgToBase64(text) {
|
|
29
|
+
const bytes = new TextEncoder().encode(text)
|
|
30
|
+
let bin = ''
|
|
31
|
+
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i])
|
|
32
|
+
return btoa(bin)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mimeFromExt(file) {
|
|
36
|
+
const ext = file.split('.').pop().toLowerCase()
|
|
37
|
+
if (ext === 'webp') return 'image/webp'
|
|
38
|
+
if (ext === 'png') return 'image/png'
|
|
39
|
+
return 'image/jpeg'
|
|
40
|
+
}
|
|
41
|
+
|
|
11
42
|
async function fetchSvg(file) {
|
|
12
|
-
const
|
|
13
|
-
const buf = new Uint8Array(await res.arrayBuffer())
|
|
43
|
+
const buf = await getPageBytes(file)
|
|
14
44
|
// If the host already applied Content-Encoding: gzip, the browser inflated it
|
|
15
45
|
// and these bytes are plain SVG (no gzip magic). Only inflate when the bytes
|
|
16
46
|
// are actually gzip — so the reader works on any host, no header dependency.
|
|
@@ -32,12 +62,24 @@ async function loadPage(p) {
|
|
|
32
62
|
text = text.replace(/(<svg[^>]*?)\swidth="[^"]*"/i, '$1').replace(/(<svg[^>]*?)\sheight="[^"]*"/i, '$1')
|
|
33
63
|
const obj = document.createElement('object')
|
|
34
64
|
obj.type = 'image/svg+xml'
|
|
35
|
-
|
|
36
|
-
|
|
65
|
+
if (window.__TOJIRU_PAGES) {
|
|
66
|
+
// Single file opened via file:// has an opaque (null) origin, so blob: URLs
|
|
67
|
+
// become blob:null/… which <object> refuses to load. A data: URL works on any
|
|
68
|
+
// origin. base64 keeps the SVG bytes intact through UTF-8 (accented text).
|
|
69
|
+
obj.data = `data:image/svg+xml;base64,${svgToBase64(text)}`
|
|
70
|
+
} else {
|
|
71
|
+
obj.data = URL.createObjectURL(new Blob([text], { type: 'image/svg+xml' }))
|
|
72
|
+
obj.addEventListener('load', () => URL.revokeObjectURL(obj.data), { once: true })
|
|
73
|
+
}
|
|
37
74
|
return obj
|
|
38
75
|
}
|
|
39
76
|
const img = document.createElement('img')
|
|
40
|
-
|
|
77
|
+
const inline = window.__TOJIRU_PAGES
|
|
78
|
+
if (inline && Object.prototype.hasOwnProperty.call(inline, p.file)) {
|
|
79
|
+
img.src = `data:${mimeFromExt(p.file)};base64,${inline[p.file]}`
|
|
80
|
+
} else {
|
|
81
|
+
img.src = p.file
|
|
82
|
+
}
|
|
41
83
|
img.alt = `page ${p.n}`
|
|
42
84
|
return img
|
|
43
85
|
}
|
|
@@ -52,7 +94,12 @@ function init(manifest) {
|
|
|
52
94
|
|
|
53
95
|
const thumbs = manifest.pages.map((p) => {
|
|
54
96
|
const t = document.createElement('img')
|
|
55
|
-
|
|
97
|
+
const inlinePages = window.__TOJIRU_PAGES
|
|
98
|
+
if (inlinePages && Object.prototype.hasOwnProperty.call(inlinePages, p.thumb)) {
|
|
99
|
+
t.src = `data:${mimeFromExt(p.thumb)};base64,${inlinePages[p.thumb]}`
|
|
100
|
+
} else {
|
|
101
|
+
t.src = p.thumb
|
|
102
|
+
}
|
|
56
103
|
t.loading = 'lazy'
|
|
57
104
|
t.alt = `page ${p.n}`
|
|
58
105
|
t.addEventListener('click', () => goTo(p.n))
|