Tekninen artikkeli

Delphi-TIFF-dekooderin kovennus: BigTIFF ja tiled TIFF

PDFlibPas dekoodaa TIFFin itse kirjoitetulla Object Pascal -jäsentimellä libtiff-sidoksen sijaan, ja versio 3.534.1 kiristi täsmälleen ne kohdat, joissa jäsentin hylkää syötteen. BigTIFF-magia 43 hylätään nyt nimeltä, TileOffsets ja TileByteCounts torjutaan jo tagianalyysin aikana, ja jokainen puskuri mitoitetaan Int64-laskennalla 256 MiB:n dekoodauskaton alla

Korjattava vika ei näy koskaan laboratoriossa. Se ilmenee skannausyhdyskäytävänä, joka on toiminut hiljaa kolme vuotta, kunnes asiakas ohjaa sen läpi paikkatietoarkiston tai kokonaisen kudosnäytteen kuvan. Tiedostossa on kelvollinen TIFF-otsikko. Se jäsenee. Lopputuloksena on sivu raidallista kohinaa tai monigigatavuinen muistivaraus, joka kaataa palvelun, eikä matkan varrella mikään koskaan julistanut syötettä virheelliseksi. Tämä on se vian muoto, jota vastaan kannattaa suunnitella: ei kaatumista, vaan itsevarmasti toimitettu väärä vastaus

Miksi II tai MM ei todista, että sinulla on klassinen TIFF?

Koska tavujärjestysmerkintä on yhteinen kummallekin variantille. Sekä klassinen TIFF että BigTIFF alkavat II- tai MM-merkinnällä, ja kenttä, joka todella erottaa ne, on heti sen jälkeen seuraava 16-bittinen magia: 42 klassiselle TIFFille TIFF 6.0 -määrittelyn mukaisesti, 43 BigTIFFille sen 64-bittisillä siirtymillä. FValidTIFF := PopWord = 42 -muotoon kirjoitettu lataaja ei ole väärässä klassisen TIFFin suhteen, mutta se kutistaa kaksi hyvin erilaista hylkäystä yhdeksi mykäksi totuusarvoksi, jolloin BigTIFF ei eroa mitenkään joku uudelleennimetystä katkaistusta JPEGistä. PDFlibPas erottaa nyt tapaukset toisistaan ja kirjaa jokaisen TPDFTIFF.LastError-kenttään: alle neljä tavua pitkä otsikko, virheellinen tavujärjestysmerkintä, magia 43 ja mikä tahansa muu magia-arvo tuottavat kukin oman tekstin. Kirjasto ei edelleenkään dekoodaa BigTIFFiä, ja sen sanominen suoraan on juuri olennaista. Kutsuja saa selville eron "tämä ei ole TIFF" ja "tämä on TIFF, jonka 64-bittistä siirtymärakennetta sisäänrakennettu dekooderi ei toteuta" välillä, mikä on ero tukipyynnön, johon vastaa yhdellä viestillä, ja sellaisen, joka venyy viikon arvailuksi, välillä

PDFlibPasin TIFF-lataaja lukee tavujärjestysmerkinnän ja 16-bittisen magian erikseen, joten lyhyt otsikko, virheellinen merkintä, BigTIFF-magia 43 ja mikä tahansa muu magia-arvo tuottavat kukin oman LastError-tekstinsä yhden mykän totuusarvon sijaan
Klassinen TIFF ja BigTIFF alkavat samalla tavujärjestysmerkinnällä, joten PDFlibPas erottaa neljä hylkäystapausta ja nimeää jokaisen LastError-kentässä
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;

Tiilet ovat eri geometria, eivät toinen offsets-taulukko

PDFlibPas hylkää tiiletetyn TIFFin jo tagianalyysin aikana, ennen kuin mihinkään pikselidataan kosketaan. Vikaa kutsuva oikotie on helppo nähdä: tagi 324 (TileOffsets) ja tagi 325 (TileByteCounts) ovat tiedostosiirtymien ja tavumäärien taulukoita, rakenteeltaan identtiset strip-taulukoiden kanssa, joten olemassa olevien strip-kenttien osoittaminen niihin maksaa kaksi riviä ja kääntyy puhtaasti. Se on silti väärin. Tiilet muodostavat kaksiulotteisen ruudukon täytetyillä reunablokeilla, omalla rivisiirtymällään tiilen sisällä ja ei lainkaan RowsPerStrip-semantiikkaa, niin kuin TIFF 6.0:n tiled-image-luku sanoo suoraan. Tiilidataa syöttäminen strip-dekooderille ei siis epäonnista äänekkäästi. SimpleExtract ja CompDecode kulkevat datan väärällä rivisiirtymällä ja tuottavat kuvan, jonka mitat ovat oikeat mutta pikselit väärät. Vanhempi koodi pahensi tätä säilyttämällä StripsAreTiles-, ColumnsPerTile- ja RowsPerTile-kentät TTIFFPage-tietueessa: tiiligeometria tallennettuna dekooderille, jolla ei ole taustalla mitään tiilikokoonpanijaa. Versiossa 3.534.1 tagien 324 ja 325 käsittelijät nostavat tiilivirheen ja hylkäävät IFD:n välittömästi, joten hylkäys kantaa sanan "tiled" eikä puhkea viikkoja myöhemmin renderöintivalituksena

PDFlibPas vertaa strip-asettelua, jossa koko leveyden kaistat jakavat yhden rivisiirtymän, ja tiiliasettelua, joka on kaksiulotteinen ruudukko täytetyillä reunablokeilla, ja hylkää tagit 324 ja 325 tagianalyysin aikana ennen kuin mihinkään pikselidataan kosketaan
Tiilitaulukot näyttävät rakenteeltaan identtisiltä strip-taulukoilta, minkä vuoksi niiden syöttäminen strip-dekooderille tuottaa oikeat mitat ja väärät pikselit

Yksi mitan rajaus ei ole muistibudjetti

Leveyden ja korkeuden rajaaminen kumpikin 65 535:een on tarpeellista mutta ei lainkaan riittävää, koska varauksia ohjaava suure on tulo. RowsPerStrip * Width * SamplesPerPixel voi ylivuottaa 32-bittisessä laskennassa hyvissä ajoin ennen kuin kumpikaan tekijä saavuttaa oman rajansa, ja ilman ylivuotoakin se voi nimetä muistivarauksen, jota mikään palvelu ei yrittäisi. PDFlibPas laskee rivitavut Int64:llä ja valvoo kolmea kattoa yhdessä: 65 535 per mitta, 32 värikomponenttia ja 256 MiB dekoodattuja tavuja

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

// TPDFTIFF.ValidatePageForDecode-rutiinin sisällä
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);

Kolme yksityiskohtaa siellä merkitsevät enemmän kuin vakiot itsessään. Korkeustesti on kirjoitettu jakolaskuna kertolaskun sijaan, joten liian suurta tuloa ei koskaan muodosteta lainkaan. Alle 1:n tai kuvan korkeutta suurempi RowsPerStrip normalisoidaan ensin korkeuteen, mikä on se yksittäisen stripin tulkinta, jota TIFF 6.0 jo edellyttää, ja se estää vihamielistä tagia paisuttamasta strip-puskuria. Ja rutiini on jaettu: ValidatePageForDecode suoritetaan tagianalyysin lopussa ja uudelleen sekä SimpleExtract- että CompDecode-metodien alussa, joten dekooderille suoraan päätyvä koodi ei voi kiertää budjettia. Tämä on sama sääntö, jota PDFlibPas noudattaa jäsentäessään epäluotettavia PDF-objektigraafeja, koska yhdestä kolmesta ovesta valvottu raja ei ole raja

PDFlibPas mitoittaa jokaisen TIFF-puskurin Int64-laskennalla, testaa kuvan korkeuden jakolaskulla, joten liian suurta tuloa ei koskaan muodosteta, ja suorittaa saman ValidatePageForDecode-rutiinin tagianalyysissä ja kummassakin dekooderin sisääntulossa
Kolme kattoa, Int64-rivitavulaskenta ja yksi jaettu validointirutiini, johon päädytään kaikista kolmesta ovesta, koska yhdestä kolmesta ovesta valvottu raja ei ole raja

Mitä kutsujan on tarkistettava ennen PageInfo:n lukemista?

Tarkista ensin ValidTIFF, sitten PageCount ja vasta sitten indeksoi PageInfo. Hylätty tiedosto voi jättää PageCountin nollaan, ja GetPageInfo vastaa lukualueen ulkopuoliseen indeksiin alustamattomalla TTIFFPage-tietueella, joten virhepolku, joka lukee resoluution tai näytemäärät matkallaan ilmoittaessaan viasta, lukeekin lopulta kohinaa. Versio 3.534.1 korjasi molemmat kutsujat kirjaston sisällä: kuvantuontipolku lukee XResin ja YResin vain kelvollisen haaran sisällä, ja TPDFlib.GetImagePageCount edellyttää ValidTIFFiä sen sijaan, että luottaisi yksin nollasta poikkeavaan sivumäärään. Alavirrassa AddImageFromFile-metodin Options-argumentti on yhdestä alkava sivunumero monisivuiselle TIFFille, joten GetImagePageCountin on oltava luotettava ennen silmukan alkua, eikä sen jälkeen. Nolla sivua on nyt todellinen vastaus, joka tarkoittaa "täällä ei ole dekoodattavaa", eikä varhaisen paluun sattumaa, mikä merkitsee eniten silloin, kun lajittelet ja lomitat duplex-skannaus-eriä ja yksi hiljaisesti väärin dekoodattu arkki päätyisi väärään kohtaan

var
  Pdf: TPDFlib;
  Pages, I, ImageID: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pages := Pdf.GetImagePageCount('inbox\scan-0417.tif');
    if Pages < 1 then
      Exit;  // virheellinen otsikko, BigTIFF, tiiliasettelu tai budjetin ylitys
    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;

Rakenna dekooderi vai linkitä libtiff?

PDFlibPas pitäytyy sisäänrakennetussa dekooderissa, ja ratkaiseva tekijä on alustakattavuus, ei tekijyys. Suunnilleen 1 873 Object Pascal -riviä kääntyy siellä missä kääntäjäkin kulkee: Win32, Win64, macOS, iOS, Android ja FPC Linuxilla. libtiff 4.7.1 on noin 30 000 C-riviä jaettuna 34 tif_*.c-käännösyksikköön, ja nykyään saatavilla olevat esikäännetyt objektitiedostot kattavat vain Windowsin. Sen omaksuminen vaihtaisi täydellisen TIFF-kattavuuden tuettujen alustojen listaan, joka kutistuu siihen koneeseen, jossa C-työkaluketju pyörii, sekä linkkerivaiheeseen, jonka kukaan ei ole vielä käynyt läpi

Sen hinta kannattaa sanoa ilman maalailemista. Sisäänrakennettu dekooderi käsittelee sen, mitä skannattujen dokumenttien työ todella tuottaa: CCITT Group 3 yksi- ja kaksiulotteisena, Group 4:n, LZW:n, Deflaten, PackBitsin ja JPEG-in-TIFF:n WhiteIsZero-, BlackIsZero-, RGB-, paletti- ja CMYK-fotometrioissa Predictor 1:n ja 2:n kanssa. Nämä payloadit osuvat yhteen ISO 32000-1 §7.4.4:n ja §7.4.6:n PDF-suodattimien kanssa, minkä vuoksi TIFF-etuosa kantaa niin suurta painoa skannausputkessa. Se, mitä se ei käsittele, on BigTIFF, tiilet, liukulukuinen Predictor 3, PixarLog ja SGILog, vanhan tyylin JPEG-pakkaus 6 sekä sub-IFD-pyramidit. Versiosta 3.534.1 lähtien jokainen niistä on nimetty hylkäys eikä väärä kuva, ja kirjasto pitää kirjallista laukaisinlistaa libtiff-päätöksen avaamiseksi uudelleen:

  • asiakas raportoi BigTIFF-tiedoston ja tarvitsee natiivin tuen muunnosvaiheen sijaan
  • asiakas raportoi tiiletetyn TIFFin lääketieteellisestä, GIS- tai teollisesta lähteestä ja tarvitsee sen dekoodattavan paikallisesti
  • asiakas raportoi liukulukuisen Predictor 3 -TIFFin
  • julkaistu haavoittuvuus osuu sisäänrakennettuihin CCITT- tai LZW-dekoodauspolkuihin
  • alustojen välinen perustelu lakkaa pätemästä, joko siksi että macOS-, iOS- ja Android-tuki lopetetaan, tai siksi että uudelleenkäytettävä libtiff-integraatio kattaa jo macOS:n ja Linuxin

Itse siirtymä on rajattu eikä hypoteettinen: USE_LIBTIFF-ehdollinen käännös pitäisi TPDFTIFFin julkisen pinnan ennallaan, ohjaisi LoadFromStreamin TIFFClientOpenin kautta virttakutsuilla ja jättäisi Pascal-jäsentimen varalle alustoille muille kuin Windowsille. Kunnes jokin näistä laukaisimista oikeasti laukeaa, kahden dekooderin ylläpito ja kaksinkertainen testimatriisi ei osta mitään, minkä asiakas voisi aistia. Kulun lykkääminen silloin, kun pakoreitti on jo kirjattuna, on eri asia kuin sen sivuuttaminen

Mihin tämä jättää skannattujen dokumenttien putken

Käsittele TPDFTIFFiä porttina eikä muuntimena. Lataa tiedosto, lue ValidTIFF ja kirjaa LastError sanatarkasti aina kun se on epätosi, koska se merkkijono on nykyään lyhin reitti kenttäraportista diagnoosiin. Portin kariutuneet tiedostot pysyvät korjattavissa muuntamalla ne ylävirrassa, mikä on käytännön vastaus BigTIFF- ja tiililähteille tänä päivänä. Kokonaan TIFFin ulkopuoliseen syötteeseen PDFlibPas vie erillisen reitin AVIF-, HEIF- ja JPEG XL -kuvasyötteen kautta, joten kysymys siitä, mikä dekooderi omistaa minkäkin muodon, pysyy eksplisiittisenä sen sijaan, että se syntyisi vasta matkalla

Kaikki tämä sijaitsee tavallisen kuva-API:n takana, joten dokumenttiputki saa tiukemman rajan muuttamatta riviakaan kutsuvaa koodia sen jälkeen, kun se tarkistaa sivumäärän, jota sen olisi jo pitänyt tarkistaa. Jos punnitat natiivia TIFF-to-PDF-reittiä Delphille tai C++Builderille, koko komponentti ja sen kuvankäsittely on dokumentoitu PDF Library for Delphi -sivulla