Conflicts with main's --no-maximize-compatibility (#8064) in the CLI
export args, output mapping and dev-dependencies, and in the
file-format parity status line and revision history: both sides kept.
Co-Authored-By: ljkx <ljkx@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP headless: waited commands run as jobs, with progress and cancel (#8129)
Headless command_run and command_batch steps that wait (the default) ran
job-capable commands inline while holding the session lock. Cancelling a
long Gaussian Blur did not stop it: it still added its history step, and
doc_inspect and jobs_list queued behind it. No progress was reported.
A waited job-capable command now starts as a background job
(Headless::command_begin) and the server polls it with the lock released
(Headless::job_poll), so other calls are served meanwhile. With a progress
token it sends notifications/progress (strictly increasing, at most ten a
second; for a batch, finished steps plus the current step's fraction).
notifications/cancelled cancels the job and leaves the document unchanged;
a drop guard does the same when rmcp drops the request future. Headless
keeps the outcome of each waited job as sync_jobs sees it end, since
jobs.list keeps only the last few. Both backends share one batch loop,
which runs no further step once the request is cancelled.
Tool hints: jobs_list is read-only, doc_select is non-destructive and
idempotent, and the scoped exports (which refuse to overwrite) are
non-destructive. doc_save's description lost a run of spaces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP headless: cancel a waited job whose request was dropped before it started
The Abandon guard was created only after command_begin returned. A request
dropped while the start still waited for the session lock left the job it
then started running, and its `waited` entry behind. The guard is now armed
inside the locked closure that starts the job, so dropping the unread output
cancels it. Review feedback on #8132.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP headless: job_poll applies finished jobs before checking the job
A caller polling Headless::job_poll directly, without sync_jobs in between,
kept seeing Running after the worker finished, because the session only
drops a finished job from its running list when its jobs are polled.
job_poll now syncs first. The MCP path already synced in headless_op.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP headless: a cancel that loses the race returns the job's result
job_abandon ignored whether Session::cancel_job cancelled anything and
always dropped the stored outcome, so headless_wait answered `Cancelled`
for a job another request had already applied to the document. It now
returns how the job really ended: Cancelled only when it cancelled the
job, the job's result when it had already ended, and an error when the
job cannot be cancelled (printing) and is still running.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP headless: waited commands run as jobs, with progress and cancel (#8129)
Headless command_run and command_batch steps that wait (the default) ran
job-capable commands inline while holding the session lock. Cancelling a
long Gaussian Blur did not stop it: it still added its history step, and
doc_inspect and jobs_list queued behind it. No progress was reported.
A waited job-capable command now starts as a background job
(Headless::command_begin) and the server polls it with the lock released
(Headless::job_poll), so other calls are served meanwhile. With a progress
token it sends notifications/progress (strictly increasing, at most ten a
second; for a batch, finished steps plus the current step's fraction).
notifications/cancelled cancels the job and leaves the document unchanged;
a drop guard does the same when rmcp drops the request future. Headless
keeps the outcome of each waited job as sync_jobs sees it end, since
jobs.list keeps only the last few. Both backends share one batch loop,
which runs no further step once the request is cancelled.
Tool hints: jobs_list is read-only, doc_select is non-destructive and
idempotent, and the scoped exports (which refuse to overwrite) are
non-destructive. doc_save's description lost a run of spaces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP headless: cancel a waited job whose request was dropped before it started
The Abandon guard was created only after command_begin returned. A request
dropped while the start still waited for the session lock left the job it
then started running, and its `waited` entry behind. The guard is now armed
inside the locked closure that starts the job, so dropping the unread output
cancels it. Review feedback on #8132.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP bridge: waited commands poll the app's job, with progress and cancel (#8130)
A waited command_run or command_batch step of an engine command now starts it
with engine.execute {wait: false} (sent once) and, when the app runs it as a
background job, polls jobs.list until it ends. A job can run past the bridge's
55s reply timeout, its progress is forwarded, and cancelling or dropping the
request sends jobs.cancel. UI command ids keep wait: true. A failed poll or a
job no longer listed is reported with its id rather than guessed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP bridge: keep the cancel guard armed until jobs.cancel returns; clarify the docs
The cancel branch disarmed the Abandon guard before awaiting jobs.cancel, so a
request future dropped during that await sent no cancel. The control-protocol
table now says polling applies only to a waited command_run. Review feedback
on #8134.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP headless: job_poll applies finished jobs before checking the job
A caller polling Headless::job_poll directly, without sync_jobs in between,
kept seeing Running after the worker finished, because the session only
drops a finished job from its running list when its jobs are polled.
job_poll now syncs first. The MCP path already synced in headless_op.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP headless: a cancel that loses the race returns the job's result
job_abandon ignored whether Session::cancel_job cancelled anything and
always dropped the stored outcome, so headless_wait answered `Cancelled`
for a job another request had already applied to the document. It now
returns how the job really ended: Cancelled only when it cancelled the
job, the job's result when it had already ended, and an error when the
job cannot be cancelled (printing) and is still running.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP bridge: a refused jobs.cancel is not reported as a cancellation
bridge_wait discarded the app's reply to jobs.cancel and always answered
`Cancelled`. The app refuses to cancel a job that is no longer running,
so a job that finished between the jobs.list poll and the cancel was
reported as cancelled although its edit may be applied. A refused cancel
now answers that the job was not cancelled and points to jobs_list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP bridge: a cancel during a failed poll still cancels the job
When notifications/cancelled arrived while a jobs.list poll was in
flight, and the poll and its retry then failed, bridge_wait disarmed the
Abandon guard and returned the poll error without checking the waiter,
so no jobs.cancel reached the app and the edit ran on after the caller
cancelled it. That error path now sends jobs.cancel first when the
request was cancelled. A failed poll without a cancel still leaves the
job running and names it.
Reported in review of #8134.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* MCP bridge: a cancel doesn't wait for a slow jobs.list poll
bridge_wait checked for a cancel only between polls, so a cancel that
arrived while the app was slow to answer jobs.list waited for the poll,
up to the 55s call timeout, before jobs.cancel was sent. The poll now
races the cancel.
That needs BridgeClient to drop the connection of a call abandoned
before its reply: it kept the one cached connection, so jobs.cancel
queued behind the unanswered poll on the same socket (the app serves a
connection's requests in order), and a half-sent request could share it.
A call now takes the connection out of the cache and returns it only
once answered, as a timeout or lost reply already dropped it.
The poll-error check from the previous commit stays for a cancel that
races a failing poll; its test is replaced by one for the slow poll,
which covers the reviewed case. A BridgeClient test checks that a
dropped call's connection is not reused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(automation): retain cleanup after failed bridge cancellation
* chore: restart CI for PR #8134
* test(automation): classify the smart object replacement MCP tool
* test(automation): synchronize dropped bridge waits and accept reconnections
* Fix CI after merging upstream
* Fix CI after merging upstream
* Deduplicate Arabic closure messages after upstream merge
* fix(engine): bound active and cancelling background workers
* Fix cancellation and draining in bridge merge
* Test release-aware draining cleanup
* Fix draining regression warning
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Khawar Zamman Wani <wanikhawar@Khawars-MacBook-Air.local>
Co-authored-by: FredApps <fredapps@users.noreply.github.com>
Thanks @wanikhawar — merging.
**Summary:** In headless MCP, waited `command_run` / batch steps run job-capable commands as engine jobs (lock released while polling), with progress notifications and cancel/abandon on request drop. Fixes the “cancel does nothing / inspect blocks” behaviour for long filters. Cap of 16 background jobs; docs updated. Stack base for #8134.
Review follow-up on #9975. The help text and the CLI book page said the
XMP and free-form text are kept in every flat export and the ICC profile,
EXIF and resolution "either way". Not every format holds them: JPEG and
WebP have no place for free-form text, TIFF writes only its named text
tags, and an Indexed Color document's palette PNG carries no XMP, text,
ICC or EXIF at all, only its resolution. The texts (and the parity-doc
revision row) now say the XMP and text are kept where the format holds
them and that the option leaves everything else as it is without it.
The test fixture gains a free-form text entry and a Display P3 profile
(from photocraft-cms, now a dev-dependency; no profile file committed),
so it checks the text is gone with `none` and kept by a default PNG, and
that the default export keeps a real ICC profile that `none` matches,
instead of comparing None with None.
Since #733 a flat export from the CLI keeps the document's whole XMP
packet and free-form text, as Save As does; only Export As starts at
Metadata: None. Batch exports of Photoshop masters therefore carry the
full DocumentAncestors list (and the text of every type layer), which
can be many times larger than the image, with no way to leave it out.
`--metadata` is a new export option shared by `convert`, `run --out`
and `batch`. `none` sets ExportOptions::xmp to XmpEmbed::None, the same
path Export As's Metadata: None and Quick Export use, so the XMP packet
and free-form text are dropped while the ICC profile, EXIF and
resolution are written as before; PSD, PSB and .pcraft keep the XMP
either way. `all` is the default, so existing scripts write the same
files. Any other value is a usage error (exit 2) naming the flag,
before anything is read or written.
Tested through the binary for PNG, WebP, JPEG and TIFF from all three
subcommands (packet present by default and with `all`, absent with
`none`; pixels, ICC and resolution unchanged), layered saves, and bad
values. Documented in the CLI book page and file-format parity doc.
A canvas over 2^26 pixels (256 MiB of RGBA8) is saved without composite/full.png. The PNG encoder's buffers are reserved fallibly, so an allocation failure reports an encode error, and a ZIP bundle that cannot be reserved with the composite is written without it.
Port behavior and regression coverage into current modules; preserve current upstream behavior.
Integrate lossless export-only JXL and independent decoder regression matrix, preserving current JPEG/TIFF save extensions and 12-bit/arithmetic JPEG decoder dependency.
- The PNG encoder gets a fallibly reserved copy of the composite's pixels instead of a
clone, and a full-size composite that cannot be copied or encoded is left out of the
ZIP or directory bundle and out of its manifest; the save goes on without it.
- The ZIP size limit for keeping the full-size composite counts the central directory
and end record as well as the entries.
- `pcraftFullComposite` that is not true, false or null is a bad parameter of
`file.saveACopy`, the other engine saves and the automation `doc.save`.
- MCP `doc_save` in bridge mode refuses `pcraftFullComposite: false` as well as `true`.
The app's `app.save` does not take the option.
A .pcraft bundle holds the flattened document only as thumb.png (at most
256 px) and composite/preview.png (at most 1024 px). A new option also
writes composite/full.png: the flattened document at its canvas size,
an 8-bit RGBA PNG made like the two previews, with the ICC profile of
its pixels embedded. The option is off by default, and a save without
it writes the same bytes as before.
- Preferences > File Handling > Save Full-Size Composite in .pcraft
Files sets it for the app's Save, Save As and Save a Copy, in the
desktop and web builds.
- The engine's file.saveACopy and its batch and export commands take
"pcraftFullComposite" (unset, the preference), the automation
doc.save and MCP doc_save take
"pcraftFullComposite": true, and photocraft-cli convert, run and
batch take --pcraft-full-composite.
The manifest names the image in a new optional member, full_composite;
the format version stays 1, and bundles without it open as before. A
canvas over 2^30 pixels, or one whose pixels cannot be allocated, is
saved without it, and so is a ZIP bundle that it would take past 4 GiB
or 65,535 entries.
Conflicts: the batch's wheel reads (#5547 Animated Zoom) keep their extra
argument with main's ids (`Id::unique`, `ui.scope_id()`); crop presets keep
#6620's `apply_choice` with main's accessible name; Bevel & Emboss keeps
#9370's per-mode id with main's name argument; the Stroke field keeps
#7203's gradients with main's name; tone.rs keeps #9139's move of the
Histogram panel to histogram_panel.rs (main only renamed ids there);
translation catalogs as unions.
Not flagged by git, the batch's new code follows egui 0.37 too: dropdowns
take an accessible name (Histogram Channel/Source #9139, tool Mode #5504,
Art History Style #1610, New Snapshot From #3567), MouseWheel events carry
`source`, `WidgetType` is `Role` (ColorButton is ColorWell, #7203),
`Id::new`/`Ui::id` are `Id::unique`/`Ui::scope_id`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sign.ps1 downloaded Microsoft.Trusted.Signing.Client from the unversioned
NuGet URL and loaded its dlib into signtool, next to the Azure signing
secrets, with no version or integrity check.
Download a pinned version (1.0.95, which the unversioned URL serves today)
and refuse it unless its SHA-256 matches, so upgrading the client is a
deliberate change in the repository. The release docs say where the pin
lives.
psd_import.rs keeps main's warning for an incomplete merged extra channel
next to the PR's Photoshop spot polarity. people.toml takes main's entries
plus jasallen's own. Docs take both sides' status lines and revision rows;
docs/scorecard.md is regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review round of the #7071 stack, which #9167 merged without
(it took the pre-review heads):
- A bare --help or -h before `--` is help after any parse error, not
only after a missing value: `mcp stray.psd --help` and
`serve --port abc --port 1 -h` print the subcommand's help.
- mcp and serve --help carry #8035's rules: what the automation roots
grant, that bridge roots are refused, and that the token flags need
--bridge or --port (or PHOTOCRAFT_CONTROL_PORT).
- check_server takes the subcommand and whether a transport is set;
the bridge-roots check moves next to the other mcp checks.
- The server tests match clap's "a value is required for" text, and
mcp.md in the book and docs names PHOTOCRAFT_CONTROL_PORT.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Say which packages install them and how to generate and install them
by hand from a checkout. The desktop app has neither.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With the environment fallbacks, clap's conflicts_with on the token pair
made a stale exported PHOTOCRAFT_CONTROL_TOKEN and
PHOTOCRAFT_CONTROL_TOKEN_FILE a usage error for every mcp and serve, though
headless mcp and stdio serve read no token. As on main, the pair is now
refused only by serve on TCP and mcp bridging, still as clap's either/or
usage error naming each half as it was given.
An automation root on the command line with PHOTOCRAFT_CONTROL_PORT set
is a usage error too: the variable would switch mcp to bridging, which
drops the roots, with only a note on stderr.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Say that PHOTOCRAFT_CONTROL_PORT is the fallback for serve --port and
mcp --bridge (with the stderr notice and the empty-is-unset escape),
and list the CLI's variables on the book's CLI page, which also says
the automation root variables stay the desktop app's.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Name what typed values made a usage error: a value out of range
(--quality 0), malformed --new or --params JSON, <FILE> together with
--new, and --control-token together with --control-token-file. A path
that is not valid Unicode now reaches the command: convert takes it,
info, run and batch fail through the automation layer (exit 1), and
droplet, which takes text, refuses it as a usage error.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The CLI page said parsing reports malformed flags and JSON. Name what
the step 0 decisions made a usage error: a single-value flag given twice
(--cmd and --params repeat), how to pass a value starting with '-', and
that `version` is only --version/-V. Say why --quality allows negative
numbers.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Brings split/tiff-extra-samples-codecs at e6fca054, which merged main
1695f323. Conflicts:
- crates/io/src/lib.rs: main moved PSD import into import_psd(); it now
passes PsdImportOptions::default().
- PsdExportOptions (psd_export.rs, tiff_layers.rs): main's new
read_linked_files next to this branch's photoshop_spot_polarity.
- docs: main's newer status lines and rows, with this branch's TIFF
statements re-applied (dated 2026-10-23). docs/scorecard.md is main's
and is regenerated after PR 2 is merged too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merges main (8f09d122d). Conflicts: the replay_save doc comment (main added
file.export.exportAs replay) and the status lines and history rows of
docs/file-format-parity.md and docs/gaps.md; both sides kept.
The docs called FILE-215-19 done and said only Save As and Save a Copy ask.
Photoshop's JPEG Options also has Matte, the progressive scan count
(Scans 3-5) and the size preview, none of which is offered, and Baseline
Optimized writes the same file as Baseline for RGB (4:2:0). The row is now
partial with those gaps named, and the docs say File > Save asks too.
docs/scorecard.md is regenerated.
Adds jpeg_options_ui::tests::the_answer_reaches_the_encoder. Every UI test
used a fake exporter, so dropping `opts.encode.jpeg_format` from
ExportSettings::io_options (the desktop and web save path) left them all
green; this test writes a real JPEG through io_options and fails without it.
Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
crates/ui-egui/src/lib.rs: replay_save doc comment names both Export As and JPEG; replay_save uses the PR's FormatChoices::from_step, after main's export_dialog::replay branch.
docs/file-format-parity.md: header keeps main's text plus the JPEG Options item (last updated 2026-10-19); TIFF row is main's, JPEG row is the PR's; change-log rows once each, newest first.
docs/gaps.md: header keeps main's text plus the G10 item (last updated 2026-10-19).
crates/format/src/store.rs: kept both field sets in PcraftWriter::default (main's blob_hash_cache/compressed_budget and the PR's lock_wait) and both setters (set_compressed_cache_budget, set_lock_wait).
Flat and layered TIFFs write and read alpha channels, spot channels and a
Quick Mask as extra samples, with Photoshop's channel resources. This is
the io part of #7415 as it stands, on current main; the next commit keeps
PSD output as it is on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Zer0C00L01 <12090083+Zer0C00L01@users.noreply.github.com>
Main added DDS saving (#5993): DDS Options, and FormatChoices carrying the
Targa and DDS answers through every save path (this branch's SaveChoices
carried Targa and JPEG). Resolved by keeping main's name and making it
the one struct for all three answers.
- lib.rs: FormatChoices { tga_bits, dds, jpeg } with from_params (strict),
from_step (lenient JPEG read of a recorded step), apply and record;
main's separate block is folded in. Both prompts (dds_options and
jpeg_options) are kept everywhere: save_to asks Targa, then DDS, then
JPEG Options; save_options_open covers all four. save_in_place takes
main's (path, choices) signature and records the plain-Save step with
plain_save_step for JPEG plus the Targa/DDS answers (save_in_place_with
folded into it). journal_as_plain_save relabels a parked Targa, DDS or
JPEG prompt and keeps tgaBits/dds*/quality/jpegFormat when dropping the
path. replay_save reads choices with from_step.
- SAVE_OPTIONS_OPEN names all four prompts; DDS Options uses it too.
- targa_options_ui: main's plain-Save step (no path) through this
branch's write_answered_save. JPEG Options gets main's Enter-on-a-
focused-Cancel fix.
- view_cmds Save a Copy asks Targa, DDS, then JPEG Options; write_copy
takes FormatChoices.
- control.rs: both app.save tests (JPEG and DDS) kept.
- engine file_cmds: SaveOpts keeps jpeg_format and dds; param docs list
both jpegFormat and ddsCompression/ddsMipmaps. CLI: both --jpeg-format
and --dds-compression/--dds-mipmaps. codecs re-exports JpegFormat and
DdsCompression/DdsOptions.
- docs/book: both entries kept (control-protocol app.save lists both).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Name missing values alongside empty ones, and say `--help`/`-h` are the
options that take no value (CodeRabbit review on #8035).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`--help` or `-h` where a flag's value should be prints usage rather than a
"needs a value" error, in every subcommand. Tests cover the missing-value
rule outside mcp/serve and the `--flag=--value` form, and the stdin test
checks each launch line that used to create a token file. The book's MCP
page documents the launch rules.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`mcp` and `serve` silently accepted positional arguments, repeated options,
empty values, credential flags for an inactive transport, and (in bridge mode)
root flags the desktop process owns. They are now usage errors (exit 2, stderr
only) before any server starts or token file is created. The shared parser
also refuses another long flag or `-h` as a missing value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DDS textures can now be saved, not just opened: uncompressed (BGRA, BGRX, L8, A8L8; lossless) or BC1/BC2/BC3 (DXT1/3/5) from a clean-room block encoder, with an optional box-filtered mip chain, in the legacy header. Save As and Save a Copy ask with DDS Options; scripts pass `ddsCompression`/`ddsMipmaps` (file commands, control `app.save`, RPC `doc.save`, MCP `doc_save`/`doc_export`), and the CLI takes `--dds-compression`/`--dds-mipmaps`. Targa and DDS answers travel together as `FormatChoices` through the Save As flow.
Review follow-ups:
- File › Save behind DDS or Targa Options records a plain Save with its answer, and replaying `file.save` applies it (#3531).
- The block encoder finds the principal axis from every channel, so a channel that varies on its own can't hide the others' variation.
- DDS joins the shared round-trip suite; the header is built before the output buffer is allocated.
- Enter on a focused Cancel in DDS, Targa or TIFF Options cancels instead of saving.
Save As and Save a Copy to a JPEG now ask for JPEG Options before writing,
as Photoshop does. Re-creates #4330 on current main and folds in the
unique parts of #5580 (#5563).
- codecs: `JpegFormat` (Baseline, Optimized, Progressive) on
`EncodeOptions`. Progressive writes SOF2. Baseline Optimized fits the
Huffman tables only at 4:4:4 (Gray, CMYK) or with Progressive: fitted
tables make the baseline scans non-interleaved, which zune-jpeg 0.5.15
decodes wrongly with 4:2:0 chroma (zune-image#419), as Save for Web
already works around.
- engine: a `jpegFormat` save param (file.saveACopy, Batch, Image
Processor, Layers to Files and every SaveOpts user; anything but the
three names is BadParams before writing) and a shared 0–12 -> 1–100
`jpeg_quality()` that maps NaN/inf to 100.
- ui-egui: `jpeg_options_ui` (Quality 0–12 with Low/Medium/High/Maximum
presets, Format Options), parked like Targa Options. It starts at the
opened JPEG's own quality (`file_encoding`, #4032), else the Export
preference; left there, OK writes that quality exactly and the step
records no `quality`. `SaveChoices` (Targa bits + JPEG) replaces the
bare `tga_bits` through file.save / file.saveAs / app.save, replay and
Save a Copy; automation never prompts. `write_answered_save` is shared
with Targa Options. `park_at` + `Start` let any save seed the prompt.
A File › Save that confirms the document's own flat JPEG is still
journalled as a plain `file.save` (#3531).
- CLI: `--jpeg-format`.
- i18n: five new strings in all 20 catalogs (#4330's duplicate de/es/zh
`Low`/`High` rows dropped: main has them since #4008).
- Docs: book raster-formats JPEG bullet, control protocol, file-format
parity, gaps G10, target-app parity; scorecard FILE-215-19 done.
Co-authored-by: SckraT <70698629+SckraT@users.noreply.github.com>
Co-authored-by: PierrunoYT <95778421+PierrunoYT@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve current upstream DDS, DPX, PCX, XPM and related export changes
while restoring JPEG XL format registration, dependencies, documentation,
Save As/Export As integration and independent lossless round-trip tests.
Retain the 16-bit alpha header correction.
A directory save lists the existing objects, writes its own, publishes its manifest and removes what that manifest doesn't reference. Two writers (two stores, or two processes) saving the same bundle could interleave, and one writer's cleanup could remove an object the other's winning manifest needs. Each save now holds an OS advisory lock on save.lock for its whole transaction; a held lock is waited for (10 s by default, PcraftWriter::set_lock_wait), then the save fails without writing. A crash releases the lock; a file system without file locks saves unlocked as before. Fixes#4957.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CMYK document with several layers, masks or effects is flattened through the
RGB compositor. Its TIFF and JPEG exports were written as sRGB RGB, losing the
document's mode and ICC profile. The composite is now converted back through the
document's CMYK profile (relative colorimetric with black point compensation)
inside each parallel worker, written as four ink channels plus straight alpha at
the document depth, and tagged with that profile (the built-in coated CMYK
profile when the document is untagged). Invalid, non-CMYK or unusable profiles
are rejected. RGB-only formats keep the direct sRGB composite, and a single
plain raster layer keeps its native samples.
This fixes the output mode and tagging only: the RGB roundtrip does not
preserve original ink separations, black generation or CMYK-native blending.
Claude-Session: https://claude.ai/code/session_01BSB7fMQUccqFQekksDGWpo
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Retain FITS support while integrating JPEG XL export. Expand GrayA
samples to RGBA at their original depth so the independent decoder
oracle can verify alpha values instead of silently losing precision.
Update capability claims, format documentation, and oracle coverage.
* Format: admit the declared .pcraft canvas before loading it (#2935)
A .pcraft manifest's canvas size was copied into the Document unchecked, so
a few bytes of JSON could declare a 300000x300000 (or u32::MAX) canvas that
the CPU reduction, thumbnails and display textures then work against in
proportion to its area.
The loader now refuses an empty canvas, a side above 300000 (Photoshop's PSB
limit, as for PSD and file.new) and an area above 4 gigapixels, right after
reading the manifest and before any tile or blob is decoded. Both limits are
LoadOptions fields so trusted callers can change them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format: admit every canvas the app can create on .pcraft load (#2935)
The default pixel budget was 2^32 (65536 x 65536), but file.new, Canvas
Size, Image Size with Resample off and PSB import all reach 300000 x
300000, so a document a user could create and save would not reopen.
The default is now the full 300000^2 PSB canvas; the side limit still
refuses anything larger, and callers handling untrusted files can lower
either budget. Loading stays proportional to the file: layers keep only
the tiles the bundle holds, so a huge declared canvas allocates nothing
for its area.
A compile-time assert in document_preset_cmds ties file.new's
MAX_DIMENSION to the loader's defaults.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* MCP bridge: reconnect before idle sockets die, retry reads, actionable timeouts (#2487)
The app closes idle control connections after 30s (IO_TIMEOUT) while the
bridge kept one persistent connection, so the first call after a pause
failed with "operation may have completed". The bridge now drops and
reconnects before sending once the connection has been idle 25s, so a
request is never written to a socket the app may have dropped.
The default call timeout drops from 60s to 55s, strictly below the app's
60s reply deadline, so the bridge's own error — naming the method and the
likely causes — is the one callers see instead of a race between the
app's deadline reply and the caller's own cutoff.
Idempotent read methods (ui.inspect and friends) get one automatic retry
on a transport failure; edits keep #1007 semantics and are never resent.
A timed-out call is never retried (it would only double the wait).
* MCP bridge: caller-vouched read retries, per-failure wording, docs contract (#2487 review)
The retry decision moves from a hand-maintained method list — most of whose
entries were engine command ids that never travel as control-protocol
methods, so they could never match — to the call sites that know a call is
a read: call never resends (#1007), call_read retries once on a transport
failure, and the MCP server opts its pure-read tools in. Failures are
worded by what they prove: a connection that could never be established is
"the request was not sent", not "may have completed"; a read timeout says
a read may be retried safely instead of demanding a state inspection. The
55s client cap, the read-retry exception, and the idle reconnect are now
documented in the control protocol and the security book.
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>
The prerequisites listed `libxkbcommon-dev ... libgtk-3-dev`, which exist only
on Debian and Ubuntu: on Arch none of those names resolve and `pacman -S` fails
on the whole line, and on Fedora and openSUSE they all need `-devel`. Nothing
else in the repository gives the equivalents.
The app already speaks four distros at runtime: linux_libs.rs carries a
{soname, debian, fedora, arch, suse} table, reads ID and ID_LIKE from
/etc/os-release and prints the matching apt, dnf, pacman or zypper command when
a runtime library is missing. This brings the build instructions up to that.
Also adds the prerequisites to the book's Building page, which said only to
install the Rust toolchain.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* OpenEXR/HDR export: linearize grayscale documents too
Fixes#2386. The EXR/HDR branch of export_flat converted RGB images to
linear sRGB but left gray ones as stored, so an sGray #808080 was written
as 0.502 instead of 0.216. Gray images are now converted from their
profile (sGray when untagged) to linear gray. An untagged grayscale
OpenEXR opens tagged linear gray (an EXR Y channel is linear luminance),
and the open policy keeps that profile without a mismatch prompt, as it
already does for linear sRGB.
* contributors: add itsF4LCON
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* docs: register the MCP server from an installed release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: drop the third-party link; the Windows path is the default install folder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>