major refactor to make it more object-oriented - #1
Merged
Conversation
marcinz606
added a commit
that referenced
this pull request
Jul 18, 2026
Half-frame cameras put two photos on one scan. A global toggle (mirrors RGB-scan) expands each file at discovery into two assets with hash <base>#1/#2 — every hash-keyed store (edits, history, marks, thumbnails) becomes per-half automatically. The decoded buffer is sliced at the auto-detected gutter before the pipeline, so bounds metering is independent per half; decode caches key on the unsuffixed hash so halves share one decode. Per-half sidecars (name.N.negpy), export stems name_1/name_2, batch workers, session restore, tutorial step.
marcinz606
pushed a commit
that referenced
this pull request
Aug 15, 2026
* Create bug_report.yml * Create config.yml * fix: move issue-template config.yml out of nested .github path * fix(export): apply Narrowband Scan's implicit ICC profile to flat exports request_export and request_batch_export resolved effective_input_icc() using the pre-flatten (Print) render_intent, so an E-6 transparency transfer's suppression of the implicit RGBScan.icc leaked into Flat exports even though RenderIntent.FLAT is never a transfer. Narrowband Scan appeared to have no effect on flat masters for E-6 sources (the default e6_normalize=False). Compute the ICC path after flat_master_config so it sees the flattened intent. * fix(export): fall back to the LUT ICC transform when imagecodecs' CMS codec is unavailable Some imagecodecs builds (observed on an unnotarized macOS arm64 install) load fine but only fail to provide cms_transform once it's actually invoked, raising ImportError. _apply_color_management_u16 swallowed that under a blanket except and silently shipped unmanaged 16-bit TIFF/JXL exports — affecting any custom Input/Output ICC, including Narrowband Scan's implicit profile, on both Print and Flat exports. Catch that specific ImportError and fall back to the existing PIL/LUT-based transform at a finer grid (PROOF_LUT_SIZE) instead, with a visible warning. * build(macos): force a fresh deep ad-hoc re-sign of the packaged .app PyInstaller's own signing pass does not reliably reach every binary it moves or rewrites under --collect-all (e.g. imagecodecs' bundled liblcms2.2.dylib, whose install name gets rewritten from @loader_path/.dylibs to @rpath). The app ships unnotarized, and Apple Silicon's code-signing enforcement is stricter than Intel's about loading such binaries at runtime. A --deep ad-hoc re-sign after PyInstaller finishes costs nothing and removes inconsistent per-binary signing as a variable, though it is not a confirmed fix for the arm64 CMS-codec failure on its own — full notarization is the only verified way to guarantee identical behavior across architectures. * docs: investigation notes for the arm64 imagecodecs CMS failure Handoff for continuing the root-cause hunt on real Apple Silicon hardware: what's confirmed, what's ruled out, the leading (unconfirmed) AMFI/Gatekeeper theory, and next diagnostic steps. * docs: confirm AMFI as root cause of the arm64 CMS import failure Reproduced on real M1 Pro hardware: spctl rejects _cms.abi3.so (ad-hoc signed, no Developer ID), and log show ties an AMFI denial for that exact file to the same second the LUT fallback warning fires in negpy.log. The durable fix (notarizing the release build) is tracked as follow-up work, not part of this branch. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * docs: rule out disable-library-validation as a free fix for the AMFI block Tested on the M1: re-signing with com.apple.security.cs.disable-library-validation (still ad-hoc, no cost) does not clear the AMFI rejection of _cms.abi3.so — confirmed with an identical log show/negpy.log timestamp match to the unmodified build. That entitlement waives Team ID matching, not AMFI's separate chain-of-trust check, so it can't help here. A Developer ID certificate (paid Apple Developer Program membership) is the only path that satisfies the check. * docs: explain why dev mode never reproduces the arm64 CMS import failure Confirmed cms_transform imports cleanly under make run / uv run python. The loading process's signature is the deciding factor, not the dylib's: uv's CPython carries the linker-signed adhoc flag (locally-built, AMFI lenient), PyInstaller's re-signed bootloader doesn't. Explains why this surfaced only from a downloaded release build, never from a dev checkout. * docs: link the notarization follow-up to issue #1 * fix(build): repoint the canonical liblcms2.2.dylib at imagecodecs' own copy The earlier AMFI/notarization root-cause theory for the arm64 ICC export failure was wrong. Verified on real M1 hardware: a minimal PyInstaller repro that only bundles imagecodecs never reproduces the failure, no matter how closely it mirrors the real build (ad-hoc signing, --deep re-sign, launched as a proper .app via open). ctypes.CDLL on the actual shipped _cms.abi3.so surfaces the real error: "Symbol not found: _cmsChannelsOfColorSpace, Expected in: rawpy/__dot__dylibs/liblcms2.2.dylib" — imagecodecs.__getattr__ swallows this and re-raises a generic ImportError, which is why it looked identical to a codec-unavailable / signing problem. cv2, PIL, rawpy, and imagecodecs each vendor their own liblcms2.2.dylib. PyInstaller collapses same-named dylibs from --collect-all into one canonical Contents/Frameworks/liblcms2.2.dylib with no guarantee it keeps the copy a given consumer needs; it picked rawpy's, which is missing a symbol imagecodecs's _cms.abi3.so requires. fix_lcms2_dylib_collision() repoints that canonical symlink at imagecodecs's own copy, after PyInstaller's build and before the codesign pass. Verified on the M1: ctypes.CDLL loads _cms.abi3.so cleanly post-fix, and codesign --verify --deep --strict still passes. Renamed NARROWBAND_ICC_ARM64_INVESTIGATION.md to LIBLCMS2_DYLIB_COLLISION.md with the corrected investigation; the earlier AMFI/notarization findings are kept in the doc as a ruled-out dead end so it isn't re-walked. Issue #1 (notarization) is being closed as not applicable to this bug. * fix(build): harden the lcms2 collision fix per second-pass review Verified two remaining risks a follow-up review raised, both empirically: - Did repointing the shared canonical symlink at imagecodecs' copy break the other four consumers (cv2, PIL, rawpy) that previously resolved to rawpy's copy? No: every consumer's undefined _cms* symbols are satisfied by imagecodecs' copy (nm -u/-gU diff), LC_ID_DYLIB compatibility versions match across all four original copies (3.0.0, so no dyld version rejection risk independent of symbols), and PIL/_imagingcms, rawpy's libraw_r, and imagecodecs' _jpeg2k.abi3.so all dlopen cleanly post-fix. - Is "works on Intel" actually architecture-safety, or luck? Downloaded the x86_64 wheels (uv pip install --target ... --python-platform x86_64-apple-darwin, no Intel hardware needed) and checked symbol closure there too: rawpy's x86_64 liblcms2.2.dylib is also missing _cmsChannelsOfColorSpace. The collision risk is identical on both architectures; Intel just never triggered it. fix_lcms2_dylib_collision() now raises instead of warn+return on a missing or ambiguous imagecodecs copy, and permanently verifies symbol closure for every known liblcms2 consumer (_LCMS2_CONSUMER_GLOBS), raising on any gap — so a future imagecodecs/lcms2 bump that isn't a strict superset fails the build instead of shipping broken. Considered a blanket "dlopen every bundled .so" smoke test instead; prototyped it and found a real, pre-existing, unrelated failure (numba's omppool needs libomp.dylib, which PyInstaller doesn't bundle) that would have made this PR fail on unrelated grounds — scoped the hardening to what this fix actually touches, noted the general version as a follow-up. image_processor.py's ImportError handler now probes the codec path with ctypes.CDLL() and logs the real OSError before falling back, so a future regression here is diagnosable from negpy.log alone instead of requiring the multi-day investigation this one took. LIBLCMS2_DYLIB_COLLISION.md updated with all of the above. Issue #1 stays closed as not applicable; opened #2 to track the unnotarized-.dmg Gatekeeper warning as its own real, separate, unfixed UX concern. * docs: verify the Intel claim against the real shipped DMGs, not inference The previous version asserted "PyInstaller's arbitrary pick order happened not to choose rawpy's copy on Intel" as fact, backed only by x86_64 wheel inspection showing the same symbol gap exists there — that shows the risk is architecture-independent, not what the real shipped Intel build did. Downloaded and inspected the actual official 0.49.0 arm64 and x86_64 DMGs (gh release download). Shipped arm64 resolves to rawpy's copy (missing the symbol, matches the reported M1 failure); shipped x86_64 resolves to imagecodecs' own copy (has the symbol, matches "works on Intel"). Confirmed rather than inferred. Also corrected an imprecise "four consumers" count (four packages vendor the dylib; seven real files consume it) and an overstated "three rebuilds" (two). --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
marcinz606
added a commit
that referenced
this pull request
Aug 16, 2026
) 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.
marcinz606
pushed a commit
that referenced
this pull request
Aug 17, 2026
* fix(half frame): keep a composite frame out of the diptych path An RGB Scan triplet, a stitch or an HDR merge could render as a half-frame diptych — split down the middle, each side run through the pipeline on its own — whenever the file it is built around had been worked on as two halves earlier. The frame came out half its width. Composites are now refused, as they already were by the split itself. A composite asset is its primary plus the extra paths, so it carries the primary's plain content hash. Discovery already declined to split those, but the other entry point claims a whole scan on the sole evidence that `<hash>#1` or `<hash>#2` edits exist, and a stale pair from an earlier half-frame session still matched. One predicate, `is_composite`, now answers for both. * fix(half frame): stop a half you only looked at from becoming an edit Turning Half Frame on and straight back off left the frame that happened to be open stuck as a diptych, never having been edited. Auto-measured bounds no longer file a settings row for a half that has none. Rendering a half meters it, and the measured bounds are persisted so navigating back is stable. Under a half's hash that write created `<hash>#1` out of nothing, and the mere existence of that row is what says the scan is a diptych. The row held no decision of the user's — only the metering, and whatever the sticky defaults were at the time. A half that carries a real edit already has a row, so its bounds keep tracking and it still returns as a diptych.
marcinz606
added a commit
that referenced
this pull request
Aug 17, 2026
A whole scan was declared a diptych on the sole evidence that a `<hash>#1` or `<hash>#2` settings row existed. The row is keyed by content hash, so it outlives the folder, the session and the mode being on, and any frame whose hash was ever worked on as two halves came back split, at half width, with a dead controls panel. Discovery now records the base hashes it actually splits with Half Frame on, and both readers require that record. Existing pairs are deliberately not backfilled, since a backfill reproduces the stale claim: those scans render whole again, keep both halves' edits, and return as diptychs the next time they are split.
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.
No description provided.