Technický článek

Import EMF v PDFlibPas: PolyDraw, Polyline a Bezier

PDFlibPas, PDF knihovna losLab pro Delphi, převádí záznamy EMF Poly* na cesty PDF tak, že se drží definice každého záznamu v [MS-EMF]: 32bitový EMR_POLYBEZIER začíná v bodě 0, polylinie zůstávají otevřené a kreslí se jen tahem, PT_CLOSEFIGURE v EMR_POLYDRAW je příznak a každý počet bodů se kontroluje proti velikosti záznamu. Tahle pravidla přibyla postupně ve v3.539.39, v3.539.41 a v3.539.43. Před nimi mohl graf v reportu vylézt z ImportEMFFromFile s vyplněným klínem tam, kde měla být trendová linie, s Bezier křivkou zahnutou ke špatnému řídícímu bodu, nebo s uzavřeným obrysem, kterému chyběla poslední strana. Ani jedna z těchto chyb nevyhodila chybovou hlášku a pravidla platí pro jakýkoli převodník EMF do PDF v Delphi i pro parser GDI záznamů

Proč se záznamy EMF Poly* při převodu do PDF pokazí?

Záznamy EMF Poly* se pokazí proto, že část svého významu nesou mimo své body: jestli je útvar otevřený, jestli začíná v aktuální pozici, které pero a štětec se uplatní a kde v záznamu body začínají. Enhanced metafile je záznam volání GDI proti device contextu, takže převodník musí přehrát nejen souřadnice, ale i stav toho device contextu. PDF žádný device context nemá. Má cestu, aktuální bod uvnitř té cesty a kreslicí operátor, který rozhoduje mezi stroke (S), fill (f) a obojím (B). Každá neshoda obou modelů se promění v tichý rozdíl ve vykreslení

Rodina Poly* navíc existuje ve dvou šířkách. Každý 32bitový záznam jako EMR_POLYLINE má 16bitového dvojčete jako EMR_POLYLINE16, které ukládá body jako páry SmallInt. GDI obvykle zaznamená kompaktní formu, když se všechny souřadnice vejdou, takže 32bitové handlery převodníku můžou roky zůstat rozbité, zatímco běžné testovací kresby se k nim nikdy nedostanou. Nejrychlejší audit je pustit stejné body oběma záznamy a porovnat výsledné cesty. Záznamy popsané tady patří všechny do skupiny kreslicích záznamů [MS-EMF] (2.3.5 Drawing Record Types)

ZáznamZačíná vUzavřený?Aktuální pozice
EMR_POLYBEZIERBod 0NeNepoužívá se, neaktualizuje se
EMR_POLYLINEBod 0Ne (jen pero)Nepoužívá se, neaktualizuje se
EMR_POLYLINETOAktuální poziceNe (jen pero)Používá se a aktualizuje se
EMR_POLYPOLYLINEPrvní bod každé polylinieNe (jen pero)Nepoužívá se, neaktualizuje se
EMR_POLYDRAWPrvní PT_MOVETO, nebo aktuální poziceJen kde je nastaven PT_CLOSEFIGUREPoužívá se a aktualizuje se

Kde vlastně začíná křivka EMR_POLYBEZIER?

Křivka EMR_POLYBEZIER začíná v bodě 0 a jen body od indexu 1 výš se sdružují po trojicích řídící bod, řídící bod, koncový bod. Záznam se 7 body nakreslí dva kubické segmenty: 0 je start, 1 až 3 tvoří první segment, 4 až 6 druhý. 16bitový handler v PDFlibPas to už dělal správně. 32bitový handler začal sdružovat od bodu 0, takže startovní bod se spolkl jako první řídící bod a každý další segment se posunul o jedničku. Křivka se vykreslila, jen úplně jiná. Od v3.539.41 obě šířky otevírají cestu m v bodě 0 a pro každou úplnou trojici za ním vypustí jedno c

Diagram PDFlibPas záznamu EMR_POLYBEZIER se sedmi body, kde bod nula otevírá cestu operátorem m a body jedna až tři a čtyři až šest tvoří každý jeden kubický segment c, v kontrastu s opraveným 32bitovým handlerem od v3.539.41 a starým sdružováním, které spolkl startovní bod jako řídící bod
Bod 0 je startovní bod a jen úplné trojice za ním se stanou kubickými segmenty, takže sedmibodový PolyBezier se vykreslí jako operátor m plus dvakrát c

Pro vlastní parser: počet, který není 1 plus násobek 3, je poškozený a přebytečné body na konci je potřeba ignorovat, ne došívat do křivky

PolyDraw: PT_CLOSEFIGURE je příznak, ne typ bodu

V EMR_POLYDRAW je PT_CLOSEFIGURE (hodnota 1) bit kombinovaný s PT_LINETO (2) nebo PT_BEZIERTO (4), takže platný typový bajt může být 3 nebo 5. Typ bodu je bajt s tímto bitem zamaskovaným pryč a příznak znamená uzavřít útvar po segmentu, který na tomto bodu končí. Starý handler v PDFlibPas porovnával bajt s jednotlivými hodnotami v case větvi, takže body typu 3 a 5 na nic nenarazily a celé se přeskočily. Obdélník kreslený přes PolyDraw ztratil zavírací stranu a Bezier trojice, jejíž poslední bod nesl příznak, ztratila ten bod, což rozchodilo všechny další trojice

Od v3.539.39 se typ čte jako Types[i] and not PT_CLOSEFIGURE a uzavření se vypustí až po celém segmentu: za přímkou u uzavřeného PT_LINETO a za třetím bodem Bezier skupiny. Poškozený soubor, který nastaví příznak na prvním nebo druhém bodu trojice, útvar zavře předčasně ne. Ve stejném vydání vyjely dvě související opravy:

  • Každý PT_MOVETO v 16bitovém EMR_POLYDRAW16 restartoval celou cestu, takže záznam se třemi útvary ponechal jen ten poslední; teď první pohyb cestu zahájí a další pohyby otevírají podcesty
  • Záznam PolyDraw, který nezačíná PT_MOVETO, začíná v aktuální pozici, jak říká definice záznamu, místo aby zapsal operátor l nebo c bez předchozího m
Anatomie typového bajtu EMR_POLYDRAW v PDFlibPas, kde je PT_CLOSEFIGURE nultý bit příznaku sloučený přes OR do PT_LINETO nebo PT_BEZIERTO, takže platné typové bajty 3 a 5 se musí před rozesláním zamaskovat přes and not PT_CLOSEFIGURE; starý case příkaz oba bajty přeskočil a uzavřené útvary ztratily poslední stranu
Zamaskujte zavírací příznak před rozesláním a vypusťte uzavření až po dokončené přímce nebo Bezier trojici, jinak PolyDraw tiše upustí body

Proč se polylinie z EMF nikdy nesmí v PDF vyplňovat?

Polylinie z EMF se nikdy nesmí vyplnit, protože EMR_POLYLINE a EMR_POLYPOLYLINE jsou otevřené útvary kreslené jen perem a vyplnění otevřené cesty v PDF ji implicitně zavře. ISO 32000-1 §8.5.3 říká, že výplňové operátory zavřou jakýkoli otevřený subpath před jeho vykreslením. Převodník, který pro tříbodovou polylinii vypustí B nebo f, tedy vykreslí vyplněný trojúhelník aktuální barvou štětce: vyplněný klín pod trendovou linií grafu. Před v3.539.41 plnil PDFlibPas obě šířky polylinií štětcem a 32bitový záznam byl navíc explicitně uzavřený. Dnes obě šířky končí jen tahem a GDI rozdíl zůstává zachován: Polygon zavře a vyplní, Polyline nikdy

Srovnání otevřené V polylinie exportované z EMR_POLYLINE v PDFlibPas: správný převodník ukončí cestu operátorem stroke S a vybraný štětec ignoruje, zatímco vypuštění f nebo B implicitně zavře otevřený subpath podle ISO 32000-1 8.5.3 a vykreslí bug s vyplněným klínem v grafu
Výplňový operátor zavře jakýkoli otevřený subpath před vykreslením, takže polylinie musí končit S bez h, f nebo B na subpath

PolylineTo začíná v aktuální pozici

EMR_POLYLINETO kreslí od aktuální pozice přes každý bod v záznamu, zůstává otevřený a aktuální pozici nechá na posledním bodu. Starý handler navíc obsahoval zvláštní případ, který vypnul pero, když první dva body sdílely y souřadnici, a nic ho nikdy nezapnulo zpátky, takže každý další záznam v souboru ztratil obrys. Stav pera patří k EMR_SELECTOBJECT a EMR_CREATEPEN; handler kreslicího záznamu do něj nemá co mluvit. Ten zvláštní případ zmizel ve v3.539.41 a jedno-bodová forma záznamu už nečte za vlastními body (opraveno ve v3.539.39)

Body PolyPolyline začínají za polem počtů

32bitový EMR_POLYPOLYLINE ukládá nPolys počtů a pak cptl bodů, přičemž body začínají na bajtovém offsetu 32 + nPolys * 4. Past je v RTL: unit Windows deklaruje TEMRPolyPolyline s aPolyCounts a aptl jako jednoprvková pole, takže aptl[0] je první bod jen tehdy, když nPolys je 1. Kód, který indexuje aptl přímo, čte hodnoty počtů jako souřadnice u každého víceřádkového záznamu. Starý handler PDFlibPas měl navíc kontrolu mezí dimenzovanou na tohle špatné rozložení, takže platné víceřádkové záznamy byly odmítnuty a jednořádkové nenakreslily nic. Od v3.539.41 hledá PDFlibPas pole bodů z vypočteného offsetu, tak jako jeho handler PolyPolygon vždycky, a kreslí každou polylinii jako vlastní otevřený subpath s jedním tahem na konci. Ve v3.539.43 dostalo 16bitové dvojče stejnou péči; kreslilo segment po segmentu, což rozbilo spoje linek a ignorovalo vybraný NULL_PEN

Výchozí pero a štětec a závorky cesty

Opravy polylinií ve v3.539.43 doplňují dvě pravidla o stavu:

  • Čerstvý GDI device context už má vybrané BLACK_PEN a WHITE_BRUSH, takže metafile, který kreslí bez jediného EMR_SELECTOBJECT, stejně kreslí černé obrysy; převodník dřív startoval bez pera a bez výplně a pro takové záznamy psal n (konec cesty, nic se nevykreslí)
  • Uvnitř závorky BeginPath / EndPath ani Polyline nepoužívá, ani neaktualizuje aktuální pozici, takže musí otevřít nový subpath ve svém prvním bodě místo napojení na předchozí útvar, a dokud se závorka neobtáhne nebo nevyplní, nesmí se nic vykreslit

Vytvoření testovacího EMF souboru přes TMetafileCanvas

Nejrychlejší cesta, jak převodník proti těmto pravidlům vyzkoušet, je zaznamenat tři riziková volání do jednoho enhanced metafile přes TMetafileCanvas. Kresba níže zaznamená křivky s dutým štětcem a pak záměrně vybere plný žlutý štětec pro polylinii: správný převodník musí ten štětec u polylinie ignorovat, takže jakákoli žlutá ve výstupním PDF je bug. PolyDraw nemá wrapper na TCanvas, takže se volá přes Windows API s handle canvasu, s typovými bajty 3 a 5, aby se zavírací příznak procvičil

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

procedure BuildPolyTestEmf(const FileName: string);
const
  // Uzavřený čtverec (3 = LINETO + CLOSEFIGURE), pak uzavřený Bezier
  // útvar, jehož poslední řídicí trojice končí 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;    // pro křivky jen obrysy
      // Bod 0 je start; 1..3 a 4..6 jsou dva kubické segmenty
      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));
      // Otevřený tvar V s vybraným plným štětcem: jen tah, nikdy se
      // neuzavře do žlutého trojúhelníku
      Canvas.Brush.Style := bsSolid;
      Canvas.Brush.Color := clYellow;
      Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
    finally
      Canvas.Free;   // ukončí záznam
    end;
    Mf.SaveToFile(FileName);
  finally
    Mf.Free;
  end;
end;

Protože se tyhle souřadnice vejdou do SmallIntu, GDI je bude normálně ukládat v 16bitových variantách. K 32bitovým handlerům se dostanete jen s producentem, který je umí zapsat, nebo se záznamy postavenými ručně. Ručně poskládané soubory přinášejí vlastní past: VCL TMetafile.LoadFromStream bere stream jako EMF jen tehdy, když zbývající délka je přísně větší než 108bajtový TEnhMetaHeader. Minimální ručně psaný EMF s krátkou hlavičkou, nebo prázdný soubor dlouhý přesně 108 bajtů, se bere za WMF a je odmítnut hlášením „Metafile is not valid“. Vždy zapište plnou 108bajtovou hlavičku včetně extenzních polí, než přijdou vaše testovací záznamy

Import EMF do PDF přes PDFlibPas

PDFlibPas importuje EMF přes ImportEMFFromFile nebo ImportEMFFromStream, které vrací nenulové image ID při úspěchu a 0 při selhání. GeneralOptions = 0 ponechá vektorovou cestu, o které tenhle článek je; 1 místo toho rasterizuje metafile do bitmapy. FontOptions = 1 přidá písma metafile jako neembeddovaná TrueType písma. Streamová varianta přetočí stream na pozici 0 před načtením, takže pošlete stream, který obsahuje jen 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);              // počátek vlevo nahoře pro DrawImage
    PDF.SetMeasurementUnits(0);    // body (points)
    // FontOptions 1 = přidá písma jako neembeddovaná TrueType
    // GeneralOptions 0 = vektorový import, 1 = bitmapa
    ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
    if ImageID = 0 then
      raise Exception.Create('The metafile could not be imported');
    PDF.SelectImage(ImageID);
    // Pro EMF jsou ImageWidth / ImageHeight rozměry rámu v bodech
    PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);

    // Stránka jen vyvolá importovaný formulář: 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;

Vektorový import EMF se stane form XObjectem, takže GetPageContentToString vrátí jen sekvenci uložení, transformace, Do a obnovení. Operátory m, l, c, h a S vyrobené ze záznamů Poly* bydlí ve streamu form XObjectu, který je komprimovaný. Pro audit je dekomprimujte v PDF object inspektoru a přečtěte form stream: u testovacího souboru výš byste měli vidět polylinii končící S bez žádného h před ní, h u každého zavíracího příznaku v útvarech PolyDraw a žádné f ani B na žádném z těchto subpathů. DrawImage zároveň škáluje importovaný EMF uniformně podle menšího z Width a Height, takže kresba drží poměr stran, i když rámeček, který pošlete, neodpovídá

Pro targety Free Pascalu se podívejte na jak se vektorový EMF importer PDFlibPas sestavuje pod Free Pascalem; sémantika záznamů je stejná všude, kde se importer zkompiluje

Jak má parser EMF zacházet s počty bodů ze souboru?

Parser EMF by měl brát každý počet bodů jako nedůvěryhodný vstup a zkontrolovat ho proti velikosti záznamu, než zkopíruje jediný bod. EnumEnhMetaFile garantuje jen to, že nSize každého záznamu zůstane uvnitř souboru. Nekontroluje, že cptl souhlasí s nSize, takže handler, který kopíruje cptl bodů přes Move, si přečte následující záznamy, nebo zajde za konec metafile, když je počet zfalšovaný nebo poškozený. Od v3.539.39 kontroluje PDFlibPas pevnou hlavičku plus počet krát bajty na bod proti nSize u PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo a Polygon v obou šířkách, s jedním extra bajtem na bod u typových bajtů PolyDraw. U záznamů PolyPoly se navíc počty na útvar musí sečíst na nejvýše deklarovaný celkový součet a útvary s nulou bodů se přeskočí

Ta samá kontrola je krátká na to, abyste si ji zkopírovali do vlastního parseru. Tahle verze validuje 32bitový EMR_POLYPOLYLINE a vrací ukazatel na jeho skutečné pole bodů:

uses
  Winapi.Windows;

// Vrací nil, dokud záznam doopravdy neobsahuje body, které deklaruje.
// Body začínají za polem počtů: 32 + nPolys * 4 bajtů od začátku, ne na
// aptl[0], které RTL deklaruje jako jednoprvkové pole
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;                         // zfalšovaný nebo useknutý počet
  Total := 0;
  Count := @P^.aPolyCounts[0];    // chůze přes ukazatel: [0..0] spustí range check
  for I := 1 to P^.nPolys do
  begin
    Inc(Total, Count^);
    Inc(Count);
  end;
  if Total > P^.cptl then
    Exit;                         // útvary si nárokují víc bodů, než existuje
  Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;

Test, zda se body vejdou, běží první, takže pole počtů je před smyčkou známé jako uvnitř záznamu. Aritmetika je v Int64, protože nPolys * 4 a cptl * 8 spočítané v 32 bitech se můžou přetáhnout přes mez a projít porovnáním

Přehled: pravidla EMF Poly* pro převod EMF do PDF

  • EMR_POLYBEZIER: bod 0 je startovní bod; sdružujte od bodu 1 po trojicích; opraveno u 32bitového záznamu ve v3.539.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: otevřené útvary, tah s S, nikdy h, f ani B, protože PDF výplň zavírá otevřené subpathy
  • EMR_POLYLINETO: začíná v aktuální pozici, zůstává otevřený, aktualizuje aktuální pozici, nikdy nesahá na stav pera
  • 32bitový EMR_POLYPOLYLINE: body začínají na bajtu 32 + nPolys * 4, ne na aptl[0]
  • EMR_POLYDRAW: zamaskujte PT_CLOSEFIGURE před rozesláním, zavřete po dokončeném segmentu, začněte v aktuální pozici, když první bod není PT_MOVETO
  • Výchozí stav device contextu je BLACK_PEN plus WHITE_BRUSH; v3.539.43 a novější ho respektují
  • Uvnitř BeginPath / EndPath si každá polylinie otevírá vlastní subpath a dokud se závorka nepoužije, nic se nevykreslí
  • Validujte každý cptl / cpts proti nSize v 64bitové aritmetice, než zkopírujete body
  • Ručně stavěné testovací EMF potřebují plnou 108bajtovou hlavičku, jinak je TMetafile.LoadFromStream přečte jako WMF

Pokud vaše reporty jdou přes jinou komponentu, sémantika záznamů je stejná; vektorový import EMF a WMF v HotPDF popisuje, jak ta komponenta mění gradientní a hatch štětce na PDF patterny, a vektorová grafika, shadery a gradienty v PDFlibPas popisuje kreslení stejných tvarů přímo přes API knihovny místo přes metafile

PDFlibPas ve verzi v3.539.43 a novější obsahuje všechna výše uvedená pravidla. Detaily a trial ke stažení najdete na produktové stránce PDFlibPas Delphi PDF library