Tehnički članak

Otvrđivanje TIFF dekodera u Delphiju: BigTIFF i tiled TIFF

PDFlibPas dekodira TIFF ručno pisanim Object Pascal parserom umjesto libtiff vezom, a verzija 3.534.1 ojačala je upravo tamo gdje taj parser odbija ulaz. BigTIFF magični broj 43 sada se odbija po imenu, TileOffsets i TileByteCounts odbacuju se već pri parsiranju oznaka, a svaki se međuspremnik dimenzionira Int64 aritmetikom ispod stropa dekodiranja od 256 MiB

Nedostatak koji ovime zatvaramo nikad se ne pokazuje u laboratoriju. Pokazuje se kao skenirajući mrežni prolaz koji je tri godine tiho radio, dok kupac kroz njega ne pošalje geoprostorni arhiv ili medicinski snimak cijelog preparata. Datoteka ima valjano TIFF zaglavlje. Parsira se. Ono što izađe jest stranica prugastog šuma ili visegigabajtna alokacija koja sruši servis, a ništa usput nije proglasilo ulaz neispravnim. To je oblik pogreške protiv kojega vrijedi inženjeriti: ne rušenje, već pogrešan odgovor poslužen samouvjereno

Zašto II ili MM ne dokazuje da imate klasični TIFF?

Jer oznaku redoslijeda bajtova dijele oba dijalekta. Klasični TIFF i BigTIFF otvaraju se s II ili MM, a polje koje ih stvarno razlikuje jest 16-bitna magična vrijednost neposredno iza nje: 42 za klasični TIFF kako ga definira specifikacija TIFF 6.0, 43 za BigTIFF s njegovim 64-bitnim ofsetima. Učitavač napisan kao FValidTIFF := PopWord = 42 nije u krivu za klasični TIFF, ali dvije vrlo različite odbitke sažima u jednu tihu logičku vrijednost, pa BigTIFF postaje nerazlučiv od obrezanog JPEG-a kojeg je netko preimenovao. PDFlibPas sada razdvaja te slučajeve i svaki bilježi u TPDFTIFF.LastError: zaglavlje kraće od četiri bajta, neispravna oznaka redoslijeda bajtova, magična vrijednost 43 i svaka druga vrijednost magije daju poseban tekst. Knjižnica i dalje ne dekodira BigTIFF, i upravo je to reći otvoreno poanta. Pozivatelj dobiva razliku između "ovo nije TIFF" i "ovo je TIFF čiji 64-bitni raspored ofseta ugrađeni dekoder ne implementira", a to je razlika između prijave podršci koju riješite jednim odgovorom i one koja se pretvori u tjedan dana nagađanja

PDFlibPas TIFF učitavač čita oznaku redoslijeda bajtova i 16-bitnu magičnu vrijednost odvojeno, pa kratko zaglavlje, neispravna oznaka, BigTIFF magična vrijednost 43 i svaka druga vrijednost magije daju poseban LastError tekst umjesto jedne tihe logičke vrijednosti
Klasični TIFF i BigTIFF otvaraju se istom oznakom redoslijeda bajtova, pa PDFlibPas razdvaja četiri slučaja odbijanja i svaki imenuje u 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;

Tileovi su drugačija geometrija, ne još jedan niz ofseta

PDFlibPas odbija tiled TIFF već pri parsiranju oznaka, prije dodira s bilo kojim piksel podacima. Prečica koja poziva pogrešku lako se uoči: oznaka 324 (TileOffsets) i oznaka 325 (TileByteCounts) nizovi su ofseta datoteke i brojača bajtova, strukturno identični strip nizovima, pa usmjeriti postojeća strip polja na njih stoji dva retka koda i prevodi se bez pogreške. Ipak je pogrešno. Tileovi tvore dvodimenzionalnu mrežu s popunjenim rubnim blokovima, vlastitim korakom retka unutar svakog tile-a i nikakvom RowsPerStrip semantikom, kako izričito kaže odjeljak TIFF 6.0 o tiled slikama. Zato hranjenje tile sadržaja strip dekoderom ne propada glasno. SimpleExtract i CompDecode prolaze podacima pogrešnim korakom i ispuštaju sliku s pravim dimenzijama i pogrešnim pikselima. Stariji kod toga je nadogradio time što je u TTIFFPage zadržavao StripsAreTiles, ColumnsPerTile i RowsPerTile: geometriju tile-ova bilježi dekoder iza kojega ne stoji nikakav sastavljač tile-ova. U 3.534.1 rukovatelji oznaka 324 i 325 dižu tile pogrešku i odmah napuštaju IFD, pa odbijanje nosi riječ "tiled" umjesto da iskoči tjednima kasnije kao prigovor na prikaz

PDFlibPas uspoređuje strip raspored, gdje pruge pune širine dijele jedan korak retka, s tile rasporedom, dvodimenzionalnom mrežom popunjenih rubnih blokova, i odbija oznake 324 i 325 pri parsiranju oznaka, prije dodira s bilo kojim piksel podacima
Tile nizovi strukturno izgledaju identično strip nizovima, pa ih strip dekoder obradi s pravim dimenzijama i pogrešnim pikselima

Jedno ograničenje dimenzije nije memorijski budžet

Ograničiti širinu i visinu na 65,535 svaku nužno je i daleko od dovoljnog, jer veličina koja pokreće alokaciju jest umnožak. RowsPerStrip * Width * SamplesPerPixel može preliti 32-bitnu aritmetiku davno prije nego što bilo koja strana dođe do svoje granice, a i bez preljeva može tražiti alokaciju koju nijedan servis ne bi smio pokušati. PDFlibPas računa bajtove retka u Int64 i provodi tri stropa zajedno: 65,535 po dimenziji, 32 komponente boja i 256 MiB dekodiranih bajtova

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

// unutar 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);

Tri pojedinosti tu važe više od samih konstanti. Test visine napisan je kao dijeljenje, a ne množenje, pa se prevelik umnožak nikad ni ne stvori. RowsPerStrip manji od 1 ili veći od visine slike prvo se normalizira na visinu, što je tumačenje s jednim stripom koje TIFF 6.0 već podrazumijeva i što sprječava neprijateljsku oznaku da napuše strip međuspremnik. A rutina je dijeljena: ValidatePageForDecode izvodi se na kraju parsiranja oznaka i ponovno na ulazu u SimpleExtract i CompDecode, pa kod koji do dekodera dođe izravno ne može zaobići budžet. To je isto pravilo koje PDFlibPas slijedi pri parsiranju nepouzdanih grafova PDF objekata, jer granica koja vrijedi na jednim od tri vrata nije granica

PDFlibPas dimenzionira svaki TIFF međuspremnik kroz Int64 aritmetiku, testira visinu slike dijeljenjem da se prevelik umnožak nikad ne stvori, i izvodi istu rutinu ValidatePageForDecode pri parsiranju oznaka i na oba ulaza u dekodere
Tri stropa, Int64 aritmetika bajtova retka i jedna dijeljena validacijska rutina do koje se dolazi iz sva tri vrata, jer granica koja vrijedi na jednim od tri vrata nije granica

Što pozivatelj mora provjeriti prije čitanja PageInfo?

Prvo provjerite ValidTIFF, zatim PageCount, i tek onda indeksirajte PageInfo. Odbijena datoteka može ostaviti PageCount na nuli, a GetPageInfo na indeks izvan raspona odgovara neinicijaliziranim TTIFFPage zapisom, pa pogreškovita staza koja usput čita rezoluciju ili broj uzoraka na kraju čita šum. Verzija 3.534.1 popravila je oba pozivatelja unutar knjižnice: put uvoza slika čita XRes i YRes samo unutar valjane grane, a TPDFlib.GetImagePageCount traži ValidTIFF umjesto da sam po sebi vjeruje broju stranica različitom od nule. Nizvodno, argument Options u AddImageFromFile jest broj stranice od 1 za višestranični TIFF, pa GetImagePageCount mora biti pouzdan prije nego što petlja krene, a ne poslije. Nula stranica sada je pravi odgovor koji znači "ovdje nema ničeg dekodirivog", a ne posljedica ranog izlaza, što je najvažnije kada povezujete i ispreplićete duplex serije skenova i jedna tiho krivo dekodirana stranica završi na pogrešnom mjestu

var
  Pdf: TPDFlib;
  Pages, I, ImageID: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
    if Pages < 1 then
      Exit;  // neispravno zaglavlje, BigTIFF, tile raspored ili iznad budžeta
    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;

Izgraditi dekoder ili povezati libtiff?

PDFlibPas zadržava ugrađeni dekoder, a odlučujući faktor je doseg platformi, a ne autorstvo. Otprilike 1.873 retka Object Pascala prevodi se svugdje gdje ide i prevodilac: Win32, Win64, macOS, iOS, Android te FPC na Linuxu. libtiff 4.7.1 jest oko 30.000 redaka C-a raspršenih po 34 tif_*.c prijevodne jedinice, a prethodno izgrađene objektne datoteke koje danas postoje pokrivaju samo Windows. Njegovo usvajanje mijenjalo bi potpuni TIFF pokrivač za popis podržanih platformi koji se sužava na mašine koje mogu pokrenuti C alatni lanac, plus fazu povezivanja koju još nitko nije prošao

Ono što to košta vrijedi reći bez uljepšavanja. Ugrađeni dekoder rukuje onim što rad sa skeniranim dokumentima stvarno proizvodi: CCITT Group 3 jednodimenzionalni i dvodimenzionalni, Group 4, LZW, Deflate, PackBits i JPEG-in-TIFF, kroz WhiteIsZero, BlackIsZero, RGB, paletne i CMYK fotometrije s Predictor 1 i 2. Ti se sadržaji poklapaju s PDF filtrima u ISO 32000-1 §7.4.4 i §7.4.6, po čemu TIFF front-end nosi toliku težinu u cjevovodu skeniranja. Ono što ne rukuje jest BigTIFF, tile-ovi, floating-point Predictor 3, PixarLog i SGILog, old-style JPEG kompresija 6 i sub-IFD piramide. Od 3.534.1 svako od toga imenovano je odbijanje, a ne pogrešna slika, i knjižnica drži zapisan popis okidača za ponovno otvaranje odluke o libtiffu:

  • kupac prijavi BigTIFF datoteku i treba nativnu podršku, a ne korak pretvorbe
  • kupac prijavi tiled TIFF iz medicinskih, GIS ili industrijskih izvora i treba ga dekodirati na mjestu
  • kupac prijavi floating-point Predictor 3 TIFF
  • objavljena ranjivost pogodi ugrađene CCITT ili LZW putove dekodiranja
  • argument o više platformi prestaje vrijediti, bilo jer podrška za macOS, iOS i Android otpada, bilo jer ponovno upotrebljiva libtiff integracija već pokriva macOS i Linux

Sama migracija ima opseg, a nije hipotetska: USE_LIBTIFF uvjet bi zadržao javnu površinu TPDFTIFF netaknutom, usmjerio LoadFromStream kroz TIFFClientOpen sa stream povratnim pozivima i ostavio Pascal parser kao fallback za ne-Windows. Dok jedan od tih okidača stvarno ne opali, održavanje dva dekodera i udvostručene testne matrice ne kupuje ništa što kupac može osjetiti. Odgađati trošak čiji je izlaz već zapisan nešto je drugo od ignoriranja

Gdje to ostavlja cjevovod skeniranih dokumenata

Tretirajte TPDFTIFF kao kapiju, a ne pretvarač. Učitajte datoteku, pročitajte ValidTIFF, i zapisujte LastError doslovno kad god je lažan, jer je taj niz sada najkraći put od terenske prijave do dijagnoze. Datoteke koje padnu na kapiji ostaju oporavljive pretvorbom uzvodno, što je danas praktičan odgovor za BigTIFF i tiled izvore. Za ulaz koji uopće nije TIFF, PDFlibPas ide zasebnim putem kroz svoj AVIF, HEIF i JPEG XL unos slika, pa pitanje koji dekoder posjeduje koji format ostaje izričito, a ne naslućeno

Sve ovo stoji iza uobičajenog API-ja za slike, pa cjevovod dokumenata dobiva čvršću granicu bez mijenjanja ijednog retka pozivnog koda osim provjere broja stranica koji je već trebao provjeravati. Ako razmatrate nativan TIFF-u-PDF put za Delphi ili C++Builder, cijela komponenta i njeno rukovanje slikama dokumentirani su na stranici PDF Library for Delphi