Tehnični članak

Izvlek slik iz naloženega PDF-ja v Delphiju s HotPDF

Na disku imate PDF, stranka ga je optično prebrala iz kupa računov, vaša naloga pa je, da slike strani znova izvlečete kot bitne slike za prehod OCR. Datoteko naložite, poiščete slikovne XObjecte in nato odkrijete del, na katerega vas nihče ne opozori: bajti v teh tokovih niso pike. Lahko so tok JPEG, z valovi stisnjen JPEG 2000, faksni zapis Group 4 ali indeksirani raster za paleto za filtrom Flate. Objekt slike pozna svojo širino in višino, dejanski vzorci pa so zaprti v filtru, ki ga je izbral izvorni program. Da dobite uporaben TBitmap, morate ta filter razveljaviti, PDF pa ponuja približno osem različnih načinov, kako so lahko ti bajti zaprti

Prav to vrzel v HotPDF zapolnjuje ExtractLoadedImage, izvorna komponenta VCL PDF za Delphi in C++Builder. Prešteje slikovne XObjecte v dokumentu, ki ste ga naložili, poroča, kaj posamezni objekt je, in tiste, ki jih zna, dekodira nazaj v 24-bitno bitno sliko. Zanimiv del ni API, ki ga sestavljajo le tri metode. Zanimivo je, zakaj mora takšna ločena dekodirna pot sploh obstajati in kaj natančno lahko oziroma česa ne more pretvoriti nazaj v pike

Zakaj naložene slike še niso dekodirane

Nalaganje v HotPDF temelji na zvestobi prehoda skozi sistem. Ko pokličete LoadFromFile, se slikovni tokovi ohranijo natanko takšni, kot so v izvorni datoteki: izvirni filter, izvirni stisnjeni bajti in izvirni slovar. To je namerno. Smisel nalaganja dokumenta je običajno kopiranje strani, združevanje datotek, dodajanje žigov, sprememba dovoljenj in ponovno zapisovanje dokumenta, pri vsem tem pa je najcenejša in najvarnejša odločitev, da vsak slikovni tok pustite pri miru. Dekodiranje vsake slike v raster že ob nalaganju bi trošilo pomnilnik in procesor za delo, ki ga večina klicateljev nikoli ne potrebuje, ponovno kodiranje ob shranjevanju pa bi pokvarilo slike, ki bi morale biti prekopirane dobesedno

Posledica je, da naloženi graf objektov ne vsebuje nobenih pik. Slikovni XObject, pri katerem je /Filter enak /DCTDecode, vsebuje bajte JPEG. HotPDF nad njim nikoli ni zagnal dekoderja JPEG, ker ga pot kopiranja in ponovnega zapisovanja ni potrebovala. Ko torej dejansko želite pike, mora API za izvlek sam izvesti dekodiranje, od začetka, za tisti filter, ki ga uporablja prav ta slika. To je isti razlog, zaradi katerega so kodeki na strani kodiranja neodvisni od nalagalnika: članek o dodajanju slik JPEG 2000 v PDF-je v Delphiju pojasni, kako se pogon JPX vključi na strani ustvarjanja, ta isti pogon pa preprosto ni bil priključen na pot branja, dokler ga API za izvlek ni začel potrebovati

API s tremi metodami

Površina je majhna. GetLoadedImageCount vrne število slikovnih XObjectov v naloženem dokumentu. GetLoadedImageInfo po indeksu napolni opisni zapis za enega izmed njih. ExtractLoadedImage vrne dekodirano bitno sliko ali nil, kadar te slike ne more dekodirati. Enumeracija temelji na indeksih in je za dano nalaganje stabilna: interno sprehodi tabelo posrednih objektov in zbere vsak tok, katerega /Subtype se razreši v /Image, zato je indeks, ki ga podate metodi GetLoadedImageInfo, isti indeks, ki ga podate metodi ExtractLoadedImage

var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  Bmp: TBitmap;
  I, Count: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('scanned-invoices.pdf', '') <= 0 then
      Exit;
    Count := Pdf.GetLoadedImageCount;
    for I := 0 to Count - 1 do
    begin
      if not Pdf.GetLoadedImageInfo(I, Info) then
        Continue;
      if not Info.Decodable then
        Continue;                       // filter or colour space not supported
      Bmp := Pdf.ExtractLoadedImage(I);
      if Bmp <> nil then
      try
        Bmp.SaveToFile(Format('img_%d.bmp', [I]));
      finally
        Bmp.Free;                       // caller owns the bitmap
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Tu sta pomembni dve pogodbeni podrobnosti. Prvič, vrnjeni TBitmap morate sprostiti sami, dokument ga ne predpomni in ni njegov lastnik. Drugič, pred klicem preverite Decodable, po klicu pa rezultat primerjajte z nil. Metoda pri nepodprtem filtru ne vrže izjeme, ampak vrne nil, tih nil v paketni zanki pa je točno tista vrsta napake, ki vam na tisočstranskem opravilu poje eno stran, ne da bi kdo to sploh opazil

Preberite opis, še preden dekodirate

THPDFLoadedImageInfo vam pove, kaj slika je, ne da bi se že zavezali k polnemu dekodiranju. Njegova polja prihajajo neposredno iz slikovnega slovarja: Width in Height v vzorcih, BitsPerComponent, ColorComponents in ColorSpace, ki opisujejo pomen po dekodiranju, torej 1 za sivo, 3 za RGB in 4 za CMYK, ter Filter kot ime stiskanja, IsImageMask za šablonske maske, ObjectNumber za osnovni posredni objekt in Decodable

Prav ta zadnja zastavica je poštena. Decodable je True samo takrat, ko tekoča gradnja točno to kombinacijo filtra in barvnega prostora zares zna pretvoriti v bitno sliko. Kodira dejansko matriko podpore, ne želje. Slika, katerega Filter trenutna gradnja ne razume, poroča Decodable = False, na tej podlagi pa se lahko odločite za beleženje, preskok ali izvlek surovega toka po lastni poti. Obravnavajte to kot predpogoj, ne kot namig

// Triage every image before committing to a decode.
var
  Pdf: THotPDF;
  Info: THPDFLoadedImageInfo;
  I: Integer;
begin
  // ... Pdf loaded ...
  for I := 0 to Pdf.GetLoadedImageCount - 1 do
  begin
    if not Pdf.GetLoadedImageInfo(I, Info) then
      Continue;
    if Info.Decodable then
      // ExtractLoadedImage(I) will return a TBitmap
    else
      // unsupported filter/colour space: log the object and skip
      Writeln(Format('Image %d obj %d: %dx%d %s/%s not decodable',
        [I, Info.ObjectNumber, Info.Width, Info.Height,
         String(Info.Filter), String(Info.ColorSpace)]));
  end;
end;

Ena izvedbena podrobnost ujame ljudi, ki opisne zapise gradijo ročno. THPDFLoadedImageInfo vsebuje dve polji tipa AnsiString, in sicer Filter ter ColorSpace. Gre za upravljani vrsti s štetjem referenc, zato je refleksno ničlenje zapisa z FillChar(Info, SizeOf(Info), 0) tukaj napačno: prepišete referenco na niz, ne da bi jo zmanjšali, kar vodi v uhajanje ali pokvarjeno stanje. HotPDF zato zapis inicializira polje po polje, in če boste kdaj ta vzorec kopirali v svojo kodo, naredite enako

En usmerjevalnik, osem poti filtrov

Razlog, da je ta zmožnost prihajala skozi več izdaj namesto v eni, je preprost: PDF nima enega slikovnega formata. Ima filtre, ISO 32000-1 §8.9.5 pa slikovnemu XObjectu dovoljuje, da v /Filter navede katerega koli izmed njih, medtem ko razlago vzorcev ločeno določajo /ColorSpace, /BitsPerComponent in morebitno polje /Decode. ExtractLoadedImage prebere ime filtra in ga za vsak primer usmeri v namenski dekoder. Podprt nabor, ki se je gradil od v2.229 do v2.231, zdaj pokriva osem ločenih poti

  • Surovi rastri (FlateDecode, LZWDecode ali brez filtra) v 8-bitnem DeviceRGB ali DeviceGray. Bajti se razširijo v zgoščen raster, edina pretvorba pa je zamenjava kanalov, opisana spodaj
  • DCTDecode (JPEG). Tok se preda VCL-jevemu TJPEGImage, ki razreši geometrijo in barvo, rezultat pa se zapiše v 24-bitno bitno sliko
  • JPXDecode (JPEG 2000). Dekodiranje opravi ozadje OpenJPEG, isti pogon, opisan v članku o JPEG 2000, pri čemer se komponente z večjo bitno globino vzorčijo navzdol na 8 bitov
  • Indeksirana barva. Paleta se prebere iz polja [/Indexed base hival lookup], vsak vzorec pa se skozi tabelo pogledov razširi v pravo barvo
  • DeviceCMYK. Štirikanalni vzorci se pretvorijo v RGB s standardno formulo za črnilo na beli podlagi
  • DeviceGray in Indexed pod 8 bitov pri 1, 2 ali 4 bitih na komponento se razpakirajo vzorec po vzorec in preslikajo v območje 0 do 255
  • CCITTFaxDecode, torej filtri faksov Group 3 in Group 4, se dekodirajo s posebnim ozadjem T.4/T.6
  • JBIG2Decode, visokokompresijski dvobarvni filter, pa se dekodira prek registriranega ozadja JBIG2, ki ga članek o izvornem stiskanju JBIG2 opisuje z vidika kodiranja

Vse se na koncu steče na isto mesto: v 24-bitno bitno sliko BGR, ker to VCL-jev TBitmap hrani nativno in to pričakuje vsak nadaljnji porabnik

Pretvorbe, ki potiho spreminjajo pike

Dve izmed teh poti vsebujeta pretvorbo, ki jo je presenetljivo lahko malo zgrešiti, pa jo je vredno razumeti, tudi če dekoderja sami nikoli ne boste prijeli. Prva je zamenjava vrstnega reda barv. Raster PDF DeviceRGB hrani vzorce v zaporedju rdeča zelena modra, od zgornje vrstice navzdol. VCL-jeva 24-bitna scanline jih hrani v zaporedju modra zelena rdeča. Zato dekodiranje navadne slike RGB ni memcpy. Pri vsaki piki se ob zapisovanju v scanline zamenjata prvi in tretji bajt. Če to obrnete narobe, se rdeča in modra zamenjata, kar je na sivinskem testu videti sprejemljivo, na barvnem pa katastrofalno. Kar zadeva vrstni red vrstic, gre preslikava naravnost skozi: zgoraj navzdol zapisani rastri PDF se poravnajo z VCL-jevim ScanLine[0] kot zgornjo vidno vrstico, zato navpični obrat ni potreben

Druga je CMYK. Slike PDF DeviceCMYK nosijo štiri črnila, pretvorba v RGB pa je izračun po kanalih, ne tabela pogledov: vsak izhodni kanal je (255 - ink) * (255 - K) / 255. To je približek naprave, ne barvno upravljana pretvorba prek profila ICC, zato je rezultat dovolj dober za prikaz in ponovno rasterizacijo, ni pa prava pot, če potrebujete barvno natančnost za tisk. Če vaš potek zahteva visoko zvestobo, izvlečeno bitno sliko obravnavajte kot predogled, izvirni tok CMYK pa obdržite za barvno upravljani cevovod

Pot Indexed skriva še lastno past pri razčlenjevanju. Paleta v barvnem prostoru /Indexed je lahko shranjena kot dobesedni niz ali kot šestnajstiški niz, HotPDF pa vrednost šestnajstiškega niza hrani kot šestnajstiško besedilo, ne kot že dekodirane bajte. Kadar je paleta šestnajstiški niz, je zato treba tabelo pogledov najprej pognati skozi pretvorbo hex v bytes, medtem ko je dobesedni niz že surov niz bajtov. Če to vejo zgrešite, štiribarvna indeksirana slika postane nesmisel, ker se vsak vnos palete bere z napačne meje bajtov

Verige filtrov: zadnji filter je pravi filter slike

En sam /Filter je preprost primer. PDF dovoljuje tudi verigo filtrov, pri kateri je tok šel zaporedno skozi več filtrov, navedenih po vrsti v polju /Filter, kot sta [/ASCII85Decode /FlateDecode] ali [/ASCIIHexDecode /DCTDecode] (ISO 32000-1 §7.4). Semantika je natančna: filtri se pri kodiranju uporabijo od leve proti desni, zato jih pri dekodiranju razveljavite od desne proti levi, zadnji filter v polju pa je tisti, ki dejansko določa slikovni format. Predhodni filtri so le transportna kodiranja, ovita okoli njega

Izvlečni mehanizem to obravnava z lupljenjem plasti. Preden se zažene kateri koli slikovni dekoder, se uporabijo vsi filtri v verigi razen zadnjega, da nastane vhod, ki ga pričakuje zadnji filter, šele nato se izvede usmerjanje na ta zadnji filter. Tako [/ASCII85Decode /DCTDecode] najprej odstrani ASCII85 iz toka, rezultat pa nato usmeri na pot JPEG. [/FlateDecode] okoli surovega rastra najprej napihne podatke, nato pa zažene rastrsko pot. Prav to ohranja vseh osem dekoderjev preprostih. Nobenemu ni treba vedeti ničesar o transportnih ovojnicah ASCII85 ali hex, saj do trenutka, ko dekoder vidi bajte, teh ovojnic ni več. To pomeni tudi, da veriga, katere zadnji filter ni podprt, čisto odpove v koraku usmerjanja, ne pa nekje na polovici postopka

Kje se izvlek konča in kaj storiti potem

Bodite pošteni do omejitev. Slika, katere zadnji filter je zunaj podprtega nabora, vrne nil, enako pa velja za sliko, katere barvnega prostora gradnja ne zna razložiti. Mehke maske in alfa se ne rekonstruirajo v bitno sliko, zato dobite osnovno sliko, ne pa že sestavljenega rezultata. Bitne globine nad 8 iz JPEG 2000 se namenoma vzorčijo navzdol, kar je izgubljivo in napačno, če ponovno arhivirate namesto prikazujete. Tudi slikovna maska, enobitna šablona brez lastne barve, je v opisu pravilno zajeta, vendar ni isto kot slikovni motiv. Če jo dekodirate in pričakujete fotografijo, boste presenečeni

Kadar izvlek ni dovolj, je surov tok še vedno tam v naloženem grafu objektov, z vsemi filtri vred, vi pa ga lahko vzamete bajt za bajtom in ga predate lastnemu specializiranemu kodeku. Prav to je rezervni izhod, ki ga zasnova s prehodno zvestobo namenoma ohranja: izvirni bajti se nikoli ne zavržejo, zato je najslabši scenarij ta, da jih dekodirate sami, ne pa da bi podatki izginili. Pri večini resničnih opravil pa podprtih osem filtrov pokrije tisto, kar dejansko izločajo skenerji, pisarniški paketi in pogoni za poročila, zanka prek GetLoadedImageCount z zaščito Decodable pa naloženi PDF v nekaj vrsticah spremeni nazaj v mapo bitnih slik

API za izvlek slik iz naloženega dokumenta skupaj s celotnim naborom tukaj opisanih dekodirnih filtrov je del paketa HotPDF Component za Delphi in C++Builder