يحوّل PDFlibPas، مكتبة PDF من losLab لـ Delphi، سجلات Poly* في EMF إلى مسارات PDF وفق تعريف كل سجل في [MS-EMF]: فسجل 32-بت مثل EMR_POLYBEZIER يبدأ من النقطة 0، و polylines تبقى مفتوحة وتُرسم بالحد فقط، و PT_CLOSEFIGURE في EMR_POLYDRAW علم لا نوع نقطة، وكل عدد نقاط يُفحص مقابل حجم السجل. وصلت هذه القواعد عبر v3.539.39 و v3.539.41 و v3.539.43. قبلها كان مخطط تقرير قد يخرج من ImportEMFFromFile بإسفين معبّأ مكان خط اتجاه، أو منحنى Bezier منحرف نحو نقطة تحكم خاطئة، أو مخطط مغلق ناقص ضلعه الأخير. ولا شيء من ذلك رفع خطأ، والقواعد تنطبق على أي محوّل Delphi من EMF إلى PDF أو أي محلل لسجلات GDI
لماذا تخطئ سجلات EMF من نوع Poly* في تحويل PDF؟
تخطئ سجلات Poly* لأن كل واحد منها يحمل جزءاً من معناه خارج نقاطه: هل الشكل مفتوح، وهل يبدأ من الموضع الحالي، وأي قلم وفرشاة يسريان، وأين في السجل تبدأ النقاط. فالـ enhanced metafile تسجيل لنداءات GDI على سياق جهاز (device context)، لذا على المحوّل أن يعيد تشغيل حالة ذلك السياق إضافة إلى الإحداثيات. أما PDF فلا يملك سياق جهاز؛ يملك مساراً، ونقطة حالية داخل ذلك المسار، ومعامل رسم يقرر بين حد (S) وتعبئة (f) والاثنين معاً (B). وكل تعارض بين النموذجين يصبح فرقاً في العرض لا يُعلَن
وعائلة Poly* تأتي بعرضين أيضاً. فكل سجل 32-بت مثل EMR_POLYLINE له توأم ذو 16-بت مثل EMR_POLYLINE16 يخزن النقاط كأزواج SmallInt. وغالباً ما يسجل GDI الصيغة المضغوطة حين تتسع كل الإحداثيات، لذا قد تبقى معالجات 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 صعوداً وحدها تُجمَّع بثلاثيات: نقطة تحكم، نقطة تحكم، نقطة نهاية. فسجل بسبع نقاط يرسم مقطعين تكعيبيين: 0 هي البداية، و 1 إلى 3 يشكلان المقطع الأول، و 4 إلى 6 الثاني. معالج 16-بت في PDFlibPas كان يفعل ذلك أصلاً. أما معالج 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. ونوع النقطة هو البايت بعد إخفاء ذلك البت، والعلم يعني أغلق الشكل بعد المقطع الذي ينتهي عند هذه النقطة. وكان معالج PDFlibPas القديم يطابق البايت مع قيم مفردة في جملة case، فنقاط النوعين 3 و 5 لم تطابق شيئاً وتخطّت كلياً. فمستطيل يُرسم بـ PolyDraw فقد ضلعه المغلق، وثلاثية Bezier آخر نقاطها تحمل العلم فقدت تلك النقطة، وهو ما أخرج كل ثلاثية لاحقة عن الإيقاع
منذ v3.539.39 يُقرأ النوع بوصفه Types[i] and not PT_CLOSEFIGURE، ولا يُصدر الإغلاق إلا بعد مقطع كامل: بعد الخط في PT_LINETO المغلق، وبعد النقطة الثالثة من مجموعة Bezier. والملف التالف الذي يضبط العلم على النقطة الأولى أو الثانية من ثلاثية لا يغلق الشكل مبكراً. وصلحان مرتبطان خرجا في الإصدار نفسه:
- كل
PT_MOVETOفيEMR_POLYDRAW16ذي 16-بت كان يعيد بدء المسار كله، فسجل يحمل ثلاثة أشكال أبقى الأخير فقط؛ الآن الحركة الأولى تبدأ المسار والحركات اللاحقة تفتح subpaths - سجل PolyDraw لا يبدأ بـ
PT_MOVETOيبدأ من الموضع الحالي كما يقول تعريف السجل، بدل كتابة معاملlأوcبلاmسابقة
لماذا يجب ألا تُعبَّأ polyline في EMF داخل PDF أبداً؟
لا يجب تعبئة polyline في EMF أبداً لأن EMR_POLYLINE و EMR_POLYPOLYLINE أشكال مفتوحة تُرسم بالقلم فقط، وتعبئة مسار مفتوح في PDF تغلقه ضمنياً. فالـ ISO 32000-1 §8.5.3 تنص أن معاملات التعبئة تغلق أي subpath مفتوح قبل رسمه. فمحوّل يصدر B أو f لـ polyline من ثلاث نقاط يرسم مثلثاً معبّأ بلون الفرشاة الحالي: الإسفين المعبّأ تحت خط اتجاه في مخطط. قبل v3.539.41 كانت PDFlibPas تعبّئ عرضَي polyline بالفرشاة، وكان سجل 32-بت يُغلق صراحة أيضاً. اليوم ينتهي كلا العرضين بالحد فقط، ويُحفظ تمييز GDI: Polygon تغلق وتعبّئ، و Polyline لا تفعل ذلك أبداً
PolylineTo يبدأ من الموضع الحالي
EMR_POLYLINETO يرسم من الموضع الحالي عبر كل نقطة في السجل، ويبقى مفتوحاً، ويترك الموضع الحالي عند آخر نقطة. واحتوى المعالج القديم أيضاً على حالة خاصة تطفئ القلم حين تتشارك أول نقطتان إحداثي y، ولم يُعِد أي شيء تشغيله، ففقد كل سجل لاحق في الملف حده. حالة القلم من شأن EMR_SELECTOBJECT و EMR_CREATEPEN؛ ومعالج سجل رسم لا علاقة له بتغييرها. أُزيلت تلك الحالة الخاصة في 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 مباشرة يقرأ قيم الأعداد كإحداثيات في كل سجل متعدد الخطوط. وكان معالج PDFlibPas القديم يضبط فحص حدوده على التخطيط الخاطئ نفسه، فتُرفض السجلات متعددة الخطوط الصالحة وترسم ذاتها الأحادية لا شيء. ومنذ v3.539.41 تحدد PDFlibPas موقع مصفوفة النقاط من الإزاحة المحسوبة، بالطريقة التي يفعلها معالج PolyPolygon عندها دائماً، وترسم كل polyline بوصفها subpath مفتوحاً خاصاً بها مع حد واحد في النهاية. وفي v3.539.43 حصل التوأم ذو 16-بت على المعاملة نفسها؛ فقد كان يرسم مقطعاً بمقطع، وهو ما كسر وصلات الخطوط وتجاهل NULL_PEN المختار
القلم والفرشاة الافتراضيان، وأقواس المسار
قاعدتا حالة تكملان إصلاحات polyline في v3.539.43:
- سياق جهاز GDI جديد يكون فيه
BLACK_PENوWHITE_BRUSHمختارين أصلاً، فملف metafile يرسم دون أيEMR_SELECTOBJECTيرسم حدوداً سوداء مع ذلك؛ وكان المحوّل يبدأ بلا قلم ولا تعبئة ويكتبn(إنهاء المسار دون رسم شيء) لتلك السجلات - داخل قوس
BeginPath/EndPathلا تستخدمPolylineالموضع الحالي ولا تحدّثه، فيجب أن تفتح subpath جديداً عند نقطتها الأولى بدل الاتصال بالشكل السابق، ولا يجوز رسم شيء حتى يُرسم القوس بحد أو يُعبَّأ
بناء ملف EMF اختباري بـ TMetafileCanvas
أسرع طريقة لفحص محوّل ضد هذه القواعد هي تسجيل النداءات الخطرة الثلاثة في enhanced metafile واحد بـ TMetafileCanvas. الرسم أدناه يسجل المنحنيات بفرشاة مجوّفة ثم يختار فرشاة صلبة صفراء لـ polyline عمداً: فالمحوّل الصحيح يجب أن يتجاهل تلك الفرشاة مع polyline، فأي أصفر في PDF الناتج خلل. و PolyDraw ليس لها غلاف في TCanvas، لذا تُستدعى عبر Windows API بمقبض الـ canvas، مستخدمةً بايتَي نوع 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 مفتوح بفرشاة صلبة مختارة: يُرسم بحد فقط ولا يُغلق
// إلى مثلث أصفر
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-بت. وللوصول إلى معالجات 32-بت تحتاج منتجاً يكتبها، أو سجلات تبنيها يدوياً. والملفات المبنية يدوياً تأتي بفخها الخاص: TMetafile.LoadFromStream في VCL يعامل الدفق بوصفه EMF فقط حين يكون الطول المتبقي أكبر قطعاً من TEnhMetaHeader ذي 108 بايت. فـ EMF مصغّر مكتوب يدوياً بترويسة قصيرة، أو فارغ طوله بالضبط 108 بايت، يُحسب WMF ويرفض برسالة "Metafile is not valid". اكتب دائماً الترويسة كاملة بـ 108 بايت بما فيها حقول الامتداد قبل سجلات الاختبار
استيراد EMF إلى PDF عبر PDFlibPas
تستورد PDFlibPas ملف EMF عبر ImportEMFFromFile أو ImportEMFFromStream، وهما تعيدان معرّف صورة غير صفري عند النجاح و 0 عند الفشل. GeneralOptions = 0 يبقي المسار المتجه الذي تدور حوله هذه المقالة؛ و 1 يحوّل الـ metafile إلى صورة نقطية بدلاً منه. و FontOptions = 1 يضيف خطوط الـ metafile كخطوط TrueType غير مضمّنة. وصيغة الدفق تُرجع الدفق إلى الموضع 0 قبل التحميل، فمرّر دفقاً لا يحوي إلا الـ 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 = صورة نقطية
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);
// الصفحة تستدعي الفورم المستورد فقط: 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 تسلسل الحفظ والتحويل و Do والاستعادة فقط. فمعاملات m و l و c و h و S الناتجة عن سجلات Poly* تسكن في تدفق الـ form XObject، وهو مضغوط. ولتدقيقها فك ضغط الملف المحفوظ في مفحص كائنات PDF واقرأ تدفق الفورم: في الملف الاختباري أعلاه سترى polyline تنتهي بـ S بلا h قبلها، و h عند كل علم إغلاق في أشكال PolyDraw، ولا f أو B على أي من هذه الـ subpaths. كما يكيّف DrawImage حجم EMF المستورد بالتساوي حسب أصغر Width و Height، فيحفظ الرسم نسبة أبعاده حتى لو لم يطابقها الصندوق الذي تمرره
لأهداف Free Pascal، انظر كيف يُبنى مستورد PDFlibPas المتجه لـ EMF تحت Free Pascal؛ ودلالات السجلات نفسها أينما تُجمَّع المكتبة
كيف يعامل محلل EMF أعداد النقاط القادمة من الملف؟
ينبغي لمحلل EMF أن يعامل كل عدد نقاط كدخل غير موثوق وأن يفحصه مقابل حجم السجل قبل نسخ نقطة واحدة. فـ EnumEnhMetaFile يضمن فقط أن nSize لكل سجل يبقى داخل الملف. وهو لا يفحص أن cptl يوافق nSize، فمعالج ينسخ 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: أشكال مفتوحة، حد بـS، أبداًhأوfأوB، لأن تعبئة PDF تغلق الـ subpaths المفتوحةEMR_POLYLINETO: ابدأ من الموضع الحالي، ابقَ مفتوحاً، حدّث الموضع الحالي، ولا تمس حالة القلم أبداً- سجل 32-بت
EMR_POLYPOLYLINE: النقاط تبدأ عند البايت32 + nPolys * 4، لا عندaptl[0] EMR_POLYDRAW: أخفِPT_CLOSEFIGUREقبل التوجيه، أغلق بعد المقطع المكتمل، وابدأ من الموضع الحالي حين لا تكون النقطة الأولىPT_MOVETO- حالة سياق الجهاز الافتراضية هي
BLACK_PENمعWHITE_BRUSH؛ وتحترمها v3.539.43 وما بعدها - داخل
BeginPath/EndPathتفتح كل polyline الـ subpath الخاص بها ولا يُرسم شيء حتى يُستخدم القوس - تحقق من كل
cptl/cptsمقابلnSizeبحساب 64-بت قبل نسخ النقاط - ملفات EMF الاختبارية المبنية يدوياً تحتاج الترويسة كاملة بـ 108 بايت، وإلا قرأها
TMetafile.LoadFromStreamبوصفها WMF
وإن كانت تقاريرك تمر عبر مكوّن مختلف فدلالات السجلات نفسها تسري؛ استيراد HotPDF المتجه لـ EMF و WMF يغطي كيف يحوّل ذلك المكوّن فرشاة التدرج والفرشاة المتشابكة إلى patterns في PDF، و الرسوم المتجهة و shaders والتدرجات في PDFlibPas يغطي رسم الأشكال نفسها مباشرة عبر API المكتبة بدل المرور بـ metafile
PDFlibPas v3.539.43 وما بعدها تتضمن كل قاعدة أعلاه. والتفاصيل وتنزيلات التجربة على صفحة منتج PDFlibPas Delphi PDF library