PhotoCraft scorecard
Generated by cargo xtask scorecard from perf/budgets.toml, perf/baseline.json, crates/io/tests/corpus.rs, scorecard/*.toml and the source tree. Do not edit by hand: change the sources and regenerate (CI runs cargo xtask scorecard --check). Checklist statuses are verified against the code; numbers here supersede estimates elsewhere.
Summary
| Area |
Done |
Partial |
Missing |
Key numbers |
| Performance |
3 |
22 |
12 |
46 scenarios: 3 within budget, 22 over budget or failing, 13 tracked only, 4 not run, 8 not measurable yet (baseline 2026-10-05) |
| File compatibility |
1 |
8 |
18 |
io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 229/309, round trip ≥ 307/309; photoshop oracles: oracle ≥ 130/256, round trip ≥ 258/256 |
| Tools |
3 |
14 |
13 |
|
| Workspace and UI |
0 |
4 |
19 |
settings that do nothing: 60 of 133 (target 0) |
| Type |
0 |
3 |
6 |
|
| Automation |
1 |
3 |
7 |
626 of 626 menu items live (100.0%) |
| Reliability |
3 |
1 |
3 |
never-crash lints on 26 of 26 crates; panic_hunt present |
| Distribution |
4 |
3 |
4 |
|
| Open bugs |
0 |
3 |
29 |
|
Performance
Budgets: perf/budgets.toml (targets from #209, #210, #211). Numbers: the committed baseline (perf/baseline.json, full run), updated only by cargo xtask perf --update-baseline. The nightly perf-nightly workflow runs cargo xtask perf on a fixed macOS runner and fails on a broken enforced budget or a p50 regression over 15 % against a baseline from the same machine class. "Over budget" rows are targets the code doesn't meet yet: they are reported, and only their regressions fail.
Baseline: 2026-10-05, commit f5d2bcb54a, Apple M4 Pro (48 GB), GPU Apple M4 Pro, macOS 15.6; load average 224.90 / 226.31 / 196.03 (1/5/15 min) at the start of the run.
| Id |
Scenario |
Target |
p50 ms |
p95 ms |
max ms |
Peak RSS |
GPU |
Status |
Issue |
| P1 |
Move a layer with layer styles (per drag update) |
≤ 25 ms p95 |
321 |
535 |
535 |
825 MB |
409 MB |
over budget (target not met yet) |
#209 |
| P2 |
Change layer opacity |
≤ 20 ms p95 |
276 |
443 |
443 |
639 MB |
409 MB |
over budget (target not met yet) |
#209 |
| P3 |
Change layer fill |
≤ 20 ms p95 |
279 |
467 |
467 |
506 MB |
409 MB |
over budget (target not met yet) |
#209 |
| P4 |
Change blend mode |
≤ 20 ms p95 |
206 |
423 |
423 |
579 MB |
409 MB |
over budget (target not met yet) |
#209 |
| P5 |
Toggle visibility |
≤ 10 ms p95 |
178 |
425 |
425 |
668 MB |
409 MB |
over budget (target not met yet) |
#209 |
| P6 |
Undo a move |
≤ 25 ms p95 |
256 |
530 |
530 |
650 MB |
409 MB |
over budget (target not met yet) |
#209 |
| P7 |
Redo a property change |
≤ 25 ms p95 |
144 |
212 |
212 |
669 MB |
409 MB |
over budget (target not met yet) |
#209 |
| P8 |
Duplicate a 12 MP layer (first) |
≤ 100 ms p95 |
508 |
699 |
699 |
907 MB |
919 MB |
over budget (target not met yet) |
#209 |
| P9 |
Duplicate a 12 MP layer (repeat) |
≤ 20 ms p95 |
406 |
627 |
627 |
749 MB |
919 MB |
over budget (target not met yet) |
#209 |
| P10 |
Paste a 12 MP layer (first) |
≤ 100 ms p95 |
1232 |
3136 |
3136 |
1.3 GB |
1.4 GB |
over budget (target not met yet) |
#209 |
| P11 |
Paste a 12 MP layer (repeat) |
≤ 20 ms p95 |
445 |
1174 |
1174 |
958 MB |
1.4 GB |
over budget (target not met yet) |
#209 |
| P12 |
Arrow-key nudge in a 150-layer document |
≤ 30 ms p95 |
– |
– |
– |
– |
– |
missing: panicked: wgpu error: Validation Error / Caused by: / In Queue::write_buffer / Buffer with 'pc_compose_uniforms' label is invalid |
#209 |
| P13 |
Brush dab on a 15000×10000 16-bit document |
≤ 80 ms p95 |
6.42 |
146 |
146 |
839 MB |
2.0 GB |
over budget (target not met yet) |
#209 |
| P14 |
Move a layer on a 15000×10000 16-bit document |
≤ 80 ms p95 |
2698 |
4318 |
4318 |
1.5 GB |
2.0 GB |
over budget (target not met yet) |
#209 |
| P16 |
Brush dab on a 32-bit document with 20 layers |
≤ 100 ms p95 |
21.5 |
161 |
161 |
793 MB |
1.6 GB |
over budget (target not met yet) |
#209 |
| P17 |
Move a layer on a 32-bit document with 20 layers |
≤ 100 ms p95 |
386 |
681 |
681 |
1.2 GB |
1.6 GB |
over budget (target not met yet) |
#209 |
| P18 |
Open a 20 MP layered PSD: first pixels (today: fully opened) |
≤ 1000 ms p95 |
1753 |
2794 |
2794 |
2.2 GB |
1.9 GB |
over budget (target not met yet) |
#210 |
| P19 |
Open a 20 MP layered PSD: editable |
≤ 3000 ms p95 |
1753 |
2794 |
2794 |
2.2 GB |
1.9 GB |
pass |
#210 |
| P21 |
Content-Aware Fill on 10 MP |
≤ 1500 ms p95 |
218 |
256 |
256 |
1.8 GB |
1.8 GB |
pass |
#210 |
| P22 |
Spot Healing stroke on 10 MP |
≤ 500 ms p95 |
157 |
247 |
247 |
1.8 GB |
1.9 GB |
pass |
#210 |
| P24 |
Import 500 brushes (longest UI-thread step) |
≤ 100 ms max |
– |
– |
– |
– |
– |
not run |
#210 |
| P25 |
Cancel a Gaussian Blur job on 24 MP (until the worker exits) |
≤ 200 ms max |
– |
– |
– |
– |
– |
not run |
#210 |
| P49 |
Cancel a Content-Aware Fill job on 24 MP (until the worker exits) |
≤ 200 ms max |
– |
– |
– |
– |
– |
not run |
#210 |
| P50 |
App frame time while a 24 MP Gaussian Blur job runs |
≤ 16 ms p95 |
– |
– |
– |
– |
– |
not run |
#210 |
| P26 |
Select › Modify › Smooth on 24 MP (radius 50) |
≤ 100 ms p95 |
99.9 |
216 |
251 |
1.0 GB |
– |
over budget (target not met yet) |
#211 |
| P27 |
Select › Modify › Feather on 24 MP (radius 50) |
≤ 100 ms p95 |
122 |
357 |
360 |
1.1 GB |
– |
over budget (target not met yet) |
#211 |
| P28 |
Content-Aware Scale 24 MP (to 90 % width) |
≤ 3000 ms p95 |
954192 |
954192 |
954192 |
2.1 GB |
– |
over budget (target not met yet) |
#211 |
| P29 |
Select Subject on 24 MP, repeated |
≤ 100 ms p95 |
559 |
693 |
693 |
1.4 GB |
– |
over budget (target not met yet) |
#211 |
| P33 |
Brush dab on A4 300 ppi CMYK (dab to screen) |
≤ 6 ms p95 |
34.7 |
235 |
235 |
775 MB |
87 MB |
over budget (target not met yet) |
#211 |
| P40 |
Brush dab 60 px on 36 MP |
track only |
1.45 |
3.19 |
3.19 |
1.3 GB |
– |
tracked |
|
| P41 |
Gaussian Blur r 40 on 36 MP |
track only |
2963 |
3069 |
3069 |
1.9 GB |
– |
tracked |
|
| P42 |
Curves dialog change, layer preview, 36 MP |
track only |
38.7 |
158 |
158 |
1.1 GB |
– |
tracked |
|
| P43 |
Save .psd, 36 MP, 3 layers |
track only |
3047 |
3446 |
3446 |
1.2 GB |
– |
tracked |
|
| P44 |
Open .psd, 36 MP, 3 layers |
track only |
3535 |
4547 |
4547 |
1.6 GB |
– |
tracked |
|
| P45 |
Full refresh with layer effects, warm caches, 36 MP (GPU) |
track only |
105 |
160 |
160 |
714 MB |
– |
tracked |
|
| P46 |
Brush dab on a layer with effects, 36 MP (GPU) |
track only |
30.6 |
83.3 |
83.3 |
863 MB |
– |
tracked |
|
| P47 |
Font-size drag step, 50 type layers |
track only |
23.3 |
60.5 |
71.2 |
600 MB |
– |
tracked |
|
| P48 |
Full refresh of a 14000×14000 document |
track only |
8715 |
9897 |
9897 |
– |
– |
tracked |
|
Not measurable yet (the code can't do it yet; no number is recorded):
| Id |
Scenario |
Target |
Why |
Issue |
| P15 |
Zoom / pan frame time on a 15000×10000 16-bit document |
≤ 16 ms per frame |
no headless canvas frame harness yet (zoom and pan sample the GPU texture in the egui paint callback) |
#209 |
| P20 |
Open a 2 GB PSD / 4–5 GB PSB |
opens with progress, editable, never blank |
opens run in the background with progress (#210), but there are no generated large-file fixtures yet (#216) |
#210 |
| P23 |
Warp / Puppet / Perspective preview at 4K |
≤ 16 ms per frame, commit in the background |
the previews run inside the UI tools; no headless frame harness, no background commit |
#210 |
| P30 |
Liquify 800 px brush on 3500×2200 at full resolution |
≤ 1 ms per update |
the Liquify proxy is capped at 1600 px and the stroke path lives in the UI tool |
#211 |
| P31 |
Camera Raw exposure change (proxy) |
≤ 10 ms |
the proxy is built inside the dialog; no headless entry point yet |
#211 |
| P32 |
Brush release on a long stroke |
no hitch at mouse-up |
the live renderer and the release re-render are UI-side; needs a frame harness |
#211 |
| P34 |
Brush preview memory, small dab on an 8K viewport |
≤ 2 MiB |
the live brush preview buffers are UI-side; no headless counter yet |
#211 |
| P35 |
Marquee drag on 24 MP with Alt/Shift |
≤ 17 ms p95 |
Shift/Alt marquee modifiers don't exist yet (#208) and the drag preview is UI-side |
#211 |
File compatibility
Corpus floors (crates/io/tests/corpus.rs): the enforced minimums; the corpus tests (cargo feature corpus, cargo xtask test-corpus, always run in CI) fail below them.
| Corpus |
Directory |
Files |
Oracle pass floor |
Round-trip floor |
| io corpus |
corpus/psd |
170 |
146 |
169 |
| psd-tools corpus |
corpus/psd-tools |
309 |
229 |
307 |
| photoshop oracles |
corpus/photoshop |
256 |
130 |
258 |
| Id |
Target |
Status |
Issue |
Note |
| FILE-215-1 |
Smart Objects export as embedded Smart Objects (SoLd + lnk2), not flat pixels |
done |
#215 |
psd_export.rs smart_blocks: PlLd + SoLd, source in lnk2 (.pcraft -> PSB); engine tests/smart_psd.rs; Photoshop 2026 opens 8/16-bit exports as live smart objects. Linked files are embedded |
| FILE-215-2 |
Smart Filters survive PSD save and re-open (filterFX descriptor read and written) |
partial |
#215 |
smart_map.rs: 14 filters mapped both ways, others kept verbatim, mask in FEid; io tests/smart_corpus.rs. PhotoCraft-only filters (Twirl, Liquify…) are left out of filterFX with a warning |
| FILE-215-3 |
Filter mask can be painted directly, with undo |
missing |
#215 |
smart_cmds.rs:382 mask only from the selection |
| FILE-215-4 |
Adjustments (Hue/Sat + Colorize, Curves, Levels, Shadows/Highlights) apply as Smart Filters |
partial |
#215 |
PSD Levels/Curves/Shadows-Highlights smart filters render (adjust_cmds.rs adjust_as_filter); Image › Adjustments on a smart object doesn't add one yet |
| FILE-215-5 |
Smart Filter rows: collapse, mask thumb, blend icon, drag reorder, context menu, live preview |
partial |
#215 |
smart_ui.rs:31 rows + visibility + editor; no collapse, drag or context menu |
| FILE-215-6 |
Smart Object corner skew and perspective (quad or homography transform) |
missing |
#215 |
doc lib.rs:312 SmartObject.transform is an Affine |
| FILE-215-7 |
Layered TIFF read and write (tag 37724), honouring the ask-before-saving pref |
missing |
#215 |
No 37724 / ImageSourceData in crates |
| FILE-215-8 |
Large and BigTIFF import with banded decode across all IFDs |
missing |
#215 |
codecs tiff.rs:3 'Only the first IFD is read' |
| FILE-215-9 |
SVG opens and places as shape layers or a vector Smart Object |
missing |
#215 |
No SVG codec or import |
| FILE-215-10 |
Multi-page PDF save, PDF presentation, layer comps to PDF, vector/text output, PDF import |
missing |
#215 |
Only print_cmds.rs:133 print_pdf (raster print page) |
| FILE-215-11 |
RAW: lossy DNG, highlight modes, .xmp sidecars, RAW prefs wired, per-camera corpus |
missing |
#215 |
raw sensor.rs:210 lossy DNG unsupported; raw_defaults unread; no xmp |
| FILE-215-12 |
Camera Raw dialog shows a histogram; settings persist as a PSD Smart Filter |
partial |
#215 |
No histogram; lens_cmds.rs:268 stores an in-app Smart Filter, PSD drops it |
| FILE-215-13 |
Video layer frames persist in .pcraft; PSD video layers mapped |
partial |
#215 |
format convert.rs:89 + tests/roundtrip.rs:282 persist frames; no PSD video mapping |
| FILE-215-14 |
User swatch model with ACO/ASE import and export |
missing |
#215 |
panels.rs:954 const SWATCHES: [[u8; 3]; 40] |
| FILE-215-15 |
Layer Style contour picker, curve editor, presets, .shc import/export |
missing |
#215 |
doc effects.rs:57 Contour model only |
| FILE-215-16 |
Advanced Blending (knockout, blend as group, mask hides effects) end to end |
missing |
#215 |
No knockout / blend_interior / transparency_shapes in doc, compose or io |
| FILE-215-17 |
CMYK/RGB documents preview spot inks; spot plates export (TIFF extra samples, PDF) |
missing |
#215 |
compose multichannel.rs:1 spot preview for Multichannel only |
| FILE-215-18 |
Save for Web per-slice settings remembered; Export As remembers and offers nearest scaling |
partial |
#215 |
file_ui.rs:188 last_web in-session only; export_dialog.rs:16 fixed defaults |
| FILE-215-19 |
Save As JPEG opens a JPEG Options dialog (quality 0-12, format options) |
missing |
#215 |
ExportSettings has jpeg_quality 1-100 only (lib.rs:118) |
| FILE-215-20 |
Artboard tool with handles and + buttons, Duplicate Artboard beside, vector PDF pages |
partial |
#215 |
artboard_ui.rs:1 names/properties only; no tool, + buttons or duplicate |
| FILE-215-21 |
Local libraries of colours, patterns, images and layers, as linked items or copies |
missing |
#215 |
Only preset libraries (preset_store.rs) |
| FILE-216-1 |
Every corpus fetched and enforced in CI, cached by pin |
missing |
#216 |
No corpus step in ci.yml; in progress in #190 |
| FILE-216-2 |
Large-file fixtures: 20 MP, 150-layer, generated 2 GB PSD and 4-5 GB PSB stress tests |
missing |
#216 |
Only banded.rs covers a >8 MP synthetic export |
| FILE-216-3 |
TIFF, RAW and EXR corpora read by tests; per-camera RAW coverage reported |
missing |
#216 |
corpus/tiff, raw and exr only listed by xtask corpus; no test reads them |
| FILE-216-4 |
Exports re-open in a third-party oracle editor |
missing |
#216 |
The round trip is our export -> our import |
| FILE-216-5 |
Save-path tests: layered TIFF, PDF, SVG, ACO/ASE, .shc, Smart Filters, atomic-save failure |
missing |
#216 |
Features absent; no atomic-save failure-injection test |
| FILE-216-6 |
30-50 end-to-end workflow acceptance tasks driven through MCP or commands |
partial |
#216 |
automation/tests/agent_tasks.rs: 10 MCP tasks |
Tools
| Id |
Target |
Status |
Issue |
Note |
| TOOL-213-1 |
Pencil tool: aliased 1 px painting, square pixel-aligned cursor |
done |
#213 |
Tool::Pencil (B group): live aliased strokes on the pixel grid, square cursor, Auto Erase; pencil_tests.rs |
| TOOL-213-2 |
Mixer Brush tool on the toolbar |
partial |
#213 |
paint.mixerBrush (brush_cmds.rs:337) + panel options; no Tool variant |
| TOOL-213-3 |
Patch tool with a live preview of the patched area |
missing |
#213 |
No patch command or tool |
| TOOL-213-4 |
Content-Aware Move tool reusing inpaint completion |
missing |
#213 |
inpaint::complete only used by spot healing (retouch_cmds.rs:386) |
| TOOL-213-5 |
Painting a type/shape/smart/fill layer offers to rasterize, then continues the stroke |
done |
#213 |
rasterize_prompt.rs: OK rasterizes then paints (two history states), Cancel does nothing; drivable via ui.dialog.confirm |
| TOOL-213-6 |
Fill dialog: contents, blend, opacity, preserve transparency; remembered; Shift+Backspace |
done |
#213 |
fill_ui.rs dialog over edit.fill (fill_cmds.rs): all contents, mode, opacity, preserve; remembered in prefs; Shift+F5 / Shift+Backspace |
| TOOL-213-7 |
Live stroke preview for every brush-based retouch tool |
missing |
#213 |
Live stroke only for Brush and Eraser (canvas.rs:1684) |
| TOOL-213-8 |
Clone Source: W/H link, drawn overlay (opacity/blend/clip/auto-hide), Alt target cursor |
partial |
#213 |
Panel W/H + overlay settings (preset_panels.rs:946-1015); no link, overlay never drawn |
| TOOL-213-9 |
Eyedropper comparison ring (new colour above, current below) while sampling |
missing |
#213 |
sample_eyedropper (canvas.rs:1552) draws no ring |
| TOOL-213-10 |
Ctrl+Alt drag resizes brush size/hardness with an on-canvas readout |
missing |
#213 |
Only [ ] resize (shortcuts.rs:270) |
| TOOL-213-11 |
Editable pressure curve, global and per control |
missing |
#213 |
No pressure curve in engine, paint or ui-egui |
| TOOL-213-12 |
Import an image file as a brush tip |
missing |
#213 |
Only brush.defineFromSelection and brush.presets.importAbr |
| TOOL-213-13 |
Brush regression tests: no seams, hard edge, zero-scatter defaults, ABR never black |
partial |
#213 |
Missing-pattern ABR test (abr_map/tests.rs:181), hardness test; no seam/black tests |
| TOOL-213-14 |
Tablet: no OS arrow cursor over the canvas on macOS, Windows and Linux |
partial |
#213 |
Brush tools set CursorIcon::None (canvas.rs:1259-1275); not verified per OS |
| TOOL-214-1 |
Select and Mask workspace: view modes, Refine Edge brush, 6 outputs |
partial |
#214 |
Opens the generic select.refineEdge dialog (menus.rs:308) |
| TOOL-214-2 |
Marquee: Shift square, Alt from centre, live W x H readout |
partial |
#214 |
marquee_end supports shift (chrome_ui.rs:167) but canvas.rs passes false (#208) |
| TOOL-214-3 |
Color Range test: Sampled Colors at Fuzziness 0 selects the exact match only |
partial |
#214 |
Fuzziness-0 tests cover midtones/shadows only (selection_cmds.rs:444-447) |
| TOOL-214-4 |
Move tool: box-select layers, Auto-Select hover outline, double-click fit, drag to a tab |
missing |
#214 |
move_mods.rs covers Shift/Alt/arrows only |
| TOOL-214-5 |
Right-click menu during Free Transform (scale, rotate, skew, warp, flip, rotate 90/180) |
missing |
#214 |
No context menu in canvas.rs or transform_tool.rs |
| TOOL-214-6 |
Puppet Warp mesh follows the object outline (constrained triangulation) |
missing |
#214 |
algo/puppet.rs:136 builds a block-grid mesh |
| TOOL-214-7 |
Content-Aware Scale through the Free Transform box with a live proxy preview |
partial |
#214 |
edit.contentAwareScale (edit_menu_cmds.rs:899) behind a parameter dialog |
| TOOL-214-8 |
A measured ruler angle prefills Image Rotation > Arbitrary (0.01 deg) |
missing |
#214 |
Arbitrary rotation is a generic dialog; ruler only feeds straighten |
| TOOL-214-9 |
Crop: W x H x Resolution with units, saved crop presets, Straighten in the options bar |
partial |
#214 |
Ratio dropdown and fields only (panels.rs:551-579) |
| TOOL-214-10 |
Pen: Alt breaks a handle, Shift 45 deg, Ctrl direct-selects, Backspace, close-drag handle |
missing |
#214 |
pen_down gets no modifiers (canvas.rs:1632) |
| TOOL-214-11 |
Stroke Path dialog: pick any brush-based tool, Simulate Pressure |
partial |
#214 |
path.stroke brush, pencil, eraser (vector_cmds.rs:1170); brush-only button, no dialog |
| TOOL-214-12 |
Shapes: on-canvas corner radius handles, shape feather, selection to shape with tolerance |
partial |
#214 |
select.convertToShape with tolerance (vector_cmds.rs:1256); radius as a field only |
| TOOL-214-13 |
Mask Properties: Density and Feather controls |
partial |
#214 |
Vector-mask density/feather in the engine (vector_cmds.rs:948); no UI controls |
| TOOL-214-14 |
Vanishing Point window: draw and edit planes, paste, clone in perspective |
partial |
#214 |
filter.vanishingPoint (vp_cmds.rs:15) behind a parameter dialog |
| TOOL-214-15 |
Content-Aware Fill workspace: paint the sampling area, preview, rotate/scale/mirror, new layer |
partial |
#214 |
Engine sampling auto, rect, custom (edit_menu_cmds.rs:324); generic dialog |
| TOOL-214-16 |
Liquify: redo (Cmd+Shift+Z) |
missing |
#214 |
liquify_ui.rs has undo only |
Workspace and UI
Settings that do nothing: 60 of 133 (target 0, #204). Counted by the prefs audit (xtask/src/scorecard.rs): a Preferences field counts as read when code outside the prefs model (prefs.rs except its impl Session blocks; tests excluded; the schema-driven dialog never names a field) accesses section.field, or .section and .field in the same file, or calls a section method that reads self.field. It is a heuristic lower bound: a generic name can look read when it isn't.
Unread: general.color_picker, general.beep_when_done, general.export_clipboard, general.resize_image_during_place, general.always_create_smart_objects_when_placing, general.animated_zoom, general.zoom_resizes_windows, general.use_legacy_free_transform, interface.ui_font_size, interface.show_channels_in_color, interface.dynamic_color_sliders, workspace.auto_collapse_icon_panels, workspace.auto_show_hidden_panels, workspace.open_documents_as_tabs, workspace.enable_floating_document_window_docking, workspace.large_tabs, workspace.enable_narrow_options_bar, tools.zoom_clicked_point_to_center, tools.enable_flick_panning, tools.vary_round_brush_hardness_on_hud, tools.show_transformation_values, tools.double_click_layer_mask_launches_select_and_mask, file_handling.image_previews, file_handling.lowercase_extension, file_handling.save_in_background, file_handling.ignore_exif_profile_tag, file_handling.ask_before_saving_layered_tiff, file_handling.maximize_psd_compatibility, performance.cache_levels, performance.legacy_compositing, scratch_disks.disks, cursors.brush_preview_color, units_and_rulers.type_units, units_and_rulers.column_width, units_and_rulers.gutter, units_and_rulers.print_resolution, units_and_rulers.screen_resolution, plug_ins.show_extension_panels, plug_ins.allow_scripts_to_connect, plug_ins.generator_enabled, type_.smart_quotes, type_.missing_glyph_protection, type_.show_font_names_in_english, type_.use_esc_to_commit, type_.text_engine, type_.font_preview, type_.fill_new_type_layers_with_placeholder, type_.recent_fonts, enhanced_controls.scrubby_slider_acceleration, enhanced_controls.touch_gestures, enhanced_controls.zoom_with_trackpad_pinch, enhanced_controls.rotate_view_with_trackpad, raw_defaults.color_space, raw_defaults.bit_depth, raw_defaults.resolution, raw_defaults.sharpen_for, raw_defaults.open_as_smart_object, raw_defaults.apply_auto_tone, integrations.allow_agent_control, integrations.control_port.
| Id |
Target |
Status |
Issue |
Note |
| UI-217-1 |
Tool options, last Export As / Save for Web / Fill settings and font recents persist |
partial |
#217 |
Fill (fill_ui.rs:72) and Liquify (liquify_ui.rs:64, #418) keep their settings in Preferences (prefs.dialogs); no tool_options/export fields in Preferences; export_dialog.rs:21 png/85 |
| UI-217-2 |
Window size and dock width remembered across restarts |
missing |
#217 |
main.rs:46 native_options(): fixed 1440x900, centred on the main monitor (#419); dock.rs:529 snapshot omits the width |
| UI-217-3 |
File > Open starts in the last-used folder |
missing |
#217 |
services.rs:110 FileDialog::new() without set_directory |
| UI-217-4 |
Preferences all take effect; recent files persist and honour recent_file_count (#204) |
missing |
#217 |
See the prefs audit and BUG-204-* |
| UI-217-5 |
Panel groups can float (menu + drag out of the dock) and are restored at launch |
missing |
#217 |
No floating groups in dock.rs/panels.rs |
| UI-217-6 |
Document tabs: overflow list, context menu (Close/Others/All/Reveal), drag to reorder |
missing |
#217 |
canvas.rs:674-770 tabs click/close only |
| UI-217-7 |
Command palette shows recently run commands on an empty query |
missing |
#217 |
palette.rs:83-108 fuzzy hits only |
| UI-217-8 |
Tool-specific canvas right-click menu; right-click spelling suggestions in text fields |
partial |
#217 |
paint_mouse.rs:67 right-click opens the brush picker only |
| UI-217-9 |
UI scale presets 100/115/125/150/200 % through zoom_factor, remembered |
missing |
#217 |
prefs.rs:61 UiScale unused; input_tests.rs:71 asserts zoom_factor stays 1.0 |
| UI-217-10 |
Appearance: animation toggle, canvas pattern toggle per theme, start-screen recents |
missing |
#217 |
No such prefs; start screen (canvas.rs:825) has no recents |
| UI-217-11 |
Soft dark and soft light themes with picker previews; outlined knobs in light themes |
missing |
#217 |
theme.rs:28 five themes |
| UI-217-12 |
Save custom New Document presets; a real Recent category |
missing |
#217 |
new_doc_ui.rs:17 'Recent' is a static list |
| UI-217-13 |
Numeric fields accept arithmetic and units; scrubby labels; double-click resets a slider |
partial |
#217 |
widgets.rs:131 DragValue scrubs; no expressions, label scrub or reset |
| UI-217-14 |
Layers panel: wheel steps blend modes, drop on New/Trash duplicates/deletes, checker behind art |
missing |
#217 |
panels.rs:1108 plain dropdown; trash is click-only (panels.rs:1213) |
| UI-217-15 |
Curves: smoothing / pencil mode, black/grey/white eyedroppers, tooltips |
missing |
#217 |
adjust_editors.rs:397 has none of them |
| UI-217-16 |
Hue/Saturation: Saturation and Lightness sliders get hue-following colour tracks |
missing |
#217 |
Only the Hue slider has a gradient |
| UI-217-17 |
Color panel: wheel plus RGB/HSB/CMYK/Lab/Gray/Web slider modes; resizes with its group |
missing |
#217 |
panels.rs:1837 single field + hex/RGB readout |
| UI-217-18 |
Alt+wheel zooms in gentle ~5 % steps; zoom controls uncramped |
missing |
#217 |
canvas.rs:1107-1125 no Alt handling |
| UI-217-19 |
Shortcut hints rendered per platform; Reveal label per OS |
missing |
#217 |
~33 hard-coded modifier glyph strings in ui-egui; menu_catalog.rs:341 one label |
| UI-217-20 |
'Show rulers in new documents' preference; guides stored as f64 |
missing |
#217 |
No rulers pref; doc lib.rs:523 guides are Vec |
| UI-217-21 |
One Preset Manager for every preset kind with folders, search, reorder, load/save sets |
partial |
#217 |
edit_menu_cmds.rs:714 covers brushes, custom shapes, patterns with reorder |
| UI-217-22 |
Stamp Visible into a new layer keeping originals; Alt+Merge keeps originals |
missing |
#217 |
Only layer.mergeVisible (edit_cmds.rs:416) |
| UI-217-23 |
Layer clipboard: copy/paste whole layers (masks, styles, type) between documents |
missing |
#217 |
Only copy/paste layer style (extra_cmds.rs:592) |
Type
| Id |
Target |
Status |
Issue |
Note |
| TYPE-218-1 |
Type on a path: path shape, glyphs along the curve, caret follows, PSD round trip |
missing |
#218 |
doc text.rs:209 TextShape has Point and Box only |
| TYPE-218-2 |
Paragraph direction (Auto/LTR/RTL) in the panel and options bar; visual RTL caret |
missing |
#218 |
Engine has TextDirection (text.rs:155); no UI |
| TYPE-218-3 |
Thai/Khmer/Indic fallback fonts; golden shaping tests for complex scripts and RTL |
missing |
#218 |
fonts.rs:28 fallback covers Arabic/Hebrew; no per-script golden tests (only a bidi test) |
| TYPE-218-4 |
Variable font axis sliders from the font's axes; opsz follows the point size |
partial |
#218 |
type_cmds.rs:184 accepts variations; FaceInfo.axes unused in the UI |
| TYPE-218-5 |
OpenType UI: Character panel buttons, Numerals and Stylistic Sets submenus |
partial |
#218 |
Type > OpenType toggles (type_extra_cmds.rs:512); no panel buttons or submenus |
| TYPE-218-6 |
Manual pair kerning (Alt+arrows), optical kerning rendered, both round-trip PSD (#206) |
missing |
#218 |
Optical renders as Metrics (text.rs:21); Alt+arrow moves by word |
| TYPE-218-7 |
Warp Text dialog previews live and opens with the layer's current warp |
missing |
#218 |
view_cmds.rs:834 always seeds arc, bend 50 |
| TYPE-218-8 |
Text editing: select all, double/triple/quadruple-click selection, paste, with tests |
partial |
#218 |
Cmd+A, paste and double-click exist; no triple/quadruple click, no tests |
| TYPE-218-9 |
Offer 'Rasterize and continue?' for Distort/Perspective/Warp on live type |
missing |
#218 |
No such prompt |
Automation
Menu wiring (docs/parity.md): 626 of 626 menu items live (100.0%). This counts dispatch, not behaviour.
| Id |
Target |
Status |
Issue |
Note |
| AUTO-1 |
Every menu item dispatches a command id (UI, CLI, control channel and MCP share them) |
done |
|
docs/parity.md: all menu items live; menus.rs is_live |
| AUTO-219-1 |
Record actions in a document-independent form (implicit layer, merged type keystrokes) |
missing |
#219 |
actions.rs:47 copies raw journal entries with explicit layer ids |
| AUTO-219-2 |
Actions panel: step and dialog toggles, reorder, delete a step, Stop, sets, .atn import |
missing |
#219 |
Panel deletes whole actions only (actions.rs:146) |
| AUTO-219-3 |
actions.record/stop/play/list commands for agents and the CLI |
missing |
#219 |
Recording is UI-only (actions.rs:37-60) |
| AUTO-219-4 |
Replay tests for crop, filters, selections and styled text on a second document |
missing |
#219 |
One replay test (actions.rs:171: new layer, rect select, fill) |
| AUTO-219-5 |
Batch: subfolders, open documents, naming template, progress/cancel, error log, stop |
partial |
#219 |
file_cmds.rs:590-631 folder/array input, per-file errors; no recursion, template or cancel |
| AUTO-219-6 |
Plug-in ABI v2: panel kind, progress/cancel calls, read other layers, Last Filter repeats |
missing |
#219 |
runtime.rs:15 ABI_VERSION = 1; manifest.rs:18 Kind::Filter only |
| AUTO-219-7 |
Local ONNX models behind a flag, managed in Preferences; subject select, background removal |
missing |
#219 |
No ONNX runtime dependency |
| AUTO-219-8 |
xtask check that every panel button routes through a command id |
missing |
#219 |
No such xtask check |
| AUTO-219-9 |
Headless commands for in-canvas type editing, Liquify/Puppet strokes, transform handles |
partial |
#219 |
filter.liquify strokes, edit.puppetWarp pins, type.edit, edit.transform; no caret API |
| AUTO-219-10 |
Agent acceptance suite grown to 30-50 workflows |
partial |
#219 |
automation/tests/agent_tasks.rs runs 10 workflows over MCP |
Reliability
Never-crash attribute on 26 of 26 crate roots.
| Id |
Target |
Status |
Issue |
Note |
| REL-1 |
panic_hunt fuzzes every command with adversarial params |
done |
|
engine/tests/panic_hunt.rs: every command, own thread, 4 s timeout; #[ignore], run in the gate |
| REL-2 |
Every workspace crate carries the never-crash deny(clippy::unwrap_used, ...) attribute |
done |
|
Counted automatically above (crate roots under crates/ and apps/); testkit gained it in #221 |
| REL-3 |
The app catches escaped panics around command dispatch and import/export |
partial |
|
engine lib.rs:336 dispatch; crash_guard.rs:21 import/export; the dispatch guard is untested |
| REL-4 |
Atomic save (temp, fsync, rename) for every document write |
missing |
#203 |
Documents use fs::write in place; prefs/presets/bundles use tmp + rename without fsync |
| REL-212-1 |
Fallible allocation for paste/erase/fill/transform/save/import, clear out-of-memory error |
missing |
#212 |
try_reserve only in io/flat.rs:221; codec max_alloc limits cover decode only |
| REL-212-2 |
Export switches to PSB when the estimated file size exceeds 2 GB |
done |
#212 |
PSD export uses checked size estimates for raster, masks, channels, and retained metadata |
| REL-212-3 |
Tests: an allocation-limit injection makes each command Err with the document unchanged |
missing |
#212 |
Only codec/raw max_alloc tests (codecs/tests/limits.rs:48) |
Distribution
| Id |
Target |
Status |
Issue |
Note |
| DIST-1 |
macOS universal build (aarch64 + x86_64), signed and notarized |
done |
|
release.yml:66,75,93 (macos-15); 0.2.0 shipped notarized |
| DIST-2 |
Windows x64 and x86 builds |
partial |
|
release.yml:113-114; signing skipped with a warning until the signing material exists |
| DIST-3 |
Linux x86_64 and aarch64: AppImage, deb, rpm, tar.gz |
done |
|
release.yml:162-163,186 |
| DIST-4 |
Flatpak single-file bundles, x86_64 and aarch64 |
done |
|
release.yml:204-216 (#163) |
| DIST-5 |
Web build (wasm32, trunk) |
done |
|
release.yml:312 |
| DIST-6 |
FreeBSD 14 x86_64 tar.gz (/usr/local tree) built in a FreeBSD VM |
partial |
|
release.yml:242-283 (freebsd job) + packaging/freebsd/package.sh; layout checked with --dry-run, not yet produced by a release run |
| DIST-220-1 |
Help > Check for Updates plus an opt-out periodic release-feed check on all desktops |
missing |
#220 |
No update check in crates |
| DIST-220-2 |
Windows ARM64 (aarch64-pc-windows-msvc) in the release matrix, signed like x64 |
missing |
#220 |
release.yml:113-114 only x86_64 and i686 msvc |
| DIST-220-3 |
In-app bug report with reviewable diagnostics that opens a prefilled issue |
missing |
#220 |
No bug report dialog in crates |
| DIST-220-4 |
Translation coverage per language reported by xtask; RTL layout for Arabic and Hebrew |
missing |
#220 |
No i18n in crates or xtask (waits on #169) |
| DIST-220-5 |
Linux AppImage x86_64 + aarch64 ship; Flatpak kept; Flathub later (#173) |
partial |
#220 |
AppImage and Flatpak ship (release.yml:162-216); Flathub not done |
Open bugs
| Id |
Target |
Status |
Issue |
Note |
| BUG-203-1 |
Every document write goes through one atomic_write (temp, fsync, rename) |
missing |
#203 |
services.rs:125 std::fs::write; file_cmds.rs:91 fs::write; automation/files.rs:67 in place |
| BUG-203-2 |
Temp file in the same dir, sync_all, rename over target, fsync dir on Unix |
missing |
#203 |
No sync_all anywhere; services.rs:71 write_atomic (prefs only) does tmp + rename, no fsync |
| BUG-203-3 |
Windows: retry the rename with backoff on PermissionDenied |
missing |
#203 |
No retry around fs::rename (services.rs:77, preset_store.rs:189, format/store.rs:457) |
| BUG-203-4 |
Unwritable directory errors clearly, never falls back to writing in place |
missing |
#203 |
No atomic path exists for documents; all document writes are in place |
| BUG-203-5 |
On any failure the original file is untouched and the temp file removed |
missing |
#203 |
services.rs:76 write_atomic leaves .tmp behind if rename fails; documents write in place |
| BUG-203-6 |
Failure-injection tests: mid-write, rename and read-only dir errors keep original bytes |
missing |
#203 |
No such tests under crates/ or apps/ |
| BUG-204-1 |
Interface prefs (UI scale, font size, channels in colour, sliders, tooltips) wired or hidden |
missing |
#204 |
prefs.rs:196-202; no reader outside prefs.rs (see the prefs audit) |
| BUG-204-2 |
workspace.large_tabs, animated_zoom, auto_show_home_screen wired or hidden |
missing |
#204 |
prefs.rs:163,166,229; no reader outside prefs.rs |
| BUG-204-3 |
RAW prefs and ask_before_saving_layered_tiff read, or hidden |
missing |
#204 |
raw_defaults (prefs.rs:712), ask_before_saving_layered_tiff (prefs.rs:312) have no readers |
| BUG-204-4 |
Recent files persisted through prefs across restarts |
missing |
#204 |
ui-egui lib.rs:461 push_recent only touches app.ui.recent_files |
| BUG-204-5 |
Recent-files list honours recent_file_count (default 20) |
missing |
#204 |
lib.rs:465 truncate(10) hard-coded; menus.rs:1065 test asserts 'capped at 10' |
| BUG-204-6 |
Test: every Prefs field is read somewhere or on a hidden allowlist |
partial |
#204 |
The scorecard's prefs audit counts unread fields; no engine test or allowlist yet |
| BUG-205-1 |
Free Transform with a selection moves only the selected part of a linked mask |
missing |
#205 |
transform_cmds.rs:127-130 warps the whole linked mask even when the selection split pixels |
| BUG-205-2 |
Live transform preview draws the layer with its mask applied |
missing |
#205 |
transform_tool.rs:173 preview_image uses the raw surface |
| BUG-205-3 |
edit.transform honours target mask: transforms only a targeted, unlinked mask |
missing |
#205 |
transform_cmds.rs:214-270 never reads a target param |
| BUG-205-4 |
Test: a linked raster mask with no selection moves with the pixels |
partial |
#205 |
Implemented at transform_cmds.rs:127; no raster-mask test |
| BUG-205-5 |
Tests: linked mask + selection stays aligned; unlinked stays; targeted mask only |
missing |
#205 |
Unlinked case honoured (m.linked check, l.128) but untested; the other two not implemented |
| BUG-206-1 |
CharStyle models per-character manual kerning separately from the auto-kern mode |
missing |
#206 |
doc/text.rs:76 only kerning: Kerning enum (Metrics/Optical/Off) |
| BUG-206-2 |
PSD import keeps Optical and manual Kerning values |
missing |
#206 |
text/psd.rs:264 maps anything not Off to Metrics; Kerning value ignored |
| BUG-206-3 |
PSD export writes the real AutoKerning mode and Kerning values |
missing |
#206 |
text/psd.rs:380-381 AutoKerning bool only; Kerning hard-coded 0 |
| BUG-206-4 |
Alt+Left/Right at a collapsed caret adjusts pair kerning; Character panel field |
missing |
#206 |
type_tool.rs:896-912 only a mode combo |
| BUG-206-5 |
Tests: PSD round trip of manual and Optical kerning, a real file in the oracle corpus |
missing |
#206 |
No kerning round-trip test |
| BUG-207-1 |
Retouch tools respect the paint target (mask, alpha channel, Quick Mask) |
missing |
#207 |
retouch_cmds.rs:83 run_stroke always uses l.surface_mut(); no paint_target() call |
| BUG-207-2 |
Sample All Layers applies to Blur, Sharpen and Smudge |
missing |
#207 |
Checkbox at retouch_ui.rs:171-190 is not passed to the stroke commands |
| BUG-207-3 |
Smudge mixes premultiplied RGBA (no dark fringe at transparent edges) |
missing |
#207 |
No premultiply in paint/src/retouch.rs |
| BUG-207-4 |
edit.transform transforms a targeted alpha channel |
missing |
#207 |
transform_cmds.rs transform() has no target handling |
| BUG-207-5 |
Tests: per-tool mask/channel targeting, Sample All Layers, no smudge fringe |
missing |
#207 |
Only smudge_drags_colour_along_the_stroke (retouch.rs:407) |
| BUG-208-1 |
Shift while dragging a Rect/Elliptical Marquee constrains to a square/circle |
missing |
#208 |
canvas.rs:1498 and :1769 pass shift=false to marquee_end |
| BUG-208-2 |
Alt while dragging a marquee draws from the centre |
missing |
#208 |
marquee_end (chrome_ui.rs:157) has no centre mode |
| BUG-208-3 |
W x H px readout beside the cursor while dragging a marquee |
missing |
#208 |
No size readout in canvas.rs or tool_feedback.rs |
| BUG-208-4 |
Add/subtract/intersect from modifiers pressed before the drag, kept by a test |
partial |
#208 |
Mods captured at press (canvas.rs:1679, used :1779); only a badge test (tool_feedback.rs:195) |
| BUG-208-5 |
Kittest: Shift drag gives a square, Alt drag is centred, readout matches the size |
missing |
#208 |
No marquee drag tests |