HotXLS læser et billedes ægte pixel-geometri ud af billedfilens egen header før nogen drawing-geometri skrives. GetImageSize i lxImage.pas dispatcher på fil-extensionen til GetJpegSize, GetGifSize, GetBmpSize, GetPngSize eller GetWmfEmfSize, konverterer pixels til points med en faktor på 0.75 og returnerer -1 når den ærligt talt ikke kan afgøre det
Den sidste klausul er den del der tog længst tid at få rigtig. En størrelses-læser der ikke kan fejle, er en størrelses-læser der lyver, og en løgn om billedgeometri dukker ikke op ved indsættelsestidspunktet. Den dukker op uger senere, når nogen åbner workbook'en og firmalogoet er et mast rektangel
Hvorfor har et billede brug for sin ægte geometri før det kommer ind i drawing'en?
Fordi regnearks-formatet gemmer udstrækningen, ikke billedet. Når AddPicture eller Images.AddFromFile placerer et billede, registrerer drawing-parten en eksplicit bredde og højde for formen — i DrawingML er det a:ext-elementets cx- og cy-attributter, i English Metric Units, per ECMA-376 Part 1 §20.4. Intet i den registrering siger "brug billedets naturlige størrelse", så den naturlige størrelse skal komme fra et sted, og det eneste sted den findes, er billedets bytes. Får du den galt med en faktor tre, er billedet strakt med en faktor tre — i hver applikation der åbner workbook'en, for evigt. Enhedskonverteringen fra de pixel-dimensioner ned til EMU er en separat sag, dækket i artiklen om EMU-enheder og skalering; dette stykke handler om trinnet før, hvor pixel-tallene overhovedet hentes. Fallback'en når en læser fejler, er 100 gange 100, og det tal er bevidst, åbenlyst forkert. Det er ikke et gæt på en plausibel størrelse, det er en markør der betyder "ingen kunne læse denne fil", og det er langt bedre end et plausibelt-udseende forkert tal, fordi en bruger rapporterer det med det samme
GIF: descriptoren der aldrig blev parset
GetGifSize er ny, og før den fandtes, kom hver GIF indsat i en worksheet ud med præcis 100 gange 100 — der var slet ingen GIF-gren, så extensionen faldt igennem til standard-armen i GetImageSize og returnerede -1. Det er den billigste header i sættet at parse, hvilket gør udeladelsen lidt pinlig og meget let at rette. GIF89a-specifikationen placerer Logical Screen Descriptor umiddelbart efter den seks-byte lange signatur, og §18 definerer dens to første felter som logical screen bredde og højde, hver en usigneret 16-bit-værdi gemt little-endian. I fil-termer er det bytes 6 og 7 for bredde, bytes 8 og 9 for højde, så en enkelt ti-byte-læsning dækker både signatur-tjekket og begge dimensioner
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
// Signatur "GIF8", derefter Logical Screen Descriptor med bredden og
// højden som little-endian-ord ved offsets 6 og 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;
Bemærk formen på det: fejl-værdierne tildeles først, og succes-stien er det eneste der overskriver dem. Hver læser i lxImage.pas følger den rækkefølge, for den gør "jeg kunne ikke læse dette" til default i stedet for noget der skal huskes ved hver tidlig exit. At kun tjekke GIF8 frem for hele GIF87a- eller GIF89a-strengen er bevidst — descriptor-layoutet er identisk i begge revisioner, og der er ingen gevinst i at afvise en fil hvis geometri kan læses
Hvad ændrer en BMP-header-størrelse på 12 egentlig?
GetBmpSize læser de fire bytes ved offset 14 som DIB-header-størrelsen og forgrener på værdien, for en BMP med en header-størrelse på 12 gemmer sine dimensioner i en helt anden felt-bredde end enhver anden BMP. [MS-WMF] §2.2.2 lægger familien ud: BITMAPCOREHEADER er 12 bytes og bærer 16-bit-bredde og -højde, mens BITMAPINFOHEADER, BITMAPV4HEADER og BITMAPV5HEADER alle begynder med 32-bit-signerede biWidth og biHeight. At læse fire bytes hvor filen har to, fejler ikke højlydt; det trækker plane-antallet og bit-dybden ind som den høje halvdel af et tal og rapporterer en bitmap flere hundrede tusinde pixels bred. Den anden fælde i samme header er tegnet på biHeight: en negativ værdi betyder ikke en negativ højde, den betyder at DIB-rækkerne er gemt top-down i stedet for bottom-up, hvilket er et pixel-rækkefølge-udsagn der intet har med geometri at gøre
f.Seek(14, soFromBeginning);
if f.Read(HeaderSize, 4)= 4 then
begin
if HeaderSize= 12 then
begin
// BITMAPCOREHEADER gemmer 16-bit-dimensioner
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;
// ...
// En negativ biHeight markerer kun en top-down-DIB; magnitude er størrelsen
if (Result= 1)and (lWidth> 0)and (lHeight<> 0) then
begin
AWidth:= lWidth* 0.75;
AHeight:= Abs(lHeight)* 0.75;
end
else
Result:= - 1;
Den sidste vagt er værd at læse nøje. Højde testes mod <> 0 frem for > 0, for negativ er lovligt og nul er ikke, og Abs anvendes kun ved konverteringspunktet. Bredde har intet tilsvarende tilfælde — en negativ biWidth er ikke defineret af formatet, så den behandles som en korrupt fil og degraderes til -1. At sende en rå negativ værdi opad i stedet ville producere en negativ udstrækning i drawing-recordet, som bliver clampet, ignoreret eller renderet som en spejlvendt form afhængig af hvilken applikation der åbner workbook'en
JPEG: hvilke markører er faktisk frame-headere?
GetJpegSize accepterer markør-sættet [$C0..$C3, $C5..$C7, $C9..$CB, $CD..$CF], og de tre huller i det område er hele pointen. ITU-T T.81 §B.2.2 definerer frame-headeren som SOFn, og frame-headeren er hvor antallet af linjer og samples per linje bor — i hver frame-type snarere end bare de almindelige. Markørerne der falder inden for samme numeriske span men ikke er frame-headere, er $C4 (DHT, definér Huffman-tabeller), $C8 (JPG, reserveret) og $CC (DAC, definér arithmetic coding-conditioning). En læser der kun genkender baseline, extended sequential og progressive — $C0, $C1 og $C2 — virker på næsten enhver JPEG et kamera eller et screenshot-værktøj producerer, og fejler så stille og roligt på dem der betyder noget for dokument-workflows: lossless-JPEG ($C3), de arithmetic-kodede varianter ($C9 til $CB) og de hierarkiske frame-typer ($CD til $CF) scanner alle forbi frame-headeren, løber ud over enden af filen og lander i fallback'en. Medicinsk billeddannelse og scannede-dokument-pipelines producerer præcis de filer
// Enhver SOFn-markør bærer frame-dimensionerne; $C4 (DHT), $C8 (JPG)
// og $CC (DAC) deler området men er ikke frame-headere
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
To detaljer i de syv linjer. De tre sprangne bytes er segment-længde-ordet plus sample-præcisions-bytten, så læsningen lander på linje-antallet; JPEG gemmer højde før bredde, hvilket er det modsatte af de fleste formater og en let transposition at lave. Og værdierne går gennem swapByte fordi JPEG er big-endian hele vejen igennem — samme grund til at GetPngSize kører sine 32-bit-IHDR-værdier gennem lswapByte. Segment-vandringen har sin egen vagt: når en markør ikke er parameterløs, tager læseren den to-byte lange længde, byte-swapper den og nægter at seeke hvis værdien er under 2 — en længde kortere end sit eget felt ville blive til et baglæns seek, og et baglæns seek i en scan-løkke er en uendelig løkke på en fil en anden kontrollerer
At returnere minus et slår at returnere et plausibelt tal
Den fælles tråd gennem alle fem læsere er at GetImageSize skelner tre udfald frem for to. 1 betyder at geometrien blev læst fra filen. -1 betyder at den ikke blev det, og kalderen skal bruge sin default. 7 returneres af GetWmfEmfSize og betyder noget helt andet: den fortæller AddPicture at tage størrelsen fra metafilens BSE-record i stedet, for en WMF eller EMF bærer sin udstrækning i workbook'ens egen picture-record frem for ved et fast offset i fil-headeren. Metafil-håndtering generelt er dækket i artiklen om den afgrænsede EMF- og WMF-dekoder. GetJpegSize viser hvorfor den skelnen har brug for et eksplicit flag frem for en slutning: scan-løkken kan terminere på tre måder — den fandt en frame-header, den ramte enden af filen, eller den hoppede af på en korrupt segment-længde — og kun den første tildeler wWidth og wHeight. Uden found-Booleanen ville de to andre stier konvertere hvad end de lokaler tilfældigvis indeholdt og levere resultatet opad som en succes. Uinitialiserede lokaler er ofte nul, og nul er det værst tænkelige svar her: det overlever hvert gyldigheds-tjek nedstrøms og producerer et usynligt billede som ingen kan vælge eller slette
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); // metafil: størrelse fra BSE-recordet
else
// W og H er 100 x 100; beslut policy frem for at acceptere en placeholder
LogWarning('unreadable image geometry: ' + FileName);
end;
end;
Hvor de ærlige grænser går
Disse læsere parser bevidst en header og stopper. De dekoder ikke pixels, så de rapporterer de gemte dimensioner og intet mere, og der er tilfælde hvor de gemte dimensioner ikke er hele historien. En animeret GIF hvis frames er mindre end logical screen, rapporterer stadig logical screen, hvilket er korrekt for layout og måske ikke matcher hvad en viewer viser. En JPEG der bærer et EXIF-orienterings-tag der foreskriver en 90-graders rotation, rapporterer de ikke-roterede frame-dimensioner, så et portrætfoto taget på en telefon kan komme tilbage som landskab. En DIB med et ikke-kvadratisk pixel-aspektforhold rapporterer rå pixel-antal, og 0.75-faktoren antager 96 DPI hele vejen igennem. Ingen af dem er fejl i header-læsningen, og ingen kan rettes uden at dekode billedet, hvilket ikke er hvad dette lag er til — de er dokumenterede grænser, så hvis din pipeline håndterer telefonfotos, så læs orienterings-tagget selv og byt værdierne om før du kalder AddPicture. Alt andet om billedplacering, forankring og drawing-objektmodellen er dækket i artiklen om diagrammer, billeder og drawings
HotXLS læser og skriver XLS, XLSX, ODS og CSV nativt i Delphi og C++Builder uden at kræve en Excel-installation, og billedindsættelse går gennem denne header-sti i hver eneste af dem. Format-support og licensdetaljer står på produkt-siden for HotXLS Delphi spreadsheet component