Teknisk artikel

Læsning af GIF-, BMP- og JPEG-header-geometri i HotXLS

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

Sådan dispatcher GetImageSize i HotXLS på fil-extensionen til læserne GetJpegSize, GetGifSize, GetBmpSize, GetPngSize og GetWmfEmfSize og rapporterer tre udfald: 1 når ægte geometri blev læst fra headeren, -1 når intet kunne læses og 100 gange 100-fallback'en gælder, og 7 når en metafil tager sin udstrækning fra BSE-recordet
GetImageSize skelner tre udfald: 1 for geometri læst fra headeren, -1 for ulæselige filer hvor den åbenlyst forkerte 100 gange 100-fallback bliver rapporteret af brugere, og 7 for metafiler hvis udstrækning bor i BSE-recordet

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

Byte-kort over GIF-headeren læst af HotXLS: bytes 0 til 5 bærer GIF8-signaturen og versionen, bytes 6 og 7 den little-endian logical screen-bredde og bytes 8 og 9 højden, så én ti-byte-læsning i GetGifSize dækker signatur-tjekket og begge dimensioner før 0.75-point-konverteringen
GIF-Logical-Screen-Descriptor pakker alt GetGifSize har brug for i ti bytes: den seks-byte lange signatur efterfulgt af little-endian-bredde ved offsets 6 og 7 og højde ved 8 og 9, med GIF87a og GIF89a der deler et identisk layout

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

Hvilke JPEG-markører HotXLS accepterer som frame-headere i GetJpegSize: SOFn-området C0 til CF med huller ved C4 for DHT, C8 for JPG og CC for DAC, plus frame-header-layoutet hvor den to-byte lange længde og præcisions-bytten springes over, højde læses før bredde, og begge værdier går gennem swapByte for big-endian-rækkefølge
SOFn-markør-området med sine tre huller: DHT, JPG og DAC deler det numeriske span men holder ingen frame-dimensioner, og at acceptere hele området lader lossless- og hierarkiske JPEG'er rapportere deres geometri

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