مقال تقني

رسومات المتجهات في PDF مع Delphi: المسارات والتدرجات

معظم كود Delphi الذي يمس PDF يعامل التنسيق كحاوية لشيئين: تشغيلات النص (runs of text) وعدد قليل من الصور النقطية الموضوعة. هذا الرأي صحيح إلى حد ما، وهو يترك الجزء الأكثر قدرة من التنسيق غير مستخدم. صفحة PDF عبارة عن قماش ثنائي الأبعاد مستقل عن الدقة (resolution-independent) مبني على نفس نموذج التصوير مثل PostScript. يمكنها رسم خطوط، ومنحنيات، ومناطق معبأة، وتدرجات، وأنماط متكررة، كلها كمتجهات (vectors) تظل حادة عند أي تكبير وتطبع بدقة الجهاز الكاملة. إذا كنت ترسم شعاراً، أو مخططاً، أو علامة مائية، أو حد شهادة، فإن مسار المتجه (vector path) يكون دائماً تقريباً هو البدائي (primitive) الصحيح، وهو أصغر وأكثر وضوحاً من الصورة النقطية (rasterized image) التي تلجأ إليها العديد من البرامج بدلاً من ذلك

يتناول هذا المقال نموذج المتجه كما يحدده ISO 32000-1 ويظهر استدعاءات PDF Library for Delphi المطابقة. الهدف هو جعل المواصفات ملموسة، لأن واجهة برمجة التطبيقات (API) ترتبط بها بشكل وثيق، وفهم أحدهما يعلمك الآخر

الصفحة عبارة عن آلة مسار

يصف ISO 32000-1 §8.5 الرسومات في مرحلتين لا تتداخلان أبداً. أولاً تقوم بإنشاء مسار، وهو هندسة بحتة بلا نتيجة مرئية. ثم ترسم ذلك المسار في عملية واحدة تحدد مخططه التفصيلي (strokes its outline)، أو تملأ داخله، أو تقوم بكلا الأمرين. لا شيء يظهر على الصفحة أثناء الإنشاء. المسار عبارة عن تسلسل مجرد من النقاط والشرائح يتم الاحتفاظ بها في حالة الرسومات حتى يستهلكه عامل الرسم، وفي هذه النقطة يتم عرضه (rendered) والتخلص منه

PDF Library for Delphi: مخطط دورة حياة مسار PDF يوضح البناء كهندسة صرفة غير مرئية يليها معامل تلوين يرسم حدودًا أو يعبئ أو كلاهما، ثم يستهلك المسار ويتخلص منه
بناء الرباعي لا يرسم شيئاً — يقرأ استدعاء DrawPath حالة الرسوميات مرة واحدة، ويصيّر حدوداً أو تعبئة أو كليهما، ثم يتخلص من المسار

يتكون المسار من مسار فرعي (subpath) واحد أو أكثر. يبدأ المسار الفرعي عند نقطة وينمو عن طريق إلحاق شرائح: خطوط مستقيمة، ومنحنيات Bezier التكعيبية، وفي بعض المنصات مستطيلات كاملة مضافة كمسار فرعي مغلق خاص بها. في PDF Library for Delphi، تفتح مساراً باستخدام StartPath، والذي يعين نقطة البداية، ثم تقوم بتوسيعه باستخدام AddLineToPath و AddCurveToPath. كل استدعاء يقدم نقطة حالية ضمنية، بحيث تستمر الشريحة التالية من حيث انتهت الشريحة الأخيرة. يرسم ClosePath شريحة مستقيمة نهائية تعود إلى بداية المسار الفرعي، وهو أمر مهم للتخطيط (stroking) لأنه ينتج تقاطع خط حقيقي (real line join) عند قمة الإغلاق بدلاً من قبعتي نهاية سائبتين (loose end caps)

// رباعي مغلق، يُرسم stroke ثم fill
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // افتح path عند vertex الأول
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // مقطع مستقيم يعود إلى (150, 100)
PDF.DrawPath(2);                   // 2 = fill وstroke؛ يُستهلك path

تستخدم المنحنيات AddCurveToPath، والذي يأخذ نقطتي تحكم Bezier ونقطة نهاية: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). يمتد المنحنى من النقطة الحالية إلى (EndX, EndY)، مسحوباً نحو نقطتي التحكم على طول الطريق. الأقواس الدائرية (Circular arcs) متوفرة من خلال AddArcToPath(CenterX, CenterY, TotalAngle)، حيث يتم أخذ نصف القطر من المسافة بين النقطة الحالية والمركز، ويصدر المحرك القوس كسلسلة من شرائح Bezier. تحتوي المستطيلات على اختصار، AddBoxToPath(Left, Top, Width, Height)، والذي يلحق مستطيلاً مغلقاً كاملاً كمسار فرعي خاص به دون StartPath سابق

قاعدتا تعبئة، ولماذا تختلفان

عندما تملأ مساراً يتقاطع مع نفسه أو يحتوي على حلقة داخلية، يحتاج العارض (renderer) إلى قاعدة لتحديد أي المناطق تكون داخل الشكل وأيها تكون ثقوباً. يحدد ISO 32000-1 §8.5.3.3 قاعدتين، ويمكنهما رسم نفس الهندسة بشكل مختلف. تحسب قاعدة اللف غير الصفري (nonzero winding rule) التقاطعات الموقعة لشعاع ملقى من نقطة اختبار إلى ما لا نهاية، وتضيف واحداً لكل شريحة تتقاطع من اليسار إلى اليمين وتطرح واحداً لكل شريحة تتقاطع بالطريقة الأخرى؛ وتكون النقطة في الداخل عندما لا يكون الإجمالي صفراً. تتجاهل قاعدة الزوجي والفردي (even-odd rule) الاتجاه وتحسب ببساطة التقاطعات، معتبرة أن النقطة في الداخل عندما يكون العدد فردياً

الحالة الكلاسيكية التي يتباعدان فيها هي شكل به ثقب، دونات أو غسالة. ارسم حداً خارجياً وحداً داخلياً بداخله. بموجب قاعدة الزوجي والفردي (even-odd rule)، تقطع الحلقة الداخلية ثقباً دائماً، لأن أي نقطة بين الحدين يتم عبورها مرة واحدة وأي نقطة داخل الحلقة الداخلية يتم عبورها مرتين. بموجب قاعدة اللف غير الصفري (nonzero winding rule)، يظهر الثقب فقط إذا كانت الحلقة الداخلية تلتف في الاتجاه المعاكس للحلقة الخارجية؛ إذا قمت بلفهما بنفس الطريقة فإن اللفات تعزز بدلاً من الإلغاء، وتملأ المنطقة الداخلية بشكل صلب. النجمة الخماسية المرسومة كمخطط تفصيلي واحد يتقاطع مع نفسه تُظهر الانقسام نفسه: يترك الزوجي والفردي البنتاغون الأوسط فارغاً بينما يملأه اللف غير الصفري

يختار PDF Library for Delphi القاعدة من خلال الاستدعاء الذي تجريه للرسم، وليس عن طريق علامة. يملأ DrawPath بقاعدة اللف غير الصفري؛ يملأ DrawPathEvenOdd بقاعدة الزوجي والفردي. كلاهما يأخذ نفس وضع العدد الصحيح: 0 يخطط (strokes) المخطط التفصيلي فقط، و 1 يملأ فقط، و 2 يملأ ويخطط. قاعدة الزوجي والفردي هي الأداة الأسهل لثقوب القطع على وجه التحديد لأنها لا تتطلب منك إدارة اتجاه المسار الفرعي

PDF Library for Delphi: مستطيلات متداخلة تُملأ وفق قاعدتَي even-odd و nonzero winding: تنحت even-odd المستطيل الداخلي كثقب بينما تمتلئ nonzero winding صلبة ما لم يلتف الحلقة الداخلية عكس الخارجية
هندسة الحلقة نفسها تحت جوابين قانونيين — يتجاهل DrawPathEvenOdd اتجاه اللف ويحفر الثقب دائماً، بينما يجمع DrawPath لفّات مُعلَّمة الإشارة تتقوى حين تتشارك المسارات الفرعية الاتجاه
// الصندوقان نفسيهما، وقاعدتا fill، ونتيجتان مختلفتان
// Nonzero winding: يدور الصندوقان في الاتجاه نفسه، لذلك فإن الداخلي
// لا يقتطع ثقبًا، ويمتلئ الصندوق الخارجي بالكامل
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120);   // outer
PDF.AddBoxToPath(140, 130, 120,  60);   // inner
PDF.DrawPath(1);                         // 1 = fill, nonzero winding

// Even-odd: يُعبر الصندوق الداخلي عددًا زوجيًا من المرات،
// لذلك يثقب فتحة مستطيلة نظيفة في الصندوق الخارجي
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120);   // outer
PDF.AddBoxToPath(140, 330, 120,  60);   // inner cut-out
PDF.DrawPathEvenOdd(1);                  // 1 = fill, even-odd

تغير التدرجات المحورية اللون على طول خط

لون التعبئة المسطح هو قيمة واحدة عبر المنطقة بأكملها. يغير التدرج اللون بشكل مستمر، وأبسط نوع هو التدرج المحوري (axial)، أو الخطي (linear). يحدده ISO 32000-1 §8.7.4.5 كتظليل محوري من النوع 2: أنت تعطي نقطتين تحددان محوراً، ولون بداية عند النقطة الأولى ولون نهاية عند الثانية، ويقوم العارض (renderer) باستيفاء (interpolates) اللون على طول ذلك المحور. كل نقطة في المنطقة المعبأة تأخذ لون إسقاطها العمودي على المحور، لذا يمتد التدرج في نطاقات بزوايا قائمة على الخط بين النقطتين

في PDF Library for Delphi، التدرج هو مورد مستند مسمى تقوم بإنشائه مرة واحدة ثم تختاره كطلاء نشط. يسجله NewRGBAxialShader. التوقيع هو NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): نقطتا نهاية المحور، وثلاثيات RGB في كل نهاية كقيم في النطاق 0 إلى 1، وعلامة Extend. مع تعيين Extend إلى 1، تستمر ألوان النهاية كتعبئة صلبة خارج نقاط نهاية المحور، وهو ما تريده عادةً حتى لا تُترك زوايا المنطقة خارج المحور غير مطلية؛ 0 يتركها دون تغيير. بمجرد وجود التظليل (shader)، تقوم بربطه باستخدام SetFillShader للمناطق المعبأة، أو SetLineShader للمخططات التفصيلية المخططة (stroked)، أو SetTextShader للنص. يظل الربط نشطاً لاستدعاءات الرسم التي تلي ذلك، بحيث يأخذ المسار الذي ترسمه بعد ذلك التدرج بدلاً من لون مسطح

PDF Library for Delphi: تشريح تدرج RGB محوري يوضح أشرطة الألوان المتعامدة مع محور التظليل، والإسقاط على المحور، وتمديد Extend للونَي النهايتين كتعبئة صلبة بعد نقطتي النهاية
يستوفي المظلِّل المحوري على طول المحور بين لونَي الطرفين، وكل نقطة تأخذ عينة إسقاطها العمودي على ذلك الخط — ويُبقي Extend = 1 ألوان النهايات جارية ما بعد الطرفين فتبقى الزوايا مصبوغة
// عرّف gradient رأسيًا مرة واحدة: أزرق في الأسفل إلى أبيض في الأعلى
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // نقطة البدء وRGB البدء
  0, 260,   1.00, 1.00, 1.00,    // نقطة النهاية وRGB النهاية
  1);                            // 1 = مدّ النهايات بلون مصمت

// حدّد gradient كـ fill، ثم ارسم مستطيلًا به
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = fill، وقد امتلأ الآن بواسطة shader

المحور هنا رأسي، من y=100 إلى y=260 عند x ثابتة، لذا تمتد نطاقات الألوان أفقياً ويتلاشى المستطيل من الأزرق في قاعدته إلى الأبيض في الأعلى. نظراً لأن التظليل (shader) مرتبط باسم، يمكن لتعريف واحد أن يملأ أي عدد من الأشكال على الصفحة، والعودة إلى لون مسطح هي مجرد استدعاء SetFillColor آخر قبل المسار التالي

أنماط التجانب (Tiling patterns) تكرر خلية

حيثما يغير التدرج لوناً واحداً بسلاسة، يكرر نمط التجانب قطعة فنية صغيرة عبر منطقة ما. يحدد ISO 32000-1 §8.7.3.1 نمط التجانب كخلية نمط، قطعة مستقلة من المحتوى، يقوم العارض بتكرارها على شبكة ثابتة لتبليط المنطقة الجاري طلاؤها. هذه هي الطريقة التي تبني بها تظليلاً (hatching) لتعبئة هندسية، أو شكل علامة تجارية متكرر خلف ترويسة، أو خلفية مزخرفة تظل حادة المتجهات (vector-sharp) ولا تزن شيئاً تقريباً بغض النظر عن حجم المنطقة، لأن الخلية تُخزن مرة واحدة ويُشار إليها في كل مكان

يبني PDF Library for Delphi خلية النمط من محتوى الصفحة الملتقط. تلتقط صفحة أو منطقة باستخدام CapturePage، وتحول الالتقاط إلى نمط مسمى باستخدام NewTilingPatternFromCapturedPage(PatternName, CaptureID)، ثم تختار هذا النمط كتعبئة حالية باستخدام SetFillTilingPattern(PatternName). من هذه النقطة فصاعداً، يُطلى أي مسار تملؤه بالخلية المتكررة بدلاً من لون مسطح، تماماً كما تعمل تعبئة التظليل ولكن مع خلية متجانبة كمصدر للطلاء. التسلسل أكثر تعقيداً من استدعاء واحد، لذلك إذا كانت خطوة الالتقاط غير مألوفة، فتعامل مع النمط كعملية من مرحلتين: أنتج الخلية الملتقطة أولاً، ثم اربطها كتعبئة بالاسم قبل رسم المنطقة التي تريد تجانبها

تجميع الأساسيات معاً

تُركب القطع مباشرة. كتلة Bezier المعبأة هي مسار من المنحنيات المطلية باستخدام DrawPath. يُظهر المخطط التفصيلي نفسه المطلي باستخدام DrawPathEvenOdd بعد إضافة حلقة داخلية ثقباً كانت التعبئة المتعرجة (winding fill) ستغلقه. المستطيل المعبأ بالتدرج هو مربع مرتبط بتظليل. يرسم المثال أدناه الثلاثة بالتسلسل بحيث يكون الفرق بين قاعدتي التعبئة مرئياً في صفحة واحدة، ثم يضع لوحة تدرج تحتها

// 1. شكل Bezier مملوء (nonzero winding)
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480);   // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480);   // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1);                                     // 1 = fill

// 2. المخطط نفسه مع حلقة داخلية، مملوء بـ even-odd لإظهار ثقب
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300);                              // new subpath: the hole
PDF.AddArcToPath(200, 300, 360);                     // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1);                              // تم اقتطاع الثقب

// 3. مستطيل مملوء بـ axial gradient
PDF.NewRGBAxialShader('footerGrad',
  60, 100,  0.95, 0.55, 0.10,
  60, 200,  0.20, 0.10, 0.40,
  1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);

تفصيلان يستحقان التمسك بهما. يقرر استدعاء الرسم قاعدة التعبئة، لذا فإن الاختيار بين DrawPath و DrawPathEvenOdd هو الاختيار بين اللف غير الصفري (nonzero winding) والزوجي والفردي (even-odd)، وبالنسبة للأشكال التي بها ثقوب، توفر لك قاعدة الزوجي والفردي من التفكير في اتجاه المسار الفرعي. ويتم أخذ عينات من حالة الرسومات في لحظة الرسم: اضبط الألوان وعرض الخط وربط التظليل (shader binding) قبل استدعاء الرسم، لأن هذه هي الحالة التي يقرأها المحرك. ابدأ بالبناء أولاً، ثم قم بتهيئة الحالة، وارسم أخيراً، وسيتصرف نموذج المتجه (vector model) بشكل يمكن التنبؤ به في كل مرة

من هنا، تتمثل الخطوات التالية الطبيعية في قراءة المتجهات والنص مرة أخرى من مستند موجود، مغطاة في مقالنا حول استخراج النص والصور والخطوط، وعرض نموذج الرسم نفسه في سياق جهاز (device context) Windows للمعاينة على الشاشة والطباعة، المغطاة في جولة الطباعة والمعاينة. تُشحن استدعاءات المسار والتظليل والنمط الموضحة هنا كجزء من مكتبة Delphi PDF جنباً إلى جنب مع واجهات برمجة تطبيقات النص والصور والنماذج والتوقيع المغطاة في مكان آخر في هذه المدونة