Article technique

Convertir un PDF vers un Espace de Couleur Unique en Delphi

Un titre rempli avec 1 0 0 rg, rouge pur, revient d'une conversion en gris comme 0.299 g. Le bleu pur atterrit à 0.114 g, assez proche du noir pour qu'un logo marine et le texte courant deviennent difficiles à distinguer. Ces nombres ne sont pas un accident d'arrondi : ce sont les poids de luminance BT.601, les mêmes qu'un lecteur applique quand il affiche une page RGB sur un périphérique gris, et ils décident du contraste qui survit quand une brochure couleur doit devenir grise

Ce que RecolorDocument calcule pour chaque couleur

PDF Library for Delphi expose la conversion comme RecolorDocument(TargetSpace) sur TPDFlib, avec 1 pour gris, 2 pour RGB et 3 pour CMYK. Elle renvoie le nombre d'éléments convertis, et la géométrie, les positions de texte et le placement d'images restent en place. Seules les valeurs de couleur et les noms d'espaces de couleur autour d'elles changent

Chaque couleur passe par RGB en route vers la cible. Le gris est la somme de luminance 0.299 R + 0.587 G + 0.114 B. Le CMYK atteint le RGB par la règle device simple d'ISO 32000-1 §10.3, où chaque canal vaut un moins la somme de son encrage et du noir, borné à zéro. Le RGB atteint le CMYK en poussant la plus grande composante dans le noir : K = 1 - max(R, G, B), et les encrages restants s'échelonnent sur ce qui reste. Quelques résultats à connaître avant d'envoyer quoi que ce soit chez un imprimeur :

  • Un gris de 0.4 g converti en CMYK devient 0 0 0 0.6 k, encrage noir seulement, ce que veut une presse pour le texte et les filets
  • Le RGB 1 0.5 0 (orange) devient 0 0.5 1 0 k, sans aucun noir
  • Un noir riche tel que 0.6 0.4 0.4 1 k devient 0 g en route vers le gris, les encrages supplémentaires disparaissent simplement
  • Le cyan pur, 1 0 0 0 k, se transforme en 0.701 g, plus clair que ce que la plupart des gens attendent d'un encrage saturé

Les opérandes convertis s'écrivent avec au plus trois décimales et toujours avec un point comme séparateur décimal, quelle que soit la locale de la machine qui exécute le job. ISO 32000-1 §7.3.3 n'autorise rien d'autre dans un flux de contenu, et un convertisseur qui formate les nombres avec la locale du système écrira 0,5 g sur une station allemande, ce qu'un lecteur analyse comme le nombre 0 suivi d'un opérateur inconnu

var
  Lib: TPDFlib;
  Converted: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('brochure.pdf', '') <> 1 then
      raise Exception.Create('Could not open brochure.pdf');
    Converted := Lib.RecolorDocument(1);   // 1 = gris, 2 = RGB, 3 = CMYK
    WriteLn(Format('%d colors, images and gradients converted', [Converted]));
    Lib.SaveToFile('brochure-gray.pdf');
  finally
    Lib.Free;
  end;
end;

Une page dont les couleurs sont déjà dans l'espace cible garde ses octets de contenu d'origine. RecolorDocument ne compte que les conversions qui traversent réellement d'un espace à un autre, si bien qu'un document déjà gris rapporte 0 et rien ne s'y réécrit. Sur un gros fichier cela compte : un diff de l'entrée et de la sortie montre exactement les pages qui ont changé

Pourquoi RecolorPage laisse la photo de couverture en couleur ?

RecolorPage convertit les opérateurs de couleur de la page sélectionnée et s'arrête là. Les images et les dégradés sont des objets au niveau du document, souvent partagés par plusieurs pages, et seul RecolorDocument les convertit. La façon évidente de convertir juste une couverture — sélectionner la page 1 et appeler RecolorPage(3) — donne donc des titres et filets CMYK sur une photographie RGB

Ce partage est délibéré. Un XObject image dessiné sur la couverture peut aussi l'être en page 12, et le convertir pour une page le convertit pour les deux. Un appel à portée de page qui atteindrait en silence des ressources partagées changerait des pages que l'appelant n'a jamais sélectionnées. C'est la pire surprise des deux, donc l'appel par page ne touche que ce qui appartient à la page

// Opérateurs de la couverture seulement ; images et dégradés ne sont pas convertis ici
Lib.SelectPage(1);
Changed := Lib.RecolorPage(3);
if Changed = 0 then
  WriteLn('Cover already uses CMYK operators (or the target was out of range)');
Lib.SaveToFile('cover-cmyk-operators.pdf');

// Pour un document qui doit tenir dans un seul espace, convertissez tout
Lib.RecolorDocument(3);
Lib.SaveToFile('all-cmyk.pdf');

Surveillez la valeur de retour dans cet extrait. Les deux appels renvoient 0 pour une cible hors de 1 à 3, et 0 est aussi la réponse d'une page qui n'avait besoin d'aucun changement, si bien qu'une coquille dans la cible ressemble exactement à un document déjà correct. Validez l'argument vous-même avant l'appel. RecolorDocument restaure la page sélectionnée avant son exécution, vous pouvez donc l'appeler au milieu d'un job page par page sans re-sélectionner ensuite

Encore un détail de portée. La passe d'opérateurs lit le flux de contenu propre à chaque page, les instructions de dessin listées sur la page elle-même, et convertit g, rg, k, leurs formes de tracé, et sc / scn quand l'espace courant est un espace device. Quand /DeviceRGB cs précède un sc converti, le nom d'espace est réécrit vers la cible aussi. Sautez ce renommage dans un convertisseur écrit à la main et un lecteur se met à chercher trois composantes RGB là où siège désormais une valeur grise

Images et palettes : lesquelles changent, lesquelles gardent leurs octets

Les images comptent autant que les opérateurs. Une brochure convertie en gris avec seulement ses opérateurs traités montre encore des photographies en couleur sur des pages grises, ce qui se lit pire que l'original ou qu'une conversion complète. RecolorDocument convertit une image quand elle a huit bits par composante, un espace de couleur device, et soit pas de compression soit du Flate simple sans paramètres de décodage

Les images indexées empruntent une autre route. Leurs échantillons sont des indices dans une table de couleurs, donc c'est la table qui est convertie et chaque échantillon garde l'indice qu'il détenait déjà. Une capture d'écran ou un graphique enregistré en image à palette 256 couleurs se convertit ainsi, quel que soit le rangement ou la compression de ses échantillons, car les échantillons ne sont jamais décodés. La base de la palette doit être un espace device ; une palette sur un profil ICC garde sa table

Diagramme montrant quelles parties d'un PDF RecolorDocument réécrit en Delphi et lesquelles, comme les images JPEG et les masques souples, passent à l'identique d'octet
Une passe RecolorDocument réécrit opérateurs, images éligibles, palettes indexées et paires de dégradé tandis que les octets JPEG, les masques et les couleurs liées à un profil passent intacts

Une image JPEG ou codée fax garde ses octets. Changer ses couleurs signifierait décoder, convertir et recompresser, ce qui ajoute une seconde génération d'artefacts JPEG et change la taille du fichier sans que personne ne l'ait demandé. Si un job exige vraiment chaque photo en gris, le réencodage est une décision séparée avec son propre réglage de qualité, et nous préférons que l'appelant la prenne explicitement plutôt qu'une conversion de couleur la prenne pour lui

Les masques souples et les masques pochoir restent en l'état pour une autre raison. Un masque souple est généralement une image DeviceGray, mais ses valeurs signifient une couverture. En convertir un en RGB à trois composantes et un lecteur cesse de le traiter comme de la transparence ; il finit peint comme une œuvre grise par-dessus le contenu qu'il était censé façonner. RecolorDocument collecte chaque objet référencé via /SMask ou /Mask avant de convertir la moindre image, et une image avec /ImageMask true n'a aucune couleur à convertir pour commencer

Les couleurs posées via des espaces ICCBased, Separation, Indexed ou Pattern au niveau des opérateurs restent aussi telles qu'écrites. Transformer une couleur spot ou une valeur étiquetée d'un profil en gris device sans lire son profil changerait l'apparence de la page, et une passe dont le métier est de préserver l'apparence ne peut pas se mettre à deviner ce que voulait dire un profil

Dégradés : l'espace de couleur et les arrêts bougent ensemble

Un shading déclare son espace de couleur une fois et remet chaque arrêt à une fonction. Renommez /ColorSpace de DeviceRGB vers DeviceGray alors que la fonction rend toujours trois nombres, et un lecteur prend la première d'entre elles, le canal rouge, comme niveau de gris. Un dégradé bleu vers blanc se rend alors en bande qui démarre au noir. RecolorDocument parcourt d'abord tout l'arbre de fonctions en lecture seule et ne convertit rien tant que chaque nœud ne peut pas l'être ; ce n'est qu'alors qu'il réécrit les arrêts et renomme l'espace

Les fonctions qu'il réécrit sont le type exponentiel, dont les deux couleurs de bout /C0 et /C1 sont de simples tableaux, et les fonctions de couture construites à partir d'elles. Une couleur /Background est convertie avec le reste. Un shading défini par une table échantillonnée ou un programme PostScript garde sa définition, tout comme les types de maillage 4 à 7, qui stockent les couleurs dans un flux plutôt que dans une fonction. Réécrire l'un ou l'autre est de la régénération, un autre métier que convertir une couleur

Diagramme de PDF Library for Delphi de la conversion de dégradés appariant un espace de couleur de shading à sa fonction d'arrêts à travers objets autonomes, patterns et ressources de shading de page, tandis que les shadings échantillonnés, PostScript et maillage gardent leur propre définition
Convertir un dégradé signifie déplacer ensemble son espace de couleur et les arrêts derrière lui, où que vive le shading, et les shadings maillage ou à fonction échantillonnée gardent leur propre définition au lieu de cela

Trouver les dégradés est un piège à part entière. Les outils de design donnent rarement à un shading un objet à lui ; il siège habituellement en ligne dans le dictionnaire de pattern qui le peint, ou dans les ressources /Shading d'une page. Quand nous avons construit la passe de dégradés, un scan des objets de premier niveau n'en a presque trouvé aucun. La recherche suit désormais le graphe d'objets, bornée à douze niveaux de profondeur et rendant chaque numéro d'objet une seule fois, si bien qu'une ressource partagée par toutes les pages est convertie une fois et qu'un fichier pointant un objet vers lui-même ne peut pas boucler

Où se situe le recolorage avant une soumission presse

Traitez RecolorDocument comme une normalisation. Elle convertit des valeurs entre espaces device ; elle n'applique pas un profil ICC, n'honore pas une intention de rendu et ne simule pas une condition presse. Pour un job qui doit correspondre à une épreuve contractuelle, un workflow piloté par profil est le bon outil, et une passe de recolorage est au plus l'étape qui met tout dans un seul espace avant que ce workflow ne tourne

Là où elle gagne sa vie, c'est le cas de production quotidien : une soumission qui doit être grise et arrive avec un logo RGB, un rapport interne qui ne doit pas s'imprimer en couleur, un lot qu'une étape de preflight rejettera tant que chaque fichier n'utilise pas un seul espace. Tout ce qui quitte l'entreprise devrait passer par RecolorDocument, jamais par RecolorPage seul, car un fichier à moitié converti passe un contrôle visuel rapide puis échoue chez l'imprimeur. Faites tourner le verdict dans le même job : les notes sur le preflight PDF/A et PDF/UA et sur le workbench de conformité et de signature montrent comment

// IsValidRecolorTarget est un helper que vous ajoutez vous-même ; la bibliothèque n'a pas un tel appel
function IsValidRecolorTarget(Space: Integer): Boolean;
begin
  Result := (Space >= 1) and (Space <= 3);
end;

procedure NormalizeFile(const Src, Dst: string; Space: Integer);
var
  Lib: TPDFlib;
begin
  if not IsValidRecolorTarget(Space) then
    raise EArgumentException.CreateFmt('Bad recolor target %d', [Space]);
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(Src, '') <> 1 then
      raise Exception.CreateFmt('Could not open %s', [Src]);
    if Lib.RecolorDocument(Space) = 0 then
      WriteLn(Src, ': already in the target space, copied unchanged');
    Lib.SaveToFile(Dst);
  finally
    Lib.Free;
  end;
end;

Un contrôle rapide à faire sur la sortie : ouvrez le contenu d'une page convertie dans n'importe quel inspecteur PDF après une conversion en gris. Le contenu propre de la page ne devrait plus contenir aucun opérateur rg, RG, k ou K. La couleur qui survit à la passe vient des cas ci-dessus : un JPEG, une couleur spot ou étiquetée d'un profil, un dégradé échantillonné ou maillage, ou une œuvre dessinée dans un form XObject, dont les instructions vivent dans un flux à part plutôt que dans le contenu de page

Si vous construisez aussi l'œuvre vectorielle au lieu de seulement la convertir, le parcours d'graphiques vectoriels, shaders et dégradés montre comment les shadings et patterns se créent au départ, ce qui rend les règles de dégradés ci-dessus plus faciles à prévoir

La passe couleur tourne en intra-processus : PDF Library for Delphi est du Pascal natif pour Delphi, C++Builder et Lazarus, donc normaliser un lot de fichiers entrants ne demande ni convertisseur externe ni outil en ligne de commande sur le serveur. La page produit de PDF Library for Delphi liste le reste de son API de traitement de documents