Artykuł techniczny

Współdzielenie słowników symboli JBIG2 między stronami w Delphi

Pięćdziesięciostronicowa zeskanowana umowa powtarza ten sam alfabet na każdej stronie, ale koder JBIG2, który buduje jeden słownik symboli na obraz, uczy się tego alfabetu pięćdziesiąt razy z osobna. HotPDF, natywny komponent PDF dla Delphi i C++Buildera, może zamiast tego gromadzić jeden współdzielony słownik symboli w obrębie całego dokumentu i awansować go do pojedynczego strumienia /JBIG2Globals na poziomie dokumentu, więc własny strumień JBIG2 każdej strony jedynie odwołuje się do identyfikatorów symboli zamiast przechowywać własną kopię alfabetu

Ten tekst celowo pozostaje wąski i opisuje wyłącznie to, jak HotPDF buduje to współdzielenie między stronami wewnętrznie — podstawy JBIG2, porównanie z CCITT i kompromisy Lossless kontra LossyLevel mieszkają już w towarzyszącym artykule o natywnej kompresji bilevel JBIG2 w Delphi, którego lekturę ten tekst zakłada

Dlaczego kompresja JBIG2 na stronę wciąż powtarza ten sam koszt?

Odpowiedź jest taka, że nic nie przenosi stanu między wywołaniami. Za każdym razem, gdy koder HotPDF buduje słownik symboli dla jednego obrazu, ten słownik jest ograniczony do tego pojedynczego wywołania AddImage: przebieg dopasowywania kształtów zaczyna się od zera, każdy glif na stronie jest klasyfikowany jako nowy, a wynikowe bitmapy zostają zakodowane arytmetycznie i zapisane od nowa. Podaj temu samemu koderowi pięćdziesiąt stron złożonych tą samą czcionką, a chętnie powtórzy cały ten przebieg uczenia pięćdziesiąt razy, ponieważ z jego punktu widzenia każda strona to niepowiązany obraz, który akurat wygląda podobnie. UseSymbolDictionary na stronę już wyprzedza z dużym zapasem płaskie kodowanie regionu generycznego na pojedynczej stronie, ale zatrzymuje się dużo poniżej pułapu, jaki prawdziwy wielostronicowy skan zostawia na stole

Jak HotPDF współdzieli jeden słownik symboli między stronami?

Włącz AccumulateGlobalsAcrossPages na THPDFJBIG2Options, a HotPDF utrzyma jeden słownik symboli żywy w pamięci przez cały czas życia dokumentu zamiast odrzucać go po każdym obrazie. Glify każdej kolejnej strony są sprawdzane względem tego bieżącego słownika, zanim cokolwiek zostanie zakodowane na nowo: kształt, który już istnieje, jest ponownie wykorzystywany przez swój identyfikator symbolu, a tylko kształt, którego nikt wcześniej nie widział, zostaje dopisany i zakodowany do słownika. Porównanie ponownie wykorzystuje tę samą logikę tolerancji, którą LossyLevel stosuje na pojedynczej stronie — lekko zaszumiony skan tej samej litery nadal liczy się jako dopasowanie — więc akumulator po cichu nie rozdyma się do jednego wpisu słownika na każdą wariację glifu na poziomie pikseli. Ekstrakcja odbywa się najpierw i zasila to porównanie: HotPDF przechodzi po bitmapie każdej strony i wyciąga spójne kształty przez wypełnianie powodziowe względem czarnych pikseli, ten sam pomysł co ręczne obrysowywanie plam atramentu, i to właśnie te wyekstrahowane kształty, a nie surowe bloki pikseli, są porównywane z bieżącym słownikiem

Jak współdzielony słownik siedzi wewnątrz strumienia /JBIG2Globals

Zgromadzony słownik jest zapisywany jako jeden segment słownika symboli wewnątrz strumienia /JBIG2Globals, trzymany pod stałym numerem segmentu, żeby każda strona mogła wskazywać na ten sam cel. Wewnątrz organizacji osadzonego JBIG2, jaką definiuje ISO 32000-1 §7.4.7, segment regionu tekstowego może wskazać inny segment jako swoje źródło symboli przez pole odwoływanego segmentu w nagłówku segmentu, i to jest dokładnie mechanizm, na którym opiera się HotPDF: strumień globalny niesie ten jeden duży słownik symboli, a własny strumień JBIG2 każdej strony kurczy się do segmentu informacji o stronie plus segmentu regionu tekstowego, którego lista odwołań wskazuje z powrotem na segment globalny. To, co kiedyś było samowystarczalnym strumieniem bitów na stronę, staje się krótką listą pozycji i identyfikatorów symboli, a każda strona zbudowana w ten sposób odwołuje się do identycznego pośredniego obiektu /JBIG2Globals, a nie do jego kopii. Własne pokrycie testów regresyjnych HotPDF sprawdza dokładnie to: zakoduj krótki dokument, w którym każda strona ma inny układ glifów, wczytaj go ponownie i policz, ile odrębnych odwołań do obiektu /JBIG2Globals pojawia się w pliku — jeden dokument, jedno odwołanie do obiektu, niezależnie od tego, ile stron dostarczyło do niego symbole

Włączanie akumulacji słownika symboli między stronami

Przełącznik siedzi na tym samym rekordzie opcji opisanym w towarzyszącym artykule i wymaga zgodności czterech ustawień, zanim akumulacja faktycznie się włączy

var
  Pdf: THotPDF;
  Bmp: TBitmap;
  PageIdx, ImgIdx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.JBIG2Options.Lossless := True;
    Pdf.JBIG2Options.UseSymbolDictionary := True;
    Pdf.JBIG2Options.UseGlobalSegments := True;
    Pdf.JBIG2Options.AccumulateGlobalsAcrossPages := True;  // opt-in, default False
    Pdf.JBIG2Options.UseExternalEncoder := False;            // accumulation needs the native path
    Pdf.JBIG2Options.UseNativeArithmeticFallback := True;
    Pdf.BeginDoc;
    for PageIdx := 0 to ScannedPages.Count - 1 do
    begin
      if PageIdx > 0 then
        Pdf.AddPage;
      Bmp := ScannedPages[PageIdx];             // 1-bit TBitmap for this page
      ImgIdx := Pdf.AddImage(Bmp, icJBIG2);
      Pdf.CurrentPage.ShowImage(ImgIdx, 0, 0, Bmp.Width, Bmp.Height, 0);
    end;
    Pdf.EndDoc;                                  // the shared /JBIG2Globals stream is finalized here
  finally
    Pdf.Free;
  end;
end;

To połączenie nie jest opcjonalną dekoracją. Zewnętrzny szew kodera opisany w artykule o kompresji bilevel — ten, który rejestruje się przez RegisterJBIG2EncoderBackend dla współczynników klasy produkcyjnej — jest zbudowany wokół kodowania na obraz, a własne demonstracje akumulacji i testy regresyjne HotPDF zawsze łączą AccumulateGlobalsAcrossPages z UseExternalEncoder := False. Traktuj to jako twarde wymaganie, a nie sugestię: współdzielenie między stronami to funkcja natywnego kodera, a zarejestrowany zewnętrzny backend po prostu nie jest częścią ścieżki, która buduje współdzielony słownik

O ile faktycznie mniejszy staje się wielostronicowy skan?

Uczciwa odpowiedź zaczyna się od tego, co najpierw nie poruszyło wskaźnika. Wcześniejsze wydanie dodało bufor adresowany treścią dla strumieni /JBIG2Globals — wyszukiwanie kluczowane 64-bitowym skrótem FNV-1a bajtów strumienia, więc dwa obrazy, które akurat wyprodukowały identyczne bajt-w-bajt dane globalne, mogły współdzielić jeden obiekt PDF. Zmierzony na prawdziwym wyjściu, ten bufor ledwie pomógł, ponieważ istniejące wykrywanie duplikatów całych obrazów w HotPDF już zwijało identyczne bajt-w-bajt obrazy, zanim bufor w ogóle dostał szansę zadziałać. Lekcja była taka, że deduplikacja na poziomie strumienia opłaca się dopiero wtedy, gdy dwa naprawdę różne obrazy stron wciąż mogą współdzielić jeden rosnący słownik, co jest dokładnie tym, co dostarcza prawdziwa akumulacja między stronami

Dla tego trudniejszego przypadku własny szacunek inżynieryjny HotPDF umieszcza dodatkową oszczędność na poziomie mniej więcej 30 do 60 procent mniejszej niż osiąga sama deduplikacja na poziomie strumienia, dla typowego wielostronicowego skanu zbudowanego z jednej powtarzającej się czcionki — zakres przesuwa się w zależności od tego, ile z wizualnego słownictwa dokumentu faktycznie się powtarza, ponieważ strona pełna unikalnych diagramów nie daje słownikowi niczego do ponownego wykorzystania. Traktuj to jako cel projektowy, a nie gwarancję dla jakiegokolwiek konkretnego wejścia, i mierz własne dokumenty zamiast ufać pojedynczej liczbie. Demo JBIG2Benchmark dostarczane z HotPDF istnieje dokładnie w tym celu: koduje ten sam wielostronicowy skan na cztery różne sposoby i wypisuje wynikowy rozmiar pliku dla każdej konfiguracji, więc porównanie działa na twojej własnej mieszance skanów, a nie syntetycznej

procedure RunScenario(const Title: string; AccumulateGlobals: Boolean);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.JBIG2Options.Lossless := True;
    Pdf.JBIG2Options.UseSymbolDictionary := True;
    Pdf.JBIG2Options.UseGlobalSegments := True;
    Pdf.JBIG2Options.AccumulateGlobalsAcrossPages := AccumulateGlobals;
    Pdf.JBIG2Options.UseExternalEncoder := not AccumulateGlobals;
    // ... encode the same three-page scan here, then compare file sizes.
  finally
    Pdf.Free;
  end;
end;

begin
  RunScenario('Per-image lossless baseline', False);
  RunScenario('Cross-page accumulated globals', True);
end.

Gdzie akumulacja między stronami napotyka swoje granice

Zgromadzony słownik jest ograniczony do 4096 symboli, tego samego pułapu, jaki natywny koder na obraz już wymusza na pojedynczej stronie. Przekrocz ten limit w środku dokumentu, a HotPDF nie zgłasza wyjątku ani nie przerywa przebiegu: akumulator odrzuca nowy glif, a strona, która go wprowadziła, automatycznie cofa się do niezależnego kodowania na obraz, więc dokument nadal wychodzi poprawny — po prostu przestajesz otrzymywać oszczędność między stronami dla stron, które przekroczyły pułap. Drugie zabezpieczenie pilnuje raczej całkowitego rozmiaru niż liczby symboli: gdy tylko łączna szerokość symboli zgromadzonego słownika przekroczy 131071 pikseli, HotPDF automatycznie zrzuca bieżącą partię na dysk i zaczyna świeżą grupę globalną, zamiast pozwalać jednej strukturze w pamięci rosnąć bez ograniczeń. Żaden z limitów nie wymaga żadnego kodu po twojej stronie, ponieważ oba to automatyczne mechanizmy zapasowe, a nie wyjątki, które trzeba przechwytywać

Zgodność z PDF/A to jedyne ustawienie, które wyłącza cały mechanizm zamiast go tylko ograniczać. HotPDF po cichu zastępuje JBIG2 przez CCITT Group 4 w chwili, gdy PDFACompliance jest niepuste, na każdej stronie, niezależnie od AccumulateGlobalsAcrossPages ani niczego innego w JBIG2Options — świadomy wybór zgodności, nie błąd, ale oznacza to, że profil archiwalny i współdzielenie symboli między stronami są dziś wzajemnie wykluczające się. Bez względu na to, na jakiej konfiguracji wylądujesz, zdekoduj to, co zapisałeś, zanim jej zaufasz: wczytaj plik z powrotem przez LoadFromFile i przeciągnij każdą stronę przez ExtractLoadedImage, które rozwiązuje za ciebie współdzielone dane globalne tak samo, jak zrobiłby to każdy zgodny czytnik, i porównaj wynik z twoimi bitmapami źródłowymi

var
  Loaded: THotPDF;
  PageBmp: TBitmap;
  PageIdx: Integer;
begin
  Loaded := THotPDF.Create(nil);
  try
    Loaded.LoadFromFile('scanned-contract.pdf');
    for PageIdx := 0 to Loaded.PagesCount - 1 do
    begin
      PageBmp := Loaded.ExtractLoadedImage(PageIdx);   // resolves the shared globals for you
      try
        // Compare PageBmp against the source bitmap for this page.
      finally
        PageBmp.Free;
      end;
    end;
  finally
    Loaded.Free;
  end;
end;

Współdzielenie słownika między stronami dotyka wyłącznie strony obrazu bilevel w dokumencie. Jeśli ten sam potok emituje też wygenerowane strony tekstowe obok skanów — strony tytułowe, strony indeksu, warstwę tekstu OCR — strumienie obiektów i strumienie xref atakują drugą połowę budżetu rozmiaru pliku, kompresując strukturę dokumentu, jaką dodają te strony. Współdzielone dane globalne JBIG2 między stronami są dostarczane jako część komponentu HotPDF dla Delphi i C++Buildera, obok opcji JBIG2 na obraz i reszty potoku kompresji