Articol tehnic

Decodor TIFF consolidat în Delphi: BigTIFF și TIFF cu dale

PDFlibPas decodează TIFF cu un parser scris de mână în Object Pascal, nu printr-o legătură către libtiff, iar versiunea 3.534.1 a întărit exact locurile în care parserul acesta refuză datele de intrare. BigTIFF cu magia 43 este acum respins prin nume, TileOffsets și TileByteCounts sunt refuzate în timpul parsării etichetelor, iar fiecare buffer este dimensionat prin aritmetică Int64 sub un plafon de decodare de 256 MiB

Defectul pe care această schimbare îl închide nu apare niciodată în laborator. Apare ca o poartă de scanare care a rulat liniștită trei ani, până când un client trimite prin ea o arhivă geospațială sau o imagine medicală de tip whole-slide. Fișierul are un antet TIFF legitim. Se parsează. Ce iese este o pagină de zgomot dungat, sau o alocare de mai mulți gigabyte care doboară serviciul, și nimic pe drum nu a declarat vreodată intrarea ca fiind invalidă. Aceasta este forma de defecțiune contra căreia merită să proiectați: nu o prăbușire, ci un răspuns greșit livrat cu încredere

De ce II sau MM nu dovedesc că aveți un TIFF clasic?

Pentru că marcajul de ordine al octeților este comun celor două dialecte. TIFF clasic și BigTIFF încep deopotrivă cu II sau MM, iar câmpul care îi diferențiază efectiv este magia pe 16 biți imediat după el: 42 pentru TIFF clasic așa cum îl definește specificația TIFF 6.0, 43 pentru BigTIFF cu offseturile sale pe 64 de biți. Un loader scris ca FValidTIFF := PopWord = 42 nu greșește în cazul TIFF clasic, dar colapsează două respingeri foarte diferite într-un singur boolean tăcut, astfel încât un BigTIFF devine de nedistins de un JPEG trunchiat pe care cineva l-a redenumit. PDFlibPas separă acum cazurile și le înregistrează pe fiecare în TPDFTIFF.LastError: un antet mai scurt de patru octeți, un marcaj de ordine al octeților invalid, magia 43 și orice altă valoare magică produc texte distincte. Biblioteca tot nu decodează BigTIFF, și tocmai spunerea asta pe față este esențialul. Apelantul primește diferența dintre „acesta nu este un TIFF” și „acesta este un TIFF al cărui aranjament de offseturi pe 64 de biți decodorul integrat nu îl implementează”, ceea ce este diferența dintre un tichet de suport la care răspundeți într-un singur mesaj și unul care devine o săptămână de ghiciri

Loaderul TIFF din PDFlibPas citește separat marcajul de ordine al octeților și magia pe 16 biți, astfel încât un antet scurt, un marcaj invalid, BigTIFF cu magia 43 și orice altă valoare magică produc fiecare texte LastError distincte în locul unui singur boolean tăcut
TIFF clasic și BigTIFF încep cu același marcaj de ordine al octeților, așa că PDFlibPas separă patru cazuri de respingere și numește fiecare în 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;

Dalele sunt o geometrie diferită, nu un alt vector de offseturi

PDFlibPas respinge TIFF cu dale în timpul parsării etichetelor, înainte de a atinge orice date de pixeli. Scurtătura care invită defectul e ușor de văzut: eticheta 324 (TileOffsets) și eticheta 325 (TileByteCounts) sunt vectori de offseturi de fișier și de contoare de octeți, identici structural cu vectorii de benzi, astfel încât a pointa câmpurile existente de benzi către ei costă două rânduri de cod și compilează curat. Este și greșit. Dalele formează o grilă bidimensională cu blocuri de margine completate, cu propriul pas de rând în interiorul fiecărei dale și fără nicio semantică RowsPerStrip, exact cum precizează secțiunea despre imagini cu dale din TIFF 6.0. Prin urmare, datele unei dale date unui decodor de benzi nu eșuează zgomotos. SimpleExtract și CompDecode parcurg datele cu pasul greșit și emit o imagine cu dimensiunile corecte și pixelii greșiți. Codul mai vechi a accentuat problema ținând StripsAreTiles, ColumnsPerTile și RowsPerTile în TTIFFPage: geometrie de dale înregistrată de un decodor fără niciun asamblor de dale în spate. În 3.534.1, rutinele de tratare ale etichetelor 324 și 325 ridică eroarea de dale și abandonează imediat IFD-ul, astfel încât respingerea poartă cuvântul „cu dale” în loc să iasă la iveală săptămâni mai târziu ca o plângere de randare

PDFlibPas compară aspectul cu benzi, în care benzile pe toată lățimea partajează un singur pas de rând, cu aspectul cu dale, o grilă bidimensională de blocuri de margine completate, și refuză etichetele 324 și 325 în timpul parsării etichetelor, înainte de a atinge orice pixel
Vectorii de dale arată structural identic cu vectorii de benzi, motiv pentru care a-i da unui decodor de benzi produce dimensiunile corecte și pixelii greșiți

O limită pe dimensiune nu este un buget de memorie

Limitarea lățimii și a înălțimii fiecare la 65.535 este necesară și de departe insuficientă, pentru că mărimea care conduce alocarea este un produs. RowsPerStrip * Width * SamplesPerPixel poate depăși aritmetica pe 32 de biți cu mult înainte ca oricare dintre cele două să își atingă propria limită, și chiar fără depășire poate numi o alocare pe care niciun serviciu n-ar trebui să o încerce. PDFlibPas calculează octeții pe rând în Int64 și impune trei plafoane împreună: 65.535 pe dimensiune, 32 de componente de culoare și 256 MiB de octeți decodați

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

// în interiorul 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);

Trei detalii de acolo contează mai mult decât constantele în sine. Testul înălțimii este scris ca o împărțire, nu ca o înmulțire, astfel încât produsul exagerat de mare nu se formează deloc. Un RowsPerStrip sub 1 sau peste înălțimea imaginii este normalizat întâi la înălțime, ceea ce este lectura pe bandă unică pe care TIFF 6.0 o implică deja și care împiedică o etichetă ostilă să umfle bufferul benzii. Iar rutina este partajată: ValidatePageForDecode rulează la sfârșitul parsării etichetelor și din nou la intrarea atât în SimpleExtract, cât și în CompDecode, astfel încât codul care ajunge direct la un decodor nu poate ocoli bugetul. Aceeași regulă o respectă PDFlibPas și când parsează grafuri de obiecte PDF nesigure, pentru că o limită impusă la una din trei uși nu este o limită

PDFlibPas dimensionează fiecare buffer TIFF prin aritmetică Int64, testează înălțimea imaginii printr-o împărțire astfel încât produsul exagerat să nu se formeze niciodată și rulează aceeași rutină ValidatePageForDecode la parsarea etichetelor și la ambele intrări ale decodoarelor
Trei plafoane, aritmetică Int64 pentru octeții pe rând și o rutină de validare partajată, accesibilă din toate cele trei uși, pentru că o limită impusă la una din trei uși nu este o limită

Ce trebuie să verifice un apelant înainte de a citi PageInfo?

Verificați întâi ValidTIFF, apoi PageCount, și abia apoi indexați PageInfo. Un fișier respins poate lăsa PageCount pe zero, iar GetPageInfo răspunde la un index în afara intervalului cu o înregistrare TTIFFPage neinițializată, astfel încât o cale de eroare care citește rezoluția sau contoarele de mostre în drum spre raportarea defecțiunii ajunge să citească zgomot. Versiunea 3.534.1 a corectat ambii apelanți din bibliotecă: calea de import a imaginii citește XRes și YRes doar în interiorul ramurii valide, iar TPDFlib.GetImagePageCount cere ValidTIFF în loc să aibă de una singură încredere într-un contor de pagini diferit de zero. Mai jos, argumentul Options al AddImageFromFile este numărul paginii pornind de la 1 pentru un TIFF multipagină, astfel încât GetImagePageCount trebuie să fie demn de încredere înainte să înceapă bucla, nu după. Zero pagini este acum un răspuns real care înseamnă „aici nu se poate decoda nimic”, nu un accident al unui return timpuriu, ceea ce contează cel mai mult când colaționați și intercalați loturi de scanări duplex și o singură foaie decodată greșit pe furiș ar ajunge pe o poziție greșită

var
  Pdf: TPDFlib;
  Pages, I, ImageID: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
    if Pages < 1 then
      Exit;  // antet invalid, BigTIFF, aspect cu dale sau peste buget
    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;

Construiți decodorul sau legați libtiff?

PDFlibPas păstrează decodorul integrat, iar factorul decisiv este acoperirea de platforme, nu paternitatea. Aproximativ 1.873 de rânduri de Object Pascal se compilează oriunde merge compilatorul: Win32, Win64, macOS, iOS, Android și FPC pe Linux. libtiff 4.7.1 înseamnă în jur de 30.000 de rânduri de C răspândite în 34 de unități de traducere tif_*.c, iar fișierele obiect precompilate care există azi acoperă doar Windows. Adoptarea lui ar schimba acoperirea completă TIFF pe o listă de platforme suportate care se strânge la mașinile capabile să ruleze lanțul de unelte C, plus o trecere prin linker pe care încă n-a parcurs-o nimeni

Costul acestei alegeri merită spus fără vestimentație de marketing. Decodorul integrat acoperă ceea ce munca cu documente scanate produce de fapt: CCITT Group 3 unidimensional și bidimensional, Group 4, LZW, Deflate, PackBits și JPEG-în-TIFF, pe fotometriile WhiteIsZero, BlackIsZero, RGB, paletă și CMYK, cu Predictor 1 și 2. Aceste sarcini utile se aliniază cu filtrele PDF din ISO 32000-1 §7.4.4 și §7.4.6, motiv pentru care front-endul TIFF cântărește atât de mult într-o conductă de scanare. Ce nu acoperă este BigTIFF, dalele, Predictor 3 în virgulă mobilă, PixarLog și SGILog, compresia JPEG în stil vechi 6 și piramidele de sub-IFD. De la 3.534.1, fiecare dintre acestea este o respingere numită, nu o imagine greșită, iar biblioteca păstrează o listă scrisă de declanșatoare pentru redeschiderea deciziei privind libtiff:

  • un client raportează un fișier BigTIFF și are nevoie de suport nativ, nu de un pas de conversie
  • un client raportează TIFF cu dale din surse medicale, GIS sau industriale și are nevoie de decodare la fața locului
  • un client raportează TIFF cu Predictor 3 în virgulă mobilă
  • o vulnerabilitate publicată lovește căile de decodare CCITT sau LZW integrate
  • argumentul cross-platform încetează să se aplice, fie pentru că suportul pentru macOS, iOS și Android este abandonat, fie pentru că o integrare reutilizabilă a libtiff acoperă deja macOS și Linux

Migrația în sine este delimitată, nu ipotetică: un condițional USE_LIBTIFF ar păstra intactă suprafața publică TPDFTIFF, ar ruta LoadFromStream prin TIFFClientOpen cu callback-uri de flux și ar lăsa parserul Pascal ca rezervă pentru non-Windows. Până când unul dintre aceste declanșatoare se activează efectiv, menținerea a două decodoare și a unei matrice de test dublate nu cumpără nimic pe care un client să îl simtă. Amânarea unui cost cu ruta de scăpare deja scrisă pe hârtie este un cu totul alt lucru decât ignorarea lui

Unde lasă asta o conductă de documente scanate

Tratați TPDFTIFF ca o poartă, nu ca un convertor. Încărcați fișierul, citiți ValidTIFF și logați LastError întocmai ori de câte ori este fals, pentru că acel șir este acum cea mai scurtă cale de la un raport din teren la un diagnostic. Fișierele care cad la poartă rămân recuperabile prin conversie în amonte, ceea ce este răspunsul practic pentru surse BigTIFF și cu dale în ziua de azi. Pentru date de intrare din afara TIFF cu totul, PDFlibPas ia o rută separată prin calea sa de intrare de imagini AVIF, HEIF și JPEG XL, astfel încât întrebarea care decodor deține ce format rămâne explicită, nu emergentă

Tot acest mecanism stă în spatele API-ului obișnuit de imagini, astfel încât o conductă de documente câștigă frontiera mai strânsă fără a schimba niciun rând de cod apelant, dincolo de verificarea contorului de pagini pe care oricum ar fi trebuit să o verifice. Dacă vă cântăriți o cale nativă TIFF-către-PDF pentru Delphi sau C++Builder, componenta completă și tratarea imaginilor sunt documentate pe pagina PDF Library for Delphi