Techninis straipsnis

Delphi TIFF dekoderio sutvirtinimas: BigTIFF ir tiled TIFF

PDFlibPas dekoduoja TIFF ranka parašytu Object Pascal analizatoriumi, o ne libtiff susiejimu, ir 3.534.1 versija sugriežtino būtent tas vietas, kuriose tas analizatorius atmeta įvestį. BigTIFF magija 43 dabar atmetama įvardijant, TileOffsets ir TileByteCounts atmetami žymų analizės metu, o kiekvieno buferio dydis skaičiuojamas Int64 aritmetika, neviršijant 256 MiB dekodavimo ribos

Šis uždaromas defektas niekada nepasireiškia laboratorijoje. Jis pasireiškia kaip skenavimo vartai, kurie ramiai veikia trejus metus, kol klientas per juos nukreipia geoduomenų archyvą arba whole-slide medicininį vaizdą. Failas turi teisėtą TIFF antraštę. Jis išanalizuojamas. Rezultatas — puslapis juostuoto triukšmo arba kelių gigabaitų atminties rezervavimas, nurungiantis tarnybą, ir niekas pakeliui niekada nepaskelbė įvesties neteisėta. Tai gedimo forma, prieš kurią verta kurti apsaugą: ne avarija, o drąsiai pateiktas neteisingas atsakymas

Kodėl II arba MM neįrodo, kad turite klasikinį TIFF?

Nes baitų eiliškumo žymė yra bendra abiem dialektams. Klasikinis TIFF ir BigTIFF kiekvienas prasideda II arba MM, o laukas, kuris iš tikrųjų juos skiria, yra 16 bitų magija iškart po ja: 42 klasikiniam TIFF, kaip apibrėžta TIFF 6.0 specifikacijoje, 43 BigTIFF su jo 64 bitų poslinkiais. Įkroviklis, parašytas kaip FValidTIFF := PopWord = 42, dėl klasikinio TIFF neklysta, tačiau sutraukia du labai skirtingus atmetimus į vieną tylią bulio reikšmę, todėl BigTIFF tampa neatskiriamas nuo perkirpto JPEG, kurį kas nors pervadino. PDFlibPas dabar atskiria šiuos atvejus ir kiekvieną užfiksuoja TPDFTIFF.LastError: antraštė, trumpesnė nei keturi baitai, neteisinga baitų eiliškumo žymė, magija 43 ir bet kuri kita magiškoji reikšmė duoda skirtingą tekstą. Biblioteka vis dar nedekoduoja BigTIFF, ir tai pasakyti atvirai yra esmė. Kvietėjas gauna skirtumą tarp „tai nėra TIFF“ ir „tai TIFF, kurio 64 bitų poslinkių išdėstymo integruotas dekoderis nėra įgyvendinęs“, o tai skirtumas tarp palaikymo kreipimosi, į kurią atsakoma vienu atsakymu, ir tos, kuri virsta savaite spėliojimų

PDFlibPas TIFF įkroviklis skaito baitų eiliškumo žymę ir 16 bitų magiją atskirai, todėl trumpa antraštė, neteisinga žymė, BigTIFF magija 43 ir bet kuri kita magiškoji reikšmė duoda skirtingą LastError tekstą vietoj vienos tylios bulio reikšmės
Klasikinis TIFF ir BigTIFF prasideda ta pačia baitų eiliškumo žyme, todėl PDFlibPas atskiria keturis atmetimo atvejus ir kiekvieną įvardija LastError lauke
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;

Plytelės yra kita geometrija, o ne dar vienas poslinkių masyvas

PDFlibPas atmeta tiled TIFF žymų analizės metu, dar neliestą jokio pikselių duomenų. Klaidą provokuojantis trumpasis kelias lengvai įžiūrimas: žyma 324 (TileOffsets) ir žyma 325 (TileByteCounts) yra failo poslinkių ir baitų skaičių masyvai, struktūriškai identiški juostų masyvams, todėl nukreipti į juos esamus juostų laukus kainuoja dvi eilutes ir kompiliuojasi be priekaištų. Tai kartu ir neteisinga. Plytelės formuoja dvimatį tinklelį su užpildytais kraštiniais blokais, savu eilutės žingsniu kiekvienos plytelės viduje ir jokia RowsPerStrip semantika, kaip išsamiai parašyta TIFF 6.0 tiled vaizdų skyriuje. Todėl plytelių duomenų suteikimas juostų dekoderiui nesukelia garsaus gedimo. SimpleExtract ir CompDecode žingsniuoja per duomenis neteisingu žingsniu ir išveda vaizdą su teisingais matmenimis ir neteisingais pikseliais. Senesnis kodas tai dar labiau pagilino, laikydamas StripsAreTiles, ColumnsPerTile ir RowsPerTile TTIFFPage struktūroje: plytelių geometrija, užfiksuota dekoderio, kurio už nugaros nėra jokio plytelių surinkėjo. 3.534.1 versijoje žymų 324 ir 325 doroklės sukelia plytelių klaidą ir nedelsdami palieka IFD, todėl atmetimas neša žodį „tiled“, o ne po savaičių iškyla kaip atvaizdavimo skundas

PDFlibPas lygina juostų išdėstymą, kai pilno pločio juostos dalijasi vienu eilutės žingsniu, su plytelių išdėstymu — dvimačiu tinkleliu iš užpildytų kraštinių blokų — ir atmeta žymas 324 ir 325 žymų analizės metu, dar neliestą pikselių duomenų
Plytelių masyvai atrodo struktūriškai identiški juostų masyvams, todėl jų perdavimas juostų dekoderiui duoda teisingus matmenis ir neteisingus pikselius

Vieno matmens suvaržymas nėra atminties biudžetas

Pločio ir aukščio suvaržymas iki po 65,535 yra būtinas ir toli gražu nepakankamas, nes dydis, lemiantis atminties rezervavimą, yra sandauga. RowsPerStrip * Width * SamplesPerPixel gali perpildyti 32 bitų aritmetiką gerokai anksčiau, nei bet kuris iš narių pasiekia savo ribą, o net be perpildymo jis gali įvardyti rezervavimą, kurio jokia tarnyba neturėtų imtis. PDFlibPas eilutės baitus skaičiuoja Int64 ir kartu laiko tris lubas: 65,535 vienam matmeniui, 32 spalvų komponentus ir 256 MiB išdekoduotų baitų

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

// TPDFTIFF.ValidatePageForDecode viduje
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);

Trys ten esančios detalės svarbesnės už pačias konstantas. Aukščio patikra parašyta kaip dalyba, o ne daugyba, todėl per didelė sandauga iš viso nesusidaro. RowsPerStrip, mažesnis nei 1 arba didesnis už vaizdo aukštį, pirmiausia normalizuojamas į aukštį — tai, ką TIFF 6.0 jau numato vienos juostos skaitymu, ir tai neleidžia priešiškai žymai išpūsti juostos buferio. Ir rutina bendra: ValidatePageForDecode vykdoma žymų analizės pabaigoje ir dar kartą abiejų SimpleExtract bei CompDecode įėjimuose, todėl kodas, tiesiogiai pasiekiantis dekoderį, negali apeiti biudžeto. Tai ta pati taisyklė, kurios PDFlibPas laikosi analizuodamas nepatikimus PDF objektų grafus, nes riba, prižiūrima tik vienų iš trijų vartų, nėra riba

PDFlibPas kiekvieno TIFF buferio dydį skaičiuoja Int64 aritmetika, vaizdo aukštį tikrina dalyba, kad per didelė sandauga iš viso nesusidarytų, ir tą pačią ValidatePageForDecode rutiną vykdo žymų analizėje ir abiejuose dekoderio įėjimuose
Trys lubos, Int64 eilutės baitų aritmetika ir viena bendra patikros rutina, pasiekiama iš visų trijų vartų, nes riba, prižiūrima tik vienų iš trijų vartų, nėra riba

Ką kvietėjas privalo patikrinti prieš skaitant PageInfo?

Pirmiausia patikrinkite ValidTIFF, paskui PageCount, ir tik tada kreipkitės į PageInfo. Atmestas failas gali palikti PageCount lygų nuliui, o GetPageInfo į iš ribų išeinantį indeksą atsako neinicializuotu TTIFFPage įrašu, todėl klaidos kelias, kelyje pranešti apie gedimą skaitantis raišką ar pavyzdžių skaičius, galiausiai skaito triukšmą. 3.534.1 versija pataisė abu kvietėjus bibliotekos viduje: vaizdo importo kelias skaito XRes ir YRes tik teisingos šakos viduje, o TPDFlib.GetImagePageCount reikalauja ValidTIFF vietoj to, kad vienas pats pasitikėtų nenuliniu puslapių skaičiumi. Toliau grandynė AddImageFromFile argumentas Options yra puslapio numeris nuo vieneto daugiapuslapiame TIFF, todėl GetImagePageCount turi būti patikimas, kol ciklas dar neprasidėjo, o ne po jo. Nulis puslapių dabar yra tikras atsakymas, reiškiantis „čia nėra nieko dekoduotino“, o ne ankstyvo grąžinimo atsitiktinumas, kas svarbiausia tada, kai surikiuojate ir supinate dvipusio skenavimo partijas ir vienas tyliai neteisingai dekoduotas lapas atsidurtų netinkamoje vietoje

var
  Pdf: TPDFlib;
  Pages, I, ImageID: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
    if Pages < 1 then
      Exit;  // bloga antraštė, BigTIFF, tiled išdėstymas arba viršytas biudžetas
    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;

Kurti dekoderį patiems ar susieti libtiff?

PDFlibPas pasilieka integruotą dekoderį, ir lemiamas veiksnys yra platformų aprėptis, o ne autorystė. Maždaug 1,873 Object Pascal eilutės kompiliuojasi ten, kur pasiekiama kompiliatorius: Win32, Win64, macOS, iOS, Android ir FPC Linux aplinkoje. libtiff 4.7.1 yra apie 30,000 C eilučių, išdėstytų 34 tif_*.c vertimo vienetuose, o šiandien egzistuojantys iš anksto sukompiliuoti objektiniai failai dengia tik Windows. Jo priėmimas sukeistų pilną TIFF aprėptį į palaikomų platformų sąrašą, traukiantį iki mašinos, gebančios paleisti C įrankių grandinę, plius linkerio etapą, kuriuo dar niekas nepraėjo

Verta be puošybos pasakyti, ką tai kainuoja. Integruotas dekoderis apdoroja tai, ką iš tikrųjų pagamina skenuotų dokumentų darbas: CCITT Group 3 vienmatę ir dvimatę, Group 4, LZW, Deflate, PackBits ir JPEG-in-TIFF, per WhiteIsZero, BlackIsZero, RGB, paletę ir CMYK fotometrijas su Predictor 1 ir 2. Šie duomenų paketai sutampa su PDF filtrais ISO 32000-1 §7.4.4 ir §7.4.6, todėl TIFF priekinė dalis skenavimo grandynėje neša tokį didelį svorį. Ko jis neapdoroja — BigTIFF, plyteles, slankiojo kablelio Predictor 3, PixarLog ir SGILog, seno stiliaus JPEG suspaudimą 6 bei sub-IFD piramides. Nuo 3.534.1 kiekvienas iš jų yra įvardytas atmetimas, o ne neteisingas vaizdas, ir biblioteka laiko užrašytą paleidiklių sąrašą libtiff sprendimui atnaujinti:

  • klientas praneša apie BigTIFF failą ir jam reikia natyvaus palaikymo, o ne konvertavimo žingsnio
  • klientas praneša apie tiled TIFF iš medicininių, GIS ar pramoninių šaltinių ir reikalauja jo dekoduoti vietoje
  • klientas praneša apie slankiojo kablelio Predictor 3 TIFF
  • publikuotas pažeidžiamumas iškyla integruotuose CCITT arba LZW dekodavimo keluose
  • kryžmiaplatformiškumo argumentas nustoja galioti — arba nutraukus macOS, iOS ir Android palaikymą, arba kai pakartotinai naudojama libtiff integracija jau dengia macOS ir Linux

Pati migracija yra apibrėžta, o ne hipotetinė: USE_LIBTIFF sąlyginė kompiliavimo direktyva išlaikytų TPDFTIFF viešąjį paviršių nepaliestą, nukreiptų LoadFromStream per TIFFClientOpen su srauto atgaliniais iškvietimais, o Pascal analizatorių paliktų kaip atsarginį variantą ne Windows sistemoms. Kol vienas iš tų paleidiklių iš tikrųjų nesuveikia, dviejų dekoderių priežiūra ir padvigubėjusi testų matrica neperka nieko, ką klientas pajustų. Atidėti kaštus, kai pabėgimo kelias jau užrašytas, yra kas kita nei jų nepaisyti

Ką tai reiškia skenuotų dokumentų grandynei

Elkitės su TPDFTIFF kaip su vartais, o ne keitikliu. Įkelkite failą, perskaitykite ValidTIFF ir užrašykite LastError žodį į žodį, kai tik jis klaidingas, nes ta eilutė dabar yra trumpiausias kelias nuo lauko pranešimo iki diagnozės. Failai, nepraėję vartų, lieka atgaunami juos konvertuojant anksčiau grandynėje — tai praktiškas atsakymas BigTIFF ir tiled šaltiniams šiandien. Įvesčiai, esančiai visai už TIFF ribų, PDFlibPas eina atskiru keliu per savo AVIF, HEIF ir JPEG XL vaizdo įvesties kelią, todėl klausimas, kuris dekoderis valdo kurį formatą, lieka aiškiai iškeltas, o ne atsirandantis savaime

Visa tai slypi už įprasto vaizdo API, todėl dokumentų grandynas gauna tvirtesnę ribą nekeisdamas nė vienos kviečiančio kodo eilutės, išskyrus puslapių skaičiaus patikrą, kurią turėjo atlikti ir anksčiau. Jei svarstote natyvų TIFF į PDF kelią Delphi arba C++Builder aplinkai, visas komponentas ir jo vaizdų apdorojimas aprašyti PDF Library for Delphi puslapyje