Techninis straipsnis

Paveikslėlių išgavimas iš PDF dokumentų naudojant PDFium komponentą Delphi aplinkoje

PDF saugo paveikslėlius kaip pirmos klasės objektus (first-class objects) savo turinio srautuose. Kai puslapyje nurodoma nuotrauka, skenavimas ar diagrama, pikselių duomenys yra XObject žodyne, kartu su puslapio geometrija. PDFium komponentas tai pateikia per dvi TPdf savybes: BitmapCount, kuri grąžina, kiek įterptų taškinės grafikos paveikslėlių (bitmaps) yra dabartiniame puslapyje, ir Bitmap[Index], kuri iškoduoją vieną iš jų į jūsų valdomą TBitmap objektą, kurį turite atlaisvinti (free). Tai yra visas išgavimo modelis. Ciklas užima keturias eilutes; sprendimų priėmimo reikalauja tik supanti infrastruktūra (plumbing)

Dokumento atidarymas

Pirmas dalykas, kurį reikia žinoti apie TPdf, yra tai, kad Active := True niekada nesukelia išimties (never raises). Įkėlimo klaidos, neteisingi slaptažodžiai, sugadinti failai: visos šios klaidos yra nuryjamos viduje, ir komponentas tiesiog lieka neaktyvus. Jūs patys turite patikrinti vėliavėlę po priskyrimo, kitaip pereisite prie puslapių ciklo, kur PageCount grąžins nulį, ir stebėsitės, kodėl niekas nebuvo išgauta

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 seka tuo pačiu modeliu: priskirkite Pdf.Password prieš nustatydami Active := True. Jei slaptažodis neteisingas, Active lieka False ir negaunate jokios išimties (exception), kurią galėtumėte pagauti. Masinio apdorojimo įrankyje, apdorojančiame šimtus failų, toks tylus elgesys iš tikrųjų yra naudingas: jūs kaupiate klaidas sąraše, o ne išvyniojate iškvietimų dėklą (call stack) kiekvienai iš jų

Puslapių iteracija ir paveikslėlių traukimas

BitmapCount priklauso kiekvienam puslapiui (per-page), todėl prieš jį nuskaitant reikia nustatyti Pdf.PageNumber. Puslapių numeracija prasideda nuo 1; numatytoji reikšmė yra 0, o tai reiškia, kad joks puslapis neįkeltas. Bitmap[Index] savybė prasideda nuo 0 ir grąžina iškvietėjui priklausantį TBitmap. Jūs privalote jį atlaisvinti (free). Pamiršus jį atlaisvinti ilgame cikle, apdorojančiame didelį dokumentą, atminties sunaudojimas greitai išaugs, nes kiekvienas paveikslėlis (bitmap) gali užimti kelis megabaitus neapdorotų (raw) pikselių duomenų prieš bet kokį suspaudimą

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;

Assigned patikrinimas yra svarbus. Nedidelis skaičius PDF generatorių įrašo vaizdo XObject objektus su nuliniais pikselių matmenimis arba su kitaip sugadintais duomenimis; tokiais atvejais komponentas grąžina nil, o ne tuščią paveikslėlį. Traktuoti nil grąžinimą kaip klaidą ir sustabdyti išgavimą yra neteisingas refleksas: praleiskite jį, užregistruokite puslapį ir indeksą, jei jums reikia audito sekos, ir tęskite. Likusioje puslapio dalyje vis dar gali būti tinkamų paveikslėlių

Atkreipkite dėmesį, kad išorinis ciklas nustato Pdf.PageNumber kiekvienoje iteracijoje. Būtent šis priskyrimas įkelia puslapį į komponento vidinę būseną ir paverčia BitmapCount prasmingu. Praleiskite tai ir jūs skaitysite to paties puslapio skaičių vėl ir vėl. Šis modelis atrodo perteklinis jį rašant, tačiau taip sukurta API: puslapis yra žymeklis (cursor), o ne kolekcija

Išvesties formato pasirinkimas

BMP yra neprarandantis kokybės (lossless) ir visada prieinamas be papildomų modulių (units), todėl jis yra patikimas numatytasis pasirinkimas, kai dar nežinote, kas yra paveikslėlyje. Kai failo dydis yra svarbus, grąžinto TBitmap pikselių formatas (pixel format) nurodo, koks kodekas yra tinkamas. 32 bitų paveikslėlis turi alfa kanalą; PNG jį išsaugo be nuostolių. Didelis 24 bitų paveikslėlis su ištisiniais tonais (continuous tone) yra kandidatas į JPEG. Mažesnius paveikslėlius arba tuos, kurie nupiešti naudojant ribotą paletę, paprastai geriau palikti kaip BMP, nei konvertuoti į JPEG, kuris prideda blokavimo artefaktų esant žemiems kokybės nustatymams ir mažai sutaupo vietos esant aukštiems

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ą lemia Bmp.PixelFormat ir matmenys. Jei PixelFormat = pf32bit, jums reikia formato, palaikančio alfa kanalą; PNG yra akivaizdus pasirinkimas, nors senesnėse Delphi versijose tam reikia PNGImage modulio. 24 bitų paveikslėliams, platesniems nei maždaug 300 pikselių, JPEG su 85 kokybe sumažina dydį tris kartus lyginant su BMP, neprarandant pastebimos kokybės daugumoje fotografinio turinio. Žemiau šios ribos BMP yra panašaus dydžio ir leidžia visiškai išvengti bet kokių sprendimų dėl kokybės

Ką BitmapCount suskaičiuoja ir ko ne

PDF atskiria vaizdo XObject objektus ir vektorinę grafiką, nupieštą naudojant kelio (path) operatorius. Puslapis, kuris vizualiai atrodo sudėtingas, gali grąžinti BitmapCount lygų nuliui, jei kiekvienas elementas yra vektorinis. Skenuoti puslapiai beveik visada grąžina tiksliai vieną: skeneris įrašo visą skenavimą kaip vieną viso puslapio vaizdo XObject objektą bet kokia skiriamąja geba (resolution), kuria skeneris buvo nustatytas. Puslapiai, kuriuose spausdintas tekstas sumaišytas su įterptomis nuotraukomis, grąžina po vieną įrašą kiekvienai nuotraukai. Dekoratyvinės linijos, šešėliuoti fonai ir lentelių rėmeliai paprastai visiškai neatsispindi paveikslėlių skaičiuje

Skaičius taip pat neapima įterptųjų paveikslėlių (inline images) – retai naudojamos PDF konstrukcijos, kur vaizdo duomenys įterpiami tiesiogiai į puslapio turinio srautą, o ne kaip pavadintas XObject objektas. Jie nepatenka į tai, ką ši API pateikia; jie yra pakankamai reti realiuose dokumentuose, todėl dauguma išgavimo įrankių jų tiesiog neapdoroja

Viena detalė, kurią verta atsiminti: BitmapCount, kurį nuskaitote, skirtas dabartiniam puslapiui pagal paskutinį PageNumber priskyrimimą. Jei jūsų kodas šakojasi (branches) arba iškviečia kokią nors funkciją, kuri pakeičia PageNumber tarp skaičiavimo ir paėmimo (fetching), galite nuskaityti mažiau paveikslėlių nei paskyrėte vietos, arba indeksas gali išeiti už ribų. Išlaikykite skaičiaus nuskaitymą ir Bitmap[] ciklą tame pačiame puslapyje neliesdami PageNumber tarp jų

TPdfView naudojimas formos programoje

TPdfView komponentas atveria (exposes) tas pačias BitmapCount ir Bitmap[] savybes, tačiau puslapis, iš kurio jis skaito, yra šiuo metu rodininyje rodomas puslapis, o ne TPdf.PageNumber. Šios dvi puslapių nuorodos (pointers) yra nepriklausomos; nustačius vieną, kita nepaslenkama. VCL formos programoje su tiesiogine peržiūros programa (live viewer) galite iškviesti Pdf.PageNumber := N, kad nukreiptumėte išgavimą per TPdf, kol peržiūros programa lieka ten, kur vartotojas paskutinį kartą slinko. Šis atskyrimas yra apgalvotas ir išlaiko peržiūros programos rodymo būseną švarią, kol fone vyksta išgavimas

Atmintis ir našumas masiniuose darbuose

Apdorojant didelį archyvą, atminties biudžetas yra pagrindinis dalykas, kurį reikia stebėti. Kiekvienas Bitmap[] iškvietimas išskiria (allocates) naują TBitmap atminties krūvoje (heap), ir 300 DPI nuskenuotame puslapyje tai lengvai gali sudaryti 25 MB neapdorotų pikselių duomenų prieš bet kokį kodavimą. Jei apdorojate puslapius griežtame cikle neatlaisvindami atminties tarp iteracijų, darbinis rinkinys (working set) auga tiesiškai su paveikslėlių skaičiumi. Teisingas modelis visada yra toks: paimkite vieną paveikslėlį, padarykite tai, ko jums reikia, atlaisvinkite jį, paimkite kitą. Jei palyginimo žingsniui jums reikia išlaikyti nuorodas į kelis paveikslėlius vienu metu, pirmiausia suskaičiuokite juos su BitmapCount ir atitinkamai paskirstykite vietą savo konteineriui, o tada atlaisvinkite kiekvieną, kai tik baigsite su juo, užuot atidėję tai iki dokumento pabaigos valymo. Dokumente su 500 skenuotų puslapių šis skirtumas gali reikšti skirtumą tarp 25 MB ir 12 GB maksimalaus RSS (peak RSS)

Čia parodytos BitmapCount ir Bitmap[] savybės yra PDFium Component, skirto Delphi ir C++Builder, dalis