Repository navigation
Release 2026.42.0: integrate all finished fixes, features and contributor PRs - #576
Merged
Merged
Conversation
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.
… the content to CMYK (#422)
…% zoom with round zoom steps
…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.
4 tasks done
…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.
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):
|
4 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
)(e.g. "1) item") no longer corrupts the saved PDF (also in the web unit and for line heads from XFDF).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
npm run test:unit: 3622 passed, 0 failed;npx vite buildandcargo checkgreen; every English key present in all 40 locales.🤖 Generated with Claude Code