PDFlibPas, la bibliothèque PDF losLab pour Delphi, convertit les enregistrements EMF Poly* en tracés PDF en suivant à la lettre chaque définition de [MS-EMF] : un EMR_POLYBEZIER 32 bits démarre au point 0, les polylines restent ouvertes et sont seulement tracées au trait, PT_CLOSEFIGURE dans EMR_POLYDRAW est un drapeau, et chaque compte de points est vérifié contre la taille de l’enregistrement. Ces règles sont arrivées en trois temps, v3.539.39, v3.539.41 et v3.539.43. Avant, un graphique de rapport pouvait sortir de ImportEMFFromFile avec un coin rempli là où une courbe de tendance était attendue, une courbe de Bézier pliée vers le mauvais point de contrôle, ou un contour fermé qui perdait son dernier côté. Aucun de ces défauts ne levait d’erreur, et ces règles valent pour n’importe quel convertisseur Delphi EMF vers PDF ou parseur d’enregistrements GDI
Pourquoi les enregistrements EMF Poly* posent-ils problème en conversion PDF ?
Si ces enregistrements tournent mal, c’est que chacun porte une partie de son sens en dehors de ses points : la figure est-elle ouverte, démarre-t-elle à la position courante, quels crayon et brosse s’appliquent, et où commencent les points dans l’enregistrement. Un métafichier enrichi est un enregistrement d’appels GDI passés à un device context, donc un convertisseur doit rejouer l’état de ce contexte en plus des coordonnées. PDF n’a pas de device context. Il a un path, un point courant dedans, et un opérateur de peinture qui tranche entre stroke (S), fill (f) et les deux (B). Chaque désaccord entre les deux modèles devient une différence de rendu silencieuse
La famille Poly* existe aussi en deux largeurs. Chaque enregistrement 32 bits comme EMR_POLYLINE a un jumeau 16 bits comme EMR_POLYLINE16 qui stocke les points en paires de SmallInt. GDI écrit d’habitude la forme compacte dès que chaque coordonnée tient, si bien que les gestionnaires 32 bits d’un convertisseur peuvent rester faux pendant des années pendant que les dessins de test du quotidien ne les atteignent jamais. L’audit le plus rapide consiste à passer les mêmes points par les deux enregistrements et à comparer les tracés obtenus. Les enregistrements traités ici sont tous dans le groupe des enregistrements de dessin de [MS-EMF] (2.3.5 Drawing Record Types)
| Enregistrement | Démarre à | Fermé ? | Position courante |
|---|---|---|---|
EMR_POLYBEZIER | Point 0 | Non | Non utilisée, non mise à jour |
EMR_POLYLINE | Point 0 | Non (crayon seul) | Non utilisée, non mise à jour |
EMR_POLYLINETO | Position courante | Non (crayon seul) | Utilisée et mise à jour |
EMR_POLYPOLYLINE | Premier point de chaque polyline | Non (crayon seul) | Non utilisée, non mise à jour |
EMR_POLYDRAW | Premier PT_MOVETO, ou position courante | Là où PT_CLOSEFIGURE est posé | Utilisée et mise à jour |
Où une courbe EMR_POLYBEZIER démarre-t-elle réellement ?
Une courbe EMR_POLYBEZIER démarre au point 0, et seuls les points à partir de l’index 1 sont groupés par trois en point de contrôle, point de contrôle, point final. Un enregistrement de 7 points dessine donc deux segments cubiques : 0 est le départ, 1 à 3 forment le premier segment, 4 à 6 le second. Le gestionnaire 16 bits de PDFlibPas faisait déjà ça. Le gestionnaire 32 bits commençait son groupement au point 0, donc le point de départ se retrouvait avalé comme premier point de contrôle et chaque segment suivant était décalé d’un cran. La courbe se rendait quand même, juste pas la bonne. Depuis v3.539.41, les deux largeurs ouvrent le path avec m au point 0 et émettent un c par triplet complet ensuite
Pour votre propre parseur : un compte qui n’est pas 1 plus un multiple de 3 est mal formé, et les points traînants doivent être ignorés plutôt que recousus en une courbe
PolyDraw : PT_CLOSEFIGURE est un drapeau, pas un type de point
Dans EMR_POLYDRAW, PT_CLOSEFIGURE (valeur 1) est un bit combiné avec PT_LINETO (2) ou PT_BEZIERTO (4), donc un octet de type valide peut valoir 3 ou 5. Le type du point est l’octet masqué de ce bit, et le drapeau signifie fermer la figure après le segment qui se termine sur ce point. L’ancien gestionnaire de PDFlibPas comparait l’octet à des valeurs simples dans un case, donc les points de type 3 et 5 ne correspondaient à rien et étaient entièrement sautés. Un rectangle dessiné en PolyDraw perdait son côté de fermeture, et un triplet de Bézier dont le dernier point portait le drapeau perdait ce point, ce qui désynchronisait tous les triplets suivants
Depuis v3.539.39, le type se lit comme Types[i] and not PT_CLOSEFIGURE, et la fermeture n’est émise qu’après un segment complet : après la ligne pour un PT_LINETO fermé, et après le troisième point d’un groupe de Bézier. Un fichier mal formé qui pose le drapeau sur le premier ou le deuxième point d’un triplet ne ferme pas la figure prématurément. Deux corrections liées sont livrées dans la même version :
- Chaque
PT_MOVETOduEMR_POLYDRAW1616 bits redémarrait le path entier, si bien qu’un enregistrement portant trois figures ne gardait que la dernière ; désormais le premier move démarre le path et les moves suivants ouvrent des sous-chemins - Un enregistrement PolyDraw qui ne commence pas par
PT_MOVETOdémarre à la position courante, comme le dit la définition de l’enregistrement, au lieu d’écrire un opérateurloucsansmpréalable
Pourquoi une polyline EMF ne doit-elle jamais être remplie en PDF ?
Une polyline EMF ne doit jamais être remplie parce que EMR_POLYLINE et EMR_POLYPOLYLINE sont des figures ouvertes dessinées au crayon seul, et remplir un path ouvert en PDF le ferme implicitement. ISO 32000-1 §8.5.3 dit que les opérateurs de remplissage ferment tout sous-chemin ouvert avant de le peindre. Un convertisseur qui émet B ou f pour une polyline de trois points peint donc un triangle rempli dans la couleur de brosse courante : le coin rempli sous une courbe de tendance de graphique. Avant v3.539.41, PDFlibPas remplissait les deux largeurs de polyline à la brosse, et l’enregistrement 32 bits était en plus fermé explicitement. Aujourd’hui les deux largeurs se terminent en stroke seul, et la distinction GDI est préservée : Polygon ferme et remplit, Polyline jamais
PolylineTo démarre à la position courante
EMR_POLYLINETO dessine depuis la position courante à travers chaque point de l’enregistrement, reste ouvert, et laisse la position courante au dernier point. L’ancien gestionnaire contenait aussi un cas particulier qui éteignait le crayon quand les deux premiers points partageaient un même y, et rien ne le rallumait jamais, donc chaque enregistrement suivant du fichier perdait son contour. L’état du crayon appartient à EMR_SELECTOBJECT et EMR_CREATEPEN ; un gestionnaire d’enregistrement de dessin n’a pas à y toucher. Ce cas particulier a sauté en v3.539.41, et la forme à un seul point de l’enregistrement ne lit plus au-delà de ses propres points (corrigé en v3.539.39)
Les points d’une PolyPolyline démarrent après le tableau de comptes
Le EMR_POLYPOLYLINE 32 bits stocke nPolys comptes puis cptl points, et les points commencent à l’offset d’octet 32 + nPolys * 4. Le piège est dans la RTL : l’unité Windows déclare TEMRPolyPolyline avec aPolyCounts et aptl en tableaux à un élément, donc aptl[0] n’est le premier point que quand nPolys vaut 1. Du code qui indexe aptl directement lit des valeurs de comptes comme coordonnées pour chaque enregistrement multi-lignes. L’ancien gestionnaire de PDFlibPas calait en plus son contrôle de bornes sur ce mauvais agencement, donc les enregistrements multi-lignes valides étaient rejetés et les mono-ligne ne dessinaient rien. Depuis v3.539.41, PDFlibPas localise le tableau de points depuis l’offset calculé, comme son gestionnaire PolyPolygon l’a toujours fait, et dessine chaque polyline comme son propre sous-chemin ouvert avec un seul stroke à la fin. En v3.539.43, le jumeau 16 bits a reçu le même traitement ; il dessinait segment par segment, ce qui cassait les jointures de lignes et ignorait un NULL_PEN sélectionné
Le crayon et la brosse par défaut, et les crochets de path
Deux règles d’état complètent les corrections de polyline en v3.539.43 :
- Un device context GDI neuf a déjà
BLACK_PENetWHITE_BRUSHsélectionnés, donc un métafichier qui dessine sans aucunEMR_SELECTOBJECTtrace quand même des contours noirs ; le convertisseur partait sans crayon ni remplissage et écrivaitn(fin de path, ne peint rien) pour ces enregistrements - Dans un crochet
BeginPath/EndPath, unePolylinen’utilise ni ne met à jour la position courante, donc elle doit ouvrir un nouveau sous-chemin à son premier point au lieu de se raccorder à la figure précédente, et rien ne peut être peint tant que le crochet n’est pas tracé ou rempli
Construire un fichier EMF de test avec TMetafileCanvas
Le plus rapide pour vérifier un convertisseur contre ces règles est d’enregistrer les trois appels risqués dans un seul métafichier enrichi avec TMetafileCanvas. Le dessin ci-dessous enregistre les courbes avec une brosse creuse puis sélectionne exprès une brosse jaune unie pour la polyline : un convertisseur correct doit ignorer cette brosse pour la polyline, donc tout jaune dans le PDF de sortie est un bug. PolyDraw n’a pas de wrapper TCanvas, il s’appelle donc via l’API Windows avec le handle du canvas, avec des octets de type 3 et 5 pour exercer le drapeau de fermeture
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Un carré fermé (3 = LINETO + CLOSEFIGURE), puis une figure de Bézier
// fermée dont le dernier triplet de contrôle finit à 5 = BEZIERTO + CLOSEFIGURE
DrawPts: array[0..7] of TPoint = (
(X: 300; Y: 40), (X: 380; Y: 40), (X: 380; Y: 120), (X: 300; Y: 120),
(X: 420; Y: 120), (X: 440; Y: 40), (X: 520; Y: 40), (X: 540; Y: 120));
DrawTypes: array[0..7] of Byte = (
PT_MOVETO, PT_LINETO, PT_LINETO, PT_LINETO or PT_CLOSEFIGURE,
PT_MOVETO, PT_BEZIERTO, PT_BEZIERTO, PT_BEZIERTO or PT_CLOSEFIGURE);
var
Mf: TMetafile;
Canvas: TMetafileCanvas;
begin
Mf := TMetafile.Create;
try
Mf.Enhanced := True;
Mf.Width := 600;
Mf.Height := 260;
Canvas := TMetafileCanvas.Create(Mf, 0);
try
Canvas.Pen.Color := clNavy;
Canvas.Pen.Width := 2;
Canvas.Brush.Style := bsClear; // contours seuls pour les courbes
// Le point 0 est le départ ; 1..3 et 4..6 sont deux segments cubiques
Canvas.PolyBezier([Point(20, 120), Point(60, 20), Point(100, 220),
Point(140, 120), Point(180, 20), Point(220, 220), Point(260, 120)]);
PolyDraw(Canvas.Handle, DrawPts[0], DrawTypes[0], Length(DrawPts));
// V ouvert avec brosse unie sélectionnée : tracé au trait, jamais
// fermé en triangle jaune
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // termine l’enregistrement
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Comme ces coordonnées tiennent dans un SmallInt, GDI stockera normalement les variantes 16 bits. Pour atteindre les gestionnaires 32 bits, il faut un producteur qui les écrit, ou des enregistrements construits à la main. Les fichiers faits main ont leur propre piège : TMetafile.LoadFromStream de la VCL ne traite le flux comme un EMF que si la longueur restante est strictement supérieure aux 108 octets du TEnhMetaHeader. Un EMF minimal écrit à la main avec un en-tête court, ou un fichier vide d’exactement 108 octets, est pris pour un WMF et rejeté avec « Metafile is not valid ». Écrivez toujours l’en-tête complet de 108 octets, champs d’extension compris, avant vos enregistrements de test
Importer l’EMF dans un PDF avec PDFlibPas
PDFlibPas importe un EMF avec ImportEMFFromFile ou ImportEMFFromStream, qui renvoient un ID d’image non nul en cas de succès et 0 en cas d’échec. GeneralOptions = 0 garde le tracé vectoriel dont cet article parle ; 1 rastérise le métafichier en bitmap à la place. FontOptions = 1 ajoute les polices du métafichier comme polices TrueType non embarquées. La variante par flux rembobine le flux à la position 0 avant le chargement, donc passez un flux qui ne contient que le métafichier
uses
System.SysUtils, PDFlibrary;
procedure EmfToPdf(const EmfFile, PdfFile: WideString);
var
PDF: TPDFlib;
ImageID: Integer;
PageOps: AnsiString;
begin
PDF := TPDFlib.Create;
try
PDF.SetOrigin(1); // origine en haut à gauche pour DrawImage
PDF.SetMeasurementUnits(0); // points
// FontOptions 1 = ajoute les polices en TrueType non embarquées
// GeneralOptions 0 = import vectoriel, 1 = bitmap
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// Pour un EMF, ImageWidth / ImageHeight sont la taille du cadre en points
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// La page ne fait qu’invoquer le formulaire importé : q ... cm /Name Do Q
PageOps := PDF.GetPageContentToString;
if Pos(AnsiString(' Do'), PageOps) = 0 then
raise Exception.Create('Expected a form XObject invocation');
if PDF.SaveToFile(PdfFile) <> 1 then
raise Exception.Create('The PDF could not be saved');
finally
PDF.Free;
end;
end;
Un import vectoriel d’EMF devient un form XObject, donc GetPageContentToString ne renvoie que la séquence save, transform, Do et restore. Les opérateurs m, l, c, h et S produits à partir des enregistrements Poly* vivent dans le flux du form XObject, qui est compressé. Pour les auditer, décompressez le fichier sauvegardé dans un inspecteur d’objets PDF et lisez le flux du form : pour le fichier de test ci-dessus, vous devez voir la polyline se terminer en S sans h avant, un h à chaque drapeau de fermeture dans les figures PolyDraw, et aucun f ni B sur ces sous-chemins. DrawImage met aussi à l’échelle un EMF importé de façon uniforme selon le plus petit de Width et Height, donc le dessin garde son ratio même si la boîte passée ne le respecte pas
Pour des cibles Free Pascal, voyez comment l’importeur vectoriel EMF de PDFlibPas se compile sous Free Pascal ; la sémantique des enregistrements est la même partout où l’importeur compile
Comment un parseur EMF doit-il traiter les comptes de points venus du fichier ?
Un parseur EMF doit traiter chaque compte de points comme une entrée non fiable et le vérifier contre la taille de l’enregistrement avant de copier le moindre point. EnumEnhMetaFile garantit seulement que le nSize de chaque enregistrement reste dans le fichier. Elle ne vérifie pas que cptl concorde avec nSize, donc un gestionnaire qui copie cptl points avec Move lira les enregistrements suivants, ou au-delà de la fin du métafichier, quand le compte est forgé ou corrompu. Depuis v3.539.39, PDFlibPas vérifie en-tête fixe plus compte fois octets par point contre nSize pour PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo et Polygon dans les deux largeurs, avec un octet de plus par point pour les octets de type de PolyDraw. Pour les enregistrements PolyPoly, les comptes par figure doivent aussi s’additionner à tout au plus le total déclaré, et les figures à zéro point sont sautées
Le même contrôle est assez court pour être copié dans votre propre parseur. Cette version valide un EMR_POLYPOLYLINE 32 bits et renvoie un pointeur vers son vrai tableau de points :
uses
Winapi.Windows;
// Renvoie nil sauf si l’enregistrement tient vraiment les points déclarés.
// Les points démarrent après le tableau de comptes : à 32 + nPolys * 4
// octets, pas à aptl[0], que la RTL déclare en tableau à un élément
function PolyPolylinePoints(Rec: PEnhMetaRecord): PPoint;
var
P: PEMRPolyPolyline;
Count: PDWORD;
PointsOffset, Total: Int64;
I: Cardinal;
begin
Result := nil;
if (Rec^.iType <> EMR_POLYPOLYLINE) or (Rec^.nSize < 32) then
Exit;
P := PEMRPolyPolyline(Rec);
if P^.nPolys = 0 then
Exit;
PointsOffset := 32 + Int64(P^.nPolys) * SizeOf(DWORD);
if PointsOffset + Int64(P^.cptl) * SizeOf(TPoint) > Rec^.nSize then
Exit; // compte forgé ou tronqué
Total := 0;
Count := @P^.aPolyCounts[0]; // marche par pointeur : [0..0] déclenche les range checks
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // les figures réclament plus de points qu’il n’y en a
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
Le test de tenue des points passe en premier, donc le tableau de comptes est déjà su être dans l’enregistrement avant que la boucle ne le parcoure. L’arithmétique est en Int64 parce que nPolys * 4 et cptl * 8 calculés en 32 bits peuvent boucler et passer la comparaison
Aide-mémoire : règles EMF Poly* pour la conversion EMF vers PDF
EMR_POLYBEZIER: le point 0 est le point de départ ; groupement par trois à partir du point 1 ; corrigé pour l’enregistrement 32 bits en v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: figures ouvertes, stroke avecS, jamaish,fniB, parce que le remplissage PDF ferme les sous-chemins ouvertsEMR_POLYLINETO: démarrer à la position courante, rester ouvert, mettre à jour la position courante, ne jamais toucher à l’état du crayonEMR_POLYPOLYLINE32 bits : les points démarrent à l’octet32 + nPolys * 4, pas àaptl[0]EMR_POLYDRAW: masquerPT_CLOSEFIGUREavant le dispatch, fermer après le segment terminé, démarrer à la position courante quand le premier point n’est pasPT_MOVETO- L’état par défaut du device context est
BLACK_PENplusWHITE_BRUSH; v3.539.43 et suivantes le respectent - Dans
BeginPath/EndPath, chaque polyline ouvre son propre sous-chemin et rien n’est peint tant que le crochet n’est pas consommé - Validez chaque
cptl/cptscontrenSizeen arithmétique 64 bits avant de copier les points - Les EMF de test faits main ont besoin de l’en-tête complet de 108 octets, sinon
TMetafile.LoadFromStreamles lit comme des WMF
Si vos rapports passent par un autre composant, la même sémantique d’enregistrements s’applique ; l’import vectoriel EMF et WMF de HotPDF explique comment ce composant transforme les brosses dégradées et hachurées en patterns PDF, et graphismes vectoriels, shaders et dégradés dans PDFlibPas couvre le dessin des mêmes formes directement via l’API de la bibliothèque plutôt que par un métafichier
PDFlibPas v3.539.43 ou ultérieure inclut toutes les règles ci-dessus. Détails et téléchargements d’essai sur la page produit de la bibliothèque PDF Delphi PDFlibPas