Imate PDF na disku, klijent je skenirao gomilu faktura, a vaš posao je da izvučete slike stranica nazad kao bitmape za OCR obradu. Učitate datoteku, pronađete image XObjecte, a zatim naletite na deo na koji vas niko ne upozori: bajtovi u tim strimovima nisu pikseli. To je JPEG codestream, ili JPEG 2000 blob kompresovan waveletom, ili Group 4 fax zapis, ili indeksirani raster iza palete iza Flate filtera. Image objekat zna svoju širinu i visinu, ali stvarni uzorci su zaključani unutar filtera koji je proizvođač izabrao. Doći do upotrebljive TBitmap znači poništiti taj filter, a PDF vam nudi otprilike osam različitih načina da ti bajtovi budu zapečaćeni
Ovo je praznina koju ExtractLoadedImage popunjava u HotPDF-u, nativnoj VCL PDF komponenti za Delphi i C++Builder. Ona nabraja image XObjecte u dokumentu koji ste učitali, prijavljuje šta je svaki od njih i dekodira one koje može nazad u 24-bitnu bitmapu. Zanimljiv deo nije API površina, koja ima tri metode. To je zašto uopšte mora da postoji poseban put dekodiranja i šta on može, a šta ne može da vrati u piksele
Zašto učitane slike nisu već dekodirane
HotPDF-ov loader je zasnovan na prolaznom očuvanju podataka. Kada pozovete LoadFromFile, strimovi slika ostaju tačno onakvi kakvi su u izvornoj datoteci: originalni filter, originalni kompresovani bajtovi, originalni rečnik. To je namerno. Suština učitavanja dokumenta je obično da kopirate stranice, spajate datoteke, pečatirate ih, menjate im dozvole i upisujete ih nazad, a za sve to najjeftinija i najbezbednija stvar je da svaki image stream ostane netaknut. Dekodiranje svake slike u raster pri učitavanju potrošilo bi memoriju i CPU na posao koji većini pozivaoca nikada ne treba, a ponovno kodiranje pri snimanju degradiralo bi slike koje su morale da budu kopirane doslovno
Posledica je da učitani graf objekata ne nosi piksele. Image XObject čiji je /Filter je /DCTDecode sadrži JPEG bajtove; HotPDF nikada nije pokrenuo JPEG dekoder nad njim, jer ništa u putanji kopiranja i ponovnog pisanja to nije tražilo. Zato, kada zaista želite piksele, API za izdvajanje mora sam da uradi dekodiranje, od nule, za onaj filter koji taj konkretni image koristi. Iz istog razloga su kodeci na strani kodiranja nezavisni od loadera: članak o dodavanju JPEG 2000 slika u PDF-ove u Delphiju opisuje kako se JPX engine priključuje na stranu kreiranja, a taj engine jednostavno nije bio povezan sa putanjom čitanja sve dok API za izdvajanje to nije zatražio
Tri-metodski API
Površina je mala. GetLoadedImageCount vraća koliko image XObjecta učitani dokument sadrži. GetLoadedImageInfo popunjava recordski opis jedne od njih po indeksu. ExtractLoadedImage vraća dekodiranu bitmapu, ili nil kada ne može da dekodira tu sliku. Nabrajanje je zasnovano na indeksu i stabilno za jedno učitavanje: interno prolazi kroz tabelu indirektnih objekata i skuplja svaki strim čiji /Subtype se razrešava u /Image, pa je indeks koji prosledite u GetLoadedImageInfo isti indeks koji prosledite 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 ugovorna detalja ovde su važna. Prvo, vraćeni TBitmap je vaš da ga oslobodite; dokument ga ne kešira niti poseduje. Drugo, proverite Decodable pre poziva, i proverite rezultat naspram nil posle. Metoda ne baca izuzetak na nepodržanom filteru, već vraća nil, a tiho nil u petlji nad serijom je tačno onakva stvar koja proguta stranicu posla sa hiljadu stranica, a da to niko ne primeti
Čitanje opisa pre dekodiranja
THPDFLoadedImageInfo vam govori šta je slika bez obavezivanja na potpuno dekodiranje. Njena polja dolaze direktno iz rečnika slike: Width i Height u uzorcima, BitsPerComponent, ColorComponents i ColorSpace opisuju interpretaciju posle dekodiranja (1 za sivu, 3 za RGB, 4 za CMYK), Filter kao imenovana kompresija, IsImageMask za stencil maske, ObjectNumber za osnovni indirektni objekat, i Decodable
Taj poslednji indikator je iskren. Decodable je True samo kada pokrenuta verzija zaista može da pretvori baš tu kombinaciju filtera i prostora boje u bitmapu. To kodira stvarnu matricu podrške, a ne želju: slika čiji Filter filter trenutna verzija ne razume prijavljuje Decodable = False, i na osnovu toga možete da granate logovanje, preskakanje ili povratak na ručno izdvajanje sirovog strima. Tretirajte to kao preduslov, ne kao nagoveštaj
// 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 sapliće ljude koji ručno grade recordske opise. THPDFLoadedImageInfo sadrži dva AnsiString string polja, Filter i ColorSpace. To su upravljani tipovi sa brojanjem referenci, pa je refleks da record poništite pomoću FillChar(Info, SizeOf(Info), 0) pogrešan ovde: time prepisujete referencu stringa bez njenog umanjenja, što curi ili kvari podatke. HotPDF upravo zato inicijalizuje record polje po polje, i ako ikad kopirate ovaj obrazac u svoj kod, uradite isto
Jedan dispatcher, osam putanja filtera
Razlog što je ovoj mogućnosti trebalo više izdanja, a ne jedno, jeste to što PDF nema format slike. On ima filtere, a §8.9.5 ISO 32000-1 dopušta image XObjectu da u /Filter navede bilo koji od njih, dok je interpretacija uzoraka zasebno određena pomoću /ColorSpace, /BitsPerComponent i opcionalnog /Decode niza. ExtractLoadedImage čita naziv filtera i usmerava ga na namenski dekoder za svaki slučaj. Podržani skup, građen kroz v2.229 do v2.231, sada pokriva osam različitih putanja
- Sirovi rasteri (FlateDecode, LZWDecode ili bez filtera) u 8-bitnom DeviceRGB ili DeviceGray. Bajtovi se raspakuju u upakovani raster, a jedina transformacija je zamena kanala, opisana niže
- DCTDecode (JPEG). Codestream se predaje VCL-ovom
TJPEGImage, koji rešava geometriju i boju, a rezultat se upisuje u 24-bitnu bitmapu - JPXDecode (JPEG 2000). Dekodira se kroz OpenJPEG backend, isti engine opisan u članku o JPEG 2000, uz resamplovanje komponenti višeg bit-dubinskog opsega na 8 bita
- Indeksirana boja. Paleta se čita iz niza
[/Indexed base hival lookup]i svaki uzorak se proširuje kroz tabelu za pretragu u pravu boju - DeviceCMYK. Uzorci sa četiri kanala pretvaraju se u RGB standardnom formulom mastila-na-belom
- DeviceGray i Indexed ispod 8 bita pri 1, 2 ili 4 bita po komponenti, raspakovani uzorak po uzorak i skalirani u opseg 0–255
- CCITTFaxDecode, fax filteri Group 3 i Group 4, dekodirani preko namenski registrovanog T.4/T.6 backenda
- JBIG2Decode, filter sa visokom kompresijom za dvobojne slike, dekodiran kroz registrovani JBIG2 backend koji članak o nativnoj JBIG2 kompresiji pokriva sa strane kodiranja
Sve završava na istom mestu: 24-bitna BGR bitmapa, jer je to ono što VCL TBitmap čuva nativno i što svaki nizvodni korisnik očekuje
Transformacije koje tiho menjaju piksele
Dve od ovih putanja uključuju transformaciju koju je lako neprimetno pogrešno uraditi, i vredi je razumeti čak i ako sami nikada ne dirate dekoder. Prva je zamena redosleda boja. PDF DeviceRGB raster čuva uzorke redosledom crveno-zeleno-plavo, sa gornjim redom prvi. VCL 24-bitni scanline ih čuva redosledom plavo-zeleno-crveno. Dakle, dekodiranje obične RGB slike nije memcpy; prvi i treći bajt svakog piksela se menjaju pri upisu u scanline. Ako to okrenete naopako, crvene i plave se zamene, što na sivom testu izgleda dobro a na kolor slici katastrofalno pogrešno. Redosled redova, koliko vredi, prolazi pravo kroz to: PDF-ovi top-down rasteri poravnavaju se sa VCL-ovim ScanLine[0] kao gornji vizuelni red, pa nema potrebe za vertikalnim obrtanjem
Druga je CMYK. PDF DeviceCMYK slike nose četiri mastila, a pretvaranje u RGB je proračun po kanalu, ne lookup: svaki izlazni kanal je (255 - ink) * (255 - K) / 255. Ovo je aproksimacija uređaja, ne konverzija sa upravljanjem bojama kroz ICC profil, pa je rezultat dovoljno dobar za prikaz i ponovno rasterizovanje, ali nije prava staza ako vam treba boja tačna za štampu. Ako vaš tok rada zahteva vernost, tretirajte izdvojenu bitmapu kao pregled i zadržite originalni CMYK strim za pipeline sa upravljanjem bojama
Putanja Indexed krije i sopstvenu zamku pri parsiranju. Paleta u prostoru boje /Indexed može da se čuva kao literalni string ili kao heksadecimalni string, a HotPDF vrednost heks stringa čuva kao heks tekst, a ne kao dekodirane bajtove. Zato, kada je paleta heks string, lookup tabelu prvo treba provući kroz dekodiranje heksa u bajtove; literalni string je već sirovi bajtni sadržaj. Propuštanje te grane dovodi do toga da indeksirana slika od četiri boje izađe kao šrot, jer se svaki unos palete čita sa pogrešne granice bajta
Lanci filtera: poslednji filter je sopstveni identitet slike
Pojedinačni /Filter naziv je lak slučaj. PDF dozvoljava i lanac filtera, gde je strim prošao kroz više koraka redom, navedenih u nizu /Filter kao što su [/ASCII85Decode /FlateDecode] ili [/ASCIIHexDecode /DCTDecode] (ISO 32000-1 §7.4). Semantika je precizna: filteri se pri kodiranju primenjuju sleva nadesno, pa ih pri dekodiranju poništavate zdesna nalevo, a poslednji filter u nizu je onaj koji zapravo definiše format slike. Početni filteri su samo transportna enkapsulacija oko njega
Ekstraktor to rešava skidanjem slojeva. Pre nego što krene bilo koji dekoder slike, svaki filter u lancu osim poslednjeg primenjuje se da bi se dobio ulaz koji poslednji filter očekuje, i tek tada se vrši usmeravanje na taj poslednji filter. Tako [/ASCII85Decode /DCTDecode] prvo uklanja ASCII85 sa strima, a zatim rezultat usmerava na JPEG putanju; [/FlateDecode] omotan oko sirovog rastera prvo se raspakuje, a zatim ide na raster putanju. To je ono što dozvoljava da osam dekodera ostane jednostavno. Nijedan ne mora da zna za ASCII85 ili heks transport omotače, jer u trenutku kada dekoder vidi bajtove, omotači su već nestali. Takođe znači da lanac čiji je poslednji filter nepodržan i dalje pada čisto na koraku usmeravanja, a ne na pola puta
Gde izdvajanje staje i šta tada uraditi
Budite iskreni prema sebi oko granica. Slika čiji je poslednji filter van podržanog skupa vraća nil, a isto tako i ona čiji prostor boje ova verzija ne može da protumači. Meke maske i alfa ne rekonstruišu se u bitmapu; dobijate osnovnu sliku, ne kompozitni rezultat. Bit-dubine iznad 8 iz JPEG 2000 se resampluju naniže, što je namerno gubitnički postupak i pogrešan potez ako ponovo arhivirate umesto da prikazujete. A image maska, jedinični stencil bez sopstvene boje, opisana je opisom ali je druga stvar od slikovnog image objekta; dekodirajte je očekujući fotografiju i bićete iznenađeni
Kada izdvajanje nije dovoljno, sirovi strim je i dalje tu u učitanom grafu objekata, sa svim filterima, i možete ga izvući bajt po bajt i predati sopstvenom specijalizovanom kodeku. To je fallback koji prolazni dizajn namerno čuva: originalni bajtovi se nikad ne bacaju, pa je najgori slučaj da ih sami dekodirate umesto da su podaci nestali. Za većinu stvarnih poslova, međutim, podržanih osam filtera pokriva ono što skeneri, kancelarijski paketi i engine-i za izveštaje zaista emituju, a petlja preko GetLoadedImageCount sa Decodable gardom vraća učitani PDF nazad u fasciklu bitmapa u nekoliko redova
API za izdvajanje učitanih slika, zajedno sa kompletnim skupom ovde opisanih filtera za dekodiranje, dolazi u HotPDF Component za Delphi i C++Builder