Technical Article

Ajout d'images JPEG 2000 aux PDF dans Delphi avec HotPDF

Une lame médicale numérisée, une tuile de levé aérien, une image de film archivée sur toute sa plage dynamique. Ce sont les images qui arrivent au format JPEG 2000, et elles arrivent ainsi pour une raison. Le format conserve 12 ou 16 bits par canal, se compresse avec une transformée en ondelettes au lieu du DCT par blocs qu'utilise le format JPEG, et peut encoder la même image sans perte ou avec perte à partir d'un seul flux de code. Lorsqu'un document construit à partir de ces sources doit devenir un PDF, l'image doit traverser un filtre que la spécification PDF réserve exactement pour ce codec

HotPDF v2.228.0 a restauré un moteur de décodage JPEG 2000 fonctionnel pour ce chemin. Une version antérieure avait livré l'unité avec des fonctions factices qui renvoyaient nil, l'API existait donc mais ne décodait rien. Le moteur actuel lie statiquement OpenJPEG 2.5.4 et transforme une source JP2 ou J2K en pixels que HotPDF peut placer sur une page

Le filtre JPXDecode dans un PDF

La norme ISO 32000-1 définit le filtre JPXDecode dans la section 7.4.9. Un XObject d'image PDF nomme sa compression dans l'entrée /Filter du dictionnaire de flux, et JPXDecode est la valeur qui indique que les données de flux sont un flux de code JPEG 2000 plutôt que le JPEG de base transporté par /DCTDecode. Le filtre est ce qui permet à un PDF de contenir des données d'image compressées par ondelettes avec une profondeur de bits élevée, et il admet à la fois les modes sans perte et avec perte du codec, car le mode est une propriété du flux de code lui-même et non de l'enveloppe qui l'entoure

Ce dernier point est celui qu'il convient de retenir. Le JPEG 2000 est un algorithme unique avec un cas particulier sans perte, et non deux formats distincts. L'ondelette réversible 5/3 reconstruit exactement les échantillons d'origine ; l'ondelette irréversible 9/7 échange cette exactitude contre un fichier plus petit. Un décodeur traite les deux de la même manière au moment de la lecture, c'est pourquoi HotPDF ne nécessite qu'un seul chemin de décodage pour accepter tout ce qu'un flux JPXDecode lui envoie

Ce que le décodeur fait aux pixels

Dans le cas courant, les XObjects d'image PDF attendent 8 bits par composante dans DeviceGray ou DeviceRGB. Le format JPEG 2000 dépasse souvent cela, et son modèle de composantes est plus général qu'un raster compacté, de sorte que le décodeur a trois tâches à accomplir avant que les données ne soient utilisables comme une image normale

Premièrement, les composantes à forte profondeur de bits sont rééchantillonnées à 8 bits. Un échantillon de 12 ou 16 bits est réduit à la plage de 0 à 255 afin que le résultat soit un raster 8 bits ordinaire. Les composantes signées sont d'abord décalées vers une plage non signée. Le détail importe car il génère une perte en lui-même : une numérisation en niveaux de gris de 16 bits perd sa plage tonale profonde au moment où elle devient une image PDF 8 bits, ce qui est le bon compromis pour une sortie à l'écran et pour l'impression, mais pas pour un ré-archivage

Deuxièmement, un espace colorimétrique YCbCr (le codec l'appelle SYCC) est converti en RGB. Le JPEG 2000 stocke souvent la couleur dans un espace luma-chroma pour une plus grande efficacité de compression, la même idée que celle utilisée par le JPEG de base, et le décodeur applique la transformée inverse standard pour que la page reçoive un véritable RGB

Troisièmement, les composantes sous-échantillonnées sont suréchantillonnées par réplication du plus proche voisin. Les canaux de chrominance sont fréquemment stockés à la moitié de la résolution, de sorte que le décodeur lit chaque composante à ses propres dimensions et à son propre facteur d'échantillonnage, puis réplique les échantillons pour amener chaque canal à la taille d'image complète avant l'entrelacement. Le plus proche voisin permet de garder cette étape peu coûteuse ; la chrominance qu'il remplit était de toute façon de basse fréquence, le coût visible est donc faible

Les boîtes JP2 contre un flux de code J2K brut

Un fichier JPEG 2000 se présente sous deux formes, et HotPDF détecte laquelle il lit à partir des premiers octets plutôt qu'à partir de l'extension de fichier. Un fichier JP2 est un conteneur structuré en boîtes : il s'ouvre avec la boîte de signature de douze octets 00 00 00 0C 6A 50 20 20 et enveloppe le flux de code avec des boîtes qui décrivent l'espace colorimétrique, la résolution et les métadonnées. Un flux de code J2K brut ne comporte aucun conteneur et commence par le marqueur SOC FF 4F FF 51. Le décodeur lit ces premiers octets, reconnaît la signature et sélectionne le codec OpenJPEG correspondant à chaque cas

Les deux formes sont gérées car les deux se rencontrent dans la nature. Les périphériques de capture et les archives qui ont besoin de métadonnées secondaires émettent du JP2 ; les outils qui souhaitent la charge utile la plus petite possible émettent le flux de code nu. Le type de format est modélisé sous la forme d'une énumération, TJpeg2000FileType, avec les membres jtInvalid, jtJP2, jtJ2K et jtJPT. Le membre JPT désigne la variante de flux JPIP ; le détecteur de signature d'octet résout les deux formes qu'il peut décoder, JP2 et J2K, et signale tout le reste comme jtInvalid afin qu'une entrée non prise en charge échoue proprement au lieu de produire des données inutilisables

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;

Sans perte et avec perte côté encodage

Le décodeur lit les deux modes sans qu'on lui indique lequel c'est. Le choix ne devient un paramètre que lorsque vous faites l'inverse et produisez un fichier JPEG 2000, ce que HotPDF peut également faire via la classe TJpeg2000Bitmap, un descendant de TBitmap qui charge et enregistre les données raster en tant que JP2. Deux propriétés régissent la sortie. LosslessCompression est un booléen qui sélectionne l'ondelette réversible lorsqu'il est vrai ; CompressionQuality est un TJpeg2000QualityRange, un entier de 1 à 100 où 1 correspond à une image petite et laide et 100 correspond à une image volumineuse et fidèle. Les valeurs par défaut se trouvent dans des constantes nommées : Jpeg2000DefaultLosslessCompression est False et Jpeg2000DefaultLossyQuality est 80

La décision est une décision relative au contenu. Le mode sans perte convient à une copie originale, à une numérisation médicale ou juridique, ou à tout élément qui pourrait être ré-encodé plus tard et qui ne doit pas accumuler de perte générationnelle. Le mode avec perte avec une qualité de 80 convient à une image destinée à l'écran ou à l'impression, où la dégradation gracieuse de l'ondelette donne un fichier sensiblement plus petit sans aucun artefact qu'un lecteur pourrait repérer. Il y a une mise en garde concernant le format CMJN à signaler : le bitmap expose SetCMYK pour marquer les données à quatre canaux comme CMJN plutôt que RGBA, ce qui est important pour les pipelines d'impression qui conservent les séparations intactes

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;

Pourquoi il n'y a pas de pipeline de filtre de décodage au chargement

Un fait architectural façonne la façon dont vous utilisez tout cela, et il est facile de supposer le contraire. HotPDF n'a pas de filtre d'image générique pour le décodage au chargement. Lorsque vous ouvrez un PDF qui contient déjà une image JPXDecode, le moteur ne décode pas ce flux. Il conserve les octets JPEG 2000 exactement tels quels, de sorte qu'une copie de page ou une fusion de document transporte l'image intacte, octet par octet. Le décodeur possède un seul point d'entrée, et il se situe du côté de la création : le module AddImage basé sur les fichiers, réparti par extension de fichier pour gérer les sources .jp2, .j2k, .jpt et .jpc

Cette séparation est la conception correcte plutôt qu'une limitation. Le fait de décoder un flux JPX intégré au chargement, pour le ré-encoder lors de la sauvegarde, convertirait une image archivée sans perte en une image avec perte et gonflerait chaque fusion, tout cela pour une image que vous aviez seulement l'intention de déplacer d'un PDF à un autre. Le fait de transmettre le flux in extenso est une opération sans perte et rapide. Le décodage est différé jusqu'au seul moment où il est véritablement requis : lorsque vous transmettez au moteur un fichier JPEG 2000 à partir du disque et que vous lui demandez de pixelliser cette image pour la placer sur une nouvelle page. À ce stade, le fichier doit devenir des pixels et le décodeur s'exécute

Enregistrement de la prise en charge et placement d'une image

L'enregistrement d'une image JPEG 2000 est facultatif via le commutateur de compilation HPDF_REGISTER_JPEG2000_PICTURE, qui est désactivé par défaut. La raison en est un conflit réel, et non une mesure de précaution : l'enregistrement global des formats de fichier jp2, j2k et jpc avec TPicture peut interférer avec la détection de format BLOB sur laquelle s'appuie TppDBImage de ReportBuilder. Définissez le commutateur lorsque cette intégration n'est pas en jeu, et les formats de fichier s'enregistrent pour que TPicture les reconnaisse ; laissez-le non défini et la répartition des extensions de AddImage décode toujours les fichiers JPEG 2000 directement, car ce chemin ne passe pas du tout par TPicture

Une fois cela compris, le placement d'une image JPEG 2000 suit le même rythme à trois appels que pour toute autre image HotPDF. Fournissez à AddImage un chemin .jp2 et un type de compression pour la façon dont l'image doit être stockée dans la sortie, puis positionnez l'index d'image renvoyé sur la page avec 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 compression que vous passez à AddImage contrôle la façon dont l'image décodée est re-stockée, et non la façon dont elle a été lue. Un fichier JPEG 2000 décodé en un bitmap peut ressortir sous la forme d'un JPEG DCTDecode, d'un raster Flate ou d'un autre filtre pris en charge, selon ce qui convient au document. Le décodage à partir de JP2 ou J2K se produit en premier indépendamment de cela, de sorte que le même appel accepte une source compressée par ondelettes et l'intègre sous la forme que le reste de votre pipeline attend

Pour une vue d'ensemble de la manière dont les images et les polices atterrissent dans la sortie générée, consultez nos notes sur la sortie de rapport avec polices et images dans Delphi. Lorsque le document que vous assemblez réutilise le contenu des PDF existants, le comportement d'intercommunication décrit ici s'associe aux mécanismes de fusion et de révision dans les flux d'objets et les mises à jour incrémentielles. Le moteur de décodage JPEG 2000 est livré dans le cadre du HotPDF Component pour Delphi et C++Builder, aux côtés des API d'image, de police et de document couvertes par ailleurs sur ce blog