No description
  • Python 63.1%
  • TypeScript 30.7%
  • CSS 5.6%
  • Dockerfile 0.3%
  • Shell 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Guillaume Quillery 56d6b56078
Some checks failed
CI / agent (Python 3.11) (push) Failing after 7m17s
Image / image (push) Successful in 27s
CI / interface (types et build) (push) Failing after 13m46s
Profils d'émulsion NegaFix (fournis par l'utilisateur)
L'agent lit les profils de films NegaFix de SilverFast depuis un dossier
fourni par le titulaire d'une licence (NEGSCAN_NEGAFIX_DIR, par défaut
<état>/negafix) ; le dépôt n'en contient aucun.

- negafix.py : format décodé (en-tête de 256 octets, XOR par la suite
  0x7B − 0x39·i, blocs de courbes à n points × 3 canaux en 0..32767),
  catalogue par jeu et par fabricant, inversion par profil ;
- interprétation retenue après essais sur de vrais négatifs Kodak et
  Fuji : entrée = négatif inversé et normalisé par canal, encodé γ 2,2 ;
  sortie = lumière linéaire. Les trois blocs, au rôle non documenté, sont
  proposés comme trois variantes ;
- recette : emulsion (« <jeu>/<film> ») et emulsion_variant, repris par
  « Appliquer à toutes », le copier-coller et les nouvelles vues ;
- GET /api/emulsions ; onglet Tonalité : jeu (ProScan 7200 par défaut,
  le plus proche du 10T), film groupé par fabricant, variante.

Tests sur des profils synthétiques.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:08:12 +02:00
.forgejo/workflows Publication de l'image sur le registre de src.zerolatitude.dev 2026-10-09 21:57:36 +02:00
agent Profils d'émulsion NegaFix (fournis par l'utilisateur) 2026-10-10 11:08:12 +02:00
deploy Agent de scan, interface web, scan brut et exports 2026-10-09 14:20:56 +02:00
docs Accès libre (--no-auth) : agent sans jeton ni appairage 2026-10-09 22:47:18 +02:00
scripts Agent de scan, interface web, scan brut et exports 2026-10-09 14:20:56 +02:00
ui Profils d'émulsion NegaFix (fournis par l'utilisateur) 2026-10-10 11:08:12 +02:00
.dockerignore Image Docker : agent, interface et SANE 2026-10-09 21:55:36 +02:00
.gitignore Agent de scan, interface web, scan brut et exports 2026-10-09 14:20:56 +02:00
compose.yaml Host : toute IP acceptée, Origin lié à l'hôte demandé, NEGSCAN_PUBLIC_HOSTS 2026-10-09 23:24:09 +02:00
Dockerfile Image Docker : agent, interface et SANE 2026-10-09 21:55:36 +02:00
LICENSE Agent de scan, interface web, scan brut et exports 2026-10-09 14:20:56 +02:00
README.md Profils d'émulsion NegaFix (fournis par l'utilisateur) 2026-10-10 11:08:12 +02:00

negative-scanner

Outil de numérisation de film (négatifs couleur et N&B, diapositives) pour scanners de film, à commencer par le Reflecta ProScan 10T. Il est simple par défaut et dispose d'un mode avancé. Ses fonctions s'inspirent de SilverFast.

Il se compose de deux parties :

  • un agent en Python, negscan, qui pilote le scanner via SANE et sert l'interface ;
  • une interface web en React, utilisable depuis la machine du scanner ou depuis une tablette ou un PC du réseau local.

Choix techniques, interface prévue et feuille de route : docs/ARCHITECTURE.md.

État

Ce qui fonctionne :

  • Capture : un aperçu rapide pour vérifier le cadrage, puis un scan brut pleine résolution (RGB + infrarouge, 16 bits linéaire), archivé tel quel dans un rouleau. La touche Espace scanne la vue placée. Le temps restant s'affiche (d'après les durées mesurées) et dans le titre de l'onglet ; un signal sonore discret annonce la fin du scan, le moment d'avancer la bande. L'étape Capture indique l'espace libre en nombre de vues ; un scan qui ne tiendrait pas sur le disque est refusé avant de lancer le scanner. Le scanner est détecté dès qu'on le branche, sous son nom (ProScan 10T). En mode avancé, le multi-échantillonnage moyenne 2 ou 4 passes recalées : il réduit le bruit du capteur, plus fort que le grain dans les zones denses d'un négatif.

  • Positif automatique : inversion du négatif (masque orange retiré, couleurs équilibrées) selon le type de film du rouleau. On peut basculer à tout moment entre Positif et Négatif. L'agent reconnaît un négatif couleur ou un N&B argentique et propose le bon réglage.

  • Développement :

    • recadrage à la souris (libre, 3:2, 4:3 ou 1:1), avec un recadrage automatique qui retire le passe-vues ;
    • rotation, miroir ;
    • exposition, contraste, température, teinte, saturation ;
    • points blanc et noir à la pipette ;
    • netteté (masque flou) et réduction du grain, avec une loupe 1:1 qui montre les pixels exportés ;
    • dépoussiérage infrarouge, avec l'affichage des défauts détectés (indisponible sur N&B argentique) ;
    • en mode avancé : histogramme, affichage de l'écrêtage, courbes RVB et par canal, rayon et seuil de netteté ;
    • zoom sur l'image : molette ou pincement sous le curseur, double-clic pour 100 % de l'aperçu, touches + − 0 ; on déplace l'image zoomée au bouton du milieu, avec Espace + glisser ou en saisissant le fond de la scène ;
    • comparaison avant/après (inversion automatique seule) avec un séparateur ;
    • densitomètre en mode avancé : valeur développée, densité du film et infrarouge sous le curseur ;
    • raviver les couleurs d'une diapositive passée ;
    • profils d'émulsion NegaFix (onglet Tonalité) : avec une licence SilverFast, les courbes mesurées de chaque film remplacent l'inversion adaptative, en trois variantes (voir « Profils NegaFix » plus bas) ;
    • en mode avancé : couleurs sélectives (teinte, saturation, luminosité de six teintes, les gris ne bougent pas) ;
    • annuler/rétablir (Ctrl+Z, Ctrl+Maj+Z), copier/coller les réglages d'une vue à l'autre (Ctrl+C, Ctrl+V), « Appliquer à toutes les vues » ;
    • suppression d'une vue (déplacée dans la corbeille du rouleau, d'où on peut la restaurer).

    Tout est enregistré dans une recette par vue : le brut ne change jamais. Une vue qui vient d'être scannée reprend l'orientation et les réglages de la précédente du rouleau.

  • Rouleaux : nom, date des photos (même approximative), suppression (vers la corbeille de la bibliothèque).

  • Export et téléchargement depuis le navigateur, au choix :

    • TIFF 16 bits ;
    • JPEG pleine résolution ;
    • JPEG 3 000 px ;
    • scan brut.

    Une planche contact réunit toutes les vues du rouleau sur un JPEG, numérotées, avec le nom, la date et le type de film.

    Les fichiers portent une légende (rouleau, vue, type de film) et leurs dates : prise de vue si elle est connue, numérisation. En mode avancé, un motif règle leur nom ({date}_{nom}_{vue}…).

  • Accès réseau en TLS, avec appairage des appareils.

Ce qui n'existe pas encore : profils d'émulsion, exports en Adobe RGB ou ProPhoto (sans calibration du capteur, ils ne contiendraient pas plus de couleurs que le sRGB). L'exposition du capteur par canal a été mesurée et abandonnée : sur ce scanner, elle n'améliore pas le rapport signal sur bruit (voir l'architecture).

Prérequis

  • Linux, avec sane-utils (fournit scanimage) et le backend pieusb, présent dans libsane1 sur Debian.

  • Python ≥ 3.11 et Node.js ≥ 20.

  • Un accès au scanner pour l'utilisateur. Sur Debian, c'est déjà le cas grâce aux règles udev de libsane1 (groupe scanner). Sinon :

    sudo tee /etc/udev/rules.d/60-negscan.rules >/dev/null <<'EOF'
    SUBSYSTEM=="usb", ATTR{idVendor}=="05e3", ATTR{idProduct}=="0145", TAG+="uaccess"
    EOF
    sudo udevadm control --reload-rules && sudo udevadm trigger
    

Pour vérifier : scanimage -L doit afficher device 'pieusb:libusb:…' is a PIE SF Scanner film scanner.

Branchez le scanner directement sur la machine, sans hub ni rallonge : le fabricant les déconseille.

Installation

cd agent
python3 -m venv .venv
.venv/bin/pip install -e '.[dev]'

cd ../ui
npm install
npm run build

Sur Debian, si python3 -m venv échoue parce que ensurepip est absent, deux solutions :

  • installer python3-venv ;
  • ou créer le venv avec --without-pip, puis y installer pip depuis https://bootstrap.pypa.io/pip/pip.pyz (.venv/bin/python pip.pyz install pip).

Utilisation

agent/.venv/bin/negscan serve          # ouvre le navigateur sur http://127.0.0.1:8765
agent/.venv/bin/negscan serve --mock   # scanner simulé, pour travailler sans le matériel

L'URL affichée au démarrage contient un jeton (#token=…). Elle appaire automatiquement le navigateur de ce poste.

Scanner, exporter, télécharger

  1. Support : choisissez ou créez un rouleau, puis indiquez le type de film (négatif couleur, négatif N&B, diapositive).
  2. Capture : placez la vue, faites un aperçu rapide, puis Scanner la vue n.
    • Archive : 4 000 dpi, environ 1 min 40 et 176 Mo par vue.
    • Rapide : 2 000 dpi, environ 1 min 10 : le gain de temps est faible, le scanner calibre et déplace sa tête au même rythme.
    • En mode avancé : toute résolution, et les modes RGB ou niveaux de gris.
  3. Développement : recadrez, redressez, réglez. L'aperçu suit chaque réglage, et un double-clic sur un curseur le remet à sa valeur par défaut. Netteté et grain ne se voient qu'à 100 % : la Loupe 1:1 s'ouvre d'elle-même, un clic sur l'image la déplace. Avant / après compare avec l'inversion automatique seule.
  4. Export : choisissez le format, puis Télécharger. Les exports appliquent la recette de chaque vue. Pour tout le rouleau : Tout télécharger (ZIP) après l'export, ou Tous les bruts (ZIP). Le navigateur reçoit le fichier, même depuis une tablette. Exporter les n vues prépare tout le rouleau et affiche un lien par fichier.

Les fichiers restent aussi sur la machine du scanner :

~/Scans/<rouleau>/
  roll.json         nom, type de film, date des photos
  raw/0001.tif      scan brut : TIFF 16 bits linéaire, RGB + infrarouge (4ᵉ canal), non développé
  raw/0001.json     métadonnées d'acquisition
  cache/0001.npy    aperçu réduit (vignettes, mesures d'inversion)
  edits/0001.json   recette : orientation, recadrage, réglages
  corbeille/…       vues supprimées (restaurables depuis l'étape Support)
  export/…          TIFF et JPEG exportés
~/Scans/corbeille/  rouleaux supprimés

Prévoyez de la place : un rouleau de 36 vues en Archive occupe environ 6 Go de bruts. Pour changer de dossier : --library, NEGSCAN_LIBRARY_DIR ou library_dir dans la configuration.

Depuis un autre appareil du réseau local

agent/.venv/bin/negscan serve --listen 0.0.0.0:8765
  1. Au premier lancement, l'agent génère un certificat auto-signé. Il affiche son empreinte SHA-256 : comparez-la à celle que montre le navigateur avant d'accepter l'avertissement. Pour utiliser votre propre certificat : --tls-cert/--tls-key.
  2. Ouvrez https://<machine>.local:8765 sur l'autre appareil.
  3. Saisissez le code d'appairage affiché au démarrage. Il vaut 10 minutes et ne sert qu'une fois. Pour en obtenir un autre :
    • lancer negscan pair ;
    • ou passer par Appareils → Appairer un nouvel appareil depuis un appareil déjà appairé.
  4. Pour lister et révoquer les appareils : negscan devices, negscan devices revoke <id>, ou le dialogue Appareils.

Hors loopback, l'agent ne sert jamais en clair.

Pour une machine dédiée (Raspberry Pi, petit serveur), voir le service systemd deploy/negscan.service.

Configuration

~/.config/negscan/config.toml, facultatif :

listen = "0.0.0.0:8765"
public_hosts = ["scanner.maison.lan"]   # noms supplémentaires acceptés dans l'en-tête Host
library_dir = "~/Scans"                  # rouleaux, scans bruts et exports
# tls_cert = "/chemin/cert.pem"
# tls_key = "/chemin/key.pem"

L'état (appareils appairés, codes, certificat) est stocké dans ~/.local/share/negscan/. On peut le déplacer avec --state-dir ou NEGSCAN_STATE_DIR.

Docker

Une image regroupe l'agent, l'interface compilée et SANE (scanimage, backend pieusb) :

docker compose pull && docker compose up -d   # image publiée : src.zerolatitude.dev/gquillery/negscan
docker compose up -d --build                  # ou construite depuis les sources
docker compose logs negscan                   # adresse de l'agent, empreinte du certificat

Puis ouvrir https://<machine>.local:8765 depuis n'importe quel appareil du réseau.

L'image est construite et publiée par le CI à chaque push sur main (:latest et :sha-<commit>).

  • Accès libre. Le conteneur est pensé pour une machine sans écran : il écoute sur tout le réseau local (0.0.0.0:8765, en TLS avec un certificat auto-signé) et ne demande ni jeton ni appairage (NEGSCAN_NO_AUTH=1). Toute personne du réseau local peut donc piloter le scanner, et voir et télécharger les scans. Les en-têtes Host et Origin restent vérifiés : une page d'un autre site ne peut pas piloter l'agent depuis le navigateur d'un visiteur. Pour revenir à l'appairage des appareils, mettre NEGSCAN_NO_AUTH=0 dans un fichier .env ; pour n'écouter que sur la machine, NEGSCAN_LISTEN=127.0.0.1:8765.
  • Réseau de l'hôte. Le conteneur partage le réseau de la machine (network_mode: host). L'agent y voit le vrai nom et les vraies IP, que son certificat TLS et son contrôle de l'en-tête Host attendent. On le joint par le nom de la machine, <nom>.local ou n'importe laquelle de ses IP ; un autre nom (DNS local, reverse proxy) se déclare dans NEGSCAN_PUBLIC_HOSTS, sans quoi l'agent répond « hôte non autorisé ».
  • Scanner. Tout /dev/bus/usb est monté, avec une règle cgroup pour les périphériques USB : le scanner reste accessible après un débranchement, même s'il change de numéro. Ce sont les droits posés par udev sur l'hôte qui décident :
    • sur un poste de bureau, udev donne le scanner à l'utilisateur connecté (ACL uaccess) : le conteneur tourne sous l'uid 1000, à ajuster avec NEGSCAN_UID ;
    • sur une machine sans session (Raspberry Pi, serveur), donner le scanner au groupe scanner par une règle udev (MODE="0660", GROUP="scanner", sur le modèle de celle des prérequis) et indiquer son gid dans SCANNER_GID (getent group scanner, 102 par défaut).
  • Données. Les appareils appairés, le certificat et les rouleaux vivent dans le volume negscan-data (/data/state, /data/scans). Pour garder les scans dans un dossier de l'hôte, monter ce dossier sur /data/scans ; il doit appartenir à l'uid du conteneur.
  • Appairer un appareil (avec NEGSCAN_NO_AUTH=0) : docker compose exec negscan negscan pair.
  • Le conteneur tourne sans privilèges : système de fichiers en lecture seule (sauf /data et /tmp), aucune capacité, no-new-privileges.

Profils NegaFix

negscan sait lire les profils de films NegaFix de SilverFast, mais n'en contient aucun : ce sont des données de LaserSoft Imaging, réservées aux titulaires d'une licence. Copiez les dossiers de profils (un sous-dossier par jeu, ProScan7200/, Other/…, contenant les fichiers .bin) dans le dossier negafix de l'agent :

  • sans Docker : ~/.local/share/negscan/negafix/ (ou le dossier désigné par NEGSCAN_NEGAFIX_DIR) ;
  • avec Docker : /data/state/negafix/ dans le volume, par exemple docker compose cp ProScan7200 negscan:/data/state/negafix/.

Ils apparaissent dans l'onglet Tonalité, après un redémarrage de l'agent. Le ProScan 10T n'a pas de jeu à lui ; le jeu ProScan 7200 (même électronique Pacific Image) est proposé par défaut. Le format et l'interprétation retenue sont décrits dans negafix.py.

Développement

scripts/dev.sh          # agent + Vite avec rechargement à chaud (ouvrir l'URL « Sur ce poste »)
scripts/dev.sh --mock   # idem avec le scanner simulé

cd agent && .venv/bin/pytest
cd ui && npm run typecheck

La CI Forgejo (.forgejo/workflows/ci.yml) fait tourner les tests de l'agent sous Python 3.11, la version minimale (celle de Raspberry Pi OS bookworm), et vérifie les types et la compilation de l'interface, à chaque push sur main.

Dans un éditeur en Flatpak, l'USB n'est visible que depuis l'hôte. scripts/dev.sh y lance donc l'agent via flatpak-spawn --host.

Licence

GPL-3.0-or-later.