Technical Article

الرسومات المتجهة في PDF باستخدام Delphi: المسارات والتدرجات

تعامل معظم أكواد Delphi التي تتعامل مع PDF هذا التنسيق كحاوية لشيئين: نصوص برمجية وبعض الصور النقطية الموضوعة. هذه النظرة صحيحة إلى حد ما، لكنها تترك الجزء الأكثر قدرة في التنسيق دون استخدام. صفحة PDF هي لوحة رسم ثنائية الأبعاد مستقلة عن الدقة مبنية على نفس نموذج التصوير مثل PostScript. يمكنها رسم الخطوط، والمنحنيات، والمناطق المملوءة، والتدرجات، والأنماط المتكررة، وكلها كمتجهات تظل حادة عند أي تكبير وتطبع بالدقة الكاملة للجهاز. إذا كنت ترسم شعاراً، أو مخططاً بيانياً، أو علامة مائية، أو إطار شهادة، فإن المسار المتجه هو دائماً البديل الصحيح تقريباً، وهو أصغر حجماً وأكثر وضوحاً من الصورة النقطية التي تلجأ إليها العديد من البرامج بدلاً من ذلك.

يستعرض هذا المقال نموذج المتجهات كما يحدده معيار ISO 32000-1 ويوضح استدعاءات PDFlibPas المطابقة. الهدف هو جعل المواصفات ملموسة، لأن واجهة برمجة التطبيقات ترتبط بها بشكل وثيق، وفهم أحدهما يعلمك الآخر.

الصفحة هي آلة مسارات

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

يتكون المسار من مسار فرعي واحد أو أكثر. يبدأ المسار الفرعي عند نقطة وينمو عن طريق إلحاق أجزاء: خطوط مستقيمة، ومنحنيات Bezier التكعيبية، وفي بعض المنصات مستطيلات كاملة تضاف كمسار فرعي مغلق خاص بها. في PDFlibPas، تفتح مساراً باستخدام StartPath، الذي يحدد نقطة البداية، ثم تمده باستخدام AddLineToPath وAddCurveToPath. يتقدم كل استدعاء بنقطة حالية ضمنية، بحيث يستمر الجزء التالي من حيث انتهى الجزء الأخير. يرسم ClosePath جزءاً مستقيماً أخيراً يعود إلى بداية المسار الفرعي، وهو أمر مهم للطلاء الخارجي (stroking) لأنه ينتج وصلاً حقيقياً للخطوط عند رأس الإغلاق بدلاً من غطاءين طرفيين سائبين.

// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // straight segment back to (150, 100)
PDF.DrawPath(2);                   // 2 = fill and stroke; path is consumed

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

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

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

الحالة الكلاسيكية التي يختلفان فيها هي شكل يحتوي على فجوة، مثل الكعكة أو الحلقة المعدنية. ارسم حدوداً خارجية وحدوداً داخلية داخلها. بموجب قاعدة الزوجي والفردي، تنحت الحلقة الداخلية دائماً فجوة، لأن أي نقطة بين الحدين يتم تقاطعها مرة واحدة وأي نقطة داخل الحلقة الداخلية يتم تقاطعها مرتين. بموجب قاعدة الالتفاف غير الصفرية، تظهر الفجوة فقط إذا التفت الحلقة الداخلية في عكس اتجاه الحلقة الخارجية، أما إذا التفت في نفس الاتجاه، فإن الالتفافات تقوي بعضها بدلاً من إلغاء بعضها، وتمتلئ المنطقة الداخلية بالكامل. يظهر نجم ذو خمس نقاط مرسوم كمخطط خارجي ذاتي التقاطع نفس الانقسام: تترك قاعدة الزوجي والفردي البنتاغون المركزي فارغاً بينما تملأه قاعدة الالتفاف غير الصفرية.

يحدد PDFlibPas القاعدة من خلال الاستدعاء الذي تجريه للتلوين، وليس عن طريق علامة. يملأ DrawPath باستخدام قاعدة الالتفاف غير الصفرية، بينما يملأ DrawPathEvenOdd باستخدام قاعدة الزوجي والفردي. يأخذ كلاهما نفس الوضع الصحيح: 0 لطلاء المخطط الخارجي فقط، و1 للتعبئة فقط، و2 للتعبئة والطلاء الخارجي معاً. قاعدة الزوجي والفردي هي الأداة الأسهل لعمل فجوات مفرغة لأنها لا تتطلب منك إدارة اتجاه المسار الفرعي.

// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
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: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
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

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

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

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

// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // start point and start RGB
  0, 260,   1.00, 1.00, 1.00,    // end point and end RGB
  1);                            // 1 = extend ends as solid color

// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = fill, now filled by the shader

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

أنماط البلاط تكرر خلية

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

يبني PDFlibPas خلية النمط من محتوى الصفحة الملتقط. يمكنك التقاط صفحة أو منطقة باستخدام CapturePage، وتحويل الالتقاط إلى نمط مسمى باستخدام NewTilingPatternFromCapturedPage(PatternName, CaptureID)... [تم الحذف للاختصار]

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

تجميع العناصر الأساسية معاً

تتكامل الأجزاء مباشرة. كتلة Bezier المملوءة هي مسار من المنحنيات الملونة باستخدام DrawPath. ويظهر نفس المخطط الخارجي الملون باستخدام DrawPathEvenOdd بعد إضافة حلقة داخلية فجوة كانت التعبئة الالتفافيه ستغلقها. المستطيل المملوء بالتدرج هو صندوق مرتبط بمظلل. يرسم المثال أدناه الثلاثة بالتتابع حتى يظهر الفرق بين قاعدتي التعبئة في صفحة واحدة، ثم يضع لوحة تدرج تحتها.

// 1. A filled Bezier shape (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. The same outline, plus an inner loop, filled even-odd to show a hole.
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);                              // hole is punched out

// 3. A rectangle filled with an 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 هو الاختيار بين الالتفاف غير الصفرية والزوجي والفردي، وبالنسبة للأشكال التي تحتوي على فجوات، توفر عليك قاعدة الزوجي والفردي عناء التفكير في اتجاه المسار الفرعي. ويتم أخذ عينات من حالة الرسومات في لحظة التلوين: اضبط الألوان، وعرض الخط، وارتباط المظلل قبل استدعاء التلوين، لأن هذه هي الحالة التي يقرأها المحرك. ابن أولاً، ثم قم بتهيئة الحالة، ولون أخيراً، وسيتصرف نموذج المتجهات بشكل متوقع في كل مرة.

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