Files
Titano/Titano/Metadata/TiffPrimitives.cs
T
Alby96andClaude Opus 5 cc1d040ac0 Titano: motore e interfaccia per time-lapse, senza dipendenze di terze parti
Applicazione desktop completa per creazione e ottimizzazione di time-lapse.
Il vincolo che ne definisce l'architettura è l'assenza totale di componenti
di terze parti: il progetto non ha alcun PackageReference e non invoca processi
esterni. Oltre alla libreria standard di .NET si usano solo API native di
Windows (WIC, Media Foundation, GDI+, DWM) richiamate via P/Invoke scritto a mano.

Sono in-house tutte le parti che di norma si delegherebbero a una libreria:
il parser binario EXIF/XMP, la misura di luminanza e la curva di deflicker,
il calcolo del campo vettoriale di movimento con il motion blur sintetico,
il multiplexer MP4 e ogni controllo dell'interfaccia.

Scelte algoritmiche che meritano una nota:
- il deflicker usa una regressione lineare locale pesata con seconda passata
  robusta, così le rampe reali di luce (alba, tramonto) sopravvivono mentre
  lo sfarfallio del diaframma viene rimosso; una media mobile semplice le
  appiattirebbe entrambe;
- la sfocatura mancante si compone in quadratura con quella già incisa nello
  scatto, perché sommarla linearmente renderebbe l'immagine troppo morbida;
- la luminanza si misura come media logaritmica troncata, invariante alla
  scala e insensibile a cieli bruciati e ombre chiuse.

L'elaborazione non produce file temporanei e mantiene un'occupazione di memoria
stazionaria: buffer poolati e canale a capacità limitata rendono i fotogrammi
vivi indipendenti dalla lunghezza della sequenza.

Verificato con "Titano.exe --selftest": 23 controlli su una sequenza sintetica
dalle proprietà note, incluse la struttura del contenitore prodotto e la sua
ri-decodifica con il lettore di sistema. Tutti superati.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 21:40:26 +02:00

166 lines
5.3 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>
/// Lettore binario endian-aware su uno <see cref="ReadOnlySpan{T}"/> logico (buffer immutabile).
/// Tutti gli accessi sono limitati: un file malformato produce valori assenti, mai eccezioni.
/// </summary>
internal readonly struct ByteView
{
private readonly byte[] _data;
private readonly int _origin;
private readonly int _length;
public readonly bool BigEndian;
public ByteView(byte[] data, int origin, int length, bool bigEndian)
{
_data = data;
_origin = Math.Clamp(origin, 0, data.Length);
_length = Math.Clamp(length, 0, data.Length - _origin);
BigEndian = bigEndian;
}
public ByteView WithEndianness(bool bigEndian) => new(_data, _origin, _length, bigEndian);
public int Length => _length;
public bool InRange(long offset, long count)
=> offset >= 0 && count >= 0 && offset + count <= _length;
public bool TryGetByte(long offset, out byte value)
{
if (!InRange(offset, 1)) { value = 0; return false; }
value = _data[_origin + (int)offset];
return true;
}
public bool TryGetUInt16(long offset, out ushort value)
{
value = 0;
if (!InRange(offset, 2)) return false;
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;
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 (!InRange(offset, count) || count <= 0) return null;
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; -1 se assente. Usata per i fallback di scansione.</summary>
public int IndexOf(ReadOnlySpan<byte> pattern, int startAt = 0)
{
if (pattern.Length == 0 || pattern.Length > _length) return -1;
var haystack = new ReadOnlySpan<byte>(_data, _origin, _length);
int idx = haystack[Math.Clamp(startAt, 0, _length)..].IndexOf(pattern);
return idx < 0 ? -1 : idx + Math.Clamp(startAt, 0, _length);
}
public byte[] ToArray(long offset, long count)
{
if (!InRange(offset, count) || count <= 0) return [];
var result = new byte[count];
Array.Copy(_data, _origin + (int)offset, result, 0, (int)count);
return result;
}
}