Technical Article

Aggiunta di immagini JPEG 2000 ai PDF in Delphi con HotPDF

Un vetrino medico scansionato, una tessera di un rilievo aereo, il fotogramma di una pellicola archiviato a piena gamma dinamica. Queste sono le immagini che arrivano come JPEG 2000, e non a caso. Il formato mantiene 12 o 16 bit per canale, comprime con una trasformata wavelet anziché la DCT a blocchi utilizzata dal JPEG, e può codificare la stessa immagine sia senza perdita di dati sia con perdita a partire da un singolo codestream. Quando un documento creato a partire da tali origini deve diventare un PDF, l'immagine deve passare attraverso un filtro che le specifiche PDF riservano esattamente per questo codec

HotPDF v2.228.0 ha ripristinato un motore di decodifica JPEG 2000 funzionante per questo percorso. Una build precedente aveva distribuito l'unità con funzioni fittizie che restituivano nil, perciò l'API esisteva ma non decodificava nulla. Il motore attuale vincola OpenJPEG 2.5.4 in modo statico e trasforma un'origine JP2 o J2K in pixel che HotPDF può posizionare su una pagina

Il filtro JPXDecode in PDF

Lo standard ISO 32000-1 definisce il filtro JPXDecode nella sezione 7.4.9. Un XObject immagine PDF specifica la sua compressione nella voce /Filter del dizionario del flusso, e JPXDecode è il valore che indica che i dati del flusso sono un codestream JPEG 2000 anziché il JPEG di base veicolato da /DCTDecode. Il filtro è ciò che consente a un PDF di contenere dati di immagine compressi con wavelet a elevata profondità di bit, e ammette sia la modalità lossless che quella lossy del codec, in quanto la modalità è una proprietà del codestream stesso e non dell'involucro che lo circonda

Quest'ultimo punto merita di essere tenuto a mente. JPEG 2000 è un unico algoritmo con un caso speciale senza perdita di dati, non due formati separati. La wavelet reversibile 5/3 ricostruisce esattamente i campioni originali; la wavelet irreversibile 9/7 scambia quell'esattezza per un file di dimensioni inferiori. Un decoder le tratta entrambe allo stesso modo al momento della lettura, motivo per cui HotPDF necessita di un solo percorso di decodifica per accettare qualsiasi cosa un flusso JPXDecode gli sottoponga

Cosa fa il decoder ai pixel

Gli XObject immagine PDF nel caso comune si aspettano 8 bit per componente in DeviceGray o DeviceRGB. JPEG 2000 supera regolarmente questo limite e il suo modello di componenti è più generale di un raster pacchettizzato, quindi il decoder deve compiere tre operazioni prima che i dati siano utilizzabili come un'immagine normale

In primo luogo, i componenti a elevata profondità di bit vengono ricampionati a 8 bit. Un campione a 12 bit o a 16 bit viene ridimensionato alla scala da 0 a 255 in modo che il risultato sia un normale raster a 8 bit. I componenti con segno vengono prima spostati nell'intervallo senza segno. Il dettaglio è importante in quanto costituisce di per sé una perdita di dati: una scansione in scala di grigi a 16 bit perde la sua profonda gamma tonale nel momento in cui diventa un'immagine PDF a 8 bit, che è il compromesso giusto per l'output su schermo e in stampa, ma non per la ri-archiviazione

In secondo luogo, uno spazio colore YCbCr (il codec lo chiama SYCC) viene convertito in RGB. JPEG 2000 memorizza spesso il colore in uno spazio luma-chroma per efficienza di compressione, la stessa idea utilizzata dal JPEG di base, e il decoder applica la trasformata inversa standard in modo che la pagina riceva il vero RGB

In terzo luogo, i componenti sottocampionati vengono sovracampionati mediante replicazione del vicino più prossimo. I canali chroma vengono frequentemente memorizzati a mezza risoluzione, perciò il decoder legge ogni componente con le sue dimensioni e il suo fattore di campionamento, per poi replicare i campioni al fine di portare ogni canale alla dimensione intera dell'immagine prima dell'interlacciamento. L'interpolazione al vicino più prossimo mantiene il passaggio economico; il chroma che sta riempiendo era a bassa frequenza in partenza, quindi il costo visivo è minimo

Contenitori JP2 rispetto a un codestream J2K grezzo

Un file JPEG 2000 si presenta in due forme e HotPDF rileva quale sta leggendo dai primi byte anziché dall'estensione del file. Un file JP2 è un contenitore strutturato a riquadri (box): si apre con la scatola della firma di dodici byte 00 00 00 0C 6A 50 20 20 e avvolge il codestream insieme ad altre scatole che descrivono spazio colore, risoluzione e metadati. Un codestream J2K grezzo non trasporta alcun contenitore e inizia con il marcatore SOC FF 4F FF 51. Il decoder legge quei byte iniziali, riconosce la firma e seleziona il codec OpenJPEG corrispondente per ogni caso

Entrambe le forme vengono gestite in quanto entrambe si verificano nella pratica. I dispositivi di acquisizione e gli archivi che necessitano dei metadati laterali emettono JP2; gli strumenti che desiderano il payload più piccolo possibile emettono il nudo codestream. Il tipo di formato è modellato come un'enumerazione, TJpeg2000FileType, con i membri jtInvalid, jtJP2, jtJ2K e jtJPT. Il membro JPT nomina la variante di streaming JPIP; il rilevatore della firma dei byte risolve le due forme che può decodificare, JP2 e J2K, e segnala qualsiasi altra cosa come jtInvalid, in modo che un input non supportato fallisca in modo pulito invece di produrre dati inutilizzabili

uses
  HPDFJpeg2000;

var
  Decoder: THPDFJpeg2000Decoder;
  Pixels: TJpeg2000ByteArray;
begin
  Decoder := THPDFJpeg2000Decoder.Create;
  try
    if Decoder.LoadFromStream(Input) then          // JP2 or J2K, auto-detected
      if Decoder.GetImageData(Pixels) then
        // Pixels is 8-bit interleaved, ColorComponents channels wide,
        // row-major top to bottom: ready for a DeviceGray/DeviceRGB XObject.
        ProcessRaster(Decoder.Width, Decoder.Height,
                      Decoder.ColorComponents, Pixels);
  finally
    Decoder.Free;
  end;
end;

Senza perdita e con perdita sul lato codifica

Il decoder legge entrambe le modalità senza che gli venga detto di quale si tratti. La scelta diventa un parametro solo quando si procede in senso opposto e si produce un file JPEG 2000, operazione che HotPDF può eseguire anche tramite la classe TJpeg2000Bitmap, un discendente di TBitmap che carica e salva dati raster come JP2. Due proprietà governano l'output. LosslessCompression è un booleano che seleziona la wavelet reversibile quando è vero; CompressionQuality è un TJpeg2000QualityRange, un intero da 1 a 100 dove 1 è piccolo e scadente e 100 è grande e fedele. I valori predefiniti risiedono in costanti nominate: Jpeg2000DefaultLosslessCompression è False e Jpeg2000DefaultLossyQuality è 80

La decisione dipende dal contenuto. La modalità lossless si adatta a una copia master, a una scansione medica o legale, a qualsiasi elemento che potrebbe essere ricodificato in seguito e non deve accumulare perdite generazionali. La modalità lossy a qualità 80 si adatta a un'immagine destinata allo schermo o alla stampa, in cui il degrado graduale della wavelet produce un file notevolmente più piccolo senza alcun artefatto che un lettore possa notare. C'è un avvertimento riguardante CMYK da segnalare: la bitmap espone SetCMYK per marcare i dati a a quattro canali come CMYK anziché RGBA, aspetto che conta per le pipeline di stampa che mantengono intatte le separazioni

uses
  HPDFJpeg2000;

var
  Bmp: TJpeg2000Bitmap;
begin
  Bmp := TJpeg2000Bitmap.Create;
  try
    Bmp.LoadFromStream(Source);              // decode an existing JP2/J2K
    Bmp.LosslessCompression := True;         // reversible 5/3 wavelet
    // or, for a smaller lossy file:
    // Bmp.LosslessCompression := False;
    // Bmp.CompressionQuality := 80;         // matches the default
    Bmp.SaveToStream(Output);                // always writes a JP2 file
  finally
    Bmp.Free;
  end;
end;

Perché non esiste una pipeline di filtri decode-on-load

Un fatto architettonico modella il modo in cui si utilizza tutto questo, ed è facile supporre il contrario. HotPDF non ha un filtro immagine generale decode-on-load. Quando si apre un PDF che contiene già un'immagine JPXDecode, il motore non decodifica tale flusso. Mantiene i byte JPEG 2000 esattamente come sono, perciò una copia di pagina o l'unione di un documento trasporta l'immagine intatta, byte per byte. Il decoder ha un unico punto di ingresso e si trova sul lato della creazione: l'API basata su file AddImage, instradata dall'estensione del file per gestire le fonti .jp2, .j2k, .jpt e .jpc

Tale divisione rappresenta il design corretto anziché una limitazione. Decodificare un flusso JPX incorporato durante il caricamento, solo per ricodificarlo al momento del salvataggio, convertirebbe un'immagine archiviata senza perdita di dati in una con perdita e gonfierebbe ogni unione, il tutto per un'immagine che si intendeva solo spostare da un PDF all'altro. Passare il flusso invariato è un'operazione rapida e senza perdita di dati. La decodifica viene posticipata all'unico momento in cui è genuinamente richiesta: quando si consegna al motore un file JPEG 2000 dal disco e si chiede di rasterizzare quell'immagine per posizionarla su una nuova pagina. A quel punto il file deve diventare pixel e il decoder entra in funzione

Registrazione del supporto e posizionamento di un'immagine

La registrazione dell'immagine JPEG 2000 è facoltativa dietro l'opzione di compilazione HPDF_REGISTER_JPEG2000_PICTURE, che è disattivata per impostazione predefinita. Il motivo è un conflitto reale, non la cautela: registrare i formati di file jp2, j2k e jpc a livello globale con TPicture può interferire con il rilevamento del formato BLOB su cui fa affidamento TppDBImage di ReportBuilder. Se si definisce l'opzione quando quell'integrazione non è in gioco, i formati di file si registrano in modo che TPicture li riconosca; se la si lascia non definita, il dispacciamento dell'estensione di AddImage decodifica comunque i file JPEG 2000 direttamente, perché tale percorso non passa affatto attraverso TPicture

Chiarito questo, posizionare un'immagine JPEG 2000 richiede lo stesso ritmo a tre chiamate di qualsiasi altra immagine in HotPDF. Si passa ad AddImage un percorso .jp2 e un tipo di compressione che indica come l'immagine debba essere archiviata nell'output, quindi si posiziona l'indice dell'immagine restituito sulla pagina con ShowImage

var
  Pdf: THotPDF;
  ImgIndex: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.BeginDoc;
    Pdf.AddPage;
    // The .jp2 source is decoded through the OpenJPEG backend, then
    // re-embedded with the compression you request here.
    ImgIndex := Pdf.AddImage('Scan_16bit.jp2', icJpeg);
    // x, y, width, height in points; final 0 is the rotation angle.
    Pdf.ShowImage(ImgIndex, 72, 72, 400, 300, 0);
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

La compressione passata ad AddImage controlla in che modo l'immagine decodificata viene riarchiviata, non come è stata letta. Un file JPEG 2000 decodificato in una bitmap può tornare in output come JPEG DCTDecode, come raster Flate o un altro filtro supportato, a seconda di cosa sia più adatto al documento. La decodifica da JP2 o J2K avviene per prima in ogni caso, quindi la stessa chiamata accetta un'origine compressa tramite wavelet e la incorpora nella forma prevista dal resto della pipeline

Per un quadro più ampio su come immagini e font approdano nell'output generato, si vedano i nostri appunti sull'output di report con font e immagini. Quando il documento che si sta assemblando riutilizza contenuti provenienti da PDF esistenti, il comportamento di passthrough descritto qui si associa alle meccaniche di unione e revisione in flussi di oggetti e aggiornamenti incrementali. Il motore di decodifica JPEG 2000 viene fornito come parte di HotPDF Component per Delphi e C++Builder, insieme alle API per immagini, font e documenti trattate altrove su questo blog