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ą
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
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