losLab PDF Library for Delphi kompresuje zeskanowaną stronę z Mixed Raster Content przez TPDFlib.CompressPageAsMRC. Metoda renderuje stronę, proguje pełnorozdzielczościową 1-bitową maskę pierwszego planu, zbiją resztę do tła JPEG i odbudowuje stronę jako dwa obrazy: tło /DCTDecode i maskę szablonową /CCITTFaxDecode malowaną bieżącym kolorem wypełnienia
Strona, dla której to istnieje, to ta, którą pełno w każdym archiwum dokumentów: 300 DPI kolorowy skan faktury albo umowy, schowany w jednym wielkim JPEG-u. Zakoduj go jakością, przy której tekst zostaje czytelny, a plik jest duży; zejdź z jakością, a litery obrosną poświatami i rozmazanymi szeryfami, bo to, w czym JPEG jest najgorszy, to ostra czarna krawędź na białym papierze. Mixed Raster Content przestaje prosić jeden kodek o dwie roboty
Dlaczego pojedynczy JPEG robi skanowany tekst rozmyty i wielki zarazem?
Pojedynczy JPEG radzi sobie ze skanowanym tekstem źle, bo kodek DCT wydaje bity na gładkie gradienty, a twarde krawędzie przybliża wysokoczęstotliwościowymi współczynnikami, które kwantyzacja wyrzuca w pierwszej kolejności. Tekst na skanie to prawie same twarde krawędzie. Utrzymanie ich czystości wymusza wysoką jakość na całej stronie, łącznie z fakturą papieru, logo i plamą po kawie, którym to nie było potrzebne
Pomysł MRC dzieli stronę według tego, czym każdy region faktycznie jest. Tusz staje się obrazem dwupoziomowym, w którym faksowy kodek, jak CCITT, rejestruje biegi czarnych i białych pikseli bezstratnie i bardzo zwięźle. Cała reszta, ton papieru, zdjęcia, kolorowe tła, staje się obrazem o ciągłym tonie, który można przechować przy ułamku rozdzielczości, bo nikt nie czyta tła pod 300 DPI. Warstwa dwupoziomowa trzyma tekst ostro w pełnej rozdzielczości; warstwa tła trzyma kolor tanio
Jak CompressPageAsMRC dzieli stronę na dwie warstwy?
TPDFlib.CompressPageAsMRC prowadzi jeden stały potok per strona. Renderuje stronę z żądanym DPI do bitmapy 24-bitowej, klasyfikuje każdy piksel ważoną luminancją Rec. 601 (299/587/114 promille) względem Threshold i zapisuje wynik do 1-bitowej maski w tej samej rozdzielczości. Równolegle kurczy ten sam render przez BGScale uśrednianiem po pudełku, więc BGScale równy 4 zamienia każdy blok pikseli 4×4 w jeden piksel tła
Treść strony jest potem wymieniana. Pierwotne operatory są usuwane, zredukowane tło jest wbudowywane jako obraz /DCTDecode w /DeviceRGB (ISO 32000-1 §7.4.8), maska jest kodowana bibliotecznym enkoderem CCITT jako obraz /CCITTFaxDecode (§7.4.6) z flagą /ImageMask true, a oba są rysowane na całym pudełku strony z maską na wierzchu. Sygnatura jest krótka:
function CompressPageAsMRC(Page: Integer; DPI: Double;
BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
Pagejest numerowany od 1 i musi mieścić się wPageCountDPIto rozdzielczość renderu rastrowego źródła, od 50 do 1200BGScaleto współczynnik redukcji tła, od 1 do 8JPEGQualityto jakość JPEG tła, od 1 do 100Thresholdto próg luminancji, od 1 do 255; piksele na progu i jaśniejsze to papier, ciemniejsze to tuszOptionsjest zarezerwowany i musi być 0
Typowa partia na zeskanowanym pliku wygląda tak. Wywołuj SetFillColor tuż przed każdą stroną, z powodu, który wyjaśnia następna sekcja
uses
SysUtils, PDFlibrary;
procedure CompressScannedFile(const SrcFile, DstFile: string);
var
Lib: TPDFlib;
P, Replaced: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(SrcFile, '') <> 1 then
raise Exception.CreateFmt('Cannot open %s', [SrcFile]);
for P := 1 to Lib.PageCount do
begin
Lib.SetFillColor(0, 0, 0); // kolor tuszu warstwy szablonu
// maska 300 DPI, tło 75 DPI, jakość JPEG 60, próg przy luminancji 160
Replaced := Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0);
if Replaced = 0 then
raise Exception.CreateFmt('MRC failed on page %d', [P]);
end;
if Lib.SaveToFile(DstFile) <> 1 then
raise Exception.CreateFmt('Cannot save %s', [DstFile]);
finally
Lib.Free;
end;
end;
Dlaczego pierwszy plan jest malowany bieżącym kolorem wypełnienia?
Warstwa pierwszego planu to maska szablonowa, a maska szablonowa nie ma własnego koloru. ISO 32000-1 §8.9.6.2 definiuje obraz z /ImageMask true jako 1-bitowy kształt: wartości próbek znaczą, gdzie idzie farba, a farbą jest dowolny kolor wypełnienia aktualny w stanie grafiki. To dokładnie czyni tę warstwę tanim i dlatego też CompressPageAsMRC nie przyjmuje parametru koloru
TPDFlib.CompressPageAsMRC maluje maskę kolorem ustawionym przez SetFillColor, więc kolejność wywołań ma znaczenie. Malarz emituje kolor wypełnienia leniwie: SetFillColor zapamiętuje kolor i znaczy go jako zmieniony, a rysowanie szablonu pisze operator rg wewnątrz własnego bloku q/Q tylko dopóki ta zmiana wciąż czeka. Narysuj pomiędzy kształt wypełniony, a oczekująca zmiana zostanie skonsumowana przez ten kształt. Ustawianie koloru bezpośrednio przed każdą stroną, jak w partii wyżej, usuwa zgadywanie. Czarny to normalny wybór; ciemny granat pasuje do formularzy pisanych niebieskim tuszem, gdy chcesz, żeby wynik mniej wyglądał jak ksero
// Formularz pisany niebieskim długopisem: warstwa tuszu w ciemnym granacie
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);
Rozmieszczenie podąża za początkiem układu współrzędnych biblioteki. CompressPageAsMRC czyta GetOrigin i rysuje oba obrazy od górnej krawędzi pudełka strony: współrzędna górna 0, gdy początek jest w lewym górnym rogu (SetOrigin(1)), współrzędna górna PageHeight przy domyślnym początku w lewym dolnym rogu. Nie musisz nic dostosowywać, ale to wyjaśnia, dlaczego warstwy lądują poprawnie niezależnie od tego, jakiego początku twój potok już używał
Jedną konsekwencję semantyki szablonu warto powiedzieć wprost: każdy piksel sklasyfikowany jako tusz bierze ten sam kolor. Czerwona pieczątka, niebieski podpis i czarny tekst zasadniczy lądują w jednym kolorze wypełnienia na warstwie pierwszego planu. Tylko tło JPEG zachowuje pod spodem blady, niskorozdzielczościowy ślad pierwotnych kolorów
Wybór DPI, BGScale, Threshold i jakości JPEG
Wartość DPI decyduje o ostrości tekstu, a 300 to rozsądny domyślny dla skanów biurowych. Maska jest trzymana w pełnej rozdzielczości renderu, więc DPI ustawia, jak cienka kreska przeżyje progowanie. Wejście wyżej niż rozdzielczość pierwotnego skanu nic nie daje, bo renderer tylko nadpróbkowywałby wbudowany obraz skanu. Pamięć to drugi limit: strona US Letter pod 300 DPI renderuje się do 2550×3300 pikseli, czyli 25 245 000 bajtów 24-bitowych danych pikselowych, zanim maska i tło zostaną wyprowadzone, a ta sama strona przy suficie 1200 DPI to 10200×13200 pikseli, około 404 MB
BGScale wymienia detal tła na rozmiar. Pod 300 DPI współczynnik 4 daje tło 75 DPI, 638×825 pikseli dla Letter, co aż nadto wystarcza dla tonu papieru i płaskich obszarów koloru. Strony ze zdjęciami albo rycinami rastra, które czytelnik faktycznie będzie oglądał, zasługują na 2; czysty tekst na białym papierze zniesie 6 albo 8, bo tło jest ledwo więcej niż zabarwieniem
Threshold to parametr, który zmienia wygląd strony. Podniesienie go klasyfikuje więcej średnio szarych pikseli jako tusz, co pogrubia cienkie kreski i ślady ołówka, ale też wciąga szum skanera i cień papieru jako drobinki. Obniżenie daje czystszy papier i cieńsze, miejscami przerwane litery. Wartości około 128-170 to rozsądny zakres startowy dla czarnego tekstu na białym; ten sam kompromis omawia renderowanie stron PDF do 1-bitowej monochromatyki w Delphi, gdzie ten sam próg luminancji steruje RenderPageToMonochromeFileEx. W przeciwieństwie do tamtej metody CompressPageAsMRC nie ma przebiegu usuwania drobin, więc skany szumne lepiej obsługują lekko niższym progiem
JPEGQuality znaczy mniej niż przy skanie jednego obrazu, bo tło nie niesie już żadnych krawędzi liter. Jakość 50-70 jest zwykle niewidoczna na tle 75 DPI za ostrą maską
Co mówi wartość zwracana, a czego nie mówi?
TPDFlib.CompressPageAsMRC zwraca rozmiar w bajtach treści strony, którą wymienił, i 0, gdy coś zawiodło. To wskaźnik sukcesu, nie miara oszczędności. Na typowej zeskanowanej stronie pierwotny strumień treści ma kilkadziesiąt bajtów, ledwo tyle operatorów, by umieścić jeden obraz, więc mała liczba dodatnia to normalny wynik, nawet gdy strona chudnie o megabajty. Strona, której pierwotny strumień treści był pusty, zgłasza 0, choć została odbudowana
Zachowanie przy porażce nie jest atomowe i to jest część, którą trzeba obwieść projektem. Sprawdzenia argumentów, render i kodowanie JPEG biegną, zanim strona zostanie dotknięta, więc BGScale poza zakresem, JPEGQuality równy 101 albo porażka renderu zostawiają stronę dokładnie taką, jaką była. Ale gdy pierwotna treść została już wyczyszczona, niepowodzenie wbudowania jednej z warstw albo jej narysowania też zwraca 0, i w tym momencie strona może być już częściowo odbudowana. Każde 0 traktuj jako powód do wyrzucenia dokumentu z pamięci zamiast go zapisać i nigdy nie pisz wyniku nad plikiem źródłowym
Żeby dowiedzieć się, co konwersja faktycznie zaoszczędziła, zmierz dokument. Porównanie długości SaveToString przed i po to najszybszy test; a na odpowiedź per kategoria: audyt rozmiaru pliku PDF z AuditDocumentSpace pokaże, czy ciężar przepłynął z obrazów tak, jak należało oczekiwać
function MRCSavings(Lib: TPDFlib): Int64;
var
Before: Int64;
P: Integer;
begin
Before := Length(Lib.SaveToString);
for P := 1 to Lib.PageCount do
begin
Lib.SetFillColor(0, 0, 0);
if Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0) = 0 then
raise Exception.CreateFmt('Page %d failed; discard this document', [P]);
end;
Result := Before - Length(Lib.SaveToString);
end;
Gdzie ten dwuwarstwowy MRC zostaje w tyle za ITU-T T.44
CompressPageAsMRC implementuje celowo uproszczony MRC. Pełny model w ITU-T T.44 używa trzech warstw na pasek: obrazu tła, obrazu pierwszego planu niosącego kolory tuszu i maski dwupoziomowej wybierającej między nimi piksel po pikselu, z miejscem na kilka par maska-pierwszy plan na jednej stronie. W terminach PDF trzecia warstwa byłaby niskorozdzielczościowym kolorowym obrazem pierwszego planu z jawną maską (§8.9.6.3) zamiast szablonu malowanego jednym kolorem. PDF Library for Delphi pomija kolorową warstwę pierwszego planu, co czyni wynik tak małym i jest też powodem, dla którego kolorowy tusz zapada się w kolor wypełnienia
- Metoda pasuje do czarnego albo jednokolorowego tekstu na zeskanowanym papierze; dokumenty z kodowaniem kolorem, tekst z podświetleniem i kolorowe pieczątki tracą kolor tuszu
- Odbudowana strona to czysty raster: każdy prawdziwy tekst, grafika wektorowa albo zaznaczalne znaki na pierwotnej stronie stają się pikselami, więc stosuj to na skanach, nie na stronach urodzonych cyfrowo, gdzie właściwymi narzędziami są subsetowanie fontów i redukcja rozdzielczości obrazów
- Maska używa bibliotecznego enkodera CCITT w dwuwymiarowym trybie T.4, zapisywanego jako
/CCITTFaxDecodez/K 1; enkoder ma też tryb Group 4 przezAddImageCCITTFromBitmapStream, aleCompressPageAsMRCgo nie wystawia i nie używa JBIG2 - Adnotacje i pola formularzy nie są spłaszczane; zostają jako obiekty nad odbudowaną treścią
- Tło jest
/DeviceRGB, więc wynik nie nadaje się do procesów PDF/X-1a, a samSetFillColorjest odrzucany w trybie PDF/X-1a - Skan, który i tak potrzebuje OCR, powinien zostać rozpoznany przed MRC, dopóki pierwotne piksele jeszcze istnieją w pełnej głębi kolorów
FAQ
Czy CompressPageAsMRC trzyma tekst przeszukiwalny?
Nie. CompressPageAsMRC wymienia treść strony na dwa obrazy, więc wszystkie operatory tekstu na pierwotnej stronie, łącznie z niewidzialną warstwą tekstu OCR, są usuwane razem z nią. Uruchom OCR na skompresowanej stronie po fakcie albo dodaj warstwę tekstu z powrotem po kompresji
Czy można kompresować tylko niektóre strony dokumentu?
Tak. CompressPageAsMRC działa na jednej stronie numerowanej od 1 na wywołanie, więc wybierasz strony i możesz nawet różnicować BGScale i Threshold per strona, na przykład współczynnik 2 dla wklejki ze zdjęciem i 6 dla stron z czystym tekstem
Jaka powinna być wartość parametru Options?
Przekazuj 0. Options jest zarezerwowany na przyszłość w bieżącym wydaniu i żaden bit nie jest zdefiniowany, więc wartość niezerowa nie niesie dzisiaj żadnego znaczenia
CompressPageAsMRC trafił do losLab PDF Library for Delphi w v3.561.0 razem z blokami, z których jest złożony: renderem, JPEG, CCITT i maską szablonową, wszystkim dostępnym jako metody publiczne, gdybyś potrzebował wariantu potoku. Biblioteka, wsparcie C++Builder i pełna referencja API są na stronie produktu PDF Library for Delphi