PDFlibPas, la libreria PDF di losLab per Delphi, converte i record Poly* dell'EMF in tracciati PDF seguendo la definizione di ogni record in [MS-EMF]: un EMR_POLYBEZIER a 32 bit parte dal punto 0, le polyline restano aperte e vengono solo tracciate, PT_CLOSEFIGURE in EMR_POLYDRAW è un flag, e ogni conteggio di punti viene controllato contro la dimensione del record. Quelle regole sono approdate con le versioni v3.539.39, v3.539.41 e v3.539.43. Prima, un grafico di report poteva uscire da ImportEMFFromFile con uno spicchio pieno dove doveva stare una linea di tendenza, una curva Bezier piegata verso il punto di controllo sbagliato o un contorno chiuso a cui mancava l'ultimo lato. Nessuno di questi errori sollevava un'eccezione, e le regole valgono per qualsiasi convertitore EMF in PDF in Delphi o parser di record GDI
Perché i record Poly* dell'EMF vanno storti nella conversione in PDF?
I record Poly* dell'EMF vanno storti perché ciascuno porta parte del proprio significato fuori dai punti: se la figura è aperta, se parte dalla posizione corrente, quale penna e quale pennello si applicano e dove nel record iniziano i punti. Un enhanced metafile è una registrazione di chiamate GDI verso un device context, quindi un convertitore deve riprodurre quello stato del device context oltre alle coordinate. Il PDF non ha un device context. Ha un path, un punto corrente dentro quel path e un operatore di disegno che decide tra stroke (S), fill (f) ed entrambi (B). Ogni disallineamento tra i due modelli diventa una differenza di resa silenziosa
La famiglia Poly* esiste anche in due larghezze. Ogni record a 32 bit come EMR_POLYLINE ha un gemello a 16 bit come EMR_POLYLINE16 che memorizza i punti come coppie di SmallInt. GDI di solito registra la forma compatta quando ogni coordinata ci sta, così i gestori a 32 bit di un convertitore possono restare sbagliati per anni mentre i disegni di prova di tutti i giorni non li raggiungono mai. L'audit più rapido è far passare gli stessi punti attraverso entrambi i record e confrontare i path risultanti. I record trattati qui sono tutti nel gruppo dei record di disegno di [MS-EMF] (2.3.5 Drawing Record Types)
| Record | Parte da | Chiuso? | Posizione corrente |
|---|---|---|---|
EMR_POLYBEZIER | Punto 0 | No | Non usata, non aggiornata |
EMR_POLYLINE | Punto 0 | No (solo penna) | Non usata, non aggiornata |
EMR_POLYLINETO | Posizione corrente | No (solo penna) | Usata e aggiornata |
EMR_POLYPOLYLINE | Primo punto di ogni polyline | No (solo penna) | Non usata, non aggiornata |
EMR_POLYDRAW | Primo PT_MOVETO, o posizione corrente | Solo dove PT_CLOSEFIGURE è impostato | Usata e aggiornata |
Da dove parte davvero una curva EMR_POLYBEZIER?
Una curva EMR_POLYBEZIER parte dal punto 0, e solo i punti dall'indice 1 in poi sono raggruppati a tre come punto di controllo, punto di controllo, punto finale. Un record con 7 punti disegna quindi due segmenti cubici: 0 è la partenza, da 1 a 3 forma il primo segmento, da 4 a 6 il secondo. Il gestore a 16 bit di PDFlibPas lo faceva già. Quello a 32 bit iniziava a raggruppare dal punto 0, così il punto di partenza veniva consumato come primo punto di controllo e ogni segmento successivo slittava di uno. La curva si rendeva comunque, solo quella sbagliata. Dalla v3.539.41 entrambe le larghezze aprono il path con m al punto 0 ed emettono una c per ogni terna completa dopo di esso
Per il parser che scrivi tu: un conteggio che non è 1 più un multiplo di 3 è malformato, e i punti in coda vanno ignorati invece di essere cuciti in una curva
PolyDraw: PT_CLOSEFIGURE è un flag, non un tipo di punto
In EMR_POLYDRAW, PT_CLOSEFIGURE (valore 1) è un bit combinato con PT_LINETO (2) o PT_BEZIERTO (4), quindi un byte di tipo valido può essere 3 o 5. Il tipo del punto è il byte con quel bit mascherato via, e il flag significa chiudi la figura dopo il segmento che termina su questo punto. Il vecchio gestore di PDFlibPas confrontava il byte con valori singoli in uno case, così i punti di tipo 3 e 5 non combaciavano con nulla e venivano saltati del tutto. Un rettangolo disegnato con PolyDraw perdeva il lato di chiusura, e una terna Bezier il cui ultimo punto portava il flag perdeva quel punto, il che disallineava tutte le terne successive
Dalla v3.539.39 il tipo si legge come Types[i] and not PT_CLOSEFIGURE, e la chiusura viene emessa solo dopo un segmento completo: dopo la linea per una PT_LINETO chiusa, e dopo il terzo punto di un gruppo Bezier. Un file malformato che imposta il flag sul primo o secondo punto di una terna non chiude la figura in anticipo. Nella stessa release sono arrivate due correzioni collegate:
- Ogni
PT_MOVETOnelEMR_POLYDRAW16a 16 bit riavviava l'intero path, così un record con tre figure conservava solo l'ultima; ora la prima move avvia il path e le move successive aprono subpath - Un record PolyDraw che non inizia con
PT_MOVETOparte dalla posizione corrente, come dice la definizione del record, invece di scrivere un operatorelocsenza unamche lo preceda
Perché una polyline EMF non va mai riempita in PDF?
Una polyline EMF non va mai riempita perché EMR_POLYLINE e EMR_POLYPOLYLINE sono figure aperte disegnate con la sola penna, e riempire un path aperto in PDF lo chiude implicitamente. ISO 32000-1 §8.5.3 stabilisce che gli operatori di fill chiudono qualunque subpath aperto prima di dipingerlo. Un convertitore che emette B o f per una polyline di tre punti dipinge quindi un triangolo pieno nel colore del pennello corrente: lo spicchio pieno sotto la linea di tendenza di un grafico. Prima della v3.539.41 PDFlibPas riempiva entrambe le larghezze di polyline con il pennello, e il record a 32 bit veniva anche chiuso esplicitamente. Oggi entrambe le larghezze terminano con il solo stroke, e la distinzione GDI resta: Polygon chiude e riempie, Polyline mai
PolylineTo parte dalla posizione corrente
EMR_POLYLINETO disegna dalla posizione corrente attraverso ogni punto del record, resta aperto e lascia la posizione corrente sull'ultimo punto. Il vecchio gestore conteneva anche un caso speciale che spegneva la penna quando i primi due punti condividevano la y, e nulla la riaccendeva più, così ogni record successivo nel file perdeva il proprio contorno. Lo stato della penna appartiene a EMR_SELECTOBJECT e EMR_CREATEPEN; un gestore di record di disegno non deve metterci mano. Quel caso speciale è stato rimosso nella v3.539.41, e la forma a un punto del record non legge più oltre i propri punti (corretto nella v3.539.39)
I punti di PolyPolyline iniziano dopo l'array dei conteggi
Il EMR_POLYPOLYLINE a 32 bit memorizza nPolys conteggi e poi cptl punti, e i punti iniziano all'offset di byte 32 + nPolys * 4. La trappola sta nella RTL: l'unità Windows dichiara TEMRPolyPolyline con aPolyCounts e aptl come array a un elemento, quindi aptl[0] è il primo punto solo quando nPolys vale 1. Il codice che indicizza aptl direttamente legge i valori dei conteggi come coordinate per ogni record multilinea. Il vecchio gestore di PDFlibPas calibrava anche il proprio controllo sui limiti su quel layout sbagliato, così i record multilinea validi venivano rifiutati e quelli a linea singola non disegnavano nulla. Dalla v3.539.41 PDFlibPas individua l'array dei punti dall'offset calcolato, come ha sempre fatto il suo gestore PolyPolygon, e disegna ogni polyline come proprio subpath aperto con un solo stroke alla fine. Nella v3.539.43 il gemello a 16 bit ha avuto lo stesso trattamento; disegnava segmento per segmento, il che rompeva le giunzioni di linea e ignorava un NULL_PEN selezionato
La penna e il pennello di default, e le parentesi di path
Due regole di stato completano le correzioni alle polyline nella v3.539.43:
- Un device context GDI appena creato ha già
BLACK_PENeWHITE_BRUSHselezionati, così un metafile che disegna senza alcunEMR_SELECTOBJECTproduce comunque contorni neri; il convertitore partiva senza penna né fill e scrivevan(chiudi il path, non dipingere niente) per tali record - Dentro una parentesi
BeginPath/EndPath, unaPolylinenon usa né aggiorna la posizione corrente, quindi deve aprire un nuovo subpath al proprio primo punto invece di collegarsi alla figura precedente, e nulla può essere dipinto finché la parentesi non è tracciata o riempita
Costruire un file EMF di prova con TMetafileCanvas
Il modo più rapido per verificare un convertitore contro queste regole è registrare le tre chiamate a rischio in un unico enhanced metafile con TMetafileCanvas. Il disegno qui sotto registra le curve con un pennello vuoto e poi seleziona di proposito un pennello giallo pieno per la polyline: un convertitore corretto deve ignorare quel pennello per la polyline, quindi qualsiasi giallo nel PDF di uscita è un bug. PolyDraw non ha un wrapper TCanvas, quindi viene chiamata attraverso la Windows API con l'handle del canvas, usando i byte di tipo 3 e 5 per esercitare il flag di chiusura
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Un quadrato chiuso (3 = LINETO + CLOSEFIGURE), poi una figura Bezier
// chiusa la cui ultima terna di controllo termina con 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; // solo contorni per le curve
// Il punto 0 è la partenza; 1..3 e 4..6 sono due segmenti cubici
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));
// Forma a V aperta con pennello pieno selezionato: solo stroke, mai
// chiusa in un triangolo giallo
Canvas.Brush.Style := bsSolid;
Canvas.Brush.Color := clYellow;
Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
finally
Canvas.Free; // chiude la registrazione
end;
Mf.SaveToFile(FileName);
finally
Mf.Free;
end;
end;
Poiché queste coordinate ci stanno in uno SmallInt, GDI di norma memorizza le varianti a 16 bit. Per raggiungere i gestori a 32 bit serve un produttore che li scriva, oppure record costruiti a mano. I file costruiti a mano portano la loro trappola: TMetafile.LoadFromStream della VCL tratta lo stream come EMF solo quando la lunghezza rimanente è strettamente maggiore dei 108 byte del TEnhMetaHeader. Un EMF minimale scritto a mano con un header corto, o uno vuoto lungo esattamente 108 byte, viene preso per un WMF e rifiutato con "Metafile is not valid". Scrivi sempre l'header completo di 108 byte, campi di estensione inclusi, prima dei tuoi record di prova
Importare l'EMF in un PDF con PDFlibPas
PDFlibPas importa un EMF con ImportEMFFromFile o ImportEMFFromStream, che restituiscono un ID immagine diverso da zero in caso di successo e 0 in caso di fallimento. GeneralOptions = 0 mantiene l'import vettoriale di cui parla questo articolo; 1 rasterizza invece il metafile in una bitmap. FontOptions = 1 aggiunge i font del metafile come font TrueType non incorporati. La variante via stream riavvolge lo stream alla posizione 0 prima del caricamento, quindi passagli uno stream che contiene solo il metafile
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 in alto a sinistra per DrawImage
PDF.SetMeasurementUnits(0); // punti
// FontOptions 1 = aggiunge i font come TrueType non incorporati
// GeneralOptions 0 = import vettoriale, 1 = bitmap
ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
if ImageID = 0 then
raise Exception.Create('The metafile could not be imported');
PDF.SelectImage(ImageID);
// Per un EMF, ImageWidth / ImageHeight sono la cornice in punti
PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);
// La pagina invoca solo il form importato: 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 EMF vettoriale diventa un form XObject, quindi GetPageContentToString restituisce solo la sequenza save, transform, Do e restore. Gli operatori m, l, c, h e S prodotti dai record Poly* vivono nello stream del form XObject, che è compresso. Per auditarli, decomprimi il file salvato in un PDF object inspector e leggi lo stream del form: per il file di prova qui sopra dovresti vedere la polyline terminare con S senza alcuna h prima, una h a ogni flag di chiusura nelle figure PolyDraw, e nessuna f o B su questi subpath. DrawImage scala inoltre un EMF importato in modo uniforme secondo la minore tra Width e Height, così il disegno mantiene le proporzioni anche se la box che gli passi non corrisponde
Per i target Free Pascal, vedi come l'importer vettoriale EMF di PDFlibPas compila sotto Free Pascal; la semantica dei record è la stessa ovunque compili l'importer
Come deve trattare un parser EMF i conteggi di punti dal file?
Un parser EMF dovrebbe trattare ogni conteggio di punti come input non fidato e controllarlo contro la dimensione del record prima di copiare un solo punto. EnumEnhMetaFile garantisce solo che ogni nSize di record stia dentro il file. Non verifica che cptl torni con nSize, quindi un gestore che copia cptl punti con Move leggerà i record successivi, o oltre la fine del metafile, quando il conteggio è contraffatto o corrotto. Dalla v3.539.39 PDFlibPas controlla header fisso più conteggio per byte per punto contro nSize per PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo e Polygon in entrambe le larghezze, con un byte extra per punto per i byte di tipo di PolyDraw. Per i record PolyPoly i conteggi per figura devono inoltre sommare a non più del totale dichiarato, e le figure a zero punti vengono saltate
Lo stesso controllo è abbastanza corto da copiare nel tuo parser. Questa versione valida un EMR_POLYPOLYLINE a 32 bit e restituisce un puntatore al suo vero array di punti:
uses
Winapi.Windows;
// Restituisce nil a meno che il record contenga davvero i punti dichiarati.
// I punti partono dopo l'array dei conteggi: a 32 + nPolys * 4 byte, non su
// aptl[0], che la RTL dichiara come array a un elemento
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; // conteggio contraffatto o troncato
Total := 0;
Count := @P^.aPolyCounts[0]; // cammina per puntatore: [0..0] fa scattare i range check
for I := 1 to P^.nPolys do
begin
Inc(Total, Count^);
Inc(Count);
end;
if Total > P^.cptl then
Exit; // le figure rivendicano più punti di quanti esistano
Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;
Il test di accomodamento dei punti gira per primo, così l'array dei conteggi è noto stare dentro il record prima che il ciclo lo percorra. L'aritmetica è Int64 perché nPolys * 4 e cptl * 8 calcolati su 32 bit possono fare wrap around e passare il confronto
Riferimento rapido: regole Poly* EMF per la conversione EMF in PDF
EMR_POLYBEZIER: il punto 0 è il punto di partenza; raggruppa dal punto 1 a terne; corretto per il record a 32 bit nella v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: figure aperte, stroke conS, maih,foB, perché il fill del PDF chiude i subpath apertiEMR_POLYLINETO: parte dalla posizione corrente, resta aperto, aggiorna la posizione corrente, non tocca mai lo stato della pennaEMR_POLYPOLYLINEa 32 bit: i punti iniziano al byte32 + nPolys * 4, non suaptl[0]EMR_POLYDRAW: maschera viaPT_CLOSEFIGUREprima del dispatch, chiudi dopo il segmento completato, parti dalla posizione corrente quando il primo punto non èPT_MOVETO- Lo stato di default del device context è
BLACK_PENpiùWHITE_BRUSH; la v3.539.43 e successive lo rispettano - Dentro
BeginPath/EndPath, ogni polyline apre il proprio subpath e nulla viene dipinto finché la parentesi non viene usata - Valida ogni
cptl/cptscontronSizein aritmetica a 64 bit prima di copiare i punti - Gli EMF di prova costruiti a mano richiedono l'header completo di 108 byte, altrimenti
TMetafile.LoadFromStreamli legge come WMF
Se i tuoi report passano da un componente diverso, la stessa semantica dei record si applica; l'import vettoriale EMF e WMF di HotPDF copre come quel componente trasforma i pennelli a gradiente e a tratteggio in pattern PDF, e grafica vettoriale, shader e gradienti in PDFlibPas copre il disegno delle stesse forme direttamente con l'API della libreria invece che attraverso un metafile
PDFlibPas v3.539.43 o successiva include ogni regola qui sopra. Dettagli e download di prova sono sulla pagina prodotto della PDFlibPas Delphi PDF library