From 113417cc1af9214ec99042bf200df7618120262a Mon Sep 17 00:00:00 2001 From: oscar981-op Date: Thu, 29 Oct 2026 10:01:11 +0100 Subject: [PATCH] fix(pdfjs): backport progress for malformed Type 4 tokens (#839) --- .github/workflows/release.yml | 2 +- Cargo.lock | 12 +- mcp-stdio/package.json | 2 +- mcpb/manifest.json | 2 +- open-pdf-cad/Cargo.toml | 2 +- open-pdf-cmyk/Cargo.toml | 2 +- open-pdf-render/Cargo.lock | 2 +- open-pdf-render/Cargo.toml | 2 +- open-pdf-studio/js/pdf/loader.js | 16 +-- .../js/solid/components/PrintQueueWindow.jsx | 3 +- open-pdf-studio/package-lock.json | 4 +- open-pdf-studio/package.json | 2 +- .../scripts/pdfjs-postscript-progress.mjs | 105 ++++++++++++++++++ .../pdfjs-postscript-progress.test.mjs | 58 ++++++++++ .../scripts/postscript-browser-e2e.mjs | 35 ++++++ .../scripts/postscript-browser-probe.mjs | 32 ++++++ .../scripts/postscript-build-check.mjs | 20 ++++ .../scripts/postscript-fixture.mjs | 20 ++++ open-pdf-studio/scripts/postscript-probe.mjs | 25 +++++ open-pdf-studio/scripts/unit-testlijst.txt | 1 + open-pdf-studio/src-tauri/Cargo.lock | 4 +- open-pdf-studio/src-tauri/Cargo.toml | 2 +- open-pdf-studio/src-tauri/tauri.conf.json | 2 +- open-pdf-studio/vite.config.js | 7 +- open-pdf-thumbnails/Cargo.toml | 2 +- pdfium-worker/Cargo.toml | 2 +- snap/snapcraft.yaml | 2 +- 27 files changed, 330 insertions(+), 38 deletions(-) create mode 100644 open-pdf-studio/scripts/pdfjs-postscript-progress.mjs create mode 100644 open-pdf-studio/scripts/pdfjs-postscript-progress.test.mjs create mode 100644 open-pdf-studio/scripts/postscript-browser-e2e.mjs create mode 100644 open-pdf-studio/scripts/postscript-browser-probe.mjs create mode 100644 open-pdf-studio/scripts/postscript-build-check.mjs create mode 100644 open-pdf-studio/scripts/postscript-fixture.mjs create mode 100644 open-pdf-studio/scripts/postscript-probe.mjs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d8aab0443..b7914c7b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ on: version: description: 'Version tag vYYYY.W.N (year, ISO week, sequence); must match the version fields' required: true - default: 'v2026.44.0' + default: 'v2026.44.6' jobs: create-release: diff --git a/Cargo.lock b/Cargo.lock index 8306a9828..02e64fdd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3660,7 +3660,7 @@ dependencies = [ [[package]] name = "open-pdf-cad" -version = "2026.44.0" +version = "2026.44.6" dependencies = [ "acadrust", "flate2", @@ -3672,7 +3672,7 @@ dependencies = [ [[package]] name = "open-pdf-cmyk" -version = "2026.44.0" +version = "2026.44.6" dependencies = [ "flate2", "image", @@ -3685,7 +3685,7 @@ dependencies = [ [[package]] name = "open-pdf-render" -version = "2026.44.0" +version = "2026.44.6" dependencies = [ "flate2", "hayro-font", @@ -3700,7 +3700,7 @@ dependencies = [ [[package]] name = "open-pdf-studio" -version = "2026.44.0" +version = "2026.44.6" dependencies = [ "aes", "anyhow", @@ -3773,7 +3773,7 @@ dependencies = [ [[package]] name = "open-pdf-thumbnails" -version = "2026.44.0" +version = "2026.44.6" dependencies = [ "libloading 0.9.0", "windows 0.61.3", @@ -3974,7 +3974,7 @@ dependencies = [ [[package]] name = "pdfium-worker" -version = "2026.44.0" +version = "2026.44.6" dependencies = [ "anyhow", "memmap2", diff --git a/mcp-stdio/package.json b/mcp-stdio/package.json index 218da57ca..39fcf1027 100644 --- a/mcp-stdio/package.json +++ b/mcp-stdio/package.json @@ -1,6 +1,6 @@ { "name": "open-pdf-studio-mcp-stdio", - "version": "2026.44.0", + "version": "2026.44.6", "private": true, "type": "module", "description": "Thin stdio↔HTTP bridge that exposes Open PDF Studio's in-app MCP server (assistant + PDF tools) to Claude Desktop / Claude Code.", diff --git a/mcpb/manifest.json b/mcpb/manifest.json index af02a7fd7..e2e460bf0 100644 --- a/mcpb/manifest.json +++ b/mcpb/manifest.json @@ -2,7 +2,7 @@ "manifest_version": "0.3", "name": "open-pdf-studio", "display_name": "Open PDF Studio", - "version": "2026.44.0", + "version": "2026.44.6", "description": "Open, measure, annotate and save PDF drawings in Open PDF Studio, the free open-source PDF editor for the construction industry.", "long_description": "Connects Claude to Open PDF Studio running on your computer. Claude can open PDF drawings, read and navigate pages, place scale regions, measure areas and lengths, create and edit annotations, cut vector snippets from one drawing into another, fill title blocks, place a quantity schedule on the sheet and save the result.\n\nEverything runs locally: the extension talks to the app on 127.0.0.1 and the PDF file itself is never sent to an external service. Claude only sees what it asks for, such as a picture of the current page view.\n\n**Before you start:** install Open PDF Studio (free, from open-aec.com). The AI link is on by default; check it under **Settings > General > AI link (MCP)**. Tools that change a document always ask for your permission.", "author": { diff --git a/open-pdf-cad/Cargo.toml b/open-pdf-cad/Cargo.toml index c70be23b6..5de2ad6a0 100644 --- a/open-pdf-cad/Cargo.toml +++ b/open-pdf-cad/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "open-pdf-cad" -version = "2026.44.0" +version = "2026.44.6" edition = "2021" license = "LGPL-3.0-or-later" description = "CAD exchange for PDF pages: exports vector content to DXF and DWG through one shared geometry model" diff --git a/open-pdf-cmyk/Cargo.toml b/open-pdf-cmyk/Cargo.toml index d401c179e..be71b2f7a 100644 --- a/open-pdf-cmyk/Cargo.toml +++ b/open-pdf-cmyk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "open-pdf-cmyk" -version = "2026.44.0" +version = "2026.44.6" edition = "2021" license = "LGPL-3.0-or-later" description = "Converts the RGB colour of a PDF to CMYK through an ICC printing profile (PDF/X export)" diff --git a/open-pdf-render/Cargo.lock b/open-pdf-render/Cargo.lock index adec9b0a3..1f8fae137 100644 --- a/open-pdf-render/Cargo.lock +++ b/open-pdf-render/Cargo.lock @@ -441,7 +441,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open-pdf-render" -version = "2026.44.0" +version = "2026.44.6" dependencies = [ "flate2", "hayro-font", diff --git a/open-pdf-render/Cargo.toml b/open-pdf-render/Cargo.toml index 8e4be1702..452d57a7e 100644 --- a/open-pdf-render/Cargo.toml +++ b/open-pdf-render/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "open-pdf-render" -version = "2026.44.0" +version = "2026.44.6" edition = "2021" license = "MIT" description = "Pure Rust PDF page renderer — renders PDF pages to RGBA bitmaps" diff --git a/open-pdf-studio/js/pdf/loader.js b/open-pdf-studio/js/pdf/loader.js index af0bf6f78..5926b26cd 100644 --- a/open-pdf-studio/js/pdf/loader.js +++ b/open-pdf-studio/js/pdf/loader.js @@ -6,6 +6,7 @@ import { setViewMode, fitPage } from './renderer.js'; import { generateThumbnails, refreshActiveTab } from '../ui/panels/left-panel.js'; import { createTab, closeTab, updateWindowTitle, markDocumentModified } from '../ui/chrome/tabs.js'; import * as pdfjsLib from 'pdfjs-dist'; +import pdfjsWorkerUrl from 'virtual:opds-pdfjs-worker-url'; import { isTauri, isMobile, heeftRustRenderer, readBinaryFile, openFileDialog, lockFile, invoke } from '../core/platform.js'; import { PDFDocument } from 'pdf-lib'; import { resetAnnotationStorage } from './form-layer.js'; @@ -81,17 +82,10 @@ export function clearCachedPdfBytes(filePath) { } } -// Set worker source (path relative to HTML file, not this module) -// Dev: Vite herschrijft de new URL(...)-vorm naar een /@fs/-URL en die valt -// onder server.fs.allow — een verse webview (zonder cache) krijgt daar 403 op, -// waarna PDF.js' fake-worker-fallback op dezelfde 403 strandt en getDocument -// nooit resolvet (document laadt nooit). Het kale /node_modules/-pad wordt -// altijd geserveerd, dus gebruik dat in dev; prod houdt de asset-URL (de -// worker wordt door rollup als asset geëmit — zie assetFileNames in -// vite.config.js). -pdfjsLib.GlobalWorkerOptions.workerSrc = import.meta.env?.DEV - ? '/node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs' - : new URL('pdfjs-dist/legacy/build/pdf.worker.mjs', import.meta.url).href; +// Vite serves/emits the pinned worker with the PostScript progress backport. +// The build hashes the patched bytes; dev retains a same-origin /node_modules/ +// URL so a fresh WebView cannot hit the historical /@fs/ access failure. +pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorkerUrl; // Gehoste bouw (#704): in de werkplek (opaque origin) start PDF.js zijn // worker niet zelf; hij zou een module-worker uit een blob maken, en die diff --git a/open-pdf-studio/js/solid/components/PrintQueueWindow.jsx b/open-pdf-studio/js/solid/components/PrintQueueWindow.jsx index cb2000487..973f31983 100644 --- a/open-pdf-studio/js/solid/components/PrintQueueWindow.jsx +++ b/open-pdf-studio/js/solid/components/PrintQueueWindow.jsx @@ -22,6 +22,7 @@ import { } from '../stores/print-queue-model.js'; import { isTauri, invoke, readBinaryFile, writeBinaryFile, saveFileDialog } from '../../core/platform.js'; import { useTranslation } from '../../i18n/useTranslation.js'; +import pdfjsWorkerUrl from 'virtual:opds-pdfjs-worker-url'; const POLL_MS = 2500; const RESULT_MS = 8000; @@ -34,7 +35,7 @@ let _pdfjs = null; async function getPdfjs() { if (_pdfjs) return _pdfjs; const lib = await import('pdfjs-dist'); - try { lib.GlobalWorkerOptions.workerSrc = new URL('pdfjs-dist/legacy/build/pdf.worker.mjs', import.meta.url).href; } catch (_) {} + try { lib.GlobalWorkerOptions.workerSrc = pdfjsWorkerUrl; } catch (_) {} _pdfjs = lib; return lib; } diff --git a/open-pdf-studio/package-lock.json b/open-pdf-studio/package-lock.json index 0638486d3..af3316c25 100644 --- a/open-pdf-studio/package-lock.json +++ b/open-pdf-studio/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-pdf-studio", - "version": "2026.44.0", + "version": "2026.44.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-pdf-studio", - "version": "2026.44.0", + "version": "2026.44.6", "license": "LGPL-3.0-or-later", "dependencies": { "@crabnebula/tauri-plugin-drag": "^2.1.0", diff --git a/open-pdf-studio/package.json b/open-pdf-studio/package.json index 8197b1eee..4c0b493fb 100644 --- a/open-pdf-studio/package.json +++ b/open-pdf-studio/package.json @@ -1,6 +1,6 @@ { "name": "open-pdf-studio", - "version": "2026.44.0", + "version": "2026.44.6", "description": "A free, open-source PDF annotation editor built with Tauri", "scripts": { "prepare:native-runtime": "node scripts/native-runtime.mjs", diff --git a/open-pdf-studio/scripts/pdfjs-postscript-progress.mjs b/open-pdf-studio/scripts/pdfjs-postscript-progress.mjs new file mode 100644 index 000000000..dc1c9c356 --- /dev/null +++ b/open-pdf-studio/scripts/pdfjs-postscript-progress.mjs @@ -0,0 +1,105 @@ +// Narrow backport of Mozilla PDF.js #22365 (e7aa97b49), Apache-2.0. +// The lexer methods below are derived from PDF.js, Copyright Mozilla Foundation. +// Licensed under the Apache License, Version 2.0: https://www.apache.org/licenses/LICENSE-2.0 +// Remove this backport only after qualifying a pdfjs-dist release containing the fix. +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; + +const BEFORE = String.raw` _getNumber() { + this._numberPattern.lastIndex = this.pos; + const match = this._numberPattern.exec(this.data); + if (!match) { + return new Token(TOKEN.number, 0); + } + const number = parseFloat(match[0]); + if (!Number.isFinite(number)) { + return new Token(TOKEN.number, 0); + } + this.pos = this._numberPattern.lastIndex; + return new Token(TOKEN.number, number); + } + _getOperator() { + this._identifierPattern.lastIndex = this.pos; + const match = this._identifierPattern.exec(this.data); + if (!match) { + return new Token(TOKEN.number, 0); + } + this.pos = this._identifierPattern.lastIndex; + const op = match[0]; + const token = lexer_Lexer.#operatorSingletons[op]; + return token ?? new Token(TOKEN.number, 0); + } +`; +const AFTER = String.raw` _getNumber() { + this._numberPattern.lastIndex = this.pos; + const match = this._numberPattern.exec(this.data); + if (!match) { + this.pos++; + return new Token(TOKEN.number, 0); + } + const number = parseFloat(match[0]); + this.pos = this._numberPattern.lastIndex; + return new Token(TOKEN.number, Number.isFinite(number) ? number : 0); + } + _getOperator() { + this._identifierPattern.lastIndex = this.pos; + const match = this._identifierPattern.exec(this.data); + if (!match) { + this.pos++; + return new Token(TOKEN.number, 0); + } + this.pos = this._identifierPattern.lastIndex; + const op = match[0]; + const token = lexer_Lexer.#operatorSingletons[op]; + return token ?? new Token(TOKEN.number, 0); + } +`; + +export function patchPostscriptProgress(source) { + // Fail closed on dependency drift instead of silently shipping an unpatched worker. + if (source.split(AFTER).length === 2 && !source.includes(BEFORE)) return source; + if (source.split(BEFORE).length !== 2) { + throw new Error('PDF.js PostScript backport no longer matches; qualify the worker before updating PDF.js (#839).'); + } + return source.replace(BEFORE, AFTER); +} + +export function pdfjsPostscriptProgress() { + let worker; + let devPath; + let build; + const virtualId = 'virtual:opds-pdfjs-worker-url'; + return { + name: 'opds-pdfjs-postscript-progress', + configResolved(config) { + build = config.command === 'build'; + const require = createRequire(resolve(config.root, 'package.json')); + const version = JSON.parse(readFileSync(require.resolve('pdfjs-dist/package.json'), 'utf8')).version; + if (version !== '6.4.299') throw new Error(`Qualify PDF.js ${version} before removing the #839 backport.`); + worker = patchPostscriptProgress(readFileSync(require.resolve('pdfjs-dist/legacy/build/pdf.worker.mjs'), 'utf8')); + devPath = `${config.base.startsWith('/') ? config.base : '/'}node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs`; + }, + configureServer(server) { + // loader.js uses this literal URL in dev; never modify shared node_modules. + server.middlewares.use((req, res, next) => { + if (req.url?.split('?')[0] !== devPath) return next(); + res.setHeader('Content-Type', 'text/javascript; charset=utf-8'); + res.setHeader('Cache-Control', 'no-cache'); + res.end(worker); + }); + }, + resolveId(id) { + if (id === virtualId) return '\0' + virtualId; + }, + load(id) { + if (id !== '\0' + virtualId) return; + if (!build) return `export default ${JSON.stringify(devPath)};`; + // Emit patched bytes BEFORE Rollup computes the asset hash. A previously + // cached vulnerable worker must never share this URL. The existing old- + // WebView plugin still transpiles the emitted worker for Chrome 91. + const ref = this.emitFile({ type: 'asset', name: 'pdf.worker.mjs', source: worker }); + return `export default import.meta.ROLLUP_FILE_URL_${ref};`; + }, + }; +} diff --git a/open-pdf-studio/scripts/pdfjs-postscript-progress.test.mjs b/open-pdf-studio/scripts/pdfjs-postscript-progress.test.mjs new file mode 100644 index 000000000..aa47946e5 --- /dev/null +++ b/open-pdf-studio/scripts/pdfjs-postscript-progress.test.mjs @@ -0,0 +1,58 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { readFileSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; +import { patchPostscriptProgress } from './pdfjs-postscript-progress.mjs'; +const require = createRequire(import.meta.url); +const source = readFileSync(require.resolve('pdfjs-dist/legacy/build/pdf.worker.mjs'), 'utf8'); +const probe = fileURLToPath(new URL('./postscript-probe.mjs', import.meta.url)); +function run(mode, program, timeout = 15000) { + // The child may be killed while blocked; use a dedicated temp root so its + // temporary worker is cleaned up by the parent even after a hard timeout. + const root = mkdtempSync(join(tmpdir(), 'opds-ps-test-')); + try { return spawnSync(process.execPath, [probe, mode, program], { + encoding: 'utf8', timeout, env: { ...process.env, TMPDIR: root, TMP: root, TEMP: root }, + }); } finally { rmSync(root, {recursive: true, force: true}); } +} +test('backport matches once, is idempotent and rejects dependency drift', () => { + const fixed = patchPostscriptProgress(source); + assert.notEqual(fixed, source); + assert.equal(patchPostscriptProgress(fixed), fixed); + assert.throws(() => patchPostscriptProgress(source.replace('const number = parseFloat(match[0]);', 'const number = 42;')), /no longer matches/); + assert.throws(() => patchPostscriptProgress(source + source), /no longer matches/); +}); +test('unpatched real PDF worker hangs on a bare sign, bounded by subprocess deadline', () => { + const result = run('old', '{ pop + 0 0 }', 10000); + assert.match(result.stdout, /READY/, 'worker reached shading before the deadline'); + assert.equal(result.error?.code, 'ETIMEDOUT', result.stdout + result.stderr); +}); +for (const program of ['{ pop + 0 0 }', '{ pop - 0 0 }', '{ pop 1e999 0 0 }', '{ pop foo 0 0 }', '{ pop @ 0 0 }', '{ 0 0 }']) { + test(`patched real PDF worker completes Type 4 shading: ${program}`, () => { + const result = run('patched', program); + assert.equal(result.status, 0, result.stdout + result.stderr); + assert.match(result.stdout, /PASS/); + }); +} + +test('real lexer advances for malformed tokens, including an invalid operator match', () => { + const fixed = patchPostscriptProgress(source); + const start = fixed.indexOf('const TOKEN =', fixed.indexOf(';// ./src/core/postscript/lexer.js')); + const end = fixed.indexOf(';// ./src/core/postscript/ast.js', start); + const { Lexer, TOKEN } = new Function(fixed.slice(start, end) + '; return {Lexer: lexer_Lexer, TOKEN};')(); + const lexer = new Lexer('+ foo - bar 1e999 baz . @'); + for (let i = 0; i < 8; i++) { + const previous = lexer.pos; + const token = lexer.next(); + assert.equal(token.id, TOKEN.number); + assert.equal(token.value, 0); + assert.ok(lexer.pos > previous); + } + assert.equal(lexer.next().id, TOKEN.eof); + const invalidOperator = new Lexer('@'); + assert.equal(invalidOperator._getOperator().value, 0); + assert.equal(invalidOperator.pos, 1); +}); diff --git a/open-pdf-studio/scripts/postscript-browser-e2e.mjs b/open-pdf-studio/scripts/postscript-browser-e2e.mjs new file mode 100644 index 000000000..b9fe1f844 --- /dev/null +++ b/open-pdf-studio/scripts/postscript-browser-e2e.mjs @@ -0,0 +1,35 @@ +// node scripts/postscript-browser-e2e.mjs +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { chromium } from 'playwright'; +const require = createRequire(import.meta.url); +const browser = await chromium.launch({ headless: true }); +try { + const page = await browser.newPage(); + await page.route('**/postscript-control-worker.mjs', route => route.fulfill({ + contentType: 'text/javascript', body: readFileSync(require.resolve('pdfjs-dist/legacy/build/pdf.worker.mjs'), 'utf8'), + })); + await page.goto(process.argv[2] || 'http://127.0.0.1:3061', { waitUntil: 'domcontentloaded' }); + await page.locator('[data-tab="drawing"]').waitFor({ timeout: 90000 }); + const render = (program, url, cancel = false) => Promise.race([ + page.evaluate(async ({program, url, cancel}) => { + const {renderFixture} = await import('/scripts/postscript-browser-probe.mjs'); + return renderFixture(program, url, cancel); + }, {program, url, cancel}), + new Promise((_, reject) => { const timer = setTimeout(() => reject(new Error(`Render deadline: ${program}`)), 15000); timer.unref(); }), + ]); + const control = await render('{ 0 0 }', '/postscript-control-worker.mjs'); + const fixed = await render('{ 0 0 }'); + assert.deepEqual(fixed, control, 'valid gradient pixels unchanged'); + assert.ok(fixed.right[0] > fixed.left[0] + 100, 'red gradient was actually rendered'); + for (const token of ['+', '-', '1e999', 'foo', '@']) { + const image = await render(`{ pop ${token} 0 0 }`); + assert.deepEqual(image.left, [0, 0, 0, 255]); + assert.deepEqual(image.right, [0, 0, 0, 255]); + console.log('PASS malformed token:', token); + } + const reopened = await render('{ 0 0 }', undefined, true); + assert.deepEqual(reopened, control, 'cancel then rerender/reopen still works'); + console.log('PASS: valid Type 4 pixels unchanged; malformed PDFs complete; cancellation and reopening work'); +} finally { await browser.close(); } diff --git a/open-pdf-studio/scripts/postscript-browser-probe.mjs b/open-pdf-studio/scripts/postscript-browser-probe.mjs new file mode 100644 index 000000000..ca3253340 --- /dev/null +++ b/open-pdf-studio/scripts/postscript-browser-probe.mjs @@ -0,0 +1,32 @@ +// Browser-side regression using the same PDF.js module/worker URL as loader.js. +import * as pdfjs from 'pdfjs-dist'; +import '../js/pdf/loader.js'; +import { postscriptFixture } from './postscript-fixture.mjs'; + +export async function renderFixture(program, workerUrl = pdfjs.GlobalWorkerOptions.workerSrc, cancel = false) { + const port = new Worker(workerUrl, { type: 'module' }); + const worker = new pdfjs.PDFWorker({ port }); + const task = pdfjs.getDocument({ data: postscriptFixture(program), worker }); + try { + const doc = await task.promise; + const page = await doc.getPage(1); + const canvas = document.createElement('canvas'); + canvas.width = canvas.height = 100; + const context = canvas.getContext('2d'); + const options = { canvasContext: context, viewport: page.getViewport({ scale: 1 }) }; + if (cancel) { + const rendering = page.render(options); + rendering.cancel(); + try { await rendering.promise; throw new Error('Cancellation was ignored'); } + catch (error) { if (error.name !== 'RenderingCancelledException') throw error; } + } + await page.render(options).promise; + const pixels = context.getImageData(0, 0, 100, 100).data; + const hash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', pixels)), x => x.toString(16).padStart(2, '0')).join(''); + return { hash, left: Array.from(context.getImageData(10, 50, 1, 1).data), right: Array.from(context.getImageData(90, 50, 1, 1).data) }; + } finally { + await task.destroy(); + worker.destroy(); + port.terminate(); + } +} diff --git a/open-pdf-studio/scripts/postscript-build-check.mjs b/open-pdf-studio/scripts/postscript-build-check.mjs new file mode 100644 index 000000000..0ae7fdcea --- /dev/null +++ b/open-pdf-studio/scripts/postscript-build-check.mjs @@ -0,0 +1,20 @@ +// Run after `vite build`: exercise the worker actually shipped to users. +import fs from 'node:fs'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { resolve } from 'node:path'; +const assets = resolve(process.argv[2] || 'dist/assets'); +const files = fs.readdirSync(assets); +const workers = files.filter(name => /^pdf\.worker-.*\.js$/.test(name)); +assert.equal(workers.length, 1, 'editor and print queue must share one patched worker'); +const worker = resolve(assets, workers[0]); +const probe = fileURLToPath(new URL('./postscript-probe.mjs', import.meta.url)); +for (const program of ['{ pop + 0 0 }', '{ pop - 0 0 }', '{ pop 1e999 0 0 }', '{ pop foo 0 0 }', '{ pop @ 0 0 }', '{ 0 0 }']) { + const result = spawnSync(process.execPath, [probe, 'built', program, worker], { timeout: 15000, encoding: 'utf8' }); + assert.equal(result.status, 0, result.stdout + result.stderr); + console.log('PASS built worker:', program); +} +const chunks = files.filter(name => name.endsWith('.js') && !workers.includes(name)); +assert.ok(chunks.some(name => fs.readFileSync(resolve(assets, name), 'utf8').includes(workers[0])), 'app references the patched asset'); +console.log('PASS one shared worker:', workers[0]); diff --git a/open-pdf-studio/scripts/postscript-fixture.mjs b/open-pdf-studio/scripts/postscript-fixture.mjs new file mode 100644 index 000000000..a6f61d4d8 --- /dev/null +++ b/open-pdf-studio/scripts/postscript-fixture.mjs @@ -0,0 +1,20 @@ +// Self-authored minimal axial shading PDF; no external fixture or user data. +export function postscriptFixture(program = '{ 0 0 }') { + const stream = data => `<< /Length ${data.length} >>\nstream\n${data}\nendstream`; + const objects = [ + '<< /Type /Catalog /Pages 2 0 R >>', + '<< /Type /Pages /Kids [3 0 R] /Count 1 >>', + '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << /Shading << /S 5 0 R >> >> /Contents 4 0 R >>', + stream('/S sh'), + '<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Function 6 0 R /Extend [true true] >>', + stream(program).replace('<< /Length', '<< /FunctionType 4 /Domain [0 1] /Range [0 1 0 1 0 1] /Length'), + ]; + let text = '%PDF-1.7\n'; + const offsets = [0]; + objects.forEach((obj, i) => { offsets.push(text.length); text += `${i + 1} 0 obj\n${obj}\nendobj\n`; }); + const xref = text.length; + text += `xref\n0 ${offsets.length}\n0000000000 65535 f \n`; + for (const offset of offsets.slice(1)) text += `${String(offset).padStart(10, '0')} 00000 n \n`; + text += `trailer\n<< /Size ${offsets.length} /Root 1 0 R >>\nstartxref\n${xref}\n%%EOF\n`; + return new TextEncoder().encode(text); +} diff --git a/open-pdf-studio/scripts/postscript-probe.mjs b/open-pdf-studio/scripts/postscript-probe.mjs new file mode 100644 index 000000000..4c15ac4d4 --- /dev/null +++ b/open-pdf-studio/scripts/postscript-probe.mjs @@ -0,0 +1,25 @@ +// Launched in a killable subprocess: the vulnerable worker can block its thread. +import { readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { createRequire } from 'node:module'; +import { postscriptFixture } from './postscript-fixture.mjs'; +import { patchPostscriptProgress } from './pdfjs-postscript-progress.mjs'; +const require = createRequire(import.meta.url); +const temp = mkdtempSync(join(tmpdir(), 'opds-postscript-')); +try { + const source = readFileSync(process.argv[4] || require.resolve('pdfjs-dist/legacy/build/pdf.worker.mjs'), 'utf8'); + const worker = join(temp, 'pdf.worker.mjs'); + writeFileSync(worker, process.argv[2] !== 'patched' ? source : patchPostscriptProgress(source)); + const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs'); + pdfjs.GlobalWorkerOptions.workerSrc = pathToFileURL(worker).href; + const task = pdfjs.getDocument({ data: postscriptFixture(process.argv[3]), useSystemFonts: false }); + const doc = await task.promise; + const page = await doc.getPage(1); + console.log('READY: requesting shading operators'); + const ops = await page.getOperatorList(); + if (!ops.fnArray.includes(pdfjs.OPS.shadingFill)) throw new Error('Fixture did not exercise shading'); + await task.destroy(); + console.log('PASS: real worker completed shading operator list'); +} finally { rmSync(temp, { recursive: true, force: true }); } diff --git a/open-pdf-studio/scripts/unit-testlijst.txt b/open-pdf-studio/scripts/unit-testlijst.txt index 854e35572..c4b95f062 100644 --- a/open-pdf-studio/scripts/unit-testlijst.txt +++ b/open-pdf-studio/scripts/unit-testlijst.txt @@ -387,6 +387,7 @@ js/pdf/page-transition.test.mjs js/mobile/mobiele-gereedschappen.test.mjs js/core/oude-webview.test.mjs scripts/oude-webview.test.mjs +scripts/pdfjs-postscript-progress.test.mjs scripts/oude-webview-pdfjs.test.mjs scripts/android-config.test.mjs js/core/fout-bericht.test.mjs diff --git a/open-pdf-studio/src-tauri/Cargo.lock b/open-pdf-studio/src-tauri/Cargo.lock index ce1d44c59..65d6b6a4f 100644 --- a/open-pdf-studio/src-tauri/Cargo.lock +++ b/open-pdf-studio/src-tauri/Cargo.lock @@ -3223,7 +3223,7 @@ dependencies = [ [[package]] name = "open-pdf-render" -version = "2026.44.0" +version = "2026.44.6" dependencies = [ "flate2", "hayro-font", @@ -3237,7 +3237,7 @@ dependencies = [ [[package]] name = "open-pdf-studio" -version = "2026.44.0" +version = "2026.44.6" dependencies = [ "axum", "base64 0.22.1", diff --git a/open-pdf-studio/src-tauri/Cargo.toml b/open-pdf-studio/src-tauri/Cargo.toml index aa9c6fc01..3493ac460 100644 --- a/open-pdf-studio/src-tauri/Cargo.toml +++ b/open-pdf-studio/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "open-pdf-studio" -version = "2026.44.0" +version = "2026.44.6" description = "A free, open-source PDF annotation editor" authors = ["OpenAEC Foundation"] license = "LGPL-3.0-or-later" diff --git a/open-pdf-studio/src-tauri/tauri.conf.json b/open-pdf-studio/src-tauri/tauri.conf.json index fbd75abbf..546ef785c 100644 --- a/open-pdf-studio/src-tauri/tauri.conf.json +++ b/open-pdf-studio/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Open PDF Studio", - "version": "2026.44.0", + "version": "2026.44.6", "identifier": "org.openaec.openpdfstudio", "build": { "frontendDist": "../dist", diff --git a/open-pdf-studio/vite.config.js b/open-pdf-studio/vite.config.js index d9d15bf58..ea121d999 100644 --- a/open-pdf-studio/vite.config.js +++ b/open-pdf-studio/vite.config.js @@ -1,5 +1,6 @@ import { defineConfig } from 'vite'; import solidPlugin from 'vite-plugin-solid'; +import { pdfjsPostscriptProgress } from './scripts/pdfjs-postscript-progress.mjs'; import { pdfjsAssets } from './scripts/pdfjs-assets.mjs'; import { readFileSync } from 'fs'; import { pdfjsWorkerOudeWebview, voorafScriptPlugin } from './scripts/oude-webview.mjs'; @@ -20,7 +21,7 @@ const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')); // (gehost of OPDS_WEB_BUILD=1) krijgen desktopmodules als de sleepplugin een // lege vervanger (#776, scripts/web-bouw.mjs). export default defineConfig(({ mode }) => ({ - plugins: [solidPlugin(), pdfjsAssets(), voorafScriptPlugin(), pdfjsWorkerOudeWebview(), gehostHtmlPlugin()], + plugins: [solidPlugin(), pdfjsAssets(), voorafScriptPlugin(), pdfjsPostscriptProgress(), pdfjsWorkerOudeWebview(), gehostHtmlPlugin()], resolve: { alias: [ // PDF.js 6's modern build calls Map.prototype.getOrInsertComputed and @@ -68,8 +69,8 @@ export default defineConfig(({ mode }) => ({ chunkFileNames: 'assets/[name]-[hash].js', // Same treatment for assets, but ONLY for .mjs files (don't touch // CSS, images, fonts, etc). PDF.js's worker is loaded via - // new URL('pdfjs-dist/build/pdf.worker.mjs', import.meta.url) - // which Vite handles as an asset import — without this, the + // virtual:opds-pdfjs-worker-url (scripts/pdfjs-postscript-progress.mjs) + // is emitted as an asset — without this, the // worker is emitted as `pdf.worker-HASH.mjs` and the web build // can't fetch it. assetFileNames: (assetInfo) => { diff --git a/open-pdf-thumbnails/Cargo.toml b/open-pdf-thumbnails/Cargo.toml index bcead4c09..81a0ae443 100644 --- a/open-pdf-thumbnails/Cargo.toml +++ b/open-pdf-thumbnails/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "open-pdf-thumbnails" -version = "2026.44.0" +version = "2026.44.6" edition = "2021" license = "LGPL-3.0-or-later" description = "Windows Shell thumbnail handler for PDF files (File Explorer previews, #605)" diff --git a/pdfium-worker/Cargo.toml b/pdfium-worker/Cargo.toml index e381c0366..79dcfcbcc 100644 --- a/pdfium-worker/Cargo.toml +++ b/pdfium-worker/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pdfium-worker" -version = "2026.44.0" +version = "2026.44.6" edition = "2021" [[bin]] diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 5c434838d..5b75c429f 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: open-pdf-studio base: core24 -version: '2026.44.0' +version: '2026.44.6' summary: Free, open-source PDF editor for drawings and documents description: | Open PDF Studio is a free, open-source PDF editor and annotator for people