مقال تقني

كيف تعمل رسومات PDF: تدفقات المحتوى والمشغلات

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

تدفق المحتوى هو bytecode بصيغة postfix

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

100 100 m    % moveto: start a new subpath at (100, 100)
200 200 l    % lineto: add a segment to (200, 200)
300 100 l    % lineto: add a segment to (300, 100)
h            % closepath: connect back to the start
S            % stroke: paint the path outline

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

الأصل في أسفل اليسار، وY يرتفع إلى الأعلى

قبل أن يصبح أي إحداثي منطقيًا عليك أن تعرف أين يقع (0, 0). يضع PDF الأصل في الزاوية السفلية اليسرى من الصفحة، مع ازدياد X إلى اليمين وازدياد Y إلى الأعلى، ويقاس ذلك بالنقاط بمعدل 72 نقطة للبوصة الواحدة (ISO 32000-2 §8.3.2). في صفحة US Letter تقع الحافة العلوية عند y = 792، لا عند y = 0. من يأتي من رسومات الشاشة، حيث يكون الأصل أعلى اليسار وY يزداد إلى الأسفل، يعكس هذا في المحاولة الأولى ويرسم الخط الأول خارج أسفل الصفحة. الوحدة مستقلة أيضًا عن وسيط العرض: 72 وحدة تساوي بوصة واحدة سواء عُرضت الصفحة على شاشة هاتف أو على جهاز إخراج طباعي.

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

// HotPDF, Delphi: y measured from the bottom edge upward, in points
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700);   // near the top of the page
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke;             // emits the moveto/lineto/stroke operators

تترجم سلسلة الاستدعاءات هذه مباشرة إلى المشغلات m وl وS أعلاه. المكتبة ليست سوى كاتب لتدفق المحتوى، لا أكثر، ومعرفة ما تصدره هي ما يسمح لك بتفسير الناتج عندما تهبط هيئة ما في مكان لم تتوقعه.

ابنِ المسار ثم ارسمه

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

مشغلات الإنشاء قليلة. يبدأ m مسارًا فرعيًا جديدًا عند نقطة. يضيف l مقطعًا مستقيمًا. يضيف c منحنى Bezier تكعيبيًا من ست معاملات، نقطتي تحكم ونقطة نهاية. وre اختصار يضيف مستطيلًا كاملًا من رباعية x وy والعرض والارتفاع. ويغلق h المسار الفرعي الحالي عائدًا إلى بدايته. لا يضع أي منها حبرًا على الصفحة؛ إنها فقط تتراكم هندسيًا.

200 250 m                    % start the subpath
300 350 400 450 500 250 c    % cubic Bezier: two control points, then endpoint
150 200 re                   % a 150 x 200 rectangle, added as its own subpath
h                            % close

استخدم المثال الأصلي البديل y من مشغل المنحنى، وهو الآن مهمل؛ أما c بنقاطه الثلاث الصريحة فهو الصيغة التي ستراها في الممارسة، وهو ما ينبغي الاعتماد عليه. ما إن يوجد المسار حتى ينهیه مشغل رسم واحد. المفردات صغيرة وتستحق الحفظ، لأن كل شكل في كل صفحة ينتهي بأحد هذه المشغلات:

  • يرسم S حدود المسار باستخدام عرض الخط ولون الرسم الحاليين.
  • يملأ f الداخل باستخدام لون التعبئة الحالي وقاعدة winding غير الصفري.
  • يملأ f* باستخدام قاعدة even-odd، وهي مهمة للأشكال المتقاطعة ذاتيًا والأشكال ذات الفتحات.
  • يملأ B ثم يرسم في عملية واحدة، بينما يغلق b المسار أولًا.
  • لا يرسم n شيئًا، وهكذا يصبح المسار منطقة قص من دون أن يترك أثرًا مرئيًا.

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

اللون نمط لا معامل

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

0.5 g                % DeviceGray fill, mid gray (0 = black, 1 = white)
0.2 0.6 0.8 rg       % DeviceRGB fill
0.8 0.2 0.1 RG       % DeviceRGB stroke (uppercase = stroke)
0.2 0.8 0.0 0.1 k    % DeviceCMYK fill

يناسب DeviceRGB الإخراج على الشاشة، بينما DeviceCMYK هو ما تتوقعه عمليات الطباعة، وDeviceGray هو أصغر خيار للمحتوى أحادي اللون. فضاءات الجهاز مريحة لكنها غير معايرة: يمكن أن تُعرض الثلاثية RGB نفسها بشكل مختلف على شاشتين، وهذه هي المشكلة التي وُجدت فضاءات الألوان القائمة على ICC ونيات الإخراج في PDF/A لحلها. في الأعمال الحساسة للون تختار فضاءً معايرًا باستخدام cs وCS وتضبط المكونات باستخدام sc وscn، لكن في المستندات العادية تتولى الاختصارات الخاصة بالجهاز المهمة. وتغلف المكتبة ذلك في استدعاءات ذات أنواع. HotPDF مثلًا يأخذ TColor واحدًا ويصدر المشغلات المطابقة:

Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150);  // x, y, width, height
Pdf.CurrentPage.Fill;

Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50);           // x, y, radius
Pdf.CurrentPage.Fill;

حالة الرسوميات ومكدس q/Q

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

q                    % save the entire graphics state
2 0 0 2 100 100 cm   % concatenate a transform: scale 2x, translate to (100,100)
0.8 g                % gray fill, scoped to this block
% ... draw scaled, gray content ...
Q                    % restore: transform and color revert

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

تقوم CTM بتحويل كل إحداثي

تقف مصفوفة التحويل الحالية بين الأرقام في مشغلاتك والصفحة الفعلية. يضرب كل إحداثي في CTM قبل رسم أي شيء، لذا فإن تغيير المصفوفة يغيّر أين وكيف يظهر كل رسم لاحق من دون لمس إحداثي واحد للمسار. يربط المشغل cm مصفوفة جديدة بالمصفوفة الحالية، ويأخذ ست معاملات تقابل المصفوفة affine [a b c d e f]:

1 0 0 1 100 50 cm        % translate by (100, 50): e and f carry the offset
2 0 0 1.5 0 0 cm         % scale x by 2, y by 1.5: a and d are the scale factors
0.707 0.707 -0.707 0.707 0 0 cm   % rotate 45 degrees (cos/sin in a, b, c, d)

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

الصور والمحتوى القابل لإعادة الاستخدام هي XObjects

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

/Photo <<
  /Type /XObject
  /Subtype /Image
  /Width 640
  /Height 480
  /BitsPerComponent 8
  /ColorSpace /DeviceRGB
  /Filter /DCTDecode        % the image data is a JPEG stream
>>

يرسم image XObject داخل المربع الواحد: فهو يشغل دائمًا المنطقة من (0, 0) إلى (1, 1) في فضاء المستخدم. لا تمرر له موضعًا أو حجمًا. بل تضبط CTM بحيث يتحول المربع الواحد إلى المستطيل الذي تريده، ثم تستدعيه باستخدام Do. لهذا فإن وضع صورة هو دائمًا تحويل يتبعه استدعاء، داخل حفظ واستعادة حتى لا يتسرب التحجيم إلى العملية التالية:

q
640 0 0 480 50 300 cm    % map the unit square to a 640x480 box at (50, 300)
/Photo Do                % paint the image XObject
Q

تدير الآلية نفسها Do form XObjects، التي تحتفظ بقطعة رسومية قابلة لإعادة الاستخدام، مثل شعار أو ختم متكرر، كتدفق محتوى خاص بها مع صندوق إحاطة. عرّفها مرة واحدة، واستدعها مرات عديدة مع CTM مختلفة، ولا تظهر البايتات في الملف إلا مرة واحدة. تخفي معظم المكتبات هذا خلف استدعاء واحد للوضع: يسجل HotPDF صورة نقطية باستخدام AddImage ويضعها باستخدام ShowImage، مع x وy والعرض والارتفاع بشكل صريح بدل أن يطلب منك بناء المصفوفة يدويًا:

var
  Bmp: TBitmap;
  ImgIndex: Integer;
begin
  Bmp := TBitmap.Create;
  try
    Bmp.LoadFromFile('logo.bmp');
    ImgIndex := Pdf.AddImage(Bmp, icFlate);
    // x, y (bottom-left), width, height, rotation angle
    Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
  finally
    Bmp.Free;
  end;
end;

تحت هذا السطر الواحد تكتب المكتبة قاموس image XObject، وتضبط CTM لتغيير حجم المربع الواحد وموضعه، ثم تصدر Do. النموذج الكامن هو ما يستحق معرفته، لأنه يفسر كل نتيجة غريبة: الصورة الممددة هي CTM بعوامل تحجيم غير متطابقة، والشعار المتماثل على أربعين صفحة هو form XObject واحد استُدعي أربعين مرة، والصورة التي تظهر مقلوبة هي انعكاس إشارة في المصفوفة، لا ملفًا تالفًا.

إلى أين يقود هذا

نموذج الرسوميات صغير ما إن ترى شكله. تدفق المحتوى هو bytecode بصيغة postfix يعمل فوق حالة قابلة للتغيير؛ تبدأ الإحداثيات من أسفل اليسار وتمر عبر CTM؛ تُبنى المسارات بصمت وتُرسم بمشغل واحد مقصود؛ وتبقى إعدادات اللون والخط قائمة حتى تحصرها بـ q/Q؛ أما الصور والرسوم القابلة لإعادة الاستخدام فهي XObjects توضع عبر تحويل مربع واحد. يكاد كل ناتج عرض مربك أن يعود إلى واحدة من هذه القواعد الخمس. إذا أردت أن ترى كيف تجلس مشغلات الرسوم هذه داخل نموذج الكائن الأكبر، فإن نظرة تقنية على بنية ملفات PDF تغطي تلك الطبقة، وإنشاء ملف PDF بسيط من الصفر تتبع البايتات من البداية إلى النهاية. يعيش رسم النص في عائلة مشغلات خاصة به وله مطباته الخاصة، والمقال المرافق عن نص PDF والتعامل مع الخطوط يغطي ذلك.

استدعاءات الرسم في Delphi المعروضة هنا، MoveTo وLineTo وStroke وRectangle وFill وSetRGBFillColor وAddImage وShowImage، هي جزء من مكوّن HotPDF لـ Delphi وC++Builder، الذي يصدر مشغلات تدفق المحتوى هذه نيابة عنك.