@gheop/tojiru 0.3.0 → 0.5.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 +36 -0
- package/dist/cli.js +34 -14
- package/dist/cli.js.map +1 -1
- package/dist/convert.js +16 -2
- package/dist/convert.js.map +1 -1
- package/dist/output/single-file.js +69 -0
- package/dist/output/single-file.js.map +1 -0
- package/dist/pages.js +19 -4
- package/dist/pages.js.map +1 -1
- package/package.json +1 -1
- package/reader/reader.js +53 -6
package/README.md
CHANGED
|
@@ -79,6 +79,29 @@ 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
|
+
| `--image-format <fmt>` | Raster page encoding: `keep` (as-is, default) or `webp` |
|
|
84
|
+
|
|
85
|
+
### Single file
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
tojiru book.pdf --single-file book.html
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
Capped at 30 MB of page data. For large documents, use the folder output and host it.
|
|
94
|
+
|
|
95
|
+
### Smaller comics
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
tojiru comic.cbz --out comic/ --image-format webp
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Comics keep their original images by default (`keep`), so the bundle matches the source.
|
|
102
|
+
Pass `--image-format webp` to re-encode raster pages as WebP — useful for colour comics
|
|
103
|
+
shipped as large PNGs. On the Pepper&Carrot sample this drops the bundle from 1.2 MB to
|
|
104
|
+
324 KB with no visible loss. Pages already in WebP are left untouched.
|
|
82
105
|
|
|
83
106
|
### Preview server
|
|
84
107
|
|
|
@@ -134,6 +157,19 @@ MIT — see [LICENSE](LICENSE).
|
|
|
134
157
|
|
|
135
158
|
## Changelog
|
|
136
159
|
|
|
160
|
+
### v0.5.0 — WebP comics (2026-06-26)
|
|
161
|
+
|
|
162
|
+
- `--image-format webp` re-encodes raster (comic) pages to WebP — the Pepper&Carrot sample drops from 1.2 MB to 324 KB. Default stays `keep` (images copied as-is). Pages already in WebP are left untouched.
|
|
163
|
+
- Re-converting into an existing folder now clears the old `pages/` and `thumbs/` first, so a format switch never leaves orphaned page files behind.
|
|
164
|
+
- The repo's comic demo is now WebP, cutting the committed `site/demo` from 1.4 MB to ~550 KB.
|
|
165
|
+
|
|
166
|
+
### v0.4.0 — Single-file output (2026-06-26)
|
|
167
|
+
|
|
168
|
+
- `--single-file [file]` option: bundles all pages, thumbnails, and the reader into one portable HTML file
|
|
169
|
+
- Double-click the file to read offline — no server, no folder needed
|
|
170
|
+
- Size guard: rejects documents whose page data exceeds 30 MB and tells you to use folder output instead
|
|
171
|
+
- Folder mode and all existing tests unchanged
|
|
172
|
+
|
|
137
173
|
### v0.3.0 — Smaller PDF bundles (2026-06-26)
|
|
138
174
|
|
|
139
175
|
- Vector PDF page coordinates are rounded to 2 decimals — ~25% smaller pages, no visible change
|
package/dist/cli.js
CHANGED
|
@@ -27,33 +27,53 @@ 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)')
|
|
31
|
+
.option('--image-format <fmt>', 'comic/raster page encoding: keep (as-is) or webp', 'keep')
|
|
30
32
|
.action(async (input, opts) => {
|
|
31
33
|
try {
|
|
32
34
|
if (!existsSync(input))
|
|
33
35
|
throw new Error(`File not found: ${input}`);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
throw new Error(`Folder ${outDir} is not empty. Use --force to overwrite.`);
|
|
36
|
+
if (opts.imageFormat !== 'keep' && opts.imageFormat !== 'webp') {
|
|
37
|
+
throw new Error(`--image-format must be "keep" or "webp" (got "${opts.imageFormat}")`);
|
|
37
38
|
}
|
|
39
|
+
const imageFormat = opts.imageFormat;
|
|
38
40
|
const isTTY = process.stderr.isTTY === true;
|
|
39
41
|
const onProgress = isTTY
|
|
40
42
|
? (done, total, label) => {
|
|
41
43
|
process.stderr.write(`\r${label} ${done}/${total}`);
|
|
42
44
|
}
|
|
43
45
|
: undefined;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
if (opts.singleFile !== undefined) {
|
|
47
|
+
// Single-file mode: bundle into a temp dir, write one HTML, remove temp dir.
|
|
48
|
+
const htmlPath = typeof opts.singleFile === 'string'
|
|
49
|
+
? opts.singleFile
|
|
50
|
+
: basename(input).replace(/\.[^.]+$/, '') + '.html';
|
|
51
|
+
const r = await convert(input, { outDir: '', title: opts.title, onProgress, singleFile: htmlPath, imageFormat });
|
|
52
|
+
if (isTTY)
|
|
53
|
+
process.stderr.write('\x1b[2K\r');
|
|
54
|
+
console.log(`✓ ${r.pageCount} pages → ${htmlPath}`);
|
|
55
|
+
process.stderr.write(` Double-click the file to read it offline.\n`);
|
|
54
56
|
}
|
|
55
57
|
else {
|
|
56
|
-
|
|
58
|
+
// Folder mode
|
|
59
|
+
const outDir = opts.out ?? basename(input).replace(/\.[^.]+$/, '');
|
|
60
|
+
if (existsSync(outDir) && (await readdir(outDir)).length > 0 && !opts.force) {
|
|
61
|
+
throw new Error(`Folder ${outDir} is not empty. Use --force to overwrite.`);
|
|
62
|
+
}
|
|
63
|
+
const r = await convert(input, { outDir, title: opts.title, onProgress, imageFormat });
|
|
64
|
+
// Clear progress line before success message
|
|
65
|
+
if (isTTY)
|
|
66
|
+
process.stderr.write('\x1b[2K\r');
|
|
67
|
+
console.log(`✓ ${r.pageCount} pages → ${r.outDir}/`);
|
|
68
|
+
if (opts.serve) {
|
|
69
|
+
const server = await serve(outDir);
|
|
70
|
+
process.stderr.write(`Serving ${outDir} at ${server.url}\n`);
|
|
71
|
+
openBrowser(server.url);
|
|
72
|
+
// Keep running — http server holds the event loop open
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
process.stderr.write(` Preview: tojiru serve ${outDir}\n`);
|
|
76
|
+
}
|
|
57
77
|
}
|
|
58
78
|
}
|
|
59
79
|
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":";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,sBAAsB,EAAE,mEAAmE,CAAC;KACnG,MAAM,CAAC,sBAAsB,EAAE,kDAAkD,EAAE,MAAM,CAAC;KAC1F,MAAM,CAAC,KAAK,EAAE,KAAa,EAAE,IAA6H,EAAE,EAAE;IAC7J,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,KAAK,EAAE,CAAC,CAAA;QACnE,IAAI,IAAI,CAAC,WAAW,KAAK,MAAM,IAAI,IAAI,CAAC,WAAW,KAAK,MAAM,EAAE,CAAC;YAC/D,MAAM,IAAI,KAAK,CAAC,iDAAiD,IAAI,CAAC,WAAW,IAAI,CAAC,CAAA;QACxF,CAAC;QACD,MAAM,WAAW,GAAG,IAAI,CAAC,WAA8B,CAAA;QAEvD,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAA;QAC3C,MAAM,UAAU,GAAG,KAAK;YACtB,CAAC,CAAC,CAAC,IAAY,EAAE,KAAa,EAAE,KAAa,EAAE,EAAE;gBAC7C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE,CAAC,CAAA;YACrD,CAAC;YACH,CAAC,CAAC,SAAS,CAAA;QAEb,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YAClC,6EAA6E;YAC7E,MAAM,QAAQ,GAAG,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ;gBAClD,CAAC,CAAC,IAAI,CAAC,UAAU;gBACjB,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,GAAG,OAAO,CAAA;YAErD,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAA;YAEhH,IAAI,KAAK;gBAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;YAC5C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,QAAQ,EAAE,CAAC,CAAA;YACnD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,+CAA+C,CAAC,CAAA;QACvE,CAAC;aAAM,CAAC;YACN,cAAc;YACd,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;YAClE,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;gBAC5E,MAAM,IAAI,KAAK,CAAC,UAAU,MAAM,0CAA0C,CAAC,CAAA;YAC7E,CAAC;YAED,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAA;YAEtF,6CAA6C;YAC7C,IAAI,KAAK;gBAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;YAE5C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAA;YAEpD,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;gBACf,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,CAAA;gBAClC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,MAAM,OAAO,MAAM,CAAC,GAAG,IAAI,CAAC,CAAA;gBAC5D,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;gBACvB,uDAAuD;YACzD,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,4BAA4B,MAAM,IAAI,CAAC,CAAA;YAC9D,CAAC;QACH,CAAC;IACH,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,UAAW,CAAW,CAAC,OAAO,EAAE,CAAC,CAAA;QAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;AACH,CAAC,CAAC,CAAA;AAEJ,mBAAmB;AACnB,OAAO;KACJ,OAAO,CAAC,aAAa,CAAC;KACtB,WAAW,CAAC,2CAA2C,CAAC;KACxD,MAAM,CAAC,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,CAAC;KACrD,MAAM,CAAC,KAAK,EAAE,GAAW,EAAE,IAAuB,EAAE,EAAE;IACrD,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,EAAE,CAAC,CAAA;QACpE,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACvD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACrC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,GAAG,OAAO,MAAM,CAAC,GAAG,IAAI,CAAC,CAAA;QACzD,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACvB,8BAA8B;IAChC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,KAAK,CAAC,UAAW,CAAW,CAAC,OAAO,EAAE,CAAC,CAAA;QAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;AACH,CAAC,CAAC,CAAA;AAEJ,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA"}
|
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, opts.
|
|
28
|
-
|
|
33
|
+
const pages = await processPages(doc, bundleDir, { imageFormat: opts.imageFormat }, 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":"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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/dist/pages.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile, copyFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, writeFile, copyFile, rm } from 'node:fs/promises';
|
|
2
2
|
import { gzipSync } from 'node:zlib';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
5
|
export async function processPages(doc, outDir, opts = {}, onProgress) {
|
|
6
6
|
const thumbWidth = opts.thumbWidth ?? 150;
|
|
7
|
+
const imageFormat = opts.imageFormat ?? 'keep';
|
|
7
8
|
const width = Math.max(4, String(doc.pages.length).length);
|
|
9
|
+
// Clear pages/ and thumbs/ first so a re-convert (e.g. --force into a previous run,
|
|
10
|
+
// or a format switch) never leaves orphaned page files behind. These two dirs are
|
|
11
|
+
// owned entirely by tojiru, so removing them does not touch user content.
|
|
12
|
+
await rm(join(outDir, 'pages'), { recursive: true, force: true });
|
|
13
|
+
await rm(join(outDir, 'thumbs'), { recursive: true, force: true });
|
|
8
14
|
await mkdir(join(outDir, 'pages'), { recursive: true });
|
|
9
15
|
await mkdir(join(outDir, 'thumbs'), { recursive: true });
|
|
10
16
|
const out = [];
|
|
@@ -21,9 +27,18 @@ export async function processPages(doc, outDir, opts = {}, onProgress) {
|
|
|
21
27
|
out.push({ n, type: 'vector', w: page.w, h: page.h, file, thumb });
|
|
22
28
|
}
|
|
23
29
|
else {
|
|
24
|
-
const ext = basename(page.imagePath).split('.').pop() ?? 'jpg';
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
const ext = (basename(page.imagePath).split('.').pop() ?? 'jpg').toLowerCase();
|
|
31
|
+
// --image-format webp re-encodes comic pages (often large lossless PNGs) to
|
|
32
|
+
// lossy WebP. Sources already in WebP are copied — re-encoding would only degrade.
|
|
33
|
+
let file;
|
|
34
|
+
if (imageFormat === 'webp' && ext !== 'webp') {
|
|
35
|
+
file = `pages/${stem}.webp`;
|
|
36
|
+
await sharp(page.imagePath).webp({ quality: 82, effort: 6 }).toFile(join(outDir, file));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
file = `pages/${stem}.${ext}`;
|
|
40
|
+
await copyFile(page.imagePath, join(outDir, file));
|
|
41
|
+
}
|
|
27
42
|
await sharp(page.imagePath).resize({ width: thumbWidth }).webp().toFile(join(outDir, thumb));
|
|
28
43
|
out.push({ n, type: 'raster', w: page.w, h: page.h, file, thumb });
|
|
29
44
|
}
|
package/dist/pages.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../src/pages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"pages.js","sourceRoot":"","sources":["../src/pages.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))
|