Skip to content

WIP: Spektrafilm packv3 - #22335

Draft
piratenpanda wants to merge 9 commits into
darktable-org:masterfrom
piratenpanda:spektrafilm_packv3
Draft

piratenpanda wants to merge 9 commits into
darktable-org:masterfrom
piratenpanda:spektrafilm_packv3

Conversation

@piratenpanda

@piratenpanda piratenpanda commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR is a WIP to prepare spektrafilm for the new arctic upsampling. This is under heavy development so don't test this yet please.

Current features:

  • Add a proper upgrade path:
    • Unify pack lookup
    • Old edits without a pack_hash get read as 0.3.3 and request that data, as this has been the only published pack so far
    • Old edits keep hanatos2025 spectral upsampling but can upgrade to other methods
    • edits stamp their pack_hash so edits keep their packs pinned to them
    • Add update check button as we don't download new data automatically
    • upgrade the pack and manifest generator to pack v3 (this lives here currently https://github.com/piratenpanda/darktable-spektrafilm/tree/arctic_upsampling)
    • Add support for arctiv2026beta4 spectral upsampling
  • Add print gamma rgb sliders
  • grain clump blur changed to 0.89 as spektrafilm's experimental branch, holding old edits at 0.8
  • Add black/whitepoint adjustment as requested several times
  • Fix Double-X paper auto selection

To-Do

  • extensive testing

Checklist

  • I have read CONTRIBUTING.md and the
    coding style.
  • I don't have merged master into the topic branch.
  • The pull request is one logical change, and every commit compiles on its
    own.
  • I ran the relevant tests: unit tests, src/tests/integration/ where the
    pixelpipe is touched, or darktable-cli as a headless smoke test.
  • New user-visible strings use _(), new preferences are registered in
    data/darktableconfig.xml.in.
  • A RELEASE_NOTES.md entry was added (only needed if fixing an issue in a release). Do not reference GitHub issues.

Test instructions

not yet done, do not test

AI assistance

Co-created with Claude Opus 5

@piratenpanda
piratenpanda marked this pull request as draft September 19, 2026 10:43
@piratenpanda
piratenpanda force-pushed the spektrafilm_packv3 branch 4 times, most recently from 2109c20 to 30cf119 Compare September 21, 2026 05:00
The engine's print curve morph already carries per-channel gamma
(morph_gamma_r/_g/_b, the reference's gamma_rgb beside its scalar gamma),
but nothing set it: commit_params only wrote morph_gamma, so the three
stayed at their 1.0 defaults and the paper's layers could only be moved
together.

Expose them as print_gamma_r/_g/_b, multiplicative on print_contrast the
way the engine combines them, and put the sliders in the print chemistry
section next to development time.

The fields are appended, so legacy_params gains an explicit v2 layout to
copy through. That also fixes the v1 path, which memcpy'd sizeof(current)
out of an older blob.
sf_fetch_resolve_pack_dir() answers "which pack do I render this edit
with"; nothing answers "which packs are on disk", which is what a
pack-management UI needs before it can offer a choice.

sf_fetch_list_packs() walks the same two locations the resolver does and
returns them in the same precedence order, reading the same 32-byte LUT
header rather than loading anything. The header peek grows an optional
id-string output on the way, so an entry can name its table and not just
hash it.

Excludes what the resolver excludes: a directory whose header hash
disagrees with its name, one missing pack.json or profiles/, and the
".incoming-<hash>" directory of a download in progress.
Upstream's GrainParams.blur is 0.89 on the experimental branch, fitted by
study b80 jointly with the multiplicative density unsharp mask. Both
halves of that mask are already here at their b80 values
(grain_usm_sigma 0.7, grain_usm_amount 1.5), so grain was being softened
by less than the mask was built to take back.

Not gated on the pack's spektrafilm_version, which cannot express the
difference: upstream's main and experimental branches both report 0.3.4
in pyproject.toml while carrying 0.65 and 0.89 respectively, so a pack
exported from either declares the same string. This is the failure the
lut_hash exists to avoid, and grain blur is not a property of the
spectral table either.

Gated instead on the edit, which is what the value actually belongs to:
SF_GRAIN_BLUR_FACTOR becomes grain_blur_base, appended to the v3 tail,
and legacy_params() pins migrated edits to 0.8 so no existing render
moves. The shipped presets keep 0.8 for the same reason.

Both pipelines compute the sigma host-side, so the kernel is unchanged.
* consume reflectance spectral upsampling tables

  A pack has always held exactly one spectral upsampling table, implied by
  spectra_lut.f32 and consumed one way: integrate the stored spectra
  against the film sensitivity. pack_format 3 names its tables in
  pack.json's spectral_upsampling array and may carry a reflectance one
  beside the irradiance one, which is consumed differently.

  Loading. The loader fills a table array either way -- a format 2 pack's
  single file is entered as the irradiance table it always was, so nothing
  past the loader needs a per-format branch. The declaration decides which
  files to read and how to consume them, never identity: each table's hash
  and id still come from its own header, because a declaration can name the
  wrong file and the hash is what every edit records. A reflectance table
  with no scene illuminant is refused rather than consumed under the film's,
  and so is a file name with a path separator in it, which would reach
  outside the directory the fetcher verified.

  Consuming. A reflectance table stores a surface, not light, recovered
  under its own scene illuminant. Relative to the irradiance path it needs
  the stored spectra relit by the film's reference illuminant; the result
  normalized per channel on the table's own emitted neutral, sampled
  through the same cubic the runtime addresses the table with, so a method
  with sharp near-white structure still resolves to gray; the input CAT16
  and the gamut-compression remap both centered on the table's scene white
  rather than the film's, since that is the white the runtime projects
  under and the relight is what carries the film's illuminant instead; and
  both halves of the hanatos sensitivity adaptation skipped, having no part
  in the reflectance formula.

  Getting any of those wrong renders plausibly rather than failing, which
  is why the pack refuses to describe a reflectance table incompletely.

  Selection is by content hash, not identifier: a pack can revise a
  method's table without renaming it, and an edit has to pin the one it was
  developed against. 0 takes the pack's default, which is what every
  existing edit and every format 2 pack gets.

  Validated against the reference: building the arctic2026beta04 table with
  kodak_portra_400 sensitivities matches upstream's own interpolator and
  formula to 1.8e-14 over all 110592 cells, and a neutral input lands on
  (1,1,1) exactly.

  SF_PACK_FORMAT_MAX stays 2 here. Nothing can load a format 3 pack until
  the module can also choose between its tables.

* choose the spectral upsampling table, and load format 3 packs

  With the engine able to consume a reflectance table, the module needs a
  way to pick one and SF_PACK_FORMAT_MAX has to admit the packs that carry
  them. Both land here, together: widening the range earlier would load a
  format 3 pack with no way to reach anything but its default table.

  upsampling_hash records the choice by content hash, like film_hash and
  paper_hash beside it, so a pack revision that reorders or drops a table
  leaves the edit still naming the one it was developed against. 0 is the
  pack's default, which is what every existing edit gets and the only thing
  a format 2 pack offers -- legacy_params() sets it explicitly for the same
  reason grain_blur_base is set there.

  The combobox is built from the loaded pack rather than from params
  introspection, since its entries are whatever that pack carries. It is
  hidden below two entries: a format 2 pack offers no choice, and a
  one-entry combobox is a control that cannot be used. The tables are
  snapshotted alongside the profiles in _rescan(), because both come from
  the same pack and a reload that changes one changes the other.

  Both halves of the hanatos sensitivity adaptation are shown inert while a
  reflectance table is selected. They belong to the irradiance formula and
  the engine does not run them, so leaving them live would offer two
  switches that do nothing.

  The selection also joins the sim cache key: it decides which table the
  whole tc LUT is built from, so it is a different sim and not a different
  parameter within one.

* define sf_table_t before the struct that holds an array of it

  struct sf_pack_t declares sf_table_t tables[SF_MAX_TABLES], but the
  typedef sat below it, so the file did not compile:

    spektra_sim.c:315:3: error: unknown type name 'sf_table_t'

  Move the typedef above the struct. No other change.

* downloads land under the data directory, not the cache

  _packs_dir() builds <user data>/darktable/spektrafilm/packs, and says
  why in its own comment: a pack cannot be rebuilt from anything on the
  machine, so clearing the cache would leave every edit made against an
  older spectral table unreproducible until it was fetched again. The file
  header described the cache directory instead, which is where someone
  looking for a downloaded pack would not find one.

* read the table list from the pack directory, not the loaded pack

  The spectral upsampling combobox never appeared. _scan_tables() asked
  the loaded pack what tables it had, but _pack is assigned only by
  _ensure_sim(), which runs in the pixelpipe -- so it is still NULL while
  gui_update() builds the GUI. The list came back empty, and the widget
  hid itself on the "nothing to choose" rule and stayed hidden.

  _scan_profiles() does not have this problem because it reads the pack
  directory rather than the loaded pack. Do the same here.

  sf_pack_peek_tables() reads pack.json and each table's 32-byte header and
  nothing else, so the GUI can ask what a pack declares without loading
  tens of megabytes it has no use for. It reports the tables default-first,
  the order sf_pack_load() puts them in, so a position means the same thing
  to both. A directory that is not a readable pack reports no tables, which
  stays the existing "no choice" case rather than becoming an error.
* resolve the automatic paper the same way in the GUI and the pipeline

  On a stock that names no target print, the print development slider was
  live but inert, and reset to a time that did nothing.

  _auto_paper_entry() picks the automatic paper in three tiers: the film's
  named target print, then a print stock with the same channel model, then
  the first printing entry there is. _resolve_stock() had only the first
  and the last. The middle tier is the one that matters, because
  target_print is optional in the pack and the entry list is sorted by
  display name, so the last resort bears no relation to the film.

  Kodak Double-X names no target print, so the two disagreed: the GUI
  resolved it to Kodak Print Film 2302 and enabled the slider over that
  paper's five development times, while the pipeline printed on Fujifilm
  Crystal Archive Type II, which is characterised at one development and
  ignores the setting. A B&W negative was being auto-printed on color
  paper, and the slider described a paper that was not in use.

  Give _resolve_stock() the same channel-model tier, and pass the film's
  model from commit_params(). The film lookup passes -1, having no such
  preference.

  Both now resolve Double-X to Kodak Print Film 2302.

* put the print development slider on the paper in force

  Selecting Double-X left the print development time reading 0 even though
  the paper it resolves to, print film 2302, is characterised at five
  times. 0 renders correctly -- it means the stock's own default, and the
  loader takes the middle member -- but it is the one discontinuity in the
  slider's range, so it reads as though nothing is set.

  _film_changed() already re-baselines the film's own time on a stock
  switch, for the reason the comment there gives: a time from the previous
  stock means nothing on this one. The print side needs the same, because
  a film switch can move the automatic paper -- Double-X resolves to 2302,
  a color negative to a color paper -- and it was never updated. The
  auto branch of _paper_changed() made it worse by zeroing the time rather
  than resolving what auto points at.

  Both now take the time from the paper actually in force. Only on auto:
  an explicitly chosen paper has not changed, so neither should its time.

  Resolving "the paper in force" was already spelled out inside
  _update_development_sensitivity(); it is now _effective_paper_entry(),
  shared by all three callers so they cannot drift apart.

  Double-X lands on 5 min. A color negative stays at 0, its auto paper
  having no development family, which is what 0 is for.
* log which pack the table list came from

  The spectral upsampling combobox hides itself below two tables, so a
  pack that declares fewer than expected is indistinguishable from a build
  that has no such control. The usual cause is a pack installed somewhere
  the module does not look, which the existing pack-resolution line does
  not show either: it reports the directory, not what was found in it.

  Report the count, the directory, and each table's hash and id, beside
  the resolution line that names the directory.

* find a pack by any table it carries, not just its default

  An edit records the spectral table it was developed against. The
  manifest match only ever compared the pack's top-level lut_hash, which
  names its DEFAULT table, so an edit made with any other table of a
  pack_format 3 pack could not fetch the pack that holds it: the download
  reported that no pack with that table is published, while the pack
  sitting in the manifest carried it all along.

  Also consider the per-table rows the manifest lists under "tables". The
  directory a pack installs into is still named for its default table's
  hash, which is what the resolver expects to find and what the entry
  already reports.

  A format 2 pack lists no tables and matches as before.

  Against a manifest holding the 0.3.4 pack, an edit asking for the arctic
  table now resolves to it instead of failing; an edit asking for the
  hanatos table resolves as it already did, that being the default.

* resolve an edit to the pack it was made with

  lut_hash names the spectral table and nothing else. A release can carry
  a table forward byte-identical while its profiles move -- hanatos2025 is
  the same bytes in 0.3.3 and 0.3.4, while all 31 profiles differ by up to
  0.42 density on the print films. Two packs then answered to one
  lut_hash, and an edit had no way to say which it was developed against:
  opening it against the other one rendered differently and reported
  nothing, the recorded table having genuinely matched.

  Packs now declare a pack_hash over their constants, profiles and tables.
  Read it into sf_pack_t, record it in the edit beside lut_hash under the
  same rule -- stamp while they still agree, never overwrite a
  disagreement, since that would erase the mismatch the field exists to
  report -- and prefer it everywhere a pack is chosen:

    - resolving the directory, before the table is consulted at all, since
      two installed packs can carry one table
    - matching the manifest, where an edit naming a pack gets that pack or
      nothing rather than falling back to the other one carrying its table

  0 means an edit made before packs had an identity, or a pack that
  declares none. Both fall back to matching the table exactly as before,
  so nothing existing changes behaviour. Presets record 0 deliberately:
  a preset is not tied to the pack it was authored on.
* always show the spectral upsampling selector

  Hiding the selector below two tables made a pack carrying fewer tables
  than expected indistinguishable from a build without the feature. Show
  it always, insensitive when there is nothing to pick, so it names the
  table in use either way.

* pin a table-less legacy edit to the first published pack

  An edit migrated from params version 1 or 2 that recorded no spectral
  table was made when exactly one data pack existed -- 0.3.3, carrying
  hanatos2025 at 565f4ec4. There was nothing else to render with, so that
  is the table it used.

  Left at 0 it resolved instead to whatever is installed, which on a
  machine that has since moved to a later pack means different profiles
  and a different render, reported as "no table recorded, taking what is
  installed".

  Set in legacy_params() and nowhere else, because that is the only place
  the record's own version proves it predates the field. A fresh edit also
  starts at 0 and must keep resolving to the default pack, so the resolver
  cannot tell the two apart and must not try.

* name the print gamma controls as the reference does

  The reference calls these gamma_factor and gamma_factor_red/green/blue.
  darktable's film side already follows that -- film_gamma_factor is
  labelled "development gamma" -- so only the print side called the same
  quantity contrast, and the three per-channel sliders inherited the odd
  name out.

  Label the scalar "print gamma" and the channels "print gamma
  red/green/blue", and follow the wording through their tooltips. The
  print_contrast field keeps its name: it is what presets and styles
  refer to, and renaming it would buy nothing the label does not.

  Placement is unchanged -- the scalar stays outside the chemistry
  collapsible, the three channel trims inside it.

* offer every installed table, not only the resolved pack's

  An edit made against 0.3.3 resolves to a pack carrying one table, so the
  selector listed one entry and sat inert. The edits most likely to want a
  method a later pack added were exactly the ones that could never reach
  it.

  List the tables of every installed pack instead, one entry per table --
  packs overlap, 0.3.3's table being byte-identical to the one 0.3.4
  carries, and two entries for it would read as two methods while picking
  between the packs arbitrarily. First pack wins, which is the precedence
  sf_fetch_list_packs() already reports.

  Choosing a table from another pack has to move the edit to that pack, or
  the pipeline would resolve to the old one and fail to find the table.
  The callback releases the recorded pack and table so resolution follows
  the choice, and both are stamped again on the next render, naming
  whichever pack actually supplied it. Resolution consults an explicitly
  chosen table first for the same reason: it may only exist elsewhere,
  which is what choosing it means.

* show the method name for a pack that declares none

  pack_format 2 named no tables, so its header says only the kind --
  "irradiance_xy_tc@0.3.3" -- and that string reached the combobox, where
  everything else reads as a method name.

  Two ways to the right one. A later pack carrying the same table declares
  it, so its identifier teaches the entry its method even though the bytes
  come from the earlier pack; the directory stays the earlier pack's,
  precedence being about which files to read. Failing that, the first
  published table is named outright: exactly one pack ever declared no
  tables, and this is the one it held.

  Reads hanatos2025 either way, with or without a later pack installed.

* record what params version an edit was born at

  Installing 0.3.4 changed how existing edits rendered. An edit predating
  pack_hash carries only its table, 565f4ec4, and 0.3.4 carries that table
  forward byte-identical -- so it answered the edit exactly as 0.3.3 does,
  outranked it as the hand-installed pack, and rendered it on profiles
  that differ by up to 0.42 density.

  pack_hash cannot express the fix, because the pack the edit wants
  declares none. origin_version records the params version the edit was
  first written at, which does: 1 and 2 predate pack identity, and so
  predate every pack that carries one, so such an edit belongs on a pack
  that declares none. legacy_params() sets it; the resolver looks for an
  installed pack carrying the table that also declares no identity.

  A field rather than a reserved pack_hash value. pack_hash 0 is where
  every fresh edit sits until its first render stamps it -- stamping needs
  a loaded pack, which needs a resolution -- so 0 cannot also mean old,
  and a sentinel inside the hash space would collide with a real pack
  eventually. It also generalises: a later migration can ask how old a
  record is without inventing another marker.

  pack_hash is never stamped onto such an edit. Reaching an identified
  pack means its own is not installed, and stamping would make the
  substitution permanent, surviving the moment its pack comes back. That
  case is now logged, being the one substitution that changes the picture
  while every hash still matches.

  With 0.3.4 hand-installed above 0.3.3: a migrated edit resolves to
  0.3.3, a fresh one to 0.3.4, an edit naming 0.3.4 to 0.3.4, and choosing
  a table still moves the edit to whichever pack supplies it.

* gray the adaptation pair as soon as a reflectance table is picked

  Both halves of the hanatos sensitivity adaptation belong to the
  irradiance formula, and sf_sim_build() does not run them for a
  reflectance table. They were already shown inert for one -- but only
  from gui_update(), which does not re-run when the combobox changes. So
  selecting arctic2026beta04 left the two toggles live until the module
  was reloaded, offering switches that changed nothing.

  Move the decision into a helper and call it from the selection callback
  as well, that being exactly the moment the pair becomes inert or live
  again.

* offer the pack a legacy edit needs instead of substituting one

  An edit predating pack identity, opened on a machine where only a later
  pack is installed, rendered on that pack. The table matched, so nothing
  objected -- but a release can carry a table forward byte-identical while
  its profiles move, which is what 0.3.3 and 0.3.4 do, so it rendered
  differently with every hash in agreement. All 31 profiles differ there,
  by up to 0.42 density on the print films.

  The pack it wants is still published, so the honest answer is to say so.
  Report no usable pack and let the module offer to fetch it, exactly as
  for an edit whose table is missing outright.

  The download asks for the edit's table rather than for nothing: the
  manifest is ordered oldest first, so asking for the table lands on the
  pack the edit was made with, where asking for nothing lands on whatever
  is flagged default -- which need not be the same pack, and will not be
  once a newer one takes over.

  An edit that has since chosen a table itself is not pinned this way and
  is left alone.

* offer the pack that carries the table an edit chose

  An edit rendering on a non-default table could not be rescued. Opened
  where that table is not installed it reported that it cannot be
  rendered, and the row that would have fetched the pack stayed hidden --
  while the update check found the very same pack and installing it fixed
  everything.

  Choosing a table moves an edit to whichever pack supplies it, and
  releases the recorded pack and table so resolution follows the choice.
  Such an edit therefore carries upsampling_hash and a lut_hash of 0. The
  data row read lut_hash alone, so it resolved to whatever was installed,
  called it an exact match and concluded nothing was missing.

  Read the table the edit actually needs -- the chosen one where there is
  one -- and look for it by every table a pack declares rather than by the
  one in its header, which names only the default. The download then asks
  for that table, and the manifest match walks the same declarations.

  An arctic edit with only 0.3.3 installed now offers 0.3.4.

* one lookup for which pack an edit needs

  Three hashes answer three different questions -- a table the edit was
  moved to, the pack it was developed against, the table it was stamped
  with -- and they are consulted in priority order. The renderer read all
  three; the module's data row read one, then two. Every time they
  disagreed the result was the same shape of bug: an edit the renderer
  refused while the row reported nothing missing, or a pack fetched that
  was not the one wanted. Three fixes in, the disagreement was still the
  underlying fault rather than any of its symptoms.

  _edit_pack_dir() answers it once, for both, and names the four outcomes
  worth telling apart: the pack is here, a substitute is here, a
  pre-identity edit has lost its pre-identity pack, or there is nothing.
  It also reports the table to ask a download for.

  No behaviour change intended beyond the removed disagreement: the order
  is the renderer's existing one, and _legacy_pack_unavailable() goes
  away, its answer now being one of the four states.
* offer to check the repository for a newer data pack

  Downloading was only ever offered when resolution failed: nothing
  installed, or installed but not the table this edit wants. A pack
  already satisfying every edit on the machine could be years behind and
  nothing would say so, the module having no reason to read the manifest.

  sf_fetch_check_start() runs the existing worker as far as the manifest
  and stops, reporting through the same status the download uses. Newer
  means the published default pack is not installed under its own
  identity; a pack declaring none cannot be compared and reports nothing
  to do, since fetching it again would change nothing.

  The data row, idle until now whenever resolution succeeded, carries the
  button. Offered rather than run on its own, because it is network
  traffic nobody asked for, and because a check that found something would
  otherwise have to decide what to do about it.

  Nothing is adopted behind an existing edit: a fetched pack installs
  beside the one you have, and only a fresh edit or a deliberate table
  choice renders on it. That is what makes the button safe to offer at all
  -- before an edit could name the pack it was made with, the same button
  would have quietly changed how existing work rendered.

* name the pack version when offering or fetching one

  The download row said only "download data pack", and the progress line
  only which file was in flight. Neither said which pack, so there was no
  way to tell from the UI whether the repository was offering the release
  you expected -- the one thing worth knowing before accepting a pack that
  brings revised profiles.

  The manifest carries spektrafilm_version per entry and the worker had it
  all along. Record it for the entry actually chosen, report it through
  sf_fetch_available_version(), and use it in the button, the status line
  and the per-file progress message: "download data pack 0.3.4",
  "downloading pack 0.3.4: profiles/kodak_gold_200.json (7/34)".

  A label and never an identity. Two packs can report one version -- the
  0.3.3 and 0.3.4 trees both did, from an editable install whose metadata
  predated the bump -- which is exactly why selection goes by pack_hash
  and this string only ever gets shown.

* check every published pack, and move the check under advanced

  The check asked _parse_manifest() for the entry a download would take,
  which is the default pack. During a rollout the default is deliberately
  the older one, so the question "is anything newer published" was put to
  the pack least likely to be. Against a repository publishing 0.3.3 as
  default and 0.3.4 beside it, the check reported that the published pack
  declares no identity -- true of 0.3.3, and beside the point.

  Scan every entry instead, skipping ones this darktable could not load
  and ones declaring no identity, and report the last not installed; the
  manifest is emitted oldest first, so that is the newest.

  A completed check also no longer announces itself as a download failure.
  It shared the worker's finish path, which knows only installed or
  failed, and a check that finds nothing is neither. The answer belongs in
  the row beside the button that asked, not in a toast.

  That row is now its own control under advanced. The row at the top of
  the module reports a pack missing right now, which belongs there because
  nothing renders without it; asking whether a newer one exists is
  housekeeping and does not.

* log the manifest url the check actually read

  The repository and ref are both configurable and both default to
  upstream's, so a check reporting nothing new is ambiguous: a repository
  with nothing newer and a ref pointing at the wrong branch produce the
  same answer, and nothing recorded which was read.

  Log it beside the pack-resolution line that already names the directory
  chosen.

* say what the update check decided about each pack

  The check reports one line for a whole manifest, so a wrong answer gives
  nothing to work back from: a pack skipped for its format, for declaring
  no identity, or for looking installed are indistinguishable from a
  repository with nothing new.

  Log the decision per entry, and say so when the manifest does not parse
  rather than treating it as an empty one.

* refresh the update row once the check has answered

  The check runs on the worker thread, but pressing the button painted the
  row straight away and never again: the poll that repaints during a fetch
  is started inside _update_data_row(), which the update button does not
  call. So the row kept the answer it had before the check began -- "the
  installed data packs are up to date", from the moment nothing was known
  yet -- while the worker went on to find a newer pack and record it.

  Start the same timer the download path uses. _update_data_row() clears
  it once the fetch stops running, and _data_poll_cb repaints both rows on
  every tick, so the answer lands as soon as it exists.

* finish the update row -- last in advanced, own progress, installed state

  Three things the row got wrong once it started working.

  It sat above the gamut compression toggle, in the middle of a section of
  render controls. It is housekeeping and the only control there that
  talks to the network, so it goes last.

  A fetch started from it reported progress in the row at the top of the
  module, which is about a pack missing right now -- neither missing nor
  urgent describes an update. The row now carries its own progress and the
  top row stays hidden for a fetch this one started.

  The bar is laid over the button rather than placed under it: the row is
  three lines in a narrow panel and a fourth that exists only mid-fetch
  makes the section jump. A thin strip along the button's bottom edge
  reads as the button filling, and the theme's progressbar colors apply
  -- a css gradient on the button could not reach them, @-colors being
  private to the provider that defines them. It is pass-through, so clicks
  belong to the button underneath.

  And a pack that had just been downloaded was still offered for download,
  nothing clearing the available hash on install. The row asks the disk
  instead, and says installed once it is there.

* rebuild the module when a pack fetched from the update row lands

  Downloading 0.3.4 left the spectral upsampling selector showing the one
  table the old pack had, until the image was left and re-entered.

  _update_data_row() rebuilds the GUI on the running-to-done transition,
  which is what rescans the profiles and the table list. A fetch started
  from the update row returned before recording that it was running, so
  the transition never happened: the fetch finished, the pack was in
  place, and the module kept the list it had read before the download.

  Record the state before returning. Re-entering the image worked because
  it re-runs gui_update() by another route.
* expose the scanner black and white point

  A slide carries base density and never reaches d-max, so a scan of one
  is washed out until its endpoints are placed. The module placed them and
  said nothing: the levels were compiled in at 0.98 and 0.01 and the fit
  ran whenever a positive stock was scanned, with no way to move either
  end.

  The reference has four controls here -- scanner.black_correction,
  white_correction and their levels -- so take those. The levels are
  sRGB-encoded display values as upstream states them, decoded on use;
  0.01 and 0.98 decode to exactly the constants that were compiled in, so
  nothing renders differently until a slider is moved. legacy_params()
  sets the same, and both corrections default on, which is what the module
  already did.

  One correction on its own moves only its own endpoint, the other being
  held where the film puts it, so the corrected end still lands exactly
  where asked ([cr] _correction_fucntion).

  On the scanner tab between output and sharpness, which is the order the
  scan stage applies them in: boost, then the endpoint fit, then blur and
  unsharp, then viewing glare. Insensitive unless a positive stock is
  being scanned -- a print has its own paper white and black.

* place the black and white point on a print too

  Only a scan of positive film had its endpoints placed. Upstream corrects
  a second case ([cr] the `not scan_film and print is negative` branch): a
  print on negative-type paper, whose range is set by the film behind it
  rather than by the paper, so the paper never reaches its own black or
  white and the print is flat until the ends are put somewhere.

  Every paper in the pack is negative-type, so this was every print -- and
  with the controls gated on the scan case, the four sliders were inert
  for anyone not scanning slides.

  The endpoints are the film at its extremes, its fog floor and its D-max
  ([st] printing.expose), carried through the enlarger and developed on
  the paper exactly as a pixel is. They cross over on the way: the film's
  thinnest point passes the most light and prints darkest, so it is the
  print's black.

  Same affine fit as the scan case, so one correction on its own still
  moves only its own endpoint. A paper and exposure that land both ends
  together leave the correction off rather than fit an arbitrary slope.

  The GUI gate follows: sensitive for a positive-film scan or a print on
  negative paper, inert for a negative scan and for positive paper, which
  carry their own endpoints.
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