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

Импорт векторов EMF и WMF в PDF на Delphi с помощью HotPDF

HotPDF, нативный компонент PDF для Delphi и C++Builder, импортирует метафайлы Windows EMF и WMF, интерпретируя каждую запись GDI непосредственно в операторы PDF вместо растрирования файла в растровое изображение: градиентные заливки становятся осевыми (axial) шаблонами затенения PDF, штриховые кисти становятся узорными (tiling) шаблонами PDF, а централизованный контроль состояния пути блокирует некорректные записи, не давая им испортить результат. Любая диаграмма — будь то TChart, поверхность GDI+ или обычный TCanvas, — которую можно экспортировать как расширенный метафайл, является кандидатом на этот путь обработки, и разница становится заметна, как только кто-то увеличивает масштаб страницы или отправляет её на печать на принтере высокого разрешения

Альтернатива, к которой по умолчанию прибегает большинство разработчиков на Delphi, — растрировать метафайл в изображение перед размещением на странице, и цена этого проявляется позже: столбчатая диаграмма, чёткая на экране, становится заметно «зубчатой», как только PDF печатается с разрешением 600 точек на дюйм или проецируется на экран в переговорной, а область CAD-чертежа со штриховой заливкой превращается в один сплошной серый прямоугольник, если стиль заливки не был перенесён. Чтение метафайла как программы, а не как картинки, позволяет избежать обеих проблем, и это более сложный путь для корректной реализации, поэтому подводные камни ниже стоит знать прежде, чем отчёт отправится в печать

Почему интерпретировать метафайл, а не растрировать его?

HotPDF придерживается векторного пути импорта EMF и WMF потому, что метафайл Windows — это записанная последовательность вызовов рисования GDI, а не картинка, и воспроизведение этих вызовов в виде операторов пути, текста и затенения PDF — именно то, что позволяет результату масштабироваться так же, как остальная часть страницы. THPDFPage.ShowMetafile и его аналог ShowMetafileEx — точки входа, которые вызывает приложение, и обе передают метафайл классу THPDFWmf, который обходит каждую запись GDI и транслирует её. Это разделение не абсолютно, и HotPDF этого не скрывает: запись метафайла, которая на самом деле является растровыми данными, например блиттинг битового изображения через StretchDIBits, встраивается как настоящий PDF Image XObject через AddImage и ShowImage — ту же пару вызовов, через которую проходит любое другое изображение на странице, — вместо того чтобы принудительно превращаться в операторы пути, неспособные выразить фотографию. Линии, заливки и текст остаются векторными; пиксели, которые уже были пикселями в исходнике, остаются пикселями и в результате. Простейший вызов не требует ничего, кроме загруженного метафайла:

var
  Pdf: THotPDF;
  Chart: TMetafile;
begin
  Pdf := THotPDF.Create(nil);
  Chart := TMetafile.Create;
  try
    Chart.LoadFromFile('quarterly-revenue.emf');  // exported from TChart or GDI+
    Pdf.FileName := 'quarterly-report.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafile(Chart);
    Pdf.EndDoc;
  finally
    Chart.Free;
    Pdf.Free;
  end;
end;

Как интерпретатор преобразует координаты GDI в пространство страницы PDF?

HotPDF отвечает на это одним проходом по собственному потоку записей метафайла вместо второй реализации GDI. THPDFWmf.Analyse считывает заголовок метафайла через вызов Win32 GetEnhMetaFileHeader, сбрасывает своё внутреннее состояние рисования и вызывает EnumEnhMetafile — тот же API перечисления, которым пользовался бы обозреватель метафайлов, — так что каждая запись EMR_* попадает в THPDFWmf.ExecuteRecord в том порядке, в котором была изначально записана. GDI выражает координаты сверху вниз в устройственных или логических единицах, выбранных собственным режимом отображения метафайла; страница PDF — снизу вверх, в точках пространства пользователя, системе координат, описанной в модели рисования холста HotPDF для путей и заливок. Каждый обработчик записи устраняет это несоответствие через ScaleX и ScaleY, которые вызывают ProjectX и ProjectY, воспроизводя собственную формулу GDI преобразования окна в область просмотра для анизотропных и изотропных режимов отображения, так что фигура, записанная шириной в пять логических единиц, оказывается правильной ширины в точках PDF независимо от того, какие протяжённости окна и области просмотра установило исходное приложение

Как градиентная заливка GDI становится шаблоном затенения PDF?

Запись EMR_GRADIENTFILL превращается в настоящий осевой шаблон затенения PDF типа 2 (ISO 32000-1 §8.7.4.5) всякий раз, когда GDI записал её в одном из двух прямоугольных режимов. THPDFWmf.VEMRGradientFill считывает собственную структуру записи прямо из буфера сырых байтов, следуя структуре MS-EMF §2.3.1.6: массив вершин с 16-битными RGBA-угловыми точками, за которым следует список прямоугольников, каждый из которых ссылается на две из этих вершин. Для GRADIENT_FILL_RECT_H цвета переходят слева направо вдоль горизонтальной средней линии прямоугольника; для GRADIENT_FILL_RECT_V — сверху вниз вдоль вертикальной средней линии. В любом случае оба угловых цвета и спроецированные координаты прямоугольника передаются напрямую в THotPDF.RegisterAxialGradient, который возвращает имя шаблона, и страница рисует прямоугольник и заполняет его через этот шаблон (SetFillPattern) вместо простого вызова SetRGBFillColor, так что заголовок таблицы в стиле электронной таблицы с полосами или градиентная область диаграммы сохраняет свой переход цвета, а не сводится к одному усреднённому цвету

Режим треугольников Гуро (Gouraud) — честно признаваемый пробел. Когда поле ulMode записи сообщает GRADIENT_FILL_TRIANGLE, VEMRGradientFill распознаёт это, фиксирует в журнале, что режим треугольников пока не реализован, и пропускает прямоугольник, вместо того чтобы приближать его двухцветным градиентом. Поточечная, попиксельная интерполяция по произвольной треугольной сетке не сводится к двухточечному осевому или радиальному затенению, а корректное выражение этого означало бы формирование PDF-затенения типа 4 или типа 5 — того же семейства сеточных (mesh) затенений, которое программа рендеринга страниц HotPDF также оставляет неотрисованным при чтении PDF обратно. Два независимых участка кода упираются в одну и ту же границу: сеточные затенения — это пробел как на стороне записи, так и на стороне чтения, и исходная диаграмма, использующая треугольники Гуро для плавного радиального свечения, откатывается к тому, какой была последняя сплошная кисть, а не к отрисованному приближению

Штриховые кисти становятся узорными шаблонами, а не сплошной серой заливкой

Штриховая кисть GDI сохраняет свою текстуру в PDF, потому что THPDFWmf.SetBrushColor проверяет CurrentBrush.lbStyle на BS_HATCHED прежде, чем откатиться к сплошной заливке, направляя этот случай в SetHatchBrushPattern. Этот метод записывает поток содержимого PDF размером 8×8 единиц из операторов обводки линий, m, l и S, выбираемых в зависимости от стиля штриховки GDI: одна горизонтальная или вертикальная линия для HS_HORIZONTAL и HS_VERTICAL, три параллельные диагонали для HS_FDIAGONAL и HS_BDIAGONAL, а также сочетания горизонталь-плюс-вертикаль или обе диагонали для HS_CROSS и HS_DIAGCROSS. THotPDF.RegisterTilingPattern регистрирует этот поток содержимого как цветной узорный шаблон (PaintType 1, ISO 32000-1 §8.7.3.1) с шагом XStep и YStep в 8 единиц, и страница заполняется через SetFillPattern точно так же, как и при осевом затенении. Поэтажный план CAD или инженерный чертёж, где штриховые заливки используются для различения материалов, сохраняет этот визуальный язык в PDF, а не теряет все области, сводя их к одинаковому серому цвету

Не каждая кисть удостаивается такой обработки, и этот пробел стоит знать прежде, чем импорт CAD-чертежа отправится в печать. EMR_CREATEDIBPATTERNBRUSHPT — запись для пользовательской кисти-узора на основе растрового изображения, а не одного из шести стандартных стилей штриховки GDI, — лишь регистрирует свой дескриптор, чтобы последующие записи SELECTOBJECT и DELETEOBJECT оставались согласованными; HotPDF пока не предоставляет конвейер ресурсов PDF Pattern для произвольных изображений-плиток, поэтому выбор такой кисти приводит к откату на сплошную заливку вместо исходной текстуры. Если заливка отображается плоской там, где в оригинале явно использовалась повторяющаяся текстура изображения, исходная кисть почти наверняка является пользовательским DIB-узором, а не стандартной штриховкой, и это тот единственный случай, который стоит сначала проверить вручную. Настройка импорта для такого чертежа по-прежнему выполняется через тот же объект параметров:

var
  Pdf: THotPDF;
  Drawing: TMetafile;
  Options: THPDFEmfOptions;
begin
  Pdf := THotPDF.Create(nil);
  Drawing := TMetafile.Create;
  Options := THPDFEmfOptions.Create;
  try
    Drawing.LoadFromFile('floor-plan.emf');
    Options.Assign(Pdf.EmfOptions);   // start from the document-wide defaults
    Options.Redraw := False;          // interpret the original EMF bytes, no GDI re-record pass
    Options.ShowNullBrush := True;    // keep explicitly unfilled CAD regions visible
    Options.UseFrame := True;         // clip output to the frame the EMF header declares
    Pdf.FileName := 'floor-plan.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.ShowMetafileEx(Drawing, Options);
    Pdf.EndDoc;
  finally
    Options.Free;
    Drawing.Free;
    Pdf.Free;
  end;
end;

Что не даёт некорректному метафайлу испортить страницу?

Ответ HotPDF — единый контрольный барьер в начале ExecuteRecord, а не защитная проверка, повторяемая в каждом из примерно восьмидесяти обработчиков записей. Скобка пути GDI, открываемая EMR_BEGINPATH и закрываемая EMR_ENDPATH или EMR_ABORTPATH, отслеживается приватным свойством PathContinue, за которым стоит поле FPathContinue. Пока эта скобка открыта, ExecuteRecord пропускает только записи построения пути — варианты перемещения, линии, полилинии, полигона, полибезье и полидро, плюс CLOSEFIGURE и небольшой набор записей преобразования и состояния DC, таких как SETWORLDTRANSFORM, SAVEDC и RESTOREDC. Любой другой тип записи, попадающий в ExecuteRecord пока скобка открыта — случайный EXTTEXTOUT или блиттинг изображения, например, — отбрасывается централизованно единственным Exit в момент поступления

Этот барьер существует потому, что скобка пути в метафайле, созданном вручную, сгенерированном сторонним инструментом или просто повреждённом, не гарантированно содержит только то, что поместил бы туда корректно сформированный файл между своими открывающей и закрывающей записями. Запись вывода текста, оказавшаяся между EMR_BEGINPATH и EMR_ENDPATH, без такого барьера либо испортила бы строящуюся геометрию пути, либо выдала бы оператор PDF для отображения текста посреди последовательности, которая должна быть чистым построением пути, и оба варианта отказа — из тех, что проявляются на одном некорректном входном файле от стороннего инструмента, а не на том, что обычно покрывает стандартный набор тестов. Централизация проверки в ExecuteRecord означает, что отдельным обработчикам VEMR* не нужно каждому защищаться от вызова в неподходящий момент; барьер решает это один раз, до диспетчеризации, вместо восьмидесяти раз после неё

Размещение векторной диаграммы рядом с текстом и изображениями на одной странице

Страница отчёта редко содержит только диаграмму, и ShowMetafile сочетается с другими операторами страниц HotPDF точно так же, как и любой другой вызов рисования. Заголовок, нарисованный через TextOut, столбчатая диаграмма со штриховой заливкой, импортированная как EMF, и логотип, размещённый через ShowImage, могут все оказаться на одной странице в одном потоке содержимого, каждый сохраняя свою нативную точность отображения — этот приём компоновки описан в руководстве HotPDF по размещению текста, шрифтов и изображений в отчёте:

Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Q2 Regional Sales');
Pdf.CurrentPage.ShowMetafile(RegionChart);   // hatch-filled bars, still vector
Pdf.CurrentPage.ShowImage(LogoIndex, 450, 760, 90, 30, 0);

Интерпретатор EMF и WMF, регистрируемые им осевые шаблоны затенения для градиентных заливок и отображение штриховых кистей в узорные шаблоны, описанные здесь, поставляются как часть стандартного компонента HotPDF для Delphi и C++Builder — нативной библиотеки VCL без каких-либо внешних зависимостей от DLL