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
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
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
Š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