With our fix-up 8a0f959d29 on top of the author's head 072a87e7b0.
Conflicts with the batch resolved: Image Size keeps #10702's point sizes and
writes `scaleStyles` only when true (no key and false play the same in the
reference application, so recorded steps save back unchanged); Convert to
Profile takes #10704's options; new tests carry #10743's `encoding` field;
#10702's absolute-intent test expects the new Convert to Profile flags.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf: reuse large buffers so a pinned glibc trim threshold costs no speed (#10479)
With MALLOC_TRIM_THRESHOLD_ set (#8743), glibc maps every allocation over
128 KiB fresh, so tiles, compositor bands and filter scratch were page-faulted
in on every call: viewport compose 3.6x slower, Gaussian Blur up to 1.8x,
.pcraft save 2.4x.
- photocraft_raster::scratch: a process-wide, capped pool of large Vec
buffers (u8/u16/f32/[f32; 4]). A miss drops the oldest buffers worth its
size down to a 256 MiB floor, so peak memory stays near glibc's defaults.
The engine releases the pool after any command that closes a document,
Headless::close does too, and an idle thread releases it after 2 s unused.
- Tiles, compose Buffer/masks/bands, algo Image and the blur paths, engine
adjustment/filter scratch, GPU tile upload and io import buffers go through
the pool.
- .pcraft save reuses one zstd FrameCompressor per worker (byte-identical).
- interactive_bench reports minor page faults per run.
36 MP, trim threshold set vs glibc defaults: viewport 5.6 vs 6.1 ms (was
22.7), Blur r4 445 vs 437 ms (was 852), Curves CPU proxy 35.0 vs 35.7 ms
(was 50), flatten 146 vs 147 ms (was 237), save .pcraft 251 vs 230 ms (was
545). RSS after closing a 5400x7200 document: 21 MB with trim set.
* fix(linux): restart once with glibc's trim threshold set so closed documents return memory (#8743)
With glibc's default malloc, the memory of a closed document stays with the
process: freeing a large buffer raises glibc's dynamic mmap threshold, so
later tile buffers land in heap pages glibc keeps. A session that opened and
closed a 5400x7200 document held ~450 MiB afterwards.
photocraft_engine::malloc_tuning::apply(), called first in both binaries'
main, re-executes the binary once with MALLOC_TRIM_THRESHOLD_=131072 on Linux
with glibc: same PID, arguments, argv[0] and environment, in safe Rust
(CommandExt::exec). A malloc variable or glibc.malloc.* tunable the user or a
package already set is left alone, so setting MALLOC_TRIM_THRESHOLD_ opts
out. No-op on macOS, Windows, musl and wasm.
Depends on the scratch-buffer pool (#10479), without which the pinned
threshold slows filters, previews, flatten and .pcraft save by 26-209%.
* fix: corpus tests build again; return remaining pooled buffers (#10479 review)
- Corpus-gated io tests and the Photoshop-oracle test moved `.px` out of a
`Buffer`, which now implements `Drop` (E0509 with `--features corpus`):
use `into_px()`.
- io: masks, the merged image, extra channels, smart-filter masks,
smart-object pixels and patterns give their `interleave` buffers back to
the scratch pool after writing them.
- compose: recycle the mask values of a pass-through group, `effect_shape`
and both `split_parts` callers.
- Styled Free Transform preview copies each band out instead of moving it,
so the band returns to the pool.
- interactive_bench: the fault count covers each whole run, untimed setup
included; say so.
* fix: corpus tests build again; return remaining pooled buffers (#10479 review)
- Corpus-gated io tests and the Photoshop-oracle test moved `.px` out of a
`Buffer`, which now implements `Drop` (E0509 with `--features corpus`):
use `into_px()`.
- io: masks, the merged image, extra channels, smart-filter masks,
smart-object pixels and patterns give their `interleave` buffers back to
the scratch pool after writing them.
- compose: recycle the mask values of a pass-through group, `effect_shape`
and both `split_parts` callers.
- Styled Free Transform preview copies each band out instead of moving it,
so the band returns to the pool.
- interactive_bench: the fault count covers each whole run, untimed setup
included; say so.
* fix: recycle the masks of adjustment-atop and shape layers (#10479 review)
* fix: recycle the masks of adjustment-atop and shape layers (#10479 review)
* fix: recycle effect hiding masks (#10479 review)
* fix: recycle effect hiding masks (#10479 review)
* fix: recycle knockout, CMYK and GPU shape masks; release the pool when Split/Merge Channels close a document (#10479 review)
* fix: recycle knockout, CMYK and GPU shape masks; release the pool when Split/Merge Channels close a document (#10479 review)
* fix: release the pool when a closed document's cancelled workers exit; idle-release switch for tests; don't pool PSD planes (#10479 review)
* fix: release the pool when a closed document's cancelled workers exit; idle-release switch for tests; don't pool PSD planes (#10479 review)
* fix: release the pool for workers cancelled before their document closes; take the pool lock for budget and idle-release changes (#10479 review)
* fix: release the pool for workers cancelled before their document closes; take the pool lock for budget and idle-release changes (#10479 review)
* fix: idle release rechecks activity under the lock; try_take_exact releases the pool and retries before failing (#10479 review)
* fix: idle release rechecks activity under the lock; try_take_exact releases the pool and retries before failing (#10479 review)
* test: cover a successful try_take_exact miss; name the failed-reservation test for what it checks (#10479 review)
* test: cover a successful try_take_exact miss; name the failed-reservation test for what it checks (#10479 review)
* fix: pool the final GPU effect shape buffer and return it after use (#10479 review)
* fix: pool the final GPU effect shape buffer and return it after use (#10479 review)
* docs: describe scratch pool lifecycle regression invariants
* test: verify polling releases cancelled document scratch buffers
* perf: recycle remaining transform selection and PSD scratch buffers
* fix: free PSD gather bands before allocating channel planes
* test: assert exact scratch take reuses pooled storage
The three version 12 fixtures were tracked inside crates/psd/fuzz/corpus,
which crates/psd/fuzz/.gitignore ignores and cargo-fuzz rewrites (cmin) or
deletes; the psd and engine test binaries include_bytes! them, so cleaning
the corpus broke the build. They move to crates/psd/tests/fixtures/atn with
a README (sizes, purpose, sha256; the seeds note folded in) and an
ATTRIBUTION.md row; the fuzzer takes the folder as an extra corpus dir.
photocraft-cli recognised an extensionless Actions file only by version
word 16, so a version 12 set without .atn (classic Mac OS Photoshop 5)
went down the JSON path and failed with a UTF-8 error. It now accepts 12
and 16, the versions photocraft-psd reads. Input: v12-commands.atn copied
to "Legacy Actions" and passed to load_actions_file.
Co-authored-by: yoriswind <164435053+yoriswind@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read bounded original SVG through the platform clipboard service for native
Paste options and action playback. Preserve a newer internal copy when an
older OS source remains unchanged, and report failed or panicking reads
without losing the existing copy. Retain ordinary raster clipboard routes.
Add source replacement, mode/output, Undo, action playback, stale-source,
size-bound and panic/retry regressions. Completes the app intake for the
source-backed backend adapted from Skyfuller1303's PR #3409.
Validation: 104 unique UI/Actions/native-service passes, one existing manual
clipboard ignore; actual app-model Pixels/Shape renders and saved Path;
2921 engine package passes (22 ignores), adversarial pass, all-target strict
engine/UI/app Clippy, full format, 31-crate layers and standard wasm/HEIF.
Conflicts resolved as unions of both sides: codecs re-exports (PngRows + TiffBands), smart_cmds source imports
(main's source_surface_from_buffer and cache API with #8571's svg_source_image_in/convert_svg_source; main's
duplicate rendering import dropped), io flat.rs test modules (#8536's banded TIFF tests and main's PNG ones),
menus/invoke.rs (#8571's smart SVG intercept and main's layer.selectUnderCursor), state.rs (#8311's
type_box_overflow and main's type_insertion_style), the canvas_geom test keeps the is_finite guard, docs
status lines keep both change lists, i18n catalogs keep both sides' rows (hi.tsv: main's wording where both
added the same string).
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#2f7bf5, its colour in ArtCraft Launcher). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).
craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- apps/photocraft/src/app_dirs.rs
- crates/codecs/src/codecs/via_image.rs
- crates/engine/src/canvas_geom/tests.rs
- crates/ui-egui/src/panels/blend_preview.rs
Main's noop_step now takes the step's params (an Arrange that cannot move the layer is a no-op too); it is public for the CLI's run and batch, which pass them.
- Smart-object contents (Replace Contents, Convert to Layers, Stack Mode,
internal Edit Contents) refused any SVG whose canvas area times its
node count passed 24 MP, e.g. a 1920x1080 layout with 12 shapes.
decode_source now uses the bounded image parse with File > Open's
layered import (import_svg_bounded_images). Convert to Layers' shape
cache cap is 1 GiB in the document's format, the import's own budget,
not 24 MP (four full-canvas shapes in a 4K document failed).
- The bounded resolver failed the whole drawing on an embedded image it
can't read (an unknown MIME type, a corrupt PNG); it now leaves it out
as usvg does, so such SVGs place and render again. Byte and decode
overruns stay errors.
- resvg gives each embedded SVG image a scratch pixmap the size of its
target and frees it after, so the scratch budget counts the deepest
chain of nested SVG images, not every sibling: 12 SVG icons on a 2 MP
canvas, or one at a 12 MP render, no longer fail.
- Main's shared nesting budget (#9397) now covers the bounded path too:
511 + 511 levels across a drawing and its embedded SVG overflowed a
1 MB stack.
- An AI working copy is regenerated through VectorCraft only when
VectorCraft is the editor; Illustrator or another program gets the
original bytes. VectorCraft is recognised by bundle id, a known
install, or its name (vectorcraft[.exe], vectorcraft-<version> AppImage,
ai.storyteller.vectorcraft).
- updateSvg refuses an AI/PDF/EPS source (updateVectorSource saves it
back in its own format); the doc no longer calls updateSvg "existing".
- Edit Contents on contents already open in the internal editor switches
to that document, as main does, instead of an error.
- Stack Mode on an SVG refuses up front when one float frame per layer
would pass 1 GiB (the layered import no longer refuses for it).
- A stored display preview still fails on an embedded image it can't
read (strict budget, its own import_svg_preview_with_work; external-
editor saves and conversions leave the image out as placement does); skipped embedded SVGs still use up the XML byte
budget; an image under an unknown MIME type or with no pixels is left
out uncharged.
- Merge: the PSD import reads a preview's native source from the
document's global blocks; main's owned open moves them out of the
file, so every stored preview was dropped.
- Main's strict Clippy rules on the PR's app code (explicit import, a
Unix-only test helper, one long opt-in test).
Co-authored-by: Mocky Digital <282319807+mockydevs@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: svg.rs (main's bufread inflate and shared render helper, kept
with the PR's bounded raster path), psd_import.rs (the PR's vector
preview moved into main's smart_content), menus/invoke.rs (the SVG
intercept moved into main's split invoke_unguarded), rendering.rs and
the ui-egui import lists (main's explicit imports), Cargo.lock, docs
status lines (unions).
Port behavior and regression coverage into current modules; preserve current upstream behavior.
Integrate lossless export-only JXL and independent decoder regression matrix, preserving current JPEG/TIFF save extensions and 12-bit/arithmetic JPEG decoder dependency.
Port behavior and regression coverage into current modules; preserve current upstream behavior.
Prebound the listener inside current authorized control_setup before Linux display and GPU initialization, retaining integration preferences, workspace grants, authentication and single-instance routing. Preserved actual port logging, added occupied-port/prebound-dispatch regressions and bounded child cleanup to current process test harness.
Port behavior and regression coverage into current modules; preserve current upstream behavior.
Register starter actions and bounded background-blur command in current modules; preserve persistent migration marker and portable JSON defaults compatibility.
serve_fonts asks for the interface language's served script font once per
language, but the font manifest loads asynchronously: when the first frame
ran before it arrived, no fallback was registered yet, the request found
nothing, and an Arabic interface on an English browser stayed without its
font until the language changed. The manifest load now asks for the
interface language's font along with the browser languages'.
Review finding on #10006.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web build has no system fonts and embeds no craft fonts, so an Arabic
(or Japanese, Chinese, Korean, Hindi) interface had no font for its labels:
served fonts were registered only in the document text engine. A served
script fallback now also joins the end of every egui font family, and the
font stack theme::install_fonts rebuilds on a language change includes it
again. The web shell asks for the interface language's script font, not
only the browser languages'.
Review finding on #10006.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main now carries #9031 (Halftone) and #8923; this branch keeps only what it adds on top: the guard for a setting beside a Gallery stack's effect list, its test, and the PSD open warning for patterns that paint nothing.
Reapply Linux Adwaita no-title winit feature and its lockfile,
keep integrated title bar as default, and port compatible tests/docs.
Preserve upstream's preferences UI refactor rather than restoring
its removed prefs_ui.rs file.
Integration batch 9 (#9775) raised the preference count to 181 and
added its own 2026-10-27 status lines and history rows to gaps.md and
ui-parity.md. Both sides are kept: the scorecard regenerates to 11 of
181 do nothing, the branch's entries say 181 and are dated 2026-10-27,
above main's 12-of-181 rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Document windows (Window › New Window), Camera Raw and the screen color
picker were built from ViewportBuilder::default() with no app id, so on
Wayland their class was empty: Hyprland rules couldn't match them and docks
couldn't map them to the .desktop file. They now use the same app id as the
main window, defined once in photocraft_ui_egui::os_window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a pen attached, or Preferences › Performance › Linux display server set
to X11, the window opens through Xwayland on a Wayland desktop. The picker
followed the window's display server and grabbed Xwayland's root window,
which holds no desktop pixels: GetImage failed with BadMatch. Choose the
backend from the desktop session instead, so Wayland always uses the
portal's PickColor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picks up the export speed work (#9877) and the rest of main. Main's
prefs audit now counts 180 preferences, so the regenerated scorecard
reads 11 of 180 do nothing, and the branch's doc entries say 180 to
match; the 2026-10-25 history rows keep their 178.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #9775: the Linux portal swaps the extension for the picked type, but macOS and rfd keep the suggested name, so an opened image.pgm could still be saved as image.pgm holding P6 data. The suggested name itself now takes .pnm for any Netpbm subtype; the services test fails without it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts with main's batch #9880:
- psd file.rs / layer.rs: #5287 (PSB by encoded size, RLE tables widened on
promotion) against #6583's lazy reader. Main's new widening and length code
read `.data`, which #6583 leaves empty while the bytes stay in the source;
it now goes through `encoded()` / `encoded_len()` (also the auto-merged
lines in `rle_widening`, `encoded_len` and `LayerRecord::write`), and the
lazy reader sets `write_as_psb: false`. New test
`a_lazy_psd_promotes_to_the_same_psb_as_an_eager_one`: a lazily read PSD
writes the same PSB bytes as an eager one; it fails with a `.data` read.
- direct_select.rs / vector_ui.rs: #9321's Type Path and #6353's hidden
Work Path both kept.
- io tests main.rs: both modules. scorecard.md regenerated (12 of 181);
ui-parity.md and gaps.md follow it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CodeRabbit on #9775: save_filters adds an unlisted suggested extension as its own type, so Save As on an opened .pgm offered .pgm again, though the encoder writes P6 for an RGB document. A suggested .pbm/.pgm/.ppm/.pam/.pfm now starts on the Netpbm type as .pnm; the services test checks every subtype name and fails without this.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Main split crates/engine/src/prefs.rs into prefs/ and moved the
gpu_startup tests to gpu_startup/tests.rs: read_section now sits beside
read_lenient in prefs/store.rs, and the malformed-sibling assertions
moved with the tests. Doc status lines and revision rows keep both sides.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #9775: the encoder picks P5/P6/P7/PFM from the image, not the name, so a .pbm, .pgm or .pfm picked in Save As held other data (an RGB document as P6 in a .pbm). Only the neutral .pnm is offered; File > Open still reads every Netpbm extension.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Main reorganised the source tree (command modules under engine/src/cmd, tools and dialogs under ui-egui/src, large files split into modules, inline test modules moved to their own files). This branch's changes to the moved and split files are ported to their new locations.