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
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
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
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