PDFlibPas, ספריית ה-PDF של losLab ל-Delphi, ממירה את רשומות ה-Poly* של EMF לנתיבי PDF לפי הגדרת כל רשומה ב-[MS-EMF]: EMR_POLYBEZIER בן 32 ביט מתחיל בנקודה 0, polylines נשארות פתוחות ומקבלות stroke בלבד, PT_CLOSEFIGURE בתוך EMR_POLYDRAW הוא דגל, וכל מניין נקודות נבדק מול גודל הרשומה. הכללים האלה נחתו על פני v3.539.39, v3.539.41 ו-v3.539.43. לפני כן, תרשים בדוח יכול היה לצאת מ-ImportEMFFromFile עם טריז ממולא במקום קו מגמה, עקמת Bezier שמתעקמת לעבר נקודת השליטה הלא נכונה, או קו מתאר סגור שחסרה לו הצלעה האחרונה. אף אחד מאלה לא העלה שגיאה, והכללים תקפים לכל ממיר EMF ל-PDF ב-Delphi ולכל פרסר של רשומות GDI
למה רשומות ה-Poly* של EMF משתבשות בהמרה ל-PDF?
רשומות ה-Poly* של EMF משתבשות כי כל אחת מהן נושאת חלק מהמשמעות שלה מחוץ לנקודות שלה: האם הצורה פתוחה, האם היא מתחילה במיקום הנוכחי, איזה עט ואיזו מברשת חלים, ואיפה ברשומה מתחילות הנקודות. enhanced metafile הוא הקלטה של קריאות GDI מול device context, ולכן ממיר צריך לשחזר גם את מצב אותו device context וגם את הקואורדינטות. ל-PDF אין device context. יש בו נתיב, נקודה נוכחית בתוך הנתיב, ואופרטור ציור שמכריע בין stroke (S), fill (f) ושניהם (B). כל אי-התאמה בין שני המודלים הופכת להבדל רינדור שקט
משפחת ה-Poly* מגיעה גם בשני רוחבים. לכל רשומה בת 32 ביט כמו EMR_POLYLINE יש תאומה בת 16 ביט כמו EMR_POLYLINE16 שמאחסנת נקודות כזוגות SmallInt. GDI בדרך כלל מקליטה את הצורה הקומפקטית כשכל קואורדינטה נכנסת, ולכן ה-handlers בני ה-32 ביט של ממיר יכולים להישאר שגויים שנים בזמן שציורי בדיקה יומיומיים לעולם לא מגיעים אליהם. הביקורת המהירה ביותר היא להזרים את אותן נקודות דרך שתי הרשומות ולהשוות את הנתיבים שיוצאים. הרשומות שמכוסות כאן כולן בקבוצת רשומות הציור של [MS-EMF] (2.3.5 Drawing Record Types)
| רשומה | מתחילה ב | סגורה? | מיקום נוכחי |
|---|---|---|---|
EMR_POLYBEZIER | נקודה 0 | לא | לא בשימוש, לא מתעדכן |
EMR_POLYLINE | נקודה 0 | לא (עט בלבד) | לא בשימוש, לא מתעדכן |
EMR_POLYLINETO | המיקום הנוכחי | לא (עט בלבד) | בשימוש ומתעדכן |
EMR_POLYPOLYLINE | הנקודה הראשונה של כל polyline | לא (עט בלבד) | לא בשימוש, לא מתעדכן |
EMR_POLYDRAW | ה-PT_MOVETO הראשון, או המיקום הנוכחי | רק היכן ש-PT_CLOSEFIGURE מוגדר | בשימוש ומתעדכן |
מאיפה עקמת EMR_POLYBEZIER באמת מתחילה?
עקמת EMR_POLYBEZIER מתחילה בנקודה 0, ורק הנקודות מאינדקס 1 והלאה מקובצות בשלשות של נקודת שליטה, נקודת שליטה, נקודת סיום. רשומה עם 7 נקודות מציירת אפוא שני מקטעים קוביים: 0 הוא ההתחלה, 1 עד 3 מרכיבים את המקטע הראשון, 4 עד 6 את השני. ה-handler בן ה-16 ביט ב-PDFlibPas כבר עשה את זה. ה-handler בן ה-32 ביט התחיל לקבץ מנקודה 0, ולכן נקודת ההתחלה נבלעה כנקודת השליטה הראשונה וכל מקטע מאוחר יותר הוסט באחד. העקמת עדיין התרנדרה, רק לא זו הנכונה. מאז v3.539.41 שני הרוחבים פותחים את הנתיב עם m בנקודה 0 ופולטים c אחד לכל שלשה שלמה אחריה
לפרסר שלכם: מניין שאינו 1 בתוספת כפולה של 3 פירושו קלט פגום, ואת הנקודות העודפות כדאי להתעלם מהן ולא לתפור אותן לתוך עקמת
PolyDraw: PT_CLOSEFIGURE הוא דגל, לא סוג נקודה
בתוך EMR_POLYDRAW, PT_CLOSEFIGURE (ערך 1) הוא ביט שמשולב עם PT_LINETO (2) או PT_BEZIERTO (4), ולכן בייט סוג תקין יכול להיות 3 או 5. סוג הנקודה הוא הבייט כשהביט הזה ממוסך ממנו, והדגל אומר: סגרו את הצורה אחרי המקטע שנגמר בנקודה הזאת. ה-handler הישן של PDFlibPas השווה את הבייט מול ערכים בודדים בפקודת case, ולכן נקודות מסוג 3 ו-5 לא התאימו לכלום ודולגו כליל. מלבן שצויר עם PolyDraw איבד את הצלעה הסוגרת שלו, ושלשת Bezier שהנקודה האחרונה שלה נשאה את הדגל איבדה את הנקודה הזאת, מה שהוציא את כל השלשות שאחר כך מהקצב
מאז v3.539.39 הסוג נקרא כ-Types[i] and not PT_CLOSEFIGURE, והסגירה נפלטת רק אחרי מקטע שלם: אחרי הקו עבור PT_LINETO סגור, ואחרי הנקודה השלישית של קבוצת Bezier. קובץ פגום שמגדיר את הדגל על הנקודה הראשונה או השנייה של שלשה לא סוגר את הצורה מוקדם מדי. שני תיקונים נלווים יצאו באותה מהדורה:
- כל
PT_MOVETOבתוךEMR_POLYDRAW16בן ה-16 ביט התחיל מחדש את כל הנתיב, ולכן רשומה שהכילה שלוש צורות שמרה רק על האחרונה; עכשיו המעבר הראשון פותח את הנתיב ומעברים מאוחרים פותחים תתי-נתיב - רשומת PolyDraw שלא מתחילה ב-
PT_MOVETOמתחילה במיקום הנוכחי, כפי שהגדרת הרשומה אומרת, במקום לכתוב אופרטורlאוcבליmשלפניו
למה polyline של EMF חייב להישאר ללא מילוי ב-PDF?
polyline של EMF אסור שתמולא כי EMR_POLYLINE ו-EMR_POLYPOLYLINE הן צורות פתוחות שמצוירות בעט בלבד, ומילוי של נתיב פתוח ב-PDF סוגר אותו בעקיפין. ISO 32000-1 §8.5.3 קובע שאופרטורי המילוי סוגרים כל תת-נתיב פתוח לפני שהם מציירים אותו. ממיר שפולט B או f עבור polyline בת שלוש נקודות מצייר אפוא משולש ממולא בצבע המברשת הנוכחית: הטריז הממולא מתחת לקו המגמה בתרשים. לפני v3.539.41, PDFlibPas מילאה את שני רוחבי ה-polyline במברשת, והרשומה בת ה-32 ביט גם נסגרה במפורש. היום שני הרוחבים מסתיימים ב-stroke בלבד, וההבחנה של GDI נשמרת: Polygon סוגר וממלא, Polyline לעולם לא
PolylineTo מתחיל במיקום הנוכחי
EMR_POLYLINETO מצייר מהמיקום הנוכחי דרך כל נקודה ברשומה, נשאר פתוח, ומשאיר את המיקום הנוכחי בנקודה האחרונה. ה-handler הישן הכיל גם מקרה פרטי שכיבה את העט כששתי הנקודות הראשונות חולקות קואורדינטת y, ושום דבר לא הדליק אותו בחזרה, כך שכל רשומה מאוחרת בקובץ איבדה את קו המתאר שלה. מצב העט שייך ל-EMR_SELECTOBJECT ול-EMR_CREATEPEN; ל-handler של רשומת ציור אין עסק בשינוי שלו. מקרה הפרטי הזה הוסר ב-v3.539.41, והצורה בת הנקודה האחת של הרשומה כבר לא קוראת מעבר לנקודות של עצמה (תוקן ב-v3.539.39)
הנקודות של PolyPolyline מתחילות אחרי מערך המניינים
EMR_POLYPOLYLINE בן ה-32 ביט מאחסן nPolys מניינים ואז cptl נקודות, והנקודות מתחילות בהיסט בייטים של 32 + nPolys * 4. המלכודת היא ב-RTL: היחידה Windows מצהירה על TEMRPolyPolyline עם aPolyCounts ו-aptl בתור מערכים של אלמנט אחד, ולכן aptl[0] היא הנקודה הראשונה רק כש-nPolys שווה 1. קוד שמאנדקס ישירות את aptl קורא ערכי מניין כקואורדינטות בכל רשומה מרובת קווים. ה-handler הישן של PDFlibPas גם ביסס את בדיקת הגבולות שלו על הפריסה השגויה הזאת, ולכן רשומות תקינות מרובות קווים נדחו ואלה של קו אחד לא ציירו כלום. מאז v3.539.41 PDFlibPas מאתרת את מערך הנקודות מההיסט המחושב, בדיוק כפי שה-handler של PolyPolygon שלה עשה תמיד, ומציירת כל polyline בתור תת-נתיב פתוח משלו עם stroke אחד בסוף. ב-v3.539.43 התאומה בת ה-16 ביט קיבלה את אותו טיפול; היא ציירה מקטע-מקטע, מה ששבר חיבורי קווים והתעלם מ-NULL_PEN נבחר
עט ומברשת ברירת המחדל, וסוגרי הנתיב
שני כללי מצב משלימים את תיקוני ה-polyline ב-v3.539.43:
- device context טרי של GDI כבר בוחר ב-
BLACK_PENוב-WHITE_BRUSH, ולכן metafile שמצייר בלי שוםEMR_SELECTOBJECTעדיין מצייר קווי מתאר שחורים; הממיר נהג להתחיל בלי עט ובלי מילוי ולכתובn(סיום נתיב, בלי ציור) עבור רשומות כאלה - בתוך סוגריים של
BeginPath/EndPath,Polylineלא משתמשת במיקום הנוכחי ולא מעדכנת אותו, ולכן היא חייבת לפתוח תת-נתיב חדש בנקודה הראשונה שלה במקום להתחבר לצורה הקודמת, ושום דבר לא רשאי להיצייר עד שהסוגריים מקבלים stroke או מילוי
בניית קובץ EMF לבדיקה עם TMetafileCanvas
הדרך המהירה ביותר לבדוק ממיר מול הכללים האלה היא להקליט את שלוש הקריאות המסוכנות לתוך enhanced metafile אחד עם TMetafileCanvas. הציור להלן מקליט את העקמות עם מברשת חלולה ואז בוחר בכוונה מברשת מוצקה צהובה עבור ה-polyline: ממיר נכון חייב להתעלם מהמברשת הזאת עבור ה-polyline, ולכן כל צהוב ב-PDF שיוצא הוא באג. ל-PolyDraw אין wrapper של TCanvas, ולכן קוראים לה דרך ה-Windows API עם handle של הקנבס, תוך שימוש בבייטי סוג 3 ו-5 כדי לממש את דגל הסגירה
uses
Winapi.Windows, System.Types, Vcl.Graphics;
procedure BuildPolyTestEmf(const FileName: string);
const
// ריבוע סגור (3 = LINETO + CLOSEFIGURE), ואחריו צורת Bezier סגורה
// שהשלשת השליטה האחרונה שלה מסתיימת עם 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 פתוחה עם מברשת מוצקה נבחרת: מקבלת stroke, לעולם לא נסגרת
// למשולש צהוב
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 ביט. כדי להגיע ל-handlers בני ה-32 ביט צריך מפיק שכותב אותן, או רשומות שבונים ביד. קבצים שנבנים ביד מגיעים עם מלכודת משלהם: TMetafile.LoadFromStream של VCL מתייחס ל-stream בתור EMF רק כשהאורך הנותר גדול ממש מ-108 הבייטים של TEnhMetaHeader. EMF שנכתב ביד בצורה מזערית עם כותרת קצרה, או כזה ריק שאורכו בדיוק 108 בייטים, מתקבל בטעות בתור WMF ונדחה עם "Metafile is not valid". תמיד כתבו את הכותרת המלאה של 108 הבייטים, כולל שדות ההרחבה, לפני רשומות הבדיקה שלכם
ייבוא ה-EMF לתוך PDF עם PDFlibPas
PDFlibPas מייבאת EMF עם ImportEMFFromFile או ImportEMFFromStream, שמחזירות מזהה תמונה שאינו אפס בהצלחה ו-0 בכישלון. GeneralOptions = 0 משאיר את נתיב הווקטור שהמאמר הזה עוסק בו; 1 ממיר במקום זאת את ה-metafile ל-bitmap. FontOptions = 1 מוסיף את גופני ה-metafile כגופני TrueType לא מוטמעים. גרסת ה-stream מחזירה את ה-stream למיקום 0 לפני הטעינה, ולכן העבירו stream שמכיל רק את ה-metafile
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 = bitmap
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);
// העמוד רק מפעיל את ה-form המיובא: 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* חיים ב-stream של ה-form XObject, שדחוס. כדי לבקר אותם, פרקו את דחיסת הקובץ השמור ב-inspector של אובייקטי PDF וקראו את ה-stream של ה-form: בקובץ הבדיקה שלמעלה אמורים לראות את ה-polyline מסתיים ב-S בלי h לפניו, h אחד ליד כל דגל סגירה בצורות ה-PolyDraw, ואף f או B על אף אחד מתתי-הנתיב האלה. DrawImage גם מותח EMF מיובא באופן אחיד לפי הקטן מבין Width ו-Height, כך שהציור שומר על יחס הממדים שלו גם כשהתיבה שמעבירים לא תואמת אותו
עבור יעדי Free Pascal, ראו איך מייבא ה-EMF הווקטורי של PDFlibPas מתקמפל תחת Free Pascal; סמנטיקת הרשומות זהה בכל מקום שבו המייבא מתקמפל
איך פרסר EMF צריך להתייחס למנייני נקודות שמגיעים מהקובץ?
פרסר EMF צריך להתייחס לכל מניין נקודות בתור קלט לא מהימן ולבדוק אותו מול גודל הרשומה לפני העתקת נקודה אחת. EnumEnhMetaFile מבטיח רק ש-nSize של כל רשומה נשאר בתוך הקובץ. הוא לא בודק ש-cptl תואם ל-nSize, ולכן handler שמעתיק cptl נקודות עם Move יקרא את הרשומות הבאות, או מעבר לסוף ה-metafile, כשהמניין מזויף או פגום. מאז v3.539.39 PDFlibPas בודקת מול nSize את הכותרת הקבועה בתוספת מניין כפול בייטים לנקודה עבור PolyDraw, PolyBezier, PolyBezierTo, Polyline, PolylineTo ו-Polygon בשני הרוחבים, עם בייט נוסף אחד לנקודה עבור בייטי הסוג של PolyDraw. עבור רשומות ה-PolyPoly המניינים לכל צורה חייבים גם להצטבר ללא יותר מהסך המוצהר, וצורות בנות אפס נקודות מדולגות
אותה בדיקה קצרה מספיק בשביל להעתיק אותה לפרסר שלכם. הגרסה הזאת מאמתת EMR_POLYPOLYLINE בן 32 ביט ומחזירה מצביע למערך הנקודות האמיתי שלו:
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] מפעיל בדיקות טווח
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 ביט יכולים להתגלגל ולעבור את ההשוואה
עזר זריז: כללי ה-Poly* של EMF להמרת EMF ל-PDF
EMR_POLYBEZIER: נקודה 0 היא נקודת ההתחלה; מקבצים מנקודה 1 בשלשות; תוקן עבור הרשומה בת ה-32 ביט ב-v3.539.41EMR_POLYLINE/EMR_POLYPOLYLINE: צורות פתוחות, stroke עםS, לעולם לאh,fאוB, כי מילוי ב-PDF סוגר תתי-נתיב פתוחיםEMR_POLYLINETO: מתחיל במיקום הנוכחי, נשאר פתוח, מעדכן את המיקום הנוכחי, לעולם לא נוגע במצב העטEMR_POLYPOLYLINEבן 32 ביט: הנקודות מתחילות בבייט32 + nPolys * 4, לא ב-aptl[0]EMR_POLYDRAW: מסכים אתPT_CLOSEFIGUREלפני הניתוב, סוגר אחרי המקטע שהושלם, מתחיל במיקום הנוכחי כשהנקודה הראשונה אינהPT_MOVETO- מצב ברירת המחדל של ה-device context הוא
BLACK_PENבתוספתWHITE_BRUSH; v3.539.43 ואילך מכבדים אותו - בתוך
BeginPath/EndPath, כל polyline פותחת תת-נתיב משלה ושום דבר לא מצויר עד שהסוגריים נצרכים - מאמתים כל
cptl/cptsמולnSizeבחשבון 64 ביט לפני העתקת נקודות - קבצי EMF לבדיקה שנבנים ביד זקוקים לכותרת המלאה של 108 הבייטים, אחרת
TMetafile.LoadFromStreamקורא אותם בתור WMF
אם הדוחות שלכם עוברים דרך רכיב אחר, אותה סמנטיקת רשומות בתוקף; ייבוא וקטורי של EMF ו-WMF ב-HotPDF מסביר איך הרכיב הזה הופך מברשות gradient ו-hatch ל-patterns של PDF, ו-גרפיקה וקטורית, shaders ו-gradientים ב-PDFlibPas מסביר לצייר את אותן צורות ישירות עם ה-API של הספרייה במקום דרך metafile
PDFlibPas v3.539.43 ומעלה כוללת את כל הכללים שלמעלה. פרטים והורדות ניסיון נמצאים בעמוד המוצר של PDFlibPas Delphi PDF library