Nikon Coolscan scanning through nkscan - #970
Draft
marcinz606 wants to merge 8 commits into
Draft
Conversation
Owner
Author
|
needs more testing |
marcinz606
force-pushed
the
feat/nkscan
branch
from
August 25, 2026 14:31
c4d7c6a to
8d90f0b
Compare
Drives a Coolscan through the nkscan Rust driver instead of SANE's coolscan3: frame rectangles measured off the film, a native IR pass, a native eject, and the driver's own openICE dust removal. The strip is measured in one pass and every preview tile is cut out of that pass, so previewing five frames takes one read of the film rather than five scans. A boundary nudged afterwards re-frames without touching the scanner. Frame rects and the pass they came from are cached per device and dropped on eject. The first native RollSession, so ScannerService.open_roll grows the branch its docstring anticipated. An empty BatchRequest.frames now means every frame on the film, resolved through ScannerService.detect_frames: a transport that measures the film has no frame range to fall back on, and the hidden spinboxes silently meant frame 1. New controls, each gated on what the unit advertises: ICE, Samples, Superfine, Film (color negative, B&W, slide, Kodachrome) and Film format. Film type decides which way frame boundaries read on a strip, and disables IR and ICE on stock whose silver or dyes stop infrared. An LS-50 reads one CCD line at a time and ignores repeated reads, so Superfine and Samples stay hidden there; where the bindings cannot say, the backend learns it from the first refusal. Also fixes a pre-existing wipe: _update_device_caps checked the autofocus box before the capability flags were refreshed, persisting every gated preference at its unsupported value. It was already losing a saved hardware auto-exposure.
…oned Leaving a preview dialog left the pass running: accept() and reject() never raise a close event, so the mixin's teardown ran only on the window close button, and the abandoned pass held the unit until the next scan came back "Scanner busy". Teardown moves to done(), cancels the request, and Cancel reads "Stop preview" while one runs — it abandons the pass and keeps the tiles in hand. Use and Scan darken meanwhile; pressing either mid-pass hit the same refusal. Both preview dialogs get a progress bar off scan_progress, the pattern the prescan dialog already had. A batch names the frame in its phase string, so the sidebar reads "Frame 3 of 5 — Scanning" rather than a bare percentage. preview_positive takes the film type: reversal stock is already a positive and only gets the level stretch, matching services.assets.thumbnails, which decides the same thing off the stored process mode. A slide previewed as a negative. A successful eject drops the frame selection and the per-frame crops — they describe the piece of film that just came out, and the next strip silently reused them. Offset and Drift stay: they register the transport. The panel's dozen rows become Film / Quality / Framing / Output, each header hiding with its whole group, and the line above Scan says what pressing it does: frame count, resolution, extra passes, estimated size. Falling out of that: a pass the device cannot run now hides instead of sitting greyed (disabled-with-a-reason is reserved for one the film blocks), and a saved DPI the device does not offer snaps to the nearest stop it does — the box showed the unreachable number while the scan used the first entry. A measured strip's Offset spans ±2.5 mm, the same as Drift. It came from the 38 mm hardware blackout distance, which is a whole frame away and addresses the neighbour rather than correcting a boundary. Its saved value also stopped being floored at 0, which lost a negative offset on reopen. The DPI box takes a validator bounded by the device range, three tooltips lose their British spelling, and database_dialog's byte formatter moves to kernel.system.text as human_bytes.
nkscan ships wheels now, so the local editable path goes and the group needs no Rust toolchain. 0.9's Capabilities answers what the backend used to guess. The fast three-line read was asked for, refused, and remembered per device — a wasted round trip on every unit that has only the one ordering; `multi_line` says so up front, and the learned-refusal state goes with it. `max_samples` is the unit's own ceiling rather than a constant, `eject` decides whether the button appears, and `framing` gates Film format: only the two mechanisms that measure the film can be told a frame length, and a holder with its own frame table fixes it. scan_frame gained lock_white_balance, and its binding default is wrong for negatives: the Python signature holds the channels together while the library's own default does not. A colour negative meters per channel, which takes the orange mask off before the converter instead of quantising the blue record through it, so the film type now decides it through the library's own locks_white_balance. Its film names are NegPy's film-type values. max_frames stays unread on purpose: mapping it to adapter_frame_capacity would put the strip dialog on the index-addressed feeder path, and frames here come from discovery. The fake module follows the 0.9 capability shape, and two new tests hold it to the real extension where that is installed — every fake field a real property, and every film name one the binding accepts. That is the drift that made the fork's two hand-added booleans look fine offline.
…again Which frames a batch takes was the strip dialog's to say, so changing the selection meant another pass over the film. A Frames box takes the list directly — 1-6, 1,2,5, empty for every frame — and the dialog writes its picks there, so the preview stays a way to see the film rather than the only way to address it. It replaces the from-to spinboxes, which only ever appeared on a feeder that reports a slot count and left a measured strip with no frame control at all. One box now serves both: a feeder falls back to every slot it holds, a measured strip to every frame it finds. Text that cannot be read stops the scan and says so above the button, rather than scanning the wrong film quietly. The status line under Batch drops the frame list the box now owns, and keeps the windows, offset and drift. ScannerSettings loads through from_dict, which turns a saved frame_from/frame_to into a selection. It also ignores a key this version dropped: the constructor threw on one, and the whole blob then fell back to defaults, taking the output folder, DPI and filename pattern with it.
Every scan is written to a temp file in the output folder and renamed into place, and the temp file carried the final extension. With that folder open as a hot folder, the watcher indexes on extension alone, so it picked the file up mid-write, then the rename took it away: a dead tile and a Thumbnail Error in the log for every frame of a batch. The part file takes the .part suffix export and the sidecar writer already use, which no loader accepts. Nothing needs to know about scanning, and the other discovery paths are covered by the same change.
…the film IR and ICE read the same infrared pass, and ICE bakes its repair into the file, so a raw IR plane beside it has only cleaned pixels to find dust in. Ticking one now unticks the other, and a saved pair comes back as ICE alone. The film gate that greys both out on B&W and Kodachrome ran before the ICE capability and the device's film list were read, so it judged the previous device: switching to a Coolscan with B&W saved left IR enabled, and switching to one with colour negative left ICE greyed out. It runs after both now. Claude-Session: https://claude.ai/code/session_01H1QXANZKX1HEEQKu4oeipf
Both sliders were pinned to 160 px, so they took a small part of a wide dialog while a stretch ate the rest. They carry the framing, so they now split the free width and the Detect frames button keeps its own. The value labels are pinned to their widest reading: a label that grows with the value takes the width out of the slider it belongs to, and the handle steps sideways under the cursor mid-drag. Claude-Session: https://claude.ai/code/session_01TS769PZf9ifizfBTvFgHYx
A pass over the Scan panel and the three preview dialogs. The commit button asked for an object name that no rule matched, so the one control that moves the transport and writes files rendered like Browse. It is filled now, and goes hollow while it reads Stop. The line that says what a scan will cost was drawn in the placeholder grey, which is under AA at that size. It carries secondary weight, with the frame count and the byte figure in primary. The three dialogs ended in three different sentences. Every footer now reads reset · Cancel · Apply <object> · Scan <what>: the middle exit names what it keeps, and Enter scans once frames are ticked rather than walking back to the panel. Prescan gains the Scan exit it never had and loses Rescan from among its exits, since acquisition belongs at the top with the other two dialogs. Each surface stacked a progress bar, a status line and a summary that came and went independently, so the button underneath moved as a scan started or failed. StatusStrip reserves one row and shows the running pass, the message it left, or the resting summary. The strip dialog opened with ninety words above the tiles. One line stays; Offset and Drift explain themselves on their own sliders, and the ⓘ opens the rest from the user guide. Both sliders take the width the row has left, with the reading above the groove like the panel sliders — beside it, mm/frame clipped. Claude-Session: https://claude.ai/code/session_01TS769PZf9ifizfBTvFgHYx
marcinz606
force-pushed
the
feat/nkscan
branch
from
August 26, 2026 19:41
e83b259 to
cd5ce2a
Compare
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.
Adds a Nikon Coolscan backend built on the
nkscandriver, next to the existing SANE one. It gets what coolscan3 cannot give us: a native IR pass, hardware eject, openICE dust removal, and frame rectangles measured off the film instead of guessed from a fixed pitch.The scanner panel is now grouped (Film / Quality / Framing / Output) and the line above Scan says what pressing it will do: frame count, resolution, extra passes, estimated size. Leaving a preview dialog now stops the pass instead of leaving the unit busy until the next scan fails.
Tested on a Coolscan LS-50 with the SA-21 strip feeder: measured strips, IR, ICE (clean) full strip scans, eject