A PDFlibPas a TIFF-et kézzel írt Object Pascal elemzővel dekódolja, libtiff-kötés helyett, és a 3.534.1-es verzió pontosan ott szigorított, ahol ez az elemző visszautasítja a bemenetet. A BigTIFF 43-as mágiája most név szerint kerül elutasításra, a TileOffsets és a TileByteCounts a címkefeldolgozás során marad el, és minden puffer Int64 aritmetikával, 256 MiB-os dekódolási plafon alatt kap méretet
A hiba, amelyet ez bezár, laborban sosem jelentkezik. Egy három éve csendben futó szkennelő átjáróként jelentkezik, amíg egy ügyfél geoinformatikai archívumot vagy egész diás orvosi képet nem irányít át rajta. A fájl szabályos TIFF-fejléccel rendelkezik. Elemzhető. Ami kijön, az egy oldalnyi sávos zaj, vagy egy több gigabájtos allokáció, amely ledönti a szolgáltatást, és az út során semmi nem nyilvánította érvénytelennek a bemenetet. Ez a hibamódus, amivel érdemes megtervezni a védekezést: nem összeomlás, hanem magabiztosan kézbesített rossz válasz
Miért nem bizonyítja az II vagy MM, hogy klasszikus TIFF van?
Mert a bájtsorrend-jelölőt mindkét változat közösen használja. A klasszikus TIFF és a BigTIFF is II-vel vagy MM-mel nyílik, és a mező, amely ténylegesen megkülönbözteti őket, a közvetlenül utána következő 16 bites mágia: 42 a TIFF 6.0 specifikációban definiált klasszikus TIFF-hez, 43 a BigTIFF-hez a 64 bites offszetjeivel. A FValidTIFF := PopWord = 42 alakban írt betöltő nem téved a klasszikus TIFF-et illetően, de két nagyon különböző elutasítást omlassza össze egyetlen némata logikai értékbe, így egy BigTIFF megkülönböztethetetlenné válik egy átnevezett, csonka JPEG-től. A PDFlibPas most szétválasztja az eseteket, és mindegyiket külön rögzíti a TPDFTIFF.LastError-ban: a négy bájtnál rövidebb fejléc, az érvénytelen bájtsorrend-jelölő, a 43-as mágia és bármely más mágiaérték eltérő szöveget ad. A könyvtár a BigTIFF-et továbbra sem dekódolja, és ennek őszinte kimondása a lényeg. A hívó megkapja a különbséget aközött, hogy ez nem TIFF, és aközött, hogy ez egy olyan TIFF, amelynek 64 bites offszet-elrendezését a beépített dekóder nem implementálja — ez pedig az a különbség, amely eldönti, hogy egy válaszban lezárható support-jegyet vagy egy hétig tartó találgatást kapunk
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;
A csempék más geometria, nem egy másik offszettömb
A PDFlibPas a csempézett TIFF-et már a címkék feldolgozása közben visszautasítja, még mielőtt bármilyen pixeladathoz hozzáérne. A hibát csalogató rövidítés könnyen látható: a 324-es címke (TileOffsets) és a 325-ös címke (TileByteCounts) fájloffszetek és bájtszámok tömbje, szerkezetileg azonos a strip-tömbökkel, így a meglévő strip-mezők rájuk irányítása két sorba kerül és hibátlanul fordul. Ez mégis rossz. A csempék kétdimenziós rácst alkotnak kibélelt széltömbökkel, saját sorlépéssel minden csempén belül, és semmilyen RowsPerStrip szemantikával nem bírnak, ahogy a TIFF 6.0 csempézett képekről szóló szakasza kimondja. A csempetartalom strip-dekóderhez jutatása ezért nem hangos hibával akad el. A SimpleExtract és a CompDecode rossz lépéssel járja be az adatot, és olyan képet ad ki, amelynek a méretei jók, a pixeljei viszont rosszak. A régebbi kód ezt tetézte azzal, hogy a StripsAreTiles, ColumnsPerTile és RowsPerTile mezőket megtartotta a TTIFFPage-ben: csempageometriát rögzített egy olyan dekóder, amely nem áll mögötte csempe-összeállító. A 3.534.1-ben a 324-es és 325-ös címkék kezelői azonnal csempehibát jeleznek és elhagyják az IFD-t, így az elutasítás magán viseli a csempézett szót, ahelyett hogy hetekkel később renderelési panaszként bukkanna fel
Egy dimenziós határ nem memóriakeret
A szélesség és a magasság 65 535-re való szorítása szükséges, és messze nem elegendő, mert az allokációt mozgató mennyiség egy szorzat. A RowsPerStrip * Width * SamplesPerPixel túlcsordulhat 32 bites aritmetikában jóval azelőtt, hogy bármelyik tényező elérné a saját határát, és túlcsordulás nélkül is olyan allokációt nevezhet meg, amelyet egyik szolgáltatásnak sem szabad megkísérelnie. A PDFlibPas a sorbájtokat Int64-ben számolja, és három plafont léptet érvénybe együtt: 65 535 dimenziónként, 32 színkomponens, és 256 MiB dekódolt bájt
const
PDFLIB_TIFF_MAX_IMAGE_DIM = 65535;
PDFLIB_TIFF_MAX_COLOR_COMPONENTS = 32;
PDFLIB_TIFF_MAX_DECODED_IMAGE_BYTES = 256 * 1024 * 1024;
// a TPDFTIFF.ValidatePageForDecode belsejében
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);
Három részlet ott fontosabb, mint maguk a konstansok. A magasságteszt osztásként van megírva, nem szorzatként, így a túlméretezett szorzat sosem keletkezik meg. Az 1 alatti vagy a képmagasság feletti RowsPerStrip először a magasságra normalizálódik, ami az egyetlen strip-olvasást jelenti, amelyet a TIFF 6.0 már eleve implikál, és amely megakadályozza, hogy egy rosszindulatú címke felfújja a strip-puffert. És a rutin közös: a ValidatePageForDecode a címkefeldolgozás végén fut le, majd újra a SimpleExtract és a CompDecode belépésénél egyaránt, így a dekódert közvetlenül elérő kód nem tudja megkerülni a keretet. Ez ugyanaz a szabály, amelyet a PDFlibPas megbízhatatlan PDF objektumgráfok elemzésekor követ, mert három ajtó közül egyen betartatott korlát nem korlát
Mit kell a hívónak ellenőriznie a PageInfo olvasása előtt?
Először a ValidTIFF-et, aztán a PageCount-ot, és csak ezután a PageInfo indexelését kell ellenőrizni. Egy elutasított fájl a PageCount-ot nullán hagyhatja, a GetPageInfo pedig tartományon kívüli indexre inicializálatlan TTIFFPage rekorddal válaszol, így egy hibaág, amely a hiba jelentése közben felbontást vagy mintaszámokat olvas, zajt olvas be. A 3.534.1-es verzió mindkét, a könyvtáron belüli hívót megjavította: a képimport útvonala a XRes-et és a YRes-et csak az érvényes ágon belül olvassa, a TPDFlib.GetImagePageCount pedig ValidTIFF-et követel ahelyett, hogy önmagában bízna egy nem nulla oldalszámban. Lejjebb az AddImageFromFile Options argumentuma a többoldalas TIFF 1-alapú oldalszáma, így a GetImagePageCount-nak a ciklus indulása előtt, nem pedig utána kell megbízhatónak lennie. A nulla oldal most valódi válasz, jelentése az, hogy itt nincs dekódolható tartalom, nem pedig egy korai kilépés balesete, ami akkor a legfontosabb, amikor duplex szkennelési kötegeket rendszerez és közrefűz, és egy csendben rosszul dekódolt lap rossz pozícióba kerülne
var
Pdf: TPDFlib;
Pages, I, ImageID: Integer;
begin
Pdf := TPDFlib.Create;
try
Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
if Pages < 1 then
Exit; // hibás fejléc, BigTIFF, csempézett elrendezés vagy kerettúllépés
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;
Saját dekóder írása vagy libtiff-linkelés?
A PDFlibPas megtartja a beépített dekódert, és a döntő tényező a platformlefedettség, nem a szerzőség. Körülbelül 1873 sor Object Pascal ott fordul, ahová a fordító eljut: Win32, Win64, macOS, iOS, Android, valamint Linuxon az FPC. A libtiff 4.7.1 nagyjából 30 000 sor C, 34 tif_*.c fordítási egységre szórva, és a ma elérhető előrefordított objektumfájlok csak Windowst fednek le. Az átvétele a teljes TIFF-lefedettséget cserélné le egy olyan támogatott platformlistára, amely összehúzódik azokra a gépekre, amelyeken a C-eszközlánc fut, plusz hozzáadna egy linker-fázist, amelyen még senki nem ment át
Amibe ez kerül, azt érdemes csipkézés nélkül kimondani. A beépített dekóder azt kezeli, amit a szkennelt dokumentumokkal való munka ténylegesen termel: CCITT Group 3 egy- és kétdimenziósat, Group 4-et, LZW-t, Deflate-et, PackBits-et és TIFF-be ágyazott JPEG-et, a WhiteIsZero, BlackIsZero, RGB, palettás és CMYK fotometriákra kiterjedően Predictor 1-gyel és 2-vel. Ezek a tartalmak egybeesnek az ISO 32000-1 §7.4.4 és §7.4.6 PDF-szűrőivel, ezért hordoz annyi súlyt a TIFF-előtét egy szkennelési folyamatban. Amit nem kezel: BigTIFF, csempék, lebegőpontos Predictor 3, PixarLog és SGILog, a régi stílusú 6-os JPEG-tömörítés és az al-IFD piramisok. A 3.534.1 óta mindegyik nevesített elutasítás, nem pedig rossz kép, és a könyvtár írásos triggerlistát tart a libtiff-döntés újranyitásához:
- egy ügyfél BigTIFF-fájlt jelent, és konverziós lépés helyett natív támogatást kér
- egy ügyfél orvosi, GIS vagy ipari forrásból származó csempézett TIFF-et jelent, és azt helyben dekódolva kéri
- egy ügyfél lebegőpontos Predictor 3 TIFF-et jelent
- egy nyilvánosságra hozott sérülékenység a beépített CCITT vagy LZW dekódolási útvonalakat érinti
- a platformfüggetlenségi érv megszűnik érvényes lenni, akár azért, mert a macOS, iOS és Android támogatás kikerül, akár azért, mert egy újrafelhasználható libtiff-integráció már lefedi a macOS-t és a Linuxot
Az átállás maga felvázolt terv, nem hipotetikus: egy USE_LIBTIFF feltételes fordítás érintetlenül hagyná a TPDFTIFF publikus felületét, a LoadFromStream-et TIFFClientOpen-on keresztül irányítaná, stream-visszahívásokkal, a Pascal-elemzőt pedig nem Windowsos tartalékként hagyná meg. Amíg e triggerjek egyike sem pattan fel valójában, két dekóder fenntartása és egy megkettőzött tesztmátrix nem ad semmit, amelyet az ügyfél érzékelne. Egy költség halasztása a már leírt menekülőúttal más dolog, mint figyelmen kívül hagyni
Mit jelent mindez egy szkennelési folyamatnak
Kezelje a TPDFTIFF-et kapuként, nem konvertátorként. Töltse be a fájlt, olvassa a ValidTIFF-et, és amikor hamis, jegyezze naplóba a LastError-et szóról szóra, mert ez a sztring mára a legrövidebb út a terepi bejelentéstől a diagnózisig. A kapun elbukó fájlok továbbra is megmenthetők, ha fentebb konvertálják őket, ami a BigTIFF és a csempézett források gyakorlati válasza ma. A TIFF-en teljesen kívül eső bemenetnél a PDFlibPas külön útvonalat vesz, a AVIF, HEIF és JPEG XL képbeviteli útvonalán, így az a kérdés, hogy melyik dekóder melyik formátumot birtokolja, explicit marad, nem pedig magától alakul ki
Mindez a szokásos kép-API mögött ül, így egy dokumentumfolyamat szorosabb határvonalat kap anélkül, hogy egyetlen hívósort megváltoztatna azon túl, hogy ellenőrzi az oldalszámot, amelyet már amúgy is ellenőriznie kellene. Ha natív TIFF-to-PDF útvonalat mérlegel Delphi vagy C++Builder alá, a teljes komponens és a képfeldolgozása a PDF Library for Delphi oldalon van dokumentálva