Teknisk artikel

12-bitars, lossless- och aritmetisk JPEG från TIFF i Delphi

PDFlibPas, PDF-biblioteket för Delphi, importerar TIFF-bilder vars JPEG-komprimering går förbi 8-bitars baseline: 12-bitars extended sequential (SOF1), Huffman lossless (SOF3), aritmetiskt kodad (SOF9 och SOF10) och old-style TIFF 6.0 JPEG med tabelloffset. Eftersom PDF DCTDecode inte kan bära de processerna avkodar PDFlibPas dem i ren Pascal och bäddar in samplesna förlustfritt med 8 eller 16 bitar per komponent

Hela uppsättningen kräver v3.724.0 eller senare, och den siktar rak på TIFF:er från skannrar, medicinsk och vetenskaplig insamlingsmjukvara och arkivsystem frusna i 1990-talet. Nedanför handlar det om varför de varianterna inte kan kopieras rakt in i PDF:en som de är, hur PDFlibPas landar dem, och Pascalfällorna på vägen

Varför kan inte en 12-bitars JPEG direkt in i en PDF?

En 12-bitars JPEG kan inte skickas vidare för PDF har inget filter som är skyldigt att avkoda den. ISO 32000-1 §7.4.8 definierar DCTDecode kring JPEG-baselineformatet, med progressive-tillägget tillkommet i PDF 1.3. Ingenting i den klausulen täcker 12-bitars sampleprecision, den förlustfria processen i ITU-T T.81 Annex H, eller den aritmetiska entropikodaren i Annex D. En läsare som möter en SOF1 12-bitars frame eller en SOF9-markör inuti en DCTDecode-ström har all rätt att rita ingenting, och de flesta viewers gör precis det

För de 8-bitars Huffman-baseline- och progressive-strips som utgör de flesta JPEG-i-TIFF-filer validerar PDFlibPas fortfarande varje scan och släpper de komprimerade bytena vidare som DCTDecode, vilket håller PDF:en liten. Allt annat avkodas. PDFlibPas läser frammarkören och sampleprecisionen för det första JPEG-blocket, och när den hittar SOF3, SOF9, SOF10 eller en precision annan än 8 går sidan genom sampleavkodaren i stället för pass-through-vägen. Att detektera aritmetisk kodning via frammarkören betyder mer än det låter: en kontroll som bara tittar på sampleprecision släpper en klassisk TIFF smyga in en 8-bitars SOF9-strip i DCTDecode-vägen och producera en PDF som läsare inte kan visa

Beslutsflöde för TIFF JPEG-block i PDFlibPas för Delphi: frammarkören routar 8-bitars scans till DCTDecode och andra processer till nativ avkodning
frammarkören, inte sampleprecisionen ensam, avgör om en TIFF JPEG-strip passeras vidare som DCTDecode eller avkodas nativt

Vad PDFlibPas skriver in i PDF:en i stället

PDFlibPas gör avkodade JPEG-block till en vanlig image XObject med BitsPerComponent 8 eller 16. Samplesna lagras okomprimerade som standard; anropa CompressImages(1) innan bilden läggs till så skriver PDFlibPas dem med FlateDecode. ISO 32000-1 §8.9.5.1 tillåter 16 bitar per komponent från PDF 1.5, så PDFlibPas höjer dokumentversionen till minst 1.5 närhelst en bild med 16-bitars samples läggs till. Det omfattar multi-strip- och tiled-bilder, vars rotobjekt är en Form XObject utan egen BitsPerComponent att inspektera

Mappningen är fast per JPEG-process:

  • 12-bitars Huffman-JPEG (sequential SOF1, progressive SOF2) dekvantiseras, inverstransformeras och breddas till 16-bitars samples
  • Aritmetiskt kodad JPEG (sequential SOF9, progressive SOF10) blir 8-bitars samples vid 8-bitars precision och 16-bitars samples vid 12-bitars precision
  • Huffman lossless JPEG (SOF3) blir 8-bitars eller 16-bitars samples, exakt matchande sin deklarerade precision

Breddningen från 12 till 16 bitar använder bitreplikering, (V shl 4) or (V shr 8), så 4095 mappas till 65535 och vitt förblir vitt i stället för att toppa vid 65520. PDFlibPas dumpar aldrig de bilderna till 8 bitar bakom din rygg. När biblioteket rasteriserar en sådan sida till sin egen visningsyta använder den den höga byten av varje sample, men den inbäddade PDF-bilden behåller alla sexton

Mappning av avkodade JPEG-processer till samplebredder i PDFlibPas för Delphi: 12-bitars Huffman breddas till 16 bitar, aritmetisk och lossless mappas per deklarerad precision
varje JPEG-process har en fast samplebredd i PDF:en, och 12-bitarsdata breddas med bitreplikering så att vitt förblir vitt

Priset är filstorlek: även med CompressImages(1) är Flate-komprimerade 16-bitars samples större än DCT-data de kom från, och utan den är de större ändå. För diagnostik- och arkiveringsarbetsflöden är den avvägningen oftast rätt. Om storlek väger tyngre för ett visst jobb visar guiden till att minska PDF-filstorlek i Delphi var bytena faktiskt tar vägen

Importera de här TIFF:erna med TPDFlib

Anropskoden ändras inte alls. TPDFlib.AddImageFromFile(Const FileName: WideString; Options: Integer): Integer tar TIFF-sidnumret, 1-baserat, i Options och returnerar ett bild-ID, eller 0 när sidan inte kan importeras. JPEG-varianter utan stöd avvisas innan bilden sätts in, så ett 0 betyder att inget halvavkodat nådde 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);   // standard user space, 1/72 tum
    Lib.SetOrigin(1);             // origo uppe till vänster för DrawImage
    Lib.CompressImages(1);        // Flate för avkodade samples
    PageCount := Lib.GetImagePageCount(TiffName);
    if PageCount = 0 then
      Exit;
    for Page := 1 to PageCount do
    begin
      ImageID := Lib.AddImageFromFile(TiffName, Page);  // Options = TIFF-sida
      if ImageID = 0 then
        Exit;                     // ej stödd eller skadad bilddata
      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;

Ingen per-formatförgrening behövs, och AddImageFromStream och AddImageFromString följer samma regler för TIFF-data i minnet

Old-style JPEG: tabeller på offset, strips utan huvuden

Sedan v3.713.0 importerar PDFlibPas old-style JPEG-TIFF:er (Compression = 6) vars tabeller kommer från offset-taggarna JPEGQTables, JPEGDCTables och JPEGACTables eller från en tables-only JPEGInterchangeFormat-ström. TIFF 6.0 Section 22 definierade den layouten, och TIFF Technical Note 2 ersatte den senare med Compression = 7 och en gemensam JPEGTables-tagg, men skannrar skrev det gamla upplägget i många år

Containersidan, BigTIFF och tiled-layouter, tas upp i genomgången om härdningen av TIFF-avkodaren; här är fokus JPEG-payloaden. Varje tabelloffset-tagg pekar på en rå 64-byte kvantifieringstabell, eller på en Huffmantabell med 16 counts följda av sina värden, en post per komponent. PDFlibPas gör om dem till DQT- och DHT-segment i en syntetiserad JPEGTables-ström. Varje strip av ren entropikodad data får sedan ett eget huvud: SOI, en frammarkör som bär stripens radantal, DRI när JPEGRestartInterval är satt, SOS, entropidatan och EOI. Stripsna kan inte klistras ihop till en scan, för old-style JPEG startar om DC-prediktionen vid varje stripgräns

Två detaljer är lätta att få fel. De tre komponenterna i en YCbCr-bild pekar ofta sina tabelloffset mot samma byte, så PDFlibPas deduplikrar tabeller efter innehåll innan den tilldelar tabellnummer. Om mer än två Huffmantabeller av en klass återstår använder den syntetiserade framen SOF1 i stället för SOF0, eftersom en baseline-frame bara får referera två DC- och två AC-tabeller. När JPEGInterchangeFormat i stället pekar på en komplett JPEG-ström som innehåller scans behandlar PDFlibPas den strömmen som hela bilden och importerar den som en enda new-style-strip. Lossless, tiled och separate-plane old-style JPEG avvisas med ett explicit fel i stället för att gissas fram, så konvertera dem till new-style JPEG uppströms

Varför kommer en CMYK JPEG i en TIFF ut inverterad?

En CMYK JPEG i en TIFF kommer ut inverterad när importören tillämpar Adobe APP14-konventionen som hör hemma i fristående JPEG-filer. Photoshop skriver inverterad CMYK i fristående JPEG:er, så PDF-producenter lägger traditionellt till /Decode [1 0 1 0 1 0 1 0] för att kompensera. TIFF PhotometricInterpretation 5 definierar samples som bläckmängder där 0 betyder ingen bläck, och etablerade TIFF-läsare tar de avkodade JPEG-samplesna till ytan utan att titta på APP14

PDFlibPas bäddar in JPEG-komprimerade CMYK TIFF-strips utan den inverterade Decode-arrayen, oavsett om en APP14-markör finns eller inte. APP14-transformflaggan avgör fortfarande om YCCK konverteras till CMYK, och DCTDecode sköter det steget själv. Fristående JPEG-filer med fyra komponenter behåller den inverterade Adobe-konventionen, så identiska JPEG-byte behandlas olika beroende på container, precis som de två formaten kräver

Varför går lossless JPEG-avkodning sönder på ett skift i Pascal?

Lossless JPEG-avkodning går sönder i Pascal för prediktorerna 5 och 6 behöver ett aritmetiskt högerskift och Pascals shr är ett logiskt. SOF3 predicerar varje sample från sin vänstra (Ra), övre (Rb) och övre vänstra (Rc) granne och kodar bara differensen. Av de sju prediktorerna i T.81 Table H.1 är prediktor 5 Ra + ((Rb - Rc) >> 1) och prediktor 6 Rb + ((Ra - Rc) >> 1), och differensen i parentes blir negativ varje gång den övre vänstra grannen är den ljusare. Mata ett negativt Integer till shr och teckenbiten blir en storleksbit, så bilden löser upp sig i brus

Skiftbeteende för lossless JPEG-prediktorer i Delphi och PDFlibPas: logiskt shr förstör negativa värden, div 2 trunkerar mot noll, HalfDown floorkör som referensen
prediktorerna 5 och 6 behöver floor-semantik, och en liten hjälpfunktion matchar referensavkodaren på Delphi och Free Pascal

Att byta skiftet mot div 2 fixar det inte heller. div trunkerar mot noll medan ett aritmetiskt skift avrundar mot negativ oändlighet, och de två är oeniga om vartannat udda negativt värde. Free Pascal har SarLongint i sin System-unit; Delphi har ingen motsvarighet, så PDFlibPas använder en hjälpfunktion som kompilerar identiskt på båda:

// Aritmetiskt skift åt höger ett steg: floor(Value / 2)
// shr är ett logiskt skift, och div 2 trunkerar mot noll
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

För typbreddssidan av samma problemslag, se Longint- och pekarefällorna på Delphi Linux64

Resten av lossless-avkodaren har gränsregler av egen art. Första raden, och första raden efter varje restart-markör, predicerar bara från den vänstra grannen och börjar på 2^(P−Pt−1) för precision P och punkttransformation Pt; varje senare rad börjar från samplen ovanför. Rekonstruktionen wrapas modulo 2^16, och punkttransformationen skalar bara utdata, aldrig prediktionskedjan. Huffman DC-kategori 16, en differens på 32768 utan extra bitar, är laglig bara i lossless-läge, så tabellparsern måste acceptera den innan den vet vilken process framen kommer deklarera

PDFlibPas stöder alla sju prediktorer, punkttransformationer och restart-intervall som täcker hela rader. JPEG-precisionen måste vara lika med BitsPerSample och vara 8 eller 16, och varje komponent måste samplas 1x1. YCbCr-losslessdata och 12-bitars lossless JPEG avvisas med ett explicit fel

12-bitars DCT och aritmetisk kodning i ren Pascal

12-bitars- och aritmetikavkodarna i PDFlibPas delar en invers DCT och skiljer sig bara i entropiavkodning. I 12-bitars Huffman-JPEG når DC-kategorierna 15 och AC-kategorierna 14, kvantifieringstabeller kan bära 16-bitars poster, och DC-prediktionen nollställs vid varje scan och varje restart. Koefficienter stannar i zigzag-ordning tills dekvantiseringen, som måste använda kvantifieringsvärdet vid samma zigzag-index, och först därefter flyttas de till naturlig ordning för den separerbara tvåpass-IDCT. Avkodade samples landar inom ±1 av en oberoende flyttalsreferensavkodare

Aritmetisk kodning ersätter Huffman med QM-kodaren i T.81 Annex D: en sannolikhetstillståndstabell med 114 poster, ett DC-statistikområde med 64 bin och ett AC-område med 256 bin, styrd av conditioning-parametrar från DAC-segmentet (standard L = 0, U = 1, Kx = 5). Statistik, DC-kontext och prediktion nollställs vid varje restart-intervall och i början av varje scan. Det är därför PDFlibPas behåller DAC-segment när den normaliserar gemensamma JPEGTables för aritmetiska bilder; släng dem och varje strip avkodas med fel statistikmodell

v3.723.0 lade till subsamplade komponenter och YCbCr-till-RGB-konvertering med JFIF-ekvationerna till de avkodade vägarna, och v3.724.0 lade till progressive scans för både 12-bitars Huffman (SOF2) och aritmetisk (SOF10) data. Subsamplad kroma replikeras till full upplösning i stället för att jämnas ut, vilket är vad en referensavkodare producerar med chroma smoothing avstängt. Hierarchical JPEG och aritmetiskt kodad lossless JPEG (SOF11) avvisas med ett explicit fel

Testa binära JPEG-fixtures i Delphi

Handbyggda JPEG-fixtures i Delphi-unittests kan falla på ett sätt som ser ut precis som en avkodningsbugg. En strängkonstant byggd av #$xx-tecken som löper förbi 255 tecken typas som UnicodeString, och att tilldela den till en AnsiString konverterar den genom den aktiva kodsidan. Byte $80 till $9F, rutin i entropikodad data, skrivs om på vägen in. Att bygga markörhuvuden i runtime har samma fälla: i Delphi ger AnsiChar + AnsiChar upphov till en UnicodeString, så appenda enskilda byte till en AnsiString eller skriv dem med SetLength och indexerad tilldelning. För fixtures, lagra texten i hex och avkoda i 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;

// Fixturen förblir ren ASCII, så ingen kodsida kan röra den
ImageID := Lib.AddImageFromString(HexToBytes(TiffHex), 1);

När du genererar testbilder, kom ihåg att jpegtran -arithmetic skriver sequential SOF9; SOF10 kräver -progressive också. Och ett bild-ID skilt från noll bevisar bara att importen accepterades. Rendera sidan och jämför pixlar, för ett pass-through-misstag producerar en välformad PDF som ingen viewer kan rita

Ta med äldre skanningsarkiv in i PDF

De här JPEG-varianterna tenderar att dyka upp alla samtidigt vid en arkivmigrering. PDFlibPas läser 12-bitars, lossless, aritmetiska och old-style JPEG-TIFF:er genom samma AddImageFromFile-anrop som allt annat, behåller full precision i PDF:en, och avvisar de sällsynta processerna utanför den uppsättningen med ett explicit fel. För licensiering, hela funktionslistan och testnedladdningar, se produktsidan för PDFlibPas PDF Library for Delphi