/** * Runs the DjVu module for johnmcm.com's /djvu-to-pdf and /open-file/djvu. * The page sends the book once; each request after that reads from it. * * Messages in: { id, type: 'open', file: File } * { id, type: 'render', page, dpi } * { id, type: 'text', page, dpi } (the page's words and where they are) * { id, type: 'texts' } (every page's text, for search) * { id, type: 'pdf', start, end, quality, dpi, g4 } * Messages out: { id, ... } answers, { id, progress: { done, total } } while * a PDF is made, or { id, error }. * * Written for johnmcm.com (2026); part of the module published at /source/djvu/. */ import init, { WasmDocument, export_pdf, outline_json } from './djvu.js'; let ready = null; let bytes = null; let doc = null; /** Each page's size in points and its own resolution. */ function pageInfo() { const pages = []; for (let i = 0; i < doc.page_count(); i++) { const p = doc.page(i); try { pages.push({ width: p.width_at(72), height: p.height_at(72), dpi: p.dpi() }); } finally { p.free(); } } return pages; } function withPage(index, fn) { const p = doc.page(index); try { return fn(p); } finally { p.free(); } } self.onmessage = async (e) => { const msg = e.data; try { ready ??= init({ module_or_path: new URL('./djvu_bg.wasm', import.meta.url) }).catch((err) => { ready = null; throw new Error('load: ' + err); }); await ready; if (msg.type === 'open') { bytes = new Uint8Array(await msg.file.arrayBuffer()); doc?.free(); // Cleared first: if the new file fails to parse, later requests say no // book is open rather than touching the freed one ("null pointer"). doc = null; doc = WasmDocument.from_bytes(bytes); let outline = []; try { outline = JSON.parse(outline_json(bytes)); } catch { // A damaged outline loses the bookmarks, not the book. } self.postMessage({ id: msg.id, pages: pageInfo(), outline }); return; } if (!doc) throw new Error('No book was opened.'); if (msg.type === 'render') { const out = withPage(msg.page, (p) => ({ width: p.width_at(msg.dpi), height: p.height_at(msg.dpi), rgba: p.render(msg.dpi) })); self.postMessage({ id: msg.id, ...out }, [out.rgba.buffer]); } else if (msg.type === 'text') { const out = withPage(msg.page, (p) => ({ text: p.text() ?? '', zones: JSON.parse(p.text_zones_json(msg.dpi) ?? '[]') })); self.postMessage({ id: msg.id, ...out }); } else if (msg.type === 'texts') { const texts = []; for (let i = 0; i < doc.page_count(); i++) { try { texts.push(withPage(i, (p) => p.text() ?? '')); } catch { texts.push(''); } } self.postMessage({ id: msg.id, texts }); } else if (msg.type === 'pdf') { const pdf = export_pdf(bytes, msg.start, msg.end, msg.quality, msg.dpi, msg.g4, (done, total) => self.postMessage({ id: msg.id, progress: { done, total } }), ); self.postMessage({ id: msg.id, pdf }, [pdf.buffer]); } } catch (err) { self.postMessage({ id: msg.id, error: String(err?.message || err) }); } };