Stagione 93 / Episodio 2
La cornice
Il contenitore RIFF di WebP byte per byte: l'intestazione di dodici byte, i chunk VP8, VP8L e VP8X, l'ordine di ICCP, ANIM, ALPH, EXIF e XMP, e perché saperlo leggere aiuta a validare, a fare debug e a proteggere la privacy.
Note dell'episodio
Bentornati in bottega. Oggi non guardiamo le tessere, guardiamo la cornice. Perché un mosaico, prima ancora di essere un disegno, è un oggetto fisico: una lastra, un telaio, un retro con l'etichetta che dice chi l'ha fatto, quando e con quali materiali.
Un file WebP ha esattamente questo: una cornice che contiene i dati dell'immagine e una serie di etichette che dicono come interpretarli. E la buona notizia è che questa cornice è semplice. Così semplice che potete leggerla con un editor esadecimale, o con venti righe di Go. Nelle note dell'episodio trovate proprio un piccolo programma che lo fa.
WebP non ha inventato il suo contenitore. Usa RIFF, il Resource Interchange File Format, definito da Microsoft e IBM all'inizio degli anni Novanta. Se avete mai aperto un file WAV o un AVI, avete già incontrato RIFF senza saperlo.
L'idea di RIFF è elegante. Un file è una sequenza di blocchi, chiamati chunk. Ogni chunk comincia con un'etichetta di quattro caratteri, il cosiddetto FourCC, seguita da quattro byte che indicano la lunghezza del contenuto, seguiti dal contenuto stesso. Se la lunghezza è dispari, si aggiunge un byte di riempimento per tenere i chunk allineati a due byte.
Questo schema ha una conseguenza bellissima: un programma che legge il file può saltare i chunk che non conosce. Legge l'etichetta, legge la lunghezza, e se l'etichetta non gli dice niente avanza di quella lunghezza. È come un mosaicista che trova sul retro del pannello un'etichetta in una lingua che non parla: non la capisce, ma sa quanto è grande e la scavalca.
Un dettaglio che fa inciampare molti: in RIFF i numeri sono little-endian, cioè il byte meno significativo viene per primo.
Ogni file WebP comincia con dodici byte fissi. I primi quattro sono le lettere R, I, F, F. Poi quattro byte con la dimensione del file meno otto: si sottraggono gli otto byte appena letti, cioè l'etichetta e la dimensione stessa. Infine quattro lettere: W, E, B, P.
Questa è la firma del formato. Se un server deve capire se un file caricato è davvero un WebP, non deve fidarsi dell'estensione né del Content-Type mandato dal client: controlla che i byte da zero a tre siano RIFF e quelli da otto a undici siano WEBP. E già che c'è, controlla che la dimensione dichiarata sia coerente con la dimensione reale del file. Un file troncato o gonfiato si riconosce subito.
Dopo l'intestazione, nel caso più semplice, c'è un solo chunk con i dati dell'immagine. E qui ci sono due possibilità.
Se il chunk si chiama VP8 seguito da uno spazio, quattro caratteri in tutto, l'immagine è compressa con perdita. Dentro c'è un fotogramma chiave VP8, esattamente come lo descrive la RFC 6386. Quello spazio finale è importante: il FourCC deve essere di quattro byte, e "VP8" ne ha tre.
Se il chunk si chiama VP8L, con la L di lossless, l'immagine è compressa senza perdita con l'algoritmo specifico di WebP.
E basta. Un file WebP semplice è fatto di venti byte di intestazioni più i dati compressi. Non ci sono metadati, non c'è profilo colore, non c'è trasparenza nel caso con perdita. È il pannello più essenziale che la bottega possa produrre.
Le dimensioni dell'immagine, in questo caso, stanno dentro i dati compressi. Per VP8 c'è una piccola intestazione del fotogramma chiave con larghezza e altezza su quattordici bit. Per VP8L c'è una firma di un byte, il valore esadecimale 2F, seguita da larghezza e altezza meno uno, sempre su quattordici bit. Ecco da dove viene il limite dei 16383 pixel per lato: è il valore massimo che entra in quattordici bit.
Appena serve qualcosa in più, entra in gioco il formato esteso. Lo riconoscete perché il primo chunk dopo l'intestazione si chiama VP8X.
VP8X è l'etichetta sul retro del mosaico. Contiene un byte di flag, cioè di interruttori, che dichiarano in anticipo quali funzioni il file usa: se c'è un profilo colore ICC, se c'è trasparenza, se ci sono metadati EXIF, se ci sono metadati XMP, se è un'animazione. Poi ci sono alcuni byte riservati, e infine larghezza e altezza della tela, ognuna meno uno, su ventiquattro bit.
Perché dichiarare in anticipo? Perché così chi legge il file può decidere subito come prepararsi. Un browser che vede il flag di animazione sa che deve predisporre una sequenza di fotogrammi. Uno strumento che vede il flag del profilo colore sa che deve cercare il chunk ICCP prima di mostrare i pixel.
Nel formato esteso l'ordine non è libero. Prima VP8X. Poi, se c'è, ICCP con il profilo colore. Poi, se è un'animazione, ANIM con i parametri globali. Poi i dati dell'immagine: per un'immagine statica, un eventuale chunk ALPH con la trasparenza seguito da VP8, oppure un VP8L da solo; per un'animazione, una serie di chunk ANMF, uno per fotogramma. Infine, eventualmente, EXIF e XMP con i metadati.
Avere il profilo colore all'inizio e i metadati alla fine non è casuale. Il profilo serve prima di disegnare; i metadati, con la data dello scatto o il modello della fotocamera, possono aspettare. Un browser che riceve il file in streaming può cominciare a decodificare senza aver letto la coda.
E i chunk sconosciuti? La specifica dice che i lettori devono ignorarli. Ecco la tolleranza di RIFF che torna utile: domani si potrebbe aggiungere un nuovo tipo di chunk senza rompere i lettori esistenti.
Proviamo a fare il mosaicista che gira il pannello e legge l'etichetta. Avete un file. Leggete dodici byte: controllate RIFF e WEBP. Poi entrate in un ciclo: leggete quattro byte di etichetta, quattro di lunghezza in little-endian, stampate etichetta e lunghezza, saltate il contenuto più l'eventuale byte di riempimento. Ripetete finché il file non finisce.
Con queste poche operazioni vedete subito se un'immagine è con perdita o senza, se ha un profilo colore, se qualcuno ci ha lasciato dentro i dati EXIF con la posizione GPS dello scatto. Quest'ultima, tra l'altro, è una verifica che vale la pena fare prima di pubblicare foto scattate con uno smartphone.
Nelle note dell'episodio trovate un programma in Go, si chiama riffwalk, che fa esattamente questo e in più decodifica i campi di VP8X, VP8L e ANIM. Lo strumento ufficiale equivalente si chiama webpinfo e fa parte di libwebp: lo incontreremo nell'episodio sette.
Potreste chiedervi: perché devo sapere com'è fatta la cornice, se tanto uso una libreria? Per tre motivi molto pratici.
Primo, la validazione degli upload. Controllare la firma e i chunk è economico e vi permette di scartare file falsi prima di passarli a un decodificatore, che è sempre la parte più esposta.
Secondo, il debug. Quando un'immagine appare con i colori sbagliati, la prima domanda è: c'è un chunk ICCP? Quando una foto risulta ruotata, la domanda è: c'è un EXIF con l'orientamento, e il visualizzatore lo rispetta?
Terzo, la privacy. Sapere che i metadati stanno in chunk separati e ben riconoscibili vi permette di rimuoverli senza ricomprimere l'immagine.
Ricapitolando: un file WebP è un file RIFF. Dodici byte di intestazione, poi chunk etichettati con quattro caratteri. VP8 con lo spazio per la compressione con perdita, VP8L per quella senza perdita, VP8X per dichiarare le funzioni estese, e poi ICCP, ANIM, ANMF, ALPH, EXIF, XMP.
Ora che abbiamo la cornice in mano, nel prossimo episodio cominciamo a guardare le tessere. Partiamo da quelle più diffuse: la compressione con perdita, il disegno a mano libera che sacrifica qualche dettaglio per pesare molto meno. Ci sentiamo lì.