Techninis straipsnis

Iš įkelto PDF vaizdų ištraukimas Delphi aplinkoje: HotPDF

Jūsų diske yra PDF, klientas jį nuskenavo iš sąskaitų krūvos, o jūsų užduotis - ištraukti puslapių vaizdus atgal kaip bitinius atvaizdus OCR apdorojimui. Įkeliate failą, randate vaizdų XObject objektus ir tada susiduriate su dalimi, apie kurią niekas neįspėja: tų srautų baitai nėra pikseliai. Tai gali būti JPEG kodų srautas, bangelių suspaustas JPEG 2000 blokas, Group 4 fakso duomenys arba indeksuotas rastras už paletės ir už Flate filtro. Vaizdo objektas žino savo plotį ir aukštį, tačiau tikrieji mėginiai paslėpti filtro viduje, kurį pasirinko generatorius. Norint gauti tinkamą TBitmap, reikia atšaukti tą filtrą, o PDF suteikia maždaug aštuonis skirtingus būdus, kaip tie baitai gali būti užantspauduoti

Būtent šią spragą HotPDF, vietiniame VCL PDF komponente, skirtame Delphi ir C++Builder, užpildo ExtractLoadedImage. Jis išvardija įkeltame dokumente esančius vaizdų XObject objektus, praneša, kas kiekvienas iš jų yra, ir tuos, kuriuos moka, iškoduoja atgal į 24 bitų bitinį atvaizdą. Įdomiausia čia ne API paviršius, kuris susideda iš trijų metodų. Svarbiausia, kodėl apskritai turi egzistuoti atskiras dekodavimo kelias ir ką jis gali bei ko negali paversti atgal į pikselius

Kodėl įkelti vaizdai dar nėra iškoduoti

HotPDF įkėliklis sukurtas pagal pralaidaus perdavimo tikslumo principą. Kai iškviečiate LoadFromFile, vaizdų srautai paliekami tiksliai tokie, kokie jie yra šaltinio faile: originalus filtras, originalūs suspausti baitai, originalus žodynas. Tai daroma sąmoningai. Dažniausiai dokumentas įkeliamas tam, kad būtų galima kopijuoti puslapius, sulieti failus, uždėti žymas, pakeisti teises ir vėl įrašyti į diską, o visoms šioms užduotims pigiausia ir saugiausia nieko neliesti kiekviename vaizdo sraute. Jei kiekvienas vaizdas būtų iškoduojamas į rastrą jau įkėlimo metu, būtų švaistoma atmintis ir CPU darbui, kurio daugumai kviečiančiųjų niekada neprireiks, o pakartotinis kodavimas išsaugant pablogintų vaizdus, kuriuos reikėjo nukopijuoti pažodžiui

To pasekmė yra tokia, kad įkeltas objektų grafas išvis neturi pikselių. Vaizdo XObject, kurio /Filter yra /DCTDecode, laiko JPEG baitus; HotPDF niekada neleido per juos JPEG dekoderio, nes kopijavimo ir perrašymo keliui to nereikėjo. Todėl kai jums iš tikrųjų reikia pikselių, ištraukimo API turi pati atlikti dekodavimą nuo nulio, pagal tą filtrą, kurį naudoja konkretus vaizdas. Dėl tos pačios priežasties ir kodavimo pusės kodekai yra nepriklausomi nuo įkėliklio: straipsnyje apie JPEG 2000 vaizdų pridėjimą į PDF Delphi aplinkoje aprašyta, kaip JPX variklis prijungiamas kūrimo pusėje, o į skaitymo kelią jis tiesiog nebuvo prijungtas, kol jo neprireikė ištraukimo API

Trijų metodų API

Paviršius nedidelis. GetLoadedImageCount grąžina, kiek įkeltame dokumente yra vaizdų XObject objektų. GetLoadedImageInfo pagal indeksą užpildo vieno iš jų deskriptoriaus įrašą. ExtractLoadedImage grąžina iškoduotą bitinį atvaizdą arba nil, kai šio vaizdo iškoduoti nepavyksta. Išvardijimas vyksta pagal indeksą ir konkrečiam įkėlimui yra stabilus: viduje jis pereina per netiesioginių objektų lentelę ir surenka kiekvieną srautą, kurio /Subtype išsisprendžia į /Image, todėl indeksas, kurį perduodate GetLoadedImageInfo, yra tas pats indeksas, kurį perduodate 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;

Čia svarbios dvi sutarties detalės. Pirma, grąžintas TBitmap priklauso jums ir jį turite atlaisvinti patys; dokumentas jo nekaupia ir nevaldo. Antra, prieš kviesdami patikrinkite Decodable, o po kvietimo rezultatą palyginkite su nil. Metodas nepakyla su klaida dėl nepalaikomo filtro, jis grąžina nil, o tylus nil paketiniame cikle yra būtent tas dalykas, kuris tūkstančio puslapių užduotyje praryja vieną puslapį taip, kad niekas nepastebi

Deskriptoriaus nuskaitymas prieš dekodavimą

THPDFLoadedImageInfo leidžia suprasti, koks tai vaizdas, dar nepradedant pilno dekodavimo. Jo laukai ateina tiesiai iš vaizdo žodyno: Width ir Height mėginių vienetais, BitsPerComponent, ColorComponents ir ColorSpace, aprašantys interpretaciją po dekodavimo (1 - pilkai, 3 - RGB, 4 - CMYK), Filter kaip įvardytas suspaudimas, IsImageMask trafaretinėms kaukėms, ObjectNumber pagrindiniam netiesioginiam objektui ir Decodable

Būtent paskutinė vėliava ir sako tiesą. Decodable yra True tik tada, kai šiuo metu vykdoma versija tikrai gali paversti konkrečią filtro ir spalvų erdvės kombinaciją bitiniu atvaizdu. Ji koduoja realią palaikymo matricą, o ne pageidavimą: vaizdas, kurio Filter dabartinė versija nesupranta, grąžina Decodable = False, ir jūs galite pagal tai šakoti registravimą, praleidimą arba perėjimą prie žaliavinio srauto ištraukimo. Vertinkite tai kaip išankstinę sąlygą, o ne kaip užuominą

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

Yra viena įgyvendinimo detalė, kuri kerta per ranka deskriptorių įrašus konstruojančius žmones. THPDFLoadedImageInfo turi du AnsiString laukus - Filter ir ColorSpace. Tai valdomi tipai su nuorodų skaitikliu, todėl instinktas anuliuoti įrašą su FillChar(Info, SizeOf(Info), 0) čia yra neteisingas: taip perrašoma eilutės nuoroda jos nemažinant, o tai nutekina atmintį arba gadina būseną. HotPDF inicijuoja įrašą lauką po lauko būtent dėl šios priežasties, ir jei kada nors kopijuosite šį modelį savo kode, darykite tą patį

Vienas dispečeris, aštuoni filtrų keliai

Priežastis, kodėl ši funkcija pasirodė per kelis leidimus, o ne viename, yra ta, kad PDF neturi vieno vaizdo formato. Jis turi filtrus, o ISO 32000-1 §8.9.5 leidžia vaizdo XObject juos įvardyti lauke /Filter, o mėginių interpretacija valdoma atskirai per /ColorSpace, /BitsPerComponent ir pasirenkamą /Decode masyvą. ExtractLoadedImage perskaito filtro pavadinimą ir nukreipia į skirtą dekoderį kiekvienu atveju. Palaikomas rinkinys, augintas nuo v2.229 iki v2.231, dabar apima aštuonis skirtingus kelius

  • Žali rastrai (FlateDecode, LZWDecode arba be filtro) 8 bitų DeviceRGB arba DeviceGray režimu. Baitai išpučiami į supakuotą rastrą, o vienintelė transformacija yra kanalų sukeisti tvarka, aprašyta žemiau
  • DCTDecode (JPEG). Kodų srautas perduodamas VCL TJPEGImage, kuris išsprendžia geometriją ir spalvą, o rezultatas priskiriamas 24 bitų bitiniam atvaizdui
  • JPXDecode (JPEG 2000). Dekoduojama per OpenJPEG galinę dalį, tą patį variklį, aprašytą straipsnyje apie JPEG 2000, o didelio bitų gylio komponentai perskaičiuojami iki 8 bitų
  • Indeksuotos spalvos. Paletė nuskaitoma iš masyvo [/Indexed base hival lookup], o kiekvienas mėginys per paieškos lentelę išplečiamas į tikrą spalvą
  • DeviceCMYK. Keturių kanalų mėginiai paverčiami į RGB pagal standartinę rašalo ant balto formulę
  • Sub-8-bit DeviceGray ir Indexed su 1, 2 arba 4 bitais vienam komponentui, išpakuojami mėginys po mėginio ir perskaluojami į 0-255 diapazoną
  • CCITTFaxDecode, Group 3 ir Group 4 fakso filtrai, dekoduojami specialia T.4/T.6 galine dalimi
  • JBIG2Decode, didelio santykio dviejų lygių filtras, dekoduojamas per registruotą JBIG2 galinę dalį, kurią iš kodavimo pusės aptaria vietinio JBIG2 suspaudimo straipsnis

Viskas nusileidžia į tą pačią vietą: 24 bitų BGR bitinį atvaizdą, nes būtent taip VCL TBitmap natūraliai saugo duomenis ir būtent to tikisi kiekvienas žemiau esantis vartotojas

Transformacijos, kurios tyliai pakeičia pikselius

Dviejuose iš šių kelių yra transformacija, kurią labai lengva subtiliai sugadinti, ir ją verta suprasti net jei paties dekoderio niekada neliesite. Pirmoji - spalvų eiliškumo sukeiti­mas. PDF DeviceRGB rastras mėginius laiko raudona-žalia-mėlyna tvarka, pradedant viršutine eilute. VCL 24 bitų scanline juos laiko mėlyna-žalia-raudona tvarka. Todėl paprasto RGB vaizdo dekodavimas nėra memcpy; pirmas ir trečias kiekvieno pikselio baitai pakeliui į scanline sukečiami vietomis. Suklyskite čia ir raudona su mėlyna apsikeis vietomis - pilkų tonų bandomajame vaizde tai atrodys normaliai, o spalvotame bus katastrofiškai blogai. Eilučių tvarka, beje, persiduoda tiesiogiai: PDF viršus-žemyn rastrai sutampa su VCL ScanLine[0] kaip viršutine matoma eile, todėl jokio vertikalaus apvertimo nereikia

Antroji yra CMYK. PDF DeviceCMYK vaizdai turi keturis rašalus, o konvertavimas į RGB yra skaičiavimas kiekvienam kanalui, o ne paieška: kiekvienas išvesties kanalas yra (255 - ink) * (255 - K) / 255. Tai įrenginio aproksimacija, o ne spalvų valdomas konvertavimas per ICC profilį, todėl rezultatas pakankamai teisingas rodymui ir pakartotiniam rastravimui, tačiau netinka, jei jums reikia spaudos tikslumo spalvų. Jei jūsų darbo eiga reikalauja tikslumo, laikykite ištrauktą bitinį atvaizdą peržiūra ir palikite originalų CMYK srautą spalvų valdomam vamzdynui

Indexed kelias slepia savąją analizavimo gaudyklę. Paletė /Indexed spalvų erdvėje gali būti saugoma kaip pažodinė eilutė arba kaip šešioliktainė eilutė, o HotPDF šešioliktainės eilutės reikšmę saugo kaip šešioliktainį tekstą, o ne kaip iškoduotus baitus. Todėl kai paletė yra šešioliktainė eilutė, paieškos lentelę pirmiausia reikia praleisti per hex-to-bytes dekodavimą; pažodinė eilutė jau ir taip yra žali baitai. Praleiskite šią šaką ir keturių spalvų indeksuotas vaizdas išeis kaip šiukšlės, nes kiekvienas paletės įrašas bus skaitomas nuo neteisingos baitų ribos

Filtrų grandinės: paskutinis filtras priklauso pačiam vaizdui

Vienas /Filter pavadinimas yra lengvas atvejis. PDF taip pat leidžia filtrų grandinę, kai srautas nuosekliai perleidžiamas per kelis filtrus, išvardytus eilės tvarka /Filter masyve, tokiame kaip [/ASCII85Decode /FlateDecode] arba [/ASCIIHexDecode /DCTDecode] (ISO 32000-1 §7.4). Semantika tiksli: koduojant filtrai taikomi iš kairės į dešinę, todėl dekoduojant juos atšaukiate iš dešinės į kairę, o paskutinis filtras masyve yra tas, kuris iš tikrųjų apibrėžia vaizdo formatą. Pradiniai filtrai tėra transporto apvyniojimai aplink jį

Ištraukiklis su tuo susitvarko sluoksniuodamas. Prieš paleidžiant bet kokį vaizdo dekoderį, pritaikomi visi filtrų grandinės filtrai, išskyrus paskutinį, kad būtų pagaminta įvestis, kurios tikisi galutinis filtras, ir tik tada dispečeris nukreipia pagal tą paskutinį filtrą. Taigi [/ASCII85Decode /DCTDecode] pirmiausia nuima ASCII85 apvalkalą nuo srauto, o tada nukreipia rezultatą į JPEG kelią; [/FlateDecode], apvyniotas aplink žalią rastrą, pirmiausia išpučia duomenis ir tuomet paleidžia rastrinį kelią. Būtent taip visi aštuoni dekoderiai gali išlikti paprasti. Nė vienam iš jų nereikia nieko žinoti apie ASCII85 ar hex transporto apvalkalus, nes kol dekoderis pamato baitus, apvalkalų jau nebėra. Tai taip pat reiškia, kad grandinė, kurios galutinis filtras nepalaikomas, tvarkingai sugenda dispečerio žingsnyje, o ne pusiaukelėje

Kur ištraukimas baigiasi ir ką tada daryti

Būkite sąžiningi sau dėl ribų. Vaizdas, kurio galutinis filtras nepatenka į palaikomą rinkinį, grąžina nil, taip pat ir toks, kurio spalvų erdvės ši versija negali interpretuoti. Soft mask ir alfa kanalai neatskuriami į bitinį atvaizdą; jūs gaunate bazinį vaizdą, o ne sukomponuotą rezultatą. Didesni nei 8 bitų gyliai iš JPEG 2000 perskaičiuojami žemyn, ir tai sąmoningai yra nuostolinga bei netinkama, jei jūs failą archyvuojate iš naujo, o ne tik rodote. O vaizdo kaukė, vieno bito trafaretas be savos spalvos, deskriptoriuje aprašoma, tačiau tai kitoks objektas nei iliustracinis vaizdas; jei dekoduosite ją tikėdamiesi nuotraukos, nustebsite

Kai ištraukimo nepakanka, žalias srautas vis dar yra čia pat įkeltame objektų grafe, su filtru ir viskuo, ir jūs galite jį ištraukti baitas po baito bei perduoti savo specializuotam kodekui. Būtent tokią atsarginę galimybę pralaidaus perdavimo architektūra ir saugo sąmoningai: originalūs baitai niekada neišmetami, todėl blogiausiu atveju jūs juos dekoduojate patys, o ne susiduriate su prarastais duomenimis. Vis dėlto daugumoje realių darbų palaikomi aštuoni filtrai apima tai, ką iš tikrųjų generuoja skeneriai, biuro paketai ir ataskaitų varikliai, o ciklas per GetLoadedImageCount su Decodable sargu keliose eilutėse paverčia įkeltą PDF atgal į bitinių atvaizdų aplanką

Įkeltų vaizdų ištraukimo API kartu su visu čia aprašytu dekodavimo filtrų rinkiniu tiekiamas kartu su HotPDF Component, skirtu Delphi ir C++Builder