HotXLS legge la geometria reale in pixel di un'immagine dall'header del file immagine stesso prima che venga scritta qualsiasi geometria di disegno. GetImageSize in lxImage.pas smista sull'estensione del file verso GetJpegSize, GetGifSize, GetBmpSize, GetPngSize o GetWmfEmfSize, converte i pixel in punti con un fattore di 0.75, e restituisce -1 quando davvero non può dirlo
Quell'ultima clausola è la parte che ha richiesto più tempo per venire bene. Un reader di dimensioni che non può fallire è un reader di dimensioni che mente, e una bugia sulla geometria dell'immagine non viene a galla al momento dell'inserimento. Viene a galla settimane dopo, quando qualcuno apre la cartella di lavoro e il logo dell'azienda è un rettangolo schiacciato
Perché un'immagine ha bisogno della sua geometria reale prima di entrare nel disegno?
Perché il formato del foglio di calcolo memorizza l'estensione, non l'immagine. Quando AddPicture o Images.AddFromFile posiziona un'immagine, la parte di disegno registra una larghezza e un'altezza esplicite per la shape — in DrawingML sono gli attributi cx e cy dell'elemento a:ext, in English Metric Units, secondo la sezione 20.4 di ECMA-376 Part 1. Nulla in quel record dice "usa la dimensione naturale dell'immagine", quindi la dimensione naturale deve venire da qualche parte, e l'unico posto dove esiste sono i byte dell'immagine. Sbagliala di un fattore tre e l'immagine viene stirata di un fattore tre, in ogni applicazione che apre la cartella di lavoro, per sempre. La conversione di unità da quelle dimensioni in pixel fino agli EMU è una questione separata, coperta in l'articolo sulle unità EMU e lo scaling; questo pezzo riguarda il passo precedente, quello in cui i numeri in pixel vengono ottenuti. Il fallback quando un reader fallisce è 100 per 100, e quel numero è deliberatamente, ovviamente sbagliato. Non è una stima di una dimensione plausibile, è un marcatore che significa "nessuno è riuscito a leggere questo file", ed è molto meglio di un numero sbagliato dall'aspetto plausibile perché un utente lo segnala subito
GIF: il descriptor che non era mai stato analizzato
GetGifSize è nuovo, e prima che esistesse ogni GIF inserita in un foglio di lavoro usciva esattamente 100 per 100: non c'era proprio un ramo GIF, quindi l'estensione ricadeva sul braccio di default di GetImageSize e restituiva -1. È l'header più economico del gruppo da analizzare, il che rende l'omissione leggermente imbarazzante e molto facile da sistemare. La specifica GIF89a mette il Logical Screen Descriptor subito dopo la firma di sei byte, e il §18 definisce i suoi primi due campi come larghezza e altezza dello schermo logico, ognuno un valore unsigned a 16 bit memorizzato little-endian. In termini di file sono i byte 6 e 7 per la larghezza, i byte 8 e 9 per l'altezza, quindi una singola lettura di dieci byte copre il controllo della firma ed entrambe le dimensioni
function GetGifSize(const AFileName: WideString; var AWidth, AHeight: Double): Integer;
var
f: TWFileStream;
Header: array [0.. 9] of Byte;
wWidth, wHeight: Word;
begin
Result:= - 1;
AWidth:= 100;
AHeight:= 100;
f:= TWFileStream.Create(AFileName, fmOpenRead);
try
// Firma "GIF8", poi il Logical Screen Descriptor con larghezza e
// altezza come word little-endian agli offset 6 e 8
if (f.Read(Header[0], SizeOf(Header))= SizeOf(Header))and
(Header[0]= Ord('G'))and (Header[1]= Ord('I'))and
(Header[2]= Ord('F'))and (Header[3]= Ord('8')) then
begin
wWidth:= Header[6]or (Header[7] shl 8);
wHeight:= Header[8]or (Header[9] shl 8);
if (wWidth> 0)and (wHeight> 0) then
begin
AWidth:= wWidth* 0.75;
AHeight:= wHeight* 0.75;
Result:= 1;
end;
end;
finally
f.Free;
end;
end;
Nota la forma: i valori di fallimento vengono assegnati per primi, e il percorso di successo è l'unica cosa che li sovrascrive. Ogni reader in lxImage.pas segue quell'ordine, perché rende "non sono riuscito a leggerlo" il default invece di qualcosa da ricordare a ogni uscita anticipata. Controllare solo GIF8 invece della stringa intera GIF87a o GIF89a è intenzionale: il layout del descriptor è identico in entrambe le revisioni, e non c'è alcun beneficio nel rifiutare un file la cui geometria è leggibile
Cosa cambia davvero una dimensione di header BMP pari a 12?
GetBmpSize legge i quattro byte all'offset 14 come dimensione dell'header DIB e fa un ramo sul valore, perché un BMP con dimensione di header 12 memorizza le sue dimensioni con una larghezza di campo completamente diversa da ogni altro BMP. Il §2.2.2 di [MS-WMF] dispone la famiglia: BITMAPCOREHEADER è di 12 byte e porta larghezza e altezza a 16 bit, mentre BITMAPINFOHEADER, BITMAPV4HEADER e BITMAPV5HEADER iniziano tutti con biWidth e biHeight signed a 32 bit. Leggere quattro byte dove il file ne ha due non fallisce con un tono alto: tira dentro il conteggio dei piani e la profondità di bit come metà alta di un numero e riporta una bitmap larga diverse centinaia di migliaia di pixel. La seconda trappola nello stesso header è il segno di biHeight: un valore negativo non significa un'altezza negativa, significa che le righe DIB sono memorizzate top-down invece di bottom-up, che è un'affermazione sull'ordine dei pixel senza nulla a che fare con la geometria
f.Seek(14, soFromBeginning);
if f.Read(HeaderSize, 4)= 4 then
begin
if HeaderSize= 12 then
begin
// BITMAPCOREHEADER memorizza dimensioni a 16 bit
if (f.Read(wWidth, 2)= 2)and (f.Read(wHeight, 2)= 2) then
begin
lWidth:= wWidth;
lHeight:= wHeight;
Result:= 1;
end;
end
else if (f.Read(lWidth, 4)= 4)and (f.Read(lHeight, 4)= 4) then
Result:= 1;
end;
// ...
// Un biHeight negativo segna solo un DIB top-down; la grandezza è la dimensione
if (Result= 1)and (lWidth> 0)and (lHeight<> 0) then
begin
AWidth:= lWidth* 0.75;
AHeight:= Abs(lHeight)* 0.75;
end
else
Result:= - 1;
La guardia finale merita una lettura attenta. L'altezza è testata contro <> 0 invece che > 0, perché il negativo è legale e lo zero no, e Abs è applicato solo al punto di conversione. La larghezza non ha un caso equivalente: un biWidth negativo non è definito dal formato, quindi viene trattato come file corrotto e retrocesso a -1. Passare verso l'alto un valore negativo grezzo produrrebbe invece un'estensione negativa nel record di disegno, che viene clampata, ignorata o renderizzata come shape capovolta a seconda dell'applicazione che apre la cartella di lavoro
JPEG: quali marker sono davvero frame header?
GetJpegSize accetta l'insieme di marker [$C0..$C3, $C5..$C7, $C9..$CB, $CD..$CF], e i tre buchi in quell'intervallo sono l'intero punto. ITU-T T.81 §B.2.2 definisce il frame header come SOFn, e il frame header è il posto dove vivono il numero di righe e di campioni per riga, in ogni tipo di frame e non solo in quelli comuni. I marker che cadono nello stesso intervallo numerico ma non sono frame header sono $C4 (DHT, define Huffman tables), $C8 (JPG, riservato) e $CC (DAC, define arithmetic coding conditioning). Un reader che riconosce solo baseline, extended sequential e progressive — $C0, $C1 e $C2 — funziona su quasi ogni JPEG prodotto da una fotocamera o da un tool di screenshot, e poi fallisce in silenzio su quelli che contano per i workflow documentali: il JPEG lossless ($C3), le varianti a codifica aritmetica ($C9 fino a $CB) e i tipi di frame gerarchici ($CD fino a $CF) scansionano oltre il frame header, finiscono fuori dal file e atterrano nel fallback. L'imaging medico e le pipeline di documenti scansionati producono esattamente quei file
// Ogni marker SOFn porta le dimensioni del frame; $C4 (DHT), $C8 (JPG)
// e $CC (DAC) condividono l'intervallo ma non sono frame header
if Seg in [$C0.. $C3, $C5.. $C7, $C9.. $CB, $CD.. $CF] then
begin
ReadLen:= f.Read(Dummy[0], 3);
ReadLen:= f.Read(wHeight, 2);
wHeight:= swapByte(wHeight);
ReadLen:= f.Read(wWidth, 2);
wWidth:= swapByte(wWidth);
found:= True;
Break;
end
Due dettagli in quelle sette righe. I tre byte saltati sono la word di lunghezza del segmento più il byte di precisione del campione, quindi la lettura atterra sul conteggio delle righe; JPEG memorizza l'altezza prima della larghezza, che è l'inverso della maggior parte dei formati e una trasposizione facile da fare. E i valori passano per swapByte perché JPEG è big-endian ovunque, la stessa ragione per cui GetPngSize fa passare i suoi valori IHDR a 32 bit per lswapByte. La camminata sui segmenti ha la sua guardia: quando un marker non è senza parametri, il reader prende la lunghezza di due byte, la inverte con il byte swap, e rifiuta di fare seek se il valore è sotto 2 — una lunghezza più corta del proprio campo si tradurrebbe in un seek all'indietro, e un seek all'indietro in un loop di scansione è un loop infinito su un file che controlla qualcun altro
Restituire meno uno batte restituire un numero plausibile
Il filo comune attraverso tutti e cinque i reader è che GetImageSize distingue tre esiti invece di due. 1 significa che la geometria è stata letta dal file. -1 significa che non lo è stata, e il chiamante dovrebbe usare il suo default. 7 è restituito da GetWmfEmfSize e significa qualcosa di completamente diverso: dice ad AddPicture di prendere la dimensione dal record BSE del metafile, perché un WMF o un EMF porta la sua estensione nel record immagine della cartella di lavoro stessa invece che a un offset fisso nell'header del file. La gestione dei metafile in generale è coperta in l'articolo sul decoder EMF e WMF limitato. GetJpegSize mostra perché quella distinzione ha bisogno di un flag esplicito invece di un'inferenza: il loop di scansione può terminare in tre modi — ha trovato un frame header, ha sbattuto sulla fine del file, o ha mollato su una lunghezza di segmento corrotta — e solo il primo assegna wWidth e wHeight. Senza il Boolean found, gli altri due percorsi convertirebbero qualsiasi cosa quelle locali contenessero per caso e consegnerebbero il risultato verso l'alto come un successo. Le locali non inizializzate sono spesso zero, e zero è la peggiore risposta possibile qui: sopravvive a ogni controllo di validità a valle e produce un'immagine invisibile che nessuno può selezionare o cancellare
var
W, H: Double;
begin
case GetImageSize(FileName, W, H) of
1: Sheet.AddPicture(FileName, Row, Col, W, H);
7: Sheet.AddPicture(FileName, Row, Col); // metafile: dimensione dal record BSE
else
// W e H sono 100 x 100; decidi una policy invece di accettare un segnaposto
LogWarning('unreadable image geometry: ' + FileName);
end;
end;
Dove stanno i limiti onesti
Questi reader analizzano deliberatamente un header e si fermano. Non decodificano i pixel, quindi riportano le dimensioni memorizzate e niente di più, e ci sono casi in cui le dimensioni memorizzate non sono tutta la storia. Una GIF animata i cui frame sono più piccoli dello schermo logico riporta comunque lo schermo logico, il che è corretto per il layout e può non combaciare con ciò che un viewer mostra. Un JPEG con un tag EXIF di orientamento che specifica una rotazione di 90 gradi riporta le dimensioni del frame non ruotato, quindi una foto verticale scattata con un telefono può tornare come orizzontale. Un DIB con rapporto d'aspetto dei pixel non quadrato riporta i conteggi grezzi di pixel, e il fattore 0.75 assume 96 DPI ovunque. Nessuno di questi è un bug nella lettura dell'header e nessuno è risolvibile senza decodificare l'immagine, che non è il compito di questo layer: sono confini documentati, quindi se la tua pipeline gestisce foto da telefono, leggi tu il tag di orientamento e scambia i valori prima di chiamare AddPicture. Tutto il resto su posizionamento delle immagini, ancoraggio e modello a oggetti del disegno è coperto in l'articolo su grafici, immagini e disegni
HotXLS legge e scrive XLS, XLSX, ODS e CSV nativamente in Delphi e C++Builder senza richiedere alcuna installazione di Excel, e l'inserimento delle immagini passa per questo percorso di header in ognuno di essi. I dettagli sul supporto dei formati e sulle licenze sono nella pagina di prodotto della HotXLS Delphi spreadsheet component