Tehnični članak

Ojačan TIFF dekodirnik v Delphiju: BigTIFF, tlakovani TIFF

PDFlibPas dekodira TIFF z ročno napisanim razčlenjevalnikom v Object Pascalu namesto z vezavo na libtiff, različica 3.534.1 pa je zaostrila ravno tiste točke, kjer ta razčlenjevalnik zavrne vhod. BigTIFF z magično vrednostjo 43 zdaj zavrne po imenu, TileOffsets in TileByteCounts zavrne že med razčlenjevanjem oznak, vsak medpomnilnik pa se velikost izračuna v aritmetiki Int64 pod stropom 256 MiB za dekodiranje

Napaka, ki jo to zapre, se v laboratoriju nikoli ne pokaže. Pokaže se kot skenirni prehod, ki tri leta teče tiho, dokler kupec skoz njega ne usmeri geoprostorskega arhiva ali medicinske slike celega stekalca. Datoteka ima popolnoma veljavno glavo TIFF. Razčleni se. Rezultat je stran progastega šuma ali večgigabajtna dodelitev, ki sesuje storitev, in nič na poti ni razglasilo vhoda za neveljaven. To je oblika odpovedi, vredna inženirske pozornosti: ne trk, temveč samozavestno podan napačen odgovor

Zakaj II ali MM ne dokazujeta klasičnega TIFFa?

Ker oznako vrstnega reda bajtov si delita oba narečja. Klasični TIFF in BigTIFF se odpreta z II ali MM, polje, ki ju dejansko loči, pa je 16-bitna magična vrednost takoj za njo: 42 za klasični TIFF iz specifikacije TIFF 6.0, 43 za BigTIFF s 64-bitnimi odmiki. Nalagalnik, napisan kot FValidTIFF := PopWord = 42, se pri klasičnem TIFFu ne moti, ampak v en tihi boolean stisne dve zelo različni zavrnitvi, zato je BigTIFF nerazločljiv od odrezanega JPEGa, ki ga je nekdo le preimenoval. PDFlibPas primere zdaj loči in vsako zabeleži v TPDFTIFF.LastError: glava, krajša od štirih bajtov, neveljavna oznaka vrstnega reda bajtov, magična vrednost 43 in katera koli druga magična vrednost vsaka dajo svoje besedilo. Knjižnica BigTIFF še vedno ne dekodira in tako odkrito povedati je ravno poanta. Klicatelj dobi razliko med »to ni TIFF« in »to je TIFF, katerega 64-bitno razporeditev odmikov vgrajeni dekodirnik ne podpira«, kar je razlika med zahtevkom za podporo, ki ga odgovorite v enem sporočilu, in takšnim, ki se spremeni v teden ugibanja

Nalagalnik TIFF v PDFlibPas bere oznako vrstnega reda bajtov in 16-bitno magično vrednost ločeno, zato krajša glava, neveljavna oznaka, BigTIFF z magično vrednostjo 43 in katera koli druga vrednost vsaka dajo svoje besedilo LastError namesto enega tihega booleana
Klasični TIFF in BigTIFF se odpreta z isto oznako vrstnega reda bajtov, zato PDFlibPas loči štiri primere zavrnitve in vsakega poimenuje v 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;

Tlaki so drugačna geometrija, ne le še ena tabela odmikov

PDFlibPas tlakovani TIFF zavrne že med razčlenjevanjem oznak, še pred katerim koli pikselnim podatkom. Bližnjica, ki vabi napako, je vidna na prvi pogled: oznaka 324 (TileOffsets) in oznaka 325 (TileByteCounts) sta tabeli odmikov datoteke in števil bajtov, strukturno identični tabelam trakov, zato usmerjanje obstoječih polj trakov vanju stane dve vrstici in se prevede brez opomb. A je napačna. Tlaki tvorijo dvorazsežno mrežo z oblazinjenimi robovi, svojim korakom vrstic znotraj vsakega tlaka in brez vsake semantike RowsPerStrip, kot razlaga razdelek o tlakovanih slikah v TIFF 6.0. Podajanje tlakovanih podatkov dekodirniku trakov se zato ne konča z glasno napako. SimpleExtract in CompDecode prehodita podatke s pokvarjenim korakom in ustvarita sliko s pravimi dimenzijami in napačnimi piksli. Starejša koda je to še stopnjevala s polji StripsAreTiles, ColumnsPerTile in RowsPerTile v TTIFFPage: geometrija tlakov, zabeležena v dekodirniku brez sestavljalnika tlakov za njim. V 3.534.1 obravnavaevalca oznak 324 in 325 sprožita napako tlakov in IFD takoj opustita, zato zavrnitev nosi besedo »tlakovano« namesto da bi se razkrila šele čez par tednov kot pritožba ob izrisu

PDFlibPas primeša postavitev trakov, kjer si pasovi polne širine delijo en korak vrstic, s postavitvijo tlakov, dvorazsežno mrežo oblazinjenih robov, ter zavrne oznaki 324 in 325 med razčlenjevanjem oznak, še pred katerim koli pikselnim podatkom
Tabele tlakov so strukturno identične tabelam trakov, zato podajanje le-teh dekodirniku trakov da prave dimenzije in napačne piksle

Omejitev dimenzij še ni pomnilniški proračun

Omejiti širino in višino na 65.535 vsako je nujno in še zdaleč ne zadosti, ker količina, ki poganja dodelitev, je zmnožek. RowsPerStrip * Width * SamplesPerPixel lahko prelije 32-bitno aritmetiko, še preden katera stran doseže lastno mejo, in tudi brez prelivanja lahko zahteva dodelitev, ki je nobena storitev ne bi smela poskusiti. PDFlibPas bajte vrstic računa v Int64 in skupaj uveljavi tri stropove: 65.535 na dimenzijo, 32 barvnih komponent in 256 MiB dekodiranih bajtov

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

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

Trije podrobnosti tam pomenijo več kot same konstante. Preizkus višine je zapisan kot deljenje in ne kot množenje, zato prevelik zmnožek sploh ni nikoli izračunan. RowsPerStrip pod 1 ali nad višino slike se najprej normalizira na višino, kar je branje po enem traku, ki ga TIFF 6.0 že implicira, in zadrži sovražno oznako pred napihovanjem medpomnilnika trakov. Rutina pa je v skupni rabi: ValidatePageForDecode teče na koncu razčlenjevanja oznak in znova na vstopu v SimpleExtract in CompDecode, zato koda, ki pride do dekodirnika neposredno, ne more obiti proračuna. To je isto pravilo, ki mu PDFlibPas sledi pri razčlenjevanju nezaupanja vrednih grafov objektov PDF, ker meja, uveljavljena na enih vratih od treh, ni meja

PDFlibPas velikost vsakega medpomnilnika TIFF izračuna v aritmetiki Int64, višino slike preveri z deljenjem, da prevelik zmnožek sploh ne nastane, in isto rutino ValidatePageForDecode izvede pri razčlenjevanju oznak in na obeh vstopih v dekodirnik
Trije stropovi, aritmetika bajtov vrstic v Int64 in ena skupna validacijska rutina, dosegljiva z vseh treh vrat, ker meja, uveljavljena na enih vratih od treh, ni meja

Kaj mora klicatelj preveriti, preden bere PageInfo?

Najprej preverite ValidTIFF, nato PageCount in šele potem indeksirajte PageInfo. Zavrnjena datoteka lahko pusti PageCount na nič, GetPageInfo pa odgovori na indeks izven obsega z neinicializiranim zapisom TTIFFPage, zato pot napake, ki med poročanjem o odpovedi bere ločljivost ali število vzorcev, na koncu bere šum. Različica 3.534.1 je popravila oba klicatelja znotraj knjižnice: pot uvoza slik bere XRes in YRes le znotraj veljavne veje, TPDFlib.GetImagePageCount pa zahteva ValidTIFF namesto da bi sam zaupala neničelnemu številu strani. Nižje v verigi je argument Options v AddImageFromFile številka strani od 1 za večstranski TIFF, zato mora biti GetImagePageCount zaupanja vreden, preden se zanka začne, in ne po njej. Nič strani je zdaj pravi odgovor v pomenu »tu ni nič dekodirljivega« in ne naključje zgodnjega izhoda, kar najbolj šteje, ko zbirate in prepletate dvostransne skenirne serije in bi en tiho zmedeno dekodiran list pristal na napačnem mestu

var
  Pdf: TPDFlib;
  Pages, I, ImageID: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
    if Pages < 1 then
      Exit;  // pokvarjena glava, BigTIFF, tlakovana postavitev ali čez proračun
    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;

Lasten dekodirnik ali povezava na libtiff?

PDFlibPas obdrži vgrajeni dekodirnik, odločujoč dejavnik pa je dosežek po platformah in ne avtorstvo. Približno 1.873 vrstic Object Pascala se prevede kamor koli gre prevajalnik: Win32, Win64, macOS, iOS, Android in FPC na Linuxu. libtiff 4.7.1 je okoli 30.000 vrstic C, razporejenih na 34 prevajalskih enot tif_*.c, predizgrajene objektne datoteke, ki obstajajo danes, pa pokrivajo le Windows. Posvojitev bi zamenjala popolno pokritost TIFF za seznam podprtih platform, skrčen na stroje, ki znajo pognati verigo orodij C, plus prehod skozi povezovalnik, ki ga še nihče ni prehodil

Kaj to stane, je vredno povedati brez omikov. Vgrajeni dekodirnik zna, kar delo s skeniranimi dokumenti dejansko ustvari: CCITT Group 3 enorazsežni in dvorazsežni, Group 4, LZW, Deflate, PackBits in JPEG-v-TIFF, čez fotometrije WhiteIsZero, BlackIsZero, RGB, paleta in CMYK s Predictor 1 in 2. Ti podatki se poklapljajo s filtri PDF v ISO 32000-1 §7.4.4 in §7.4.6, zato nosi vmesnik TIFF toliko teže v skenirnem cevovodu. Česa ne zna, so BigTIFF, tlaki, plavajoči Predictor 3, PixarLog in SGILog, staro stiskanje JPEG 6 in piramide pod-IFD. Od 3.534.1 je vsak od teh imenovana zavrnitev in ne napačna slika, knjižnica pa vzdržuje zapisan seznam sprožilcev za ponovno odprtje odločitve o libtiffu:

  • kupec prijavi datoteko BigTIFF in potrebuje izvorno podporo namesto koraka pretvorbe
  • kupec prijavi tlakovani TIFF iz medicinskih, GIS ali industrijskih virov in potrebuje dekodiranje na mestu
  • kupec prijavi TIFF s plavajočim Predictor 3
  • objavljena ranljivost zadene vgrajene poti dekodiranja CCITT ali LZW
  • argument o več platformah preneha veljati, bodisi ker se podpora za macOS, iOS in Android opusti, bodisi ker ponovno uporabna integracija libtiffa že pokriva macOS in Linux

Sama selitev je orisana in ne domišljena: pogojni USE_LIBTIFF bi ohranil javno površino TPDFTIFF nedotaknjeno, usmeril LoadFromStream skozi TIFFClientOpen s povratnimi klici toka in pustil Pascalov razčlenjevalnik kot zasilno pot za ne-Windows. Dokler kateri od teh sprožilcev dejansko ne sproži, vzdrževanje dveh dekodirnikov in podvojenih testnih matrik ne kupi ničesar, kar bi kupec začutil. Odlaganje stroška z že zapisano zasilno potjo je nekaj drugega kot njegovo ignoriranje

Kaj to pomeni za cevovod skeniranih dokumentov

TPDFTIFF obravnavajte kot vrata in ne kot pretvornik. Naložite datoteko, preberite ValidTIFF in vsakič, ko je ta napačen, zabeležite LastError dobesedno, ker je ta niz zdaj najkrajša pot od poročila s terena do diagnoze. Datoteke, ki ne gredo skozi vrata, ostanejo rešljive z pretvorbo višje v verigi, kar je danes praktičen odgovor za vire BigTIFF in tlakovane. Za vhod, ki je povsem izven TIFFa, PDFlibPas vzame ločeno pot skozi svojo pot uvoza slik AVIF, HEIF in JPEG XL, tako da vprašanje, kateri dekodirnik lasti kateri format, ostane izrecno in ne naključno

Vse to sedi za navadnim API-jem za slike, zato dokumentni cevovod dobi ožjo mejo brez spremembe niti ene vrstice klicne kode, razen preverjanja števila strani, ki ga je že moralo preverjati. Če vrednotite izvorno pot TIFF-v-PDF za Delphi ali C++Builder, sta celotna komponenta in njena obravnava slik opisana na strani PDF Library for Delphi