Teknisk artikel

TIFF med 12-bit, tabsfri og aritmetisk JPEG til PDF i Delphi

PDFlibPas, PDF-biblioteket til Delphi, importerer TIFF-billeder, hvis JPEG-komprimering går ud over 8-bit baseline: 12-bit extended sequential (SOF1), tabsfri Huffman (SOF3), aritmetisk kodet (SOF9 og SOF10) og gammeldags TIFF 6.0 JPEG med tabeloffsets. Fordi PDF DCTDecode ikke kan bære de processer, afkoder PDFlibPas dem i native Pascal og indlejrer samplesne tabsfrit med 8 eller 16 bit per komponent

Hele sættet kræver v3.724.0 eller nyere og sigter direkte på TIFF'er fra scannere, medicinsk og videnskabelig capture-software og arkivsystemer frosset fast i 1990'erne. Nedenfor er hvorfor disse varianter ikke kan kopieres ind i PDF'en som de er, hvordan PDFlibPas lander dem, og de Pascal-fælder, der blev fundet på vejen

Hvorfor kan en 12-bit JPEG ikke gå direkte ind i en PDF?

En 12-bit JPEG kan ikke sendes igennem, for PDF har intet filter, der er forpligtet til at afkode den. ISO 32000-1 §7.4.8 definerer DCTDecode omkring JPEG-baselineformatet, med progressive-udvidelsen tilføjet i PDF 1.3. Intet i den klausul dækker 12-bit sample-præcision, den tabsfrie proces i ITU-T T.81 Annex H eller den aritmetiske entropikoder i Annex D. En læser, der møder en SOF1 12-bit frame eller en SOF9-markør inde i en DCTDecode-stream, har ret til ikke at tegne noget, og de fleste viewers gør præcis det

Til de 8-bit Huffman baseline- og progressive strips, som udgør de fleste JPEG-i-TIFF-filer, validerer PDFlibPas stadig hvert scan og sender de komprimerede byte igennem som DCTDecode, hvilket holder PDF'en lille. Alt andet afkodes. PDFlibPas læser frame-markøren og sample-præcisionen i den første JPEG-blok, og når den finder SOF3, SOF9, SOF10 eller en præcision andet end 8, går siden gennem sample-decoderen i stedet for pass-through-vejen. At genkende aritmetisk kodning på dens frame-markør betyder mere, end det lyder: et tjek, der kun kigger på sample-præcision, lader en klassisk TIFF smugle en 8-bit SOF9-strip ind i DCTDecode-stien og producere en PDF, læsere ikke kan vise

Beslutningsflow for TIFF JPEG-blokke i PDFlibPas til Delphi: frame-markøren ruter 8-bit scans til DCTDecode og andre processer til native afkodning
frame-markøren, ikke sample-præcisionen alene, afgør om en TIFF JPEG-strip sendes igennem som DCTDecode eller afkodes nativt

Hvad PDFlibPas skriver ind i PDF'en i stedet

PDFlibPas gør afkodede JPEG-blokke til et almindeligt image XObject med BitsPerComponent 8 eller 16. Samplesne gemmes ukomprimeret som default; kald CompressImages(1) før billedet tilføjes, og PDFlibPas skriver dem med FlateDecode. ISO 32000-1 §8.9.5.1 tillader 16 bit per komponent fra PDF 1.5, så PDFlibPas hæver dokumentversionen til mindst 1.5, hver gang et billede med 16-bit samples tilføjes. Det inkluderer multi-strip- og tiled-billeder, hvis rodobjekt er et Form XObject uden eget BitsPerComponent at inspicere

Mappingen er fast pr. JPEG-proces:

  • 12-bit Huffman JPEG (sequential SOF1, progressive SOF2) dekvantiseres, inverse-transformeres og udvides til 16-bit samples
  • Aritmetisk kodet JPEG (sequential SOF9, progressive SOF10) bliver til 8-bit samples ved 8-bit præcision og 16-bit samples ved 12-bit præcision
  • Tabsfri Huffman JPEG (SOF3) bliver til 8-bit eller 16-bit samples, præcist som dens deklarede præcision

Udvidelsen fra 12 til 16 bit bruger bit-replikering, (V shl 4) or (V shr 8), så 4095 mappes til 65535, og hvid forbliver hvid i stedet for at toppe ved 65520. PDFlibPas nedgraderer aldrig disse billeder til 8 bit i det skjulte. Når biblioteket rasteriserer en sådan side til sin egen visningsflade, bruger det den høje byte af hver sample, men det indlejrede PDF-billede beholder alle seksten

Mapping af afkodede JPEG-processer til samplebredder i PDFlibPas til Delphi: 12-bit Huffman udvides til 16 bit, aritmetisk og tabsfri mappes efter deklareret præcision
hver JPEG-proces har en fast samplebredde i PDF'en, og 12-bit data udvides med bit-replikering, så hvid forbliver hvid

Prisen er filstørrelse: selv med CompressImages(1) er Flate-komprimerede 16-bit samples større end de DCT-data, de kom fra, og uden den er de større endnu. Til diagnostiske og arkivmæssige workflows er det afkald normalt den rigtige handel. Hvis størrelsen betyder mere i et givet job, viser guiden til at reducere PDF-filstørrelse i Delphi, hvor byte faktisk går hen

Import af disse TIFF'er med TPDFlib

Den kaldende kode ændres slet ikke. TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer tager TIFF-sidenummeret (1-baseret) i Options og returnerer et image ID, eller 0, når siden ikke kan importeres. Ikke-understøttede JPEG-varianter afvises, før billedet indsættes, så en 0 betyder, at intet halvafkodet nåede dokumentet

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);   // default user space, 1/72 tomme
    Lib.SetOrigin(1);             // top-venstre origo for DrawImage
    Lib.CompressImages(1);        // Flate til afkodede samples
    PageCount := Lib.GetImagePageCount(TiffName);
    if PageCount = 0 then
      Exit;
    for Page := 1 to PageCount do
    begin
      ImageID := Lib.AddImageFromFile(TiffName, Page);  // Options = TIFF-side
      if ImageID = 0 then
        Exit;                     // ikke-understøttet eller beskadiget billeddata
      Lib.SelectImage(ImageID);
      case Lib.ImageResolutionUnits of
        2: DPI := Lib.ImageHorizontalResolution;          // punkter pr. inch
        3: DPI := Lib.ImageHorizontalResolution * 2.54;   // punkter pr. 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;

Der skal ingen forgrening pr. format til, og AddImageFromStream og AddImageFromString følger de samme regler for TIFF-data i hukommelsen

Gammeldags JPEG: tabeller på offsets, strips uden headers

Siden v3.713.0 importerer PDFlibPas gammeldags JPEG-TIFF'er (Compression = 6), hvis tabeller kommer fra JPEGQTables-, JPEGDCTables- og JPEGACTables-offsettagene eller fra en tables-only JPEGInterchangeFormat-stream. TIFF 6.0 Section 22 definerede det layout, og TIFF Technical Note 2 erstattede det senere med Compression = 7 og et delt JPEGTables-tag, men scannere fortsatte med at skrive den gamle form i årevis

Containersiden, BigTIFF og tiled-layouts, er dækket i indlægget om hardening af TIFF-decoderen; her er fokusset JPEG-payloaden. Hvert tabeloffsettag peger på en rå 64-byte kvantiseringstabel eller på en Huffman-tabel med 16 counts efterfulgt af dens værdier, én post pr. komponent. PDFlibPas omsætter dem til DQT- og DHT-segmenter i en syntetiseret JPEGTables-stream. Hver strip af ren entropy-kodet data får derefter sin egen header: SOI, en frame-markør med stripens rækkeantal, DRI når JPEGRestartInterval er sat, SOS, entropy-dataene og EOI. Stripsene kan ikke limes sammen til ét scan, for gammeldags JPEG nulstiller DC-prædiktion ved hver stripgrænse

To detaljer er lette at få forkert. De tre komponenter i et YCbCr-billede peger ofte deres tabeloffsets på de samme byte, så PDFlibPas deduplikerer tabeller efter indhold, før den tildeler tabelnumre. Hvis der er mere end to Huffman-tabeller af én klasse tilbage, bruger den syntetiserede frame SOF1 i stedet for SOF0, for en baseline-frame må kun referere to DC- og to AC-tabeller. Når JPEGInterchangeFormat i stedet peger på en komplet JPEG-stream, der indeholder scans, behandler PDFlibPas den stream som hele billedet og importerer den som én enkelt strip i nyt format. Tabsfri, tiled og separate-plane gammeldags JPEG afvises med en eksplicit fejl i stedet for at blive gættet, så konvertér dem til JPEG i nyt format upstream

Hvorfor kommer en CMYK JPEG inde i en TIFF ud inverteret?

En CMYK JPEG inde i en TIFF kommer ud inverteret, når importeren anvender Adobe APP14-konventionen, som hører til selvstændige JPEG-filer. Photoshop skriver inverteret CMYK i selvstændige JPEG'er, så PDF-producenter tilføjer traditionelt /Decode [1 0 1 0 1 0 1 0] for at kompensere. TIFF PhotometricInterpretation 5 definerer samples som trykfarvemængder, hvor 0 betyder ingen farve, og gængse TIFF-læsere tager de afkodede JPEG-samples som de er uden at kigge på APP14

PDFlibPas indlejrer JPEG-komprimerede CMYK TIFF-strips uden det inverterede Decode-array, uanset om en APP14-markør er til stede eller ej. APP14-transformflagget afgør stadig, om YCCK konverteres til CMYK, og DCTDecode håndterer det trin selv. Selvstændige firekomponent-JPEG-filer beholder den inverterede Adobe-konvention, så identiske JPEG-byte behandles forskelligt afhængigt af containeren, netop som de to formater kræver

Hvorfor knækker tabsfri JPEG-afkodning på et Pascal-shift?

Tabsfri JPEG-afkodning knækker i Pascal, for prediktor 5 og 6 kræver et aritmetisk right shift, og Pascals shr er et logisk ét. SOF3 prædikterer hver sample ud fra sin venstre (Ra), øvre (Rb) og øvre-venstre (Rc) nabo og koder kun forskellen. Af de syv prediktorer i T.81 Table H.1 er prediktor 5 Ra + ((Rb - Rc) >> 1) og prediktor 6 Rb + ((Ra - Rc) >> 1), og den klammede forskel bliver negativ, hver gang den øvre-venstre nabo er den lyseste. Giver du en negativ Integer til shr, bliver signbit en værdibit, så billedet opløses i støj

Shift-adfærd for tabsfri JPEG-prediktorer i Delphi og PDFlibPas: logisk shr ødelægger negative værdier, div 2 trunkerer mod nul, HalfDown runder ned som referencen
prediktor 5 og 6 behøver floor-semantik, og en lille hjælper matcher referencedecoderen på Delphi og Free Pascal

At bytte shiftet ud med div 2 løser det heller ikke. div trunkerer mod nul, mens et aritmetisk shift runder mod negativ uendelighed, og de to er uenige om hver ulige negativ værdi. Free Pascal har SarLongint i sin System-unit; Delphi har ingen ækvivalent, så PDFlibPas bruger en hjælpefunktion, der kompilerer ens på begge:

// Aritmetisk right shift med én: floor(Value / 2)
// shr er et logisk shift, og div 2 trunkerer mod nul
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

Til typebreddesiden af samme problemklasse, se Longint- og pointerfælder på Delphi Linux64

Resten af den tabsfrie decoder har egne grænseregler. Første række, og første række efter hver restart-markør, prædikterer kun ud fra venstre nabo og starter ved 2^(P−Pt−1) for præcision P og point transform Pt; hver senere række starter fra samplen ovenover. Rekonstruktionen wrappes modulo 2^16, og point transform skalerer kun outputtet, aldrig prædiktionkæden. Huffman DC-kategori 16, en forskel på 32768 uden ekstra bit, er kun lovlig i tabsfri tilstand, så tabelparseren skal acceptere den, før den ved, hvilken proces frame'en vil deklarere

PDFlibPas understøtter alle syv prediktorer, point transforms og restart-intervaller, der dækker hele rækker. JPEG-præcisionen skal være lig BitsPerSample og være 8 eller 16, og hver komponent skal være samplet 1x1. YCbCr tabsfri data og 12-bit tabsfri JPEG afvises med en eksplicit fejl

12-bit DCT og aritmetisk kodning i native Pascal

12-bit- og aritmetik-decoderne i PDFlibPas deler én invers DCT og adskiller sig kun i entropy-afkodning. I 12-bit Huffman JPEG når DC-kategorierne 15 og AC-kategorierne 14, kvantiseringstabeller kan have 16-bit-poster, og DC-prædiktionen nulstilles ved hvert scan og hver restart. Koefficienterne bliver i zigzag-orden indtil dekvantiseringen, som skal bruge kvantiseringsværdien ved samme zigzag-indeks, og først derefter går de over til naturlig orden for den separerbare to-pass IDCT. De afkodede samples lander inden for ±1 af en uafhængig floating-point-referencedecoder

Aritmetisk kodning erstatter Huffman med QM-koderen fra T.81 Annex D: en sandsynlighedstilstandstabel med 114 indgange, et DC-statistikområde med 64 bins og et AC-område med 256 bins, styret af conditioning-parametre fra DAC-segmentet (defaults L = 0, U = 1, Kx = 5). Statistik, DC-kontekst og prædiktion nulstilles ved hvert restart-interval og ved starten af hvert scan. Det er derfor, PDFlibPas beholder DAC-segmenter, når den normaliserer delt JPEGTables for aritmetiske billeder; dropper du dem, afkodes hver strip med den forkerte statistikmodel

v3.723.0 tilføjede subsamplede komponenter og YCbCr-til-RGB-konvertering med JFIF-ligningerne til disse afkodede veje, og v3.724.0 tilføjede progressive scans til både 12-bit Huffman (SOF2) og aritmetisk (SOF10) data. Subsampled chroma replikeres til fuld opløsning i stedet for at blive udglattet, hvilket er det, en referencedecoder producerer med chroma smoothing slået fra. Hierarkisk JPEG og aritmetisk kodet tabsfri JPEG (SOF11) afvises med en eksplicit fejl

Test af binære JPEG-fixtures i Delphi

Håndbyggede JPEG-fixtures i Delphi unittests kan fejle på en måde, der ligner en decoderbug på prikken. En strengkonstant bygget af #$xx-tegn, der løber over 255 tegn, typet som UnicodeString, og tildeling til en AnsiString konverterer den gennem den aktive kodepage. Byte $80 til $9F, som er rutine i entropy-kodet data, bliver omskrevet på vejen ind. At bygge markør-headers på run time har samme fælde: i Delphi giver AnsiChar + AnsiChar en UnicodeString, så append enkelte byte til en AnsiString eller skriv dem med SetLength og indekseret tildeling. Til fixtures: gem hex-tekst og afkod den på run time:

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;

// Fixtureen forbliver ren ASCII, så ingen kodepage kan røre den
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);

Når du genererer testbilleder, så bemærk at jpegtran -arithmetic skriver sequential SOF9; SOF10 kræver også -progressive. Og et image ID forskelligt fra nul beviser kun, at importen blev accepteret. Render siden og sammenlign pixels, for en pass-through-fejl producerer en velformet PDF, som ingen viewer kan tegne

Legacy-scanarkiver ind i PDF

Disse JPEG-varianter dukker typisk op på én gang under en arkivmigrering. PDFlibPas læser 12-bit, tabsfri, aritmetisk og gammeldags JPEG-TIFF'er gennem det samme AddImageFromFile-kald som alt andet, beholder fuld præcision i PDF'en og afviser de sjældne processer uden for det sæt med en eksplicit fejl. Til licensering, den fulde featureliste og trial-downloads, se PDFlibPas PDF Library for Delphi productsiden