Technisch artikel

PDFlibPas EMF-import: PolyDraw-, Polyline- en Bezier-regels

PDFlibPas, de losLab PDF-library voor Delphi, zet de EMF-Poly*-records om naar PDF-paden door elke recorddefinitie in [MS-EMF] te volgen: een 32-bit EMR_POLYBEZIER begint bij punt 0, polylines blijven open en krijgen uitsluitend een stroke, PT_CLOSEFIGURE in EMR_POLYDRAW is een vlag, en elk puntaantal wordt tegen de recordgrootte gecontroleerd. Die regels zijn verschenen in v3.539.39, v3.539.41 en v3.539.43. Daarvoor kon een grafiek in een rapport uit ImportEMFFromFile rollen met een gevulde wig waar een trendlijn hoorde, een Bezier-curve die naar het verkeerde control point boog, of een gesloten omtrek waarvan de laatste zijde ontbrak. Geen van die dingen gaf een foutmelding, en de regels gelden voor elke Delphi EMF-naar-PDF-converter en elke GDI-recordparser

Waarom gaan EMF-Poly*-records mis bij PDF-conversie?

EMF-Poly*-records gaan mis omdat elk record een deel van zijn betekenis buiten zijn punten vervoert: of de figuur open is, of hij bij de huidige positie begint, welke pen en brush gelden, en waar in het record de punten beginnen. Een enhanced metafile is een opname van GDI-aanroepen tegen een device context, dus een converter moet die device-context-status net zo goed meespelen als de coördinaten. PDF heeft geen device context. Het heeft een path, een current point daarin en een tekenoperator die kiest tussen stroke (S), fill (f) en beide (B). Elke mismatch tussen de twee modellen wordt een stil weergaveverschil

De Poly*-familie bestaat bovendien in twee breedtes. Elk 32-bit record zoals EMR_POLYLINE heeft een 16-bit tweeling zoals EMR_POLYLINE16 die punten opslaat als SmallInt-paren. GDI schrijft meestal de compacte vorm weg zodra elke coördinaat past, dus de 32-bit handlers van een converter kunnen jarenlang fout blijven terwijl alledaagse testtekeningen ze nooit bereiken. De snelste controle is dezelfde punten door beide records heen te halen en de resulterende paden te vergelijken. De records hier behandeld zitten allemaal in de groep van tekenrecords van [MS-EMF] (2.3.5 Drawing Record Types)

RecordBegint bijGesloten?Huidige positie
EMR_POLYBEZIERPunt 0NeeNiet gebruikt, niet bijgewerkt
EMR_POLYLINEPunt 0Nee (alleen pen)Niet gebruikt, niet bijgewerkt
EMR_POLYLINETOHuidige positieNee (alleen pen)Gebruikt en bijgewerkt
EMR_POLYPOLYLINEEerste punt van elke polylineNee (alleen pen)Niet gebruikt, niet bijgewerkt
EMR_POLYDRAWEerste PT_MOVETO, of huidige positieAlleen waar PT_CLOSEFIGURE is gezetGebruikt en bijgewerkt

Waar begint een EMR_POLYBEZIER-curve eigenlijk?

Een EMR_POLYBEZIER-curve begint bij punt 0, en alleen de punten vanaf index 1 worden in drietallen gegroepeerd als control point, control point, eindpunt. Een record met 7 punten tekent dus twee cubic segmenten: 0 is de start, 1 tot 3 vormen het eerste segment, 4 tot 6 het tweede. De 16-bit handler in PDFlibPas deed dat al. De 32-bit handler begon pas te groeperen bij punt 0, zodat het startpunt als eerste control point werd opgebruikt en elk later segment één positie opschoof. De curve werd nog steeds gerenderd, alleen de verkeerde. Sinds v3.539.41 openen beide breedtes het path met m op punt 0 en geven ze één c per volledig drietal daarna uit

PDFlibPas-diagram van een EMR_POLYBEZIER-record met zeven punten waarin punt nul het path opent met m en de punten één tot drie en vier tot zes elk één cubic c-segment vormen, met de opgeloste 32-bit handler sinds v3.539.41 naast de oude groepering die het startpunt als control point opgebruikte
Punt 0 is het startpunt en alleen volledige drietallen erna worden cubic segmenten, dus een PolyBezier met zeven punten rendert als m plus twee c-operators

Voor uw eigen parser: een aantal dat niet gelijk is aan 1 plus een veelvoud van 3 is misvormd, en de punten die overblijven hoort u te negeren in plaats van ze tot een curve aan elkaar te plakken

PolyDraw: PT_CLOSEFIGURE is een vlag, geen punttype

In EMR_POLYDRAW is PT_CLOSEFIGURE (waarde 1) een bit dat wordt gecombineerd met PT_LINETO (2) of PT_BEZIERTO (4), dus een geldig typebyte kan 3 of 5 zijn. Het punttype is de byte met dat bit uitgemaskeerd, en de vlag betekent: sluit de figuur af na het segment dat op dit punt eindigt. De oude PDFlibPas-handler vergeleek de byte met losse waarden in een case-statement, dus punten van type 3 en 5 matchten nergens en werden volledig overgeslagen. Een rechthoek die met PolyDraw werd getekend verloor zijn sluitzijde, en een Bezier-drietal waarvan het laatste punt de vlag droeg verloor dat punt, wat elk later drietal uit de pas liet lopen

Sinds v3.539.39 wordt het type gelezen als Types[i] and not PT_CLOSEFIGURE, en de sluiting wordt pas na een volledig segment gegeven: na de lijn bij een gesloten PT_LINETO, en na het derde punt van een Bezier-groep. Een misvormd bestand dat de vlag op het eerste of tweede punt van een drietal zet, sluit de figuur niet voortijdig. Twee verwante fixes zijn in dezelfde release verschenen:

  • Elke PT_MOVETO in de 16-bit EMR_POLYDRAW16 herstartte het hele path, dus een record met drie figuren hield alleen de laatste over; nu start de eerste move het path en openen latere moves subpaths
  • Een PolyDraw-record dat niet met PT_MOVETO begint, start op de huidige positie, zoals de recorddefinitie zegt, in plaats van een l- of c-operator te schrijven zonder voorafgaande m
PDFlibPas-anatomie van een EMR_POLYDRAW-typebyte waarin PT_CLOSEFIGURE vlagbit nul is dat via OR in PT_LINETO of PT_BEZIERTO terechtkomt, dus geldige typebytes 3 en 5 moeten met and not PT_CLOSEFIGURE gemaskeerd worden vóór de dispatch; het oude case-statement sloeg beide bytes over en gesloten figuren verloren hun laatste zijde
Maskeer de close-vlag af vóór de dispatch en geef de sluiting pas na een voltooide lijn of Bezier-drietal, anders laat PolyDraw stilletjes punten vallen

Waarom mag een EMF-polyline nooit gevuld worden in PDF?

Een EMF-polyline mag nooit gevuld worden, want EMR_POLYLINE en EMR_POLYPOLYLINE zijn open figuren die alleen met de pen worden getekend, en het vullen van een open path in PDF sluit het impliciet. ISO 32000-1 §8.5.3 stelt dat de fill-operators elke open subpath sluiten voordat ze die tekenen. Een converter die B of f geeft voor een polyline van drie punten, tekent dus een gevulde driehoek in de huidige brushkleur: de gevulde wig onder een trendlijn in een grafiek. Vóór v3.539.41 vulde PDFlibPas beide polyline-breedtes met de brush, en het 32-bit record werd bovendien expliciet gesloten. Tegenwoordig eindigen beide breedtes in een stroke en het GDI-onderscheid blijft behouden: Polygon sluit en vult, Polyline doet dat nooit

PDFlibPas-vergelijking van een open V-polyline geëxporteerd uit EMR_POLYLINE: een correcte converter eindigt het path met de stroke-operator S en negeert de geselecteerde brush, terwijl f of B geven de open subpath impliciet sluit onder ISO 32000-1 8.5.3 en de gevulde-wig-grafiekbug tekent
Een fill-operator sluit elke open subpath vóór het tekenen, dus polylines moeten eindigen in S zonder h, f of B op de subpath

PolylineTo begint op de huidige positie

EMR_POLYLINETO tekent vanaf de huidige positie langs elk punt in het record, blijft open en laat de huidige positie op het laatste punt achter. De oude handler bevatte bovendien een special case die de pen uitzette zodra de eerste twee punten een y-coördinaat deelden, en niets zette hem ooit weer aan, dus elk later record in het bestand verloor zijn omtrek. Pen-status hoort bij EMR_SELECTOBJECT en EMR_CREATEPEN; een tekenrecord-handler heeft niets te zoeken bij het veranderen ervan. Die special case is in v3.539.41 verwijderd, en de vorm met één punt leest niet langer voorbij zijn eigen punten (opgelost in v3.539.39)

De punten van PolyPolyline beginnen na de counts-array

Het 32-bit EMR_POLYPOLYLINE slaat nPolys aantallen en daarna cptl punten op, en de punten beginnen op byte-offset 32 + nPolys * 4. De valkuil zit in de RTL: de unit Windows declareert TEMRPolyPolyline met aPolyCounts en aptl als arrays met één element, dus aptl[0] is het eerste punt alleen als nPolys 1 is. Code die aptl direct indexeert, leest bij elk multi-line record aantalwaarden als coördinaten. De oude PDFlibPas-handler baseerde bovendien zijn bounds check op die verkeerde layout, dus geldige multi-line records werden geweigerd en single-line records tekenden niets. Sinds v3.539.41 lokaliseert PDFlibPas de puntarray vanaf de berekende offset, zoals zijn PolyPolygon-handler altijd al deed, en tekent hij elke polyline als eigen open subpath met één stroke op het eind. In v3.539.43 kreeg de 16-bit tweeling dezelfde behandeling; die tekende segment voor segment, wat line joins brak en een geselecteerde NULL_PEN negeerde

De standaard pen en brush, en path-brackets

Twee statusregels maken de polyline-fixes in v3.539.43 compleet:

  • Een verse GDI device context heeft al BLACK_PEN en WHITE_BRUSH geselecteerd, dus een metafile die zonder enige EMR_SELECTOBJECT tekent, tekent toch zwarte omtrekken; de converter begon voorheen zonder pen en zonder fill en schreef n (end path, niets tekenen) voor zulke records
  • Binnen een BeginPath / EndPath-bracket gebruikt een Polyline de huidige positie niet en werkt ze niet bij, dus ze moet op haar eerste punt een nieuwe subpath openen in plaats van door te lopen naar de vorige figuur, en er mag niets worden getekend totdat de bracket is gestroked of gevuld

Een EMF-testbestand bouwen met TMetafileCanvas

De snelste manier om een converter aan deze regels te toetsen is de drie risicovolle aanroepen in één enhanced metafile op te nemen met TMetafileCanvas. De tekening hieronder neemt de curves op met een holle brush en selecteert daarna opzettelijk een effen gele brush voor de polyline: een correcte converter moet die brush voor de polyline negeren, dus elk geel in de uitgaande PDF is een bug. PolyDraw heeft geen TCanvas-wrapper, dus hij wordt via de Windows API met de canvas-handle aangeroepen, met typebytes 3 en 5 om de close-vlag te testen

uses
  Winapi.Windows, System.Types, Vcl.Graphics;

procedure BuildPolyTestEmf(const FileName: string);
const
  // Een gesloten vierkant (3 = LINETO + CLOSEFIGURE), daarna een gesloten
  // Bezier-figuur waarvan het laatste control-drietal eindigt op 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;    // alleen omtrekken voor de curves
      // Punt 0 is de start; 1..3 en 4..6 zijn twee cubic segmenten
      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));
      // Open V-vorm met een effen brush geselecteerd: gestroked, nooit
      // gesloten tot een gele driehoek
      Canvas.Brush.Style := bsSolid;
      Canvas.Brush.Color := clYellow;
      Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
    finally
      Canvas.Free;   // beëindigt de opname
    end;
    Mf.SaveToFile(FileName);
  finally
    Mf.Free;
  end;
end;

Omdat deze coördinaten in een SmallInt passen, slaat GDI normaal de 16-bit varianten op. Om bij de 32-bit handlers te komen heeft u een producent nodig die ze schrijft, of records die u met de hand bouwt. Handgebouwde bestanden brengen hun eigen valkuil mee: VCL TMetafile.LoadFromStream behandelt de stream pas als EMF wanneer de resterende lengte strikt groter is dan de 108 bytes van TEnhMetaHeader. Een minimaal handgeschreven EMF met een korte header, of een leeg exemplaar van precies 108 bytes, wordt voor een WMF aangezien en geweigerd met "Metafile is not valid". Schrijf altijd de volledige header van 108 bytes, inclusief de extensievelden, vóór uw testrecords

De EMF in een PDF importeren met PDFlibPas

PDFlibPas importeert een EMF met ImportEMFFromFile of ImportEMFFromStream, die bij succes een niet-nul image ID teruggeven en bij falen 0. GeneralOptions = 0 houdt het vectorpad vast waar dit artikel over gaat; 1 rasteriseert de metafile in plaats daarvan naar een bitmap. FontOptions = 1 voegt de metafile-fonts toe als niet-ingebedde TrueType-fonts. De streamvariant spoelt de stream eerst terug naar positie 0 vóór het laden, dus geef een stream door die alleen de metafile bevat

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);              // linkerbovenhoek als origine voor DrawImage
    PDF.SetMeasurementUnits(0);    // punten
    // FontOptions 1 = fonts toevoegen als niet-ingebedde TrueType
    // GeneralOptions 0 = vectorimport, 1 = bitmap
    ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
    if ImageID = 0 then
      raise Exception.Create('The metafile could not be imported');
    PDF.SelectImage(ImageID);
    // Bij een EMF zijn ImageWidth / ImageHeight de framemaat in punten
    PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);

    // De pagina roept alleen de geïmporteerde form aan: 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;

Een vector-EMF-import wordt een form XObject, dus GetPageContentToString geeft alleen de save-, transform-, Do- en restore-reeks terug. De operators m, l, c, h en S die uit de Poly*-records voortkomen, zitten in de form-XObject-stream, die gecomprimeerd is. Om ze te controleren, decomprimeert u het opgeslagen bestand in een PDF object inspector en leest de form-stream: in het testbestand hierboven hoort u de polyline te zien eindigen in S zonder h ervoor, een h bij elke close-vlag in de PolyDraw-figuren, en geen f of B op een van deze subpaths. DrawImage schaalt een geïmporteerde EMF ook uniform met het kleinste van Width en Height, dus de tekening behoudt zijn beeldverhouding zelfs als het kader dat u doorgeeft daar niet aan voldoet

Voor Free Pascal-doelen, zie hoe de PDFlibPas EMF-vectorimporteur onder Free Pascal bouwt; de record-semantiek is overal gelijk waar de importeur compileert

Hoe moet een EMF-parser puntaantallen uit het bestand behandelen?

Een EMF-parser moet elk puntaantal als niet-vertrouwde invoer behandelen en het tegen de recordgrootte controleren voordat er ook maar één punt wordt gekopieerd. EnumEnhMetaFile garandeert alleen dat elke record-nSize binnen het bestand blijft. Hij controleert niet of cptl met nSize klopt, dus een handler die cptl punten met Move kopieert, leest de volgende records of voorbij het einde van de metafile zodra het aantal is vervalst of corrupt. Sinds v3.539.39 controleert PDFlibPas vaste header plus aantal keer bytes per punt tegen nSize voor PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo en Polygon in beide breedtes, met één extra byte per punt voor PolyDraw-typebytes. Voor de PolyPoly-records moeten de aantallen per figuur bovendien samen niet meer uitkomen dan het opgegeven totaal, en figuren met nul punten worden overgeslagen

Dezelfde controle is kort genoeg om in uw eigen parser over te nemen. Deze versie valideert een 32-bit EMR_POLYPOLYLINE en geeft een pointer naar zijn echte puntarray terug:

uses
  Winapi.Windows;

// Geeft nil terug tenzij het record echt de punten bevat die het aangeeft.
// De punten beginnen na de counts-array: 32 + nPolys * 4 bytes naar binnen,
// niet op aptl[0], dat de RTL als array met één element declareert
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;                         // vervalst of afgekapt aantal
  Total := 0;
  Count := @P^.aPolyCounts[0];    // met de pointer lopen: [0..0] triggert range checks
  for I := 1 to P^.nPolys do
  begin
    Inc(Total, Count^);
    Inc(Count);
  end;
  if Total > P^.cptl then
    Exit;                         // figuren claimen meer punten dan er zijn
  Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;

De punten-passen-test draait eerst, dus de counts-array ligt bekend binnen het record voordat de lus hem doorloopt. De rekenkunde is Int64 omdat nPolys * 4 en cptl * 8 in 32 bits berekend kunnen omslaan en de vergelijking toch laten passeren

Snelnaslag: EMF-Poly*-regels voor EMF-naar-PDF-conversie

  • EMR_POLYBEZIER: punt 0 is het startpunt; groepeer vanaf punt 1 in drietallen; opgelost voor het 32-bit record in v3.539.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: open figuren, stroke met S, nooit h, f of B, want PDF-fill sluit open subpaths
  • EMR_POLYLINETO: begin op de huidige positie, blijf open, werk de huidige positie bij, raak de pen-status nooit aan
  • 32-bit EMR_POLYPOLYLINE: de punten beginnen op byte 32 + nPolys * 4, niet op aptl[0]
  • EMR_POLYDRAW: maskeer PT_CLOSEFIGURE af vóór de dispatch, sluit pas na het voltooide segment, begin op de huidige positie als het eerste punt geen PT_MOVETO is
  • De default-status van de device context is BLACK_PEN plus WHITE_BRUSH; v3.539.43 en later houden die aan
  • Binnen BeginPath / EndPath opent elke polyline zijn eigen subpath en er wordt niets getekend totdat de bracket wordt gebruikt
  • Valideer elke cptl / cpts tegen nSize in 64-bit rekenkunde voordat u punten kopieert
  • Met de hand gebouwde test-EMF's hebben de volledige header van 108 bytes nodig, anders leest TMetafile.LoadFromStream ze als WMF

Als uw rapporten via een andere component lopen, geldt dezelfde record-semantiek; HotPDF EMF- en WMF-vectorimport beschrijft hoe die component gradient- en hatch-brushes omzet in PDF-patterns, en vectorkunst, shaders en gradients in PDFlibPas beschrijft hoe u dezelfde vormen rechtstreeks met de library-API tekent in plaats van via een metafile

PDFlibPas v3.539.43 of later bevat alle bovenstaande regels. Details en proefdownloads staan op de productpagina van de PDFlibPas Delphi PDF-library