Artykuł techniczny

TIFF 12-bitowy, lossless i arytmetyczny JPEG do PDF w Delphi

PDFlibPas, biblioteka PDF dla Delphi, importuje obrazy TIFF, których kompresja JPEG wykracza poza 8-bitowy baseline: 12-bitowy extended sequential (SOF1), bezstratny Huffman (SOF3), kodowany arytmetycznie (SOF9 i SOF10) oraz starego typu JPEG z TIFF 6.0 z offsetami tabel. Ponieważ PDF DCTDecode nie potrafi nieść tych procesów, PDFlibPas dekoduje je w czystym Pascalu i osadza próbki bezstratnie przy 8 albo 16 bitach na komponent

Pełny zestaw wymaga v3.724.0 lub nowszej i celuje wprost w TIFF-y ze skanerów, medycznego i naukowego oprogramowania akwizycji oraz systemów archiwalnych zamrożonych w latach 90. Poniżej jest wyjaśnienie, dlaczego tych wariantów nie da się skopiować do PDF-a jak leci, jak PDFlibPas je ląduje i jakie pułapki Pascala znalazły się po drodze

Dlaczego 12-bitowego JPEG-a nie można wprost włożyć do PDF-a?

12-bitowego JPEG-a nie da się przepuścić, bo PDF nie ma filtra, który byłby zobowiązany go zdekodować. ISO 32000-1 §7.4.8 definiuje DCTDecode wokół formatu baseline JPEG, z rozszerzeniem progressive dodanym w PDF 1.3. Nic w tym punkcie nie obejmuje 12-bitowej precyzji próbek, procesu bezstratnego z aneksu H ITU-T T.81 ani arytmetycznego kodera entropii z aneksu D. Czytnik, który spotka ramkę SOF1 12-bitową albo marker SOF9 wewnątrz strumienia DCTDecode, ma pełne prawo nie narysować niczego, i większość przeglądarek robi dokładnie to

Dla 8-bitowych pasów Huffman baseline i progressive, z których składa się większość plików JPEG-in-TIFF, PDFlibPas nadal waliduje każdy scan i przepuszcza skompresowane bajty jako DCTDecode, co trzyma PDF mały. Wszystko inne jest dekodowane. PDFlibPas czyta marker ramki i precyzję próbek pierwszego bloku JPEG i, gdy znajdzie SOF3, SOF9, SOF10 albo precyzję inną niż 8, strona przechodzi przez dekoder próbek zamiast ścieżki przepuszczenia. Wykrywanie kodowania arytmetycznego po marcerze ramki znaczy więcej, niż brzmi: kontrola patrząca tylko na precyzję próbek pozwoli klasycznemu TIFF-owi przemycić 8-bitowy pas SOF9 na ścieżkę DCTDecode i wyprodukować PDF-a, którego czytniki nie wyświetlą

Drzewo decyzji dla bloków JPEG w TIFF w PDFlibPas dla Delphi: marker ramki kieruje scany 8-bitowe do DCTDecode, a pozostałe procesy do dekodowania natywnego
marker ramki, nie sama precyzja próbek, decyduje, czy pas JPEG z TIFF przechodzi jako DCTDecode, czy jest dekodowany natywnie

Co PDFlibPas pisze w PDF-ie zamiast tego

PDFlibPas zamienia zdekodowane bloki JPEG w zwykły obraz XObject z BitsPerComponent 8 albo 16. Próbki są domyślnie przechowywane bez kompresji; wołając CompressImages(1) przed dodaniem obrazu, zmuszasz PDFlibPas do zapisania ich z FlateDecode. ISO 32000-1 §8.9.5.1 pozwala na 16 bitów na komponent od PDF 1.5, więc PDFlibPas podnosi wersję dokumentu co najmniej do 1.5, ilekroć dodawany jest obraz z 16-bitowymi próbkami. To obejmuje obrazy wielopasmowe i kafelkowe, których obiekt główny to Form XObject bez własnego BitsPerComponent do zajrzenia

Mapowanie jest stałe dla każdego procesu JPEG:

  • 12-bitowy JPEG Huffman (sequential SOF1, progressive SOF2) jest dekwantyzowany, odwracanie transformowany i rozszerzany do próbek 16-bitowych
  • JPEG kodowany arytmetycznie (sequential SOF9, progressive SOF10) staje się próbkami 8-bitowymi przy precyzji 8 bitów i 16-bitowymi przy precyzji 12 bitów
  • Bezstratny JPEG Huffman (SOF3) staje się próbkami 8- albo 16-bitowymi, dokładnie według swojej zadeklarowanej precyzji

Rozszerzanie z 12 do 16 bitów używa replikacji bitów, (V shl 4) or (V shr 8), więc 4095 mapuje się na 65535 i biel zostaje białą zamiast kończyć na 65520. PDFlibPas nigdy nie zrzuca tych obrazów do 8 bitów za twoimi plecami. Kiedy biblioteka rasteryzuje taką stronę na własną powierzchnię wyświetlania, bierze wysoki bajt każdej próbki, ale osadzony obraz PDF trzyma wszystkie szesnaście

Mapowanie zdekodowanych procesów JPEG na szerokości próbek w PDFlibPas dla Delphi: 12-bitowy Huffman rozszerza się do 16 bitów, arytmetyczny i lossless mapują według zadeklarowanej precyzji
każdy proces JPEG ma stałą szerokość próbki w PDF, a dane 12-bitowe są rozszerzane replikacją bitów, więc biel zostaje biała

Ceną jest rozmiar pliku: nawet z CompressImages(1) 16-bitowe próbki skompresowane Flate są większe niż dane DCT, z których przyszły, a bez tego większe jeszcze. Dla workflow diagnostycznych i archiwalnych ten handel zwykle się opłaca. Jeśli dla danego zadania rozmiar znaczy więcej, przewodnik po zmniejszaniu rozmiaru pliku PDF w Delphi pokazuje, gdzie bajty faktycznie uciekają

Import tych TIFF-ów przez TPDFlib

Kod wywołujący nie zmienia się wcale. TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer bierze numer strony TIFF liczonej od 1 w Options i zwraca ID obrazu albo 0, gdy strony nie da się zaimportować. Niewspierane warianty JPEG są odrzucane przed wstawieniem obrazu, więc 0 znaczy, że nic półzdekodowanego nie dotarło do dokumentu

uses
  PDFlibrary;

function TiffToPdf(const TiffName, PdfName: WideString): Boolean;
var
  Lib: TPDFlib;
  Page, PageCount, ImageID: Integer;
  DPI, W, H: Double;
begin
  Result := False;
  Lib := TPDFlib.Create;
  try
    Lib.SetMeasurementUnits(0);   // domyślna przestrzeń użytkownika, 1/72 cala
    Lib.SetOrigin(1);             // origin lewy górny dla DrawImage
    Lib.CompressImages(1);        // Flate dla zdekodowanych próbek
    PageCount := Lib.GetImagePageCount(TiffName);
    if PageCount = 0 then
      Exit;
    for Page := 1 to PageCount do
    begin
      ImageID := Lib.AddImageFromFile(TiffName, Page);  // Options = strona TIFF
      if ImageID = 0 then
        Exit;                     // nieobsługiwane albo uszkodzone dane obrazu
      Lib.SelectImage(ImageID);
      case Lib.ImageResolutionUnits of
        2: DPI := Lib.ImageHorizontalResolution;          // punkty na cal
        3: DPI := Lib.ImageHorizontalResolution * 2.54;   // punkty na cm
      else
        DPI := 0;
      end;
      if DPI <= 0 then
        DPI := 300;
      W := Lib.ImageWidth * 72 / DPI;
      H := Lib.ImageHeight * 72 / DPI;
      if Page > 1 then
        Lib.NewPage;
      Lib.SetPageDimensions(W, H);
      Lib.DrawImage(0, 0, W, H);
    end;
    Result := Lib.SaveToFile(PdfName) = 1;
  finally
    Lib.Free;
  end;
end;

Nie potrzeba żadnego rozgałęziania per format, a AddImageFromStream i AddImageFromString stosują te same reguły do danych TIFF trzymanych w pamięci

Stary typ JPEG: tabele pod offsetami, pasy bez nagłówków

Od v3.713.0 PDFlibPas importuje TIFF-y ze starym typem JPEG (Compression = 6), których tabele pochodzą z tagów offsetowych JPEGQTables, JPEGDCTables i JPEGACTables albo ze strumienia JPEGInterchangeFormat zawierającego same tabele. TIFF 6.0 Section 22 definiował ten układ, a TIFF Technical Note 2 później zastąpił go Compression = 7 i współdzielonym tagiem JPEGTables, ale skanery pisały starą formę jeszcze latami

Strona kontenera, BigTIFF i układy kafelkowe, jest opisana w wpisie o utwardzaniu dekodera TIFF; tu akcent pada na payload JPEG. Każdy tag offsetowy tabeli wskazuje surową tabelę kwantyzacji 64 bajtów albo tabelę Huffman z 16 zliczeniami i jej wartościami, jedną pozycję na komponent. PDFlibPas zamienia je w segmenty DQT i DHT w syntezowanym strumieniu JPEGTables. Każdy pas gołych danych kodowanych entropią dostaje potem własny nagłówek: SOI, marker ramki z liczbą wierszy tego pasa, DRI gdy ustawione jest JPEGRestartInterval, SOS, dane entropii i EOI. Pasów nie da się skleić w jeden scan, bo stary typ JPEG restartuje predykcję DC na każdej granicy pasa

Dwa szczegóły łatwo zepsuć. Trzy komponenty obrazu YCbCr często wskazują swoje offsety tabel na te same bajty, więc PDFlibPas deduplikuje tabele po zawartości, zanim przydzieli numery tabel. Jeśli zostaje więcej niż dwie tabele Huffman jednej klasy, syntezowana ramka używa SOF1 zamiast SOF0, bo ramka baseline może referować tylko dwie tabele DC i dwie AC. Kiedy JPEGInterchangeFormat wskazuje zamiast tego kompletny strumień JPEG zawierający scany, PDFlibPas traktuje ten strumień jako cały obraz i importuje go jako pojedynczy pas nowego typu. Bezstratny, kafelkowy i oddzielne-płaszczyzny stary typ JPEG są odmawiane z jawnym błędem zamiast zgadywania, więc konwertuj je na nowy typ JPEG upstream

Dlaczego CMYK JPEG wewnątrz TIFF-a wychodzi odwrócony?

CMYK JPEG wewnątrz TIFF-a wychodzi odwrócony, gdy importer stosuje konwencję Adobe APP14 należną samodzielnym plikom JPEG. Photoshop zapisuje CMYK odwrócone w samodzielnych JPEG-ach, więc producenci PDF tradycyjnie dodają /Decode [1 0 1 0 1 0 1 0] dla kompensacji. TIFF PhotometricInterpretation 5 definiuje próbki jako ilości farby, gdzie 0 znaczy brak farby, a popularne czytniki TIFF biorą zdekodowane próbki JPEG po face value bez patrzenia na APP14

PDFlibPas osadza CMYK pasy TIFF skompresowane JPEG-em bez odwróconej tablicy Decode, niezależnie od obecności markera APP14. Flagowa transformacja APP14 nadal decyduje, czy YCCK jest konwertowane na CMYK, i DCTDecode robi ten krok sam. Samodzielne pliki JPEG o czterech komponentach zachowują odwróconą konwencję Adobe, więc identyczne bajty JPEG są traktowane różnie zależnie od kontenera, tak jak wymagają oba formaty

Dlaczego dekodowanie bezstratnego JPEG-a pada na przesunięciu w Pascalu?

Dekodowanie bezstratnego JPEG-a pada w Pascalu, bo predyktory 5 i 6 potrzebują arytmetycznego przesunięcia w prawo, a pascalowskie shr jest logiczne. SOF3 przewiduje każdą próbkę z lewego (Ra), górnego (Rb) i górnego-lewego (Rc) sąsiada i koduje tylko różnicę. Spośród siedmiu predyktorów z tabeli H.1 T.81 predyktor 5 to Ra + ((Rb - Rc) >> 1), predyktor 6 to Rb + ((Ra - Rc) >> 1), a różnica w nawiasie robi się ujemna za każdym razem, gdy górno-lewy sąsiad jest jaśniejszy. Podaj ujemny Integer do shr i bit znaku staje się bitem wartości, więc obraz rozpada się w szum

Zachowanie przesunięć dla predyktorów bezstratnego JPEG w Delphi i PDFlibPas: logiczne shr psuje wartości ujemne, div 2 ucina w stronę zera, HalfDown zaokrągla w dół jak referencja
predyktory 5 i 6 potrzebują semantyki floor, a mały helper pasuje do dekodera referencyjnego na Delphi i Free Pascal

Podmiana przesunięcia na div 2 też nie leczy. div ucina w stronę zera, podczas gdy arytmetyczne przesunięcie zaokrągla w stronę minus nieskończoności, i oba rozjeżdżają się na każdej ujemnej wartości nieparzystej. Free Pascal ma SarLongint w unit System; Delphi nie ma odpowiednika, więc PDFlibPas używa helpera kompilującego się identycznie na obu:

// Arytmetyczne przesunięcie w prawo o jeden: floor(Value / 2)
// shr to przesunięcie logiczne, a div 2 ucina w stronę zera
function HalfDown(Value: Integer): Integer;
begin
  if Value >= 0 then
    Result := Value shr 1
  else
    Result := -((1 - Value) shr 1);
end;

// Value = -3:  -3 shr 1 = 2147483646,  -3 div 2 = -1,  HalfDown(-3) = -2

Stronę szerokości typów tej samej klasy problemów opisuje wpis o pułapkach Longint i wskaźników na Delphi Linux64

Reszta dekodera bezstratnego ma własne reguły brzegowe. Pierwszy wiersz, i pierwszy wiersz po każdym markerze restartu, przewiduje wyłącznie z lewego sąsiada i startuje od 2^(P−Pt−1) dla precyzji P i point transform Pt; każdy późniejszy wiersz startuje od próbki powyżej. Rekonstrukcja zawija modulo 2^16, a point transform skaluje tylko wyjście, nigdy łańcuch predykcji. Kategoria DC 16 w Huffman, różnica 32768 bez bitów dodatkowych, jest legalna tylko w trybie bezstratnym, więc parser tabel musi ją akceptować, zanim dowie się, jaki proces zadeklaruje ramka

PDFlibPas wspiera wszystkie siedem predyktorów, point transformy i interwały restartu pokrywające całe wiersze. Precyzja JPEG musi równać się BitsPerSample i wynosić 8 albo 16, a każdy komponent musi być próbkowany 1x1. Dane lossless YCbCr i 12-bitowy bezstratny JPEG są odmawiane z jawnym błędem

12-bitowe DCT i kodowanie arytmetyczne w czystym Pascalu

Dekodery 12-bitowy i arytmetyczny w PDFlibPas dzielą jedno odwrotne DCT i różnią się tylko dekodowaniem entropii. W 12-bitowym JPEG Huffman kategorie DC sięgają 15, AC 14, tabele kwantyzacji mogą nieść pozycje 16-bitowe, a predykcja DC resetuje się przy każdym scanie i każdym restarcie. Współczynniki zostają w porządku zigzag do dekwantyzacji, która musi użyć wartości kwantyzacji pod tym samym indeksem zigzag, i dopiero potem przechodzą na porządek naturalny dla separowalnego dwuprzebiegowego IDCT. Zdekodowane próbki lądują w ±1 względem niezależnego zmiennoprzecinkowego dekodera referencyjnego

Kodowanie arytmetyczne zastępuje Huffman koderem QM z aneksu D T.81: 114-pozycyjna tabela stanów prawdopodobieństwa, 64-kublikowy obszar statystyk DC i 256-kublikowy obszar AC, sterowane parametrami kondycjonowania z segmentu DAC (domyślnie L = 0, U = 1, Kx = 5). Statystyki, kontekst DC i predykcja resetują się przy każdym interwale restartu i na początku każdego scanu. Dlatego PDFlibPas zachowuje segmenty DAC przy normalizacji współdzielonych JPEGTables dla obrazów arytmetycznych; wyrzuć je i każdy pas dekoduje się ze złym modelem statystyk

v3.723.0 dodało do tych ścieżek dekodowanych komponenty subsamplowane i konwersję YCbCr-to-RGB równaniami JFIF, a v3.724.0 dodało scany progressive dla danych 12-bitowych Huffman (SOF2) i arytmetycznych (SOF10). Subsamplowana chroma jest replikowana do pełnej rozdzielczości zamiast wygładzana, co daje dekoder referencyjny z wyłączonym wygładzaniem chromy. Hierarchiczny JPEG i arytmetycznie kodowany bezstratny JPEG (SOF11) są odmawiane z jawnym błędem

Testowanie binarnych fixture'ów JPEG w Delphi

Ręcznie składane fixture'y JPEG w testach jednostkowych Delphi potrafią zawieść w sposób wyglądający dokładnie jak bug dekodera. Stała stringowa złożona ze znaków #$xx przekraczająca 255 znaków dostaje typ UnicodeString, a przypisanie jej do AnsiString konwertuje ją przez aktywną stronę kodową. Bajty $80 do $9F, rutynowe w danych kodowanych entropią, są przepisywane przy wejściu. Składanie nagłówków markerów w runtime ma tę samą pułapkę: w Delphi AnsiChar + AnsiChar daje UnicodeString, więc dopisuj pojedyncze bajty do AnsiString albo pisz je przez SetLength i przypisanie indeksowane. Dla fixture'ów trzymaj tekst hex i dekoduj go w runtime:

uses
  SysUtils, PDFlibrary;

function HexToBytes(const Hex: AnsiString): AnsiString;
var
  I: Integer;
begin
  SetLength(Result, Length(Hex) div 2);
  for I := 1 to Length(Result) do
    Result[I] := AnsiChar(StrToInt('$' + string(Copy(Hex, I * 2 - 1, 2))));
end;

// Fixture zostaje czystym ASCII, więc żadna strona kodowa go nie ruszy
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);

Generując obrazy testowe, pamiętaj, że jpegtran -arithmetic pisze sequential SOF9; SOF10 wymaga dodatkowo -progressive. I niezerowe ID obrazu dowodzi tylko, że import został przyjęty. Zrenderuj stronę i porównaj piksele, bo błąd ścieżki przepuszczenia produkuje dobrze uformowanego PDF-a, którego żadna przeglądarka nie narysuje

Przenoszenie archiwów skanów do PDF

Te warianty JPEG mają w zwyczaju wychodzić naraz podczas migracji archiwum. PDFlibPas czyta TIFF-y z 12-bitowym, bezstratnym, arytmetycznym i starym typem JPEG tym samym wywołaniem AddImageFromFile co wszystko inne, trzyma pełną precyzję w PDF-ie i odmawia rzadkich procesów spoza tego zestawu z jawnym błędem. Licencjonowanie, pełną listę funkcji i pobrania próbne znajdziesz na stronie produktu PDFlibPas PDF Library for Delphi