Article technique

Import EPS dans PDFlibPas : gsave, ImageMatrix et imagemask

Un logo EPS posé sur une page PDF peut sortir avec son contour manquant, une marque monochrome imprimée en son propre négatif, ou Æ transformé en á. Chaque symptôme marque un endroit où la sémantique graphique de PostScript et celle de PDF ne sont pas d'accord, si bien que l'import EPS de PDFlibPas pour Delphi interprète le programme PostScript et écrit chaque différence en opérateurs PDF natifs

Interpréter le programme ou le passer tel quel

Le PostScript peut se loger à l'intérieur d'un PDF de deux façons, et une seule montre quelque chose à l'écran. ISO 32000-1 §8.8.2 définit un PostScript XObject, un flux /Subtype /PS dont les octets sont copiés, sans interprétation, dans le travail d'impression seulement quand le périphérique de sortie est une imprimante PostScript ; partout ailleurs il n'a aucun effet, et la vue d'ensemble de la §8.8 dit que les PostScript XObjects ne doivent pas être utilisés. Le PDF/A les exclut carrément. TPDFlib.NewPostScriptXObject et DrawPostScriptXObject écrivent cette forme de passage direct, tandis que ImportEPSFromFile et DrawPostScript font passer le programme au travers d'un interpréteur et émettent des chemins ordinaires, des images en ligne et du texte. Envoyez le dessin au travers de l'interpréteur, et gardez un PS XObject pour un fragment destiné au RIP d'un atelier d'impression, le raster image processor devant une presse ou une imprimante de production. Interpréter un EPS est aussi un autre métier que l'import vectoriel EMF, où l'entrée est une liste d'enregistrements GDI à rejouer ; un fichier EPS est un programme

procedure AddPrintRoomHook(PDF: TPDFlib);
var
  PSRef: Integer;
begin
  // Stocké verbatim comme flux /Subtype /PS et référencé avec Do.
  // Il ne s'exécute que sur une imprimante PostScript ; les visionneurs ne dessinent rien
  PSRef := PDF.NewPostScriptXObject(
    'userdict /PrintRoomMark known { PrintRoomMark } if');
  if PSRef <> 0 then
    PDF.DrawPostScriptXObject(PSRef);
end;
Là où le modèle d'exécution PostScript et PDF se séparent, et ce que produit une traduction d'opérateurs ligne à ligne
Comportement PostScriptComportement PDFRésultat d'une traduction littérale
gsave / grestore sauvegardent et restaurent le chemin courant et le point courantq / Q laissent le chemin tranquille, et la peinture le termine (§8.5.1)Un contour ou un segment en cours après grestore disparaît
clip garde le chemin courant pour une peinture ultérieureW est suivi d'un opérateur de peinture, d'ordinaire n, qui termine le cheminUn contour après le clip n'a rien à tracer
ImageMatrix mappe l'espace utilisateur vers l'espace imageChaque image remplit le carré unitaire, première rangée en haut (§8.9.4)Les images de bas en haut ou tournées arrivent retournées ou mal placées
true imagemask peint les bits à 1Un stencil avec le Decode [0 1] par défaut peint les bits à 0 (§8.9.6.2)Un logo monochrome s'imprime en son négatif
Les polices Roman Standard-14 ont StandardEncoding par défautLe nom StandardEncoding n'a pas de sens particulier (§9.6.6.1)Accents, guillemets et ligatures changent de lettres
Les commentaires %%BoundingBox et %%HiResBoundingBox définissent la zone du dessinLe placement est la matrice que l'écrivain émetLe dessin se décale ou change d'échelle hors de sa boîte prévue

Où le contour est-il passé après grestore ?

Le contour a disparu parce que PDF n'a jamais sauvegardé le chemin. ISO 32000-1 §8.5.1 le dit en une phrase : « En PDF (contrairement à PostScript), le chemin courant ne fait pas partie de l'état graphique », et après la peinture d'un chemin il n'y a plus de chemin courant jusqu'à ce qu'un m ou un re en commence un nouveau. Les producteurs PostScript comptent sur la règle opposée. La manière habituelle de dessiner une forme remplie avec un contour est gsave fill grestore stroke : le remplissage consomme une copie sauvegardée du chemin, grestore rapporte le chemin, et le contour peint la même géométrie. Mappez gsave sur q, fill sur f et grestore sur Q, et le contour arrive avec un chemin vide, si bien que la forme est remplie et le contour envolé. Un lineto après grestore qui continue le chemin restauré échoue de la même façon, en segment manquant

Chronologie comparant gsave fill grestore stroke en PostScript à la traduction littérale q f Q S dans l'import EPS de PDFlibPas pour Delphi
la correspondance littérale q f Q termine le chemin au remplissage, si bien que PDFlibPas garde un chemin PostScript privé et que le contour après grestore retrouve quand même sa géométrie
uses
  SysUtils, PDFlibrary;

const
  // Triangle rempli avec un contour : le remplissage tourne dans gsave/grestore,
  // et le contour après grestore réutilise le chemin restauré
  TriangleEPS =
    '%!PS-Adobe-3.0 EPSF-3.0'#10 +
    '%%BoundingBox: 0 0 200 120'#10 +
    'newpath 20 20 moveto 180 20 lineto 100 100 lineto closepath'#10 +
    'gsave 0.85 setgray fill grestore'#10 +
    '3 setlinewidth 0 setgray stroke'#10;

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);   // Left et Top mesurés depuis le coin en haut à gauche
    // Width et Height de 0 gardent la taille de boîte englobante 200 x 120
    if PDF.DrawPostScript(72, 72, 0, 0, TriangleEPS) = 1 then
      PDF.SaveToFile('triangle.pdf')
    else
      Writeln('DrawPostScript failed, code ', PDF.LastErrorCode);
    // Attendu : un triangle gris clair avec un contour noir de 3 unités.
    // Une traduction littérale q/f/Q/S remplit le triangle et perd le contour
  finally
    PDF.Free;
  end;
end.

PDFlibPas garde son propre chemin PostScript, séparé de ce que le flux de contenu PDF porte à l'instant, et n'écrit le chemin PDF que quand quelque chose peint ou découpe. Chaque point est fixé sous la matrice de transformation en vigueur quand moveto ou lineto a tourné, comme en PostScript, où un scale ou un concat ultérieur ne déplace jamais les points déjà sur le chemin. currentpoint remappe le point stocké en arrière au travers de la matrice courante, si bien qu'il répond dans l'espace utilisateur présent. Un gsave enregistre une référence à la fin de la chaîne de chemins plus le point courant, la matrice, la police et l'espace de couleurs, si bien que des sauvegardes imbriquées ne copient jamais des chemins entiers

Le découpage est l'endroit où les deux modèles de chemin se séparent encore, et l'import suit PostScript là aussi : clip et eoclip émettent W n pour le côté PDF mais laissent le chemin logique en place pour le contour qui suit souvent, tandis que rectclip, qui commence un chemin vide neuf en PostScript, émet re W n et le vide. Le stockage de chemins est plafonné à 100 000 nœuds, et les rejeux tirent sur le budget d'opérations partagé, si bien qu'un fichier hostile ne peut pas boucler gsave et stroke pour gonfler la sortie

Placer les images : ImageMatrix pointe dans l'autre sens

Une image EPS arrive retournée quand son ImageMatrix est traité comme placement, car il décrit la correspondance opposée. En PDF, la §8.9.4 attache la bordure de l'image au carré unitaire de l'espace utilisateur avec la rangée d'échantillons supérieure en haut, une matrice implicite image vers utilisateur de [1/w 0 0 -1/h 0 1]. Le ImageMatrix de PostScript mappe l'espace utilisateur vers l'espace image. La matrice de placement PDF est donc [w 0 0 -h 0 h] multipliée par l'inverse de ImageMatrix. Pour la matrice courante de haut en bas [w 0 0 -h 0 h] le produit est l'identité. Voilà pourquoi les convertisseurs naïfs ont l'air corrects en usage courant. Un balayage de bas en haut écrit avec [w 0 0 h 0 0] donne [1 0 0 -1 0 1], un retournement vertical à l'intérieur du carré unitaire, et les matrices tournées ou inclinées ne demandent aucun cas particulier. Quand une source de données finie se termine tôt, PDFlibPas ne peint que les rangées complètes, garde le ImageMatrix original et met à l'échelle par les rangées réellement arrivées, si bien qu'une image tronquée remplit la bande supérieure de sa zone à la bonne échelle ; étirer les rangées survivantes sur toute la hauteur produirait une image déformée qui semble intacte

Schéma dérivant la matrice de placement PDF de la convention ImageMatrix PostScript pour l'import EPS de PDFlibPas en Delphi, avec résultats droits et retournés et traitement des rangées tronquées
la matrice de placement est la correspondance carré unitaire PDF fois l'ImageMatrix inverse, si bien que le dessin de haut en bas arrive droit et que seuls les balayages de bas en haut se retournent

Quel bit l'imagemask peint-il ?

Avec true comme opérande de polarité, imagemask peint les bits à 1, et en PDF cela veut dire Decode [1 0]. ISO 32000-1 §8.9.6.2 définit [0 1] comme défaut d'un masque d'image, à savoir qu'un échantillon 0 marque la page avec la couleur courante et un 1 laisse l'arrière-plan tranquille, et [1 0] inverse cela. Le booléen PostScript dit quelle valeur d'échantillon peint. Le lire comme « inverse le défaut PDF » et le logo s'imprime en négatif, un bloc plein avec la marque découpée dedans. PDFlibPas mappe true sur [1 0] et false sur [0 1], émet le stencil comme image en ligne avec /IM true pour qu'il peigne dans la couleur que le programme a posée, et sous la forme dictionnaire n'accepte que ces deux tableaux Decode

Polarité imagemask PostScript mappée aux tableaux Decode PDF dans l'import EPS de PDFlibPas pour Delphi : true peint les bits à 1, false imprime le négatif
le booléen PostScript nomme la valeur d'échantillon qui peint, et mapper true sur Decode [1 0] garde un logo monochrome de s'imprimer en son négatif

Les échantillons de douze bits cachent un décalage plus petit. PDF ne connaît pas d'images 12 bits ; la §8.9.5.1 admet 1, 2, 4, 8 et, depuis PDF 1.5, 16 bits par composante. Décaler chaque échantillon de quatre bits vers la gauche le fait tenir dans 16 bits sans rien perdre, mais la plus grande valeur devient 65520, pas 65535, si bien qu'avec le Decode par défaut chaque échantillon clair sort environ 0.023 pour cent trop sombre. L'import garde le décalage et étire la borne haute du Decode par 65535/65520, ce qui restaure chaque valeur normalisée. Une image 12 bits monte la version de sortie à PDF 1.5 automatiquement ; en mode PDF/A ou PDF/X, ou quand la version de sortie est verrouillée sous 1.5, l'import s'arrête avec l'erreur 538 avant que la page ne change

Pourquoi Æ sort-il en á ?

Une police Roman en PostScript utilise StandardEncoding tant que le programme ne la réencode pas, et PDF ne peut pas nommer cette table : ISO 32000-1 §9.6.6.1 ne donne aucun sens particulier au nom, laissant WinAnsiEncoding, MacRomanEncoding et MacExpertEncoding comme choix prédéfinis. Les tables de l'annexe D.2 montrent ce qui tourne mal quand les codes sont remis à WinAnsi inchangés : l'octal 341 (0xE1) est AE en StandardEncoding et aacute en WinAnsi, si bien que Æ devient á ; 0xAE est la ligature fi dans l'un et ® dans l'autre ; 0x27 est quoteright en StandardEncoding et un quotesingle droit en WinAnsi, ce qui change les apostrophes typographiques en apostrophes de machine à écrire. Pour chaque police Roman Standard-14 qu'un programme utilise, PDFlibPas écrit une ressource de police Type 1 privée dont le dictionnaire /Encoding porte /BaseEncoding /WinAnsiEncoding et un tableau /Differences qui nomme les 256 codes dans l'ordre StandardEncoding, si bien qu'aucun code ne dépend de la façon dont un visionneur remplit sa table de base. Les largeurs viennent du même encodage, une table ToUnicode laisse copie et recherche rendre Æ, et Symbol et ZapfDingbats gardent leurs codes intégrés. La police que votre code avait sélectionnée avant l'import reste intacte, car réencoder une police en cache pour plaire à l'EPS changerait le texte déjà posé avec elle

Ces ressources nomment les polices sans les embarquer, ce qui se heurte aux profils qui exigent l'embarquement. Dans un document préparé pour PDF/A, PDF/X ou PDF/UA, ou après SetEmbedAllFonts(1), le texte EPS échoue avec l'erreur 538 et la page reste telle quelle. Pour une sortie d'archivage, la route plus propre est un EPS exporté avec le texte converti en contours, qui s'importe comme chemins plats dans n'importe quel mode ; les polices déjà laissées non embarquées dans un fichier existant demandent l'embarquement des polices manquantes pour le PDF/A à la place. Le texte EPS se mappe sur les 14 polices standard par leurs noms exacts ; un dessin qui embarque son propre programme de police s'importe proprement une fois son texte passé en contours, ce qui empêche aussi une police de substitution locale de déplacer chaque ligne

Boîtes englobantes, commentaires DSC et fichiers EPS binaires

Le placement met la boîte englobante du dessin à l'échelle dans le rectangle que vous passez, si bien que la boîte doit venir du bon endroit. PDFlibPas ne lit %%BoundingBox et %%HiResBoundingBox que dans de vraies lignes de commentaires DSC, c'est-à-dire des commentaires de premier niveau qui ouvrent une ligne, et une boîte haute résolution valide l'emporte sur la boîte entière. Une recherche de sous-chaîne naïve trouve aussi ces mots-clés à l'intérieur de chaînes littérales, de corps de procédures et de données d'image binaires. Les fichiers EPS portent les trois. Une valeur d'en-tête (atend) reporte les nombres à un commentaire dans le trailer, lu quand le scanner l'atteint ; un fichier sans aucune boîte retombe sur 612 par 792, et une boîte vide ou inversée échoue avec l'erreur 124. Width et Height mettent la boîte à l'échelle indépendamment : passez 0 pour les deux pour garder la taille naturelle, ou passez les deux dans le rapport de forme de la boîte, car une seule valeur non nulle étire le dessin le long d'un seul axe

Les données binaires décident du point d'entrée. DrawPostScript prend un WideString, très bien pour du PostScript construit dans le code, mais les échantillons d'images bruts, les données Flate lues au travers de currentfile et l'enveloppe DOS EPS contiennent des octets qui ne survivent pas à un voyage en texte Unicode. ImportEPSFromFile lit le fichier en octets bruts, reconnaît l'en-tête de prévisualisation C5 D0 D3 C6, vérifie que l'offset et la longueur du segment PostScript tombent à l'intérieur du fichier, et n'exécute que ce segment en ignorant la prévisualisation TIFF ou WMF. Les deux appels prennent au plus 4 Mio de source, exigent Options = 0, renvoient 1 ou 0, et ajoutent soit tout le dessin soit rien. La raison de l'échec se trouve dans LastErrorCode, qui se remet à zéro quand on le lit, donc capturez-le une fois

uses
  SysUtils, PDFlibrary;

function PlaceEPS(PDF: TPDFlib; const FileName: string;
  Left, Top: Double): Boolean;
var
  Code: Integer;
begin
  // 0, 0 : taille naturelle depuis %%HiResBoundingBox ou %%BoundingBox
  Result := PDF.ImportEPSFromFile(Left, Top, 0, 0, FileName) = 1;
  if Result then
    Exit;
  Code := PDF.LastErrorCode;   // la lecture efface le code, donc le lire une fois
  case Code of
    402:
      Writeln(FileName, ': file cannot be opened');
    PDFLIB_ERROR_POSTSCRIPT_INVALID:      // 124
      Writeln(FileName, ': malformed PostScript, bad bounding box or nothing painted');
    PDFLIB_ERROR_POSTSCRIPT_UNSUPPORTED:  // 538
      Writeln(FileName, ': construct the import does not handle, or Standard-14 text in an archival profile');
    PDFLIB_ERROR_POSTSCRIPT_BUDGET:       // 539
      Writeln(FileName, ': source over 4 MiB or a work limit reached');
  else
    Writeln(FileName, ': import failed with code ', Code);
  end;
end;

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    if PlaceEPS(PDF, 'logo.eps', 40, 40) then
      PDF.SaveToFile('letterhead.pdf');
  finally
    PDF.Free;
  end;
end.

Quand l'erreur 538 nomme un opérateur, réexportez le dessin depuis son éditeur en EPS simple avec le texte converti en contours : c'est d'ordinaire plus rapide que de courir après l'opérateur, et un dessin en contours se place aussi dans un document PDF/A ou PDF/UA construit avec la PDFlibPas Delphi PDF Library sans le conflit de polices Standard-14