- gpu: the test modules share one GPU_LOCK, and timing and vector_mask
take it too, so one process drives one wgpu device at a time.
- ui-egui: the wgpu tests in pattern_preview_canvas, adjust_preview_gpu,
marquee_autoscroll and layout_perf take gpu_lock().
- affinity compound_limits: the child process selects its test by the
module path (compound_limits::<name>); with --exact, the bare name
matches no test and the child passes without running the check.
- xtask scorecard reads crates/io/tests/it/corpus.rs.
- Docs and comments name the new test paths and the --test it filters.
crates/engine/src/file_cmds.rs: batch keeps the save-point closure (takes the output path) and also copies the session's authorize hook into each scratch session, as main now does.
crates/raw/src/sensor.rs: both sides cap the memory of retained tiles; kept the existing retained_segment_bytes check (checked multiplication, same limit) and dropped main's duplicate inline loop.
PhotoCraft reads GIMP's .xcf (and .xcf.gz) files, read only, through a new
reader in photocraft-io written from the public XCF specification: versions
0 to 26, uncompressed, RLE and zlib tiles, 8/16/32-bit integer and
16/32/64-bit float precisions (linear precisions get a linear profile),
pixel layers at any position, groups by item path with pass-through,
layer masks applied or disabled, opacity, visibility, locks, colour tags,
the 2.10 and legacy blend modes mapped to Photoshop's (modes without an
equivalent open as Normal with a note), the ICC profile, resolution,
guides, the comment, extra channels and the saved selection; indexed
images open as RGB. Text, vector and link layers open as their pixels;
layer effects, paths and floating selections are reported.
Fixtures are written by GIMP 3.2 itself (scripts/xcf_fixtures.py), each
with GIMP's merged rendering as a PNG. In gamma-encoded blend and
composite spaces our composite matches GIMP within 0.5/255; GIMP's
linear-light default, its Clip to backdrop and its clamp-after-alpha
for unbounded modes are documented and measured in docs/xcf.md.
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.
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.
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.
* Linux: a virtual keyboard claiming BTN_TOOL_PEN no longer sends the window through Xwayland (#5284)
On Wayland the app opens its window through Xwayland when a pen is attached
(#639), and it took any input device whose key bitmap has BTN_TOOL_PEN for a
pen. A virtual keyboard made with uinput (RustDesk's, for one) claims every
key code, that one included, so the window went through Xwayland on machines
with no pen at all. On a fractionally scaled desktop (GNOME 46 at 125%) the
X11 window is drawn at the logical size and upscaled by the compositor, so
the UI came out blurred even though Auto UI Scale already followed the
system's 1.25 (#1237): native Wayland, where winit negotiates
wp_fractional_scale_v1 and renders at the physical 3840 px, was never used.
A pen now also needs ABS_X and ABS_Y in its absolute-axis bitmap, as
libinput asks of a tablet tool; a keyboard has no axes. Regression tests
with the virtual keyboard's own bitmaps, a pen beside it, and a pen display.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Docs: on a fractionally scaled Wayland desktop, Xwayland is blurred and native Wayland has no file drops (#5284)
Say where the trade-off is: a native Wayland window is sharp at 125%,
150% and 175% but takes no dropped files (#386); an Xwayland window takes
drops and pen input (#639) but is enlarged by the compositor. List the ways
to open through Xwayland and what counts as a pen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
- ROADMAP.md: the oracle floor is 145 of 258 (crates/io/tests/corpus.rs,
gaps.md G1), not 133 of 256; 953 open issues, not 579.
- docs/architecture.md: ~539,000 lines in ~1,310 files and ~8,040
#[test] functions, recounted 2026-10-13.
- README.md: "a layer kind filter" instead of "layer filters"; only the
Kind filter exists.
- AGENTS.md: drop an empty bullet at the end of section 7.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Open .kra and .krz with paint layers (8/16-bit RGBA and gray, any offset), nested
and pass-through groups, names, visibility, opacity, locks, blend modes and
transparency masks, plus the image's ICC profile and resolution. Vector, filter,
fill, file and clone layers and the other mask kinds are left out with a warning;
float, CMYK and Lab documents open as Krita's merged image (preview). Import only:
Save suggests .ora. Checked against 35 Krita 5.2.9-authored files.
Co-authored-by: Scott Campbell <251750033+scottcampbelldata@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(psd): rasterized fill and smart layers stay rasterized after save and reopen
Layer > Rasterize > Fill Content / Smart Object swapped the layer's content
to pixels but kept the fill (SoCo/GdFl/PtFl) or placed-layer (SoLd/PlLd/SoLE)
blocks read from the PSD. Written back, they made the reopened file a fill
layer or smart object again and discarded the pixels. Photoshop writes none
of them for a pixel layer.
- Rasterize drops those blocks from the layer.
- The PSD exporter never writes fill, placed, type (TySh), shape-content
(vscg/vstk), adjustment or CgEd blocks for a raster layer, whichever
command made it one; a vector mask (vmsk/vsms with its vogk) stays.
Tests: synthetic fill + smart object rasterize/save/reopen, the exporter
backstop with stale blocks, and Photoshop-authored corpus files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(psd): keep a pixel layer's vstk when it has no vector mask, as Photoshop does
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Rust standard library has needed Windows 10 since 1.78, so on
Windows 7 and 8 PhotoCraft fails at launch with "The procedure entry
point ProcessPrng could not be located in the dynamic link library
bcryptprimitives.dll" (#2310). The MSI installed anyway.
It now has a launch condition: a registry search sets WINDOWS10ORLATER
from HKLM\...\Windows NT\CurrentVersion\CurrentMajorVersionNumber,
which exists only from Windows 10 on (in the 32- and 64-bit views), and
without it the installer stops with "PhotoCraft needs Windows 10 or
later." Repair and uninstall still run (Installed). MSI's VersionNT
isn't used: it has reported both 603 and 1000 on Windows 10.
The README's Windows downloads now say Windows 10 or later is required;
the portable zip can't check, as Windows rejects the exe before any of
its code runs.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add FITS read/write codec
FITS (the astronomy format) opens and saves as a flat image: the primary
HDU or first IMAGE extension; gray, or RGB when NAXIS3 = 3; BITPIX 8/16/
32/64/-32/-64 with BSCALE/BZERO/BLANK; rows follow ROWORDER (bottom-up by
default). Camera U16 data stays exact; float data is normalised to 0..1.
Header cards round-trip as text metadata (HIERARCH for long names).
Tile-compressed (fpack) files are refused as unsupported.
Wired into File > Open, Save As and the batch commands as .fits/.fit/.fts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Test FITS open and save through photocraft-io
Gray 16-bit, RGB 8-bit and RGB 32-bit float documents round-trip exactly,
and header cards travel as the document's text metadata.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* FITS: hostile-header regression test (absurd axes, bad BITPIX, no END, HDU chains, overflowing sizes)
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>
* Color Sampler tool (#1046)
Add the Color Sampler to the Eyedropper (I) flyout: click to pin numbered
sample points to canvas pixels, drag to move, Alt-click (or drag off-canvas)
to remove, Clear All in the options bar, at most 10 per document. Each point
reads one composite pixel (RGB 0-255 and naive device CMYK, same as the
pointer readout) and the Info panel lists them under their number with the
position in the ruler unit; the readouts refresh when the document changes.
Points are per-document engine view state (DocState.color_samplers): they
belong to their document, are discarded on close, and are not saved to
.pcraft or PSD. They are driven by commands shared by the UI, CLI, control
channel and MCP (view.colorSamplers.add/.move/.delete/.clear/.list); the
mutations change no pixels and add no History step.
Points follow the canvas geometry (crop, Canvas Size, Image Size, Image
rotation/flip, rotated crop) through a pending Session.sampler_map that
Session::edit applies before culling the points that land outside the new
canvas; the count removed is reported in the status bar.
- engine: new sampler_cmds.rs (commands + shared readout helpers) and the
sampler_map wiring in lib.rs, image_cmds.rs, mode_cmds.rs, commands.rs,
analysis_cmds.rs (compound rolls points back on error).
- ui-egui: Tool::ColorSampler + original sampler.svg icon, toolbar slot,
Info panel list, options bar, pointer handling, overlay markers, cursor.
- docs/scorecard.md regenerated (TOOL-1649-1 no longer says the tool is
missing). No menu item, so parity is unchanged (627/627).
* Color Sampler follow-up: tool count, README, i18n, roadmap
The full ui-egui suite (not just the filtered tests) surfaced these gaps:
- state.rs: Tool::ALL.len() assertion 52 -> 53.
- README.md: "52 tools" -> "53 tools" and add Color Sampler to the list,
so readme_tool_list_matches_tool_all passes.
- i18n: the options-bar hint, "{n} of {max} sample points" and
"No document open" need entries in every catalog that claims complete
menus (every_tl_literal_is_translated); added to all 16 (coverage
1584/1584). The translations were drafted alongside the tool.
- docs/roadmap.md: drop Color Sampler from the missing-tools list and note
it landed.
* Color Sampler: fit main's new tools and Image Size resample type
Reset Tool covers ColorSampler, the test ViewXform sets aspect, and
Image Size checks Method::None instead of an Option.
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>
Convert Point is a tool of its own, built on the existing path.convertPoint
edit that Pen + Alt already drives. A click makes a smooth anchor a corner, a
drag from an anchor pulls out smooth handles, a drag on a handle moves it
alone. The tool selects the anchor it converts so its handles show; a press on
empty canvas or a segment does nothing, and Cmd/Ctrl is Direct Selection, as
with the Pen. It shares the Pen's key (P).
Original glyph with an ATTRIBUTION.md row; tool name in all catalogs; README
tool list and the G6 / ui-parity / roadmap counts updated (tools 56 of 68, the
earlier 53 was stale).
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Frame tool (K, its own toolbar slot after the Crop group) draws a
rectangular or elliptical frame; the options bar picks the shape, Shift
constrains to a square or circle and Alt draws from the centre. A frame
drawn over the selected pixel layer's content takes that layer in;
otherwise it is an empty frame.
Frames are built as Frame from Layers builds them: a group clipped by a
layer mask (rectangular or anti-aliased elliptical). The new engine
command layer.new.frame does it in one Session::edit, so one undo step.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
New Perspective Crop tool in the Crop flyout (C): drag a box or click four
corners, drag corners independently onto a skewed object, perspective grid
(Show Grid), Enter commits, Esc cancels. W/H/Resolution come from the Crop
tool's fields (Front Image, Clear), or the quad's mean side lengths.
Engine: image.perspectiveCrop {corners, width?, height?, resolution?,
interpolation?} maps the quad onto an upright canvas through the
quad-to-rectangle homography (Heckbert's square-to-quad map, inverted):
every raster layer, mask, alpha channel and the Quick Mask, one undo step.
The Background stays opaque over the background colour; type, shape, fill
and smart object layers are rasterized first; vector geometry follows the
nearest affine map. Non-convex, self-crossing, counter-clockwise or
degenerate quads are refused with an error.
algo: warp_surface_clipped bounds the per-tile warp by the new canvas even
when a layer reaches past the map's horizon; convex_quad, quad_size,
quad_to_rect.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Photoshop's Artboard tool in the Move flyout (V, ⇧V cycles Move ↔ Artboard):
dragging on an empty spot draws a new board (`layer.new.artboard`), clicking a
board selects it, dragging it moves it with its contents, and the selected
board's edge and corner handles resize it (`layer.artboard.set`,
moveContents: false). Each gesture is one undo step. Options bar: Size preset,
W/H, Make Portrait, Make Landscape, Add New Artboard. Original toolbar glyph
(assets/icons/artboard.svg, ATTRIBUTION.md row).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The Type flyout (T, Shift+T cycle) gains Vertical Type Mask and Horizontal
Type Mask, in Photoshop's order. They type like the Type tools, with the
canvas under Quick Mask's colour and the glyphs clear while editing. On
commit the glyph coverage replaces the selection; no layer is left behind
and the whole session is one "Type Mask" history step. Escape always
cancels and leaves the selection unchanged.
The commit runs a new engine command, type.createSelection: a type layer's
glyphs (optionally removing that layer, the tools' hidden working text) or
type.create's keys directly, with new/add/subtract/intersect and an
anti-aliasing override. type.create's layer building is shared through
type_cmds::new_text_layer.
Original dashed-T icons (ATTRIBUTION.md), tool label translations in every
catalog, README tool list and the gaps/parity docs updated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The Pen flyout (P / Shift+P) now holds Pen, Freeform Pen and Curvature Pen.
Freeform Pen: a drag draws freehand; on release the trace is fitted with
cubic Beziers within the options bar's Curve Fit (default 2 px) by
photocraft_vector::trace::fit_trace (Schneider, "An Algorithm for
Automatically Fitting Digitized Curves", Graphics Gems 1990, the fitting
Make Work Path already uses), keeping turns sharper than 60 degrees as
corners. Releasing on the start point closes the path.
Curvature Pen: each click adds a smooth point and the path runs through
every point (Catmull-Rom style handles from
photocraft_vector::edit::curvature_knots); dragging a point moves it,
double-clicking toggles corner/smooth, clicking the first point closes,
Enter/Esc finish and Backspace removes the last point, as with the Pen.
Both commit through the Pen's path: a shape layer in Shape mode, else the
targeted vector mask, else the work path, as one History step.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Paths could be reshaped after drawing since #865, but the number of
anchors could not change. Two new engine commands do that, and two tools
in the Pen flyout drive them on the canvas.
- photocraft_vector::edit::add_anchor splits the segment at t with de
Casteljau, so a curve keeps its shape and gets a smooth anchor; a
straight segment gets a corner on the line. delete_anchor joins the
neighbours keeping their handles, drops an emptied subpath and refuses
to remove a path's only anchor.
- path.addAnchor / path.deleteAnchor take the same targets as the other
point edits (work path, saved path by name, shape layer path, vector
mask) and record one history step each.
- Tool::AddAnchorPoint / DeleteAnchorPoint join the P group after the Pen.
They act on Direct Selection's paths, highlight the segment or anchor
under the pointer, show a +/- cursor badge, swap with Alt and turn into
Direct Selection with Cmd/Ctrl. An edit clears a Direct Selection on the
same path so it never points at a shifted anchor.
- Icons derived from Lucide pen-tool (ATTRIBUTION.md), hints and tool
names in all 16 catalogs, README tool list, roadmap.
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The engine already had paint.colorReplacement, but the desktop UI had no
tool for it: no Tool entry, no toolbar slot, no options bar and no stroke
routing, so only agents and the CLI could use it.
Tool::ColorReplacement joins the Brush (B) flyout after the Mixer Brush,
so B / Shift+B cycle to it and ui.set / ui.inspect name it. Its options bar
has Mode, Sampling (Continuous / Once / Background Swatch), Limits,
Tolerance and Anti-alias, all mapped to the command's params, with the
foreground as the replacement colour. Strokes are drawn live through
LiveDab, which can now preview the stroke's finishing dab, so the preview
at the last move matches the commit. The command refuses a mask, channel
or Quick Mask target instead of silently recolouring the layer. New icon
derived from Lucide's brush; label translated in all 16 catalogs.
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Release prep 0.7.0: add Linux riscv64 tar.gz to the release; bump to 0.7.0
- release.yml: new linux-riscv64 job, ported from GridCraft: cross-compiled on ubuntu-24.04
against ports.ubuntu.com riscv64 libs, tar.gz only, photocraft-cli --version smoke-tested
under QEMU. The draft-release job now needs it, so SHA256SUMS.txt covers it.
- packaging/linux/package.sh: CROSS_ARCH / CROSS_TARGET / CROSS_COMPILE / EMULATOR support.
- linux_libs: search /usr/lib/riscv64-linux-gnu on riscv64 (was the x86_64 triplet).
- README and docs/releasing.md list the riscv64 tarball.
- Version 0.6.0 -> 0.7.0 (cargo xtask version set), Cargo.lock refreshed; contributors refreshed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* canvas: crisp elliptical marquee ants again in an unrotated view
#2732 (a27d7175) built the elliptical marquee's live ants in document space and snapped every
vertex to the half-pixel grid on its own. Near the edge midpoints the nearly flat arcs became
1 px stairs, whose antialiased steps are grey, so the ants no longer contrast on white or black:
drag_preview_canvas's marquee test fails on main (ubuntu and windows). In an axis-aligned view
(rotation a multiple of 90 degrees) draw the ellipse inscribed in the snapped screen box again,
as before #2732; rotated views keep the mapped ellipse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Both tools join the Marquee flyout (no key, outside the M / Shift+M cycle,
as in Photoshop). A click selects the one-pixel row (column) under the
pointer across the whole canvas; a press-drag moves the line until the
release. The selection goes through select.rect, so Shift adds, Alt
subtracts, Shift+Alt intersects and the options-bar modes apply; Feather
applies, Anti-alias and Style/Width/Height are greyed. Selection-mode
cursor badges, the selection right-click menu and snapping cover them.
Original row/column glyphs with an ATTRIBUTION.md row; tool names in all
catalogs; README tool list and the G6 / ui-parity rows updated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Progress docs per craftrules standards/progress-docs.md: target-app parity (merges parity-estimate.md), gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and UI, brush, format, hardware and localization parity docs. docs/parity.md is now docs/parity-checklist.md.
The Windows test failure (file_dialog::tests::save_dialogs_remember_where_each_document_was_last_saved) is pre-existing on main: the test hard-codes POSIX paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Open and save .ora with pixel layers (any offset), nested groups, pass-through
groups, names, visibility, opacity, edit/alpha locks and blend modes (svg:* plus
Krita's krita:* names), at 8 and 16 bit. Save applies pixel masks, renders
text/shape/fill/smart layers to pixels and reports what OpenRaster can't hold.
Wired into File > Open/Save, the CLI, automation/MCP, the web picker and the
desktop file associations. Checked against 34 Krita 5.2.9-authored files
(opt-in krita_corpus test) and Krita re-opening our exports.
Co-authored-by: Scott Campbell <251750033+scottcampbelldata@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Add the Remove tool with non-local patch completion
Tool::Remove (J flyout) paints a hole that paint.remove fills from its
surroundings as a background job, in one undo step. A stroke around an
object also removes what it encloses; the selection limits the hole.
The fill (photocraft_algo::nonlocal) follows Newson, Almansa, Gousseau and
Pérez (IPOL 2017): texture features in the patch distance, onion-peel
initialisation, match fields upsampled between levels and a best-patch
final copy, plus gradient-domain hiding of edge and interior seams.
Sparse holes are filled in tiles. inpaint::complete (Content-Aware Fill,
Spot Healing, Content-Aware Move) is unchanged.
On 20 holes in public-domain photos (remove_quality) the fill keeps 0.89
of the texture it replaces, against 0.54 with the Wexler/PatchMatch
completion. 24 MP (remove_bench): 100 px ring 120 ms, 1000 px ring 2.5 s.
* Translate the Remove tool's label and tighten comments
Adds "Remove Tool" to every UI catalog, following each one's pattern for
tool names and its existing word for Remove. Corrects comments that no
longer matched the code: the benchmark's description, the tile and margin
minimums, and the reasons given for two parameters.
* Put the Remove tool first in the J group
The J flyout's slot now shows Remove until another J tool is picked, and
J from another group selects it; repeated J goes on to Spot Healing Brush,
Healing Brush, Patch, Content-Aware Move and Red Eye.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Squashed onto main, with the maintainer's asks applied:
- Photoshop's blend menus (layers, brush, pencil, gradient, layer style) stay as they are;
the Layers panel and Blending Options list a Paint.NET mode only for a layer that uses one.
- The six Paint.NET mode labels are translated in every catalog, and the blend-mode i18n test
iterates layer_modes().
- pdn::pixels rejects truncated uncompressed (version 1) data before allocating its buffer.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Edit › Preferences › General takes ⌘K and Edit › Search moves to ⌘F, as in Photoshop. Nothing
else was bound to ⌘F (Filter › Last Filter stays ⌘⌥F); the shortcut-capture tests follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Type options bar and Character panel swatches open the full Color Picker (Text Color),
applying to the selected characters or the whole layer via type.setStyle; Cancel is inert. A
shared "Pick screen color" action in the full dialog and compact popups samples the desktop:
X11/Windows/macOS snapshots with an 11x11 magnifying loupe (4x-24x), the XDG PickColor portal on
Wayland, the EyeDropper API on the web. Captures stay in memory, are bounded at 64 MP, and are
ICC-converted to sRGB where the display publishes a profile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
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>
* Wayland: drop guidance offers paste and this install's XWayland command (#338, #386)
winit 0.30 (eframe 0.36) delivers no file drops on Wayland. The notice told every
user to run `WAYLAND_DISPLAY= photocraft`, which does nothing for the AppImage
(no `photocraft` on PATH) or the Flatpak (fallback-x11 grants no X socket while
Wayland is available). It now names the command for this install (Flatpak flags,
the quoted $APPIMAGE path, or `photocraft`), omits it without an X server, and
offers pasting a copied image file with Edit › Paste's shortcut, which works on
Wayland since #384. The start screen hint on Wayland says the same.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Wayland drop notice: 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>
* 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>
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
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.
* Lossy WebP: a pure-Rust VP8 encoder with quality and lossless controls (#648)
crates/codecs gains codecs/vp8: a clean-room VP8 key-frame encoder from RFC 6386
(boolean coder, 16x16 and 4x4 intra prediction with per-macroblock mode decision,
forward DCT/WHT paired with the bit-exact inverse transforms, dc/ac quantiser tables,
token coding with per-frame probability updates, skipped macroblocks). webp.rs wraps
the frame in the RIFF container (bare VP8, or VP8X with ALPH alpha and ICCP/EXIF/XMP).
EncodeOptions: webp_lossless now picks the encoder instead of erroring, webp_quality
(0-100) sets the quantiser; a lossy WebP reports a LossyCompression fidelity warning.
Export As: the WebP entry gets Quality and a Lossless switch (new tl! string in all
catalogs). CLI --quality, MCP/RPC doc_save quality and the engine's JPEG-style
quality turn a WebP lossy; without a quality WebP stays lossless as before.
Tests: tests/webp_lossy.rs decodes every file with image-webp (PSNR by quality,
sizes fall with quality and are >= 4x below lossless on noisy photos, flat frames
under 1.2 KB, alpha exact, metadata round trip, odd/tiny sizes, 16-bit input, the
16383 px limit as an error, text edges), plus unit tests for the coder, transforms,
prediction and header.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* i18n(it): Lossless (Export As WebP switch) in the Italian catalog that landed on main
Co-Authored-By: Claude Mythos 5.1 <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>
* Add the Magnetic Lasso tool
A selection border that snaps to edges as the pointer moves, in the
Lasso (L) flyout after the Polygonal Lasso.
- photocraft-algo `magnetic`: live-wire edge tracing (Mortensen &
Barrett 1995): Sobel strength, non-maximum suppression and gradient
direction link costs, a shortest path inside a corridor of the
detection width around the pointer's path, sub-pixel edge centres.
Pixels are read lazily in 128 px blocks.
- Engine: `EdgeSource` (the active pixel layer, else the composite) and
`select.magneticLasso`: rough points in, a border along the edges out;
`trace: false` selects a given outline, which is how the tool commits.
- UI: click to start, move to trace (button up or down), points fasten
by themselves (Frequency), click fastens, Alt-click straight, Alt-drag
freehand, Backspace/Delete removes points, click the first point,
double-click or Enter closes, Esc cancels, [ ] change the width.
Options bar: Width, Contrast, Frequency, tablet pressure. `ui.inspect`
reports the border in progress.
- The Lasso and Polygonal Lasso now apply the options bar's Feather;
`select.lasso` rejects a feather outside 0..1000.
- Icon adapted from Noun Project's "Magnetic Lasso Tool" by Lil' Seal
(CC BY 3.0, licence file and ATTRIBUTION.md row).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* it: translate the Magnetic Lasso strings (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>