Artykuł techniczny

Backendi kodera JBIG2 i konsolidator Free Pascala

PDFlibPas potrafi kodować obrazy dwupoziomowe jako JBIG2 dwoma różnymi backendami. Jednym jest natywny koder MMR w Object Pascalu, obecny zawsze. Drugim jest zewnętrzny koder słowników symboli, dający na skanowanym tekście wyraźnie mniejsze wyniki, i jest opcjonalny: projekt musi zlinkować jednostkę backendu, żeby w ogóle zaistniał. To rozróżnienie jest źródłem najczęstszej niespodzianki związanej z tą funkcją, więc warto powiedzieć o nim najpierw: DefaultJBIG2EncodeOptions domyślnie żąda kodera zewnętrznego, a gdy jednostka backendu nie jest zlinkowana, żądanie po cichu cofa się do ścieżki MMR w Pascalu

Na Delphi i C++Builder zewnętrzny backend to zbiór wcześniej zbudowanych statycznych plików obiektowych. Na Free Pascalu musiał stać się biblioteką DLL, a droga do tego wniosku to historia konsolidatora użyteczna dla każdego, kto próbował linkować obiekty C++ do programu w Free Pascalu

Rejestracja jest kontraktem

Jednostka backendu rejestruje się z sekcji inicjalizacyjnej, wywołując RegisterJBIG2EncoderBackend. Wywołujący żądają go albo przez bit opcji PDF_JBIG2_OPTION_EXTERNAL_ENCODER, mający wartość 4, albo przez parametr UseExternalEncoder rozszerzonych punktów wejścia dla obrazów. Parasol biblioteki celowo nie wciąga jednostki backendu, bo noszenie dużego zbioru obiektów powinno być decyzją każdego projektu; w drzewie C++Builder jest on na przykład włączany jawnie przez projekty, które go chcą

Konsekwencją dla wywołujących jest to, że żądanie kodera zewnętrznego to preferencja, nie gwarancja, a build, który zapomni jednostki, produkuje większe pliki zamiast błędu. Jeśli rozmiar wyjścia na tyle Cię obchodzi, że prosisz o lepszy koder, to na tyle obchodzi, żeby sprawdzić, czy rzeczywiście go masz

Przepływ żądania kodowania JBIG2 w PDFlibPas, w którym preferencja kodera zewnętrznego po cichu cofa się do natywnej ścieżki MMR w Pascalu bez jednostki backendu
Żądanie zewnętrznego kodera słowników symboli to preferencja: zlinkowany, wynik się kurczy; niezlinkowany, ścieżka MMR w Pascalu działa po cichu z większymi plikami
uses
  PDFlibrary,
{$IFDEF FPC}
  PDFlibJBIG2EncDLL;    // backend dynamiczny dla Free Pascala
{$ELSE}
  PDFlibJBIG2EncC;      // zbiór obiektów statycznych dla Delphi / C++Builder
{$ENDIF}

var
  Pdf: TPDFlib;
  ImageId: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.NewPage;
    // Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
    // BlackDotSize, LossyLevel
    ImageId := Pdf.AddImageJBIG2FromFileEx('scan-page-1.tif',
      0, 1, 1, 0, 0, 0);
    if ImageId = 0 then
      raise Exception.Create('JBIG2 encoding failed');
    Pdf.SaveToFile('archive.pdf');
  finally
    Pdf.Free;
  end;
end;

Skompilowanie jednostki to dwie linie. Symbole były robotą

Skompilowanie samej jednostki backendu pod Free Pascalem kosztowało dokładnie dwie zmiany: ustawienie dialektu asemblera i zastąpienie konstruktora ustawień formatu opartego na rekordzie globalną zmienną domyślną. To uczciwe odzwierciedlenie tego, jak przenośny jest prosty Pascal między tymi dwoma kompilatorami

Strona symboli była prawdziwą robotą. Zbiór obiektów odwołuje się do 176 symboli C. Z nich 128 miało już implementacje w Pascalu wewnątrz jednostki i potrzebowało tylko doczepionych nazw eksportowych, bo Delphi używa nazwy funkcji jako nazwy symbolu, podczas gdy Free Pascal wymaga jawnej deklaracji nazwy publicznej. Dwadzieścia siedem było współdzielonych z kodekiem JPEG 2000 i trzeba było je wyeksportować z dokładnie jednego miejsca, bo podwójna definicja psuje każdy program linkujący oba. Pozostałe 21 to wpisy platformowe i czasu wykonania C: szesnaście funkcji plikowych Win32 plus garść wywołań biblioteki standardowej, i trafiły do nowej jednostki kompatybilności

Nic z tego nie jest pojęciowo trudne, a wszystko jest konieczne, zanim konsolidator w ogóle spróbuje. Na konsolidatorze to się zatrzymało

Trzy ścieżki linkowania, trzy ślepe zaułki

Wewnętrzny konsolidator Free Pascala nie potrafi czytać tych plików obiektowych, bo powstały one kompilatorem emitującym asocjacyjne sekcje COMDAT, a wewnętrzny konsolidator zgłasza, że ich nie obsługuje. To kategoryczna odmowa, nie ostrzeżenie

Przejście na zewnętrzny konsolidator wyglądało jak odpowiedź. Konsolidator binutils dołączany do Free Pascala pada bezpośrednio przy zastosowaniu garbage collection sekcji do tego archiwum, a ta flaga jest częścią stałego zestawu parametrów, które Free Pascal przekazuje dla celu 64-bit Windows, więc nie da się jej usunąć z linii poleceń; udokumentowane przełączniki jej tłumienia są na tej ścieżce ignorowane. Podanie znacznie nowszego binutils zawodzi inaczej: nie potrafi w ogóle przetworzyć skryptu linkowania Free Pascala, produkując puste wyjście bez skryptu i ścianę błędów relokacji ze skryptem

Granica odkryta po drodze jest warta znajomości, nawet jeśli nigdy nie trafisz na problem konsolidatora. Zewnętrzny konsolidator rozwiązuje ścieżki plików obiektowych względem katalogu wyjściowego pliku wykonywalnego, a nie drzewa źródeł, więc względna dyrektywa dołączania obiektu działa tylko wtedy, gdy katalog wyjściowy akurat równa się katalogowi roboczemu kompilacji. Biblioteka nie może tego zakładać o projekcie konsumenta, co samo w sobie jest powodem, by wolić linkowaną bibliotekę od luźnych obiektów

Trzy nieudane ścieżki konsolidatora dla obiektów kodera JBIG2 C++ pod Free Pascalem oraz biblioteka DLL z dwoma płaskimi punktami wejścia C, która je rozwiązała
Sekcje COMDAT łamią wewnętrzny konsolidator, a oba zewnętrzne zawodzą, więc koder C++ jest dostarczany jako jedna biblioteka DLL wiązana dynamicznie przez jednostkę backendu

Dlaczego inny kompilator C++ nie pomaga

Następny oczywisty pomysł to przebudowanie strony C++ kompilatorem, którego obiekty Free Pascal potrafi czytać. To też nie działa, a powód jest fundamentalny, a nie kwestią przełączników. Minimalna jednostka translacji C++ zawierająca szablon, skompilowana z każdą funkcją generowania kodu wyłączoną, wciąż emituje słabe symbole zewnętrzne, bo instantiacja szablonów i inline tworzy je konsekwentnie z konstrukcji. Free Pascal odrzuca tę klasę symboli kategorycznie. Odwrotny kierunek też zawodzi: powszechnie używany konsolidator C++ nie potrafi skonsumować obiektów drugiego kompilatora z powodu tej samej obsługi sekcji COMDAT

Kod C++ nie może więc trafić do Free Pascala jako obiekty żadną dostępną drogą. Może trafić jako biblioteka DLL i tak się stało: koder i jego zależność przetwarzania obrazu są zbudowane w jednej bibliotece wystawiającej dwa płaskie punkty wejścia C, a jednostka backendu Free Pascala wiąże je dynamicznie i rejestruje się dokładnie tak, jak robi to backend statyczny. Ścieżka Delphi i C++Builder nie została tknięta wcale, co jest właściwym wynikiem; problem przenośności na jednym toolchainie nie powinien zaburzać toolchaina, który już działał

Polarność to jedyna rzecz, która cię ugryzie

Między bitmapą dwupoziomową Windows a koderem JBIG2 czeka niezgodność konwencji, której żaden system typów nie wyłapie. Linia skanowania urządzeniowo niezależnej bitmapy o jednym bicie na piksel traktuje ustawiony bit jako biały. Koder traktuje ustawiony bit jako czarny. Przekaż linie bez zmian i dostaniesz w pełni poprawny strumień JBIG2 fotograficznego negatywu twojej strony

Konwencje polarności jednobitowej DIB i JBIG2, gdzie ustawiony bit jest biały w linii skanowania i czarny w koderze, naprawiane przez inwersję każdego bajtu
Te same bajty, przeciwne znaczenie: bez inwersji każdego bajtu koder produkuje poprawny strumień JBIG2 negatywu fotograficznego
// Jednobitowa DIB: ustawiony bit znaczy biały. Koder JBIG2:
// ustawiony bit znaczy czarny. Odwróć każdy bajt na wejściu
for I := 0 to RowBytes - 1 do
  Row[I] := Row[I] xor $FF;

Metoda weryfikacji znaczy tyle, co sama poprawka. Porównywanie długości skompresowanych strumieni nie mówi nic, bo obraz negatywu kompresuje się do podobnego rozmiaru. Spojrzenie na stronę dowodzi tylko, że nie jest jawnie odwrócona. Pewnym sprawdzeniem jest wyrenderowanie wyjścia obu ścieżek kodowania, natywnej w Pascalu i zewnętrznej, do PNG i porównanie ich bajt w bajt: oba kodery są bezstratne na tym samym obrazie źródłowym, więc cokolwiek poza dokładnym dopasowaniem jest błędem jednego z nich. To porównanie jest teraz stałym testem regresji i to ten rodzaj asercji, który warto budować zawsze, gdy dwie implementacje mają zgadzać się dokładnie

Którego backendu używać

Dla ogólnej treści dwupoziomowej, drukowanych rasterów, grafiki liniowej, mieszanej grafiki, natywny koder MMR w Pascalu jest wystarczający i nie ma kosztu wdrożeniowego. Dla skanowanego tekstu, czyli przypadku, dla którego JBIG2 zaprojektowano, zewnętrzny koder słowników symboli to miejsce, gdzie mieszka redukcja rozmiaru, bo wydziela powtarzające się kształty glifów do słownika zamiast rekodować każde wystąpienie. Jeśli produkujesz archiwa skanowanych dokumentów, ta różnica jest na tyle duża, że zmienia planowanie przestrzeni dyskowej

Pytanie leżące wyżej, jak w ogóle powstaje obraz dwupoziomowy, znaczy dla rozmiaru wyjścia równie dużo; renderowanie monochromatyczne oparte na regionach omawia artykuł o renderowaniu regionów monochromatycznych, a strategia rozmiaru całego dokumentu — optymalizacja rozmiaru pliku PDF i wydzielanie podzbiorów fontów. Dla zestawów skanów z powtarzającymi się stronami deduplikacja często wygrywa z lepszą kompresją, czego tematem jest perceptualna deduplikacja obrazów. Dostępność toolchainów i backendów per platforma jest wypisana na stronie produktu losLab PDF Developer Library