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>
166 lines
5.3 KiB
C#
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;
|
|
}
|
|
}
|