HotPDF, רכיב ה-PDF הילידי (native) של Delphi ו-C++Builder, מייבא קובצי metafile מסוג EMF ו-WMF של Windows על ידי פירוש כל רשומת GDI ישירות לאופרטורים של PDF במקום להשטיח את הקובץ לביטמאפ: מילויי גרדיאנט הופכים לתבניות הצללה צירית של PDF, מברשות משטח (hatch) הופכות לתבניות ריצוף (tiling), ושער מרכזי של מצב-נתיב חוסם רשומות פגומות מלהשחית את הפלט. כל תרשים ש-TChart, משטח GDI+, או TCanvas רגיל יכולים לייצא כ-metafile משופר הוא מועמד לנתיב הזה, וההבדל בולט ברגע שמישהו מתקרב (zoom) לעמוד או שולח אותו למדפסת ברזולוציה גבוהה
החלופה שרוב מפתחי ה-Delphi פונים אליה כברירת מחדל היא הפיכת ה-metafile לביטמאפ לפני הנחתו על העמוד, והמחיר מתגלה רק מאוחר יותר: תרשים עמודות שהיה חד על המסך הופך גושי (blocky) באופן ניכר ברגע שה-PDF מודפס ב-600 DPI או מוקרן על מסך חדר ישיבות, ואזור CAD ממולא הצללה (hatch) קורס למלבן אפור שטוח יחיד אם סגנון המילוי לא נשמר לאורך הדרך. קריאת ה-metafile כתוכנית ולא כתמונה היא מה שמונע את שתי הבעיות, וזהו הנתיב הקשה יותר למימוש נכון, ולכן שווה להכיר את המכשולים שלהלן לפני שדוח יוצא לדרך
למה לפרש metafile במקום להשטיח אותו לביטמאפ?
HotPDF שומר על ייבוא ה-EMF וה-WMF במסלול הווקטורי משום ש-metafile של Windows הוא רצף מוקלט של קריאות ציור GDI, לא תמונה, וניגון מחדש של הקריאות הללו כאופרטורים של נתיב, טקסט והצללה ב-PDF הוא מה שמאפשר לתוצאה להתקנן (scale) כמו שאר העמוד. THPDFPage.ShowMetafile ובן-זוגה ShowMetafileEx הן נקודות הכניסה שאפליקציה קוראת להן, ושתיהן מוסרות את ה-metafile ל-THPDFWmf, המחלקה שעוברת על כל רשומת GDI ומתרגמת אותה. ההבחנה אינה מוחלטת, ו-HotPDF לא מתיימר אחרת: רשומת metafile שהיא בפועל נתוני רסטר, blit של ביטמאפ מסוג StretchDIBits לדוגמה, משובצת כ-Image XObject אמיתי של PDF דרך AddImage ו-ShowImage, אותו זוג קריאות שכל תמונה אחרת בעמוד עוברת, ולא נכפית כאופרטורי נתיב שלא יכולים לבטא צילום. קווים, מילויים וטקסט נשארים וקטוריים; פיקסלים שכבר היו פיקסלים במקור נשארים פיקסלים בפלט. הקריאה הפשוטה ביותר לא דורשת דבר מעבר ל-metafile הטעון:
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 עונה על כך במעבר יחיד על זרם הרשומות של ה-metafile עצמו, במקום מימוש שני של GDI. THPDFWmf.Analyse קוראת את כותרת ה-metafile דרך קריאת ה-Win32 GetEnhMetaFileHeader, מאפסת את מצב הציור הפנימי שלה, וקוראת ל-EnumEnhMetafile, אותו API ספירה שצופה metafile היה משתמש בו, כך שכל רשומת EMR_* מגיעה אל THPDFWmf.ExecuteRecord בסדר שבו הוקלטה במקור. GDI מבטא קואורדינטות מלמעלה-למטה ביחידות התקן או לוגיות שנבחרות על ידי מצב המיפוי של ה-metafile עצמו; עמוד PDF הוא מלמטה-למעלה בנקודות מרחב-משתמש, מערכת הקואורדינטות המכוסה במודל ציור הקנבס של HotPDF עבור נתיבים ומילויים. כל פונקציית טיפול ברשומה פותרת את אי-ההתאמה הזו דרך ScaleX ו-ScaleY, שקוראות ל-ProjectX ול-ProjectY כדי לנגן מחדש את נוסחת החלון-לתצוגה (window-to-viewport) של GDI עצמה עבור מצבי המיפוי האניזוטרופיים והאיזוטרופיים, כך שצורה שהוקלטה ברוחב חמש יחידות לוגיות נוחתת ברוחב הנכון בנקודות PDF ללא קשר לאילו היקפי חלון ותצוגה (viewport) האפליקציה המקורית הגדירה
איך מילוי גרדיאנט של GDI הופך לתבנית הצללה של PDF?
רשומת EMR_GRADIENTFILL הופכת לתבנית הצללה צירית אמיתית מסוג 2 של PDF (ISO 32000-1 סעיף 8.7.4.5) בכל פעם ש-GDI הקליט אותה באחד משני מצבי המלבן. THPDFWmf.VEMRGradientFill קוראת את הפריסה של הרשומה ישירות ממאגר הבייטים הגולמי, בהתאם למבנה MS-EMF סעיף 2.3.1.6: מערך קודקודים של פינות RGBA בנות 16 סיביות, ואחריו רשימת מלבנים שכל אחד מהם מפנה לשני קודקודים מהם. עבור GRADIENT_FILL_RECT_H, הצבעים נעים משמאל לימין לאורך קו האמצע האופקי של המלבן; עבור GRADIENT_FILL_RECT_V, הם נעים מלמעלה למטה לאורך קו האמצע האנכי. בשני המקרים, שני צבעי הפינה וקואורדינטות המלבן המוקרנות עוברים ישירות אל THotPDF.RegisterAxialGradient, שמחזירה שם תבנית, והעמוד מצייר את המלבן וממלא אותו דרך התבנית הזו (SetFillPattern) במקום קריאת SetRGBFillColor שטוחה, כך שכותרת מפוספסת בסגנון גיליון אלקטרוני או אזור תרשים גרדיאנט שומרים על המעבר שלהם במקום לקרוס לצבע ממוצע אחד
מצב משולשי Gouraud הוא הפער הכן. כאשר שדה ulMode של הרשומה מדווח GRADIENT_FILL_TRIANGLE, VEMRGradientFill מזהה זאת, רושמת ביומן שמצב המשולשים עדיין לא ממומש, ומדלגת על המלבן במקום לנחש קירוב דו-צבעי. אינטרפולציה לפי קודקוד ולפי פיקסל על פני רשת משולשים שרירותית לא מצטמצמת להצללה צירית או רדיאלית בעלת שתי עצירות, ולביטוי נכון שלה יידרש פליטת הצללת רשת (mesh shading) מסוג 4 או 5 של PDF, אותה משפחת הצללות שגם מנוע העיבוד של HotPDF משאיר בלתי-צבוע בעת קריאת PDF בחזרה. שני מסלולי קוד לא קשורים נוחתים על אותו גבול: הצללות רשת הן הפער הן בצד הכתיבה והן בצד הקריאה, ותרשים מקור שמשתמש במשולשי Gouraud לזוהר רדיאלי חלק חוזר לברשת מוצקה אחרונה כלשהי, לא לקירוב מעובד
מברשות משטח (hatch) הופכות לתבניות ריצוף, לא לאפור שטוח
מברשת משטח (hatch) של GDI שומרת על המרקם שלה ב-PDF משום ש-THPDFWmf.SetBrushColor בודקת את CurrentBrush.lbStyle עבור BS_HATCHED עוד לפני שהיא חוזרת למילוי שטוח, ומנתבת את המקרה הזה אל SetHatchBrushPattern במקום. השיטה הזו כותבת זרם תוכן PDF ביחידות 8x8 של אופרטורי קו משורטט, m, l, ו-S, שנבחרים לפי סגנון ה-hatch של 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 או שרטוט הנדסי שמסתמכים על מילויי hatch כדי להבחין בין חומרים שומרים על השפה החזותית הזו ב-PDF במקום לאבד כל אזור לאפור זהה
לא כל מברשת זוכה ליחס הזה, ושווה להכיר את הפער לפני שייבוא CAD יוצא לדרך. EMR_CREATEDIBPATTERNBRUSHPT, הרשומה עבור מברשת תבנית תמונת-ביטמאפ מותאמת אישית ולא אחד משישה סגנונות ה-hatch הקבועים של GDI, רק רושמת את הידית שלה כך שרשומות SELECTOBJECT ו-DELETEOBJECT מאוחרות יותר יישארו עקביות; HotPDF עדיין לא חושף צינור משאב Pattern של PDF עבור תמונות ריצוף שרירותיות, כך שבחירת המברשת הזו נופלת לחלופת צבע-שטוח במקום למרקם המקור. אם מילוי נראה שטוח במקום שבו המקור השתמש בבירור במרקם תמונה חוזרת, מברשת המקור כמעט בוודאות היא תבנית DIB מותאמת אישית ולא hatch תקני, וזה המקרה היחיד ששווה לבדוק ידנית קודם. הגדרת ייבוא עבור שרטוט כזה עדיין עוברת דרך אותו אובייקט אפשרויות:
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;
מה מונע מ-metafile פגום להשחית את העמוד?
התשובה של HotPDF היא שער יחיד בראש ExecuteRecord במקום בדיקת הגנה שחוזרת על עצמה בכל אחת מכשמונים פונקציות הטיפול ברשומות שלה בקירוב. סוגר נתיב GDI, שנפתח על ידי EMR_BEGINPATH ונסגר על ידי EMR_ENDPATH או EMR_ABORTPATH, מנוטר על ידי מאפיין פרטי בשם PathContinue הנתמך על ידי השדה FPathContinue. כל עוד הסוגר הזה פתוח, ExecuteRecord מאפשרת מעבר רק לרשומות בניית-נתיב, גרסאות ה-move, line, polyline, polygon, polybezier ו-polydraw, בתוספת CLOSEFIGURE ומספר קטן של רשומות טרנספורמציה ומצב-DC כמו SETWORLDTRANSFORM, SAVEDC, ו-RESTOREDC. כל סוג רשומה אחר שמגיע אל ExecuteRecord בעוד הסוגר פתוח, EXTTEXTOUT תועה או blit ביטמאפ לדוגמה, נזרק החוצה באופן מרכזי עם Exit יחיד ברגע שהוא מגיע
השער הזה קיים משום ש-metafile שנכתב ידנית, נוצר על ידי כלי, או פשוט פגום, אינו מובטח להכיל רק את מה שקובץ תקין היה שם בין רשומות הפתיחה והסגירה שלו. רשומת פלט-טקסט שנוחתת בין EMR_BEGINPATH ל-EMR_ENDPATH, ללא שער, הייתה או מזהמת את גיאומטריית הנתיב שבבנייה או פולטת אופרטור הצגת-טקסט של PDF באמצע רצף שאמור להיות בניית-נתיב טהורה, ושני מצבי הכשל הללו הם מהסוג שמתגלה על קלט פגום בודד מכלי צד-שלישי, לא על משהו שחבילת בדיקות רגילה נתקלת בו במקרה. ריכוז הבדיקה ב-ExecuteRecord אומר שפונקציות ה-VEMR* הבודדות לא צריכות כל אחת להתגונן מפני קריאה בזמן הלא נכון; השער מחליט על כך פעם אחת, לפני השיגור (dispatch), במקום שמונים פעם אחריו
הצבת תרשים וקטורי לצד טקסט ותמונות באותו עמוד
עמוד דוח לעיתים רחוקות מכיל רק תרשים, ו-ShowMetafile משתלבת עם שאר אופרטורי העמוד של HotPDF בדיוק כמו כל קריאת ציור אחרת. כותרת שנכתבה עם TextOut, תרשים עמודות ממולא hatch שיובא כ-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, תבניות ההצללה הצירית שהוא רושם עבור מילויי גרדיאנט, ומיפוי תבניות הריצוף עבור מברשות hatch המתוארים כאן, כולם נשלחים כחלק מרכיב HotPDF הסטנדרטי עבור Delphi ו-C++Builder, ספריית VCL ילידית ללא כל תלות ב-DLL חיצוני עבור כל זה