Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.1'
default: 'v2026.44.9'

jobs:
create-release:
Expand Down
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion mcp-stdio/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "open-pdf-studio-mcp-stdio",
"version": "2026.44.1",
"version": "2026.44.9",
"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.",
Expand Down
2 changes: 1 addition & 1 deletion mcpb/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": "0.3",
"name": "open-pdf-studio",
"display_name": "Open PDF Studio",
"version": "2026.44.1",
"version": "2026.44.9",
"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": {
Expand Down
2 changes: 1 addition & 1 deletion open-pdf-cad/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "open-pdf-cad"
version = "2026.44.1"
version = "2026.44.9"
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"
Expand Down
2 changes: 1 addition & 1 deletion open-pdf-cmyk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "open-pdf-cmyk"
version = "2026.44.1"
version = "2026.44.9"
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)"
Expand Down
2 changes: 1 addition & 1 deletion open-pdf-render/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion open-pdf-render/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "open-pdf-render"
version = "2026.44.1"
version = "2026.44.9"
edition = "2021"
license = "MIT"
description = "Pure Rust PDF page renderer — renders PDF pages to RGBA bitmaps"
Expand Down
16 changes: 5 additions & 11 deletions open-pdf-studio/js/pdf/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion open-pdf-studio/js/solid/components/PrintQueueWindow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions open-pdf-studio/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion open-pdf-studio/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "open-pdf-studio",
"version": "2026.44.1",
"version": "2026.44.9",
"description": "A free, open-source PDF annotation editor built with Tauri",
"scripts": {
"prepare:native-runtime": "node scripts/native-runtime.mjs",
Expand Down
105 changes: 105 additions & 0 deletions open-pdf-studio/scripts/pdfjs-postscript-progress.mjs
Original file line number Diff line number Diff line change
@@ -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};`;
},
};
}
58 changes: 58 additions & 0 deletions open-pdf-studio/scripts/pdfjs-postscript-progress.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
35 changes: 35 additions & 0 deletions open-pdf-studio/scripts/postscript-browser-e2e.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// node scripts/postscript-browser-e2e.mjs <isolated-vite-url>
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(); }
Loading
Loading