Teknisk artikel

Härdning av Delphi TIFF-avkodare: BigTIFF och tiled TIFF

PDFlibPas avkodar TIFF med en handskriven Object Pascal-parser i stället för en libtiff-bindning, och version 3.534.1 åtskarpade exakt där denna parser vägrar indata. BigTIFF-magivärdet 43 avvisas nu vid namn, TileOffsets och TileByteCounts vägras under taggtolkningen, och varje buffert dimensioneras med Int64-arithmetik under ett avkodningstak på 256 MiB

Felet detta stänger syns aldrig i ett labb. Det syns som en skanningsgateway som kört tyst i tre år, tills en kund skickar ett geospatialt arkiv eller en whole-slide-medicinsk bild genom den. Filen har en korrekt TIFF-header. Den kan tolkas. Resultatet är en sida med randigt brus, eller en allokering på flera gigabyte som tar ner tjänsten, och inget längs vägen förklarade någonsin indata som ogiltigt. Det är den felform man bygger emot: inte en krasch, utan ett felaktigt svar som levereras självsäkert

Varför bevisar II eller MM inte att du har en klassisk TIFF?

Eftersom byteordningsmarkören delas av båda dialekterna. Klassisk TIFF och BigTIFF öppnar båda med II eller MM, och fältet som faktiskt skiljer dem åt är den 16-bitarsmagi som kommer omedelbart efter: 42 för klassisk TIFF enligt TIFF 6.0-specifikationen, 43 för BigTIFF med sina 64-bitarsoffsets. En laddare skriven som FValidTIFF := PopWord = 42 har inte fel om klassisk TIFF, men den kollapsar två mycket olika avvisanden till en tyst boolesk flagga, så att en BigTIFF blir omöjlig att skilja från en avhuggen JPEG som någon döpt om. PDFlibPas skiljer nu på fallen och registrerar vart och ett i TPDFTIFF.LastError: en header kortare än fyra byte, en ogiltig byteordningsmarkör, magivärde 43 och alla andra magivärden ger var och en distinkt text. Biblioteket avkodar fortfarande inte BigTIFF, och att säga det rakt ut är själva poängen. Anroparen får skillnaden mellan "detta är ingen TIFF" och "detta är en TIFF vars 64-bitarsoffsetlayout den inbyggda avkodaren inte implementerar", vilket är skillnaden mellan ett supportärende du svarar på i ett meddelande och ett som blir en veckas gissande

PDFlibPas TIFF-laddare läser byteordningsmarkören och 16-bitarsmagin var för sig, så att en kort header, en ogiltig markör, BigTIFF-magivärde 43 och alla andra magivärden var och en ger distinkt LastError-text i stället för en tyst boolesk flagga
Klassisk TIFF och BigTIFF öppnar med samma byteordningsmarkör, så PDFlibPas skiljer fyra avvisandefall och namnger vart och ett i LastError
var
  Tiff: TPDFTIFF;
  Page: Integer;
begin
  Tiff := TPDFTIFF.Create;
  try
    Tiff.LoadFromFile('inbox\scan-0417.tif');
    if not Tiff.ValidTIFF then
      raise Exception.Create('TIFF rejected: ' + Tiff.LastError);
    if Tiff.PageCount < 1 then
      raise Exception.Create('TIFF carries no decodable page');
    for Page := 1 to Tiff.PageCount do
      Writeln(Format('page %d: %dx%d, %d spp',
        [Page,
         Tiff.PageInfo[Page].Width,
         Tiff.PageInfo[Page].Height,
         Tiff.PageInfo[Page].SamplesPerPixel]));
  finally
    Tiff.Free;
  end;
end;

Tiles är en annan geometri, inte ytterligare en offsets-array

PDFlibPas vägrar tiled TIFF medan taggar tolkas, innan någon pixeldata röras. Den genväg som bjuder in felet är lätt att se: tag 324 (TileOffsets) och tag 325 (TileByteCounts) är arrayer av filoffsets och byteantal, strukturellt identiska med strip-arrayerna, så att peka de befintliga stripfälten på dem kostar två rader och kompilerar rent. Det är också fel. Tiles bildar ett tvådimensionellt rutnät med utfyllda kantblock, sitt eget radsteg inuti varje tile, och ingen RowsPerStrip-semantik alls, som tiled-bildsektionen i TIFF 6.0 redogör för. Att mata tile-payloads till en strip-avkodare misslyckas därför inte högt. SimpleExtract och CompDecode går igenom data med fel radsteg och producerar en bild med rätt dimensioner och fel pixlar. Den äldre koden förvärrade detta genom att behålla StripsAreTiles, ColumnsPerTile och RowsPerTile i TTIFFPage: tile-geometri registrerad av en avkodare utan någon tile-monterare bakom. I 3.534.1 höjer hanterarna för tag 324 och 325 tile-felet och överger IFD omedelbart, så att avvisandet bär ordet "tiled" i stället för att dyka upp veckor senare som ett renderingsklagomål

PDFlibPas jämför strip-layout, där helsbreddsbänder delar ett radsteg, med tile-layout, ett tvådimensionellt rutnät av utfyllda kantblock, och vägrar tag 324 och 325 under taggtolkningen innan någon pixeldata röras
Tile-arrayer ser strukturellt identiska ut med strip-arrayer, vilket är anledningen till att det, när de matas till en strip-avkodare, ger rätt dimensioner och fel pixlar

En dimensionsgräns är ingen minnesbudget

Att begränsa bredd och höjd till 65 535 vardera är nödvändigt och långt ifrån tillräckligt, eftersom den storhet som driver allokeringen är en produkt. RowsPerStrip * Width * SamplesPerPixel kan spilla över 32-bitarsarithmetik långt innan någon av sidorna når sin egen gräns, och även utan spill kan den namnge en allokering som ingen tjänst bör försöka. PDFlibPas beräknar radbyte i Int64 och driver tre tak tillsammans: 65 535 per dimension, 32 färgkomponenter och 256 MiB avkodade byte

const
  PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
  PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
  PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;

// inuti TPDFTIFF.ValidatePageForDecode
BitsPerPixel := Int64(P.BitsPerSample) * P.SamplesPerPixel;
RowBytes := (Int64(P.Width) * BitsPerPixel + 7) div 8;
if (RowBytes < 1) or
   (RowBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
   (Int64(P.Height) > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES div RowBytes) then
  Exit(False);

DecodedBytes := RowBytes * P.RowsPerStrip;
if (DecodedBytes < 1) or
   (DecodedBytes > PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES) or
   (DecodedBytes > MaxInt) then
  Exit(False);

Tre detaljer där betyder mer än konstanterna själva. Höjdtestet är skrivet som en division i stället för en multiplikation, så att den överdimensionerade produkten aldrig bildas alls. En RowsPerStrip under 1 eller över bildhöjden normaliseras först till höjden, vilket är den single-strip-läsning TIFF 6.0 redan antyder och som stoppar en fientlig tagg från att blåsa upp stripbufferten. Och rutinen är delad: ValidatePageForDecode körs i slutet av taggtolkningen och igen vid ingången av både SimpleExtract och CompDecode, så att kod som når en avkodare direkt inte kan gå runt budgeten. Det är samma regel PDFlibPas följer när otillförlitliga PDF-objektgrafer tolkas, eftersom en gräns som drivs vid en av tre dörrar inte är en gräns

PDFlibPas dimensionerar varje TIFF-buffert med Int64-arithmetik, testar bildhöjden med en division så att den överdimensionerade produkten aldrig bildas, och kör samma rutin ValidatePageForDecode vid taggtolkningen och vid båda avkodaringångarna
Tre tak, Int64-radbyte-arithmetik och en delad valideringsrutin som nås från alla tre dörrarna, eftersom en gräns som drivs vid en av tre dörrar inte är en gräns

Vad måste en anropare kontrollera innan PageInfo läses?

Kontrollera ValidTIFF först, sedan PageCount, och först därefter indexera PageInfo. En avvisad fil kan lämna PageCount på noll, och GetPageInfo svarar på ett index utanför intervallet med en oinitierad TTIFFPage-post, så att en felsökväg som läser upplösning eller sampelantal på vägen till att rapportera felet slutar med att läsa brus. Version 3.534.1 rättade båda anroparna inuti biblioteket: bildimportvägen läser XRes och YRes endast inuti den giltiga grenen, och TPDFlib.GetImagePageCount kräver ValidTIFF i stället för att lita på ett icke-noll sidantal på egen hand. Nedströms är Options-argumentet till AddImageFromFile sidnumret, ettbaserat, för en flersidig TIFF, så att GetImagePageCount måste vara pålitligt innan loopen startar snarare än efteråt. Noll sidor är nu ett riktigt svar som betyder "här finns inget som går att avkoda", inte en olycka från en tidig retur, vilket spelar störst roll när du sammanställer och flätar ihop duplex-skannbatcher och ett tyst feltolkat blad skulle hamna på fel position

var
  Pdf: TPDFlib;
  Pages, I, ImageID: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
    if Pages < 1 then
      Exit;  // fel header, BigTIFF, tiled-layout eller över budget
    Pdf.NewDocument;
    for I := 1 to Pages do
    begin
      Pdf.NewPage;
      ImageID := Pdf.AddImageFromFile('inbox\scan-0417.tif', I);
      if ImageID > 0 then
      begin
        Pdf.SelectImage(ImageID);
        Pdf.DrawImage(0, 0, 595, 842);
      end;
    end;
    Pdf.SaveToFile('scan-0417.pdf');
  finally
    Pdf.Free;
  end;
end;

Bygga avkodaren eller länka libtiff?

PDFlibPas behåller den inbyggda avkodaren, och den avgörande faktorn är plattformsräckvidd snarare än författarskap. Ungefär 1 873 rader Object Pascal kompilerar överallt där kompilatorn tar vägen: Win32, Win64, macOS, iOS, Android och FPC på Linux. libtiff 4.7.1 är runt 30 000 rader C utspridda över 34 tif_*.c-översättningsenheter, och de förbyggda objektfiler som finns i dag täcker endast Windows. Att anta det skulle byta fullständig TIFF-täckning mot en lista över plattformar med stöd som krymper till de maskiner som kan köra C-verktygskedjan, plus ett länkningspass ingen ännu har gått igenom

Vad det kostar är värt att säga utan försköning. Den inbyggda avkodaren hanterar det som arbete med inskannade dokument faktiskt producerar: CCITT Group 3 endimensionellt och tvådimensionellt, Group 4, LZW, Deflate, PackBits och JPEG-i-TIFF, över WhiteIsZero, BlackIsZero, RGB, palett- och CMYK-fotometri med Predictor 1 och 2. Dessa payloads stämmer överens med PDF-filtrena i ISO 32000-1 §7.4.4 och §7.4.6, vilket är därför TIFF-front-enden väger så tungt i en skanningspipeline. Det den inte hanterar är BigTIFF, tiles, flyttals-Predictor 3, PixarLog och SGILog, gammaldags JPEG-komprimering 6 och sub-IFD-pyramider. Sedan 3.534.1 är vart och ett av dessa ett namngivet avvisande i stället för en fel bild, och biblioteket förvarar en skriftlig trigglista för att öppna libtiff-beslutet på nytt:

  • en kund rapporterar en BigTIFF-fil och behöver inbyggt stöd i stället för ett konverteringssteg
  • en kund rapporterar tiled TIFF från medicinska, GIS- eller industriella källor och behöver den avkodad på plats
  • en kund rapporterar flyttals-Predictor 3 TIFF
  • ett publicerat säkerhetshål landar på de inbyggda CCITT- eller LZW-avkodningsvägarna
  • plattformsoberoende-argumentet slutar gälla, antingen för att macOS-, iOS- och Android-stödet släpps, eller för att en återanvändbar libtiff-integration redan täcker macOS och Linux

Själva migreringen är avgränsad snarare än hypotetisk: en USE_LIBTIFF-villkorlig skulle hålla den publika ytan hos TPDFTIFF intakt, dirigera LoadFromStream genom TIFFClientOpen med ström-callbacks, och lämna Pascal-parsern som icke-Windows-fallback. Tills en av dessa triggers faktiskt utlöses, kostar det inget en kund kan känna att underhålla två avkodare och en fördubblad testmatris. Att skjuta upp en kostnad med flyktvägen redan nedskriven är en annan sak än att ignorera den

Där detta lämnar en pipeline för inskannade dokument

Behandla TPDFTIFF som en grind snarare än en konverterare. Ladda filen, läs ValidTIFF, och logga LastError ordagrant när den är falsk, eftersom den strängen nu är den kortaste vägen från en fältrapport till en diagnos. Filer som faller vid grinden förblir återhämtningsbara genom att konverteras uppströms, vilket är det praktiska svaret för BigTIFF- och tiled-källor i dag. För indata helt utanför TIFF tar PDFlibPas en separat väg genom sin AVIF-, HEIF- och JPEG XL-bildindata-väg, så att frågan om vilken avkodare som äger vilket format förblir explicit i stället för framväxande

Allt detta ligger bakom det vanliga bild-API:t, så att en dokumentpipeline får den skarpare gränsen utan att ändra en rad anropskod utöver att kontrollera sidantalet som den redan borde ha kontrollerat. Om du väger en inbyggd TIFF-till-PDF-väg för Delphi eller C++Builder, är hela komponenten och dess bildhantering dokumenterad på sidan PDF Library for Delphi