@gheop/tojiru 0.3.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 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,13 @@ 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
+
137
155
  ### v0.3.0 — Smaller PDF bundles (2026-06-26)
138
156
 
139
157
  - Vector PDF page coordinates are rounded to 2 decimals — ~25% smaller pages, no visible change
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
- const r = await convert(input, { outDir, title: opts.title, onProgress });
45
- // Clear progress line before success message
46
- if (isTTY)
47
- process.stderr.write('\x1b[2K\r');
48
- console.log(`✓ ${r.pageCount} pages → ${r.outDir}/`);
49
- if (opts.serve) {
50
- const server = await serve(outDir);
51
- process.stderr.write(`Serving ${outDir} at ${server.url}\n`);
52
- openBrowser(server.url);
53
- // Keep running http server holds the event loop open
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
- process.stderr.write(` Preview: tojiru serve ${outDir}\n`);
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":"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"}
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, opts.outDir, {}, opts.onProgress);
28
- await writeFolder(buildManifest(doc.title, doc.kind, pages), opts.outDir);
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
@@ -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;AAGhD,MAAM,UAAU,GAAgB,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,aAAa,CAAC,CAAA;AAavG,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,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,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;QACvE,MAAM,WAAW,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QACzE,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;IAClD,CAAC;AACH,CAAC"}
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gheop/tojiru",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Turn a fixed-page document (PDF, comic, DjVu) into a static web reader",
5
5
  "type": "module",
6
6
  "bin": {
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 res = await fetch(file)
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
- obj.data = URL.createObjectURL(new Blob([text], { type: 'image/svg+xml' }))
36
- obj.addEventListener('load', () => URL.revokeObjectURL(obj.data), { once: true })
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
- img.src = p.file
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
- t.src = p.thumb
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))