Commit Graph

2494 Commits

Author SHA1 Message Date
Brandon Thomas
8407d9ccb1
Release 0.10.0 (#7192)
Some checks are pending
Packaging lint / lint (push) Waiting to run
Packaging lint / Verified AppImage inputs (push) Waiting to run
* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cargo.lock: photocraft-ipp at 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:28:00 -04:00
skylar taylor-barrick
ead6d2197f
Package: report its paths with / on Windows too (#7184)
`file.package` absolutised the caller's `dir` and then built `folder`, `document`
and every `links` entry on it with `join`, which always writes `/`. On Windows
that left one string spelt both ways:
`C:\Users\...\pkg/Print Me/Links/art.png`. The control channel takes `/` and
rejects alternate separators, so a reported path could not be handed back to it.

Spell the absolutised directory with `/` as well, keeping a leading `\\`
(verbatim and UNC prefixes, where Win32 does not accept `/`) intact.
2026-10-17 19:20:33 -04:00
Diego Chagas
235e7578f3
Print through CUPS over IPP and the print portal, so File › Print works in the Flatpak (#2728) (#3017)
Main prints through craft-print, which runs `lp`; a Flatpak has no `lp`,
so File › Print fails there. This keeps craft-print as the route wherever
`lp` is on the path (and on Windows) and adds two more behind it:

- `photocraft-ipp`, a pure-Rust IPP client (CUPS-Get-Printers/Classes/
  Default, Print-Job over HTTP/1.1) that talks to CUPS over its socket or
  TCP; the Flatpak manifests grant --socket=cups for it. A `printServer`
  named in the request always takes this route. `file.print.printers`
  lists the queues.
- The XDG Print portal as the app's spooler (`Session::print_spooler`),
  for a sandbox without the CUPS socket or a system without a printer:
  the system print dialog, seeded with copies, paper and orientation.
  PHOTOCRAFT_PRINT_PORTAL=1 prefers it, =0 removes it.

A route that exists but fails (unknown printer, refused job) is an error;
only a missing route moves on. Results gain `backend` (lp, windows, ipp,
portal). docs/printing.md describes the routes.
2026-10-17 15:55:54 -06:00
Michael Higgins
1d65bd605b
Web: Place Embedded and canvas drops place images (#5749) (#5817)
* Web: Place Embedded and canvas drops place images (#5749)

File > Place Embedded was disabled in the browser (the engine command reads a
path), though the menu path reads the picked file's bytes. Files dropped on
the canvas were always opened as new documents. The web shell now tracks the
drag position, and drops on the canvas are placed from their bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Format (cargo fmt)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Narrow to the web canvas-drop half: Place Embedded enablement landed in #5585

Drop the picker test that covered the part #5585 now owns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:20:44 -07:00
skylar taylor-barrick
06ad0d5c95
Merge pull request #2863 from 1Git2Clone/ci-panic-hunt
Some checks are pending
Documentation / mdBook build (push) Waiting to run
Nix / eval (push) Waiting to run
Packaging lint / lint (push) Waiting to run
Packaging lint / Verified AppImage inputs (push) Waiting to run
CI: run panic_hunt in the Linux test job
2026-10-17 12:14:20 -06:00
skylar taylor-barrick
039b86f8f6
Merge pull request #7149 from skylartaylor/linked-path-checks
Follow linked paths only on this computer or the document's share
2026-10-17 12:13:47 -06:00
skylar taylor-barrick
482fd899cf Build: take craft-docpath from the merged craft-libs rev
craft-libs#169 is merged, so the fork pin is no longer needed. Pinned to
storytold/craft-libs 82a24b006eefa036b43c09a67fb9a3fbd17ffe63 (main after the merge) rather than the older 12281d07,
which predates resolve_absolute and the Windows colon refusal.
2026-10-17 12:02:56 -06:00
Gert Pellin - GPE
ba40454b18
Logging: a second launch leaves the running instance's log alone (#6432) (#6478)
logging::rotate renamed photocraft.log up the chain on every start without
knowing whether another instance still wrote to it. The running instance
kept its handle, so after a second launch its live log was .1.log, after a
third .2.log, and a fourth launch deleted it.

The instance that writes photocraft.log now holds an exclusive lock on
photocraft.lock until it exits (a separate file, since Windows locks are
mandatory and would keep the log itself from being opened). A start that
finds that lock held leaves the chain alone and writes
photocraft-<pid>.log, marked by photocraft-<pid>.lock. Those side-by-side
logs are pruned to the newest KEEP once their instances have exited; a
held one is never touched. Rotation and pruning run under rotate.lock so
two starts at once can't interleave. On a file system without locks every
start rotates as before.
2026-10-17 11:57:39 -06:00
Khawar Zamman Wani
378231214a
Desktop app: photocraft --help prints usage; unknown options are usage errors (#6431) (#6435)
* fix(desktop): `photocraft --help` prints usage; unknown options are usage errors (#6431)

The desktop binary's argument loop took every argument it didn't know as
a file, so `photocraft --help`, `-h` or a typo such as `--safegpu`
opened the window and logged "Couldn't open --help".

- `--help` / `-h` print the usage (every option and environment
  variable) and exit 0.
- Any other argument starting with `-` (except macOS `-psn_`) is named
  on stderr and the launch exits 2 before the log file or window, along
  with any bad `--control` port (`control_args_exit_code` becomes
  `usage_exit_code`).
- `--` ends the options, so a file whose name starts with `-` opens.
- `--help` and `--version` write without `println!`, so a closed pipe
  (`photocraft --help | head -1`) can't panic.

Tests: apps/photocraft/tests/command_line_usage.rs runs the binary with
no display; 4 of its 6 tests fail on the old code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(desktop): usage tests exit before the window opens; fmt smart_filter_authorization

`arguments_after_double_dash_are_files` and `macos_process_serial_number_is_still_ignored`
passed only valid arguments, so the app went on to open its window. Removing DISPLAY makes
that fail fast on Linux, but Windows runners have a desktop and both tests hung until
nextest's 600 s timeout. They now add a bad `--control` value so the launch always fails
with the usage errors, and assert stderr names only that error (no "unknown option").

Also `cargo fmt` crates/automation/tests/smart_filter_authorization.rs, left unformatted
on main by #5684, which failed the Format step of every PR's ubuntu test job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:57:27 -06:00
Zuujis
796ee3b103
Image Assets: two layers naming the same file no longer overwrite each other (#6614) (#6668)
* Image Assets: two layers naming the same file no longer overwrite each other (#6614) [WIP]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Image Assets: two layers naming the same file no longer overwrite each other (#6614)

generate_assets wrote every requested asset, so two layers (or two
default variants) naming one file silently replaced each other and the
result listed the file twice. Each file is now written once per run:
layers go top first, as the Layers panel lists them, the first writer
keeps the name, and later ones are reported in errors. Names are
compared ignoring case, since Logo.png and logo.png are one file on
Windows and macOS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:55:54 -06:00
David Mann
589941a536
Save: refuse to replace a read-only file (#6600) (#6643)
Saving writes a temporary file and renames it over the destination. A
rename only needs the folder to be writable, so on Linux and macOS a
read-only (chmod 444) file was silently replaced, while on Windows the
rename failed with a vague "locked or read-only" access error.

atomic_write now asks the AtomicFs whether the existing destination is
read-only (new `is_readonly`, false by default for other implementors)
and refuses before anything is written: "cannot save <file>: the file
is read-only; the original file was not changed". New files and
writable files save as before. Photoshop refuses the same way; the
user saves a copy with Save As.

Tests: a fake AtomicFs reporting read-only gets the error with nothing
written or renamed; a real file marked read-only keeps its bytes and
mode and gets the clear message on every platform.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 11:55:43 -06:00
David Mann
c9899e9c29
Duotone: ink curve points must be finite (#7049) (#7050)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:55:31 -06:00
Marco Arnulfo
e196d8e47c
Opening a file that is already open shows its tab instead of a second copy (fixes #6598) (#6630) 2026-10-17 11:53:21 -06:00
Zuujis
1dcaa28b96
Quick Export: Same Folder asks before replacing the document's own file (#6599) (#6632)
With Export Preferences › Location = Same Folder, Quick Export wrote
<name>.<format> beside the document's file with no check, so a document
opened from photo.png was exported over photo.png itself. The app now
opens the save dialog for that case, and file.export.quickExport refuses
it without a `path`. Overwriting an earlier export beside a PSD is kept.

Adds file_cmds::same_file (text, dev+inode on Unix, case-insensitive
canonical path on Windows).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:53:10 -06:00
Ivan
a4fe4ffbed
Contributors: add 1Git2Clone
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:37:13 +03:00
Ivan
45afd2e9d7
CI: run panic_hunt in the Linux test job
The full-registry fuzz (AGENTS.md Rule 9) is #[ignore]d and no workflow ran it, so nothing
enforced "must stay green". The new step reruns the Test step's nextest selection with
--run-ignored only, filtered to the panic_hunt binary, so nothing rebuilds; the ci profile
terminates it if it hangs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:37:12 +03:00
Hamza Naveed
efa1edd079
algo: gate the recorded-hash test's helpers with the test (#7053)
`blocks` and `fnv` exist only for `harris_and_describe_hashes_recorded_from_the
_serial_code`, which is `#[cfg(all(target_os = "linux", target_arch =
"x86_64"))]` because the hashes it compares against go through the platform's
hypot / ln / cos / atan2 / sin_cos. Everywhere else the test is compiled out and
the two helpers are left unused, so `-D warnings` fails on `dead_code`:

    error: function `blocks` is never used   crates/algo/src/features.rs:704
    error: function `fnv` is never used      crates/algo/src/features.rs:708

They now carry the same cfg as the only thing that calls them.

CI's matrix is ubuntu-latest and windows-latest and runs clippy on both, so this
fails the Windows leg as much as a local run on macOS: `target_os = "linux"` is
false for both.
2026-10-17 08:39:15 -06:00
Joseph
351149f0df
Fix the build: restore export_presets in load_presets_json (#5344 x #5616) (#6906)
#5344 added the export_presets field to Persisted; #5616 rewrote load_presets_json to read each set with take() and its Persisted initializer lacked the new field, so main stopped compiling (E0063 at presets/mod.rs:180). Read export_presets the same way as the other sets; the existing load step and round-trip test cover it.

Co-authored-by: 00xJ0 <56056764+00xJ0@users.noreply.github.com>
2026-10-17 08:32:54 -06:00
Nicolas Falesy
5f65579ecb
perf(panorama): Photomerge 4× faster, bit for bit the same (#6489)
Some checks are pending
Documentation / mdBook build (push) Waiting to run
Nix / eval (push) Waiting to run
Packaging lint / lint (push) Waiting to run
Packaging lint / Verified AppImage inputs (push) Waiting to run
N is the whole algo-side Photomerge pipeline on 3 × 8 MP photos (register at the engine's
scale with the Auto layout, gain + vignette fit, seams, multi-band blend), 16 threads:
1042 ms before, 264 ms after, 3.9×, rounded to 4×. The engine's own glue (reading regions,
building weights, writing layers) is not in this number.

Registration and multi-band blending ran almost entirely on one thread per image (four
images on 16 threads), so most of the machine sat idle. Every output is bit for bit what
the serial code produced: a probe that hashes the public Photomerge entry points
(detect_many, match_pairs, align for every layout, photometric, seam_labels, multiband at
1 to 7 levels with 1 and 3 channels, with holes in the alpha, with images sticking out of
the canvas, plus direct harris / describe calls) matches on all 349 outputs before and
after, on 4 × 2 MP, 3 × 8 MP, 97×61, 211×157, 333×251, 320×240 two-image and 40×35 sets,
at 16, 3 and 1 threads.

- features `blur` (Harris window and BRIEF smoothing): the row pass runs rows in
  parallel; the column pass cuts the image into bands of 64 columns, each holding the
  `&mut` row segments of its own columns (no unsafe), run in parallel. Every column keeps
  its own f32 running sum, started from its first window summed in order and updated in
  the same order, so only the loop nest changes.
- `harris`: Sobel, the corner response and the non-maximum scan are parallel by rows (the
  candidates are fully ordered by score, y, x afterwards, so arrival order is irrelevant).
  `describe`: one descriptor per corner in parallel, kept in corner order.
- `reduce`, `expand`, `push_pull`: per-row maps in parallel. `reduce`'s column pass adds
  five whole rows in tap order (same per-element sum, now vectorised); `expand` works out
  its source columns once and truncates instead of calling libm `floor` (the clamped
  position is never negative); `push_pull` builds each coarse row from its two fine rows in
  raster order, which is the order the serial scatter-add used.
- `multiband`: grid fill per image, Laplacian difference, band accumulation (every row
  walks the bands in the original order), normalisation, collapse and the coverage union
  are parallel. Float additions are never reordered.

Left serial on purpose: the peak fold in `harris` (about 1 ms, an order-free max but not
worth the proof), the corner spacing grid, `seam_labels` (a dynamic-programming path whose
rows depend on the previous one), `photometric` sample gathering, and the pyramid levels
themselves (each level needs the one above).

Median of 5 (8 MP rows: 7), release, ms, interleaved before/after runs. The laptop was
busy with unrelated builds the whole time (load average 11 to 18 on 16 threads), so the
1-thread rows are noisy (a single thread is time-sliced against them) and rows can move
by 10 to 20 percent.

| Case (probe)                   | before (16 thr) | after (16 thr) | speedup | before (1 thr) | after (1 thr) |
|--------------------------------|----------------:|---------------:|--------:|---------------:|--------------:|
| register, 4 × 2 MP             |             154 |             60 |    2.6× |            336 |           167 |
| multiband, 4 × 2 MP, 3 ch      |             407 |            108 |    3.8× |            591 |           407 |
| multiband, 4 × 2 MP, 1 ch      |             307 |             73 |    4.2× |            813 |           566 |
| whole pipeline, 4 × 2 MP       |             437 |            160 |    2.7× |            716 |           504 |
| register, 3 × 8 MP (full size) |             327 |            124 |    2.6× |            978 |           391 |
| multiband, 3 × 8 MP, 3 ch      |            2348 |            916 |    2.6× |           1742 |          1150 |
| whole pipeline, 3 × 8 MP       |            1042 |            264 |    3.9× |           2264 |          1280 |

The 8 MP multiband row at 16 threads is the noisiest (minimum of 7: 959 to 224 ms, 4.3×);
the whole-pipeline row is steady. Max RSS for the 3 × 8 MP whole pipeline (the probe's
inputs are about 1.05 GB of it): 2.79 GB before, 2.79 GB after (three runs each).

Tests: the serial `blur`, `reduce`, `expand`, `push_pull` and `multiband` are kept in the
test modules as references and compared bit for bit over sizes that cross every band and
row boundary (1×1, 1-wide, 1-high, odd sizes, 63 / 64 / 65 / 129 columns, radius 0 to
wider than the image, holes, partial and zero weights, ROIs outside the canvas, 1 to 7
levels). Hard-coded hashes recorded from the old code cover multiband, push_pull,
reduce + expand everywhere (plain f32 arithmetic), and harris + describe on Linux x86-64
(they go through the platform's hypot / ln / cos / atan2 / sin_cos).


Claude-Session: https://claude.ai/code/session_01AfRHB1wHUjrJx6agBYGnVu

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:20:22 -06:00
wwew
bd3b5d9192
fix(automation): refuse a group/world-readable control-token file (#4966) (#5459) 2026-10-16 20:20:03 -06:00
aiongg
7c44b2bc15 Build: use craft-docpath from craft-libs 2026-10-17 02:19:47 +00:00
Zuujis
fbcb70b317
Save: ask before overwriting a file another app changed (#6596) (#6652)
* Save: ask before overwriting a file another app changed (#6596) [WIP]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Save: ask before overwriting a file another app changed (#6596)

File › Save now notes the size and modification time of a document's
own file when it is opened or saved, and checks them again before
writing back. If another app changed the file in between, a prompt asks
whether to replace it (Cancel keeps both; Save As saves elsewhere)
instead of silently overwriting the other app's changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:19:32 -06:00
Korg1ch
4c881326ea
fix(windows): keep crash-recovery autosaves out of the roaming profile (#5648) (#6412)
* fix(windows): keep crash-recovery autosaves out of the roaming profile (#5648)

* test(windows): cover the local recovery folder and moving old autosaves (#5648)
2026-10-16 20:19:06 -06:00
MarekSpodBiedry
fbfc72ceb5
Close All, Close Others and Quit ask again about documents dirtied during the prompt (#4954) (#5079) 2026-10-16 20:18:47 -06:00
MarekSpodBiedry
20013ef9bd
Linked smart objects: don't refresh a document while a background job runs on it (#4973) (#5074) 2026-10-16 20:18:17 -06:00
David Mann
3d2914368d
PSD type: clamp warp bend and distortion before the f32 cast (#6856) (#6857)
A TySh warpValue, warpPerspective or warpPerspectiveOther beyond f32 became inf and the warp was silently dropped; they are now clamped to -100..100 like type.warpText.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 20:17:27 -06:00
Nicolas Falesy
5bd428a239
perf(stack): Stack Mode 11× faster (#6462)
Layer › Smart Objects › Stack Mode (`algo::stack::combine`) was one serial
per-pixel loop (1.0x parallelism, identical at 1 and 16 threads), and Entropy
built a HashMap per channel per pixel.

- Output pixels are computed in parallel in 4096-pixel chunks (`par_rows`, so
  wasm keeps its serial path). Each chunk owns its scratch buffers.
- Each pixel uses exactly the same arithmetic in the same order as before, so
  every mode except Entropy is bit-identical: 110 output hashes (10 modes x 11
  cases: 3 x 24 MP, 7 layers of odd/mismatched sizes, transparency, HDR values,
  1 layer, no layers) match the old code.
- Entropy: a `[u32; 256]` histogram plus a short list of the bins touched
  replaces the HashMap; touched bins are summed in ascending order, so the result
  is now deterministic. The old HashMap summed in random iteration order and
  differed run to run (up to 1.2e-7 between two runs of the old code). New vs old
  output: max abs difference 1.19e-7 (one f32 ulp near 1.0), on up to ~10% of
  values in the 7-layer cases; the 3-layer 24 MP case is identical.
- Modes that never use the mean (Median, Min, Max, Range, Summation, Entropy) no
  longer compute it; Median uses an unstable sort (same result under total_cmp).

3 x 24 MP, release, median of 5 interleaved before/after runs (load average
~3.5-4.5 from an unrelated build running alongside), ms:

| Mode       | before | after (16 thr) | speedup | after (1 thr) |
|------------|-------:|---------------:|--------:|--------------:|
| Median     |   1100 |             99 |   11.1x |          1064 |
| Mean       |    308 |             36 |    8.6x |           338 |
| StdDev     |    342 |             41 |    8.3x |           380 |
| Entropy    |   4727 |            269 |   17.5x |          2730 |
| Kurtosis   |    458 |             55 |    8.3x |           488 |
| Maximum    |    368 |             36 |   10.1x |           346 |

The other modes land between 8x and 10x. Max RSS for Median is unchanged
(1.50 GB, of which 1.13 GB is the input layers).

Tests: hard-coded bit patterns recorded from the old code for every mode but
Entropy; Entropy against hand-computed values and run-to-run determinism; and a
chunk-boundary test comparing a multi-chunk image to per-pixel results.


Claude-Session: https://claude.ai/code/session_01AfRHB1wHUjrJx6agBYGnVu

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:16:45 -06:00
F4LCON
75978a72f0
Targa: RLE packets end with their scanline (#6382) (#6386)
The image crate's Targa encoder let run-length packets run on from the end of
one row into the next. TGA 2.0 says a packet must not cross a scanline, and
readers that decode a row at a time misread such files: Pillow fails on them
with "buffer overrun", and texture tools and game engines reject them.

Targa is now written by our own encoder (codecs::tga), as Photoshop writes
it: RLE true colour or grayscale, every packet within its row, rows stored
bottom-up (the origin readers that ignore the descriptor assume), the alpha
bits in the descriptor and the TGA 2.0 footer. Reading stays with the image
crate.
2026-10-16 20:15:32 -06:00
nrq6993-a11y
c61cf9f0bd
fix(engine): load each persisted preset set independently (#5616)
One malformed set in the preferences file (gradients, swatches, styles...)
made load_presets_json ignore the whole presets blob, so saved New Document
presets vanished at every launch: the shell merges back only changed keys,
leaving the damaged set on disk. Each set now loads on its own; null and
malformed sets keep their defaults.

Refs storytold/photocraft#5602

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 19:20:06 -06:00
nrq6993-a11y
a1e19a0b87
fix(text): break paragraph lines at the horizontally scaled width (#5611)
Horizontal scale was applied after line breaking, so paragraph text below
100% wrapped early and text above 100% overflowed the box. Break box text
at the available width divided by the paragraph's horizontal scale (the
largest one when runs mix), scale the first-line indent and alignment
offset back to the box, and keep the scaled trailing space out of the
line extent.

Fixes storytold/photocraft#5565

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 19:19:59 -06:00
Matthew Santiago
d213273c74
GPU composite: lower-stack cache chunks have no texture view (#5423)
The prefix cache's chunk textures are only copied to and from
(COPY_SRC | COPY_DST), but went through Tex::new, which always creates
a default view. Vulkan rejects a view on an image with no sampled,
storage or attachment usage (VUID-VkImageViewCreateInfo-image-04441);
wgpu doesn't check it, so it only shows with the validation layers on,
once per new chunk while painting. The view was never used. Chunks now
hold a bare wgpu::Texture. No rendering change.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:19:53 -06:00
Ahmed Magdy
4e5c6a018a
fix(io): SVG text placed by a transform keeps it on import (#4806) (#5334)
usvg builds the glyph outlines of a text element (Text::flattened) with
identity absolute transforms, and the SVG importer read each path's
abs_transform(). Text positioned by its own or an ancestor's transform,
as Illustrator writes every text element (matrix(1 0 0 1 x y)), lost it:
it landed off the canvas, so nothing showed, or at its untransformed
place and size.

The converter now composes each node's transform from its parent chain,
which equals abs_transform() outside text, and sizes raster fallbacks
from the same transform. A new test checks translate, matrix, scale and
transformed groups against resvg's own render; it fails on main.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:19:47 -06:00
F4LCON
a05daee325
Filters: the live preview is never coarser than the screen (#6326) (#6351)
On a document over about 5 MP with Low Resolution Previews on, a filter
dialog's live preview ran on a reduced proxy at every zoom, so at 100 % the
canvas showed the image in k-pixel blocks and hid exactly what Unsharp Mask,
High Pass and Smart Blur do.

ensure_filter_preview now caps its factor with proxy::view_factor(k, zoom),
the same rule adjustment previews got in #5764, so from 100 % the preview is
full resolution and stays reduced when zoomed out. The preview already runs on
a cancellable background worker, so the full-resolution path does not block the
UI thread.

Also in this change:

- canvas.rs and filter_preview_ui.rs share one canvas::filter_preview_factor,
  so the in-dialog preview window cannot size textures from an uncapped factor
  and rebuild them when the first result arrives.
- The cap is gated on cfg!(target_arch = "wasm32"), so the web build keeps its
  proxy rather than running a full-resolution filter inline on the UI thread.
- The end-to-end test moved into mod tests and covers Low Resolution Previews
  off; a new factor test covers 25/50/75/100/400 %, NaN and zero zoom.
2026-10-16 19:14:50 -06:00
Tanish Manku
132257912a
README: put Community packages after the Get started status note (follow-up to #1149) (#6886)
The Community packages heading from #1149 landed before the maintainers' releasing note and the IMPORTANT status admonition, so those two paragraphs rendered under Community packages instead of Get started. Move the section to just above Documentation; the content is unchanged.
2026-10-16 17:52:24 -07:00
Ivan · VanTan
10fdcbbefc
chore: fix workspace Clippy collapsible_match warnings (#5190)
Strict workspace Clippy stopped on collapsible_match in affinity, raw, plugins, engine, ui-egui and xtask. Pure conditions become guarded match arms (each falling through to an existing empty arm), and the ruler drag state and the measurement command result are bound to locals, preserving evaluation order, error messages and fallbacks.

The same lint fixes also appear as a side commit in #5157 (David2766), which can drop it on rebase.
2026-10-16 17:42:36 -07:00
Gustavo Ziger
d900daa0ba
fix(web): File › Place Embedded is enabled in the browser and places the picked file's bytes (#1209, #5749) (#5585)
With a document open in the web app, Place Embedded was disabled because its menu enablement inherited the engine's native-filesystem check, although the menu path reads the picker's bytes and never touches a path. Place Embedded is now enabled whenever a document and a file-dialog service exist; Place Linked keeps the engine's native guard, since the web could only link to a bare file name (caught in #5817 by Higgs32584). An enablement test runs with the native check forced off, and a file-dialog test covers the browser answer shape, cancel, invalid bytes and the Free Transform handoff.

Fixes #1209. Fixes #5749.
2026-10-16 17:38:56 -07:00
Michael Higgins
3722dfbc63
fix(ui): Arabic and Hebrew layer names and the title read right to left (#5159) (#5385)
egui places font runs left to right with no bidi reordering, so a right-to-left layer name showed every word correctly but in reversed order. bidi_text::visual puts a label into display order with the Unicode Bidirectional Algorithm (unicode-bidi, already in the tree via usvg): right-to-left words stay in logical order for the shaper, everything else in a right-to-left run is reversed around them and brackets are mirrored. The Layers panel's rows and the app-drawn title bar use it; the title keeps a left-to-right paragraph so '— PhotoCraft' stays at the right, and a long right-to-left name is cut at its logical end so its start survives. Display only: stored names and the OS title keep the logical text.

Fixes #5159.

Co-authored-by: ahmedhosnypro <ahhosnyas@gmail.com>
2026-10-16 17:36:06 -07:00
Gert Pellin - GPE
5a33441af3
fix(engine): an unknown layer id or name lists the document's layers, and a name matches regardless of case as a fallback (#5058) (#5133)
A command that fails on a layer id or name the document doesn't have now says which layers there are, top-down as id and name (at most 20, names cut at 64 characters), so an agent can recover from the error alone: ids are not positions, and a freshly opened PNG's Background is layer 2. NoLayer becomes UnknownLayer { id, known } at both command entry points, keeping the 'no such layer' prefix. A layer name that matches no layer exactly matches regardless of case; an exact match always wins.

Fixes #5058.
2026-10-16 17:34:40 -07:00
Gert Pellin - GPE
a6a1d2ed22
feat(algo): face detection with libfacedetection's CNN in pure Rust, and image.detectFaces (part of #3603) (#4513)
photocraft_algo::faces::detect finds faces in an RGBA image with a score, a box and five landmarks, through a pure-Rust port of libfacedetection's YuNet network (Shiqi Yu, BSD-3-Clause; licence, NOTICE and ATTRIBUTION rows included). The weights are the library's, converted to half precision (107 KB), parsed defensively. image.detectFaces reports the document's faces to scripts and agents; it is read-only with no history step and no menu entry. The network is compiled out of the browser build, where the command reports itself unavailable. Head outputs are pinned against the C++ reference.

Part of #3603.
2026-10-16 17:31:02 -07:00
Gert Pellin - GPE
1b3bc48267
docs(readme): Community packages section with the Gentoo ::snakebyte overlay (#1149)
Adds a Community packages heading to the README, noting these installers are maintained by community members, with the Gentoo recipe for media-gfx/photocraft-bin from the ::snakebyte overlay. The overlay's ebuild fetches the official Linux x86_64 release tarball with Manifest-pinned hashes that match the release's SHA256SUMS.
2026-10-16 17:28:51 -07:00
Harrison O'Connor-Hoover
8a4ed48354
fix(ui): numeric fields drop oversized text, paste and IME events before layout (part of #2150) (#2258)
A single oversized input event reached egui text layout in a numeric field even though it could never become a valid value. number_edit now drops Text, Paste and IME events longer than 128 scalar values while the field has focus, and the custom parser rejects accumulated text over the cap before normalizing. DragValue keeps focus, accessibility, drag and arithmetic behaviour. Six harness tests cover the event kinds, the Unicode boundary, accumulation, undo, and document text fields staying unrestricted.

Part of #2150.
2026-10-16 17:25:50 -07:00
wwew
dd69f0797d
fix(engine): reserve the define-brush tip and selection-mask buffers fallibly (#212) (#5469)
brush.defineFromSelection allocated the sampled tip and the selection mask infallibly over the whole selection, so a large selection could abort the process. Both now go through the engine's fallible allocator and return a normal error that leaves the brush, presets, history and document untouched. A failpoint test asserts all four.

Part of #212.
2026-10-16 17:23:08 -07:00
safvanedge
aecac608b4
fix(ui): Type tool caret stays solid after typing, navigation or a click, then resumes blinking (#5511) (#5512)
The caret's blink phase was tied to the wall clock, so an arrow key or keystroke that landed in the off half-cycle moved an invisible caret. A transient caret_blink_time on UiState is reset on every handled key event, on a click that places the caret, and while dragging; draw_overlay computes the phase from it so the caret is solid for 530 ms after each interaction, and the repaint is scheduled to the next phase boundary.

Fixes #5511.
2026-10-16 17:20:53 -07:00
Vikram Bhaskaran
c7d2c85faf
fix(ui): Color Picker's Only Web Colors snaps every edit and is remembered across pickers (#4995) (#5442)
With Only Web Colors on, every picker edit (field, sliders, hex, Lab/CMYK, screen sampler, Current) snaps through the clamped 216-colour ramp. The checkbox is remembered on the Color panel state so the foreground, field, command, Layer Style and type-style pickers open with the same setting. Opening with it on snaps the colour and its original together, so OK without an edit leaves a fill layer unchanged. The hex field follows the colour when another path changes it while focused.

Fixes #4995.
2026-10-16 17:19:03 -07:00
Nigel Ihza Firdausy
9628290b36
fix(ui): Layer › New Fill Layer › Solid Color opens the Color Picker, creating the layer on OK (#5144) (#5166)
Creating a Solid Color fill from the Layer menu or the Layers panel footer now opens the shared Color Picker seeded with the foreground colour. The layer is created on OK in one history step; Cancel leaves the document and history untouched. Parameterised calls (CLI, MCP, actions) still create the layer directly.

Fixes #5144.
2026-10-16 17:13:51 -07:00
Marco Arnulfo
db46713c2c
Unsaved-changes prompt: the default answer takes keyboard focus, so arrows and Enter work (part of #6441) (#6466) 2026-10-16 17:03:15 -06:00
Jason Xiang
06ad07ab9b
fix(io): color-manage grayscale WebP exports (#5875) 2026-10-16 17:02:54 -06:00
Jason Xiang
f61ed26e14
fix(io): honor version-aware OpenRaster group isolation (#5872) 2026-10-16 17:02:23 -06:00
Jason Xiang
840edc311c
fix(io): preserve linear grayscale profiles for selected EXR parts (#5867) 2026-10-16 17:01:52 -06:00
Jason Xiang
462adeadd2
test(io): cover mixed-region CMYK TIFF composites (#5588) 2026-10-16 17:01:10 -06:00