Files
Titano/Titano/Metadata/TiffPrimitives.cs
T
Alby96andClaude Opus 5 e476ebb648 Correzione dei difetti emersi sui DNG GoPro reali
Provato il programma sulle quattro sequenze in K:\2024 (oltre 3000 scatti DNG
convertiti da GPR con le applicazioni Adobe). Ne sono usciti quattro difetti,
tutti latenti perché la verifica sintetica usa JPEG piccoli.

1. Risoluzione letta dalla miniatura. Nei DNG la IFD0 descrive un'anteprima
   256x192 marcata NewSubfileType=1 e l'immagine vera vive in una SubIFD:
   leggendo ImageWidth dalla prima directory il programma credeva di lavorare
   su 256x192 e avrebbe esportato un video di quelle dimensioni. Ora si sceglie
   la directory dell'immagine principale, preferendo quelle non marcate come
   versione ridotta.

2. Lettura dei metadati lentissima. Si leggevano 4 MiB per file a prescindere:
   179 s per una cartella da 1005 scatti. La finestra sul file ora si estende
   solo quando un offset punta oltre quanto gia' caricato, e di un DNG da 15 MiB
   ne bastano 128 KiB. Stessa cartella: 7,4 s.

3. Decodifica corrotta quando interveniva il ridimensionamento. Il convertitore
   a 48bppRGB precedeva lo scaler; con i codec RAW quell'ordine restituisce
   righe disallineate, immagini a strisce e luminanza sbagliata di due stop,
   senza segnalare alcun errore. Lo scaler ora precede il convertitore, come
   indica Microsoft. I 16 bit per canale restano.

4. Multiplexer che corrompeva il primo fotogramma chiave. Il buffer di
   conversione Annex-B veniva riallocato senza copiare il contenuto: le NAL gia'
   scritte per quel campione diventavano zeri e il campione usciva con un
   prefisso di lunghezza nullo in testa. Il file restava formalmente valido ma
   il lettore di sistema si fermava dopo sedici fotogrammi su quaranta. Con i
   JPEG del test il campione non superava mai la capacita' iniziale, per questo
   non era mai emerso; ora il buffer parte piccolo, cosi' il percorso di crescita
   viene esercitato da qualunque sequenza.

Aggiunto inoltre il vincolo di conformita' dei codec. Una sorgente 4:3 da
4000x3000 supera il Livello 5.2 di H.264: l'encoder hardware la accetta e
dichiara il Livello 6.0, ma i decodificatori comuni non aprono il file. La
risoluzione viene ora ricondotta al massimo riproducibile conservando le
proporzioni, e la riduzione e' dichiarata nella barra di stato invece di
avvenire in silenzio. Le sorgenti 16:9 fino al 4K UHD non sono toccate.

Il campo di movimento non viene piu' calcolato quando non serve: con pose da
30 s su intervalli da 34 s lo shutter angle e' gia' 317 gradi e non c'e'
sfocatura da sintetizzare. Il render della sequenza aurora passa da 1,3 a
8,5 fotogrammi al secondo.

La verifica del motore sale a 24 controlli: si aggiungono l'invarianza della
luminanza alla scala di decodifica e l'allineamento delle NAL dentro ogni
campione, i due invarianti che avrebbero intercettato i difetti 3 e 4. Nuovo
comando --diagnose per esaminare una cartella reale.

Verificato sui file dell'utente: 40 fotogrammi a risoluzione nativa, H.264 e
HEVC, entrambi riletti per intero dal lettore di sistema.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:01:35 +02:00

260 lines
8.5 KiB
C#

namespace Titano.Metadata;
/// <summary>Tipi di dato definiti dalla specifica TIFF 6.0 / Exif 2.32.</summary>
internal enum TiffType : ushort
{
Unknown = 0,
Byte = 1,
Ascii = 2,
Short = 3,
Long = 4,
Rational = 5,
SByte = 6,
Undefined = 7,
SShort = 8,
SLong = 9,
SRational = 10,
Float = 11,
Double = 12,
Ifd = 13,
}
/// <summary>Voce di una IFD: 12 byte nel file, con il valore inline se occupa ≤ 4 byte.</summary>
internal readonly struct TiffEntry
{
public readonly ushort Tag;
public readonly TiffType Type;
public readonly uint Count;
/// <summary>Offset assoluto (rispetto all'inizio del blocco TIFF) dove risiede il valore.</summary>
public readonly long ValuePosition;
public TiffEntry(ushort tag, TiffType type, uint count, long valuePosition)
{
Tag = tag;
Type = type;
Count = count;
ValuePosition = valuePosition;
}
public static int SizeOf(TiffType type) => type switch
{
TiffType.Byte or TiffType.Ascii or TiffType.SByte or TiffType.Undefined => 1,
TiffType.Short or TiffType.SShort => 2,
TiffType.Long or TiffType.SLong or TiffType.Float or TiffType.Ifd => 4,
TiffType.Rational or TiffType.SRational or TiffType.Double => 8,
_ => 0,
};
public long ByteLength => (long)SizeOf(Type) * Count;
}
/// <summary>
/// Finestra su una sorgente di byte, caricata progressivamente.
///
/// I file RAW pesano decine di megabyte ma le directory dei metadati stanno nei primi
/// chilobyte: leggerli per intero, o anche solo a blocchi fissi di qualche megabyte,
/// costa centinaia di millisecondi per file e rende impraticabile l'ingestion di una
/// sequenza da mille scatti. Qui si legge il minimo indispensabile e si estende la
/// finestra solo se un offset punta oltre quanto già caricato.
/// </summary>
internal sealed class ByteWindow
{
/// <summary>Granularità delle letture: evita una chiamata di I/O per ogni tag.</summary>
private const int ChunkSize = 64 * 1024;
private readonly Stream? _stream;
private readonly long _total;
private byte[] _buffer;
private int _loaded;
/// <summary>Sorgente già interamente in memoria (segmento APP1 di un JPEG, chunk PNG).</summary>
public ByteWindow(byte[] data, int length)
{
_buffer = data;
_loaded = Math.Clamp(length, 0, data.Length);
_total = _loaded;
}
/// <summary>Sorgente su file: si carica subito solo la testa.</summary>
public ByteWindow(Stream stream, long total, int initial)
{
_stream = stream;
_total = Math.Max(0, total);
_buffer = new byte[(int)Math.Min(_total, Math.Max(ChunkSize, initial))];
Ensure(Math.Min(_total, initial));
}
public byte[] Buffer => _buffer;
/// <summary>Byte effettivamente presenti in memoria.</summary>
public int Loaded => _loaded;
/// <summary>Byte complessivamente disponibili nella sorgente.</summary>
public long Total => _total;
/// <summary>Garantisce che i primi <paramref name="end"/> byte siano caricati.</summary>
public bool Ensure(long end)
{
if (end <= _loaded) return true;
if (end > _total || _stream is null || end > int.MaxValue) return false;
long target = Math.Min(_total, Math.Max(end, (long)_loaded + ChunkSize));
if (_buffer.Length < target)
{
long grown = Math.Min(_total, Math.Max(target, (long)_buffer.Length * 2));
Array.Resize(ref _buffer, (int)grown);
}
try
{
_stream.Position = _loaded;
while (_loaded < target)
{
int read = _stream.Read(_buffer, _loaded, (int)(target - _loaded));
if (read <= 0) break;
_loaded += read;
}
}
catch (IOException)
{
return false;
}
return end <= _loaded;
}
}
/// <summary>
/// Lettore binario endian-aware su una <see cref="ByteWindow"/>, con origine propria.
/// Tutti gli accessi sono limitati: un file malformato produce valori assenti, mai eccezioni.
/// </summary>
internal readonly struct ByteView
{
private readonly ByteWindow _window;
private readonly int _origin;
public readonly bool BigEndian;
public ByteView(ByteWindow window, int origin, bool bigEndian)
{
_window = window;
_origin = Math.Max(0, origin);
BigEndian = bigEndian;
}
public ByteView(byte[] data, int origin, int length, bool bigEndian)
: this(new ByteWindow(data, Math.Clamp(origin + length, 0, data.Length)), origin, bigEndian)
{
}
public ByteView WithEndianness(bool bigEndian) => new(_window, _origin, bigEndian);
/// <summary>Byte teoricamente raggiungibili da questa origine.</summary>
public long Length => Math.Max(0, _window.Total - _origin);
/// <summary>Byte già caricati a partire da questa origine: limite delle ricerche per scansione.</summary>
private int Available => Math.Max(0, _window.Loaded - _origin);
public bool InRange(long offset, long count)
=> offset >= 0 && count >= 0 && _window.Ensure(_origin + offset + count);
public bool TryGetByte(long offset, out byte value)
{
if (!InRange(offset, 1)) { value = 0; return false; }
value = _window.Buffer[_origin + (int)offset];
return true;
}
public bool TryGetUInt16(long offset, out ushort value)
{
value = 0;
if (!InRange(offset, 2)) return false;
var data = _window.Buffer;
int p = _origin + (int)offset;
value = BigEndian
? (ushort)((data[p] << 8) | data[p + 1])
: (ushort)((data[p + 1] << 8) | data[p]);
return true;
}
public bool TryGetUInt32(long offset, out uint value)
{
value = 0;
if (!InRange(offset, 4)) return false;
var data = _window.Buffer;
int p = _origin + (int)offset;
value = BigEndian
? ((uint)data[p] << 24) | ((uint)data[p + 1] << 16) | ((uint)data[p + 2] << 8) | data[p + 3]
: ((uint)data[p + 3] << 24) | ((uint)data[p + 2] << 16) | ((uint)data[p + 1] << 8) | data[p];
return true;
}
public bool TryGetInt32(long offset, out int value)
{
bool ok = TryGetUInt32(offset, out uint raw);
value = unchecked((int)raw);
return ok;
}
public bool TryGetSingle(long offset, out float value)
{
value = 0f;
if (!TryGetUInt32(offset, out uint raw)) return false;
value = BitConverter.UInt32BitsToSingle(raw);
return true;
}
public bool TryGetDouble(long offset, out double value)
{
value = 0d;
if (!InRange(offset, 8)) return false;
TryGetUInt32(offset, out uint a);
TryGetUInt32(offset + 4, out uint b);
ulong raw = BigEndian ? ((ulong)a << 32) | b : ((ulong)b << 32) | a;
value = BitConverter.UInt64BitsToDouble(raw);
return true;
}
public string? GetAscii(long offset, long count)
{
if (count <= 0 || !InRange(offset, count)) return null;
var data = _window.Buffer;
int p = _origin + (int)offset;
int n = (int)count;
// Le stringhe Exif sono NUL-terminate; alcuni firmware riempiono di spazi.
int end = n;
for (int i = 0; i < n; i++)
{
if (data[p + i] == 0) { end = i; break; }
}
while (end > 0 && (data[p + end - 1] == ' ' || data[p + end - 1] == '\t')) end--;
if (end <= 0) return null;
return System.Text.Encoding.Latin1.GetString(data, p, end);
}
/// <summary>
/// Ricerca di un pattern di byte nella porzione già caricata; -1 se assente.
/// Usata dai ripieghi per scansione, che lavorano su sorgenti interamente in memoria.
/// </summary>
public int IndexOf(ReadOnlySpan<byte> pattern, int startAt = 0)
{
int available = Available;
if (pattern.Length == 0 || pattern.Length > available) return -1;
int from = Math.Clamp(startAt, 0, available);
var haystack = new ReadOnlySpan<byte>(_window.Buffer, _origin + from, available - from);
int index = haystack.IndexOf(pattern);
return index < 0 ? -1 : index + from;
}
public byte[] ToArray(long offset, long count)
{
if (count <= 0 || !InRange(offset, count)) return [];
var result = new byte[count];
Array.Copy(_window.Buffer, _origin + (int)offset, result, 0, (int)count);
return result;
}
}