* Layered TIFF: open and save Photoshop layer data in TIFF files, in either byte order (part of #215)
Photoshop keeps a layered TIFF's layers in two private tags: 37724 (ImageSourceData, the same
layer section a PSD has) and 34377 (the image resources). PhotoCraft now reads and writes both,
so a TIFF opens with its groups, masks, layer styles, type, shapes, smart objects and adjustment
layers exactly as the PSD path would, and saves them back.
* photocraft-psd: a `tiff` module with `ImageSourceData` (parse/write, PSD- and PSB-sized),
the 34377 resource helpers, and a field-by-field byte-order transcoder. In Intel-order TIFFs
Photoshop byte-swaps the whole block structure (reversed keys, little-endian integers and
doubles) while pixel samples stay big-endian; the transcoder knows the layer-info structure,
masks, blending ranges and every block photocraft-io maps (descriptors, TySh, lrFX, the
binary adjustments, paths, patterns, filter effects, metadata settings, linked layers,
annotations...), recognizes descriptor-shaped unknown blocks, and drops the rest with a
warning instead of passing byte-swapped data to other readers. The reader gained an optional
little-endian mode. 23 unit tests; a fuzz target in the CI matrix.
* photocraft-codecs: the two tags travel as opaque bytes on `Metadata`, read by a
bounds-checked walk of IFD0 (classic and BigTIFF, both byte orders) and written back as
BYTE / UNDEFINED tags. `tiff_writes_little_endian()` tells which order the encoder produces.
* photocraft-io: `tiff_layers` rebuilds a PSD model from the TIFF's pixels plus the tags on
import, and on export runs the PSD exporter (new `merged_matte` / `merged_compression`
options), interleaves its merged planes for the TIFF image and stores the layer section in
the encoder's byte order. `ExportOptions.tiff_layers` (default on). Lab and Multichannel
documents save flat; extra alpha channels are not written (one transparency sample).
* UI: the TIFF Options prompt ("Including layers will increase file size." / "Discard Layers
and Save a Copy" / "Don't show again") behind Preferences > File Handling > Ask Before
Saving Layered TIFF Files; `ExportSettings.tiff_layers`; catalogs updated.
* Tests: io round trips at 8/16/32 bits in Gray/RGB/CMYK, equivalence with the PSD path, every
TIFF compression, the flat fall-backs, malformed input, and files written by psdtags in both
byte orders (fixtures from scripts/layered_tiff_fixtures.py); a corpus test (feature
`corpus`) writes every corpus PSD as a layered TIFF and compares the result with the PSD
round trip (700 files, 0 differences, 0 crashes). Codecs tests cover the tag walk on
hand-made files.
* Scorecard FILE-215-7 done, BUG-204-3 partial; README and book updated.
Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
* Layered TIFF: de and pt-br strings; neutral wording in the psd tiff docs
The four psdtags fixtures stay out of the repository (no binary test data here): the
generator now writes them to the gitignored corpus/layered-tiff/, and the tests that read
them skip with a message when absent.
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 Mythos 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
korean_covers_generated_filter_options_and_gallery_names (from #604) requires a
Korean label for every generated filter option. Relight (#735) added the options
ambient and warmth; its CI ran on a base without #604's test, so main now fails
it. Adds the two catalog entries.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Type: hitTest, caret and navigate commands so agents can edit text by position (part of #219)
Stateless queries in document pixels. The word, line and hit-test helpers move from type_tool.rs into photocraft-text, and the Type tool calls the same code.
* Regenerate docs/scorecard.md after rebase
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>
* Support Alt-held polygon segments in the freehand lasso
* Translate the Alt lasso hints into Simplified Chinese
* Test mixed freehand and Alt-click lasso gestures
* Lasso: a press inside the selection still drags it; Alt lasso hints in every catalog
The Alt-polygon lasso takes over the Lasso's pointer events, so the selection drag from
#686 (press inside the ants to move the outline, Cmd-drag to float the pixels) no longer
reached the canvas. Hand those presses, and the drag they start, back to the canvas path.
Also adds the two new hint strings to every translation 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>
* fix(i18n): follow native UI languages on first launch
* i18n system tests: German and Portuguese are supported now; use Swedish as the unsupported tag
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>
* Shapes: a hairline vector stroke no longer darkens the shape's edge pixels
A stroked shape with layer effects (or clipped layers) is drawn as our
fill plus a vector stroke fitted to Photoshop's own pixels: where those
pixels reach past our fill, the stroke takes the alpha the fill doesn't
explain, `(pixels - fill) / (1 - fill)`. It kept our stroke's colour for
that alpha. When Photoshop's fill covers more of an edge pixel than ours
(shapes snapped to whole pixels with Align Edges, our path a fraction off
the grid), the fitted alpha is mostly Photoshop's fill, so a 0.25 px grey
border on a white card was painted as a solid grey line: the edge came out
at the stroke's full colour (122) instead of Photoshop's 220.
The fitted stroke now takes the colour that, drawn Normal over our fill,
gives back Photoshop's pixel (un-compositing the premultiplied colour),
and keeps our colour only where the fitted alpha is negligible. Where
Photoshop's pixels agree with our fill this is our stroke's colour, as
before.
Test: hairline_vector_stroke_keeps_photoshops_edge_pixel_colour (edge
pixel 0.478 before, 0.86 after). The compose, gpu, io and engine tests
and the psd/io/engine corpus tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shapes: neutral wording in the hairline-stroke fix; test it at 8, 16 and 32 bits
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: EthanSuquet <292885762+EthanSuquet@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Use the TIFF decoder's normalized WhiteIsZero samples without a second inversion, and parse BigTIFF IFD0 Orientation so decode applies the recorded rotation.
Fixes#691Fixes#693
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
* fix(ui): bound Variables data-set index from control input (#756)
* test(ui): cover hostile Variables data-set indices (#756)
* style: format regression coverage for #756
---------
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Formatting only: cargo fmt --check (rustfmt 1.99, as CI) fails on main at
crates/codecs/tests/deep_exr.rs:360 since #642 landed.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Add Filter › Relight to redirect a photograph's lighting
Introduce a tiled Retinex and Lambertian kernel with angle, elevation,
intensity, ambient, warmth and softness. Wire it as filter.render.relight
with a live-preview dialog, selection support, and smart-filter recording.
* Tidy Relight kernel loops
Rewrite the per-channel colour multiply and alpha restore so the filter
builds cleanly under clippy.
* Format the Relight kernel and enablement check
Keep rustfmt's wrapping on the alpha restore and collapse the lock
predicate to a single expression.
* Step Relight shading gradients at the blur scale
A one-pixel difference vanishes on large documents, so the lamp
collapsed to a flat multiply. Sample S at ±σ and keep a 256 px
sphere direction check so that does not regress.
* i18n: German entry for Filter › Render › Relight
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Brazilian Portuguese entry for Filter › Render › Relight
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>
Engine (scripts, the control channel and agents can drive it):
- `select.float {"dx","dy"}`: cuts the selected pixels of the active layer
into a floating piece the first time, then moves it by whole pixels.
The document is unchanged while it floats (`DocState::floating`);
`float_cmds::displayed` gives the document with the piece at its offset.
- `select.drop`: drops the piece into its layer and moves the selection
with it, one history step ("Move Selected Pixels").
- Any other command drops a floating piece first (a hook at the top of
dispatch, so it lands where it was shown); Undo puts it back instead.
- The layer is split once, only inside the selection, and the piece is
cropped to it: placing it costs the piece, not the layer (6000×4000
layer, 500×400 piece: 18 ms to cut, under 5 ms per move).
Canvas (Marquee / Lasso):
- A drag inside the ants moves the outline (`select.transformSelection`);
a click inside without moving deselects; ⇧ / ⌥ drags still draw.
- ⌘-drag floats the selected pixels and shows them at the pointer live;
plain drags on the piece move it again; a press elsewhere drops it.
- A move cursor over the ants; the ants follow the dragged / floating piece.
Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* codecs: decode deep OpenEXR (deepscanline/deeptile), composited flat or structured
Deep EXR files errored with "not supported: deep data not supported yet"
(the `exr` crate rejects deep headers, and its chunk framing for them is an
unfinished guess), so the chunk stream is walked directly from the bytes, per
the OpenEXR file-layout specification:
- offset tables after the headers; deep chunks of [y | tile coords, packed
table size, packed data size, unpacked data size, compressed cumulative
sample-count table, compressed sample data],
- NONE, RLE and ZIPS (zlib, the +128 difference predictor and the half-split
interleave - applied to the count table and the sample data alike; a stored
payload of uncompressed length is raw). ZIP-16 and mip/rip-map deep tiles
decline with `unsupported`,
- samples are channel-grouped, per pixel; u32 counts cap the allocation and
go through `Limits` before anything is held in memory.
`decode` composites to the flat image (samples sorted by Z, front-to-back
`over` with premultiplied colour, un-premultiplied to straight alpha) and
reports a new DecodeWarning::DeepFlattened; `decode_deep_exr` returns the
structured DeepImage (per-channel samples plus cumulative counts) for callers
that composite deep data themselves. exr::Error::NotSupported now maps to
CodecError::unsupported instead of malformed, like the TIFF codec.
Validated against the OpenEXR repository's deep test images (11/42/64/objectid
.deep.exr: 256x256, up to 8 samples/pixel, plausible z and alpha), and covered
by a synthetic generator (NONE/RLE/ZIPS x scanline/tile) with hand-computed
compositing oracles, truncation sweeps and hostile offsets/sizes (13 tests).
* Deep EXR: match the OpenEXR layout (RLE tokens and predictor, per-row tile tables and data), keep HDR
Checked against deep files written by the OpenEXR 3.5 library (scan line and tiled,
NONE/RLE/ZIPS): RLE tokens were inverted and skipped the predictor and byte split, the
count table of a tile restarts at every row, a tile's sample data is grouped per row, edge
tiles may store a full-size table, and the flattened colour was clamped to 1.0. The
synthetic writer in the tests now follows the same layout; new HDR / edge-tile test.
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>
* Apply the Interface UI Font Size preference
* Regenerate the scorecard after rebase (58 unread settings); drop the before/after screenshots
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>
With allLayers: false the search keeps only the active layer; when that
layer isn't a type layer the list is empty, and the find loop indexed
layers[0] on it (find, change and changeFind). Read the layer with
get() and stop when there is none, so the command finds nothing and
returns {"found": null}, as it does when nothing matches.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#663 and #700 each added one entry to the complete-catalog list in
source_key_set_is_covered_by_complete_catalogs. Each line passed rustfmt on
its own branch; together they exceed the width limit, so cargo fmt --check
fails on main and on every PR updated to main. Formatting only.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(image): Arbitrary rotation starts at the ruler's straightening angle
Fixes#602
* Arbitrary rotation: ignore a non-finite ruler angle (from #659)
Co-Authored-By: Alex Gorevski <alex.gorevski@live.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Alex Gorevski <alex.gorevski@live.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A run with end before start reached style_range with a > b, and the
styled run's length (b - a) underflowed: a panic in debug builds (the
dispatch guard turned it into a generic internal error), a huge stored
run length in release.
type.edit now checks every run before changing anything and rejects
end < start with a BadParams error naming the run, so the layer is left
untouched and no history step is recorded. style_range also treats a
reversed range as empty, so no other caller can underflow it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Fix#695: validate Layer Style effect fields
Reject non-object effect parameter fields before preview or confirmation, render a visible validation message, and keep the canvas on the original document when preview input is invalid.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
* fix(ui): keep global light preview fields compatible
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
* Layer Style: show an invalid-parameters error once
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>
Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
Resampling read the pixels beyond a layer's content as the default pixel
(transparent for layers, white for masks). For content that reaches the
canvas edge, such as a Background or any full-canvas layer, the filter
taps past the border mixed that in, so after Image Size the outermost
rows and columns were translucent: a white Background enlarged 200 %
came out with alpha 162/255 in the corners and 203/255 along the edges.
Export As at any scale other than 100 %, and Generate Image Assets at
200 %, run Image Size first, so every such export had a translucent
frame. Channels, the quick mask and the selection faded the same way.
Image Size now resamples layers, masks, channels and the selection with
resize_surface_in_canvas: taps past the canvas edge (or past a layer's
own pixels where they extend beyond the canvas) read the nearest edge
sample. Inside the canvas nothing changes, so a layer away from the edge
keeps its soft border. resize_surface itself, used by transforms and
placing, is unchanged.
Tests: canvas_edges_repeat_instead_of_fading (algo: every filter, up and
down, 8, 16 and 32 bits; inner layers unchanged) and
image_size_keeps_canvas_edges_opaque (engine; fails before: corner alpha
0.635), which also checks a full selection stays full to the edge.
Co-authored-by: EthanSuquet <292885762+EthanSuquet@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Improve Korean Photoshop terminology and dynamic UI translations
* Keep untranslated form labels in sentence case; add the new strings to de and pt-br
label_of fell back to the Title Case filter label whenever the sentence-case label had no
translation, which changed English form labels ("Use antialias" -> "Use Antialias"). It now
uses the Title Case translation only when one exists. German and Brazilian Portuguese (merged
since this branch was opened) get "No patterns" and "Enable {name}".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: kijoong <gijoong.gim@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A JPEG holds XMP in one APP1 segment, at most 65,533 bytes. Documents
from the source application often carry far more: their XMP lists every
document ever placed in them (DocumentAncestors, thousands of ids) and
the text of every type layer (TextLayers). The encoder passed the packet
through whole, the segment was refused, and the export failed with "App
segment exceeds maximum allowed data length of 65533". That stopped
every JPEG path: Export As, Save a Copy, convert, Layers and Artboards to
Files, Batch and Image Processor. 3 of 16 real designer documents in a
sweep hit it.
When the packet doesn't fit, the encoder now leaves out those two
layered-document properties (any namespace prefix), which a flat image
doesn't need; everything else in the packet is kept. If it still
doesn't fit, the XMP is left out, and so is an EXIF block over 64 KB.
Both cases are reported ahead of time by a new fidelity warning,
MetadataTooLarge, so the export shows what was dropped. Formats without
a segment limit (PNG, TIFF, WebP) still keep the packet whole.
Test: jpeg_with_oversized_metadata_still_encodes (fails before: encode
error), at 8, 16 and 32 bits.
Co-authored-by: EthanSuquet <292885762+EthanSuquet@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Canonicalize imported bounds before rendering or moving range handles. Keep drag constraints ordered and cover both non-canonical and ordinary ranges.
Fixes#696
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
* Fix multi-file open dialog (#595)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
* Multi-file open test: write its fixtures under the temp dir
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>
Copilot-Session: e77cd34a-ce3e-4981-996f-4d37d49df90d
PSD type stores a line break inside a paragraph (Shift+Return) as U+0003,
and the layer text keeps it as is. The layout passed it to the line
breaker as an ordinary character: it isn't a break opportunity there, so
the line ran on, and the font has no glyph for it, so a missing-glyph box
was drawn in its place. Nothing showed until the text was re-laid out,
because an opened layer draws its saved pixels; any edit of the text, an
Image Size or an export at another scale then turned a two-line headline
into one long line with a box in the middle.
The layout now hands U+0003 to the line breaker as a newline (same byte
length, so every text offset stays put): the line ends, the next one
starts with the paragraph's leading and no paragraph spacing, and no
glyph is drawn. The text itself is unchanged, so PSD save writes it back.
Test: forced_line_break_starts_a_line_in_the_same_paragraph (fails
before: one line), covering point, centred and box text and rendering at
8, 16 and 32 bits.
Co-authored-by: EthanSuquet <292885762+EthanSuquet@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Rect::from_xywh(i32::MAX, y, 1, 1) saturates to an empty rect, so the composite has no pixels and .px[0] panicked. Coordinates are also cast with as i32, so 2^32 silently read column 0. Read the coordinates with i32::try_from (BadParams when out of range) and return transparent for the empty rect, which is what any other off-canvas pixel reads as.
- Edit › Transform › Skew, Distort and Perspective now set a mode on the box
instead of just starting Free Transform: in Distort a corner moves alone
with no keys held (Skew: edges skew; Perspective: corners move in mirrored
pairs). While a box is up they switch its mode, as does a right-click on
the canvas (Free Transform, Scale, Rotate, Skew, Distort, Perspective),
with the current mode checked.
- Distort moves only corners (or the whole box from inside): dragging
outside doesn't rotate, edge handles don't move, nothing snaps; the cursor
shows when a drag would do nothing.
- Corner drags stay proportional by default (⇧ frees them). Preferences ›
General › Use Legacy Free Transform (already a setting, hidden until now)
swaps that: free by default, ⇧ keeps proportions.
- The handle under a press is hit-tested where the press was, before
snapping, which could push it off a corner and move the whole box.
Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Typing 1280*2 or 20*2+5-2 into a numeric field applies the result on
Enter, Tab or click-away (+ - * /, * and / first). Plain numbers still
apply as they're typed. Arithmetic is held until it's committed, so a
caller that rounds a partial result can't cut it short, and changed()
now means a new value rather than a keystroke.
The floating Properties card scopes its fields per layer, so text still
being typed for one layer can't commit to the next.
Fixes#601
* fix: Color panel chips pick the colour to edit and open the Color Picker on double-click
A click on the foreground or background chip makes the Color panel's field and hue
strip edit that colour; a double-click opens the Color Picker on it, with its RGB,
HSB, Lab, CMYK and hex fields. Each chip remembers its own hue for greys.
Fixes#655
* refactor(ui): keep the Color panel's edited chip in UiState
The chip the Color panel edits is UiState::color_panel.background, so the
control channel and MCP can read it and set it with
ui.set {colorPanel: {background}}. The double-click test now clicks where
the chips overlap, where the foreground chip takes the click.
Menus:
- Titles open their menu on the press and never toggle on the release (no
blink), so one gesture can press a title, drag to an item and release on
it to run it. A plain click still opens the menu and leaves it open;
pressing an open title closes it.
Title bar:
- The window draws under a transparent title bar, so AppKit dragged it from
anywhere in that strip, menus included. AppKit's own title-bar dragging is
off (NSWindow.movable = NO via safe objc2 calls, no unsafe; objc2 crates
pinned to the versions already in Cargo.lock), and the app's drag region
is only the free gap between the menus and the right-hand controls.
Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(linux): AppImage registers its desktop entry and icon on launch
On Wayland the compositor resolves the dock icon from the window's app_id
(ai.storyteller.photocraft, already set) against a host-installed .desktop
file. An AppImage installs nothing, so every run showed the generic Wayland
logo (#593). The app cannot set the icon itself: winit 0.30.13 (egui 0.36.2's
pin) has an empty set_window_icon on Wayland; xdg-toplevel-icon-v1 landed on
winit's 0.31 line only.
- packaging/linux/AppRun is a script instead of a symlink: best-effort
integration into $XDG_DATA_HOME before launch (desktop entry with Exec
rewritten to the AppImage's path and TryExec dropped, hicolor icons
copied), content-compared so an unchanged install costs a few cmp calls,
refreshed when the AppImage moves, PHOTOCRAFT_NO_DESKTOP_INTEGRATION=1
opts out, and no failure can block the launch (best-effort steps silent).
- packaging/linux/apprun-test.sh: 8-case smoke (passthrough, Exec rewrite
with spaces, metadata preserved, idempotence, move refresh, opt-out,
unwritable data dir, extracted AppDir). packaging-lint runs it and now
shellchecks AppRun.
- Docs: releasing.md Linux section; README install paragraph.
Fixes#593
* AppRun: skip desktop integration when the AppImage path needs Exec escaping
A quote, backquote, dollar, backslash, percent or line break in the path would need
desktop-entry escaping (a line break could even add keys), so integration is skipped
and the app still launches.
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>
* Brazilian Portuguese UI translation (pt-br)
Add a pt-br catalog covering every key in the existing catalogs (2,492
entries), register it as "Português (Brasil)" with Portuguese plural
rules, and resolve pt, pt-BR and pt-PT locales to it. i18n coverage
1404/1404.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keyboard Shortcuts dialog: show menus and commands in the UI language
The command list showed the raw English menu paths and labels while the
rest of the dialog was translated. Translate them the way the menus do
(tr / tr_id); search matches the translated and the English names, like
the command palette. Also rustfmt the pt-br registry row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* pt-br: gate it in xtask i18n-coverage (from #637, thanks @RicSchonfelder); licence note covers German too
Co-Authored-By: RicSchonfelder <RicSchonfelder@users.noreply.github.com>
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>
Co-authored-by: RicSchonfelder <RicSchonfelder@users.noreply.github.com>
Isolates the subject of a pixel layer in one undoable step: a layer mask
by default, with optional transparency bake and a solid fill underneath.
The command is available from Properties Quick Actions, the Layer menu,
the CLI, and the control channel.
* Surface Blur: sliding 8-bit histogram instead of the full window sum
Surface Blur summed the whole (2r+1)^2 window for every pixel and channel, so radius 100 read about 40,000 samples per pixel. For 8-bit layers (every colour sample k / 255) keep a 256-bin histogram per channel that slides along the row, one column out and one in per step, and weight only the levels within the threshold, with the same weight expression as the direct sum. 16-bit and float layers, and radii where the direct sum is cheaper, keep the direct sum (now surface_direct).
* Surface Blur test: as_chunks_mut (clippy)
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>
The square Minimum and Maximum scanned 2r+1 samples per pixel on each axis, so the cost grew with the radius (0.2..500). Preserve: Roundness already uses the van Herk / Gil-Werman running_extreme in the same file; use it for both passes of the square too. A min or max picks one of the same samples whatever the order, so the output is bit-identical; a new test checks it against the direct window.
- The eye column takes drags: pressing an eye toggles that layer and every
row the pointer then sweeps over gets the same visibility (each row once
per drag). A drag that starts on an eye no longer reorders the layers.
- A hidden layer's eye box is left empty instead of drawing a struck-out
eye; it is still clickable.
Co-authored-by: Jason Short <3344856+jason4short@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Box Blur ran a direct separable kernel of 2r+1 taps per pixel per axis, so its cost grew linearly with the radius (1..2000). Gaussian Blur already blurs with running-sum box passes (gaussian_boxes); generalise that into box_passes(widths) and use it for Box Blur radii above 4, keeping the direct kernel for radius 1..4 where it is quicker and output stays as before.
The command palette and the channel rename field called request_focus on
every frame. egui interrupts IME composition on each request, so egui-winit
disabled and re-enabled zwp_text_input_v3 every frame. With an input method
such as fcitx5 the compositor regrabbed the keyboard each time, a key release
landing in the gap was lost, and winit's client-side repeat never stopped.
Both fields now request focus only when they do not have it, matching the
other rename fields. Tests assert the IME is not interrupted after the first
frame.