Tehnički članak

Izvlačenje slika iz učitanog PDF-a u Delphiju: HotPDF

Imate PDF na disku, korisnik je skenirao hrpu računa, a vaš je posao izvući slike stranica natrag kao bitmapu za OCR obradu. Otvorite datoteku, pronađete image XObjecte, i onda otkrijete dio na koji vas nitko ne upozori: bajtovi u tim tokovima nisu pikseli. To su JPEG codestreamovi, ili JPEG 2000 blobovi komprimirani valnom transformacijom, ili Group 4 fax nizovi, ili indeksirani raster iza palete iza Flate filtra. Objekt slike zna svoju širinu i visinu, ali stvarni uzorci zatvoreni su unutar filtra koji je proizvođač odabrao. Dobivanje upotrebljivog TBitmap znači poništiti taj filtar, a PDF vam daje otprilike osam različitih načina da se ti bajtovi zapečate

To je praznina koju ExtractLoadedImage popunjava u HotPDF-u, izvornoj VCL PDF komponenti za Delphi i C++Builder. Ona nabraja image XObjecte u dokumentu koji ste učitali, govori što je svaki od njih i dekodira one koje može natrag u 24-bitnu bitmapu. Zanimljiv dio nije sama površina API-ja, koja se svodi na tri metode. Zanimljivo je zašto uopće mora postojati zaseban put dekodiranja i što može, a što ne može vratiti natrag u piksele

Zašto učitane slike nisu već dekodirane

Učitavač HotPDF-a građen je oko vjernosti prolaza kroz podatke. Kada pozovete LoadFromFile, tokovi slika ostaju točno onakvi kakvi su u izvornoj datoteci: izvorni filtar, izvorni komprimirani bajtovi, izvorni rječnik. To je namjerno. Svrha učitavanja dokumenta obično je kopirati stranice, spajati datoteke, stavljati žigove, mijenjati dozvole i spremati ih natrag, a za sve to je najjeftinije i najsigurnije ostaviti svaki image tok netaknut. Dekodiranje svake slike u raster pri učitavanju trošilo bi memoriju i CPU na posao koji većini poziva nikad ne treba, a ponovno kodiranje pri spremanju degradiralo bi slike koje su trebale biti kopirane doslovno

Posljedica je da učitani graf objekata ne nosi piksele. Image XObject čiji je /Filter je /DCTDecode drži JPEG bajtove; HotPDF ga nikada nije proveo kroz JPEG dekoder, jer ništa u putu kopiranja i ponovnog pisanja to nije ni tražilo. Dakle, kada stvarno želite piksele, API za izdvajanje mora obaviti dekodiranje sam, od nule, za koji god filtar ta konkretna slika koristi. To je isti razlog zbog kojeg su kodeci na strani kodiranja neovisni o učitavaču: članak o dodavanju JPEG 2000 slika u PDF-ove u Delphiju opisuje kako se JPX engine uključuje u dio za stvaranje, a taj motor jednostavno nije bio spojen na put čitanja dok ga API za izdvajanje nije zatrebao

Tri-metodni API

Površina je mala. GetLoadedImageCount vraća koliko image XObjecta sadrži učitani dokument. GetLoadedImageInfo ispunjava opisni zapis za jedan od njih po indeksu. ExtractLoadedImage vraća dekodiranu bitmapu, ili nil kada tu sliku ne može dekodirati. Nabrajanje je indeksno i stabilno za jedno učitavanje: interno prolazi kroz tablicu neizravnih objekata i skuplja svaki tok čiji se /Subtype razrješava u /Image, pa je indeks koji proslijedite u GetLoadedImageInfo isti indeks koji proslijedite u 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;

Dva detalja ugovora ovdje su važna. Prvo, vraćeni TBitmap je vaš za oslobađanje; dokument ga ne predmemorira niti posjeduje. Drugo, provjerite Decodable prije poziva i usporedite rezultat s nil nakon njega. Metoda ne izbacuje iznimku na nepodržanom filtru, nego vraća nil, a tiho nil u petlji za obradu serije upravo je ono što proguta stranicu iz posla od tisuću stranica, a da to nitko ne primijeti

Čitanje opisa prije dekodiranja

THPDFLoadedImageInfo govori što je slika bez obveze potpunog dekodiranja. Njezina polja dolaze izravno iz rječnika slike: Width i Height u uzorcima, BitsPerComponent, ColorComponents i ColorSpace koji opisuju tumačenje nakon dekodiranja (1 za sivo, 3 za RGB, 4 za CMYK), Filter kao naziv filtra, IsImageMask za stencil maske, ObjectNumber za temeljni neizravni objekt, i Decodable

Taj posljednji flag je jedini iskren. Decodable je True samo kada trenutačna gradnja stvarno može tu specifičnu kombinaciju filtra i prostora boja pretvoriti u bitmapu. To kodira stvarnu matricu podrške, a ne želju: slika čiji Filter trenutačna gradnja ne razumije prijavljuje Decodable = False, i na temelju toga možete granati prema zapisu, preskakanju ili vraćanju na izdvajanje sirovog toka sami. Smatrajte to preduvjetom, a ne naznakom

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

Jedan detalj implementacije ljude koji ručno sastavljaju opisne zapise zna ugristi. THPDFLoadedImageInfoTHPDFLoadedImageInfoAnsiString sadrži dva Filter polja, ColorSpace i FillChar(Info, SizeOf(Info), 0). To su upravljani tipovi s brojanjem referenci, pa je refleks da zapis poništite s

Jedan raspoređivač, osam putova filtara

Razlog zbog kojeg je ovoj mogućnosti trebalo nekoliko izdanja, a ne jedno, jest to što PDF nema format slike. Ima filtre, a §8.9.5 ISO 32000-1 dopušta da image XObject bilo koji od njih imenuje u /Filter, pri čemu je tumačenje uzoraka zasebno upravljano kroz /ColorSpace, /BitsPerComponent i opcionalni /Decode niz. ExtractLoadedImage čita naziv filtra i usmjerava ga na namjenski dekoder za svaki slučaj. Podržani skup, građen kroz v2.229 do v2.231, sada pokriva osam različitih putova

  • Sirovi rasteri (FlateDecode, LZWDecode ili bez filtra) u 8-bitnom DeviceRGB ili DeviceGray. Bajtovi se infliraju u upakirani raster, a jedina transformacija je zamjena kanala, opisana niže
  • DCTDecode (JPEG). Codestream se predaje VCL-ovu TJPEGImage, koji razrješava geometriju i boju, a rezultat se dodjeljuje u 24-bitnu bitmapu
  • JPXDecode (JPEG 2000). Dekodiran kroz OpenJPEG backend, isti motor opisan u članku o JPEG 2000, s komponentama visoke dubine bita koje se resampliraju natrag na 8 bita
  • Indeksirana boja. Paleta se čita iz [/Indexed base hival lookup] niza i svaki se uzorak proširi kroz lookup tablicu u pravu boju
  • DeviceCMYK. Četverokanalni uzorci pretvaraju se u RGB standardnom formulom tinta-na-bijelom
  • Sub-8-bit DeviceGray i Indexed pri 1, 2 ili 4 bita po komponenti, raspakirani uzorak po uzorak i skalirani u raspon 0–255
  • CCITTFaxDecode, Group 3 i Group 4 fax filtri, dekodirani putem namjenskog T.4/T.6 backenda
  • JBIG2Decode, high-ratio bilevel filtar, dekodiran kroz registrirani JBIG2 backend o kojem članak o izvornoj JBIG2 kompresiji pokriva sa strane kodiranja

Sve završava na istom mjestu: 24-bitnoj BGR bitmapi, jer je to ono što VCL TBitmap pohranjuje izvorno i ono što svaki nizvodni potrošač očekuje

Transformacije koje tiho mijenjaju piksele

Dva od tih putova uključuju transformaciju koju je lako suptilno pogriješiti, i vrijedno ju je razumjeti čak i ako nikad sami ne dirate dekoder. Prva je zamjena redoslijeda boja. PDF DeviceRGB raster pohranjuje uzorke u red-zeleno-plavo redoslijedu, od vrha prema dolje. VCL-ov 24-bitni scanline pohranjuje ih plavo-zeleno-crveno. Dakle, dekodiranje obične RGB slike nije memcpy; prvi i treći bajt svakog piksela zamijene se na putu u scanline. Obrnite to i vaše crvene i plave zamijene mjesta, što izgleda dobro na testnoj slici u sivim tonovima i katastrofalno krivo na onoj u boji. Redoslijed redaka, koliko vrijedi, prolazi ravno kroz: PDF-ovi rasteri od vrha prema dnu poravnaju se s VCL-ovim ScanLine[0] kao gornjim vizualnim redom, pa nije potrebno okomito okretanje

Druga je CMYK. PDF DeviceCMYK slike nose četiri tinte, a pretvorba u RGB nije lookup nego izračun po kanalu: svaki izlazni kanal je (255 - ink) * (255 - K) / 255. To je aproksimacija uređaja, a ne pretvorba upravljana bojama kroz ICC profil, pa je rezultat dovoljno dobar za prikaz i ponovno rasteriziranje, ali nije pravi put ako trebate boju točnu za tisak. Ako vaš radni tok traži vjernost, tretirajte izdvojenu bitmapu kao pregled i zadržite izvorni CMYK tok za cjevovod s upravljanjem bojama

Indexed put skriva i vlastitu zamku pri parsiranju. Paleta u /Indexed color spaceu može se pohraniti kao doslovni string ili kao heksadekadski string, a HotPDF vrijednost heksadekadskog stringa sprema kao heksadekadski tekst, a ne kao dekodirane bajtove. Dakle, kad je paleta heksadekadski string, lookup tablicu prvo treba provesti kroz dekodiranje iz heksa u bajtove; doslovni string već je sirovi niz bajtova. Propustite tu granu i indeksirana slika u četiri boje izlazi kao smeće, jer se svaki zapis palete čita s pogrešne granice bajta

Lanci filtara: posljednji filtar je sam image

Jedan /Filter naziv je lakši slučaj. PDF dopušta i lanac filtara, u kojem je tok prošao kroz nekoliko njih redom, navedenih po redu u /Filter nizu kao što je [/ASCII85Decode /FlateDecode] ili [/ASCIIHexDecode /DCTDecode] (ISO 32000-1 §7.4). Semantika je precizna: filtri se pri kodiranju primjenjuju slijeva nadesno, pa ih pri dekodiranju poništavate zdesna nalijevo, a zadnji filtar u nizu onaj je koji doista definira format slike. Vodeći filtri samo su transportna kodiranja omotana oko njega

Izdvajanje to rješava ogoljivanjem slojeva. Prije nego što se pokrene bilo koji dekoder slike, svaki filtar u lancu osim posljednjeg primijeni se da proizvede ulaz koji posljednji filtar očekuje, i tek se onda radi raspodjela prema tom posljednjem filtru. Tako [/ASCII85Decode /DCTDecode] najprije uklanja ASCII85 s toka, a zatim rezultat usmjerava na JPEG put; [/FlateDecode] omotan oko sirovog rastera prvo se inflira pa onda prolazi raster putom. To je ono što omogućuje da osam dekodera ostane jednostavno. Nijedan od njih ne mora znati za ASCII85 ili heks transportne omotače, jer do trenutka kad dekoder vidi bajtove, omotači su već nestali. To također znači da lanac čiji je posljednji filtar nepodržan i dalje čisto pada na koraku raspodjele, a ne na pola puta

Gdje izdvajanje staje i što tada učiniti

Budite iskreni prema granicama. Slika čiji je završni filtar izvan podržanog skupa vraća nil, a isto vrijedi i za onu čiji prostor boja build ne može protumačiti. Soft maske i alfa ne rekonstruiraju se u bitmapu; dobijete baznu sliku, ne kompozitni rezultat. Dubine bita iznad 8 kod JPEG 2000 se resampliraju nadole, što je namjerno gubitnički postupak i pogrešan potez ako ponovno arhivirate, a ne samo prikazujete. A image maska, jednobitni stencil bez vlastite boje, opisana je u descriptoru, ali je nešto drugo od slikovne slike; dekodirajte je očekujući fotografiju i iznenadit ćete se

Kad izdvajanje nije dovoljno, sirovi tok i dalje je ondje u učitanom grafu objekata, sa svim filtrima, i možete ga izvući bajt za bajtom i predati vlastitom specijaliziranom kodeku. To je fallback koji pass-through dizajn namjerno čuva: izvorni bajtovi nikad se ne bacaju, pa je najgori slučaj da ih dekodirate sami, a ne da su podaci nestali. Za većinu stvarnih poslova, ipak, podržanih osam filtara pokriva ono što skeneri, uredski paketi i report enginei doista isporučuju, a petlja preko GetLoadedImageCount s Decodable guardom vraća učitani PDF natrag u mapu bitmapa u nekoliko redaka

API za izdvajanje učitanih slika, zajedno s kompletnim skupom filtera za dekodiranje opisanih ovdje, dolazi u HotPDF Component za Delphi i C++Builder