PageSourceSearch

https://catenapdf.vercel.app/workers/fb2-to-pdf.worker.js

js catenapdf.vercel.app collected 2026-10-03 06:22:54 UTC 4,239 bytes, 143 lines download raw bytes

1/**
2 * FB2 to PDF Worker (via Pyodide + PyMuPDF)
3 * PyMuPDF has native FB2 (FictionBook) support, providing high-quality conversion!
4 * 
5 * Note: FB2 format is primarily used for Cyrillic text (Russian, Ukrainian, etc.)
6 * PyMuPDF's built-in fonts support Cyrillic characters, so no additional font download is needed.
7 * 
8 * Uses PyMuPDF's native convert_to_pdf() for optimal file size and text preservation.
9 */
10
11import { loadPyodide } from '/pymupdf-wasm/pyodide.js';
12
13let pyodide = null;
14let initPromise = null;
15
16async function init() {
17    if (pyodide) return pyodide;
18
19    self.postMessage({ type: 'status', message: 'Loading Python environment...' });
20
21    pyodide = await loadPyodide({
22        indexURL: '/pymupdf-wasm/',
23        fullStdLib: false
24    });
25
26    self.postMessage({ type: 'status', message: 'Installing dependencies...' });
27
28    const install = async (url) => {
29        await pyodide.loadPackage(url);
30    };
31
32    const basePath = '/pymupdf-wasm/';
33    await install(basePath + 'numpy-2.2.5-cp313-cp313-pyodide_2025_0_wasm32.whl');
34    await install(basePath + 'pymupdf-1.26.3-cp313-none-pyodide_2025_0_wasm32.whl');
35
36    self.postMessage({ type: 'status', message: 'Initializing converter...' });
37
38    pyodide.runPython(`
39import pymupdf
40import gc
41import zipfile
42import io
43
44def convert_fb2_to_pdf(input_obj, quality='medium'):
45    """
46    Convert FB2 (FictionBook) to PDF using PyMuPDF's native conversion.
47    
48    This method preserves text as vector content (not images), resulting in:
49    - Much smaller file sizes
50    - Searchable/selectable text
51    - Better quality at any zoom level
52    
53    Supports both .fb2 and .fb2.zip files.
54    """
55    if hasattr(input_obj, "to_py"):
56        input_bytes = bytes(input_obj.to_py())
57    else:
58        input_bytes = bytes(input_obj)
59    
60    # Check if it's a zipped FB2
61    fb2_bytes = input_bytes
62    try:
63        with zipfile.ZipFile(io.BytesIO(input_bytes)) as zf:
64            fb2_files = [name for name in zf.namelist() if name.lower().endswith('.fb2')]
65            if fb2_files:
66                fb2_bytes = zf.read(fb2_files[0])
67    except:
68        pass
69    
70    # Open FB2 with PyMuPDF
71    fb2_doc = pymupdf.open(stream=fb2_bytes, filetype="fb2")
72    
73    # Use PyMuPDF's native PDF conversion - preserves text, much smaller file size
74    pdf_bytes = fb2_doc.convert_to_pdf()
75    
76    fb2_doc.close()
77    gc.collect()
78    
79    # Optionally optimize the PDF
80    pdf_doc = pymupdf.open("pdf", pdf_bytes)
81    
82    # Save with compression and garbage collection
83    optimized_bytes = pdf_doc.tobytes(
84        garbage=4,      # Maximum garbage collection
85        deflate=True,   # Compress streams
86        clean=True      # Clean up redundant objects
87    )
88    
89    pdf_doc.close()
90    gc.collect()
91    
92    return optimized_bytes
93    `);
94
95    return pyodide;
96}
97
98self.onmessage = async (event) => {
99    const { type, id, data } = event.data;
100
101    try {
102        if (type === 'init') {
103            if (!initPromise) initPromise = init();
104            await initPromise;
105            self.postMessage({ id, type: 'init-complete' });
106            return;
107        }
108
109        if (type === 'convert') {
110            if (!pyodide) {
111                if (!initPromise) initPromise = init();
112                await initPromise;
113            }
114
115            const { file, quality = 'medium' } = data;
116            const arrayBuffer = await file.arrayBuffer();
117            const inputBytes = new Uint8Array(arrayBuffer);
118
119            self.postMessage({ type: 'status', message: 'Converting FB2 to PDF...' });
120
121            const convertFunc = pyodide.globals.get('convert_fb2_to_pdf');
122            const resultProxy = convertFunc(inputBytes, quality);
123            const resultBytes = resultProxy.toJs();
124            resultProxy.destroy();
125
126            const resultBlob = new Blob([resultBytes], { type: 'application/pdf' });
127
128            self.postMessage({
129                id,
130                type: 'convert-complete',
131                result: resultBlob
132            });
133        }
134
135    } catch (error) {
136        console.error('Worker error:', error);
137        self.postMessage({
138            id,
139            type: 'error',
140            error: error.message || String(error)
141        });
142    }
143};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.