Artykuł techniczny

Wydobywanie obrazów z plików PDF w Delphi z PDFium Component

PDF przechowuje obrazy jako obiekty pierwszej kategorii wewnątrz swoich strumieni treści. Gdy strona odwołuje się do zdjęcia, skanu albo diagramu, dane pikseli mieszkają w słowniku XObject obok geometrii strony. PDFium Component wystawia to przez dwie właściwości TPdf: BitmapCount, które zwraca liczbę osadzonych bitmap na bieżącej stronie, oraz Bitmap[Index], które dekoduje jedną z nich do TBitmap, którego jesteś właścicielem i musisz zwolnić. To cały model wydobywania. Pętla ma cztery linie; osądu wymaga otaczająca ją instalacja

Otwieranie dokumentu

Pierwszą rzeczą, którą trzeba wiedzieć o TPdf, jest to, że Active := True nigdy nie zgłasza wyjątku. Awarie wczytania, złe hasła, uszkodzone pliki: wszystkie są połykane wewnętrznie, a komponent po prostu zostaje nieaktywny. Musisz sam sprawdzić flagę po przypisaniu, inaczej wejdziesz w pętlę stron z PageCount zwracającym zero i będziesz się zastanawiać, czemu nic się nie wydobyło

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');
    // przejdź do wydobywania
  finally
    Pdf.Free;
  end;
end;

Pliki chronione hasłem idą tym samym wzorcem: przypisz Pdf.Password przed ustawieniem Active := True. Jeśli hasło jest złe, Active zostaje False i nie dostajesz żadnego wyjątku do przechwycenia. W narzędziu wsadowym przetwarzającym setki plików takie ciche zachowanie jest wręcz przydatne: gromadzisz awarie na liście, zamiast rozwijać stos wywołań przy każdej z nich

Iterowanie po stronach i pobieranie bitmap

BitmapCount dotyczy pojedynczej strony, więc ustawiasz Pdf.PageNumber przed jego odczytem. Numery stron liczone są od jedynki; domyślnie jest 0, co oznacza, że żadna strona nie jest wczytana. Właściwość Bitmap[Index] liczona jest od zera i zwraca TBitmap należący do wywołującego. Musisz go zwolnić. Zaniedbaj zwolnienie wewnątrz długiej pętli po dużym dokumencie, a pamięć rośnie szybko, bo każda bitmapa może mieć kilka megabajtów surowych danych pikseli przed jakąkolwiek kompresją

Pętla wydobywania obrazów PDF w Delphi z PDFium, sprawdzająca cichą flagę Active, ustawiająca PageNumber dla każdej strony, czytająca BitmapCount, pomijająca bitmapy nil i zwalniająca każdy TBitmap po zapisie
Pętla czyta licznik dopiero po przesunięciu kursora strony, pomija bitmapy nil pochodzące ze zniekształconych XObject i zwalnia każdy należący do wywołującego TBitmap przed pobraniem następnego
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;

Zabezpieczenie Assigned ma znaczenie. Niewielka liczba generatorów PDF zapisuje obrazowe XObject o zerowych wymiarach pikselowych albo w inny sposób zniekształcone; w takich przypadkach komponent zwraca nil, a nie pustą bitmapę. Potraktowanie zwrotu nil jako błędu i zatrzymanie wydobywania to zły odruch: pomiń go, zaloguj stronę i indeks, jeśli potrzebujesz ścieżki audytu, i jedź dalej. Reszta strony wciąż może dać poprawne obrazy

Zauważ, że zewnętrzna pętla ustawia Pdf.PageNumber przy każdej iteracji. To właśnie to przypisanie wczytuje stronę do wewnętrznego stanu komponentu i nadaje BitmapCount sens. Pomiń je, a będziesz w kółko czytać licznik tej samej strony. Wzorzec sprawia wrażenie nadmiarowego, gdy go piszesz, ale tak zaprojektowano to API: strona jest kursorem, a nie kolekcją

Wybór formatu wyjściowego

BMP jest bezstratny i zawsze dostępny bez dodatkowych modułów, co czyni go rozsądnym ustawieniem domyślnym, gdy jeszcze nie wiesz, co obraz zawiera. Gdy liczy się rozmiar pliku, format pikseli zwróconego TBitmap mówi ci, który kodek jest odpowiedni. Bitmapa 32-bitowa niesie kanał alfa; PNG zachowuje go bez strat. Duży obraz 24-bitowy o ciągłej tonacji jest kandydatem na JPEG. Obrazy mniejsze albo rysowane ograniczoną paletą zwykle lepiej zostawić jako BMP, niż przepuszczać przez JPEG, który przy niskich ustawieniach jakości dokłada artefakty blokowe, a przy wysokich niewiele oszczędza

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: bezstratny, bez dodatkowych modułów
  end;
end;

W praktyce wyborem formatu steruje Bmp.PixelFormat i wymiary. Jeśli PixelFormat = pf32bit, potrzebujesz formatu niosącego alfę; PNG jest oczywistym wyborem, choć w starszych wersjach Delphi wymaga modułu PNGImage. Dla obrazów 24-bitowych szerszych niż mniej więcej 300 pikseli JPEG przy jakości 85 daje trzykrotną redukcję rozmiaru wobec BMP, bez zauważalnej straty w większości treści fotograficznej. Poniżej tego progu BMP jest porównywalny rozmiarowo i całkowicie unika decyzji o jakości

Co BitmapCount liczy, a czego nie

PDF rozróżnia obrazowe XObject i grafikę wektorową rysowaną operatorami ścieżek. Strona, która wygląda na wizualnie złożoną, może zwrócić BitmapCount równe zero, jeśli każdy element jest wektorowy. Strony skanowane niemal zawsze zwracają dokładnie jeden: skaner zapisuje cały skan jako pojedynczy pełnostronicowy obrazowy XObject w rozdzielczości, na jaką był ustawiony. Strony mieszające skład tekstowy z osadzonymi zdjęciami zwracają jeden wpis na zdjęcie. Ozdobne linie, cieniowane tła i obramowania tabel zwykle nie pojawiają się w liczniku bitmap w ogóle

Co obejmuje BitmapCount na stronie PDF w PDFium: liczy obrazowe XObject, takie jak skany i zdjęcia, podczas gdy tekst wektorowy, linie, cieniowane wypełnienia i obrazy wbudowane pozostają poza licznikiem
Strona skanowana zwraca dokładnie jeden pełnostronicowy obrazowy XObject, podczas gdy strona czysto wektorowa może wyglądać na zatłoczoną, a zwrócić licznik zero

Licznik nie obejmuje też obrazów wbudowanych, rzadko używanej konstrukcji PDF, w której dane obrazu są osadzone bezpośrednio w strumieniu treści strony, a nie jako nazwany XObject. Wypadają one poza to, co udostępnia to API; w prawdziwych dokumentach są na tyle rzadkie, że większość narzędzi do wydobywania po prostu ich nie obsługuje

Jeden szczegół warto trzymać w głowie: BitmapCount, które czytasz, dotyczy strony bieżącej wedle ostatniego przypisania PageNumber. Jeśli twój kod rozgałęzia się albo wywołuje jakąkolwiek funkcję zmieniającą PageNumber między liczeniem a pobieraniem, możesz odczytać mniej obrazów, niż zarezerwowałeś na nie miejsca, albo zaindeksować poza koniec. Trzymaj odczyt licznika i pętlę Bitmap[] na tej samej stronie, nie dotykając pomiędzy nimi PageNumber

Użycie TPdfView w aplikacji formularzowej

Komponent TPdfView udostępnia te same właściwości BitmapCount i Bitmap[], ale strona, z której czyta, to aktualnie wyświetlana strona widoku, a nie TPdf.PageNumber. Te dwa wskaźniki stron są niezależne; ustawienie jednego nie przesuwa drugiego. W aplikacji formularzowej VCL z żywą przeglądarką możesz wywołać Pdf.PageNumber := N, żeby sterować wydobywaniem przez TPdf, podczas gdy przeglądarka zostaje tam, dokąd użytkownik ostatnio przewinął. Ten rozdział jest zamierzony i utrzymuje stan wyświetlania przeglądarki w czystości, gdy w tle biegnie wydobywanie

Pamięć i wydajność w zadaniach wsadowych

W dużym archiwum główną rzeczą do pilnowania jest budżet pamięci. Każde wywołanie Bitmap[] przydziela nowy TBitmap na stercie, a na skanowanej stronie 300 DPI to bez trudu 25 MB surowych danych pikseli przed jakimkolwiek kodowaniem. Jeśli przetwarzasz strony w ciasnej pętli bez zwalniania między iteracjami, zbiór roboczy rośnie liniowo z liczbą obrazów. Właściwy kształt jest zawsze taki: pobierz jedną bitmapę, zrób, co potrzebujesz, zwolnij ją, pobierz następną. Jeśli musisz trzymać naraz referencje do kilku bitmap na potrzeby kroku porównania, policz je najpierw przez BitmapCount i przydziel kontener odpowiednio, a potem zwalniaj każdą, gdy tylko z nią skończysz, zamiast odkładać sprzątanie na koniec dokumentu. Na dokumencie z 500 skanowanymi stronami ta różnica może oznaczać różnicę między 25 MB a 12 GB szczytowego RSS

Pokazane tutaj właściwości BitmapCount i Bitmap[] są częścią PDFium Component dla Delphi i C++Builder