Commit Graph
187 Commits
Author SHA1 Message Date
callmeBallochandClaude Opus 5.5 a8d7c6ef3f AF.3: put the Affinity fuzz targets and corpus back in CI
The maintainers approved the new crate, its fuzz targets and the corpus
upstreams in principle (storytold/photocraft#1607), so this undoes the
interim opt-in: ci.yml fuzzes preview and import_preview again, corpus
--all fetches the Affinity set, and its test runs under the normal
corpus feature. The provenance section stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
2026-10-09 06:38:40 +00:00
callmeBallochandClaude Opus 5.5 edb42922c1 AF.3: keep Affinity out of CI's corpus and fuzz jobs; state the clean-room provenance
Review on storytold/photocraft#1607 asked maintainers to decide on new
fuzz targets in ci.yml and four third-party corpus upstreams in
corpus --all. Until they do, neither touches CI: ci.yml is main's again
(the targets stay in crates/affinity/fuzz, run locally), and the
Affinity corpus is fetched only by corpus --affinity and tested with
--features corpus-affinity, so --all and test-corpus are unchanged.
The plan is to move those files into photocraft-corpus.

The crate README now spells out the provenance: no Affinity software,
afread's MIT description re-described without translating code, every
structure confirmed on public files (CRC-32 per entry, self-typed
fields), semantics fitted to Affinity's own thumbnails, and how the
sample documents were licensed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
2026-10-09 05:19:56 +00:00
callmeBallochandClaude Opus 5.5 57ae2fb1a3 Merge storytold/main into AF.3 native Affinity import
Conflicts: File › Open and batch extension lists (SVG, swatches and
Affinity together); the import dispatcher (upstream's type-layer
preparation with the new result fields); the pinned EXR and Affinity
corpora side by side; README, book and development docs.

Affinity type layers now draw through text_import::prepare, so they get
the same raster bound as other imported type, and SVG import fills the
two new ImportResult fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
2026-10-09 04:26:18 +00:00
harrisonoconnorhover 0e0311d245 Reject unsupported document indices in MCP screenshots 2026-10-09 11:12:46 +07:00
MarekSpodBiedry fb71daf2c6 Control channel: ui.click rejects a count above 256 instead of queueing unbounded input (#982) (#1446) 2026-10-09 11:12:34 +07:00
MarekSpodBiedry 746f2530e6 Plug-in names reject bidi controls, zero-width spaces and other invisible format characters, and need a visible character (#1011) (#1444) 2026-10-09 11:12:27 +07:00
Harrison O'Connor-Hoover 672c0775de Reject control requests that expire before dispatch (#1525) 2026-10-09 11:11:47 +07:00
Harrison O'Connor-Hoover de9cc5f12c Do not replay bridge requests after an uncertain result (#1527) 2026-10-09 11:11:44 +07:00
BFlatandClaude b693d690e1 docs: MCP tool names and job tools, codec README JPEG metadata and deep EXR (#1552, #1553, #1559) (#1573)
- architecture.md listed dotted MCP tool names (doc.open, command.run...) that the server
  rejects; it now uses the advertised underscore names, adds the missing tools, and points
  out that dotted names belong to the JSON control protocol.
- development.md's MCP tool inventory gains jobs_list and jobs_cancel.
- crates/codecs/README.md: oversized JPEG EXIF/XMP is dropped with a warning (XMP first sheds
  its layered-document properties), not an error; supported deep EXR opens flattened with a
  DeepFlattened warning, and only deep ZIP and mip/rip-map tiles are unsupported.


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

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 10:23:19 +07:00
Xus BadiaandClaude Opus 5.5 bfc59c784f macOS DMG: branded window background and icon layout (#1201)
The DMG opens on a 660x400 Finder window with the PhotoCraft artwork, 128 pt icons and the
Applications link beside the app. packaging/macos/dmg/ holds the SVG source, a reproducible
generator, an 87 KB HiDPI palette TIFF (was 1.0 MB) and the .DS_Store layout. The volume is
named "PhotoCraft" (the background alias resolves by volume name); the DMG file name keeps
the version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:11:00 -04:00
Filip PłachecińskiandClaude Opus 5.5 f0ae12dd22 Self-host the web app with Docker (nginx, community-maintained) (#1334)
A multi-stage Dockerfile builds photocraft-web with Trunk and serves dist/web from an
unprivileged nginx on :8080, with /healthz, precompressed gzip, the wasm MIME type and
immutable hashed assets. Rust and Trunk follow the release workflow, and the
PHOTOCRAFT_BUILD_SHA/DATE build args stamp About. Documented in packaging/web/README.md;
not built in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:10:55 -04:00
optimalbyteeandoptimalbytee c7dbb37b3c Speed up wide Motion Blur with adaptive FFT convolution (#1471)
Keep the merged upstream row kernel for shorter streaks and unsupported inputs.
Use bounded overlap-save convolution of its merged bilinear coefficients when
channel pairs, parallelism and memory estimates favor FFT. Preserve alpha cutoff
behavior, cancellation, selections and depth/model-neutral filtering.

Validate 303 algorithm tests in debug and release, plus native checks and full
image comparisons. Repeated current-upstream comparisons show 2.31x at 24 MP
and 4.15x at 1.5 MP for angle 30 / distance 1000 on the measured host.

Co-authored-by: optimalbytee <optimalbytee@users.noreply.github.com>
2026-10-09 09:02:00 +07:00
Brandon ThomasandClaude Opus 5.5 0b5803500c Regenerate docs/scorecard.md and docs/parity.md after the latest community merges (#1530)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:24:37 -04:00
619095bfd0 Pattern Stamp tool in the S-group flyout (#922)
* Add Pattern Stamp as a Clone Stamp flyout sibling

Introduce paint.patternStamp to paint the current pattern through a
brush stroke. Aligned strokes keep a document-space tile origin;
unaligned strokes restart at the first point; Impressionist jitters
each dab with a seeded RNG. Empty patterns and strokes larger than
4096² pixels return an error instead of allocating.

* Record Pattern Stamp on the tools scorecard

Allow the aligned stamp helper's argument count under clippy.

* Pattern Stamp: raise the stroke cap to 8192² so a 24 MP diagonal fits

The 4096² bounding-box cap refused a corner-to-corner stroke on a 6016x4000 canvas (~24M px).
Unit test covers that diagonal and a still-rejected 9000² box; the hostile-input test uses a
footprint above the new cap. Scorecard note labels Impressionist as an approximation.

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

* i18n(pl): translate the Pattern Stamp strings

The Polish catalog landed on main after this PR was written; the
translation-coverage tests need every language to carry new UI strings.

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

* Tool::ALL holds 51 tools now that Red Eye and Pattern Stamp both landed

The rebase onto main (with #1084's Red Eye tool) leaves Tool::ALL one entry
longer; bump the array length and the count test.

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

* Flyout test: qualify egui::Vec2 (panels.rs no longer imports Vec2)

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:07:06 -04:00
Brandon ThomasandClaude Opus 5.5 ed6f989bc6 Regenerate docs/scorecard.md and docs/parity.md after the latest community merges (#1518)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:47:09 -04:00
5bef14dbdc Swatches: user swatch model with .aco/.ase import and export (FILE-215-14, part of #215) (#886)
- photocraft-psd: clean-room .aco (v1/v2, UTF-16 names, RGB/HSB/CMYK/Lab/Gray at 16 bits,
  other spaces kept verbatim) and .ase (groups, global/spot/process, RGB/CMYK/LAB/Gray floats,
  unknown models kept) readers and writers; byte-stable round trips, hostile input -> Err.
- engine: presets::swatches (colours in their own model, display/foreground conversion through
  photocraft-cms working spaces, persisted with the prefs, default 40 colours in 4 groups) and
  swatch_cmds (swatches.list/add/delete/rename/move/newGroup/renameGroup/deleteGroup/use/reset/
  import/export) with graceful-failure tests.
- ui: swatches_ui.rs replaces the const grid (groups, tooltips, click = foreground and selected
  type, Alt/Ctrl-click = background, rename/delete/move, panel menu Import/Replace/Export/Reset),
  Color Picker "Add to Swatches", .aco/.ase via File > Open and drag-and-drop; strings translated
  in every complete catalog.
- Scorecard FILE-215-14 -> done.

Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 20:43:01 -04:00
2f42b1cc80 Camera Raw opens in a window of its own (#1361)
* Camera Raw opens in a window of its own

The Camera Raw dialog (Filter › Camera Raw Filter and the open-time
dialog) was a full-window overlay drawn over the PhotoCraft window,
covering its title bar: it looked like a fullscreen app without window
controls. Adobe Camera Raw is a separate window.

Show it in an egui immediate viewport: an OS window titled "Camera Raw
(name)" / "Camera Raw Filter (layer)", 1360 x 900 by default, resizable
(minimum 900 x 600). The dialog stays modal: the main window is dimmed
and takes no input while it is open. Closing the window is Cancel (for
the open-time dialog that closes the raw being opened). Where the
platform has no extra windows (ctx.embed_viewports(), the web build) it
is drawn over the main window as before, with its painted title strip.

The drawing code is unchanged apart from the title strip, which is not
painted in a window of its own.

* Camera Raw window: scrim theme token, close-is-Cancel test, document the screenshot gap

- theme::Tokens gains `scrim` (the dim over the main window behind a modal
  that takes all input); Camera Raw uses it instead of a hard-coded colour.
- closing_the_camera_raw_window_cancels: the overlay fallback keeps the
  dialog open, a close request on its window cancels without history.
- control-protocol.md: eframe 0.36 cannot screenshot an immediate viewport,
  so ui.screenshot shows the dimmed main window while Camera Raw has its own
  window; agents read it via ui.inspect.cameraRaw and see it offscreen with
  the snapshot example or kittest (which embed viewports).

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 20:39:15 -04:00
b0680fd421 Advanced Blending: knockout, blend as group, transparency shapes, masks hide effects (FILE-215-16, part of #215) (#903)
* Advanced Blending: knockout, blend as group, transparency shapes, masks hide effects (part of #215)

Blending Options > Advanced Blending now works end to end:

- doc: AdvancedBlending on every layer (knockout none/shallow/deep, blend
  interior effects as group, blend clipped layers as group, transparency
  shapes layer, layer/vector mask hides effects), saved in .pcraft.
- PSD and layered TIFF: knko, infx, clbl, tsly, lmgm, vmgm read into the
  model and written back (in place, or added when not at the defaults);
  the TIFF byte-order transcoder handles lmgm/vmgm as byte flags.
- CPU compositor (compose/src/advanced.rs): shallow and deep knockouts
  (isolated groups and clipping groups stop a deep knockout), clipped
  layers blended individually, effects over the whole layer when
  Transparency Shapes is off, effects built from an unmasked copy when a
  mask hides them, interior effects under fill opacity.
- GPU: documents using Advanced Blending fall back to the CPU compositor.
- engine: layer.layerStyle.blendingOptions takes channels, knockout and the
  five switches; inspect reports them; Copy/Paste Layer Style carries them.
- UI: the Blending Options page laid out like Photoshop's (General,
  Advanced Blending, Blend If with split sliders), translated.

psd-tools corpus oracle 229 -> 236 (seven knockout files now pass; floor
raised); other corpora and all round trips unchanged. Scorecard
FILE-215-16 -> partial.

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

* Polish catalog rows for Advanced Blending; regenerate docs/scorecard.md

pl.tsv landed on main and the coverage test needs every catalog to translate new UI strings.

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

* Undo damage: compare Layer.advanced in layer_pixels_equal; regenerate scorecard

#810's exhaustive Layer destructure must classify the new advanced-blending
field; it changes the composite, so it is compared.

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

* Regenerate docs/scorecard.md

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

---------

Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 20:08:03 -04:00
4cb7cf3931 Layered TIFF: the review changes of #672 (keep unknown blocks with their byte order, io behind the engine, fixture attribution) (#835)
* Review: keep unknown blocks with their byte order, io behind the engine, fixtures documented

Addresses the review on #672:

* Unknown layer blocks read from a little-endian TIFF are no longer dropped. Their bytes are
  kept verbatim with the signature `MIB8` (`46B8` for `8B64`) marking the little-endian
  layout (`TaggedBlock::is_foreign_order`), so a save back to TIFF restores them exactly. A
  malformed known block is kept the same way. Only a save to the other byte order has to drop
  such a block, with a warning: a PSD export strips them (`strip_foreign_order_blocks`), and a
  big-endian block of unknown layout still cannot go into a little-endian TIFF.
* The UI no longer depends on `photocraft-io`: the layer predicate is exposed by the engine
  (`file_cmds::tiff_would_write_layers`); io is a dev-dependency again.
* The layered TIFF path ends in the same `encode_image` as every flat export, so export-wide
  metadata policies (such as #733's XMP choice) apply to its composite too.
* Scorecard FILE-215-7 is `partial`: extra alpha channels and a Quick Mask are not written.
* The committed fixtures get an ATTRIBUTION.md row and a README describing their contents and
  how they were generated (psdtags + tifffile); the Python generator script is removed.

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* tiff_corpus: ask for layers explicitly (headless default is flat since #825)

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* i18n: drop the duplicate Korean "Ambient" entry that fails bundled_catalogs_are_consistent

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* i18n: drop the duplicate Korean "Warmth" entry as well

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* agent_tasks: tolerate Windows holding the test directory open during teardown

The three tests that remove their directory right after cancelling the client failed on Windows (CI and locally) with os error 32: the spawned server task is still closing its files. The cleanup now retries briefly and otherwise leaves the directory for the next run's tmp() to clear, as it already did at the start.

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* preset store: a missing directory is an empty store on Windows too

DirBackend::read passed the OS message through, and Windows words a missing directory as "cannot find the path specified (os error 3)", which missing_file() did not recognise, so attaching a store on a path that does not exist yet (first launch, and two engine unit tests) reported actions.json as skipped. The backend now reports every NotFound as "<name>: not found", the wording the predicate and the memory backend already use. Regression test: opening a store on a nested missing directory warns about nothing and writes nothing.

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* Review: keep scripts/layered_tiff_fixtures.py, drop the fixtures README and attribution rows

The four .tif fixtures those described were never committed (binary test data stays out of the repo; the tiff_layers tests read them from the gitignored corpus/layered-tiff/, written by the script that tiff_layers.rs:181 still points to).

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* automation: the Direct Selection path commands edit vector paths, not files

#865 added path.moveAnchors, path.moveHandle, path.bendSegment and path.convertPoint, whose registry docs mention the vector path they return, so registry_filesystem_path_params_are_classified fails on main (Linux and Windows): they join the list of commands whose "path" is a document path.

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* i18n(cs): the lower-case "{n} layer|{n} layers" plural (main red since #829)

xtask's source_key_set_is_covered_by_complete_catalogs fails on main: the key reached every catalog but the Czech one. Same forms as the capitalised twin already in cs.tsv.

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* Drop the duplicate path.* entries from the workspace test (main already lists them)

main already has path.moveAnchors/moveHandle/bendSegment/convertPoint in this match; the
duplicates trip unreachable_patterns.

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

---------

Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 19:57:19 -04:00
Brandon ThomasandClaude Opus 5.5 9d3447b153 Regenerate docs/scorecard.md and docs/parity.md after the latest community merges (#1489)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:34:20 -04:00
Remidesbois 46fb94f813 Type Tool: Ctrl/Cmd temporary transform handles while editing (#1091) (#1202)
* type: transform editable text with temporary Ctrl/Cmd handles

* type: cover temporary transforms, editable files and preview costs

* docs: describe temporary Type transforms and automation

* type: release temporary previews immediately when gestures end

* test: benchmark the active 24 MP Type document
2026-10-08 19:10:13 -04:00
UnemarsandClaude Opus 5.5 3439d327f7 Preferences › Performance › Low Resolution Previews (on by default) (#1247)
Live previews of large documents (filter dialogs, adjustment dialogs that fall back to the
filter preview, zoomed-out adjustment dialogs, an adjustment layer's sliders while they drag)
render on a nearest-neighbour reduced copy: a 16 MP photo previews at every 3rd pixel, drawn
3x blocky at 100 %. Turning the setting off renders them at full resolution.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:10:02 -04:00
d57fdf0116 Zoom: the Zoom tool's click honours Zoom Clicked Point to Center (#204) (#1241)
* Zoom: the Zoom tool's click honours Zoom Clicked Point to Center (#204)

Preferences > Tools > Zoom Clicked Point to Center now moves the clicked point
to the centre of the view instead of keeping it under the pointer; scroll-wheel
zoom keeps the point under the pointer either way. The settings audit drops from
57 to 56 unread fields; docs/scorecard.md regenerated.

* Regenerate docs/scorecard.md after rebasing onto main

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:51:35 -04:00
5a97ee30f3 Brush tests: seams, hard edge, zero scatter and ABR tip colour (#213) (#1221)
* Brush tests: seams, hard edge, zero scatter and ABR tip colour (#213)

Adds the four TOOL-213-13 regressions: minimum coverage inside a stroke
(spacing 0.1/0.25/0.5), a hardness-1 rim measured against a soft falloff,
dabs staying on the path at zero scatter, and an imported ABR tip painting
the brush colour instead of black. Scorecard row flipped to done.

* Contributors: add Reynold Hu to people.toml

Real name credited, display name synced from the public GitHub profile at the
next contributors.py refresh (every field is opt-in).

* Regenerate docs/scorecard.md after rebase onto main

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:28:12 -04:00
BFlatandClaude Opus 5.5 da86700693 Right-click Brush Preset picker picks presets like Photoshop (#615, #1031) (#1080)
* Right-click Brush Preset picker: pick presets like Photoshop (#615, #1031)

One picker for the canvas right-click and the options-bar brush chip, drawn
once a frame and kept in UiState (brush_picker, brush_picker_list). Its
preset list is the Brushes panel's (brushes_tab::preset_list): a click picks
and keeps it open, a double-click picks and closes it, drag reorders and
right-click renames or deletes. A gear menu adds New Brush Preset… (named in
the rename bar), Rename/Delete Brush, list or grid view and Import Brushes….
Hardness shows for round tips only; Enter applies a typed size before it
closes the picker. ui.set gains brushPicker and brushPickerView.

Also fixes the Brushes panel's Rename Brush…/Rename Group… context items,
which never opened the rename bar.

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

* Brush Preset picker: Polish translations

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 18:28:06 -04:00
BFlatandClaude Opus 5.5 652b9726be Pen tablets: Xwayland for a pen on Wayland, macOS monitor after NSApp (#639, #759) (#1070)
Wayland compositors give a pen only to clients that bind zwp_tablet_v2,
which winit 0.30 doesn't, so on Wayland the pen did nothing in the
window. With a pen attached (BTN_TOOL_PEN in /proc/bus/input/devices),
$DISPLAY set and the X11 libraries installed, the window now opens
through Xwayland, where the X11 reader gets pressure, tilt and the
eraser. PHOTOCRAFT_NATIVE_WAYLAND=1 keeps native Wayland.

macOS: the AppKit event monitor was installed before winit created the
shared NSApplication; it now goes in from eframe's app creation. The
pen's button-up (pressure 0) no longer zeroes the pressure of the
stroke's last moves, which reach the UI in the same frame.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 05:08:44 +07:00
Brandon ThomasandClaude Opus 5.5 fa09fcb218 Regenerate docs/scorecard.md (139 preferences after #1016) (#1435)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:28:28 -04:00
17d72cf771 Fix Classic Gradient transparency and open the Gradient Editor (#905)
* Fix Classic Gradient transparency and open its editor

* Translate the minimal Gradient Editor

* Document gradient preset and editor controls

* Group gradient translations to avoid append conflicts

* Split the minimal Gradient Editor out; keep the Classic Gradient transparency fix

The floating editor (preset list + opacity-stop number rows) is not Photoshop's Gradient
Editor, so it goes to a follow-up. What stays: classic drags no longer override the preset
with opaque FG/BG colours, the options-bar swatch shows the selected preset in both modes,
labelled Gradients-panel items, and the classic transparency regression tests. Drops the
ui.set/inspect gradientEditor field and the editor's translations.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:05:51 -04:00
b885173da1 Embed PhotoCraft CLI Windows icon and version metadata (#889)
* Embed PhotoCraft CLI Windows icon and version metadata

The CLI crate now sets build to build.rs and takes winresource 0.1 as a
build dependency. On a Windows target the script embeds photocraft.ico
and sets ProductName to PhotoCraft, FileDescription to PhotoCraft
command-line interface, CompanyName to Learning Machines LLC, the shared
copyright line, OriginalFilename to photocraft-cli.exe, and InternalName
to photocraft-cli. Version strings keep the Cargo package version,
including a pre-release tag, and the numeric file and product versions
are major.minor.patch.0. A missing resource compiler prints a cargo
warning and the link continues, while PHOTOCRAFT_REQUIRE_WINRES makes
the script return an error that names rc.exe or windres. package.ps1
runs check-cli-resources.ps1 after the PE subsystem checks and before
signing, including SkipBuild artifacts, and that script checks those
VERSIONINFO fields against the package version and compares the icon
Windows extracts from the CLI with photocraft.ico drawn at 32x32,
without launching the binary. docs/releasing.md records the same CLI
resource contract beside the existing GUI one.

Refs #613

* Drop the untested check-cli-resources.ps1 release gate

It has never run on Windows, and a misfiring 200-line gate in package.ps1 would block
releases. The CLI build.rs icon/VERSIONINFO stays; PHOTOCRAFT_REQUIRE_WINRES=1 in
package.ps1 already makes a missing resource compiler a build error.

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

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:05:38 -04:00
75b6931e71 Open camera raws in Camera Raw first (Open / Cancel) (#1309)
* Open camera raws in Camera Raw first (Open / Cancel)

Photoshop opens a raw file in Adobe Camera Raw before it becomes a
document; PhotoCraft developed it with the defaults straight away and
said so in a warning. An interactive open (File › Open, Open Recent,
drag and drop, the command line) of a raw developed from its sensor data
now shows the Camera Raw dialog first, titled "Camera Raw (name)", with
Open and Cancel.

- Open re-develops the raw when Temperature, Tint or Exposure changed:
  as-shot white-balance multipliers times the filter's gains, and the
  develop exposure (photocraft_io::raw::import_raw_tuned). The other
  settings are one filter.cameraRaw step; the document starts
  unmodified. Without an as-shot white balance or without the file's
  bytes, Temperature / Tint stay RGB adjustments as in the filter.
- Cancel closes the document again.
- The "developed with default settings" note is no longer shown when
  the dialog is.
- Preference rawDefaults.openInCameraRaw (default on), the first
  visible Camera Raw Defaults setting. Automation opens (app.open, MCP,
  CLI) never show the dialog.
- "NIKON CORPORATION" + "NIKON D3200" now reads "NIKON D3200" in the
  import note.

Tests: open → set → Open re-develops (brighter, still 16-bit, one
document, unmodified); Cancel closes; unchanged settings add no step;
preference off skips the dialog; a closed document is an error.
Translations for the two new strings in every catalog.

* Camera Raw open: re-develop in the background, keep tab and view

Open re-read and re-developed the whole raw on the UI thread. In the desktop
app it now runs as a job (cameraRaw.redevelop, document locked) like the
open itself; the reply is {job, pending} and the shell finishes when it
lands. The final Camera Raw step goes through app.run (a job too) and the
document is marked unmodified once it lands. Tests and the web still run
inline.

The re-developed document replaces the old one at the same tab position
and path, and its view (zoom, scroll) carries over. New test covers the
background path with documents on both sides.

wb_gains now uses camera_raw::white_balance_gains, shared with the Camera
Raw filter and documented as an observed approximation. 'Developing {name}'
translated in every catalog.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:04:51 -04:00
88568297ab Show Clone Stamp source preview inside the brush cursor (#1042)
* Show Clone Stamp source preview inside the brush cursor

* Drop the Clone Stamp preview screenshots and doc page

Binary screenshots and a standalone doc page aren't wanted in-tree (review):
remove docs/clone-stamp-preview.md, the two PNGs, their licence file and the
ATTRIBUTION.md row.

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

* Clone preview: pass the command name to clone_sample and propagate its far-source error

clone_sample gained a cmd parameter and returns Result since #1165.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:04:27 -04:00
f41eb4f70d SVG opens as shape layers and places as a vector smart object (FILE-215-9, part of #215) (#1305)
* SVG opens as shape layers and places as a vector smart object (FILE-215-9, part of #215)

crates/io/src/svg.rs: usvg parses (already in the tree through egui_extras); paths become
ShapeLayer knots with the transform chain baked in, quadratics raised to cubics and every
subpath after the first joining the component so the fill rule spans the path as SVG does;
groups keep opacity and blend mode (transform-only and id-only wrapper groups vanish);
text is shaped into outline shapes with the craft fonts plus the system's; gradients become
the layer's gradient fill (stops and angle, with a warning). Clip paths, masks, filters,
pattern paints and images are rasterised by resvg into pixel layers over their bounding box
rather than dropped. Limits: 16384 px canvas, 2000 elements, 24 M px per raster, bounded
recursion; malformed input is IoError::Svg.

smart_cmds::render rasterises an SVG source at the placement's axis scale (1/16 steps,
cached per scale) and divides the placement by it, affine and projective alike; place_bytes
renders that cache up front. svg/svgz join the openable lists; book page; scorecard row done.

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

* SVG import: never read local files from <image href>

usvg's default image resolver reads any absolute or relative path in an
<image href>, so opening an untrusted SVG could embed local files
(/etc/..., ~/.ssh/...) into the document. Parse with a resolver that
only accepts data: URLs (resources_dir None, string hrefs ignored).
Embedded sub-SVGs already get no <image> resolution from usvg. The font
database (and its system-font scan) is now only loaded when the drawing
may contain text. Regression test: a PNG on disk referenced by absolute
path, file:// URL and relative path is not loaded; a data: URL still is.

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

---------

Co-authored-by: leon <claude.udder842@passmail.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-08 17:04:21 -04:00
TiredIsa 6197270230 Preferences: reset a single shortcut or menu colour override (#988) (#1064) 2026-10-08 16:39:16 -04:00
f6c34f63cb Add Polish translation (#1192)
* Add Polish translation

* Polish: clippy-clean plural rule, plural test, ATTRIBUTION and docs rows

plural_polish used manual range checks (clippy::manual_range_contains) in a
match (); now a plain if/else with ranges. Adds polish_plural_rule, the
pl.tsv ATTRIBUTION row like the other catalogs, and the localization doc row.

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

* pl.tsv: drop three duplicate keys (Wave, High, Low)

bundled_catalogs_are_consistent and i18n-coverage reject duplicate
(context, source) pairs; the first translation of each is kept.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:38:28 -04:00
BFlatandClaude Opus 5.5 83936bdcee Fix #1271, fix #1316: Preferences › Interface › System Title Bar (#1344)
* Fix #1271, fix #1316: Preferences › Interface › System Title Bar

Since #986, Windows and Linux draw PhotoCraft's own one-row title bar
with its own window buttons. On tiling window managers those buttons do
nothing, and some Linux desktops look better with their own decorations.
The new System Title Bar preference (off by default) gives the window the
system's title bar and buttons back; it is read when the app starts.
macOS always uses the system's. Translated in every shipped language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

* Scorecard: count the System Title Bar preference

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-09 03:36:21 +07:00
Gert Pellin - GPE 3d09578322 raw: decode Nikon compressed NEF (lossless + lossy) (#1310)
* raw: decode Nikon compressed NEF (lossless + lossy)

Nikon's default NEF (TIFF compression 34713) opened only as the embedded
8-bit JPEG preview. Decode the Huffman-coded strip: lossless and lossy
type 1 / 2, 12 and 14 bit, with the seeds and curve of maker note 0x0096
in the maker note's byte order. "Lossy after split" files stay
unsupported and keep the preview fallback.

Clean-room: format facts from prose write-ups (Clévy, Claff, ExifTool tag
names, T.81). The fixed Huffman tables are not in the files; they come
from LightCraft's black-box analysis of CC0 raw.pixls.us samples
(storytold/lightcraft#86, verified there on ~45 files from ~35 bodies).
No decoder code is shared: nefc.rs is written against this crate's TIFF
reader.

Also read Nikon BlackLevel (0x003d) in 14-bit units for 12-bit data, as
measured for LightCraft (D750 / D850 / Z 50).

Refs #50.

* raw: Nikon NEF without a BlackLevel tag is black-subtracted in camera

A D3200 NEF's darkest samples are 0-4 (noise around zero): assume 0
without the "black level not recorded" warning when the Nikon maker
note is present but has no 0x003d tag.

* raw: trim Nikon's right-edge padding columns

A D3200 NEF pads every row with 46 columns of 256 (exact in 97.5% of the
rows, lossy-coding noise elsewhere) after its 6034 image columns; they
developed as a pink stripe on the right. Trailing columns (at most 128)
that hold the last column's value in at least 90% of their rows, when
that value is dark, are left out of the active area and crop. A white-
clipped edge is never trimmed.
2026-10-08 16:18:58 -04:00
DWAA1660andClaude Opus 5.5 347b6c9009 UI Scale Auto keeps the system's fractional scale on 4K (#1072) (#1237)
Auto forced at least 200% on any 4K monitor, overriding a fractional
system scale such as 125% or 150%, so Linux users on 4K got either
100% or 200% and nothing between. Auto now follows whatever scale the
system reports and uses the 200% fallback only when the system reports
no scaling (100%), which is the case #225 was fixing.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:17:18 +07:00
Gert Pellin - GPE cbed56b7f1 Desktop: write log records to standard error and a rotating log file (#1143)
* Desktop: write log records to standard error and a rotating log file

The desktop app installed no logger, so every log::warn!/log::error! in
the workspace was discarded: GPU startup fallbacks, a lost wgpu device,
the crash guard's panic report. A launch from a desktop menu has no
terminal either, so there was nothing to attach to a bug report.

apps/photocraft/src/logging.rs (no new dependency):
- standard error plus <settings dir>/logs/photocraft.log; each launch
  rotates the previous log to photocraft.1.log and .2.log, so the log of
  a run that crashed survives the next start; the file is capped at 16 MiB
- records logged before the settings directory is known are buffered
  (bounded) and written once the file is attached
- default levels: info for photocraft*, warn for everything else;
  RUST_LOG replaces them with env_logger-style directives
- never panics: an unusable directory or a failed write leaves standard
  error as the only sink

The logger is installed first in main (so the panic hook is recorded) and
the file is opened after argument parsing (--version and usage errors
leave no file). Documented in docs/development.md (Logs).

* Contributors: credit switch87 as Gert Pellin

* Desktop: report an unusable log folder through the logger

eprintln! panics when standard error is a broken pipe (a GUI launch on
Windows); log::warn! reaches standard error the same way once the file
sink has given up, and never panics. Same fix as storytold/vectorcraft#465.
2026-10-08 16:09:32 -04:00
Tanish Manku 964a41dc02 Corpus: pin the deep OpenEXR test images and check them in CI (#881)
The deep EXR decoder (#642) was verified against the OpenEXR project's deep test images, but those
lived in a hand-copied `corpus/exr/` and the test skipped when they were absent, so CI never read
them. `cargo xtask corpus --exr` (and `--all`) now fetches the five `*.deep.exr` files of
openexr v3.5.2 `src/test/bin/test_images`, sha256-verified against `xtask/exr-corpus.sha256`.

`real_deep_files_decode` is behind the `corpus` feature and fails when the corpus is missing. It
checks each file's sample total, largest per-pixel count, cumulative counts and channel lengths,
and that the decoded u32 ID channels hold exactly the IDs listed by the upstream `.txt` sidecars
(an independent oracle), plus the flattened decode. FILE-216-3 moves to partial (EXR read in CI;
TIFF and RAW still are not).
2026-10-08 16:08:56 -04:00
lionel 69c01c1bff Startup: preserve the GPU backend after windowing errors (#1228)
* fix(startup): preserve GPU backend after non-graphics errors

* fix(startup): retain prior crash evidence on windowing errors
2026-10-08 15:32:54 -04:00
Streetblock 15cfdcd9e8 docs: refresh implemented tool status in the roadmap (#1205) 2026-10-08 15:32:43 -04:00
bfentyandBrandon Fenty f9b72468b4 docs: stop linking readers to the private craftrules repo (#493) (#779)
The contributor docs offer github.com/storytold/craftrules as the place
to read the font rules and release playbook, but the repository
returns 404 to everyone outside the organization, so the cited text is
unreachable for outside contributors. Keep the sibling-checkout paths
and say plainly that the repo is not public and a maintainer can supply
the rules relevant to a change.

Co-authored-by: Brandon Fenty <bfenty@users.noreply.github.com>
2026-10-08 15:04:21 -04:00
Claude 3bab0d4f52 PSD text fill: keep the fix and its tests, drop the proof-only screenshots and example
The Photopea screenshots, their ATTRIBUTION row and the example that made
them documented the bug for review; the unit tests in crates/text cover
the behaviour, so they are not needed in the tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHgFEHEsefoQoBParH47JA
2026-10-09 01:57:17 +07:00
Rage 7bb971f77c Fix PSD text fill after edits in Photopea 2026-10-09 01:57:17 +07:00
Nigel Ihza Firdausy 7d78e1dd2c Export Preferences: WebP quality and lossless settings for Quick Export (#1260) (#1279)
* feat(export): persist WebP lossless mode and quality in export preferences (#1260)

* fix(export): apply configured WebP quality to Quick Export output

* fix(ui): show format-specific JPEG and WebP export preference controls

* fix(ui): use WebP quality and lossless preferences in Export As dialog

* test(export): verify WebP quick export mode, quality, validation, and legacy prefs

* style: format WebP export regression test as rustfmt expects

* style: apply rustfmt layout to conditional WebP preference labels

* docs(scorecard): account for two WebP export preference fields (#1260)
2026-10-09 01:57:01 +07:00
callmeBallochandClaude Opus 5.5 ae0313d325 AF.3: native Affinity import with artboards, vectors, text and images
photocraft-affinity now reads native Affinity documents (container
versions 8-12): the FAT and revision chain, stored/zlib/zstd entries
with predictors and CRC checks, the tagged object stream in doc.dat,
and the document model (pages, artboards, layers, groups, curves,
geometric and compound shapes, fills, gradients, strokes, text, placed
images, pixel layers, masks and embedded-document caches). Every size
is bounded and malformed input returns an error.

photocraft-io maps it to an RGB8 document with artboards, groups, shape
layers, type layers, embedded smart objects, pixel layers and
vector/layer masks; what can't be mapped is listed in the warnings. The
embedded preview is now only a fallback when the native data can't be
read, and only that preview is refused by Place and warningless imports.

`cargo xtask corpus --affinity` pins 21 public CC0/MIT documents,
sha256-verified; the io corpus test compares each import with the
thumbnail Affinity saved in it (mean difference 0-4.6 of 255).
Affinity export is not implemented: no Affinity installation was
available to validate written files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GQPh8UDgMyARR5Hat9UchC
2026-10-08 18:46:22 +00:00
callmeBalloch 1bb37eda05 AF.1: import bounded Affinity previews with fidelity warnings
Read the indexed version-12 PNG preview with bounds, chunk ordering and CRC checks. Show native-content and resolution warnings, preserve the source through normal and automation opening, and reject auxiliary routes that cannot display the warning. Add format documentation, malformed-input and save-lifecycle tests, native-oracle hooks and parser/import fuzz targets. Editable native layers and Affinity export remain unsupported.
2026-10-08 17:42:23 +02:00
452672765d Photo Filter and gradient glows render like Photoshop (#442, #443) (#850)
* Photo Filter and gradient glows render like Photoshop (#442, #443)

Photo Filter multiplied encoded RGB by the filter colour and rescaled to Rec.601 luma. Photoshop
multiplies the pixel's D50 XYZ (relative to white) by the filter colour's, mixed by density, and
Preserve Luminosity restores the encoded luminosity like the Luminosity blend mode (SetLum +
ClipColor); 32-bit documents keep luminance Y. rgb16 photo-filter.psd now matches within 0.6/255
(was 125/255); rgb8 mean error 0.3/255 (was 20.8).

Outer and Inner Glow gradients were laid out across the canvas (a vertical linear gradient), so
the colour bands ran top to bottom. Photoshop colours the glow at strength v with the gradient at
1 - v (first stop at the edge) and the glow is opaque from v = range^2. outer-glow-gradient.psd:
mean 1.4/255, max 9.9 (was 252).

CPU and GPU paths updated together; photoshop oracle floor 132 -> 133.

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

* Merge origin/main; regenerate docs/scorecard.md

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-07 23:49:10 -04:00
01cd6fffa2 Layer › Smart Objects › Convert to Layers (#535) (#829)
* Layer > Smart Objects > Convert to Layers (#535)

Unpacks a smart object's contents back into layers at its placement:
several layers land in a group named after the smart object (carrying
its opacity, blend mode, masks and style); a single layer replaces it
directly when those fold in exactly. Contents are conformed to the
document's mode, profile and depth; smart filters are discarded.
Also in the Layers panel context menu and the Properties quick actions.

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

* Parity: Convert to Layers is live (627 of 627); raise FLOOR

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

* Convert to Layers: pass no inherited group locks to transform_layer (#847 added the parameter)

The layers are transformed inside released(), which lifts every lock in their tree.

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

* it: translate Convert to Layers (Italian landed after this PR was written)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-07 23:44:21 -04:00
BFlatandClaude Opus 5.5 244a99c39f Windows and Linux: one title bar with the app icon (no stacked OS bar); long context menus scroll (#986)
* Windows and Linux: one title bar with the app icon, like Photoshop

The window had the OS title bar stacked above PhotoCraft's own top bar. On
Windows and Linux the window now has no OS decorations and the top bar is
the title bar, as in VectorCraft and Photoshop on Windows:

- the app icon (brand.rs) leads the bar, before the menus;
- Minimize, Maximize/Restore and Close sit flush in the top-right corner
  (Close turns red on hover; it runs File > Exit, so unsaved work is asked
  about first);
- the free gap between the menus and the controls still drags the window
  and a double-click there maximizes it;
- invisible 5 pt edges (12 pt corners) resize the window.

The right-hand group gets its own rect between the menus and the caption
buttons (egui's MenuBar takes the whole row, so it used to be laid out from
past the bar's end). Narrow windows drop what is also in a menu before
anything overlaps: Discord, then the theme toggle, then search, then the
workspace switcher narrows. macOS keeps its traffic lights.

New theme tokens caption_close / caption_close_text, Minimize / Maximize /
Restore in all 12 catalogs, and a --custom-titlebar flag for the snapshot
example.

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

* Long right-click menus scroll inside the window instead of running off it

widgets::menu_scroll caps a context menu at the visible window height
(clear of a taskbar a too-tall window runs under, #315). egui moves the
popup up to fit, and it scrolls only when it is taller than the window.
Used by the Layers, canvas tool, Channels, Paths and document-tab menus;
the menu bar's menus already scroll (menu_nav::level).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:02:07 +07:00