Skip to content

Release 2026.42.0: integrate all finished fixes, features and contributor PRs - #576

Merged
DutchSailor merged 96 commits into
mainfrom
integratie/release-2026.42.0
Oct 14, 2026
Merged

DutchSailor merged 96 commits into
mainfrom
integratie/release-2026.42.0

Conversation

@DutchSailor

@DutchSailor DutchSailor commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Release 2026.42.0: one integration of all finished work, instead of separate PRs. Merge with "Create a merge commit" (not squash): the included PRs then land unchanged and GitHub closes them, and the issues below close through the keywords here.

Bugs reported by users

Feature requests from users

Contributor PRs included

Our own issues

Translations

Every key that exists in English is now translated in all 40 languages, including the backlog of earlier releases (about 28,000 missing keys and 1,300 untranslated texts).

Verification on the combined branch

🤖 Generated with Claude Code

DutchSailor and others added 30 commits September 24, 2026 19:22
New workspace crate open-pdf-cmyk for the CMYK conversion of the PDF/X
export (#422). This first part reads ICC headers: a file only counts as a
printing profile when it carries the acsp signature, the CMYK data colour
space and the output device class. The display name comes from the desc
tag (ICC v2 text description or v4 multi-localised Unicode) and falls
back to the file name. Profiles are searched recursively in the system
colour folders of Windows, macOS and Linux.
The document conversion only talks to a CmykTransform trait, so unit
tests can use a plain RGB-to-CMYK formula with hand-checkable numbers.
The report counts, per kind of content, what was converted and what was
left alone with a reason code the interface can translate.
rg/RG become k/K, and sc/scn/SC/SCN are converted while the active
colour space is RGB (DeviceRGB, CalRGB or ICCBased with three
components), following cs/CS and the q/Q stack. A DeviceRGB operand of
cs/CS becomes DeviceCMYK; named spaces keep their name because the
resource entry itself is replaced. Grey stays grey. Uncoloured patterns
on an RGB base get their components converted too.

The rewriter works on tokens with byte positions, so everything that is
not a colour operator - text, paths, comments, inline image data - is
copied byte for byte. Inline images are handed to the caller with their
parsed dictionary; malformed operators are left alone and reported.
Indexed images on an RGB base only get their palette converted. Other
RGB images are decoded (ASCIIHex, ASCII85, RunLength, Flate with PNG or
TIFF predictors, and DCT as the last filter), unpacked from 1 to 16 bits
with /Decode applied, converted, and written back as Flate with
/DeviceCMYK and 8 bits per component. /SMask is kept.

Inline images are rewritten with [/AHx /Fl] data, so no stray EI can
appear inside the image data. JPEG 2000, LZW, colour-key masks and
unreadable data are refused with a reason for the report.
…pdate

Walks every page with its content streams, inherited resources,
annotation appearances and thumbnail; from the resources it follows Form
XObjects (recursively, each once), images, tiling and shading patterns,
shadings, soft masks in graphics states and Type 3 glyph procedures.

Named RGB colour spaces are replaced in the resources, transparency
groups and soft-mask backdrops move to DeviceCMYK, and an image's /Matte
follows its parent. Axial and radial shadings with an exponential or a
stitching function get their C0/C1 converted; three single-output
functions are merged into one. Other shadings, and Separation or DeviceN
on an RGB alternate, are left and reported. ICCBased RGB colour uses its
embedded profile as source when the transform accepts it.

Everything is read from the original; changed objects are appended as an
incremental update, so untouched objects stay byte for byte.
…onse format

LcmsTransform converts from sRGB, or from an embedded RGB profile, to the
chosen CMYK printing profile with relative colorimetric plus black point
compensation (default) or perceptual intent. Single colours go through a
16-bit transform so paper white stays exactly 0 0 0 0.

convert_with_profile validates the profile, converts the document and
returns the profile bytes and name for the output intent. pack_response
puts report, profile and PDF in one binary buffer for the webview.

The integration test uses a real printing profile from the system colour
folder and skips itself when there is none.
Reads a PDF (never written to), converts it with a printing profile and
prints the report with timings, for end-to-end checks on real files.
The webview sends the PDF as a raw body and the profile path in a
header; headers are ASCII, so the path travels percent-encoded.
Rewriting photos as Flate made photo-heavy files grow many times over.
Images that were DCT-encoded now go back as a CMYK JPEG at a fixed
quality of 92, through the libjpeg-turbo build already in the
workspace; Flate stays for everything else.

The JPEG uses the inverted form found in practically every existing PDF:
inverted samples, the APP14 marker, and /Decode [1 0 1 0 1 0 1 0] on the
image. PDF readers do not invert a CMYK JPEG by themselves but do follow
/Decode; checked with PDFium and PDF.js on red, green, blue, white, black
and grey patches, which render the same as the Flate and vector
versions.
convert_document_with_progress calls back with (done, total) pages, first
with zero and then after every page, so the app can show how far a long
conversion is.
…JPEG

Source JPEGs almost always use 4:2:0 already; luminance and black stay at
full resolution. Inverting the samples in place saves a second buffer,
which for a 150-megapixel scan is hundreds of megabytes.
…ed a CMYK profile

pdfx-enrich.js holds the enrichment with pdf-lib only, so it runs under
node --test. Without an output profile it writes exactly what the export
wrote before: the test pins the PDF/X-3 and PDF/X-4 output for a fixed
input to the bytes of the previous implementation. With a CMYK printing
profile the OutputIntent embeds that profile (Flate, /N 4) and its name
becomes OutputConditionIdentifier and Info.
…file

pdfx_list_cmyk_profiles scans the system colour folders,
pdfx_inspect_profile checks a file the user picked, and
pdfx_convert_to_cmyk takes the PDF as a raw request body with the profile
path and rendering intent in headers, and answers with one raw buffer
(no JSON number arrays, see #463). The work runs on a blocking thread
and reports page progress as a pdfx-cmyk-progress event. All logic lives
in open-pdf-cmyk.
A command that takes a raw body needs its parameters as request
headers; invoke() now forwards a third options argument when given and
calls Tauri exactly as before when it is not.
…h and Dutch

pdfx-cmyk.js sends the PDF as a raw body with the profile path and
rendering intent as headers, unpacks the binary answer without copying,
formats the report (converted and not converted per kind, with reasons,
and a warning when the file more than doubles in size) and validates the
remembered choice. New strings live under appMenu pdfxCmyk in en and nl;
pdfxNote now describes the new behaviour, and the other locales, which
carried an English copy of the old note, get the new English text.
…nvert on export

The PDF/X options get an output profile list: sRGB without conversion
(still the default), the CMYK printing profiles found in the system
colour folder, and Choose file... for any .icc or .icm, which is checked
and refused with a clear message when it is not a CMYK output profile.
A rendering intent list (relative colorimetric with black point
compensation, or perceptual) appears once a CMYK profile is chosen. A
short text explains where to get a profile. The last choice is kept in
the preferences.

On export, a CMYK profile first sends the PDF to the Rust conversion,
with page progress in the loading message, then embeds that profile as
the output intent, and ends with the report of what was converted and
what was not. The sRGB path never calls Rust. In the browser build the
one desktop-only message is shown. The KNOWN LIMITATIONS block now
matches this behaviour; unembedded standard fonts remain a limitation.
Multi-line messages keep their line breaks.
Explains the output profile choice of the PDF/X export: ask the print
shop for its profile or use a freely available profile set from a
printing industry association, where to put it on Windows, macOS and
Linux so the app finds it, the two rendering intents, and what is and is
not converted.
Only APIs that also exist on the workspace's minimum Rust version.
…treams

lopdf 0.34 decodes the PNG Average predictor as left + up/2 instead of
(left + up)/2. Cross-reference stream rows that use it, and the rows
after them, get nonsense offsets, and those objects silently disappear
from the loaded document; the conversion then silently skipped them. On
a real drawing this left two annotation appearances in RGB.

The loader now reads the cross-reference chain itself with the correct
predictor, compares it with lopdf's table, and reloads every object whose
position differs: plain objects only when the file really has
'<nr> <gen> obj' at that offset, compressed ones from their object
stream. The object parser learned indirect references for this.
…them

A content stream, Form XObject, appearance, pattern, shading or image
that a document points to but that cannot be loaded is now counted as
not converted with the reason 'unreadable', so the report never claims
more than was done.
The test now looks for the APP14 marker bytes themselves.
…file

The warning now needs more than a doubling and more than 1 MB extra; a
few-kilobyte drawing grew 30 times just from the embedded printing
profile. The text names both causes of growth.
…nd zoom steps

Rendering
- Zoom tiles are refined to the exact screen resolution once the view
  settles; between 150% and 300% a 300% coverage tile was shown shrunk by
  a non-integer factor, softening thin strokes.
- The page bitmap and tiles are drawn on whole device pixels (rounded
  transform translation) so the canvas no longer blends every pixel with
  its neighbours at a fractional offset.
- Continuous view renders each page at scale * devicePixelRatio and sizes
  the canvas from the bitmap, so it is drawn 1:1 instead of being resampled
  by a fraction of a pixel (blurry table rules and text).
- New Page Display preference 'Page sharpening' (Off/Light/Medium/Strong/
  Maximum, default Medium): a light 3x3 unsharp mask on fresh page and tile
  bitmaps for darker, crisper text. Changing it flushes the bitmap caches.

Zoom
- 100% is defined by a screen resolution preference (Page Display, default
  110 dpi: 1 PDF point = dpi/72 CSS px) instead of 1 point = 1 px. The
  status bar shows/accepts the percentage in that unit and Actual Size uses
  it; the internal scale is unchanged.
- Zoom buttons, keys, Ctrl+wheel and pinch (single-page and continuous view)
  step through round levels: 100, 125, 150, 200, 300, ...
An idle window kept two requestAnimationFrame loops running at the display
refresh rate: the viewport render loop rescheduled itself every frame even
without work, and the canvas scrollbars polled the viewport every frame even
with scrollbars switched off. On a 240 Hz screen that cost about 15% of a CPU
core in the renderer and 7% in the GPU process while nothing happened.

- The viewport render loop now runs on demand: viewport.dirty and
  viewport.active are accessors that plan one frame when there is work, and
  the loop continues only while work remains (a wheel ease-out still pumps
  every frame). All existing writers are unchanged.
- The canvas scrollbars follow the viewport through volgViewport() (after
  each render and when the viewport turns on or off) instead of polling; the
  bar geometry moved unchanged to scrollbalk-geometrie.js.
- Tests cover the frame planner, the real viewport loop under a fake rAF and
  clock, and the scrollbar geometry against the old formulas.
Dragging a grip of a rotated shape (such as a rectangle with /Rotation 270
from another editor) jumped whenever an object snap engaged: the snapped
delta was measured from the grip in the unrotated frame instead of where the
grip is drawn. The dashed tracking line started from the same wrong point,
and its tooltip multiplied the length by the document measure scale, which
is an object, showing "NaN px".

- The snap origin and the tracking-line base use the on-screen grip
  position; unrotated shapes are unchanged.
- The tooltip converts the length with the same scale lookup as the
  width/height fields (e.g. "3.13 m") and shows points only without a scale.
- A two-point symbol without stored end points no longer collapses on a
  snapped drag, and the equal-size snap skips rotated images.
- Tests cover rotated shapes of all types, the tooltip units and the wiring
  in the resize handler.
…om grid)

Adds a Pages per Sheet group to the print dialog: a fixed count or a custom
columns-by-rows grid, a page order (horizontal/vertical, normal or reversed)
and an optional page border. The finished print PDF (one page per source
page, from the raster or the vector path) is re-laid out on sheets by
print-meerperblad.js, so both paths and both targets (printer and Save as
PDF) share it. The page scaling does not apply with more than one page per
sheet; each page is fitted and centred in its cell.
Some viewers (phone previews, chat apps) skip annotation types such as
polygons, so a plan with markups looks empty there. The new Export card
'Flattened Copy for Sharing' saves a copy through the Save-as-PDF print path
with every page at its own size and the markups drawn into the page, so they
show in any viewer. Text stays selectable; the original is left untouched.
DutchSailor and others added 14 commits October 14, 2026 01:35
…el per point

Without a screen calibration, 100% zoom now follows the screen resolution
preference, so the second sentence of pageDisplay.calibrationNone was wrong.
English and Dutch point to the preference; the other languages keep only
their first sentence.
# Conflicts:
#	open-pdf-studio/scripts/unit-testlijst.txt
# Conflicts:
#	open-pdf-studio/js/solid/components/app-menu/ExportPanel.jsx
#	open-pdf-studio/scripts/unit-testlijst.txt
# Conflicts:
#	open-pdf-studio/scripts/unit-testlijst.txt
…rdinate tools (#541)

Adds Offset, Stretch, Enlarge, Fillet, Chamfer and Spot Coordinate to the
Drawing tab, Move with a base point, and typed dimensions for boxes,
circles, ellipses, polygons and L-shapes.
- Lao (#537): register 'lo' in the language list, count 40 locales in the
  locale tests, and use one wording for Shift Page.
- Pages per sheet (#540): flush lazily embedded images and fonts before
  placing pages, so raster prints no longer give blank cells.
- Drawing tools (#541): never edit locked, hidden or locked-layer objects;
  keep arc segments on stretch, enlarge and offset; update measurement
  labels after an edit; keep spot coordinates on recalculation; fix typed
  shape dimensions; undo for an interrupted enlarge preview; offset copies
  through the duplication primitive.
- Flattened copy (#542): keep hidden layers hidden and warn about form
  field values before saving.
- Add Text (#543): only move the start point when the text does not fit.
- Translations: every key that exists in English is now translated in all
  39 other languages, including the backlog of earlier releases.
… or Chamfer

syncMeasurementAfterEdit recomputed measureArea with the document scale.
Use herberekenVlakMaat, the routine grip edits use, so an area with its
own measure scale keeps its value after an edit tool changes it.
Lao is the 40th locale; the edge-length, display calibration and tab
switching tests still expected 39.
… into all locales

Every key that exists in English is now present in all 40 languages,
including the CMYK/PDF-X export options, the page sharpening and zoom
DPI preferences, and the Lao texts for the features of this release.
Two texts whose English changed in this release are retranslated.
…re part of the page

# Conflicts:
#	open-pdf-studio/scripts/unit-testlijst.txt
# Conflicts:
#	open-pdf-studio/scripts/unit-testlijst.txt
…ize cursor to each handle

The dispatcher forwards a real double-click as a synthetic pointer-down with
detail 2. The image tool already handles the double-click on the second press,
so the extra call started a drag that no pointer-up ended: after replacing a
picture, the drag preview followed the mouse. Edge handles now show a straight
resize cursor instead of the diagonal one.
Adds the new image-editing hint and move/replace error messages and the
Edit Scan Text button, hint and empty-page message to the 38 locales that
only had English and Dutch, in English key order. The image tool's ribbon
label now reads Edit image in every language, matching its new behaviour.
#550, #551)

- MCP tools app_list_extensions, app_install_extension and
  app_reload_extension install, update and reload a local .oppx package
  without a file dialog or app restart. Installs are transactional
  (staging folder, backup, journal, recovery at startup); on any failure
  the previous working version stays active, and open tabs and unsaved
  changes are left alone.
- registerRibbonTab lets an extension add a normal ribbon tab with groups,
  actions, dropdowns and status, drawn by the host (no DOM access), with
  keyboard navigation, collapse and an overflow menu. Extension actions get
  command ids usable through app_list_commands and app_run_command.
- One contribution registry per extension cleans up every contribution on
  deactivate and reload. Extension ids are validated strictly.
…e plugin API stubs

plugin-api.js now also imports the system type registry, the system
geometry host and getAnnotationBounds. Load the dependency-free
annotation-batch.js for real (the system type contract needs
assertSerializable), stub the geometry host and export
getAnnotationBounds from the state stub.
The Lao locale arrived after the extension work branched off, so it
missed extensionsPanel.activationFailed, extensionsPanel.viaMcp,
common.moreTabs and common.extensionTabFailed. The wiring test now
expects all 40 locales.
…tools

The five system type tools stay together right after app_facade_element;
the three extension tools follow them at the end of the list.
@DutchSailor

Copy link
Copy Markdown
Collaborator Author

Live gate on a test build of this branch (one run for the whole release, before #572, #573 and #550/#551 were added; those have their own unit tests and, for #550/#551, 38 live checks over MCP):

Check Result
Text rotation (reference, external file, round trip) pass
Placement under the cursor (zoom 1 / 2 / 3.52) pass
Continuous view shows the page (zoom 0.75 / 1 / 1.5 / 2.5) pass
Fast zoom: anchor holds, wrapper sizes consistent pass
Search: 54 checks (single and continuous, 50 / 100 / 175 %), results list, Escape pass
No duplicate annotations on save or close-and-save pass (the first attempt timed out on its very first call right after the test instance started; the rerun passed)
Save round trip over the verification set 33 good, 2 with damage: the same two files as in releases 2026.39.2 and 2026.41.0, so no regression
Rotation sweep 46/46 render

@DutchSailor
DutchSailor merged commit f0fb1b8 into main Oct 14, 2026
4 checks passed
@DutchSailor
DutchSailor deleted the integratie/release-2026.42.0 branch October 14, 2026 04:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment