Skip to content

Render a half-frame scan as a diptych once the split is turned off - #865

Merged
marcinz606 merged 2 commits into
mainfrom
feat/diptych
Aug 16, 2026
Merged

Render a half-frame scan as a diptych once the split is turned off#865
marcinz606 merged 2 commits into
mainfrom
feat/diptych

Conversation

@marcinz606

Copy link
Copy Markdown
Owner

What

Half-frame mode gives each exposure its own asset (<hash>#1 / #2) and its own edit. Turning the mode off never lost those edits — nothing deletes the rows, and both theft guards (load_or_promote skipping the path fallback for a half, load_file_settings_by_path excluding %#%) keep them intact. It only made them unreachable: the whole-frame asset keys on the unsuffixed hash and came back with defaults.

A whole-frame scan that carries half edits is now a diptych: half 1 renders under its own config, half 2 under its own, and the two are joined back into the original geometry — one image, one export.

How

  • services/assets/half_frame.pydiptych_configs (a missing half copies its sibling), join_halves, gap_px, and slice_half(half=0) for crop-without-split.
  • image_processor.pyprocess_export split at the encode seam into _render_export_buffer + _encode_export; diptych= renders both halves and joins them, then encodes once, so no double compression.
  • workers/render.pyRenderWorker._render_diptych: slice, two run_pipeline runs, join.
  • controller.py_mark_diptychs flags the whole roll in one query at discovery; _diptych_task stamps the saved split profile onto the asset; wired into all three export builders and the preview/render path.
  • Read-only asset: composite_kind"diptych", both-sides-filled split badge, controls panel disabled with a status hint. Export settings stay editable (they live outside ControlsPanel.pages).
  • Export name: <name>-DIPTYCH.<ext>, reusing the existing -HDR composite hook.

Two details that are load-bearing rather than incidental:

  • Each half is sliced before the pipeline, so its normalization measures the pixels it was edited on. Rendering the whole frame twice and splicing columns would meter across both exposures and drift the look away from what the user set.
  • Each half gets its own pipeline identity (<source_hash>#1 / #2), or the stage caches collide and the second render comes back as the first.

log_bounds writeback is suppressed for a diptych — those bounds were measured on one half, under that half's edit.

Decisions worth a second opinion

  • The gutter is filled black, not copied from the source. The gap keeps the cut band's width, so the original geometry survives. But the source gutter is scene-linear negative data: pasted in it renders as a bright bar (unexposed film base is low density, and the pipeline is what inverts it), and running the pipeline on a thin dark strip renormalizes it into noise. Black is what the space between two exposures looks like once rendered. _GAP_FILL is a module constant if the finish border colour turns out to read better.
  • Export sizing and border apply per half, because they resolve inside the pipeline (the GPU path handles them in its own finish stage, so they cannot be deferred to the join). A long-edge-2000 diptych is two 2000 px frames side by side. Percent and native-size exports are unaffected.
  • The gate is implicit: a scan renders as a diptych whenever #1/#2 rows exist and half-frame mode is off. No new UI, but also no way to opt one scan out.
  • Unequal half heights are centre-padded, never resampled — an export must not resize pixels the pipeline already sized.

Not in scope

Filmstrip thumbnails and contact-sheet tiles stay whole-frame: a diptych tile costs two pipeline runs per proxy. A diptych's thumbnail therefore will not match its export. Both caveats are in docs/USER_GUIDE.md.

Testing

make lint, make type clean; uv run pytest tests4183 passed, 11 skipped.

New coverage in tests/test_half_frame.py: join_halves geometry and centre-padding, gap_px scale invariance, slice_half(half=0), diptych_configs (both halves / one / neither / a half / a #stitch composite), the render worker using two configs on two slices with distinct hashes, metrics coming from half 1 and marked, _mark_diptychs, the stamped split geometry, composite_kind, and the export filename.

One pre-existing test needed a stub: test_export_flush drives request_export on a bare MagicMock, which cannot unpack the new _diptych_task return.

docs/CHANGELOG.md untouched, per repo convention.

Half-frame mode gives each exposure its own asset (`<hash>#1` / `#2`) and its own
edit. Turning the mode off never lost those edits - nothing deletes the rows - but
it made them unreachable: the whole-frame asset keys on the unsuffixed hash and
came back with defaults.

A whole-frame scan that carries half edits is now a diptych. Half 1 renders under
its own config, half 2 under its own, and the two are joined back into the
original geometry. Each half is sliced before the pipeline, so its normalization
measures the pixels it was edited on, and each gets its own pipeline identity or
the stage caches collide and the second render comes back as the first.

The gap keeps the cut band's width but is filled black rather than copied from the
scan: the source gutter is scene-linear negative data, so pasting it in gives a
bright bar, and running the pipeline on a thin dark strip renormalizes it into
noise. A half with no saved edit takes its sibling's.

A diptych is read-only - two configs drive the canvas, so the controls panel has
nothing to drive - and is flagged at discovery in one query for the roll. It
carries the both-sides-filled split badge and exports as one `<name>-DIPTYCH` file.
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Preview load metrics (github-linux-x64)

format cold load throughput
CR2 0.759 s 29.57 MPx/s
NEF 0.597 s 41.22 MPx/s
ARW 0.212 s 95.13 MPx/s
RAF 0.627 s 25.95 MPx/s
DNG 0.649 s 36.89 MPx/s

fixtures: rawsamples.ch · sha: 206e591 · full JSON

@marcinz606
marcinz606 marked this pull request as ready for review August 16, 2026 15:41
One conflict, in controller.py: both sides added an import next to the other.
Kept both — diptych_configs from the half-frame helpers and render_export_filename
from the Linear Output destination work (#862).
@marcinz606
marcinz606 merged commit f49cb2d into main Aug 16, 2026
2 checks passed
@marcinz606
marcinz606 deleted the feat/diptych branch August 16, 2026 15:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant