Dwa zgłoszenia przychodzą w tygodniu po wydaniu funkcji kompresji: zeskanowana umowa ma teraz schodkowe, mesiste kształty liter, a przezroczyste logo na stronie tytułowej siedzi w bladym halo. PDFiumPas odpowiada na oba w jednym miejscu. TPdf.OptimizeImages mierzy każdy obraz, zanim go pomniejszy, potem dobiera jądro resamplingu i akumuluje kolor w formie świadomej alfy
Nie zawsze tak było. Przed v3.100.0 ta sama metoda pomniejszała każdy obraz niebilevelowy stałym krokiem najbliższego sąsiada, czyli dokładnie algorytmem, który rodzi oba zgłoszenia: próbkowała punktowo jeden piksel źródła na piksel wyjścia i traktowała RGB leżący pod w pełni przezroczystym pikselem tak, jakby czytelnik miał go kiedykolwiek zobaczyć. Przepisanie w v3.100.0 zastępuje tę pojedynczą ścieżkę pięcioma jądrami, zmierzoną regułą wyboru i jawnym budżetem pamięci roboczej
Dlaczego pomniejszanie czyni zeskanowany tekst postrzępionym?
Bo próbkowanie punktowe odpowiada na złe pytanie. Gdy skan 300 DPI jest przeliczany na 150 DPI, każdy piksel docelowy stoi za blokiem dwóch na dwa piksele źródła, a najbliższy sąsiad zatrzymuje jeden z czterech i wyrzuca resztę. Który przeżyje, zależy od zaokrąglenia, więc krawędź kresek gładko wygładzona w źródle staje się rzutem monetą na piksel. Wynik to klasyczne aliasowe schodki wzdłuż krawędzi glifów, plus moiné na obszarach rastra, gdzie odrzucone próbki akurat niosły wzór. W PDF ma to większe znaczenie niż na ekranie, bo szkoda jest trwała. XObject obrazu niesie swoje dane próbek razem z /Width, /Height i /BitsPerComponent (ISO 32000-1 §8.9.5), a resampling przepisuje wszystkie trzy wewnątrz pliku. Zły zoom w przeglądarce to klatka, którą można przerysować, a PDFiumPas ma na to osobną maszynerię w cache renderowania i wydajności zoomu. Złe pomniejszenie to nowy dokument, który wręczasz klientowi
Jak PDFiumPas mierzy szczegóły i dobiera jądro
PDFiumPas decyduje per obraz, nie per dokument. Zanim wybierze jądro, liczy znormalizowany wynik szczegółów luminancji z ograniczonej siatki próbkowania: kroki poziome i pionowe to (Width + 63) div 64 i (Height + 63) div 64, więc skan o 12000 pikseli i miniatura o 300 pikseli kosztują w przybliżeniu ten sam przebieg 64 na 64. W każdej próbkowanej pozycji sumuje różnicę bezwzględną do sąsiada po prawej i sąsiada poniżej, na maksymalnie trzech kanałach, potem dzieli przez liczbę próbek razy 255. Wynik ląduje w zakresie od 0 do 1, gdzie płaska grafika biznesowa siedzi przy zerze, a gęsta tekstura fotograficzna pnie się w górę
Drabinka wyboru biegnie potem w stałej kolejności. Jeśli ResampleFilter ma wartość inną niż pirfAdaptive, ten filtr jest używany dosłownie. W przeciwnym razie: treść 1-bitowa bierze pirfBilevel; ContentClass równy piccLineArt bierze pirfBox; współczynnik skali 4 lub więcej również bierze pirfBox, bo przy takiej redukcji średnia obszarowa jest zarazem najtańszą i najpoprawniejszą odpowiedzią; piccPhoto, wynik szczegółów 0.08 lub wyższy albo PreferredQuality 0.9 lub wyższy bierze pirfLanczos z jego trójpłatowym jądrem; skala 2 lub więcej albo jakość 0.7 lub wyższa bierze pirfBicubic o promieniu 2; wszystko, co zostaje, bierze pirfBilinear. Ponieważ TPdfImageOptimizeOptions.Default ustawia PreferredQuality na 0.85, domyślne uruchomienie nigdy nie cofa się do bilinear, chyba że redukcja jest łagodna, a treść płaska
uses
PDFium;
procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InputFile;
// Domyślne: TargetDpi 150, MinDpiRatio 1.5, PreserveBilevel True,
// MinDimension 8, pirfAdaptive, piccAuto, jakość 0.85, budżet 64 MiB.
Options := TPdfImageOptimizeOptions.Default;
Options.TargetDpi := 150;
Options.MinDpiRatio := 1.5;
Options.ContentClass := piccAuto;
Options.PreferredQuality := 0.85;
if Pdf.OptimizeImages(Options, Report) and (Report.OptimizedCount > 0) then
Pdf.SaveAs(OutputFile);
finally
Pdf.Free;
end;
end;
Obraz jest dotykany tylko wtedy, gdy większy z jego DPI rozmieszczenia poziomego i pionowego, podzielony przez TargetDpi, osiąga MinDpiRatio. Ta straż istnieje po to, by zdjęcie 160 DPI celujące w cel 150 DPI nie było kodowane ponownie dla sześciu procent zysku, który kosztuje generację jakości. Obrazy poniżej MinDimension na dowolnej osi, domyślnie 8, są pomijane jako ikony albo linie
Dlaczego przezroczyste logo nabywa białej obwódki?
Bo kolor pod w pełni przezroczystym pikselem jest dowolny, a zwykła średnia ważona pozwala mu głosować. Wyeksportuj logo z narzędzia projektowego, a niewidoczny margines jest często biały, czarny albo taki, jaki było płótno; kanał alfa go ukrywa, a proste sumowanie po stopie jądra natychmiast wmiesza go z powrotem w widoczną krawędź. PDFiumPas unika tego, akumulując próbki BGRA w postaci premultiplied i cofając premultiplied dopiero na pikselu docelowym
Konkretnie, każda wnosząca próbka dodaje channel * alpha * weight do akumulatora koloru, alpha * weight do akumulatora alfy, a weight do sumy wag. Kolor docelowy jest potem dzielony przez akumulator alfy, a nie przez sumę wag, i to jest krok, który się liczy: dzielenie przez sumę wag ciągnęłoby kolor w stronę niewidocznych pikseli, podczas gdy dzielenie przez zakumulowaną alfę odtwarza kolor, na którym widoczne próbki faktycznie się zgodziły. Alfa docelowa to osobna wielkość, 255 * AlphaSum / WeightSum. Formaty bez alfy dzielą przez sumę wag jak zwykle, bajt wypełnienia celu FPDFBitmap_BGRx jest zapisywany jako stałe 255, a każdy kanał jest zaciskany do 0–255 przed zapisem. Ta alfa pochodzi normalnie z wpisu miękkiej maski w słowniku obrazu (ISO 32000-1 §11.4), który PDFium już skomponował do bufora BGRA, który resampler dostaje
// Kształt wewnętrznej pętli akumulacji, na każdą wnoszącą próbkę źródła
if SrcFormat = FPDFBitmap_BGRA then
Alpha := PByte(PAnsiChar(Pixel) + 3)^ / 255
else
Alpha := 1;
for Channel := 0 to Min(BytesPerPixel, 3) - 1 do
Accumulated[Channel] := Accumulated[Channel] +
PByte(PAnsiChar(Pixel) + Channel)^ * Alpha * Weight;
AlphaSum := AlphaSum + Alpha * Weight;
WeightSum := WeightSum + Weight;
// ... a na pikselu docelowym, unpremultiply względem sumy alfy
if SrcFormat = FPDFBitmap_BGRA then
begin
if Abs(AlphaSum) > 1E-12 then
ValueSum := Accumulated[Channel] / AlphaSum
else
ValueSum := 0;
end
else
ValueSum := Accumulated[Channel] / WeightSum;
Trzymanie grafiki liniowej 1-bitowej poza strefą szarości
Każde jądro ciągłe zastosowane do skanu bilevelowego produkuje szarość, a szarość jest dokładnie tym, czego obraz w stylu faksu nie może zawierać. PDFiumPas zostawia więc obrazy 1-bitowe w spokoju domyślnie: PreserveBilevel ma wartość True w TPdfImageOptimizeOptions.Default, a takie obrazy lądują w SkippedCount nietknięte. Ustaw False, a ścieżka pirfBilevel przejmuje stery zamiast jądra wygładzającego. Przechodzi dokładny prostokąt źródła pokrywający każdy piksel docelowy, uśrednia luminancję z wagami 0.114, 0.587 i 0.299 w kolejności pamięci BGR i zaciina wynik progiem 127.5 na płaskie 0 albo 255. Nic pośredniego nie da się zapisać, więc krawędzie zostają ostre i żadne szare halo nie powstaje wokół cienkich kresek; kanał alfa źródła BGRA jest uśredniany normalnie, a cel BGRx dostaje stałe 255. Jeśli potrzebujesz pikseli leżących pod spodem, a nie mniejszego dokumentu, ekstrakcja obrazów z dokumentów PDF to osobna ścieżka
Co się dzieje, gdy obraz przekracza budżet pamięci roboczej?
Pozostaje dokładnie taki, jaki był, i jest liczony. MaxWorkingBytes domyślnie wynosi 64 MiB i jest egzekwowany dwa razy. Zanim bitmapa docelowa powstanie, PDFiumPas odrzuca obraz, jeśli szerokość razy wysokość razy bajty na piksel przekraczają budżet. Po udanym FPDFBitmap_CreateEx sprawdza jeszcze raz, używając realnego stride razy wysokość, bo wypełnienie wierszy może pchnąć alokację ponad limit, który naiwny iloczyn zdawał się mieścić. Każde z tych odrzuceń niszczy cel i nie zwraca nic. Miej jasność co do degradacji, którą to implikuje: obraz ponad budżet nie jest resamplowany w niższej jakości i nie jest dzielony na kafle. Oryginał zostaje w dokumencie, BudgetExceededCount i SkippedCount oba rosną, więc uruchomienie może raportować sukces, podczas gdy dokument jest zoptymalizowany tylko częściowo. To celowe zachowanie fail-safe, ale oznacza, że raport nie jest lekturą opcjonalną. Osobny tryb awarii też istnieje: obrazy, których bitmapy PDFium nie potrafi wyprodukować wcale, jak CMYK, JPX, JBIG2 albo źródła z maską, zwiększają zamiast tego FailedCount i równie dobrze pozostają nietknięte
procedure OptimizeBatch(const Files: array of string);
var
Pdf: TPdf;
Options: TPdfImageOptimizeOptions;
Report: TPdfImageOptimizeReport;
I: Integer;
begin
Options := TPdfImageOptimizeOptions.Default;
Options.PreserveBilevel := False; // użyj głosu obszarowego bilevel
Options.ContentClass := piccPhoto; // wymuś Lanczos dla zestawów zdjęć
Options.MaxWorkingBytes := 256 * 1024 * 1024; // zapas na duże skany
Pdf := TPdf.Create(nil);
try
for I := Low(Files) to High(Files) do
begin
Pdf.FileName := Files[I];
if not Pdf.OptimizeImages(Options, Report) then
begin
WriteLn('optimize failed: ', Report.ErrorMessage);
Continue;
end;
if Report.BudgetExceededCount > 0 then
WriteLn(Files[I], ': ', Report.BudgetExceededCount,
' image(s) over budget and kept at full size');
if Report.FailedCount > 0 then
WriteLn(Files[I], ': ', Report.FailedCount,
' image(s) could not be decoded to a bitmap');
if Report.OptimizedCount > 0 then
Pdf.SaveAs(ChangeFileExt(Files[I], '.opt.pdf'));
end;
finally
Pdf.Free;
end;
end;
Czytanie raportu, zanim wyślesz plik
TPdfImageOptimizeReport jest zbudowany do diagnozowania, nie tylko do logowania. Obok OptimizedCount, SkippedCount i FailedCount wystawia jeden licznik na jądro, więc BoxFilterCount, BilinearFilterCount, BicubicFilterCount, LanczosFilterCount i BilevelFilterCount mówią ci, co reguła adaptacyjna faktycznie wywnioskowała o twoim korpusie. Wynik w całości boxowy znaczy, że redukcje były strome albo treść została sklasyfikowana jako line art; wynik w całości Lanczosowy na dokumencie uznanym za line art to znak, że ContentClass powinien być ustawiony jawnie. AverageDetailScore to liczba do porównania z progiem 0.08 dla Lanczosa przy strojeniu PreferredQuality, a PeakWorkingBytes pokazuje, ile z MaxWorkingBytes uruchomienie naprawdę potrzebowało. Niepoprawne opcje zawodzą głośno, nie po cichu: nie dodatnie TargetDpi, MinDpiRatio poniżej 1, PreferredQuality poza zakresem 0–1 albo nie dodatnie MaxWorkingBytes rzuca EPdfError, zanim jakakolwiek strona zostanie dotknięta. A OptimizeImages edytuje tylko dokument w pamięci; każda zmodyfikowana strona jest zatwierdzana przez FPDFPage_GenerateContent, po czym i tak sam wywołujesz SaveAs. Żeby na oko zobaczyć, co się zmieniło, wyrenderuj dokumenty przed i po do bitmap tak, jak opisano w konwersji stron PDF na obrazy JPEG, i porównaj je przy pełnym zoomie
Adaptacyjny resampling to jedna z tych funkcji, które są niewidzialne, gdy działają, i generują zgłoszenia do wsparcia, gdy nie działają, dlatego pomiar, obsługa alfy i budżet pamięci musiały wylądować razem, a nie jako trzy osobne udoskonalenia. Jeśli oceniasz to do produktu w Delphi, C++Builder albo Lazarus, pełna powierzchnia API i szczegóły licencjonowania są na stronie komponentu PDFiumPas Delphi PDFium