Artykuł techniczny

Mixed Raster Content w Delphi: skan PDF jako JPEG + CCITT

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

Skan jako pojedynczy JPEG kontra podział warstwowy Mixed Raster Content tworzony przez PDFlibPas CompressPageAsMRC w Delphi: kodek DCT rozmazuje twarde krawędzie liter i wymusza wysoką jakość na całej stronie, podczas gdy tusz zostaje bezstratnym 1-bitowym szablonem CCITT pod 300 DPI, a ton papieru tłem JPEG DeviceRGB pod 75 DPI
Mixed Raster Content przestaje prosić jeden kodek o dwie roboty: warstwa dwupoziomowa trzyma litery ostre w pełnej rozdzielczości, a tło trzyma kolor za ułamek kosztów, więc jakość 50-70 jest zwykle niewidoczna

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

Przepływ danych CompressPageAsMRC w PDFlibPas dla Delphi: jeden render 24-bitowy pod 300 DPI jest progowany luminancją Rec. 601 do pełnorozdzielczościowego szablonu CCITTFaxDecode K 1 ImageMask, uśredniany po pudełku przez BGScale 4 do tła DCTDecode DeviceRGB, a oba obrazy zastępują pierwotne operatory strony z maską na wierzchu
Obie warstwy pochodzą z jednego renderu: DPI decyduje, jak cienka kreska przeżyje progowanie, BGScale wymienia detal tła od 2 dla fotografii do 8 dla czystego tekstu, a odbudowana strona maluje szablon bieżącym kolorem wypełnienia

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;
  • Page jest numerowany od 1 i musi mieścić się w PageCount
  • DPI to rozdzielczość renderu rastrowego źródła, od 50 do 1200
  • BGScale to współczynnik redukcji tła, od 1 do 8
  • JPEGQuality to jakość JPEG tła, od 1 do 100
  • Threshold to próg luminancji, od 1 do 255; piksele na progu i jaśniejsze to papier, ciemniejsze to tusz
  • Options jest 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

Sekwencja leniwej emisji koloru wypełnienia dla warstwy szablonowej MRC w PDFlibPas w Delphi: SetFillColor zapisuje kolor jako oczekującą zmianę, rysowanie szablonu ImageMask pisze operator rg wewnątrz własnego bloku q i Q, dopóki zmiana wciąż czeka, a przeplatający kształt wypełniony konsumuje oczekującą zmianę zamiast niego
Maska szablonowa nie posiada koloru, więc każdy piksel tuszu bierze jeden kolor wypełnienia: czerwona pieczątka, niebieski podpis i czarny tekst zlewają się razem, dlatego SetFillColor trzeba wywoływać bezpośrednio przed każdą stroną
// 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 /CCITTFaxDecode z /K 1; enkoder ma też tryb Group 4 przez AddImageCCITTFromBitmapStream, ale CompressPageAsMRC go 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 sam SetFillColor jest 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