HotPDF, рідний PDF-компонент для Delphi та C++Builder, імпортує метафайли Windows EMF і WMF, інтерпретуючи кожен запис GDI безпосередньо в оператори PDF замість того, щоб згортати файл у растрове зображення: градієнтні заливки стають осьовими (axial) шаблонами затінення PDF, штрихові пензлі — візерунковими (tiling) шаблонами PDF, а централізований шлюз стану контуру блокує спотворені записи, аби вони не пошкодили результат. Будь-яка діаграма, яку TChart, поверхня GDI+ чи звичайний TCanvas може експортувати як розширений метафайл, є кандидатом на цей шлях, і різниця стає очевидною, щойно хтось наближає сторінку або надсилає її на друк із високою роздільною здатністю
Альтернатива, до якої більшість розробників Delphi звертається за замовчуванням, — растеризувати метафайл у растрове зображення перед розміщенням на сторінці, і ціна цього проявляється лише пізніше: стовпчикова діаграма, що виглядала чітко на екрані, помітно розсипається на пікселі, щойно PDF надрукують із роздільною здатністю 600 DPI або спроектують на екран у переговорній кімнаті, а область САПР зі штриховою заливкою перетворюється на суцільний сірий прямокутник, якщо стиль заливки не передається далі. Читання метафайлу як програми, а не як картинки, дозволяє уникнути обох проблем — і водночас це складніший шлях для коректної реалізації, тому нижченаведені підводні камені варто знати ще до того, як звіт піде в роботу
Чому інтерпретувати метафайл, а не згортати його в растр?
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 перетворюється на справжній осьовий (axial) шаблон затінення 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, тож заголовок таблиці зі смугастим градієнтом або область градієнтного графіка на діаграмі зберігає перехід кольорів замість того, щоб звестися до одного усередненого кольору
Режим трикутників Гуро — це чесна прогалина. Коли поле ulMode запису повідомляє GRADIENT_FILL_TRIANGLE, VEMRGradientFill розпізнає це, реєструє в журналі, що режим трикутників ще не реалізований, і пропускає прямокутник, а не намагається вгадати двоколірне наближення. Інтерполяція по вершинах і пікселях для довільної трикутної сітки не зводиться до осьового чи радіального затінення з двома точками кольору, а коректне вираження цього означало б формування шаблону затінення сітки PDF типу 4 чи типу 5 — того самого сімейства затінень, яке рендерер сторінок 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 так само, як і у випадку осьового затінення. План поверху САПР чи інженерне креслення, що покладається на штрихові заливки для розрізнення матеріалів, зберігає цю візуальну мову в PDF замість того, щоб усі області злилися в однаковий сірий колір
Не кожен пензель заслуговує на таке ставлення, і цю прогалину варто знати ще до того, як імпорт САПР піде в роботу. 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 і невеликий набір записів трансформації та стану контексту пристрою, як-от 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 для будь-чого з цього