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