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.2'
default: 'v2026.44.11'

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.2",
"version": "2026.44.11",
"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.2",
"version": "2026.44.11",
"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.2"
version = "2026.44.11"
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.2"
version = "2026.44.11"
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.2"
version = "2026.44.11"
edition = "2021"
license = "MIT"
description = "Pure Rust PDF page renderer — renders PDF pages to RGBA bitmaps"
Expand Down
77 changes: 77 additions & 0 deletions open-pdf-studio/js/pdf/organizer-pointer-drag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Internal page moves must not enter HTML5 drag-and-drop: on Windows,
// Tauri owns WebView2's native drop handler to receive external file paths.
export function createOrganizerPointerDrag(element, { keyAt, start, move, finish }) {
const doc = element.ownerDocument;
let gesture = null;
let swallowClick = false;

function removeListeners() {
doc.removeEventListener('pointermove', pointerMove, true);
doc.removeEventListener('pointerup', pointerUp, true);
doc.removeEventListener('pointercancel', pointerCancel, true);
doc.defaultView?.removeEventListener('blur', cancel);
}
function end(event, cancelled) {
const previous = gesture;
if (!previous) return;
gesture = null;
removeListeners();
if (element.hasPointerCapture?.(previous.id)) element.releasePointerCapture(previous.id);
if (!previous.active) return;
swallowClick = true;
event?.preventDefault();
event?.stopPropagation();
finish({ clientX: event?.clientX ?? previous.x, clientY: event?.clientY ?? previous.y, cancelled });
}
function pointerMove(event) {
if (!gesture || gesture.id !== event.pointerId) return;
if (!gesture.active) {
if (Math.hypot(event.clientX - gesture.x, event.clientY - gesture.y) < 5) return;
gesture.active = true;
start(gesture.key);
try { element.setPointerCapture(event.pointerId); } catch { /* pointer already released */ }
}
gesture.x = event.clientX;
gesture.y = event.clientY;
event.preventDefault();
event.stopPropagation();
move(event);
}
function pointerUp(event) {
if (gesture?.id === event.pointerId) end(event, false);
}
function pointerCancel(event) {
if (gesture?.id === event.pointerId) end(event, true);
}
function cancel() { end(null, true); }
function pointerDown(event) {
swallowClick = false;
if (gesture || event.button !== 0 || event.isPrimary === false || event.pointerType === 'touch') return;
const key = keyAt(event.target);
if (key == null) return;
gesture = { id: event.pointerId, key, x: event.clientX, y: event.clientY, active: false };
doc.addEventListener('pointermove', pointerMove, true);
doc.addEventListener('pointerup', pointerUp, true);
doc.addEventListener('pointercancel', pointerCancel, true);
doc.defaultView?.addEventListener('blur', cancel);
}
function click(event) {
if (!swallowClick) return;
swallowClick = false;
event.preventDefault();
event.stopPropagation();
}
element.addEventListener('pointerdown', pointerDown);
element.addEventListener('lostpointercapture', pointerCancel);
element.addEventListener('click', click, true);
return {
isDragging: () => !!gesture?.active,
cancel,
destroy() {
end(null, true);
element.removeEventListener('pointerdown', pointerDown);
element.removeEventListener('lostpointercapture', pointerCancel);
element.removeEventListener('click', click, true);
},
};
}
71 changes: 71 additions & 0 deletions open-pdf-studio/js/pdf/organizer-pointer-drag.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createOrganizerPointerDrag } from './organizer-pointer-drag.js';

function fixture() {
const doc = new EventTarget();
doc.defaultView = new EventTarget();
const element = new EventTarget();
element.ownerDocument = doc;
let captured = null;
element.setPointerCapture = id => { captured = id; };
element.hasPointerCapture = id => captured === id;
element.releasePointerCapture = () => { captured = null; };
const calls = [];
const controller = createOrganizerPointerDrag(element, {
keyAt: () => 'p2', start: key => calls.push(['start', key]),
move: e => calls.push(['move', e.clientX]), finish: e => calls.push(['finish', e.cancelled]),
});
const send = (type, props = {}) => {
const e = new Event(type, { cancelable: true });
Object.assign(e, { pointerId: 1, clientX: 0, clientY: 0, button: 0, pointerType: 'mouse', isPrimary: true, ...props });
(['pointerdown', 'click', 'lostpointercapture'].includes(type) ? element : doc).dispatchEvent(e);
return e;
};
return { controller, calls, send, blur: () => doc.defaultView.dispatchEvent(new Event('blur')), captured: () => captured };
}

test('click and sub-threshold jitter preserve normal selection', () => {
const f = fixture(); f.send('pointerdown'); f.send('pointermove', { clientX: 3 }); f.send('pointerup');
assert.deepEqual(f.calls, []); assert.equal(f.send('click').defaultPrevented, false); f.controller.destroy();
});
test('pointer move completes without any HTML5 drag event and consumes its click', () => {
const f = fixture(); f.send('pointerdown'); f.send('pointermove', { clientX: 8 });
assert.equal(f.captured(), 1); f.send('pointerup', { clientX: 90 });
assert.deepEqual(f.calls, [['start', 'p2'], ['move', 8], ['finish', false]]);
assert.equal(f.captured(), null); assert.equal(f.send('click').defaultPrevented, true);
assert.equal(f.send('click').defaultPrevented, false); f.controller.destroy();
});
for (const type of ['pointercancel', 'lostpointercapture']) {
test(`${type} cancels instead of committing`, () => {
const f = fixture(); f.send('pointerdown'); f.send('pointermove', { clientX: 8 }); f.send(type);
assert.deepEqual(f.calls.at(-1), ['finish', true]); assert.equal(f.controller.isDragging(), false);
f.send('pointerup'); assert.equal(f.calls.length, 3); f.controller.destroy();
});
}
test('explicit cancellation and cleanup release capture and document listeners', () => {
for (const method of ['cancel', 'destroy']) {
const f = fixture(); f.send('pointerdown'); f.send('pointermove', { clientX: 8 }); f.controller[method]();
f.send('pointermove', { clientX: 20 }); f.send('pointerup');
assert.deepEqual(f.calls, [['start', 'p2'], ['move', 8], ['finish', true]]);
assert.equal(f.captured(), null); f.controller.destroy();
}
});
test('right button, secondary pointer and touch scrolling do not begin page moves', () => {
for (const props of [{ button: 2 }, { isPrimary: false }, { pointerType: 'touch' }]) {
const f = fixture(); f.send('pointerdown', props); f.send('pointermove', { clientX: 20 }); f.send('pointerup');
assert.deepEqual(f.calls, []); f.controller.destroy();
}
});
test('other pointer IDs cannot move or finish the active gesture', () => {
const f = fixture(); f.send('pointerdown'); f.send('pointermove', { pointerId: 2, clientX: 20 });
f.send('pointerup', { pointerId: 2 }); assert.deepEqual(f.calls, []);
f.send('pointermove', { clientX: 8 }); f.send('pointerup');
assert.deepEqual(f.calls.at(-1), ['finish', false]); f.controller.destroy();
});

test('losing window focus cancels an unfinished drag', () => {
const f = fixture(); f.send('pointerdown'); f.send('pointermove', { clientX: 8 }); f.blur();
assert.deepEqual(f.calls.at(-1), ['finish', true]); assert.equal(f.captured(), null);
f.send('pointerup'); assert.equal(f.calls.length, 3); f.controller.destroy();
});
Loading
Loading