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 с закрашенным клином там, где должна быть линия тренда, с кривой Безье, согнутой к чужой контрольной точке, или с замкнутым контуром, у которого не хватало последней стороны. Ни одна из этих историй не поднимала ошибку, и правила относятся к любому конвертеру Delphi EMF в PDF и к любому парсеру GDI-записей
Почему записи EMF Poly* ломаются при конверсии в PDF?
Записи EMF Poly* ломаются потому, что часть своего смысла каждая носит не в точках: замкнута фигура или открыта, стартует ли она с текущей позиции, какое перо и кисть действуют и с какого места в записи начинаются точки. Расширенный метафайл — это запись вызовов 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 плюс кратное трём, — признак испорченной записи, а хвостовые точки стоит игнорировать, а не пришивать к кривой
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перезапускал весь путь, так что запись с тремя фигурами сохраняла лишь последнюю; теперь первый ход стартует путь, а последующие открывают подпути - Запись 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/EndPathполилинияPolylineни использует, ни обновляет текущую позицию, поэтому обязана открыть новый подпуть со своей первой точки, а не примкнуть к предыдущей фигуре, и пока скобка не обведена и не залита, рисовать нельзя ничего
Собираем тестовый EMF-файл с TMetafileCanvas
Быстрее всего проверить конвертер на этих правилах, записав три рискованных вызова в один расширенный метафайл через 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, возвращающие при успехе ненулевой image ID, а при неудаче 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] задевает 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- Состояние контекста устройства по умолчанию —
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