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

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

HotPDF, native Delphi и C++Builder PDF компонентът, импортира Windows EMF и WMF метафайлове, като интерпретира всеки GDI запис директно като PDF оператори, вместо да превръща файла в растерно изображение: градиентните запълвания стават PDF axial shading patterns, щриховите четки стават PDF tiling patterns, а централизираният gate за състоянието на пътя блокира повредените записи, за да не замърсят изхода. Всяка диаграма, която TChart, GDI+ повърхност или обикновено TCanvas може да експортира като enhanced metafile, е кандидат за този път и разликата се вижда веднага щом някой увеличи страницата или я изпрати към принтер с висока резолюция

Алтернативата, към която повечето Delphi разработчици се обръщат по подразбиране, е да растеризират метафайла до растерно изображение, преди да го поставят на страницата, а цената се вижда едва по-късно: стълбовидна диаграма, която е била ясна на екрана, става видимо назъбена, щом PDF файлът бъде отпечатан с 600 DPI или прожектиран на голям екран, а щрихово запълнена CAD област се превръща в един правоъгълник с равномерен сив цвят, ако стилът на запълването не бъде пренесен. Именно четенето на метафайла като програма, а не като изображение, избягва и двата проблема, а този подход е по-труден за правилна реализация, затова си струва да познавате следващите препятствия, преди отчетът да бъде публикуван

Защо да интерпретираме метафайл, вместо да го превръщаме в растерно изображение?

HotPDF запазва импорта на EMF и WMF по векторния път, защото Windows метафайлът е записана последователност от GDI команди за рисуване, а не изображение, и възпроизвеждането на тези команди като PDF оператори за пътища, текст и shading позволява на резултата да се мащабира като останалата част от страницата. THPDFPage.ShowMetafile и съответстващият му ShowMetafileEx са входните точки, които приложението извиква, и и двете подават метафайла към THPDFWmf, класа, който обхожда всеки GDI запис и го превежда. Разграничението не е абсолютно и HotPDF не твърди обратното: запис от метафайл, който действително съдържа растерни данни, например bitmap blit чрез 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 от прозорец към viewport за анизотропните и изотропните режими на преобразуване, така че фигура, записана с ширина пет логически единици, да попадне с правилната ширина в PDF точки независимо от размерите на прозореца и viewport-а, зададени от приложението източник

Как GDI градиентно запълване става PDF shading pattern?

Записът EMR_GRADIENTFILL става реална PDF Type 2 axial shading pattern (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, което връща име на pattern, а страницата рисува правоъгълника и го запълва чрез тази pattern с SetFillPattern, вместо с плоско извикване SetRGBFillColor, така че лентовото заглавие в стил на електронна таблица или градиентната област на диаграма запазва преливането си, вместо да се свие до един усреднен цвят

Режимът Gouraud triangle е честна граница. Когато полето ulMode на записа съобщава GRADIENT_FILL_TRIANGLE, VEMRGradientFill го разпознава, записва диагностично съобщение, че режимът triangle все още не е реализиран, и пропуска правоъгълника, вместо да гадае двуцветно приближение. Интерполацията по върхове и пиксели през произволна триъгълна мрежа не се свежда до axial или radial shading с два стопа, а правилното ѝ изразяване би означавало излъчване на PDF Type 4 или Type 5 mesh shading, същото семейство shading, което рендерерът на страниците на HotPDF също оставя непокрито при четене на PDF. Два несвързани пътя на кода стигат до една и съща граница: mesh shading е пропуск и от страната на записването, и от страната на четенето, а изходна диаграма, която използва Gouraud triangles за гладко радиално сияние, се връща към последната плътна четка, а не към рендирано приближение

Щриховите четки стават tiling patterns, а не плоско сиво запълване

GDI щриховата четка запазва текстурата си в PDF, защото THPDFWmf.SetBrushColor проверява CurrentBrush.lbStyle за BS_HATCHED, преди изобщо да премине към плътно запълване, и насочва този случай към SetHatchBrushPattern. Този метод записва 8-by-8-единичен PDF content stream от оператори за начертаване на линии, m, l и S, избрани според стила на GDI четката: една хоризонтална или вертикална линия за HS_HORIZONTAL и HS_VERTICAL, три успоредни диагонала за HS_FDIAGONAL и HS_BDIAGONAL, както и комбинациите хоризонтала плюс вертикала или двата диагонала за HS_CROSS и HS_DIAGCROSS. THotPDF.RegisterTilingPattern регистрира този content stream като цветна tiling pattern (PaintType 1, ISO 32000-1 §8.7.3.1) със стъпка от 8 единици по XStep и YStep, а страницата запълва чрез SetFillPattern по същия начин като axial shading. План на CAD етаж или инженерна рисунка, която използва щрихови запълвания за разграничаване на материалите, запазва този визуален език в PDF, вместо всяка област да се изгуби в еднакво сиво

Не всяка четка заслужава тази обработка и тази граница е важно да се знае, преди да публикувате CAD импорт. EMR_CREATEDIBPATTERNBRUSHPT, записът за custom bitmap-image pattern brush, а не за един от шестте стандартни GDI hatch стила, регистрира само своя handle, за да останат последващите записи SELECTOBJECT и DELETEOBJECT съгласувани; HotPDF все още не предоставя PDF Pattern resource pipeline за произволни tile изображения, затова изборът на тази четка преминава към fallback с плътен цвят вместо към текстурата от източника. Ако запълването се визуализира плоско, въпреки че оригиналът явно използва повтаряща се текстура от изображение, изходната четка почти сигурно е custom DIB pattern, а не стандартен hatch, и това е случаят, който първо трябва да проверите ръчно. Конфигурирането на импорт за подобна рисунка все пак използва същия options обект:

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 е единен gate в началото на ExecuteRecord, вместо защитна проверка, повтаряна във всеки от приблизително осемдесетте обработващи записа. GDI path bracket, отворен с EMR_BEGINPATH и затворен с EMR_ENDPATH или EMR_ABORTPATH, се проследява чрез частното свойство PathContinue, поддържано от полето FPathContinue. Докато bracket-ът е отворен, ExecuteRecord пропуска само записи за изграждане на пътя — вариантите move, line, polyline, polygon, polybezier и polydraw — както и CLOSEFIGURE и малък набор от записи за transform и DC state, например SETWORLDTRANSFORM, SAVEDC и RESTOREDC. Всеки друг тип запис, който достигне до ExecuteRecord при отворен bracket, например случаен EXTTEXTOUT или bitmap blit, се отхвърля централно с едно Exit в момента на пристигането му

Този gate е нужен, защото path bracket в ръчно създаден, генериран от инструмент или просто повреден метафайл не е гарантирано да съдържа само онова, което добре оформеният файл би поставил между записите за отваряне и затваряне. Запис за извеждане на текст, попаднал между EMR_BEGINPATH и EMR_ENDPATH, без gate би замърсил геометрията на изграждания път или би излъчил PDF оператор за показване на текст в средата на поредица, която трябва да съдържа само изграждане на път, а и двата отказа са от типа, който се проявява при един повреден вход от външен инструмент, а не при сценарий, обхванат от нормален тестов набор. Централизирането на проверката в ExecuteRecord означава, че отделните обработващи VEMR* handlers не трябва всеки поотделно да се защитават от извикване в неподходящ момент; gate-ът взема това решение веднъж, преди dispatch, вместо осемдесет пъти след него

Поставяне на векторна диаграма до текст и изображения на една страница

Страницата на отчета рядко съдържа само диаграма, а ShowMetafile се комбинира с останалите оператори на страницата в HotPDF точно както всяко друго drawing извикване. Заглавие, начертано с TextOut, стълбовидна диаграма с щрихово запълване, импортирана като EMF, и лого, поставено с ShowImage, могат да попаднат на една и съща страница в един и същ content stream, като всяко запазва собствената си точност, а моделът на композиция е разгледан в ръководството на 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, axial shading patterns, които регистрира за градиентни запълвания, и mapping-ът към tiling pattern за щрихови четки, описани тук, се доставят като част от стандартния HotPDF Component за Delphi и C++Builder, native VCL library без външна DLL зависимост за никоя от тези функции