Skip to content

major refactor to make it more object-oriented - #1

Merged
marcinz606 merged 1 commit into
mainfrom
oop-refactor
Jan 3, 2026
Merged

major refactor to make it more object-oriented#1
marcinz606 merged 1 commit into
mainfrom
oop-refactor

Conversation

@marcinz606

Copy link
Copy Markdown
Owner

No description provided.

@marcinz606
marcinz606 merged commit 5aa74c8 into main Jan 3, 2026
@marcinz606
marcinz606 deleted the oop-refactor branch January 7, 2026 21:38
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.
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