Article technique

Mixed Raster Content en Delphi : scan PDF en JPEG + CCITT

losLab PDF Library for Delphi compresse une page scannée en Mixed Raster Content via TPDFlib.CompressPageAsMRC. La méthode rend la page, binarise un masque de premier plan 1 bit pleine résolution, réduit le reste en fond JPEG, et reconstruit la page en deux images : un fond /DCTDecode et un masque pochoir /CCITTFaxDecode peint dans la couleur de remplissage courante

La page pour laquelle tout ça existe est celle dont chaque archive documentaire déborde : un scan couleur à 300 DPI dune facture ou dun contrat, stocké en un seul gros JPEG. Encodez-le à une qualité qui garde le texte lisible et le fichier est gros ; baissez la qualité et les lettres se couvrent de halos et leurs empattements bavent, parce que la pire chose que sache faire JPEG, cest un bord noir net sur du papier blanc. Mixed Raster Content cesse de demander à un seul codec de faire deux travaux

Pourquoi un JPEG unique rend-il le texte scanné à la fois flou et gros ?

Un JPEG unique traite mal le texte scanné parce que le codec DCT dépense ses bits sur des dégradés lisses et approxime les bords durs avec des coefficients haute fréquence que la quantification jette en premier. Le texte dun scan nest quasiment que des bords durs. Garder ces bords propres force un réglage de qualité élevé sur toute la page, y compris la texture du papier, le logo et la tache de café, dont aucun navait besoin

Lidée MRC découpe la page selon ce que chaque zone est réellement. Lencre devient une image bilevel, où un codec fax comme CCITT enregistre sans perte et très compact des séries de pixels noirs et blancs. Tout le reste, ton du papier, photos, fonds colorés, devient une image à ton continu stockable à une fraction de la résolution, parce que personne ne lit un fond à 300 DPI. La couche bilevel garde le texte net à pleine résolution ; la couche de fond garde la couleur à bas coût

Scan JPEG unique face à la découpe en couches Mixed Raster Content créée par PDFlibPas CompressPageAsMRC en Delphi : le codec DCT bave les bords durs du texte et force une qualité élevée sur toute la page, tandis que lencre devient un pochoir CCITT 1 bit sans perte à 300 DPI et que le ton du papier devient un fond JPEG DeviceRGB à 75 DPI
Mixed Raster Content cesse de demander à un codec de faire deux travaux : la couche bilevel garde les lettres nettes à pleine résolution et le fond garde la couleur pour une fraction du coût, si bien quune qualité de 50 à 70 est dordinaire invisible

Comment CompressPageAsMRC découpe-t-il une page en deux couches ?

TPDFlib.CompressPageAsMRC exécute un pipeline fixe par page. Il rend la page au DPI demandé en bitmap 24 bits, classe chaque pixel avec une luminance pondérée Rec. 601 (299/587/114 pour mille) face à Threshold, et écrit le résultat dans un masque 1 bit à la même résolution. En parallèle il réduit le même rendu par BGScale avec une moyenne par bloc, si bien quun BGScale de 4 transforme chaque bloc 4×4 de pixels en un pixel de fond

Flux de données CompressPageAsMRC dans PDFlibPas pour Delphi : un rendu 24 bits à 300 DPI est binarisé par la luminance Rec. 601 en pochoir ImageMask CCITTFaxDecode K 1 pleine résolution, moyenné par blocs avec BGScale 4 en fond DCTDecode DeviceRGB, et les deux images remplacent les opérateurs de page dorigine avec le masque au-dessus
Les deux couches dérivent dun seul rendu : le DPI décide de la finesse de trait qui survit à la binarisation, BGScale échange le détail du fond de 2 pour des photographies à 8 pour du texte simple, et la page reconstruite dessine le pochoir dans la couleur de remplissage courante

Le contenu de la page est ensuite remplacé. Les opérateurs dorigine sont retirés, le fond réduit est incorporé comme image /DCTDecode en /DeviceRGB (ISO 32000-1 §7.4.8), le masque est encodé par lencodeur CCITT de la bibliothèque en image /CCITTFaxDecode (§7.4.6) marquée /ImageMask true, et les deux sont dessinés sur toute la boîte de page avec le masque au-dessus. La signature est courte :

function CompressPageAsMRC(Page: Integer; DPI: Double;
  BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
  • Page part de 1 et doit rester dans PageCount
  • DPI est la résolution de rendu du raster source, de 50 à 1200
  • BGScale est le facteur de réduction du fond, de 1 à 8
  • JPEGQuality est la qualité JPEG du fond, de 1 à 100
  • Threshold est la coupure de luminance, de 1 à 255 ; les pixels au-dessus sont du papier, les plus sombres sont de lencre
  • Options est réservé et doit valoir 0

Un lot typique sur un fichier scanné ressemble à ceci. Appelez SetFillColor juste avant chaque page, pour une raison que la section suivante explique

uses
  SysUtils, PDFlibrary;

procedure CompressScannedFile(const SrcFile, DstFile: string);
var
  Lib: TPDFlib;
  P, Replaced: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(SrcFile, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [SrcFile]);
    for P := 1 to Lib.PageCount do
    begin
      Lib.SetFillColor(0, 0, 0);  // couleur dencre de la couche pochoir
      // masque 300 DPI, fond 75 DPI, qualité JPEG 60, coupure à luma 160
      Replaced := Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0);
      if Replaced = 0 then
        raise Exception.CreateFmt('MRC failed on page %d', [P]);
    end;
    if Lib.SaveToFile(DstFile) <> 1 then
      raise Exception.CreateFmt('Cannot save %s', [DstFile]);
  finally
    Lib.Free;
  end;
end;

Pourquoi le premier plan est-il peint dans la couleur de remplissage courante ?

La couche de premier plan est un masque pochoir, et un masque pochoir na pas de couleur propre. ISO 32000-1 §8.9.6.2 définit une image avec /ImageMask true comme une forme 1 bit : les valeurs déchantillons marquent où va la peinture, et la peinture est la couleur de remplissage courante de létat graphique. Cest exactement ce qui rend la couche bon marché, et cest aussi pourquoi CompressPageAsMRC ne prend aucun paramètre de couleur

TPDFlib.CompressPageAsMRC peint le masque avec la couleur posée par SetFillColor, donc lordre des appels compte. Le peintre émet la couleur de remplissage paresseusement : SetFillColor enregistre la couleur et la marque comme changée, et le dessin du pochoir écrit lopérateur rg dans son propre bloc q/Q tant que ce changement est encore en attente. Dessinez une forme pleine entre les deux et le changement en attente est consommé par cette forme. Poser la couleur juste avant chaque page, comme dans le lot ci-dessus, supprime les devinettes. Le noir est le choix normal ; un bleu marine foncé convient aux formulaires à encre bleue où vous voulez que le résultat ressemble moins à une photocopie

Séquence démission paresseuse de la couleur de remplissage pour la couche pochoir MRC de PDFlibPas en Delphi : SetFillColor enregistre la couleur comme changement en attente, le dessin du pochoir ImageMask écrit lopérateur rg dans son propre bloc q et Q tant que le changement est en attente, et une forme pleine intercalaire consomme le changement en attente à sa place
Un masque pochoir ne possède aucune couleur, donc chaque pixel dencre prend une seule couleur de remplissage : un tampon rouge, une signature bleue et un texte noir fusionnent ensemble, doù lappel de SetFillColor requis juste avant chaque page
// Formulaire au stylo bleu : rendre la couche dencre en bleu marine foncé
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);

Le placement suit lorigine de coordonnées de la bibliothèque. CompressPageAsMRC lit GetOrigin et dessine les deux images depuis le bord supérieur de la boîte de page : coordonnée haute 0 quand lorigine est en haut à gauche (SetOrigin(1)), coordonnée haute PageHeight avec lorigine par défaut en bas à gauche. Vous navez rien à ajuster, mais cela explique pourquoi les couches atterrissent correctement quelle que soit lorigine que votre pipeline utilisait déjà

Une conséquence de la sémantique pochoir mérite dêtre dite franchement : chaque pixel classé encre prend la même couleur. Un tampon rouge, une signature bleue et un texte noir finissent tous en une seule couleur de remplissage sur la couche de premier plan. Seul le JPEG de fond garde en dessous une faible trace basse résolution des couleurs dorigine

Choisir DPI, BGScale, Threshold et la qualité JPEG

La valeur DPI décide de la netteté du texte, et 300 est la valeur par défaut raisonnable pour des scans de bureau. Le masque reste à la pleine résolution de rendu, donc DPI définit la finesse de trait qui survit à la binarisation. Monter au-dessus de la résolution du scan dorigine napporte rien, parce que le moteur de rendu ne ferait que suréchantillonner limage de scan incorporée. La mémoire est lautre limite : une page US Letter à 300 DPI se rend en 2550×3300 pixels, soit 25 245 000 octets de données pixels 24 bits avant que masque et fond soient dérivés, et la même page au plafond de 1200 DPI fait 10200×13200 pixels, environ 404 Mo

BGScale échange le détail du fond contre la taille. À 300 DPI, un facteur de 4 donne un fond à 75 DPI, 638×825 pixels pour du Letter, ce qui suffit largement pour le ton du papier et les aplats. Les pages avec des photographies ou des illustrations en trame quun lecteur regardera vraiment méritent 2 ; du texte simple sur papier blanc peut prendre 6 ou 8, le fond nétant guère plus quune teinte

Threshold est le paramètre qui change laspect de la page. Le monter classe davantage de pixels gris moyen comme encre, ce qui épaissit les traits fins et les marques de crayon mais attire aussi le bruit du scanner et lombre du papier comme poussières. Le baisser donne un papier plus propre et des lettres plus fines, parfois coupées. Des valeurs autour de 128 à 170 forment une plage de départ raisonnable pour du texte noir sur blanc ; le compromis entre les deux est le même que celui évoqué dans le rendu de pages PDF en monochrome 1 bit en Delphi, où le même seuil de luminance pilote RenderPageToMonochromeFileEx. À la différence de cette méthode, CompressPageAsMRC na pas de passe de dépollution, donc des scans bruités sont mieux servis par un seuil un peu plus bas

JPEGQuality importe moins que pour un scan en image unique, parce que le fond ne porte plus aucun bord de texte. Une qualité de 50 à 70 est dordinaire invisible sur un fond à 75 DPI derrière un masque net

Que dit la valeur de retour, et que ne dit-elle pas ?

TPDFlib.CompressPageAsMRC renvoie la taille en octets du contenu de page quil a remplacé, et 0 quand quelque chose a échoué. Cest un indicateur de succès, pas une mesure déconomies. Sur une page scannée typique, le flux de contenu dorigine compte quelques dizaines doctets, juste assez dopérateurs pour placer une image, donc un petit nombre positif est le résultat normal même quand la page rétrécit de mégaoctets. Une page dont le flux de contenu dorigine était vide rapporte 0 alors même quelle a été reconstruite

Le comportement en échec nest pas atomique, et cest la partie autour de laquelle concevoir. Les vérifications darguments, le rendu et lencodage JPEG tournent avant que la page soit touchée, donc un BGScale hors plage, un JPEGQuality de 101 ou un échec de rendu laissent la page exactement comme elle était. Une fois le contenu dorigine effacé, en revanche, un échec dincorporation ou de dessin de lune des couches renvoie aussi 0, et à ce moment la page peut déjà être partiellement reconstruite. Traitez tout 0 comme une raison de jeter le document en mémoire plutôt que de le sauvegarder, et nécrivez jamais le résultat par-dessus le fichier source

Pour savoir ce que la conversion a réellement économisé, mesurez le document. Comparer la longueur de SaveToString avant et après est le contrôle le plus rapide ; pour une réponse par catégorie, laudit de taille de fichier PDF avec AuditDocumentSpace montre si le poids est bien sorti des images comme prévu

function MRCSavings(Lib: TPDFlib): Int64;
var
  Before: Int64;
  P: Integer;
begin
  Before := Length(Lib.SaveToString);
  for P := 1 to Lib.PageCount do
  begin
    Lib.SetFillColor(0, 0, 0);
    if Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0) = 0 then
      raise Exception.CreateFmt('Page %d failed; discard this document', [P]);
  end;
  Result := Before - Length(Lib.SaveToString);
end;

Où ce MRC à deux couches sarrête avant ITU-T T.44

CompressPageAsMRC implémente un MRC délibérément simplifié. Le modèle complet dITU-T T.44 utilise trois couches par bande : une image de fond, une image de premier plan qui porte les couleurs dencre, et un masque bilevel qui choisit entre eux pixel par pixel, avec de la place pour plusieurs paires masque/premier plan sur une page. En termes PDF, la troisième couche serait une image de premier plan couleur basse résolution avec un masque explicite (§8.9.6.3) au lieu dun pochoir peint en une seule couleur. PDF Library for Delphi saute la couche de couleur de premier plan, ce qui rend le résultat si petit et explique aussi pourquoi lencre colorée se replie dans la couleur de remplissage

  • La méthode convient au texte noir ou monochrome sur papier scanné ; les documents codés par couleurs, le texte surligné et les tampons colorés perdent leur couleur dencre
  • La page reconstruite est du raster pur : tout vrai texte, art vectoriel ou caractère sélectionnable de la page dorigine devient pixels, donc appliquez-la à des scans, pas à des pages natives, où le subsetting de polices et la réduction dimages sont les bons outils
  • Le masque utilise lencodeur CCITT de la bibliothèque en mode T.4 bidimensionnel, écrit comme /CCITTFaxDecode avec /K 1 ; lencodeur possède aussi un mode Group 4 via AddImageCCITTFromBitmapStream, mais CompressPageAsMRC ne lexpose pas, et il nutilise pas JBIG2
  • Les annotations et les champs de formulaire ne sont pas aplatis ; ils restent des objets au-dessus du contenu reconstruit
  • Le fond est en /DeviceRGB, donc la sortie ne convient pas aux workflows PDF/X-1a, et SetFillColor lui-même est rejeté en mode PDF/X-1a
  • Un scan qui aura besoin dOCR doit être reconnu avant le MRC, tant que les pixels dorigine existent encore en pleine profondeur de couleur

FAQ

CompressPageAsMRC garde-t-il le texte recherchable ?

Non. CompressPageAsMRC remplace le contenu de la page par deux images, donc tout opérateur de texte de la page dorigine, y compris une couche de texte OCR invisible, est retiré avec elle. Lancez lOCR sur la page compressée ensuite, ou rajoutez la couche de texte après compression

Peut-on ne compresser que certaines pages dun document ?

Oui. CompressPageAsMRC traite une page par appel (numérotation à partir de 1), donc vous choisissez les pages et pouvez même faire varier BGScale et Threshold par page, par exemple un facteur de 2 pour un insert photo et 6 pour des pages de texte simple

Que faut-il passer au paramètre Options ?

Passez 0. Options est réservé pour un usage futur dans la version actuelle et aucun bit nest défini, donc une valeur non nulle ne porte aucun sens aujourdhui

CompressPageAsMRC est livré dans losLab PDF Library for Delphi v3.561.0 avec les briques de rendu, JPEG, CCITT et masque pochoir dont il est assemblé, toutes disponibles en méthodes publiques si vous avez besoin dune variante du pipeline. La bibliothèque, son support C++Builder et la référence API complète sont sur la page produit PDF Library for Delphi