Feat/half frame rectangle editor - #761
Merged
marcinz606 merged 2 commits intoAug 5, 2026
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Half-Frame: crop/gutter editor + export-matches-preview fix
Summary
Two commits on one branch:
Why
Half-frame cameras expose two frames on one scan, and scanners capture both in a single pass. NegPy already split each scan at an auto-detected x, but the split position couldn't be corrected when the gutter fell outside the default, the physical black separator bled into both halves, and there was no way to crop the scan to the usable film area.
While testing that, two export bugs turned up:
request_exportlooked up the export asset bypath. Both halves share a path, sonext(...)returned the first match (half 1) regardless of which half was current. Exporting frame 2 shipped frame 1'sfile_info(half=0) — the whole scan, or the wrong half.preview_render_size, then sliced the half in the controller. Export (and HQ preview) sliced the half at full resolution, then the analysis stage downsampled the half.INTER_AREAaverages different pixel neighborhoods in the two orders — the whole-scan downsample bleeds the gutter/other half into the analysis buffer, shifting the measured log bounds. Different bounds → different normalization → a color cast (e.g. red) on export that the LQ preview didn't show. HQ preview matched export becausefull_resolution=Trueskips the whole-scan downsample, so both did slice-then-downsample.What changed
Crop/gutter editor (commit 48bece6)
negpy/desktop/view/widgets/half_frame_dialog.py, new): modal pop-up showing a positive (inverted + auto-levelled) preview of one scan. A draggable/resizable green rectangle defines the crop (everything outside is discarded); a draggable orange centerline sets the split; a Cut thickness slider discards a band centered on the split. Returnscrop_rect,split_x,gutter_thicknesson Apply.negpy/services/assets/half_frame.py):slice_half()andslice_for_asset()acceptcrop_rect(normalized x1,y1,x2,y2) andgutter_thickness(normalized fraction of cropped width). The gutter discards a band centered on the split so the physical separator doesn't bleed into either half.negpy/desktop/controller.py): newopen_half_frame_dialog()loads the scan, seeds the editor from the saved profile (or auto-detect viadetect_split_x), and persists the result as a global setting (half_frame_profile)._active_half()and_slice_half_source()now carrycrop_rectandgutter_thicknessthrough the render path. The profile is forwarded on asset discovery requests so re-discovery applies it.negpy/desktop/view/sidebar/files.py): new Adjust Half Frame toolbutton (tune icon) re-opens the editor on the current scan and triggers re-discovery so the new profile takes effect immediately. Enabling Half Frame mode also offers the editor on the current frame.negpy/services/rendering/image_processor.py,negpy/desktop/workers/export.py,negpy/services/assets/thumbnails.py):crop_rectandgutter_thicknessflow through export, tile rendering, and thumbnail generation so previews, full-res exports, and filmstrip thumbs all honour the profile.tests/test_half_frame.py):TestSliceHalfCropGuttercovers crop-rect-only slicing, gutter band discard, andslice_for_assetreading both fields.tests/test_file_browser_toolbar.pyupdated for the new button.docs/USER_GUIDE.md): Half Frame entry documents the rectangle editor, the Cut thickness slider, the saved-profile behaviour across acquisition methods, and the Adjust Half Frame toolbutton.Export-matches-preview fix (commit 59b2237)
negpy/desktop/controller.py):request_exportnow matches the asset byhash(unique per half) instead ofpath(shared), so the active half'sfile_inforeaches the exporter.negpy/services/rendering/preview_manager.py): the active half is now sliced from the full-res decode before the preview downsample (both the splash and linear paths), so the analysis stage sees the same pixels export analyzes. Threaded ahalf_slicetuple(half, split_x, crop_rect, gutter_thickness)through:PreviewLoadTask(negpy/desktop/workers/render.py) — the worker passes it toload_linear_preview,load_splash_and_linear, and the cache-warm path.load_linear_preview/load_splash_and_linear/_load_from_open_raw/_try_splash_from_open_raw— slicefull_linearandir_full(and the splash thumbnail) before the resize.negpy/services/rendering/preview_cache.py):PreviewCacheKeynow includeshalf,split_x,crop_rect,gutter_thickness, so the two halves get separate cache entries (they're different buffers now).negpy/desktop/controller.py):_split_active_halfis a passthrough (preview buffers arrive already sliced);_active_halfrefactored to share a new_half_slice_for_assethelper; prefetch now warms the half-suffixed hash with the neighbor'shalf_sliceso cache-warm keys match real loads.How to test
Notes
b80eb4f) already sliced after the whole-scan downsample in the controller via_split_active_half. The crop/gutter work inherited that ordering unchanged; the fix moves the slice ahead of the downsample so preview and export analyze identical pixels.Disclosure
Portions of this change were written with the assistance of an AI coding tool (Claude / opencode). All code was reviewed and is the author's responsibility.⏎