Teknisk artikel

Läsa GIF-, BMP- och JPEG-headergeometri i HotXLS

HotXLS läser en bilds verkliga pixelgeometri ur bildfilens egen header innan någon ritgeometri skrivs. GetImageSize i lxImage.pas skickar vidare på filtillägget till GetJpegSize, GetGifSize, GetBmpSize, GetPngSize eller GetWmfEmfSize, konverterar pixlar till punkter med en faktor på 0.75, och returnerar -1 när den verkligen inte kan avgöra det

Den sista satsen är den del som tog längst att få rätt. En storleksläsare som inte kan misslyckas är en storleksläsare som ljuger, och en lögn om bildgeometri yppar sig inte vid insättningstillfället. Den yppar sig veckor senare, när någon öppnar arbetsboken och företagslogotypen är en tilltryckt rektangel

Varför behöver en bild sin verkliga geometri innan den hamnar i ritningen?

För att kalkylarksformatet lagrar utbredningen, inte bilden. När AddPicture eller Images.AddFromFile placerar en bild registrerar ritningsdelen en explicit bredd och höjd för formen — i DrawingML är det a:ext-elementets cx- och cy-attribut, i English Metric Units, enligt ECMA-376 Part 1 §20.4. Ingenting i den posten säger "använd bildens naturliga storlek", så den naturliga storleken måste komma någonstans ifrån, och det enda stället den finns är bildbyten. Får du den fel med en faktor tre är bilden utsträckt med en faktor tre, i varje applikation som öppnar arbetsboken, för alltid. Enhetskonverteringen från de pixeldimensionerna ned till EMU är en separat fråga, täckt i artikeln om EMU-enheter och skalning; det här stycket handlar om steget före, där pixelnumren över huvud taget hämtas. Fallbacken när en läsare misslyckas är 100 gånger 100, och den siffran är medvetet, uppenbart fel. Det är inte en gissning på en rimlig storlek, det är en markör som betyder "ingen kunde läsa den här filen", och den är långt bättre än ett rimligt utseende felaktigt nummer för att en användare rapporterar det omedelbart

Hur GetImageSize i HotXLS skickar vidare på filtillägget till läsarna GetJpegSize, GetGifSize, GetBmpSize, GetPngSize och GetWmfEmfSize och rapporterar tre utfall: 1 när äkta geometri lästes ur headern, -1 när ingenting kunde läsas och 100 gånger 100-fallbacken gäller, och 7 när en metafil tar sin utbredning från BSE-posten
GetImageSize skiljer på tre utfall: 1 för geometri läst ur headern, -1 för oläsbara filer där den uppenbart felaktiga 100 gånger 100-fallbacken rapporteras av användare, och 7 för metafiler vars utbredning lever i BSE-posten

GIF: deskriptorn som aldrig parsades

GetGifSize är ny, och innan den fanns kom varje GIF som sattes in i ett kalkylblad ut på exakt 100 gånger 100 — det fanns ingen GIF-gren alls, så filtillägget ramlade igenom till standardarmen i GetImageSize och returnerade -1. Det här är den billigaste headern i uppsättningen att parsas, vilket gör utelämnandet lite pinsamt och väldigt lätt att fixa. GIF89a-specifikationen placerar Logical Screen Descriptor omedelbart efter den sexbyte långa signaturen, och §18 definierar dess två första fält som den logiska skärmens bredd och höjd, vart och ett ett osignerat 16-bitarsvärde lagrat little-endian. I filtal är det byte 6 och 7 för bredd, byte 8 och 9 för höjd, så en enda tio-byte-läsning täcker signaturkontrollen och båda dimensionerna

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", sedan Logical Screen Descriptor med bredden och
    // höjden som little-endian-ord vid offset 6 och 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;

Lägg märke till formen: felvärdena tilldelas först, och framgångsvägen är det enda som skriver över dem. Varje läsare i lxImage.pas följer den ordningen, för den gör "jag kunde inte läsa detta" till standard i stället för något som måste kommas ihåg vid varje tidig exit. Att bara kontrollera GIF8 i stället för hela strängen GIF87a eller GIF89a är avsiktligt — deskriptorlayouten är identisk i båda revisionerna, och det finns ingen vinst i att avvisa en fil vars geometri är läsbar

Bytekarta över den GIF-header HotXLS läser: byte 0 till 5 bär GIF8-signaturen och versionen, byte 6 och 7 den little-endian-lagrade logiska skärmbredden och byte 8 och 9 höjden, så en tio-byte-läsning i GetGifSize täcker signaturkontrollen och båda dimensionerna före 0.75-punktkonverteringen
GIF Logical Screen Descriptor packar allt GetGifSize behöver i tio byte: den sexbyte långa signaturen följd av little-endian-bredd vid offset 6 och 7 och höjd vid 8 och 9, med GIF87a och GIF89a som delar en identisk layout

Vad ändrar egentligen en BMP-headerstorlek på 12?

GetBmpSize läser de fyra byten vid offset 14 som DIB-headerstorleken och förgrenar på värdet, för en BMP med en headerstorlek på 12 lagrar sina dimensioner i en helt annan fältbredd än alla andra BMP:er. [MS-WMF] §2.2.2 lägger ut familjen: BITMAPCOREHEADER är 12 byte och bär 16-bitars bredd och höjd, medan BITMAPINFOHEADER, BITMAPV4HEADER och BITMAPV5HEADER alla börjar med 32-bitars signerade biWidth och biHeight. Att läsa fyra byte där filen har två misslyckas inte högljutt; det drar in planantalet och bitdjupet som den höga halvan av ett tal och rapporterar en bitmapp flera hundra tusen pixlar bred. Den andra fällan i samma header är tecknet på biHeight: ett negativt värde betyder inte negativ höjd, det betyder att DIB-raderna lagras uppifrån och ned i stället för nedifrån och upp, vilket är ett pixelordningspåstående som inte har något med geometri att göra

  f.Seek(14, soFromBeginning);
  if f.Read(HeaderSize, 4)= 4 then
  begin
    if HeaderSize= 12 then
    begin
      // BITMAPCOREHEADER lagrar 16-bitars 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;
  // ...
  // Ett negativt biHeight markerar bara en top-down-DIB; magnituden är storleken
  if (Result= 1)and (lWidth> 0)and (lHeight<> 0) then
  begin
    AWidth:= lWidth* 0.75;
    AHeight:= Abs(lHeight)* 0.75;
  end
  else
    Result:= - 1;

Det sista guardet är värt att läsa noga. Höjden testas mot <> 0 i stället för > 0, för negativt är lagligt och noll är inte det, och Abs tillämpas bara vid konverteringspunkten. Bredd har inget motsvarande fall — ett negativt biWidth är inte definierat av formatet, så det behandlas som en korrupt fil och degraderas till -1. Att i stället skicka ett rått negativt värde uppåt skulle producera en negativ utbredning i ritningsposten, som klampas, ignoreras eller renderas som en vänd form beroende på vilken applikation som öppnar arbetsboken

JPEG: vilka markörer är egentligen frame-headers?

GetJpegSize accepterar marköruppsättningen [$C0..$C3, $C5..$C7, $C9..$CB, $CD..$CF], och de tre luckorna i det intervallet är hela poängen. ITU-T T.81 §B.2.2 definierar frame-headern som SOFn, och frame-headern är där antalet rader och sampel per rad bor, i varje frame-typ snarare än bara de vanliga. Markörerna som faller inom samma numeriska spann men inte är frame-headers är $C4 (DHT, definiera Huffman-tabeller), $C8 (JPG, reserverad) och $CC (DAC, definiera villkorsstyrning för aritmetisk kodning). En läsare som bara känner igen baseline, extended sequential och progressive — $C0, $C1 och $C2 — fungerar på nästan varje JPEG en kamera eller ett skärmbildsverktyg producerar, och misslyckas sedan tyst på dem som spelar roll för dokumentarbetsflöden: lossless-JPEG ($C3), de aritmetiskt kodade varianterna ($C9 till $CB) och de hierarkiska frame-typerna ($CD till $CF) skannar alla förbi frame-headern, springer av filens slut och hamnar i fallbacken. Medicinsk bildbehandling och skannade-dokument-pipelines producerar exakt de filerna

// Vilken SOFn-markör som helst bär frame-dimensionerna; $C4 (DHT), $C8 (JPG)
// och $CC (DAC) delar intervallet men är inte frame-headers
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

Två detaljer i de sju raderna. De tre hoppade byten är segmentlängdsordet plus sampelprecisionbyten, så läsningen landar på radantalet; JPEG lagrar höjd före bredd, vilket är tvärtemot de flesta format och en lätt transponering att göra. Och värdena går genom swapByte för att JPEG är big-endian rakt igenom, samma anledning som GetPngSize kör sina 32-bitars IHDR-värden genom lswapByte. Segmentvandringen har sitt eget guard: när en markör inte är parameterlös tar läsaren den tvåbyte långa längden, byte-swappar den och vägrar att söka om värdet är under 2 — en längd kortare än sitt eget fält skulle översättas till en bakåtsökning, och en bakåtsökning i en skanningsloop är en oändlig loop på en fil någon annan kontrollerar

Vilka JPEG-markörer HotXLS accepterar som frame-headers i GetJpegSize: SOFn-intervallet C0 till CF med luckor vid C4 för DHT, C8 för JPG och CC för DAC, plus frame-headerlayouten där den tvåbyte långa längden och precisionbyten hoppas över, höjden läses före bredden och båda värdena går genom swapByte för big-endian-ordning
SOFn-markörintervallet med sina tre luckor: DHT, JPG och DAC delar det numeriska spannet men håller inga frame-dimensioner, och att acceptera hela intervallet låter lossless- och hierarkiska JPEG:er rapportera sin geometri

Att returnera minus ett slår att returnera ett rimligt nummer

Den gemensamma tråden genom alla fem läsarna är att GetImageSize skiljer på tre utfall snarare än två. 1 betyder att geometrin lästes ur filen. -1 betyder att den inte gjorde det, och anroparen ska använda sin standard. 7 returneras av GetWmfEmfSize och betyder något helt annat: det säger åt AddPicture att ta storleken från metafilens BSE-post i stället, för en WMF eller EMF bär sin utbredning i arbetsbokens egen bildpost snarare än vid en fast offset i filheadern. Metafilhantering i allmänhet täcks i artikeln om den begränsade EMF- och WMF-dekodern. GetJpegSize visar varför den distinktionen behöver en explicit flagga snarare än en slutledning: skanningsloopen kan terminera på tre sätt — den hittade en frame-header, den nådde filens slut, eller den gav upp vid en korrupt segmentlängd — och bara det första tilldelar wWidth och wHeight. Utan den booleska found skulle de andra två vägarna konvertera vad de lokala variablerna råkat innehålla och lämna resultatet uppåt som en framgång. Oinitierade lokala variabler är ofta noll, och noll är det värsta tänkbara svaret här: det överlever varje giltighetskontroll nedströms och producerar en osynlig bild som ingen kan välja eller radera

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: storlek från BSE-posten
  else
    // W och H är 100 x 100; bestäm policy i stället för att acceptera en platshållare
    LogWarning('unreadable image geometry: ' + FileName);
  end;
end;

Var de ärliga gränserna går

Dessa läsare parsar medvetet en header och slutar. De avkodar inte pixlar, så de rapporterar de lagrade dimensionerna och inget mer, och det finns fall där de lagrade dimensionerna inte är hela historien. En animerad GIF vars frames är mindre än den logiska skärmen rapporterar fortfarande den logiska skärmen, vilket är korrekt för layout och kanske inte matchar vad en visare visar. En JPEG som bär en EXIF-orienteringstagg som anger en 90-graders rotation rapporterar de oroterade frame-dimensionerna, så ett porträttfoto taget med en telefon kan komma tillbaka som liggande. En DIB med ett icke-kvadratiskt pixelformat rapporterar råa pixelantal, och faktorn 0.75 antar 96 DPI rakt igenom. Inget av det är buggar i headerläsningen och inget är fixbart utan att avkoda bilden, vilket inte är vad det här lagret är till för — de är dokumenterade gränser, så om din pipeline hanterar telefonfoton, läs orienteringstaggen själv och byt plats på värdena innan du anropar AddPicture. Allt annat om bildplacering, förankring och ritningsobjektmodellen täcks i artikeln om diagram, bilder och ritningar

HotXLS läser och skriver XLS, XLSX, ODS och CSV nativt i Delphi och C++Builder utan någon Excel-installation, och bildinsättning går genom den här headervägen i vart och ett av dem. Formatstöd och licensdetaljer finns på produktsidan för HotXLS Delphi spreadsheet component