Commit Graph
365 Commits
Author SHA1 Message Date
Dylan M. TaylorandCodebuff 96042b8e7a fix(text): bound untrusted font and PSD text processing (#8768)
* fix(text): bound font paths and imported text processing

Reject encoded path tricks and cap resource-heavy font and PSD text processing to keep untrusted inputs bounded.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* fix(text): reject percent-encoded DEL in served paths; satisfy clippy chunks_exact lint

---------

Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-23 09:40:04 -04:00
Dylan M. Taylor 6649b78526 Use chrono instead of hand-rolled UTC conversion (#8234)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the civil_from_days copies behind the app's log timestamp, the
Measurement Log's date/time, and the perf report date with chrono as a
workspace dependency (reusing the version already in the tree). Output formats
are unchanged and pinned by the existing tests. AGENTS.md now says time goes
through chrono.

Follow-up to storytold/cadcraft#1785

* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed

chrono's From<SystemTime> unwraps internally, so a clock past year 262143
panicked. logging::timestamp and analysis_cmds::now_iso now read the duration
since the epoch and go through the fallible DateTime::from_timestamp, reading
an out-of-range or pre-epoch clock as the epoch as the old saturating code
did. All three ISO helpers (timestamp, now_iso, perf::iso_utc) write the year
with {:04} rather than %Y, which prefixes a '+' past year 9999.

Tests cover the far-future and year-10000 cases for timestamp and iso_utc.

* Don't build an unrepresentable SystemTime in the far-future test

UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.

* time: preserve old out-of-range date conversion (no epoch fallback)

`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any
instant past chrono's year range to 1970-01-01, changing output the old
hand-rolled Howard-Hinnant code converted exactly (e.g. 10_000_000_000_000 s
was 318857-05-20 17:46:40, not the epoch). Restore the old conversion domain
with a cycle-based helper that routes the calendar through chrono (the
calendar repeats every 400 years = 146_097 days, so month/day/time come from
chrono and only the year is carried in i128, keeping every i64 exact).

- apps/photocraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
  pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/engine/src/analysis_cmds.rs: extract pure `iso_from_unix` (cycle
  based) from `now_iso` so the conversion is tested directly.
- xtask/src/perf.rs: `iso_utc` cycle-based, accepts the full u64 range the
  old code did.

Pinned old outputs in tests:
  logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch),
          10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
  iso_from_unix 10_000_000_000_000 -> 318857-05-20T17:46:40Z,
          -10_000_000_000_000 -> -314918-08-13T06:13:20Z, i64 extremes
  iso_utc u64::MAX -> 584554051223-11-09T07:00:15Z (was epoch),
          10_000_000_000_000 -> 318857-05-20T17:46:40Z

* Don't build a year-318857 SystemTime unchecked in the logging test

Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-22 13:54:10 -04:00
aiongg a6428d3dfb Build each crate's integration tests as one binary
Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs; helper modules under tests/common move with them.

These keep their own binary (own process):
- engine panic_hunt: a process-wide panic hook counts every panic.
- engine text_gamma: changes the process-wide text gamma.
- engine smart_filter_stages, mask_cache_purge: assert on global caches.
- codecs pnm_peak_memory: reads the process's peak RSS.
- photocraft mac_menu_appkit: harness = false.
2026-10-22 16:55:34 +00:00
81925d7769 Integration: Raw Defaults resolution on Revert, Batch, Stack, headless open and CLI convert (#7811)
The resolution preference reached File › Open, but the other ways a raw
becomes a document of its own still opened it at 72 ppi: File › Revert
(a raw opened at 240 dropped to 72), Batch / Image Processor / Lens
Correction batch (a raw saved as PSD came out at 72 while Open + Save
gave 240), Load Files into Stack, the headless MCP `doc.open`, and the
CLI (`convert` at 72 while `run`/`info`, which open headless, now take
the default 300). They now apply the same `raw_defaults` rule; Batch
passes the user's value into its scratch sessions.

Tests that fail without this change (72 vs 240/300):
revert_batch_and_stack_open_a_raw_at_the_raw_defaults_resolution
(engine, one assertion per path), doc_open_gives_a_raw_the_raw_defaults_resolution
(automation), convert_opens_a_raw_at_the_raw_defaults_resolution (CLI).

Co-authored-by: Korg1ch <57859048+Korg1ch@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 15:42:42 +00:00
aiongg 9cb9535a95 Integration: #4315 DICOM gets its own File > Open filter 2026-10-22 15:39:30 +00:00
aiongg d87a169738 Integration: cargo fmt 2026-10-22 15:38:48 +00:00
aiongg 576ba0e02b Integration: track t12
# Conflicts:
#	contributors/people.toml
#	crates/compose/src/lib.rs
#	crates/compose/src/tests.rs
#	crates/engine/src/print_cmds.rs
#	crates/engine/src/print_cmds/tests.rs
#	docs/target-app-parity.md
2026-10-22 15:38:33 +00:00
aiongg 5857afd121 Integration: track t11
# Conflicts:
#	apps/photocraft-cli/src/lib.rs
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	docs/gaps.md
2026-10-22 15:38:31 +00:00
aiongg 71991b0a03 Integration: track t10
# Conflicts:
#	contributors/people.toml
#	crates/engine/src/prefs.rs
#	crates/ui-egui/src/layer_style/contour.rs
#	docs/ui-parity.md
2026-10-22 15:38:28 +00:00
aiongg 1dc684a2b6 Integration: track t09
# Conflicts:
#	contributors/people.toml
#	crates/engine/src/atn_cmds/tests.rs
#	crates/engine/src/file_cmds.rs
#	crates/engine/src/jobs.rs
#	docs/gaps.md
#	docs/target-app-parity.md
2026-10-22 15:38:26 +00:00
aiongg 4d1b922ddf Integration: track t02
# Conflicts:
#	contributors/people.toml
#	crates/engine/src/file_cmds/tests.rs
#	crates/engine/src/print_cmds/tests.rs
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
2026-10-22 15:38:14 +00:00
aiongg 82cf3c828c Integration: track t01
# Conflicts:
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	docs/file-format-parity.md
#	docs/gaps.md
2026-10-22 15:38:14 +00:00
aiongg 0e80156397 Integration: track t16
Resolutions: CLI usage, flag lists and flag parsing carry both --tiff-compression (t15) and
--jpeg-format (t16); file.saveACopy, batch, imageProcessor and layersToFiles param specs carry
both tiffCompression and jpegFormat; file_cmds keeps tiff_compression_param and
jpeg_format_param; atn_cmds keeps flag_set/applies_at_canvas_bounds (t15) and UNHOLDABLE (t16),
edit.transform uses the pixel_target guard (t15) with layer_ref(p)? (t16); ui-egui open paths
build ImportOptions (t16) and apply raw_defaults (t15); docs headers/rows unioned; atn tests keep
both sides; tr.tsv "Keep Using CPU" kept once.
2026-10-22 15:38:13 +00:00
aiongg 3d1a4d6679 Integration: track t15
# Conflicts:
#	crates/engine/src/atn_cmds/tests.rs
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/id.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	docs/file-format-parity.md
#	docs/ui-parity.md
2026-10-22 15:38:12 +00:00
aiongg a7c80a83d2 Merge PR #6791: Intel Iris Xe on DX12: no more red blocks in brush previews and tool icons (#6790) 2026-10-22 15:37:46 +00:00
aiongg 1033aab750 Integration: cargo fmt 2026-10-22 02:56:39 +00:00
aiongg 882cbe1c2c Merge PR #7240: Save As on Linux: the one file type offered gives the name its extension 2026-10-22 02:36:51 +00:00
aiongg dfde38919e Merge PR #7883: Save As JPEG: JPEG Options dialog (quality 0–12, Baseline / Optimized / Progressive) (FILE-215-19)
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).
2026-10-22 02:27:46 +00:00
aiongg 7a25b3f38b Merge PR #7099: test(cli): pin photocraft-cli argument parsing before the clap port (#7071) 2026-10-22 02:27:40 +00:00
aiongg a77b078589 Merge PR #7417: Camera Raw: choose the demosaic method when a raw opens
crates/io/src/lib.rs: kept main's import_psd (and its call in import_stages); import_stages takes the PR's ImportOptions argument.
crates/io/src/raw.rs: the PR's demosaic summary on main's developed_document(name, bytes, ...) call.
docs/scorecard.md: ours (regenerated at combine).
2026-10-22 02:25:41 +00:00
aiongg 28a1ee7b6e Merge PR #6468: fix(photocraft): --control 0 logs the port actually bound (#6427) 2026-10-22 02:24:53 +00:00
aiongg f271b0e07d Merge PR #7051: Save: ask before replacing a file that an added or lowercased extension names 2026-10-22 02:24:07 +00:00
aiongg 3db9314ca2 Merge PR #6265: Codecs: save Cineon (.cin) film frames
apps/photocraft/src/services.rs: SAVE_FILTERS keeps the DDS entry from main and the PR's Cineon entry.
crates/codecs/README.md: Cineon table row from the PR (read and write), then main's DICOM and Amiga IFF rows.
crates/codecs/src/format.rs: caps keep the PR's Cineon (read and write) and main's Dicom and Iff rows.
crates/codecs/src/lib.rs: encode keeps the PR's Cineon arm and main's Dicom and Iff arms.
docs/file-format-parity.md: header kept from main with the Cineon write item added; Cineon / DPX row from the PR, DICOM row from main; the PR's change-log line added once at the top.
docs/gaps.md: header kept from main with the Cineon write item added; Missing list without 'Cineon write'; the PR's Cineon progress line replaces main's read-only one; the PR's change-log line added once at the top.
2026-10-22 02:24:03 +00:00
aiongg 1398aaadff Merge PR #8107: Open/Save: refuse file names that aren't valid Unicode instead of rewriting them (#6601) 2026-10-22 02:23:25 +00:00
aiongg fd55223656 Merge PR #8237: Clipboard: a panic while reading the clipboard is reported, not a crash (refs #7582) 2026-10-22 02:23:17 +00:00
aiongg 1190c5d727 Merge PR #7454: CLI convert: refuse to write over the input file unless --in-place 2026-10-22 02:23:14 +00:00
aiongg c50708ea08 Merge PR #7242: A second launch that sets only environment overrides keeps its own process
apps/photocraft/src/single_instance.rs: both sides' appended tests kept whole.
2026-10-22 02:23:01 +00:00
aiongg 96cce9b2f9 Merge PR #4713: Codecs: open Amiga IFF/ILBM bitmaps read-only
apps/photocraft-web/src/web.rs: OPEN_EXTS kept main's list (with the DICOM extensions) and added iff, lbm, ilbm.
apps/photocraft/src/services.rs: OPEN_EXTS kept main's list (with the DICOM extensions) and added iff, lbm, ilbm.
crates/codecs/README.md: kept both the DICOM and the Amiga IFF table row and section.
crates/codecs/src/format.rs: kept both the Dicom and the Iff variants in every match, detect runs the DICOM check then the IFF check, Format::ALL length re-counted (22).
crates/codecs/src/lib.rs: one use line importing dicom and iff; both decode and encode arms kept.
docs/gaps.md: header line kept from main, with the PR's 'Cineon and Amiga IFF read' wording.
2026-10-22 02:22:54 +00:00
aiongg 2bf3e4e0be Merge PR #5470: feat(tiff): a tiffCompression option for scripted TIFF saves
crates/engine/src/file_cmds/tests.rs: both sides' appended tests kept whole.
docs/file-format-parity.md: header line keeps ours plus the PR's change item, date 2026-10-18; TIFF row has both notes; change-log rows kept both, newest first.
2026-10-22 02:22:15 +00:00
aiongg 5ff4d17b1b Merge PR #6136: chore: remove unused deps and dead code
crates/engine/src/float_cmds.rs: kept main's arm and dropped only the dead "edit.stepBackward" id (edit.undo | history.stepBackward).
2026-10-22 02:22:01 +00:00
aiongg 36443b13ed Merge PR #6726: macOS: Beep When Done plays the system alert sound (NSBeep) instead of a terminal bell (#6537) 2026-10-22 02:21:55 +00:00
aiongg 7b50fbd796 Merge PR #4315: Codecs: open uncompressed DICOM single-frame images (#1048)
apps/photocraft-web/src/web.rs: OPEN_EXTS kept main's list and added dcm, dicom, dc3 after cin.
apps/photocraft/src/services.rs: OPEN_EXTS kept main's list and added dcm, dicom, dc3 after cin.
2026-10-22 02:21:52 +00:00
aiongg f32f882f3a Merge main into integration-10
# Conflicts:
#	apps/photocraft-web/src/web.rs
#	apps/photocraft/src/services.rs
#	crates/engine/src/file_cmds/tests.rs
#	crates/engine/src/float_cmds.rs
#	crates/engine/src/float_cmds/tests.rs
#	xtask/src/corpus.rs
#	xtask/src/corpus_pins.rs
#	xtask/src/main.rs
2026-10-21 21:56:26 +00:00
aiongg e4a413241a Merge PR #8004: Windows on ARM: auto picks DirectX 12 for Qualcomm graphics (#7537) 2026-10-21 21:56:21 +00:00
aiongg 7b81efddcb Merge PR #5436: IO: open .psd through the PSD reader, not codecs (#5258) 2026-10-21 21:56:21 +00:00
aiongg f3c20b9242 Merge PR #7626: Flatpak: a second launch hands its files to the running PhotoCraft over a Unix socket (#1540) 2026-10-21 21:56:10 +00:00
aiongg 419f6ef4c7 Merge PR #6910: Web: one press per tap, so menus open again on touch (#2095) 2026-10-21 21:55:49 +00:00
skylar taylor-barrick 6d8cbac7dc Merge pull request #2945 from diegochagas/feat/xcf-read
Open GIMP XCF files: layers, groups, masks, blend modes, channels and selection
2026-10-21 14:35:32 -06:00
Diego Chagas ca18689304 Open GIMP XCF files: layers, groups, masks, modes, channels and selection (#1076)
PhotoCraft reads GIMP's .xcf (and .xcf.gz) files, read only, through a new
reader in photocraft-io written from the public XCF specification: versions
0 to 26, uncompressed, RLE and zlib tiles, 8/16/32-bit integer and
16/32/64-bit float precisions (linear precisions get a linear profile),
pixel layers at any position, groups by item path with pass-through,
layer masks applied or disabled, opacity, visibility, locks, colour tags,
the 2.10 and legacy blend modes mapped to Photoshop's (modes without an
equivalent open as Normal with a note), the ICC profile, resolution,
guides, the comment, extra channels and the saved selection; indexed
images open as RGB. Text, vector and link layers open as their pixels;
layer effects, paths and floating selections are reported.

Fixtures are written by GIMP 3.2 itself (scripts/xcf_fixtures.py), each
with GIMP's merged rendering as a PNG. In gamma-encoded blend and
composite spaces our composite matches GIMP within 0.5/255; GIMP's
linear-light default, its Clip to backdrop and its clamp-after-alpha
for unbounded modes are documented and measured in docs/xcf.md.
2026-10-21 16:57:22 -03:00
UnemarsandClaude Opus 5.5 eca10449ce Merge origin/main into raw-demosaic-choice
docs/scorecard.md regenerated with cargo xtask scorecard (24 of 172); the
Preferences test keeps main's Enhanced Controls rows and the demosaic row;
catalog tails keep both sides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:18:37 +02:00
aiongg 54229918c9 Merge PR #7271: macOS: keep the bundle's Dock icon after the window opens 2026-10-21 18:15:07 +00:00
aiongg 94d28535d3 Merge PR #6464: Survive a lost window GPU device: draw nothing, save unsaved work for recovery, close (#3438, #1215) 2026-10-21 18:15:02 +00:00
Higgs32584andClaude Opus 5.5 f916c98536 Merge upstream/main into revive/4330-jpeg-options
Conflicts only in the parity docs: status lines take main's text with the
JPEG Options entry (FILE-215-19) on top; scorecard takes main's rows with
FILE-215-19 done and File compatibility 10 / 12 / 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:56:32 -04:00
IvanandClaude Opus 5.5 8d6cc319b8 test(cli): pin --params order against positional pairing (#7071)
run_pairs_params_with_the_preceding_cmd counted the named layers, which
a naive zip of params[i] to cmd[i] also passes. Assert which layer
carries each name, so only pairing each --params with the --cmd before
it passes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 19:08:51 +03:00
IvanandClaude Sonnet 5.5 2e119be390 test(cli): tighten help and --cmd/--params pinning (#7071)
`commands --help` also exits 0 with output and empty stderr when it
runs, so the help test could not tell help from execution; assert the
command list (`file.new`) is absent from help output.

Pin that a `--cmd` without `--params` gets its own default: `First` and
`Third` must each appear exactly once, so a neighbour's params leaking
onto the middle command fails. Also assert exit 0 in the reversed-order
repeated-flag case and explain the layer count.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 19:08:51 +03:00
IvanandClaude Sonnet 5.5 2ab2de8efa test(cli): pin argument-parsing behaviour before the clap port (#7071)
Add in-process tests for the current hand-written parser so the module
split and the clap port can prove they keep behaviour: top-level
help/version/no-args dispatch, `<sub> --help`/`-h` for all eight
subcommands (returns on stdout, nothing runs), unknown flags on every
subcommand (exit 2, flag named on stderr), `--flag=value`, bare flag
given a value, value flag missing its value, last-wins for repeated
value flags, and `run` pairing each `--params` with its preceding
`--cmd`.

Assertions use exit codes, which stream got output and the offending
flag name, not full help or error wording, so they survive clap's
different messages.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 19:08:41 +03:00
aiongg 77557d75f1 Merge PR #8229: Integration batch: 8 PRs on main
# Conflicts:
#	crates/engine/src/layer_style.rs
#	crates/io/src/psd_export.rs
#	crates/io/src/smart_map.rs
#	docs/file-format-parity.md
2026-10-21 15:45:07 +00:00
Michael Higgins 9344d466e4 Merge branch 'storytold:main' into fix/7582-clipboard-crash-guard 2026-10-21 11:24:06 -04:00
Branch Sync 48d2fb45bd Merge remote-tracking branch 'refs/remotes/upstream/main' into HEAD 2026-10-21 11:18:54 -04:00
Branch Sync 960d927a37 Merge remote-tracking branch 'refs/remotes/upstream/main' into HEAD 2026-10-21 11:18:25 -04:00