مقال تقني

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

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

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

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

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

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

// 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)، مسحوباً نحو نقطتي التحكم على طول الطريق. الأقواس الدائرية (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)، يظهر الثقب فقط إذا كانت الحلقة الداخلية تلتف في الاتجاه المعاكس للحلقة الخارجية؛ إذا قمت بلفهما بنفس الطريقة فإن اللفات تعزز بدلاً من الإلغاء، وتملأ المنطقة الداخلية بشكل صلب. النجمة الخماسية المرسومة كمخطط تفصيلي واحد يتقاطع مع نفسه تُظهر الانقسام نفسه: يترك الزوجي والفردي البنتاغون الأوسط فارغاً بينما يملأه اللف غير الصفري

يختار PDFlibPas القاعدة من خلال الاستدعاء الذي تجريه للرسم، وليس عن طريق علامة. يملأ DrawPath بقاعدة اللف غير الصفري؛ يملأ DrawPathEvenOdd بقاعدة الزوجي والفردي. كلاهما يأخذ نفس وضع العدد الصحيح: 0 يخطط (strokes) المخطط التفصيلي فقط، و 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

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

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

في PDFlibPas، التدرج هو مورد مستند مسمى تقوم بإنشائه مرة واحدة ثم تختاره كطلاء نشط. يسجله 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 للنص. يظل الربط نشطاً لاستدعاءات الرسم التي تلي ذلك، بحيث يأخذ المسار الذي ترسمه بعد ذلك التدرج بدلاً من لون مسطح

// 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 ثابتة، لذا تمتد نطاقات الألوان أفقياً ويتلاشى المستطيل من الأزرق في قاعدته إلى الأبيض في الأعلى. نظراً لأن التظليل (shader) مرتبط باسم، يمكن لتعريف واحد أن يملأ أي عدد من الأشكال على الصفحة، والعودة إلى لون مسطح هي مجرد استدعاء SetFillColor آخر قبل المسار التالي

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

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

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

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

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

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

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