PageSourceSearch

https://pictorx.com/js/pdf/pdf-export-service.js?v=NNs_6UufmXc05a0qd2CG97Gdf-kYoDspHFIM2u462Fk

js pictorx.com collected 2026-10-09 04:46:25 UTC 5,950 bytes, 129 lines download raw bytes

1/**
2 * File ▸ Save ▸ Save as PDF: every page of the open document as a page of a PDF file.
3 * Each page is its flattened image (on white) embedded as a JPEG, sized at 96 dpi
4 * (1 px = 0.75 pt), so the PDF has the document's proportions. The PDF is written here,
5 * without a library. (Not added to recent files: the editor cannot open a PDF.)
6 */
7class PdfExportService {
8    static suggestedFileName(lm) {
9        if (!lm) return "image.pdf";
10        const remembered = lm.lastSaveAsFileName;
11        if (typeof remembered === "string" && remembered.trim() && /\.pdf$/i.test(remembered.trim())) {
12            return remembered.trim();
13        }
14        const base = String(lm.fileName || "image").trim() || "image";
15        const simple = (base.split(/[/\\]/).pop() || "image")
16            .replace(/\.(png|jpe?g|jpeg|webp|gif|bmp|picx|json|svg|pdf)$/i, "");
17        return `${simple || "image"}.pdf`;
18    }
19
20    /** The flattened canvas of every page (the current page only when the project has no pages). */
21    static pageCanvases() {
22        const pages = app.currentProject?.pages;
23        const lms = Array.isArray(pages) && pages.length
24            ? pages.map((page) => page.lm)
25            : [app.lm];
26        return lms
27            .filter(Boolean)
28            .map((lm) => lm.getFlatCanvas())
29            .filter((canvas) => canvas && canvas.width > 0 && canvas.height > 0);
30    }
31
32    /** A JPEG of the canvas on white (JPEG has no transparency), as bytes. */
33    static async jpegBytes(canvas, quality = 0.92) {
34        const flat = document.createElement("canvas");
35        flat.width = canvas.width;
36        flat.height = canvas.height;
37        const g = flat.getContext("2d");
38        g.fillStyle = "#ffffff";
39        g.fillRect(0, 0, flat.width, flat.height);
40        g.drawImage(canvas, 0, 0);
41        const blob = await new Promise((resolve) => flat.toBlob(resolve, "image/jpeg", quality));
42        if (!blob) throw new Error("JPEG encoding failed");
43        return new Uint8Array(await blob.arrayBuffer());
44    }
45
46    /** A PDF with one page per canvas. */
47    static async buildPdf(canvases) {
48        const encoder = new TextEncoder();
49        const chunks = [];
50        const offsets = [];
51        let length = 0;
52        const write = (part) => {
53            const bytes = typeof part === "string" ? encoder.encode(part) : part;
54            chunks.push(bytes);
55            length += bytes.length;
56        };
57        const object = (id, parts) => {
58            offsets[id] = length;
59            write(`${id} 0 obj\n`);
60            parts.forEach(write);
61            write("\nendobj\n");
62        };
63        const pt = (px) => Math.round(px * 0.75 * 100) / 100;
64
65        // 1 = catalog, 2 = page tree, then per page: page, content stream, image.
66        const pageIds = canvases.map((_, i) => 3 + i * 3);
67        write("%PDF-1.4\n%\xE2\xE3\xCF\xD3\n");
68        object(1, ["<< /Type /Catalog /Pages 2 0 R >>"]);
69        object(2, [`<< /Type /Pages /Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] /Count ${canvases.length} >>`]);
70        for (let i = 0; i < canvases.length; i++) {
71            const canvas = canvases[i];
72            const [pageId, contentId, imageId] = [pageIds[i], pageIds[i] + 1, pageIds[i] + 2];
73            const w = pt(canvas.width);
74            const h = pt(canvas.height);
75            const jpeg = await PdfExportService.jpegBytes(canvas);
76            const content = `q ${w} 0 0 ${h} 0 0 cm /Im0 Do Q`;
77            object(pageId, [
78                `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${w} ${h}] `
79                + `/Resources << /XObject << /Im0 ${imageId} 0 R >> >> /Contents ${contentId} 0 R >>`,
80            ]);
81            object(contentId, [`<< /Length ${content.length} >>\nstream\n${content}\nendstream`]);
82            object(imageId, [
83                `<< /Type /XObject /Subtype /Image /Width ${canvas.width} /Height ${canvas.height} `
84                + `/ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${jpeg.length} >>\nstream\n`,
85                jpeg,
86                "\nendstream",
87            ]);
88        }
89        const count = 3 + canvases.length * 3;
90        const xref = length;
91        write(`xref\n0 ${count}\n0000000000 65535 f \n`);
92        for (let id = 1; id < count; id++) write(`${String(offsets[id]).padStart(10, "0")} 00000 n \n`);
93        write(`trailer\n<< /Size ${count} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`);
94        return new Blob(chunks, { type: "application/pdf" });
95    }
96
97    static async saveWithDialog() {
98        if (!app.lm) return;
99        const suggestedName = PdfExportService.suggestedFileName(app.lm);
100        let blob;
101        try {
102            const canvases = PdfExportService.pageCanvases();
103            if (!canvases.length) return;
104            blob = await PdfExportService.buildPdf(canvases);
105        } catch (err) {
106            console.error("Failed to export PDF:", err);
107            return;
108        }
109        if (!("showSaveFilePicker" in window)) {
110            downloadBlobViaLink(blob, suggestedName);
111            await app.persistProjectState("saveAs");
112            void saveProjectVersion("save");
113            return;
114        }
115        try {
116            const handle = await window.showSaveFilePicker({
117                suggestedName,
118                types: [{ description: "PDF document", accept: { "application/pdf": [".pdf"] } }],
119            });
120            await writeBlobToFileHandle(handle, blob);
121            const chosenName = String(handle.name || "").trim() || suggestedName;
122            app.lm.lastSaveAsFileName = chosenName;
123            await app.persistProjectState("saveAs");
124            void saveProjectVersion("save");
125        } catch (err) {
126            if (err.name !== "AbortError") console.error("Failed to save PDF:", err);
127        }
128    }
129}

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.