Article technique

Noyaux de downsampling HotPDF et dithering d'impression

HotPDF expose trois noyaux de downsampling d'images via la propriété ImageDownsampleKernel et une passe Floyd-Steinberg séparée via RenderOutputDither. Le premier contrôle l'apparence des photographies après qu'on les a réduites pour tenir un budget de taille, le second contrôle leur apparence après qu'une page a été réduite au noir et blanc. Aucun des deux n'est actif par défaut, et les deux sont opt-in pour la même raison : ils coûtent du temps réel

La pression qui mène les gens ici est familière. Un contrat numérisé de 60 Mo doit partir par une passerelle de messagerie qui refuse tout ce qui dépasse 10 Mo, ou un lot de relevés doit arriver sur un périphérique monochrome façon fax qui rend chaque pixel gris en papier ou en toner. Les deux problèmes sont des problèmes de rééchantillonnage, et chacun a une réponse rapide qui est moche et une réponse lente qui est juste

Ce en quoi les trois noyaux diffèrent réellement

THPDFResampleKernel a trois valeurs, et elles se placent à des points réellement différents de la courbe vitesse-qualité. rkHalftone délègue au chemin GDI historique StretchBlt avec le mode HALFTONE, qui malgré son nom est un filtrage de classe bilinéaire : rapide, adéquat pour les images en trait et les captures d'écran, et enclin aux bords croquants qu'on reconnaît instantanément sur des photographies réduites. rkBicubic exécute un noyau Catmull-Rom séparable, et rkLanczos3 exécute un sinc à fenêtre séparable avec un support de trois lobes

Les deux noyaux séparables s'exécutent en deux passes, horizontale puis verticale, avec 6 à 12 taps par pixel de destination en Pascal pur. C'est grosso modo un ordre de grandeur plus lent que le chemin GDI, et c'est exactement pourquoi rkHalftone reste le défaut. Sur un lot nocturne de milliers de pages, la différence est une décision d'ordonnancement, pas une préférence. Sur un document unique qu'un utilisateur attend, Lanczos3 coûte presque rien et se voit nettement mieux

Courbes de poids des trois noyaux de downsampling HotPDF : rkHalftone délègue au chemin GDI HALFTONE de classe bilinéaire avec un support de un, rkBicubic exécute une cubique Catmull-Rom séparable avec un support de deux, et rkLanczos3 exécute un sinc à fenêtre avec un support de trois, échangeant grosso modo un ordre de grandeur de vitesse contre des photographies visiblement meilleures
Les trois valeurs de noyau se placent à des points réellement différents de la courbe vitesse-qualité : un chemin GDI de classe bilinéaire, une cubique Catmull-Rom et un sinc à fenêtre à trois lobes, et les noyaux séparables normalisent les poids pour que rien ne sonne au-delà du noir ou du blanc

Deux propriétés d'implémentation valent la peine d'être connues parce qu'elles déterminent ce que la sortie peut et ne peut pas faire. Les bords serrés par réplication de bord plutôt que par enroulement ou fondu, et les poids normalisés par pixel de destination. Ensemble, ces deux points garantissent que le résultat ne sonne jamais sous le noir ni au-dessus du blanc, si bien que le halo de dépassement classique de Lanczos autour d'un bord dur n'apparaît pas comme des artefacts écrêtés dans l'image encodée

var
  Pdf: THotPDF;
  Info: THPDFLoadedResourceOptimizationInfo;
  Changed: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('scanned-contract.pdf');
    Pdf.ImageDownsampleKernel := rkLanczos3;   // à définir avant l'appel
    Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
    if Changed > 0 then
    begin
      Writeln('resampled images: ', Info.DownsampledImageCount);
      Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
      Pdf.SaveToFile('scanned-contract-150dpi.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

L'argument MinimumSavingsBytes, 4096 ci-dessus, est la garde qui maintient l'opération honnête. Réencoder une image déjà efficacement compressée peut produire un flux plus grand que l'original, et un downsampler qui remplace aveuglément chaque image fera parfois grossir le fichier qu'on lui demandait de réduire. Le seuil dit : ne valider le remplacement que s'il économise au moins ce nombre d'octets. PreservedCalibratedImageCount rapporte l'autre décision conservatrice, les images laissées intactes parce qu'elles portent un espace colorimétrique calibré que le rééchantillonnage compromettrait

Pourquoi un coefficient polynomiale faux est-il si difficile à repérer ?

Parce qu'un noyau d'interpolation cassé ne plante pas et ne lève rien, il produit juste une image subtilement fausse d'une façon que personne ne peut attribuer. Le noyau Catmull-Rom est cubique par morceaux, et sa branche externe en forme de Horner imbriquée est ((-0.5t + 2.5)t - 4)t + 2. Écrivez ce coefficient médian -5 au lieu de -4 et la fonction s'évalue quand même, renvoie toujours des nombres dans une plage plausible, et produit toujours une image

Le dégât apparaît sous la forme d'un W(1) qui vaut -1 là où il doit valoir 0. Les poids négatifs s'accumulent, la somme s'écrête à zéro, et le symptôme visible est un dégradé dont l'extrémité gauche passe au noir et un bord de pas qui perd ses tons intermédiaires. Rien dans l'échec ne pointe vers un polynôme. Le contrôle qui l'attrape en quelques secondes est arithmétique plutôt que visuel : un noyau interpolant doit satisfaire W(0) = 1 et W(±1) = W(±2) = 0, et tout noyau qui rate ces trois points a une erreur de coefficient, point final. Affirmez ces trois valeurs dans un test unitaire et toute la classe de défauts de frappe disparaît

Tracé de la branche externe Catmull-Rom bicubique de HotPDF montrant pourquoi un coefficient faux se cache : la coquille qui écrit -5 au lieu de -4 dans la forme de Horner imbriquée s'évalue quand même et laisse W(1) à -1 et W(2) à -2 là où zéro est exigé, si bien qu'affirmer W(0) égal à 1 plus les deux contraintes de zéro l'attrape en quelques secondes
Un noyau cassé ne plante jamais, il renvoie juste des nombres plausibles, c'est pourquoi l'œil ne peut pas attraper une coquille de coefficient. W(0) = 1 avec des zéros à plus et moins un et deux, c'est un test unitaire de trois lignes

Le dithering Floyd-Steinberg, et sa place dans le pipeline

La passe de dithering est un problème différent du rééchantillonnage et vit à un autre endroit du pipeline. RenderOutputDither applique la diffusion d'erreur Floyd-Steinberg après la composition de page, ce qui est la seule placement qui ait du sens pour un aperçu d'impression monochrome ou un export façon fax : l'opération consiste à réduire un raster fini à un bit par pixel, pas à savoir comment les images individuelles ont été mises à l'échelle en entrée

L'algorithme lui-même est court. La luminance est seuillée à 50 pour cent, et l'erreur de quantification est diffusée à quatre voisins avec les poids classiques 7/16, 3/16, 5/16 et 1/16, vers la droite, en bas à gauche, en bas et en bas à droite. Le pixel de sortie vaut 0 ou 255 dans chaque canal. Ce que l'alternative naïve vous donne à la place, un seuil dur sans diffusion, transforme une photographie en silhouette et perd chaque mi-ton qui portait le contenu

Placement dans le pipeline de rendu du dithering Floyd-Steinberg de HotPDF : RenderOutputDither s'exécute après la composition de page sur le raster 24 bits fini, seuillant la luminance à 50 pour cent et diffusant chaque erreur de quantification à droite et en dessous avec les poids 7/16, 3/16, 5/16 et 1/16 via un tampon de ligne qui doit accumuler, produisant une sortie monochrome à un bit
Le dithering appartient après la composition parce qu'il réduit un raster fini à un bit, pas à cause de la façon dont les images ont été mises à l'échelle. Les poids de diffusion somment à un, et le tampon de ligne doit accumuler plutôt qu'écraser
// Dithering au rendu pour un périphérique d'aperçu monochrome
Pdf.RenderOutputDither := True;

// Ou appliquer la même passe à un bitmap que vous possédez déjà. Le
// bitmap doit être pf24bit ; la fonction renvoie False plutôt que deviner
if not HPDFFloydSteinbergDitherBitmap(Preview) then
  raise Exception.Create('dither expects a 24-bit bitmap');

// Accès direct au noyau quand on rééchantillonne hors du pipeline document
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
  Small.SaveToFile('thumb.bmp');
finally
  Small.Free;
end;

Il y a un détail d'implémentation de la diffusion d'erreur qui mord tout le monde une fois. Le tampon d'erreur de ligne à ligne doit accumuler. Chaque pixel de la ligne suivante reçoit des contributions de trois pixels différents de la ligne courante, les taps 3/16, 5/16 et 1/16, et si le code affecte au lieu d'ajouter, chaque écriture jette la contribution précédente et seul le dernier tap survit. L'image a l'air quand même tramée, c'est ce qui rend la chose difficile à remarquer, mais la texture est fausse et la reproduction tonale dérive. Le test qui l'attrape est quantitatif : tramez un champ gris moyen uniforme et exigez que la couverture intérieure tombe entre 40 et 60 pour cent

Quelle combinaison un pipeline de réduction de taille doit-il utiliser ?

Accordez le noyau à ce que les images sont réellement, et traitez le dithering comme une affaire de périphérique plutôt que de compression. Pour des numérisations photographiques qui doivent survivre à un budget de taille, rkLanczos3 à 150 ou 200 DPI garde le détail que les gens remarquent tout en coupant le compte de pixels d'un facteur quatre ou plus. Pour les captures d'écran, les schémas et les images en trait, rkHalftone fait vraiment l'affaire et bien plus vite, car ces images ont peu de dégradés tonaux à préserver. Pour un lot mixte où vous ne pouvez pas inspecter chaque image, rkBicubic est le juste milieu raisonnable : meilleur que le bilinéaire, à peu près moitié moins de taps que Lanczos3

Le downsampling est un levier parmi plusieurs, et pas toujours le plus gros. Les numérisations bilevel répondent en général bien mieux à l'encodeur couvert dans la compression bilevel JBIG2 native en Delphi, où le gain vient des dictionnaires de symboles plutôt que des comptes de pixels. Avant de décider, il aide de savoir ce que le fichier contient réellement, et c'est à cela que sert l'extraction des images et de leurs filtres de décodage : un inventaire des objets image et de leur compression existante vous dit si le rééchantillonnage a quelque chose à gagner

Si vous construisez la surface d'aperçu qui montre le résultat, c'est dans le même chemin de rendu documenté dans le rendu d'une page PDF vers un bitmap que RenderOutputDither prend effet, si bien que l'aperçu tramé et la sortie tramée viennent d'un seul chemin de code plutôt que de deux implémentations qui divergent

Le principe général derrière les deux fonctionnalités est que les réglages de qualité doivent être explicites et réversibles. HotPDF garde le comportement historique par défaut pour qu'une application existante se mette à jour sans changement de sortie ni de minutage qui surprenne, et place les chemins plus beaux et plus lents à une assignation de propriété de distance. Les deux font partie du composant PDF Delphi HotPDF, aux côtés de la machinerie d'optimisation de ressources et de rendu sur laquelle ils s'appuient