Technical Article

Vaizdų išgavimas iš PDF dokumentų naudojant PDFium VCL programoje Delphi

PDF formatas saugo vaizdus kaip savarankiškus objektus savo turinio srautuose. Kai puslapyje pateikiama nuotrauka, nuskenuotas vaizdas ar diagrama, pikselių duomenys saugomi XObject žodyne šalia puslapio geometrijos. „PDFium VCL“ leidžia pasiekti šiuos duomenis per dvi TPdf savybes: BitmapCount, kuri grąžina įterptų taškinių vaizdų skaičių dabartiniame puslapyje, ir Bitmap[Index], kuri iškoduoja pasirinktą vaizdą į TBitmap objektą (kurio savininku tampate jūs ir turite jį atlaisvinti). Tai yra visas išgavimo modelis – ciklas susideda iš keturių eilučių, tačiau sėkmingam darbui reikalingas tinkamas pasiruošimas.

Dokumento atidarymas

Pirmiausia verta žinoti, kad TPdf savybės Active := True nustatymas nesukelia jokių klaidų pranešimų. Įkėlimo klaidos, neteisingi slaptažodžiai, sugadinti failai – visa tai yra apdorojama viduje, o komponentas tiesiog lieka neaktyvus. Todėl po priskyrimo turite patys patikrinti šią būseną, kitaip pradėsite puslapių ciklą, kur PageCount grąžins nulį, ir stebėsitės, kodėl nieko nepavyko išgauti.

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
    begin
      Writeln('Failed to open: ', Pdf.FileName);
      Exit;
    end;
    Writeln(Pdf.PageCount, ' pages');
    // proceed to extraction
  finally
    Pdf.Free;
  end;
end;

Slaptažodžiu apsaugoti failai apdorojami pagal tą pačią schemą: priskirkite reikšmę savybei Pdf.Password prieš nustatydami Active := True. Jei slaptažodis neteisingas, Active liks False ir jokia išimtis (exception) nebus sukelta. Paketiniuose įrankiuose, apdorojančiuose šimtus failų, toks elgesys yra naudingas, nes leidžia kaupti klaidų sąrašą, o ne nutraukti programos vykdymą dėl kiekvieno failo.

Puslapių peržiūra ir taškinių vaizdų gavimas

Savybė BitmapCount veikia konkrečiam puslapiui, todėl prieš ją nuskaitant reikia nustatyti Pdf.PageNumber. Puslapių indeksavimas prasideda nuo 1; numatytoji reikšmė yra 0, kas reiškia, kad joks puslapis neįkeltas. Savybė Bitmap[Index] naudoja indeksavimą nuo 0 ir grąžina jums priklausantį TBitmap objektą. Privalote jį atlaisvinti. Jei pamiršite iškviesti atlaisvinimą ilgajame cikle, apdorojant didelį dokumentą, sunaudojama atmintis sparčiai augs, nes kiekvienas vaizdas iki suspaudimo gali užimti po kelis megabaitus neapdorotų pikselių duomenų.

procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
  Page, Idx: Integer;
  Bmp: TBitmap;
  OutPath: string;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    for Idx := 0 to Pdf.BitmapCount - 1 do
    begin
      Bmp := Pdf.Bitmap[Idx];
      if not Assigned(Bmp) then
        Continue;
      try
        OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
        Bmp.SaveToFile(OutPath);
      finally
        Bmp.Free;
      end;
    end;
  end;
end;

Patikrinimas su Assigned yra labai svarbus. Kai kurie PDF generavimo įrankiai sukuria vaizdo XObject objektus su nuliniais matmenimis arba kitais pažeistais duomenimis – tokiais atvejais komponentas grąžina nil reikšmę, o ne tuščią taškinį vaizdą. Klaidinga būtų nutraukti išgavimą gavus nil reikšmę: tiesiog praleiskite ją, jei reikia, užregistruokite puslapį bei indeksą žurnale ir tęskite darbą. Likusi puslapio dalis vis tiek gali turėti tvarkingų vaizdų.

Atkreipkite dėmesį, kad išorinis ciklas kiekvienoje iteracijoje nustato Pdf.PageNumber. Šis priskyrimas įkelia puslapį į vidinę komponento būseną ir leidžia teisingai nuskaityti BitmapCount. Jei to nepadarysite, nuolat nuskaitinėsite to paties puslapio vaizdų skaičių. Toks šablonas gali atrodyti perteklinis, tačiau taip suprojektuotas šis API: puslapis veikia kaip žymeklis, o ne kaip kolekcija.

Išvesties formato pasirinkimas

BMP formatas yra be nuostolių (lossless) ir visada prieinamas be papildomų modulių importavimo, todėl jis yra geras numatytasis pasirinkimas, kai dar nežinote vaizdo turinio. Kai failo dydis yra svarbus, grąžinto TBitmap pikselių formatas padeda parinkti tinkamą kodeką. 32 bitų taškinis vaizdas turi alfo kanalą (skaidrumą), kurį be nuostolių išsaugo PNG formatas. Didelis 24 bitų tolygių tonų vaizdas labiausiai tinka JPEG formatui. Mažesnius vaizdus arba vaizdus su ribota spalvų palete geriau palikti BMP formatu, nes JPEG gali pridėti matomų suspaudimo artefaktų ir neduos didelės naudos failo dydžiui.

procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
  Jpg: TJPEGImage;
begin
  case UpperCase(ExtractFileExt(FileName)) of
    '.JPG', '.JPEG':
      begin
        Jpg := TJPEGImage.Create;
        try
          Jpg.Assign(Bmp);
          Jpg.CompressionQuality := 85;
          Jpg.SaveToFile(FileName);
        finally
          Jpg.Free;
        end;
      end;
  else
    Bmp.SaveToFile(FileName);  // BMP: lossless, no extra units
  end;
end;

Praktikoje formato pasirinkimą nulemia Bmp.PixelFormat savybė ir vaizdo matmenys. Jei PixelFormat = pf32bit, jums reikia formato, palaikančio alfo kanalą – PNG yra akivaizdus pasirinkimas, nors senesnėse „Delphi“ versijose tam reikės įtraukti PNGImage modulį. 24 bitų vaizdams, kurių plotis didesnis nei 300 pikselių, JPEG formatas su 85 kokybės nustatymu leidžia sumažinti failą maždaug tris kartus, lyginant su BMP, be pastebimo kokybės praradimo. Mažesniems vaizdams BMP išlieka geras pasirinkimas, nes failų dydžiai yra panašūs ir nereikia priimti sprendimų dėl kokybės.

Ką skaičiuoja ir ko neskaičiuoja BitmapCount

PDF formatas atskiria vaizdus (XObjects) nuo vektorinės grafikos, brėžiamos naudojant kelius. Puslapis, kuris vizualiai atrodo sudėtingas, gali grąžinti BitmapCount reikšmę, lygią nuliui, jei visi jo elementai yra vektoriniai. Nuskenuoti puslapiai beveik visada grąžina tiksliai vieną vaizdą – skeneris visą puslapį įrašo kaip vieną didelį vaizdo objektą pasirinkta skiriamąja geba. Puslapiuose, kur tekstas derinamas su nuotraukomis, bus grąžinamas po vieną įrašą kiekvienai nuotraukai. Dekoratyvinės linijos, fono užpildai ar lentelių rėmeliai paprastai nepatenka į šis skaičių.

Šis skaičius taip pat neapima vidinių vaizdų (inline images) – tai retai naudojama PDF konstrukcija, kai vaizdo duomenys įterpiami tiesiai į puslapio turinio srautą, o ne aprašomi kaip atskiras XObject objektas. Šis API tokių vaizdų nepasiekia, tačiau realiuose dokumentuose jie sutinkami labai retai, todėl dauguma išgavimo įrankių jų tiesiog neapdoroja.

Dar viena detalė, kurią verta atsiminti: nuskaitomas BitmapCount galioja tam puslapiui, kuris buvo įkeltas paskutiniu PageNumber priskyrimu. Jei jūsų kodas atlieka šakojimąsi arba iškviečia funkcijas, kurios pakeičia PageNumber tarp skaičiavimo ir vaizdų gavimo žingsnių, galite pabandyti kreiptis į neegzistuojantį indeksą. Skaičiaus nuskaitymą ir ciklą per Bitmap[] vykdykite tame pačiame puslapyje, nekeisdami PageNumber reikšmės tarp šių žingsnių.

TPdfView naudojimas langų programose

Atmintis ir našumas paketiniuose darbuose

Apdorojant didelius archyvus, atminties valdymas yra svarbiausias uždavinys. Kiekvienas kreipinys į Bitmap[] išskiria naują TBitmap objektą atmintyje – 300 DPI nuskenuotame puslapyje tai gali lengvai užimti 25 MB neapdorotų pikselių duomenų dar prieš bet kokį kodavimą. Jei apdorojate puslapius greitu ciklu ir neatlaisvinate atminties, naudojami ištekliai augs tiesiškai su kiekvienu vaizdu. Teisinga eiga visada yra tokia: paimkite vieną taškinį vaizdą, atlikite su juo reikiamus veiksmus, atlaisvinkite jį ir tik tada pereikite prie kito. Jei jums reikia vienu metu laikyti kelis vaizdus palyginimui, pirmiausia nuskaitykite jų skaičių su BitmapCount ir atitinkamai suplanuokite talpyklą, tačiau atlaisvinkite kiekvieną vaizdą iškart, kai baigiate su juo dirbti, o ne atidėkite tai iki dokumento apdorojimo pabaigos. Dokumente su 500 nuskenuotų puslapių šis skirtumas nulems, ar programa sunaudos 25 MB, ar 12 GB operatyviosios atminties.

Komponentas TPdfView pateikia tas pačias savybes BitmapCount ir Bitmap[], tačiau puslapis, kurį jis nuskaito, yra šiuo metu vaizduojamas puslapis peržiūros lange, o ne TPdf.PageNumber. Šie du puslapių rodikliai yra nepriklausomi – nustačius vieną, kitas nesikeičia. VCL langų programoje su aktyviu peržiūros langu galite nustatyti Pdf.PageNumber := N išgavimui per TPdf komponentą, kol naudotojo peržiūros langas lieka tame puslapyje, kurį jis skaitė. Šis atskyrimas yra apgalvotas ir leidžia atlikti vaizdų išgavimą fone, nekeičiant vartotojo sąsajos būsenos.

Šiame straipsnyje aprašytos savybės BitmapCount ir Bitmap[] yra dalis „Delphi“ ir „C++Builder“ skirtos bibliotekos PDFium VCL Component.