Technisch artikel

12-bit, lossless en arithmetic JPEG in PDFs met Delphi

PDFlibPas, de PDF-bibliotheek voor Delphi, importeert TIFF-afbeeldingen waarvan de JPEG-compressie verder gaat dan 8-bit baseline: 12-bit extended sequential (SOF1), Huffman lossless (SOF3), arithmetic-coded (SOF9 en SOF10) en old-style TIFF 6.0 JPEG met table-offsets. Omdat PDF DCTDecode die processen niet kan dragen, decodeert PDFlibPas ze in native Pascal en sluit de samples verliesvrij in met 8 of 16 bits per component

De volledige set vraagt om v3.724.0 of nieuwer en is scherp gericht op TIFFs van scanners, medische en wetenschappelijke capture-software en archiefsystemen die in de jaren negentig zijn vastgevroren. Hieronder staat waarom deze varianten niet 1-op-1 in de PDF te kopieren zijn, hoe PDFlibPas ze naar binnen werkt en welke Pascal-valkuilen we onderweg vonden

Waarom kan een 12-bit JPEG niet rechtstreeks een PDF in?

Een 12-bit JPEG kun je niet doorlaten lopen, want PDF heeft geen filter dat die moet kunnen decoderen. ISO 32000-1 §7.4.8 definieert DCTDecode rond het JPEG-baselineformaat, met de progressive-uitbreiding er sinds PDF 1.3 bij. Niets in die clausule dekt 12-bit sampleprecisie, het lossless-proces van ITU-T T.81 Annex H of de arithmetic-entropiecoder van Annex D. Een reader die in een DCTDecode-stream een SOF1 12-bit frame of een SOF9-marker tegenkomt, mag er net zo goed niets van tekenen, en de meeste viewers doen precies dat

Voor de 8-bit Huffman-baseline- en progressive-strips die het merendeel van de JPEG-in-TIFF-bestanden uitmaken, valideert PDFlibPas elke scan nog steeds en geeft de gecomprimeerde bytes door als DCTDecode, wat de PDF klein houdt. Al het andere wordt gedecodeerd. PDFlibPas leest de framemarker en sampleprecisie van het eerste JPEG-blok, en als hij SOF3, SOF9, SOF10 of een andere precisie dan 8 vindt, gaat de pagina door de sampledecoder in plaats van via de doorgifte-route. Arithmetic-coding herkennen aan de framemarker is belangrijker dan het klinkt: een controle die alleen naar de sampleprecisie kijkt, laat een klassieke TIFF een 8-bit SOF9-strip de DCTDecode-route in glippen en een PDF opleveren die readers niet kunnen tonen

Beslissingsstroom voor TIFF-JPEG-blokken in PDFlibPas voor Delphi: de framemarker stuurt 8-bit scans naar DCTDecode en andere processen naar native decoding
de framemarker, niet de sampleprecisie alleen, bepaalt of een TIFF JPEG-strip doorgaat als DCTDecode of native wordt gedecodeerd

Wat PDFlibPas er in plaats daarvan in de PDF schrijft

PDFlibPas maakt van gedecodeerde JPEG-blokken een gewone image XObject met BitsPerComponent 8 of 16. De samples worden standaard ongecomprimeerd opgeslagen; roep CompressImages(1) aan voordat je het beeld toevoegt en PDFlibPas schrijft ze met FlateDecode. ISO 32000-1 §8.9.5.1 staat 16 bits per component toe vanaf PDF 1.5, dus PDFlibPas verhoogt de documentversie naar minstens 1.5 zodra er een beeld met 16-bit samples wordt toegevoegd. Dat geldt ook voor multi-strip- en tiled-afbeeldingen, waarvan het rootobject een Form XObject is zonder eigen BitsPerComponent om te bekijken

De mapping ligt per JPEG-proces vast:

  • 12-bit Huffman JPEG (sequential SOF1, progressive SOF2) wordt gedequantiseerd, invers getransformeerd en verbreed naar 16-bit samples
  • Arithmetic-coded JPEG (sequential SOF9, progressive SOF10) wordt 8-bit samples bij 8-bit precisie en 16-bit samples bij 12-bit precisie
  • Huffman lossless JPEG (SOF3) wordt 8-bit of 16-bit samples, exact passend bij de opgegeven precisie

Het verbreden van 12 naar 16 bits gebruikt bitreplicatie, (V shl 4) or (V shr 8), dus 4095 wordt 65535 en wit blijft wit in plaats van vast te lopen op 65520. PDFlibPas verlaagt deze afbeeldingen nooit stilletjes naar 8 bits. Als de bibliotheek zo'n pagina rasteriseert voor zijn eigen displaysurface gebruikt hij de high byte van elke sample, maar het ingesloten PDF-beeld houdt alle zestien bits vast

Mapping van gedecodeerde JPEG-processen op samplebreedtes in PDFlibPas voor Delphi: 12-bit Huffman verbreedt naar 16 bits, arithmetic en lossless volgen de opgegeven precisie
elk JPEG-proces heeft een vaste samplebreedte in de PDF en 12-bit data wordt door bitreplicatie verbreed zodat wit wit blijft

De prijs is de bestandsgrootte: ook met CompressImages(1) zijn met Flate gecomprimeerde 16-bit samples groter dan de DCT-data waar ze vandaan kwamen, en zonder nog groter. Voor diagnostische en archiefworkflows is die afweging meestal juist. Telt grootte voor een bepaalde klus zwaarder, dan laat de gids voor het verkleinen van de PDF-bestandsgrootte in Delphi zien waar de bytes echt naartoe gaan

Deze TIFFs importeren met TPDFlib

De aanroepende code verandert helemaal niet. TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer krijgt in Options het TIFF-paginanummer (1-based) mee en geeft een image-ID terug, of 0 als de pagina niet te importeren is. Niet-ondersteunde JPEG-varianten worden geweigerd voordat het beeld wordt ingevoegd, dus een 0 betekent dat er niets half gedecodeerd in het document is beland

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);   // standaard user space, 1/72 inch
    Lib.SetOrigin(1);             // origine linksboven voor DrawImage
    Lib.CompressImages(1);        // Flate voor gedecodeerde samples
    PageCount := Lib.GetImagePageCount(TiffName);
    if PageCount = 0 then
      Exit;
    for Page := 1 to PageCount do
    begin
      ImageID := Lib.AddImageFromFile(TiffName, Page);  // Options = TIFF-pagina
      if ImageID = 0 then
        Exit;                     // niet-ondersteunde of beschadigde beelddata
      Lib.SelectImage(ImageID);
      case Lib.ImageResolutionUnits of
        2: DPI := Lib.ImageHorizontalResolution;          // dots per inch
        3: DPI := Lib.ImageHorizontalResolution * 2.54;   // dots per 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;

Eén vertakking per formaat is niet nodig, en AddImageFromStream en AddImageFromString volgen dezelfde regels voor TIFF-data die in het geheugen staat

Old-style JPEG: tabellen op offsets, strips zonder headers

Sinds v3.713.0 importeert PDFlibPas old-style JPEG-TIFFs (Compression = 6) waarvan de tabellen uit de offsettags JPEGQTables, JPEGDCTables en JPEGACTables komen of uit een stream met alleen tabellen via JPEGInterchangeFormat. TIFF 6.0 Section 22 definieerde die opzet en TIFF Technical Note 2 verving haar later door Compression = 7 met een gedeelde JPEGTables-tag, maar scanners schreven de oude vorm nog jarenlang weg

De containerkant, BigTIFF en tiled-layouts, is behandeld in het verslag over het verharden van de TIFF-decoder; hier draait het om de JPEG-payload. Elke table-offsettag wijst naar een rauwe quantisatietabel van 64 bytes, of naar een Huffman-tabel van 16 counts gevolgd door zijn waarden, één item per component. PDFlibPas maakt daarvan DQT- en DHT-segmenten in een gesynthetiseerde JPEGTables-stream. Elke strip met pure entropy-coded data krijgt dan een eigen header: SOI, een framemarker met het rijenaantal van die strip, DRI als JPEGRestartInterval is gezet, SOS, de entropydata en EOI. De strips zijn niet aan één scan te plakken, want old-style JPEG herstart de DC-voorspelling bij elke stripgrens

Twee details ga je makkelijk fout. De drie componenten van een YCbCr-beeld wijzen hun table-offsets vaak naar dezelfde bytes, dus PDFlibPas ontdubbelt tabellen op inhoud voordat hij tabelnummers toewijst. Blijven er meer dan twee Huffman-tabellen van één klasse over, dan gebruikt het gesynthetiseerde frame SOF1 in plaats van SOF0, want een baselineframe mag maar naar twee DC- en twee AC-tabellen verwijzen. Wijst JPEGInterchangeFormat in plaats daarvan naar een complete JPEG-stream met scans, dan behandelt PDFlibPas die stream als het hele beeld en importeert hij hem als één strip in nieuwe stijl. Lossless, tiled en separate-plane old-style JPEG worden met een expliciete foutmelding geweigerd in plaats van ergens op te gokken, dus converteer die upstream naar new-style JPEG

Waarom komt een CMYK JPEG in een TIFF geïnverteerd uit de bus?

Een CMYK JPEG in een TIFF komt geïnverteerd uit de bus wanneer de importer de Adobe APP14-conventie toepast die bij losse JPEG-bestanden hoort. Photoshop schrijft geïnverteerde CMYK naar losse JPEGs, dus PDF-producenten voegen traditioneel /Decode [1 0 1 0 1 0 1 0] toe als compensatie. TIFF PhotometricInterpretation 5 definieert samples als inkt Hoeveelheden waarbij 0 geen inkt betekent, en gangbare TIFF-readers nemen de gedecodeerde JPEG-samples voor wat ze zijn, zonder naar APP14 te kijken

PDFlibPas sluit JPEG-gecomprimeerde CMYK TIFF-strips in zonder de geïnverteerde Decode-array, of er nu een APP14-marker staat of niet. De APP14-transformvlag bepaalt nog steeds of YCCK naar CMYK wordt omgezet, en DCTDecode doet die stap vanzelf. Losse JPEG-bestanden met vier componenten houden de geïnverteerde Adobe-conventie, dus identieke JPEG-bytes worden per container anders behandeld, zoals de twee formaten dat vragen

Waarom loopt lossless JPEG-decoderen vast op een Pascal-shift?

Lossless JPEG decoderen breekt in Pascal omdat voorspellers 5 en 6 een rekenkundige shift naar rechts nodig hebben en de shr van Pascal een logische is. SOF3 voorspelt elke sample vanuit de linker- (Ra), boven- (Rb) en linksboven-buur (Rc) en codeert alleen het verschil. Van de zeven voorspellers in T.81 Table H.1 is voorspeller 5 Ra + ((Rb - Rc) >> 1) en voorspeller 6 Rb + ((Ra - Rc) >> 1), en het verschil tussen haakjes wordt negatief zodra de linksboven-buur de lichtere is. Geef een negatieve Integer aan shr en het tekenbit wordt een magnituudbit, dus het beeld lost op in ruis

Shiftgedrag voor lossless JPEG-voorspellers in Delphi en PDFlibPas: logische shr corrumpeert negatieve waarden, div 2 kapt richting nul af, HalfDown rondt naar beneden af zoals de referentie
voorspellers 5 en 6 hebben floor-semantiek nodig en een kleine hulpfunctie matcht de referentiedecoder op Delphi en Free Pascal

De shift vervangen door div 2 lost het ook niet op. div kapt richting nul af terwijl een rekenkundige shift richting negatief oneindig rondt, en de twee verschillen op elke oneven negatieve waarde. Free Pascal heeft SarLongint in zijn System-unit; Delphi heeft geen equivalent, dus PDFlibPas gebruikt een hulpfunctie die op beide identiek compileert:

// Rekenkundige shift naar rechts met één: floor(Value / 2)
// shr is een logische shift, en div 2 kapt richting nul af
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

Voor de typenbreedtekant van dezelfde klasse problemen, zie de Longint- en pointervalkuilen op Delphi Linux64

De rest van de lossless decoder heeft eigen grensregels. De eerste rij, en de eerste rij na elke restartmarker, voorspelt alleen vanuit de linkernabu en begint bij 2^(P−Pt−1) voor precisie P en point transform Pt; elke latere rij begint vanaf de sample erboven. De reconstructie wikkelt modulo 2^16 en de point transform schaalt alleen de output, nooit de voorspelketen. Huffman DC-categorie 16, een verschil van 32768 zonder extra bits, is alleen in lossless-modus legaal, dus de tabelparser moet die accepteren voordat hij weet welk proces het frame zal opgeven

PDFlibPas ondersteunt alle zeven voorspellers, point transforms en restartintervallen die hele rijen bestrijken. De JPEG-precisie moet gelijk zijn aan BitsPerSample en 8 of 16 zijn, en elke component moet 1x1 gesampled zijn. YCbCr-losslessdata en 12-bit lossless JPEG worden met een expliciete foutmelding geweigerd

12-bit DCT en arithmetic coding in native Pascal

De 12-bit- en arithmetic-decoders in PDFlibPas delen één inverse DCT en verschillen alleen in entropy-decodering. In 12-bit Huffman JPEG reiken DC-categorieën tot 15 en AC-categorieën tot 14, mogen quantisatietabellen 16-bit items bevatten en wordt de DC-voorspelling bij elke scan en elke restart gereset. Coëfficiënten blijven in zigzagvolgorde tot de dequantisatie, die de quantisatiewaarde op dezelfde zigzagindex moet gebruiken, en pas daarna gaan ze naar de natuurlijke volgorde voor de scheidbare two-pass IDCT. Gedecodeerde samples landen binnen ±1 van een onafhankelijke floating-point referentiedecoder

Arithmetic coding vervangt Huffman door de QM-coder van T.81 Annex D: een probabiliteitstabel met 114 items, een DC-statistiekgebied van 64 bins en een AC-gebied van 256 bins, gestuurd door conditioning-parameters uit het DAC-segment (defaults L = 0, U = 1, Kx = 5). Statistieken, DC-context en voorspelling resetten bij elk restartinterval en aan het begin van elke scan. Daarom houdt PDFlibPas DAC-segmenten vast als hij gedeelde JPEGTables normaliseert voor arithmetic-beelden; laat je die vallen, dan decodeert elke strip met het verkeerde statistiekmodel

v3.723.0 voegde gesubsamplede componenten en YCbCr-naar-RGB-conversie met de JFIF-vergelijkingen toe aan deze gedecodeerde paden, en v3.724.0 voegde progressive scans toe voor zowel 12-bit Huffman- (SOF2) als arithmetic-data (SOF10). Gesubsamplede chroma wordt gerepliceerd naar volledige resolutie in plaats van gladgestreken, wat een referentiedecoder oplevert met chroma smoothing uit. Hierarchical JPEG en arithmetic-coded lossless JPEG (SOF11) worden met een expliciete foutmelding geweigerd

Binaire JPEG-fixtures testen in Delphi

Met de hand gebouwde JPEG-fixtures in Delphi-unit tests kunnen op een manier mislukken die precies op een decoderbug lijkt. Een stringconstante van #$xx-tekens die voorbij 255 tekens loopt, wordt als UnicodeString getypeerd, en toewijzen aan een AnsiString converteert haar via de actieve codepage. Bytes $80 tot $9F, die routine zijn in entropy-coded data, worden onderweg herschreven. Markerheaders bouwen op runtime heeft dezelfde valkuil: in Delphi levert AnsiChar + AnsiChar een UnicodeString op, dus append losse bytes aan een AnsiString of schrijf ze met SetLength en toewijzing per index. Sla voor fixtures hex-tekst op en decodeer die op 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;

// De fixture blijft pure ASCII, dus geen codepage kan er iets aan doen
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);

Genereer je testafbeeldingen, let er dan op dat jpegtran -arithmetic sequential SOF9 schrijft; SOF10 vraagt ook om -progressive. En een image-ID ongelijk aan nul bewijst alleen dat de import werd geaccepteerd. Render de pagina en vergelijk pixels, want een doorgifte-fout levert een welgevormde PDF op die geen viewer kan tekenen

Legacy-scanarchieven naar PDF brengen

Deze JPEG-varianten komen tijdens een archiefmigratie meestal in één keer allemaal boven water. PDFlibPas leest 12-bit, lossless, arithmetic en old-style JPEG TIFFs via dezelfde AddImageFromFile-aanroep als alles anders, houdt de volledige precisie in de PDF vast en weigert de zeldzame processen buiten die set met een expliciete foutmelding. Voor licenties, de volledige functielijst en proefdownloads, zie de productpagina van de PDFlibPas PDF Library for Delphi