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 із залитим сектором там, де мала бути лінія тренду, з кривою Безьє, зігнутою до неправильної контрольної точки, чи із закритим контуром без останньої сторони. Жодна з цих ситуацій не піднімала помилки, а правила стосуються будь-якого конвертера EMF у PDF на Delphi чи парсера записів GDI
Чому записи EMF Poly* ламаються при конвертації в PDF?
Записи EMF Poly* ламаються тому, що частина їхнього сенсу лежить за межами точок: чи фігура відкрита, чи вона стартує з поточної позиції, яке перо і пензель застосовуються, і де в записі починаються точки. Enhanced metafile — це запис викликів GDI до контексту пристрою, тож конвертер має відтворити не лише координати, а й стан того контексту пристрою. У PDF контексту пристрою немає. Є шлях, поточна точка всередині нього й оператор малювання, який обирає між обведенням (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, втрачав замикальну сторону, а трійка Безьє, чиї остання точка несла прапорець, втрачала ту точку, що збивало з кроку всі наступні трійки
З v3.539.39 тип читається як Types[i] and not PT_CLOSEFIGURE, а закриття видається лише після повного сегмента: після лінії для замкненого PT_LINETO і після третьої точки групи Безьє. Деформований файл, що ставить прапорець на першій чи другій точці трійки, не замикає фігуру завчасно. У тому самому релізі вийшли два суміжні виправлення:
- Кожен
PT_MOVETOу 16-бітовомуEMR_POLYDRAW16перезапускав увесь шлях, тож запис із трьома фігурами зберігав лише останню; тепер перший move стартує шлях, а наступні відкривають підшляхи - Запис 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: юніт 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 вже має вибраними
BLACK_PENіWHITE_BRUSH, тож метафайл, що малює без жодногоEMR_SELECTOBJECT, усе одно дає чорні контури; конвертер же стартував без пера і без заливки та писавn(завершити шлях, нічого не малюючи) для таких записів - Усередині дужок
BeginPath/EndPathPolylineне використовує і не оновлює поточну позицію, тож має відкривати новий підшлях із першої точки замість приєднання до попередньої фігури, і нічого не можна малювати, доки дужку не обведено чи не залито
Створюємо тестовий EMF-файл із TMetafileCanvas
Найшвидший спосіб перевірити конвертер проти цих правил — записати три ризиковані виклики в один enhanced metafile через TMetafileCanvas. Малюнок нижче записує криві з порожнім пензлем, а потім навмисно вибирає суцільний жовтий пензель для полілінії: коректний конвертер має проігнорувати той пензель для полілінії, тож будь-який жовтий у вихідному PDF — це баг. У PolyDraw немає обгортки в TCanvas, тож він викликається через Windows API з хендлом канви, з байтами типу 3 і 5, щоб провокувати прапорець закриття
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// Закритий квадрат (3 = LINETO + CLOSEFIGURE), а потім закрита фігура
// Безьє, чия остання трійка контрольних точок закінчується на 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 із коротким заголовком чи порожній, рівно 108 байтів завдовжки, приймається за WMF і відхиляється з «Metafile is not valid». Завжди пишіть повний 108-байтовий заголовок, з полями розширення, перед своїми тестовими записами
Імпортуємо EMF у PDF через PDFlibPas
PDFlibPas імпортує EMF через ImportEMFFromFile чи ImportEMFFromStream, які повертають ненульовий ідентифікатор зображення в разі успіху і 0 у разі невдачі. GeneralOptions = 0 зберігає векторний шлях, про який ця стаття; 1 растеризує метафайл у растр. FontOptions = 1 додає шрифти метафайлу як невбудовані TrueType. Варіант зі стрімом перемотує потік на позицію 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 = растр
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-об'єктів і прочитайте потік форми: у тестовому файлі вище полілінія має закінчуватися на 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 звіряє фіксований заголовок плюс лічильник, помножений на байти за точку, з 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] вмикає перевірки діапазону
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- Усталений стан контексту пристрою —
BLACK_PENплюсWHITE_BRUSH; v3.539.43 і новіші його дотримуються - Усередині
BeginPath/EndPathкожна полілінія відкриває власний підшлях, і нічого не малюється, доки дужку не використано - Валідуйте кожен
cptl/cptsпротиnSizeу 64-бітовій арифметиці перед копіюванням точок - Ручні тестові EMF потребують повного 108-байтового заголовка, інакше
TMetafile.LoadFromStreamпрочитає їх як WMF
Якщо ваші звіти йдуть через інший компонент, семантика записів та сама; векторний імпорт EMF і WMF у HotPDF розповідає, як той компонент перетворює градієнтні та штрихові пензлі на PDF-патерни, а векторна графіка, шейдери та градієнти в PDFlibPas — про малювання тих самих фігур напряму через API бібліотеки, без метафайлу
PDFlibPas v3.539.43 чи новіший містить усі наведені правила. Подробиці та пробні завантаження — на сторінці продукту PDFlibPas Delphi PDF library