HotXLS čte skutečnou geometrii obrázku v pixelech z hlavičky samotného obrazového souboru, ještě než se zapíše jakákoli kreslicí geometrie. GetImageSize v lxImage.pas rozesílá podle přípony souboru do GetJpegSize, GetGifSize, GetBmpSize, GetPngSize nebo GetWmfEmfSize, převádí pixely na body faktorem 0.75 a vrací -1, když to skutečně neumí určit
Ta poslední klauzule je část, která se ladila nejdéle. Čtečka velikosti, která nemůže selhat, je čtečka velikosti, která lže, a lež o geometrii obrázku se neprojeví v okamžiku vložení. Projeví se o týdny později, když někdo otevře workbook a firemní logo je zmáčknutý obdélník
Proč obrázek potřebuje reálnou geometrii, než se dostane do kresby?
Protože tabulkový formát ukládá rozsah, ne obraz. Když AddPicture nebo Images.AddFromFile umísťuje obrázek, kreslicí část zaznamená pro tvar explicitní šířku a výšku — v DrawingML to jsou atributy cx a cy elementu a:ext, v English Metric Units, podle ECMA-376 Part 1 §20.4. V tom záznamu nic neříká "použij přirozenou velikost obrazu", takže přirozená velikost musí přijít odněkud, a jediné místo, kde existuje, jsou bajty obrazu. Zmeňte to o faktor tři a obrázek je natažený o faktor tři, v každé aplikaci, která workbook otevře, navždy. Převod jednotek z těch pixelových rozměrů dolů na EMU je samostatná záležitost, pokrytá v článku o EMU jednotkách a škálování; tenhle kus je o kroku před tím, kde se pixelová čísla vůbec získají. Fallback, když čtečka selže, je 100 na 100, a to číslo je záměrně, očividně špatné. Není to odhad věrohodné velikosti, je to značka znamenající "nikdo tenhle soubor nepřečetl", a je daleko lepší než věrohodně vypadající špatné číslo, protože uživatel ho nahlásí okamžitě
GIF: deskriptor, který se nikdy neparsoval
GetGifSize je nový, a než existoval, vyšel každý GIF vložený do worksheetu přesně jako 100 na 100 — žádná GIF větev vůbec nebyla, takže přípona propadla na defaultní rameno GetImageSize a vrátila -1. Tohle je nejlevnější hlavička z celé sady k parsování, takže je opomenutí mírně trapné a velmi snadno opravitelné. Specifikace GIF89a dává Logical Screen Descriptor hned za šestibajtový podpis a §18 definuje jeho první dvě pole jako šířku a výšku logické obrazovky, každé jako unsigned 16bitovou hodnotu uloženou little-endian. V souborových pojmech to jsou bajty 6 a 7 pro šířku, bajty 8 a 9 pro výšku, takže jediné desetibajtové čtení pokryje kontrolu podpisu i oba rozměry
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
// Podpis "GIF8", pak Logical Screen Descriptor s šířkou a
// výškou jako little-endian slovy na offsetech 6 a 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;
Všimněte si tvaru: hodnoty selhání se přiřadí první a cesta úspěchu je jediná věc, která je přepisuje. Každá čtečka v lxImage.pas jde v tomto pořadí, protože to dělá "nepřečetl jsem to" defaultem, ne něčím, co se musí pamatovat při každém předčasném exitu. Kontrola jen GIF8 místo plného řetězce GIF87a nebo GIF89a je záměrná — rozložení deskriptoru je v obou revizích identické a nemá smysl odmítat soubor, jehož geometrie je čitelná
Co vlastně mění velikost BMP hlavičky 12?
GetBmpSize čte čtyři bajty na offsetu 14 jako velikost DIB hlavičky a podle hodnoty se větví, protože BMP s velikostí hlavičky 12 ukládá rozměry v úplně jiné šířce pole než každý jiný BMP. [MS-WMF] §2.2.2 rozvrhuje rodinu: BITMAPCOREHEADER má 12 bajtů a nese 16bitovou šířku a výšku, zatímco BITMAPINFOHEADER, BITMAPV4HEADER a BITMAPV5HEADER všechny začínají 32bitovými signed biWidth a biHeight. Čtení čtyř bajtů tam, kde jich soubor má dva, neselže hlasitě; vtáhne počet rovin a bitovou hloubku jako horní polovinu čísla a ohlásí bitmapu širokou několik set tisíc pixelů. Druhá past v téže hlavičce je znaménko biHeight: záporná hodnota neznamená zápornou výšku, znamená, že jsou řádky DIB uložené shora dolů místo zdola nahoru, což je tvrzení o pořadí pixelů, které s geometrií nemá nic společného
f.Seek(14, soFromBeginning);
if f.Read(HeaderSize, 4)= 4 then
begin
if HeaderSize= 12 then
begin
// BITMAPCOREHEADER ukládá 16bitové rozměry
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;
// ...
// Záporné biHeight jen značí top-down DIB; velikost je absolutní hodnota
if (Result= 1)and (lWidth> 0)and (lHeight<> 0) then
begin
AWidth:= lWidth* 0.75;
AHeight:= Abs(lHeight)* 0.75;
end
else
Result:= - 1;
Závěrečnou stráž stojí za to přečíst pozorně. Výška se testuje proti <> 0 místo > 0, protože záporná je legální a nula ne, a Abs se aplikuje jen v bodě převodu. Šířka nemá žádný ekvivalentní případ — záporné biWidth formát nedefinuje, takže se bere jako poškozený soubor a demotuje na -1. Předat nahoru místo toho surovou zápornou hodnotu by v kreslicím záznamu vyprodukovalo záporný rozsah, který se podle aplikace otevírající workbook upne, ignoruje nebo vykreslí jako převrácený tvar
JPEG: které markery jsou skutečně frame hlavičky?
GetJpegSize přijímá sadu markerů [$C0..$C3, $C5..$C7, $C9..$CB, $CD..$CF] a tři mezery v tom rozsahu jsou celý smysl. ITU-T T.81 §B.2.2 definuje frame hlavičku jako SOFn a právě ve frame hlavičce žijí počty řádků a vzorků na řádek, ve všech typech frame, ne jen v těch běžných. Markery, které padají do stejného číselného rozpětí, ale nejsou frame hlavičky, jsou $C4 (DHT, definice Huffmanových tabulek), $C8 (JPG, rezervováno) a $CC (DAC, definice aritmetického kódování). Čtečka rozpoznávající jen baseline, extended sequential a progressive — $C0, $C1 a $C2 — funguje na skoro každém JPEGu, který vyprodukuje foťák nebo nástroj na snímky obrazovky, a pak tiše selže na těch, které záleží pro dokumentové workflow: lossless JPEG ($C3), varianty s aritmetickým kódováním ($C9 až $CB) a hierarchické typy frame ($CD až $CF) všechny proseknou kolem frame hlavičky, vyběhnou z konce souboru a přistanou ve fallbacku. Lékařské zobrazování a pipeline skenovaných dokumentů produkují přesně tyhle soubory
// Každý marker SOFn nese rozměry frame; $C4 (DHT), $C8 (JPG)
// a $CC (DAC) sdílejí rozsah, ale nejsou frame hlavičky
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
Dva detaily v těch sedmi řádcích. Tři přeskočené bajty jsou slovo délky segmentu plus bajt přesnosti vzorku, takže čtení dopadne na počet řádků; JPEG ukládá výšku před šířkou, což je opak většiny formátů a snadná záměna. A hodnoty procházejí swapByte, protože JPEG je celý big-endian, stejný důvod, proč GetPngSize prohání své 32bitové IHDR hodnoty přes lswapByte. Procházení segmentů má vlastní stráž: když marker není bez parametrů, čtečka vezme dvoubajtovou délku, byte-swapne ji a odmítne seekovat, pokud je hodnota pod 2 — délka kratší než vlastní pole by se přeložila na zpětný seek a zpětný seek ve scan smyčce je nekonečná smyčka na souboru, který někdo jiný ovládá
Vrátit mínus jedničku je lepší než vrátit věrohodné číslo
Společné vlákno napříč všemi pěti čtečkami je, že GetImageSize rozlišuje tři výsledky místo dvou. 1 znamená, že se geometrie přečetla ze souboru. -1 znamená, že nepřečetla, a volající by měl použít svůj default. 7 vrací GetWmfEmfSize a znamená něco úplně jiného: říká AddPicture, aby velikost vzal místo toho ze záznamu BSE metafilu, protože WMF nebo EMF nese svůj rozsah v obrázkovém záznamu workbooku, ne na pevném offsetu v hlavičce souboru. Zacházení s metafily obecně je pokryté v článku o ohraničeném EMF a WMF vektorovém dekodéru. GetJpegSize ukazuje, proč to rozlišení potřebuje explicitní příznak místo odvozování: scan smyčka může skončit třemi cestami — našla frame hlavičku, narazila na konec souboru, nebo odpadla na poškozené délce segmentu — a jen ta první přiřazuje wWidth a wHeight. Bez Booleanu found by ty další dvě cesty převedly cokoli, co ty lokální proměnné náhodou obsahovaly, a předaly výsledek nahoru jako úspěch. Neinicializované lokální proměnné jsou často nula a nula je tady nejhorší možná odpověď: přežije každou kontrolu platnosti dole a vyprodukuje neviditelný obrázek, který nikdo neumí vybrat ani smazat
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: velikost ze záznamu BSE
else
// W a H jsou 100 x 100; rozhodněte o politice místo přijetí placeholderu
LogWarning('unreadable image geometry: ' + FileName);
end;
end;
Kde jsou poctivé limity
Tyto čtečky záměrně parsují hlavičku a zastaví se. Nedekódují pixely, takže hlásí uložené rozměry a nic víc, a jsou případy, kdy uložené rozměry nejsou celý příběh. Animovaný GIF, jehož framy jsou menší než logická obrazovka, pořád hlásí logickou obrazovku, což je správné pro layout a nemusí to odpovídat tomu, co ukáže prohlížeč. JPEG nesoucí EXIF orientační tag s otočením o 90 stupňů hlásí neotočené rozměry frame, takže portrét vyfocený telefonem se může vrátit jako na šířku. DIB s nesquare poměrem stran pixelů hlásí surové počty pixelů a faktor 0.75 předpokládá 96 DPI všude. Nic z toho není chyba ve čtení hlavičky a nic z toho nejde opravit bez dekódování obrazu, k čemuž tahle vrstva není — jsou to dokumentované hranice, takže pokud vaše pipeline zpracovává telefonní fotky, přečtěte si orientační tag sami a prohoďte hodnoty před voláním AddPicture. Všechno ostatní o umísťování obrázků, kotvení a objektovém modelu kresby je pokryté v článku o grafech, obrázcích a kresbách
HotXLS čte a zapisuje XLS, XLSX, ODS a CSV nativně v Delphi a C++Builderu bez nutnosti instalace Excelu a vkládání obrázků prochází touto hlavičkovou cestou v každém z nich. Podpora formátů a licenční detaily jsou na produktové stránce HotXLS Delphi spreadsheet component