Техническа статия

PDFlibPas импорт на EMF: PolyDraw, Polyline и Bezier

PDFlibPas, PDF библиотеката на losLab за Delphi, превръща EMF Poly* записите в PDF пътища, спазвайки дефиницията на всеки запис в [MS-EMF]: 32-битовият EMR_POLYBEZIER тръгва от точка 0, полилиниите остават отворени и само се ощриховат, PT_CLOSEFIGURE в EMR_POLYDRAW е флаг, а всеки брой точки се проверява спрямо размера на записа. Тези правила влязоха през v3.539.39, v3.539.41 и v3.539.43. Преди тях диаграма от отчет можеше да излезе от ImportEMFFromFile с запълнен клин там, където трябва да е линия на тренда, с Bezier крива, извита към грешна контролна точка, или със затворен контур, на който липсва последната страна. Нито едно от тези не вдигаше грешка, а правилата важат за всеки Delphi EMF към PDF конвертор или парсер на GDI записи

Защо EMF Poly* записите се чупят при конверсия в PDF?

EMF Poly* записите се чупят, защото всеки от тях носи част от значението си извън точките си: дали фигурата е отворена, дали тръгва от текущата позиция, кои писалка и четка важат и откъде в записа започват точките. Enhanced metafile е запис на GDI извиквания към device context, затова конверторът трябва да възпроизведе не само координатите, но и състоянието на този device context. PDF няма device context. В него има път, текуща точка в този път и оператор за рисуване, който решава между щрих (S), запълване (f) и двете (B). Всяко разминаване между двата модела става тиха разлика в рендирането

Семейството Poly* идва и в две ширини. Всеки 32-битов запис като EMR_POLYLINE има 16-битов близнак като EMR_POLYLINE16, който пази точките като двойки SmallInt. GDI обикновено записва компактния вариант, когато всяка координата се събира, така че 32-битовите обработчици на един конвертор може с години да са грешни, докато обикновените тестови чертежи изобщо не стигат до тях. Най-бързата проверка е да пуснете същите точки и през двата записа и да сравните получените пътища. Всички разгледани тук записи са в групата на рисуващите записи на [MS-EMF] (2.3.5 Drawing Record Types)

ЗаписЗапочва отЗатворен?Текуща позиция
EMR_POLYBEZIERТочка 0НеНе се ползва, не се обновява
EMR_POLYLINEТочка 0Не (само писалка)Не се ползва, не се обновява
EMR_POLYLINETOТекуща позицияНе (само писалка)Ползва се и се обновява
EMR_POLYPOLYLINEПървата точка на всяка полилинияНе (само писалка)Не се ползва, не се обновява
EMR_POLYDRAWПървият PT_MOVETO или текущата позицияСамо където PT_CLOSEFIGURE е включенПолзва се и се обновява

Откъде всъщност започва кривата на EMR_POLYBEZIER?

Кривата на EMR_POLYBEZIER започва от точка 0 и само точките от индекс 1 нататък се групират по три — контролна точка, контролна точка, крайна точка. Запис с 7 точки значи рисува два кубични сегмента: 0 е началото, 1–3 са първият сегмент, 4–6 са вторият. 16-битовият обработчик в PDFlibPas вече го правеше така. 32-битовият започваше групирането от точка 0, откъдето началната точка се поглъщаше като първа контролна точка, а всеки следващ сегмент се отместваше с едно. Кривата и излизаше — просто грешната. От v3.539.41 и двете ширини отварят пътя с m в точка 0 и издават по един c за всяка пълна тройка след нея

PDFlibPas диаграма на EMR_POLYBEZIER запис със седем точки, където точка нула отваря пътя с m, а точките от едно до три и от четири до шест образуват по един кубичен c сегмент — фиксираният 32-битов обработчик от v3.539.41 срещу старото групиране, поглъщало началната точка като контролна
Точка 0 е началната точка и само пълните тройки след нея стават кубични сегменти, така че PolyBezier със седем точки се рендира като m плюс два оператора c

За вашия собствен парсер: брой, който не е 1 плюс кратно на 3, е некоректен, а излишните точки в края трябва да се пренебрегнат, а не да се зашиват в кривата

PolyDraw: PT_CLOSEFIGURE е флаг, не тип точка

В EMR_POLYDRAW PT_CLOSEFIGURE (стойност 1) е бит, който се комбинира с PT_LINETO (2) или PT_BEZIERTO (4), така че валиден байт за тип може да е 3 или 5. Типът на точката е байтът с този бит маскиран, а флагът значи „затвори фигурата след сегмента, завършващ на тази точка“. Старият обработчик в PDFlibPas съпоставяше байта с единични стойности в case оператор, така че точки от тип 3 и 5 не съвпадаха с нищо и изцяло се прескачаха. Правоъгълник, рисуван с PolyDraw, губеше затварящата си страна, а Bezier тройка, чиято последна точка носеше флага, губеше тази точка, което изместваше всяка следваща тройка от ритъма

От v3.539.39 типът се чете като Types[i] and not PT_CLOSEFIGURE, а затварянето се издава едва след пълен сегмент: след линията при затворен PT_LINETO и след третата точка на Bezier група. Некоректен файл, който включи флага на първата или втората точка от тройка, не затваря фигурата преждевременно. Две свързани поправки излязоха в същата версия:

  • Всеки PT_MOVETO в 16-битовия EMR_POLYDRAW16 рестартираше целия път, така че запис с три фигури пазеше само последната; сега първото движение стартира пътя, а следващите отварят подпътища
  • PolyDraw запис, който не започва с PT_MOVETO, тръгва от текущата позиция, както казва дефиницията на записа, вместо да изпише оператор l или c без предхождащ m
PDFlibPas анатомия на тип байт в EMR_POLYDRAW, където PT_CLOSEFIGURE е флагов бит нула, OR-нат в PT_LINETO или PT_BEZIERTO, така че валидните тип байтове 3 и 5 трябва да се маскират с and not PT_CLOSEFIGURE преди разпределянето; старият case оператор прескачаше двата байта и затворените фигури губеха последната си страна
Маскирайте флага за затваряне преди разпределянето и издавайте затварянето едва след завършена линия или Bezier тройка, иначе PolyDraw тихо губи точки

Защо EMF полилиния никога не бива да се запълва в PDF?

EMF полилиния никога не бива да се запълва, защото EMR_POLYLINE и EMR_POLYPOLYLINE са отворени фигури, рисувани само с писалката, а запълване на отворен път в PDF го затваря имплицитно. ISO 32000-1 §8.5.3 гласи, че запълващите оператори затварят всеки отворен подпът, преди да го нарисуват. Конвертор, който издаде B или f за полилиния от три точки, рисува запълнен триъгълник в текущия цвят на четката: запълненият клин под линията на тренда в диаграма. Преди v3.539.41 PDFlibPas запълваше и двете ширини на полилинията с четката, а 32-битовият запис и изрично се затваряше. Днес и двете ширини завършват само с щрих, а GDI разликата е запазена: Polygon затваря и запълва, Polyline никога не го прави

PDFlibPas сравнение на отворена V полилиния, изнесена от EMR_POLYLINE: коректен конвертор завършва пътя със stroke оператора S и игнорира избраната четка, докато издаването на f или B имплицитно затваря отворения подпът по ISO 32000-1 8.5.3 и рисува запълнения клин — бъгът на диаграмите
Запълващ оператор затваря всеки отворен подпът преди рисуване, така че полилиниите трябва да завършват със S, без h, f или B по подпътя

PolylineTo тръгва от текущата позиция

EMR_POLYLINETO чертае от текущата позиция през всяка точка в записа, остава отворен и оставя текущата позиция на последната точка. Старият обработчик съдържаше и специален случай, който изключваше писалката, когато първите две точки споделят y координата, и нищо никога не я включваше обратно, така че всеки следващ запис във файла губеше контура си. Състоянието на писалката е работа на EMR_SELECTOBJECT и EMR_CREATEPEN; обработчик на рисуващ запис няма работа да го пипа. Този специален случай беше премахнат в v3.539.41, а едноточковият вариант на записа вече не чете след собствените си точки (поправено в v3.539.39)

Точките на PolyPolyline започват след масива с бройки

32-битовият EMR_POLYPOLYLINE пази nPolys бройки и после cptl точки, а точките започват от байтово отместване 32 + nPolys * 4. Капанът е в RTL: unit-ът Windows декларира TEMRPolyPolyline с aPolyCounts и aptl като едноелементни масиви, така че aptl[0] е първата точка само когато nPolys е 1. Код, който индексира aptl директно, чете стойностите на бройките като координати за всеки многоредов запис. Старият обработчик в PDFlibPas оразмеряваше и проверката си за граници по същото грешно подреждане, така че валидните многоредови записи се отхвърляха, а едноредовите не рисуваха нищо. От v3.539.41 PDFlibPas намира масива с точки от изчисленото отместване, както неговият PolyPolygon обработчик винаги е правил, и рисува всяка полилиния като собствен отворен подпът с един щрих накрая. В v3.539.43 16-битовият близнак получи същото лечение; той чертаеше сегмент по сегмент, което чупеше съединенията на линиите и игнорираше избран NULL_PEN

Подразбиращите се писалка и четка и скобите на пътя

Две правила за състояние доизпълняват поправките по полилиниите в v3.539.43:

  • Пресен GDI device context вече има избрани BLACK_PEN и WHITE_BRUSH, така че метафайл, който рисува без нито един EMR_SELECTOBJECT, пак рисува черни контури; конверторът преди започваше без писалка и без запълване и записваше n (край на пътя, без рисуване) за такива записи
  • Вътре в скобите BeginPath / EndPath една Polyline нито ползва, нито обновява текущата позиция, така че трябва да отвори нов подпът от първата си точка вместо да се свърже с предишната фигура, и нищо не бива да се рисува, докато скобата не бъде ощрихована или запълнена

Създаване на EMF тестов файл с TMetafileCanvas

Най-бързият начин да проверите един конвертор срещу тези правила е да запишете трите рискови извиквания в един enhanced metafile с TMetafileCanvas. Чертежът по-долу записва кривите с куха четка и после нарочно избира плътна жълта четка за полилинията: коректен конвертор трябва да игнорира тази четка за полилинията, така че всяко жълто в изходния PDF е бъг. PolyDraw няма обвивка в TCanvas, затова се извиква през Windows API с handle-а на канваса, с тип байтове 3 и 5, за да упражним флага за затваряне

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

procedure BuildPolyTestEmf(const FileName: string);
const
  // Затворен квадрат (3 = LINETO + CLOSEFIGURE), после затворена
  // Bezier фигура, чиято последна тройка завършва с 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;    // само контури за кривите
      // Точка 0 е началото; 1..3 и 4..6 са два кубични сегмента
      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 форма с избрана плътна четка: само щрих, никога
      // не се затваря в жълт триъгълник
      Canvas.Brush.Style := bsSolid;
      Canvas.Brush.Color := clYellow;
      Canvas.Polyline([Point(20, 240), Point(120, 160), Point(220, 240)]);
    finally
      Canvas.Free;   // приключва записа
    end;
    Mf.SaveToFile(FileName);
  finally
    Mf.Free;
  end;
end;

Тъй като тези координати се събират в SmallInt, GDI обикновено ще запише 16-битовите варианти. За да стигнете до 32-битовите обработчици, ви трябва производител, който ги записва, или записи, сглобени на ръка. Ръчно сглобените файлове носят собствен капан: VCL TMetafile.LoadFromStream третира потока като EMF само когато остатъчната дължина е строго по-голяма от 108-байтовия TEnhMetaHeader. Минимален ръчно написан EMF с къс header, или празен с точно 108 байта, се приема за WMF и се отхвърля с „Metafile is not valid“. Винаги пишете пълния 108-байтов header, включително extension полетата, преди тестовите си записи

Импортиране на EMF в PDF с PDFlibPas

PDFlibPas импортира EMF чрез ImportEMFFromFile или ImportEMFFromStream, които връщат ненулево image ID при успех и 0 при неуспех. GeneralOptions = 0 запазва векторния път, за който е тази статия; 1 растеризира метафайла в bitmap вместо това. FontOptions = 1 добавя шрифтите на метафайла като не-вградени TrueType шрифтове. Вариантът със stream превърта потока до позиция 0 преди зареждане, така че подайте поток, който съдържа само метафайла

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);              // начало в горния ляв ъгъл за DrawImage
    PDF.SetMeasurementUnits(0);    // пунктове
    // FontOptions 1 = добавя шрифтовете като не-вградени TrueType
    // GeneralOptions 0 = векторен импорт, 1 = bitmap
    ImageID := PDF.ImportEMFFromFile(EmfFile, 1, 0);
    if ImageID = 0 then
      raise Exception.Create('The metafile could not be imported');
    PDF.SelectImage(ImageID);
    // За EMF ImageWidth / ImageHeight са размерът на рамката в пунктове
    PDF.DrawImage(36, 36, PDF.ImageWidth, PDF.ImageHeight);

    // Страницата само извиква импортираната форма: 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;

Векторният EMF импорт става form XObject, така че GetPageContentToString връща само поредицата save, transform, Do и restore. Операторите m, l, c, h и S, породени от Poly* записите, живеят в потока на form XObject, който е компресиран. За да ги одитирате, декомпресирайте записания файл в PDF object inspector и прочетете form потока: за тестовия файл по-горе трябва да видите полилинията да завършва със S без h преди нея, по един h при всеки флаг за затваряне в PolyDraw фигурите и никакви f или B по тези подпътища. DrawImage мащабира и импортиран EMF равномерно по по-малкия от Width и Height, така че чертежът пази съотношението си, дори кутията, която подадете, да не му отговаря

За Free Pascal цели вижте как се компилира EMF векторният импортер на PDFlibPas под Free Pascal; семантиката на записите е същата, където и да се компилира импортерът

Как парсерът на EMF трябва да третира бройките точки от файла?

Парсерът на EMF трябва да третира всеки брой точки като недоверен вход и да го провери спрямо размера на записа, преди да копира и една точка. EnumEnhMetaFile гарантира само, че nSize на всеки запис остава вътре във файла. Той не проверява дали cptl отговаря на nSize, така че обработчик, копиращ cptl точки с Move, ще прочете следващите записи или ще излезе след края на метафайла, когато бройката е фалшифицирана или повредена. От v3.539.39 PDFlibPas проверява фиксиран header плюс брой по байтове на точка спрямо nSize за PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo и Polygon в двете ширини, с един допълнителен байт на точка за тип байтовете на PolyDraw. За PolyPoly записите бройките по фигура трябва и да се съберат до не повече от декларираното, а фигурите с нули точки се прескачат

Същата проверка е достатъчно кратка, за да я копирате в собствения си парсер. Тази версия валидира 32-битов EMR_POLYPOLYLINE и връща указател към истинския му масив с точки:

uses
  Winapi.Windows;

// Връща nil, освен ако записът наистина не съдържа декларираните точки.
// Точките започват след масива с бройки: 32 + nPolys * 4 байта навътре, не
// при aptl[0], който RTL декларира като едноелементен масив
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;                         // фалшифицирана или отрязана бройка
  Total := 0;
  Count := @P^.aPolyCounts[0];    // обхождане с пойнтер: [0..0] чупи range checks
  for I := 1 to P^.nPolys do
  begin
    Inc(Total, Count^);
    Inc(Count);
  end;
  if Total > P^.cptl then
    Exit;                         // фигурите искат повече точки, отколкото има
  Result := PPoint(NativeUInt(Rec) + NativeUInt(PointsOffset));
end;

Проверката дали точките се събират върви първа, така че масивът с бройки е известно, че е вътре в записа, преди цикълът да го обходи. Аритметиката е Int64, защото nPolys * 4 и cptl * 8, сметнати в 32 бита, могат да препълнят и да минат сравнението

Бърза справка: EMF Poly* правила за конверсия EMF към PDF

  • EMR_POLYBEZIER: точка 0 е началната точка; групирайте от точка 1 по три; поправено за 32-битовия запис в v3.539.41
  • EMR_POLYLINE / EMR_POLYPOLYLINE: отворени фигури, щрих със S, никога h, f или B, защото запълването в PDF затваря отворените подпътища
  • EMR_POLYLINETO: тръгва от текущата позиция, остава отворен, обновява текущата позиция, никога не пипа състоянието на писалката
  • 32-битов EMR_POLYPOLYLINE: точките започват от байт 32 + nPolys * 4, не при aptl[0]
  • EMR_POLYDRAW: маскирайте PT_CLOSEFIGURE преди разпределянето, затваряйте след завършения сегмент, тръгвайте от текущата позиция, когато първата точка не е PT_MOVETO
  • Подразбиращото се състояние на device context е BLACK_PEN плюс WHITE_BRUSH; v3.539.43 и по-новите го спазват
  • Вътре в BeginPath / EndPath всяка полилиния отваря собствен подпът и нищо не се рисува, докато скобата не бъде употребена
  • Валидирайте всеки cptl / cpts спрямо nSize в 64-битова аритметика, преди да копирате точки
  • Ръчно сглобените тестови EMF файлове имат нужда от пълния 108-байтов header, иначе TMetafile.LoadFromStream ги чете като WMF

Ако отчетите ви минават през друг компонент, семантиката на записите е същата; HotPDF EMF и WMF векторен импорт покрива как този компонент превръща gradient и hatch четки в PDF patterns, а векторна графика, shaders и градиенти в PDFlibPas покрива рисуването на същите фигури директно с API на библиотеката вместо през метафайл

PDFlibPas v3.539.43 или по-нова включва всяко правило по-горе. Подробности и trial изтегляния има на продуктовата страница на PDFlibPas Delphi PDF library