PDFlibPas dekóduje TIFF parserem psaným ručně v Object Pascalu namísto vazby na libtiff a verze 3.534.1 zpřísnila přesně ta místa, kde tento parser vstup odmítá. BigTIFF s magickým číslem 43 se nyní odmítá jménem, TileOffsets a TileByteCounts se zavrhují už při parsování značek a každý buffer se rozměřuje aritmetikou Int64 pod stropem 256 MiB pro dekódování
Porucha, kterou tahle změna zavírá, se v laboratoři nikdy neukáže. Objeví se jako skenovací brána, která tři roky tiše běží, dokud zákazník nepustí přes ni geografický archiv nebo celosnímkový zdravotnický snímek. Soubor má legitimní hlavičku TIFF. Zparsuje se. Ven vyletí stránka pruhovaného šumu, nebo vícegigabajtová alokace, která shodí službu, a nic na té cestě nikdy neoznačilo vstup za neplatný. To je tvar poruchy, proti kterému se vyplatí inženýrsky bránit: ne pád, ale sebejistě doručená špatná odpověď
Proč II nebo MM neprokazuje, že máte klasické TIFF?
Protože značku byte order sdílejí oba dialekty. Klasické TIFF i BigTIFF začínají II nebo MM a polem, které je skutečně odlišuje, je šestnáctibitová magie bezprostředně za ním: 42 pro klasické TIFF definované ve specifikaci TIFF 6.0, 43 pro BigTIFF se 64bitovými offsety. Loader napsaný jako FValidTIFF := PopWord = 42 se u klasického TIFF nemýlí, ale sbalí dvě velmi odlišná zamítnutí do jednoho tichého booleanu, takže BigTIFF je nerozeznatelný od zkráceného JPEGu, který někdo jen přejmenoval. PDFlibPas nyní případy rozděluje a každou zvlášť zapisuje do TPDFTIFF.LastError: hlavička kratší než čtyři bajty, neplatná značka byte order, magie 43 i každá jiná magická hodnota dávají odlišné texty. Knihovna BigTIFF stále nedešifruje a říct to napřímo je přesně ten smysl. Volající dostane rozdíl mezi „toto není TIFF“ a „toto je TIFF, jehož 64bitové rozložení offsetů vestavěný dekodér neimplementuje“, což je rozdíl mezi tickety, na který odpovíte jednou odpovědí, a tickety, který se změní na týden hádání
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;
Dlaždice jsou jiná geometrie, ne další pole offsetů
PDFlibPas zamítá dlaždicovaný TIFF už při parsování značek, dřív než se dotkne jakéhokoli pixelu. Zkratka, která chybu zve, je snadno vidět: značky 324 (TileOffsets) a 325 (TileByteCounts) jsou pole offsetů a délek v bajtech, strukturálně shodné s poli pruhů, takže nasměrovat stávající pole pruhů na ně stojí dva řádky a přeloží se to čistě. Je to ale špatně. Dlaždice tvoří dvojrozměrnou mřížku s vyplněnými okrajovými bloky, mají vlastní krok řádku uvnitř každé dlaždice a žádnou sémantiku RowsPerStrip, jak rozebírá sekce o dlaždicovaných snímcích ve specifikaci TIFF 6.0. Krmení dlaždicových dat dekodéru pruhů proto nehavaruje hlasitě. SimpleExtract a CompDecode projdou data špatným krokem a vyplivnou snímek se správnými rozměry a špatnými pixely. Starší kód to ještě prohluboval tím, že držel StripsAreTiles, ColumnsPerTile a RowsPerTile v TTIFFPage: geometrie dlaždic zapsaná dekodérem, který žádný skladač dlaždic nemá. Ve 3.534.1 obslužné rutiny značek 324 a 325 vyhodí chybu dlaždic a IFD okamžitě opustí, takže odmítnutí nese slovo „tiled“ a nevyplave o týdny později jako stížnost na vykreslování
Jedno omezení rozměru není rozpočet paměti
Omezit šířku a výšku na 65 535 je nutné a zdaleka to nestačí, protože veličina řídící alokaci je součin. RowsPerStrip * Width * SamplesPerPixel může přetéct 32bitovou aritmetiku dřív, než některá strana dosáhne vlastního limitu, a i bez přetečení může popsat alokaci, kterou by si žádná služba neměla dovolit. PDFlibPas počítá bajty řádku v Int64 a vynucuje tři stropy společně: 65 535 na rozměr, 32 barevných složek a 256 MiB dekódovaných bajtů
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// uvnitř 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);
Tři detaily tam hrají větší roli než konstanty samy. Test výšky je zapsán jako dělení, ne jako násobení, takže převislý součin vůbec nevznikne. RowsPerStrip pod 1 nebo nad výšku snímku se nejdřív normalizuje na výšku, což je čtení do jednoho pruhu, které TIFF 6.0 přímo implikuje, a zároveň to brání nepřátelské značce nafouknout buffer pruhu. A rutina je sdílená: ValidatePageForDecode běží na konci parsování značek a znovu na vstupu SimpleExtract i CompDecode, takže kód, který do dekodéru vejde přímo, nemůže kolem rozpočtu obejít. Totožné pravidlo PDFlibPas dodržuje při parsování nedůvěryhodných grafů PDF objektů, protože limit vynucený u jedněch ze tří dveří není limit
Co musí volající zkontrolovat před čtením PageInfo?
Nejdřív ValidTIFF, pak PageCount a teprve potom indexujte PageInfo. Zamítnutý soubor může nechat PageCount na nule a GetPageInfo odpoví na index mimo rozsah neinicializovaným záznamem TTIFFPage, takže chybová cesta, která cestou k nahlášení poruchy čte rozlišení nebo počty vzorků, nakonec čte šum. Ve 3.534.1 se opravili oba volající uvnitř knihovny: cesta importu snímku čte XRes a YRes jen uvnitř platné větve a TPDFlib.GetImagePageCount vyžaduje ValidTIFF místo toho, aby věřila nenulovému počtu stránek samému o sobě. Níže po proudu je argument Options funkce AddImageFromFile číslem stránky od jedničky pro vícestránkový TIFF, takže GetImagePageCount musí být důvěryhodná dřív, než se smyčka rozjede, a ne až po ní. Nula stránek je teď skutečná odpověď ve smyslu „tady se nic nedá dekódovat“, ne artefakt předčasného returnu, což se nejvíc projeví, když slučujete a prokládáte dávky duplexních skenů a jedna potichu špatně dekódovaná strana by skončila na špatném místě
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // špatná hlavička, BigTIFF, dlaždicové rozložení nebo přes rozpočet
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;
Vlastní dekodér, nebo linkovat libtiff?
PDFlibPas drží vestavěný dekodér a rozhodujícím faktorem je dosah napříč platformami, ne autorství. Zhruba 1 873 řádků Object Pascalu se přeloží všude, kam se dostane kompilátor: Win32, Win64, macOS, iOS, Android a FPC na Linuxu. libtiff 4.7.1 je kolem 30 000 řádků C roztahaných do 34 překladových celků tif_*.c a předkompilované objektové soubory, které dnes existují, pokrývají jen Windows. Přijetím byste vyměnili kompletní pokrytí TIFF za seznam podporovaných platforem, který se smrskne na stroje, kde běží C toolchain, plus průchod linkerem, který ještě nikdo neprošel
Co to stojí, stojí říct bez laciného marketingu. Vestavěný dekodér zvládá, co práce se skenovanými dokumenty reálně produkuje: CCITT Group 3 jednorozměrný i dvourozměrný, Group 4, LZW, Deflate, PackBits a JPEG v TIFF, přes fotometrie WhiteIsZero, BlackIsZero, RGB, paletu a CMYK s Predictorem 1 a 2. Tyto payloady se kryjí s PDF filtry v ISO 32000-1 §7.4.4 a §7.4.6, a proto nese TIFF front-end ve skenovací pipeline takovou váhu. Nezvládá BigTIFF, dlaždice, predictor 3 pro plovoucí desetinnou čárku, PixarLog a SGILog, starou JPEG kompresi 6 a pyramidy pod-IFD. Od 3.534.1 je každá z těch věcí pojmenované odmítnutí místo špatného snímku a knihovna si vede písemný seznam spouštěčů pro znovuotevření rozhodnutí o libtiff:
- zákazník nahlásí BigTIFF soubor a potřebuje nativní podporu místo kroku s konverzí
- zákazník nahlásí dlaždicovaný TIFF z medicínských, GIS nebo průmyslových zdrojů a potřebuje ho dekódovat na místě
- zákazník nahlásí TIFF s predictorem 3 pro plovoucí desetinnou čárku
- zveřejněná zranitelnost dopadne na vestavěné cesty dekódování CCITT nebo LZW
- přestane platit argument napříč platformami, ať už protože podpora macOS, iOS a Android skončí, nebo protože existující znovupoužitelná integrace libtiff pokryje macOS a Linux
Samotná migrace je zarámovaná, ne hypotetická: podmíněný USE_LIBTIFF by zachoval veřejný povrch TPDFTIFF nedotčený, směroval LoadFromStream přes TIFFClientOpen se streamovými callbacky a nechal Pascal parser jako fallback pro ne-Windows. Dokud jeden z těch spouštěčů skutečně nezazvoní, údržba dvou dekodérů a dvojnásobné testovací matrice nekupuje nic, co by zákazník pocítil. Odkládat náklad s tím, že úniková cesta už je sepsaná, je něco jiného než náklad ignorovat
Co to znamená pro pipeline skenovaných dokumentů
Berte TPDFTIFF jako bránu, ne jako konvertor. Načtěte soubor, přečtěte ValidTIFF a kdykoli je nepravdivé, logujte LastError doslova, protože tenhle řetězec je dnes nejkratší cesta z hlášení od zákazníka k diagnóze. Soubory, které bránou neprojdou, zůstávají zachránitelné konverzí po proudu, a to je dnes praktická odpověď na BigTIFF i dlaždicované zdroje. Pro vstup mimo TIFF úplně bere PDFlibPas zvlášť cestu přes vstup snímků AVIF, HEIF a JPEG XL, takže otázka, který dekodér vlastní který formát, zůstává výslovná místo toho, aby vznikala sama
Všechno tohle sedí za obyčejným snímkovým API, takže pipeline dokumentů dostane těsnější hranici bez změny jediného řádku volacího kódu, kromě kontroly počtu stránek, kterou stejně měla dělat. Pokud zvažujete nativní cestu TIFF do PDF pro Delphi nebo C++Builder, celá komponenta a její zpracování snímků jsou zdokumentované na stránce PDF Library for Delphi