Technický článek

TIFF JPEG 12bit, lossless a aritmetický do PDF v Delphi

PDFlibPas, PDF knihovna pro Delphi, importuje TIFF obrázky, jejichž JPEG komprese sahá za 8bitový baseline: 12bitový extended sequential (SOF1), Huffman lossless (SOF3), aritmetické kódování (SOF9 a SOF10) a starý TIFF 6.0 JPEG s tabulkami na offsetech. Protože PDF DCTDecode tyhle procesy přenést neumí, dekóduje je PDFlibPas v nativním Pascalu a vzorky vkládá bezztrátově s 8 nebo 16 bity na komponentu

Kompletní sada vyžaduje v3.724.0 a novější a míří přímo na TIFFy ze skenerů, z lékařského a vědeckého zachytávacího softwaru a z archivních systémů zmrzlých v devadesátých letech. Níže je, proč se tyhle varianty nedají zkopírovat do PDF tak jak jsou, jak je PDFlibPas dostane do dokumentu a jaké pascalské pasti cestou vyšly najevo

Proč se 12bitový JPEG nedostane přímo do PDF?

12bitový JPEG se propustit nedá, protože PDF nemá žádný filtr, který by měl povinnost ho dekódovat. ISO 32000-1 §7.4.8 definuje DCTDecode kolem baseline formátu JPEG, s progresivním rozšířením přidaným v PDF 1.3. Nic v té klauzuli nepokrývá 12bitovou přesnost vzorků, lossless proces z ITU-T T.81 Annex H ani aritmetický entropický kódovač z Annex D. Čtečka, která uvnitř streamu DCTDecode narazí na 12bitový frame SOF1 nebo marker SOF9, smí klidně neukreslit vůbec nic a většina prohlížečů přesně to dělá

U 8bitových Huffman baseline a progresivních stripů, ze kterých se skládá většina souborů JPEG-in-TIFF, PDFlibPas stále kontroluje každý scan a komprimované bajty jen propustí jako DCTDecode, takže PDF zůstává malé. Všechno ostatní se dekóduje. PDFlibPas načte frame marker a vzorkovou přesnost prvního JPEG bloku a když najde SOF3, SOF9, SOF10 nebo přesnost jinou než 8, jde stránka přes vzorkový dekodér místo propustné cesty. Detekce aritmetického kódování podle frame markeru je důležitější, než zní: kontrola dívající se jen na vzorkovou přesnost pustí klasickému TIFFu 8bitový strip SOF9 do cesty DCTDecode a vyrobí PDF, které čtečky neumí zobrazit

Rozhodovací tok pro TIFF JPEG bloky v PDFlibPas pro Delphi: frame marker směruje 8bitové scany do DCTDecode a ostatní procesy do nativního dekódování
o tom, jestli TIFF JPEG strip projde jako DCTDecode nebo se dekóduje nativně, rozhoduje frame marker, ne jen vzorková přesnost

Co PDFlibPas místo toho zapíše do PDF

PDFlibPas z dekódovaných JPEG bloků udělá obyčejný image XObject s BitsPerComponent 8 nebo 16. Vzorky se defaultně ukládají nekomprimované; zavolejte před přidáním obrázku CompressImages(1) a PDFlibPas je zapíše přes FlateDecode. ISO 32000-1 §8.9.5.1 dovoluje 16 bitů na komponentu od PDF 1.5, takže PDFlibPas zvedne verzi dokumentu aspoň na 1.5, kdykoliv se přidá obrázek s 16bitovými vzorky. To zahrnuje multi-strip a tiled obrázky, jejichž root objekt je Form XObject bez vlastního BitsPerComponent, na který by se dalo koukat

Mapování na jednotlivé JPEG procesy je pevné:

  • 12bitový Huffman JPEG (sequential SOF1, progressive SOF2) se dequantizuje, inverzně transformuje a rozšíří na 16bitové vzorky
  • Aritmeticky kódovaný JPEG (sequential SOF9, progressive SOF10) se stane 8bitovými vzorky při 8bitové přesnosti a 16bitovými při 12bitové přesnosti
  • Huffman lossless JPEG (SOF3) se stane 8bitovými nebo 16bitovými vzorky, přesně podle své deklarované přesnosti

Rozšíření z 12 na 16 bitů používá bit replikaci, (V shl 4) or (V shr 8), takže 4095 se mapuje na 65535 a bílá zůstane bílá místo dosažení stropu na 65520. PDFlibPas tyhle obrázky nikdy nesesadí na 8 bitů za vašimi zády. Když knihovna rasterizuje takovou stránku pro vlastní zobrazovací plochu, použije vyšší bajt každého vzorku, ale vložený PDF obrázek si nechá všech šestnáct

Mapování dekódovaných JPEG procesů na šířky vzorků v PDFlibPas pro Delphi: 12bitový Huffman se rozšiřuje na 16 bitů, aritmetický a lossless se mapují podle deklarované přesnosti
každý JPEG proces má v PDF pevnou šířku vzorků a 12bitová data se rozšiřují bit replikací, takže bílá zůstane bílá

Cenou je velikost souboru: i s CompressImages(1) jsou Flate komprimované 16bitové vzorky větší než DCT data, ze kterých vznikly, a bez něj jsou větší ještě. Pro diagnostické a archivační workflow je tenhle kompromis většinou správný. Když pro konkrétní zakázku znamená velikost víc, průvodce snižováním velikosti PDF v Delphi ukazuje, kam bajty skutečně mizí

Import těchhle TIFFů přes TPDFlib

Volající kód se nemění vůbec. TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer bere číslo TIFF stránky od jedničky v Options a vrací image ID, nebo 0, když se stránka importovat nedá. Nepodporované JPEG varianty se odmítnou před vložením obrázku, takže 0 znamená, že do dokumentu nedoputovalo nic napůl dekódovaného

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);   // defaultní user space, 1/72 palce
    Lib.SetOrigin(1);             // levý horní počátek pro DrawImage
    Lib.CompressImages(1);        // Flate pro dekódované vzorky
    PageCount := Lib.GetImagePageCount(TiffName);
    if PageCount = 0 then
      Exit;
    for Page := 1 to PageCount do
    begin
      ImageID := Lib.AddImageFromFile(TiffName, Page);  // Options = TIFF stránka
      if ImageID = 0 then
        Exit;                     // nepodporovaná nebo poškozená obrazová data
      Lib.SelectImage(ImageID);
      case Lib.ImageResolutionUnits of
        2: DPI := Lib.ImageHorizontalResolution;          // bodů na palec
        3: DPI := Lib.ImageHorizontalResolution * 2.54;   // bodů 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;

Žádné větvení podle formátu není potřeba a AddImageFromStream a AddImageFromString se pro TIFF data v paměti drží stejných pravidel

Starý JPEG: tabulky na offsetech, stripy bez hlaviček

Od v3.713.0 importuje PDFlibPas staré JPEG TIFFy (Compression = 6), jejichž tabulky přichází z offset tagů JPEGQTables, JPEGDCTables a JPEGACTables nebo ze streamu JPEGInterchangeFormat obsahujícího jen tabulky. TIFF 6.0 Section 22 tenhle layout definoval a TIFF Technical Note 2 ho později nahradila Compression = 7 a sdíleným tagem JPEGTables, ale skenery psaly starou formu ještě roky

Stranu kontejneru, BigTIFF a tiled layoutů, pokrývá zápis o hardeningu TIFF dekodéru; tady se pozornost soustředí na JPEG payload. Každý tag s offsetem tabulky ukazuje na surovou 64bajtovou kvantizační tabulku nebo na Huffman tabulku z 16 countů následovaných svými hodnotami, jednu položku na komponentu. PDFlibPas z nich udělá segmenty DQT a DHT v syntetizovaném streamu JPEGTables. Každý strip holých entropicky kódovaných dat pak dostane vlastní hlavičku: SOI, frame marker nesoucí počet řádků daného stripu, DRI, když je nastavené JPEGRestartInterval, SOS, entropická data a EOI. Stripy se nedají slepit do jednoho scanu, protože starý JPEG restartuje DC predikci na každé hranici stripu

Dva detaily jdou snadno pokazit. Tři komponenty YCbCr obrázku často míří svými offsety tabulek na tytéž bajty, takže PDFlibPas deduplikuje tabulky podle obsahu, než přidělí čísla tabulek. Pokud zůstane víc než dvě Huffman tabulky jedné třídy, syntetizovaný frame použije SOF1 místo SOF0, protože baseline frame smí odkazovat jen na dvě DC a dvě AC tabulky. Když JPEGInterchangeFormat míří na kompletní JPEG stream obsahující scany, bere PDFlibPas tenhle stream jako celý obrázek a importuje ho jako jediný strip nového stylu. Lossless, tiled a separate-plane starý JPEG se odmítá s explicitní chybou místo hádání, takže tyhle případy konvertujte na novostylý JPEG už před importem

Proč vychází CMYK JPEG uvnitř TIFFu inverzně?

CMYK JPEG uvnitř TIFFu vychází inverzně, když importér aplikuje konvenci Adobe APP14, která patří samostatným JPEG souborům. Photoshop zapisuje invertované CMYK do samostatných JPEG, takže PDF producenti tam tradičně přidávají /Decode [1 0 1 0 1 0 1 0] jako kompenzaci. TIFF PhotometricInterpretation 5 definuje vzorky jako množství inkoustu, kde 0 znamená bez inkoustu, a hlavní TIFF čtečky berou dekódované JPEG vzorky tak, jak jsou, bez koukání na APP14

PDFlibPas vkládá JPEG komprimované CMYK TIFF stripy bez invertovaného pole Decode, ať marker APP14 je, nebo není. Příznak transformace APP14 i tak rozhoduje o tom, jestli se YCCK konvertuje na CMYK, a tenhle krok zvládá DCTDecode sám. Samostatné čtyřkomponentové JPEG soubory si nechávají invertovanou Adobe konvenci, takže identické JPEG bajty se podle kontejneru zacházejí různě, jak oba formáty vyžadují

Proč se dekódování lossless JPEG rozbije na pascalském posunu?

Dekódování lossless JPEG se v Pascalu rozbije, protože prediktory 5 a 6 potřebují aritmetický posun doprava a pascalský shr je logický. SOF3 predikuje každý vzorek z levého (Ra), horního (Rb) a horního levého (Rc) souseda a kóduje jen rozdíl. Ze sedmi prediktorů v T.81 Table H.1 je prediktor 5 Ra + ((Rb - Rc) >> 1) a prediktor 6 Rb + ((Ra - Rc) >> 1) a ta závorkovaná hodnota jde do záporných pokaždé, když je horní levý soused ten světlejší. Pošlete záporný Integer do shr a bit znaménka se stane bitem velikosti, takže obraz se rozpadne v šum

Chování posunů pro prediktory lossless JPEG v Delphi a PDFlibPas: logický shr poškodí záporné hodnoty, div 2 zaokrouhluje k nule, HalfDown floonuje jako referenční dekodér
prediktory 5 a 6 potřebují floor semantiku a malý helper odpovídá referenčnímu dekodéru na Delphi i Free Pascal

Vyměnit posun za div 2 to taky neopraví. div zaokrouhluje k nule, zatímco aritmetický posun k zápornému nekonečnu, a na každé liché záporné hodnotě se oba neshodnou. Free Pascal má SarLongint ve své unitě System; Delphi ekvivalent nemá, takže PDFlibPas používá helper, který se kompiluje identicky na obou:

// Aritmetický posun doprava o jedničku: floor(Value / 2)
// shr je logický posun a div 2 zaokrouhluje k nule
function HalfDown(Value: Integer): Integer;
begin
  if Value >= 0 then
    Result := Value shr 1
  else
    Result := -((1 - Value) shr 1);
end;

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

Stranu šířek typů téže třídy problémů popisuje pasti Longint a ukazatelů na Delphi Linux64

Zbytek lossless dekodéru má vlastní pravidla na hranicích. První řádek a první řádek po každém restart markeru predikuje jen z levého souseda a startuje na 2^(P−Pt−1) pro přesnost P a point transform Pt; každý další řádek startuje od vzorku nad ním. Rekonstrukce se obalí modulo 2^16 a point transform škáluje jen výstup, nikdy predikční řetězec. Huffman DC kategorie 16, rozdíl 32768 bez extra bitů, je legální jen v lossless režimu, takže parser tabulek ji musí přijmout, ještě než ví, který proces frame deklaruje

PDFlibPas podporuje všech sedm prediktorů, point transformy a restart intervaly pokrývající celé řádky. JPEG přesnost se musí rovnat BitsPerSample a být 8 nebo 16 a každá komponenta musí být samplovaná 1x1. YCbCr lossless data a 12bitový lossless JPEG se odmítají s explicitní chybou

12bitové DCT a aritmetické kódování v nativním Pascalu

12bitový a aritmetický dekodér v PDFlibPas sdílí jednu inverzní DCT a liší se jen v dekódování entropie. V 12bitovém Huffman JPEG sahají DC kategorie do 15 a AC kategorie do 14, kvantizační tabulky mohou nést 16bitové položky a DC predikce se resetuje při každém scanu a každém restartu. Koeficienty zůstávají v pořadí zigzag až do dequantizace, která musí použít kvantizační hodnotu na stejném zigzag indexu, a teprve potom se přesunou do přirozeného pořadí pro separovatelnou dvouprůchodovou IDCT. Dekódované vzorky dopadají do ±1 od nezávislého referenčního dekodéru s plovoucí čárkou

Aritmetické kódování nahrazuje Huffman QM kódovačem z T.81 Annex D: tabulka pravděpodobnostních stavů se 114 položkami, DC statistická oblast o 64 binách a AC oblast o 256 binách, řízené conditioning parametry ze segmentu DAC (defaultně L = 0, U = 1, Kx = 5). Statistiky, DC kontext a predikce se resetují v každém restart intervalu a na začátku každého scanu. Proto PDFlibPas nechává segmenty DAC, když normalizuje sdílené JPEGTables pro aritmetické obrázky; bez nich se každý strip dekóduje se špatným statistickým modelem

v3.723.0 přidala do těchhle dekódovaných cest subsamplované komponenty a konverzi YCbCr-to-RGB podle JFIF rovnic a v3.724.0 přidala progresivní scany pro 12bitová Huffman (SOF2) i aritmetická (SOF10) data. Subsamplovaná chroma se replikuje na plné rozlišení místo vyhlazování, což je to, co produkuje referenční dekodér s vypnutým chroma smoothingem. Hierarchický JPEG a aritmeticky kódovaný lossless JPEG (SOF11) se odmítají s explicitní chybou

Testování binárních JPEG fixture v Delphi

Ručně poskládané JPEG fixture v unit testech v Delphi můžou selhat přesně tak, jako by se pokazil dekodér. Řetězcová konstanta poskládaná ze znaků #$xx, která přesáhne 255 znaků, dostane typ UnicodeString a přiřazení do AnsiString ji překonvertuje přes aktivní kódovou stránku. Bajty $80 až $9F, které jsou v entropicky kódovaných datech běžné, se při tom přepíšou. Skládání hlaviček markerů za běhu má stejnou past: v Delphi dává AnsiChar + AnsiChar UnicodeString, takže jednotlivé bajty přidávejte do AnsiString nebo je zapište přes SetLength a indexované přiřazení. Pro fixture uložte hex text a dekódujte ho za běhu:

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 zůstává čisté ASCII, takže se ho kódová stránka nedotkne
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);

Když generujete testovací obrázky, počítejte s tím, že jpegtran -arithmetic píše sequential SOF9; pro SOF10 potřebuje i -progressive. A nenulové image ID dokazuje jen to, že se import přijal. Vykreslete stránku a porovnejte pixely, protože omyl v propustné cestě vyrobí formálně správné PDF, které nikdo neumí nakreslit

Legacy skenovací archivy do PDF

Tyhle JPEG varianty se během migrace archivu obvykle vynoří najednou. PDFlibPas čte TIFFy s 12bitovým, lossless, aritmetickým a starým JPEG přes stejné volání AddImageFromFile jako všechno ostatní, drží v PDF plnou přesnost a vzácné procesy mimo tu sadu odmítá s explicitní chybou. Licence, kompletní seznam funkcí a trial ke stažení najdete na produktové stránce PDFlibPas PDF Library for Delphi