مقال تقني

تصدير صفحات PDF إلى SVG في Delphi باستخدام HotPDF

يُصدّر HotPDF أي صفحة من مستند PDF محمّل إلى ترميز SVG مستقل باستدعاء واحد هو BuildLoadedPageSVG، الذي يُعيد مستند SVG الكامل كسلسلة نصية. يحمل الترميز المُصدَّر هندسة الصفحة، والنص كعناصر text حقيقية في SVG، والصور النقطية المضمَّنة، وحالة الخط التي أنشأتها عوامل PDF عند كل عملية رسم

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

لماذا لا تتحوّل صفحة PDF إلى SVG ببساطة؟

ثمة ثلاثة أوجه عدم تطابق تجعل التحويل غير بسيط، وجميعها تنتج مخرجات تبدو معقولة إلى أن تقارنها جنبًا إلى جنب مع الأصل. الأول هو محور y. فمساحة المستخدم في PDF تنمو صعودًا من الزاوية السفلية اليسرى للصفحة؛ بينما تنمو SVG هبوطًا من الزاوية العلوية اليسرى. وقلب واحد على مستوى الصفحة بأكملها يُصلح إحداثيات الرسم لكنه يكسر كل حرف رسومي، لأن قلب اللوحة بأكملها يعكس أيضًا أشكال الحروف

وأوجه عدم التطابق الثاني هو الوراثة. ففي PDF، يقوم q وQ بدفع وسحب حالة رسومية تشمل عرض الخط، ونهاية الخط، وتقاطع الخطوط، وحد الزاوية، ومصفوفة الشرطات، وطور الشرطات، والشفافية. أما في SVG، فالعنصر الذي لا يُسمّي سمة معيّنة يرثها من مجموعة سلف له، وهذه قاعدة نطاق مختلفة تمامًا. والمُصدِّر الذي يتتبّع فقط مصفوفة التحويل الحالية وينسى حالة الخط يسمح للحالة المُستعادة بعد Q بالتسرّب إلى العناصر التي تليها

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

استدعاء واحد للحالة الشائعة

بالنسبة إلى المهمة الاعتيادية المتمثلة في تصدير الصفحات لعارض ويب، أو أداة مقارنة فروق، أو تسليم تصميم، فسطح واجهة البرمجة دالة واحدة فقط. تأخذ BuildLoadedPageSVG فهرس صفحة يبدأ من صفر ضمن المستند المحمّل حاليًا، وتُعيد مستند SVG كـ AnsiString:

var
  Pdf: THotPDF;
  I: Integer;
  Svg: AnsiString;
  Output: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statements.pdf', '') <= 0 then
      Exit;                     // LoadFromFile تُعيد عدد الصفحات
    for I := 0 to Pdf.LoadedPageCount - 1 do
    begin
      Svg := Pdf.BuildLoadedPageSVG(I);
      if Length(Svg) = 0 then
        Continue;
      Output := TFileStream.Create(Format('page-%d.svg', [I + 1]), fmCreate);
      try
        Output.WriteBuffer(Svg[1], Length(Svg));
      finally
        Output.Free;
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

وتتوفر الوظيفة نفسها عبر أداة سطر أوامر HotPDF كأمر export-svg، وهو مفيد في خطوط أنابيب البناء وبرمجيات اختبار الانحدار حين تريد تمثيلًا نصيًا قابلًا لمقارنة الفروق لصفحة ما من دون كتابة أي كود Pascal. ولأن SVG نص، فإنه رفيق طبيعي لمسار الرسم النقطي الموصوف في عرض صفحة PDF كصورة نقطية: فالصورة النقطية تخبرك بشكل الصفحة، بينما يخبرك SVG بما تتكون منه

كيف يُطابَق نص PDF مع عناصر النص في SVG؟

يُركّب HotPDF سلسلة مصفوفة النص كبادئة مضروبة في CTM مضروبة في مصفوفة النص مضروبة في قلب الحرف الرسومي، حيث قلب الحرف الرسومي هو ضرب من اليمين بـ matrix(1,0,0,-1,0,0). ويوجد هذا العامل الأيمن فقط لإلغاء القلب الرأسي على مستوى الصفحة بالنسبة إلى أشكال الحروف الرسومية، إذ إن نص SVG المرسوم داخل إطار محلي مقلوب سيظهر مقلوبًا رأسًا على عقب لولا ذلك. ووضع هذا التصحيح داخل المصفوفة بدلًا من كود خاص بحالات معيّنة يعني أن النص المُدار والمعكوس والمائل يظهر جميعه بشكل صحيح من دون فروع إضافية

يستخدم التموضع الأفقي صياغة x متعددة القيم الخاصة بعنصر text في SVG، بإحداثية واحدة لكل حرف، تُجمَّع من تقدّم كل حرف رسومي إضافةً إلى تباعد الأحرف Tc وتباعد الكلمات Tw الساريين في تلك اللحظة. أما التحجيم الأفقي Tz فيُطوى داخل عمودَي a وc من مصفوفة النص بدلًا من إصداره بشكل منفصل، بحيث يظل العارض الذي يتجاهل سمات النص غير الشائعة يضع كل حرف رسومي في المكان الذي وضعه فيه PDF. والنص الناتج عبر تشكيل معقّد، المشروح في تشكيل نص السكربتات المعقّدة، يسلك المسار نفسه، لأن أداة التشكيل تكون قد حلّت المجموعات إلى أحرف رسومية موضوعة قبل تفسير تدفق المحتوى

الدوران والصور: قلبان يسهل عكسهما بالخطأ

الصفحة التي تحمل مُدخل /Rotate غير صفري تحتاج إلى تحويل مسبق مؤلَّف من قلب مقابل ارتفاع اللوحة بعد الدوران، ودوران معبَّر عنه في مساحة عرض محورها y صاعد. ومصفوفات الدوران الثلاث هي (0,-1,1,0,0,W) لزاوية 90 درجة، و(-1,0,0,-1,W,H) لزاوية 180، و(0,1,-1,0,H,0) لزاوية 270، حيث W وH هما أبعاد الصفحة قبل الدوران. واشتقاق هذه المصفوفات يدويًا يفتح الباب أمام أخطاء إشارة في ثلاثة مواضع بالضبط، لذا يُركّبها المُصدِّر عبر إجراء ضرب المصفوفات نفسه الذي يتعامل مع كل تحويل آخر

وتحتاج الصور المضمَّنة إلى قلب خاص بها، لأن مساحة الصورة في PDF تضع صف العيّنات الأول عند الحافة العلوية للمربع الواحد، بينما يحمل عنصر image في SVG إطارًا محليًا محوره y هابط. لذا فالتحويل المُصدَر هو CTM مضروب من اليمين في matrix(1,0,0,-1,0,1). وإخطاء هذه الخطوة ينتج صورًا فوتوغرافية معكوسة رأسيًا على صفحة سليمة في كل ما عداها، وهو نوع من العيوب يلاحظه المراجع البشري فورًا، بينما لا يكتشفه الاختبار الآلي في كثير من الأحيان

ما الذي يحفظه جهاز الحالة الرسومية فعليًا؟

يُرسل HotPDF عوامل حالة الخط w وJ وj وM وd عبر واجهة جهاز اختيارية منفصلة، بحيث أُضيفت دقّة الخط من دون تغيير جدول الدوال الافتراضية لجهاز المحتوى الحالي، ومن دون كسر التوافق الثنائي للكود المبني وفق إصدارات سابقة. وعمليًا، يتلقّى SVG المُصدَر كلمات مفتاحية مُترجَمة بدلًا من أعداد PDF الصحيحة الخام:

// تتحول تعدادات PDF الصحيحة إلى سمات كلمات مفتاحية في SVG
//   نهاية الخط  0, 1, 2  ->  butt, round, square
//   تقاطع الخط  0, 1, 2  ->  miter, round, bevel
//
// عرض الخط صفر يعني في PDF خطًا شعريًا بمقياس الجهاز، لذا
// يُصدر المُصدِّر vector-effect="non-scaling-stroke" لإبقاء
// الخط مرئيًا وبعرض قريب من بكسل جهاز واحد بعد تطبيق CTM
//
// f* B* b* تختار قاعدة الزوجي-الفردي وتصدر fill-rule="evenodd"،
// بينما f B b تُبقيان على افتراضي SVG وهو الالتفاف غير الصفري

استعادة الحالة عند Q تشمل الشفافية، وعرض الخط، والنهاية، والتقاطع، وحد الزاوية، ومصفوفة الشرطات، وطور الشرطات معًا. وتلتقط كائنات Form XObject المتداخلة المجموعة الكاملة نفسها وتستعيدها عند حدودها، بحيث لا يمكن لحد متقطّع مُعرَّف داخل طابع أن يتسرّب نمطه إلى محتوى الصفحة الذي يليه. وإذا كنت تتتبّع بالفعل سلوك القص وCTM لأسباب أخرى، فهذا هو نموذج الحالة نفسه الذي يظهر في استيراد المتجهات بصيغتَي EMF وWMF، لكنه يعمل في الاتجاه المعاكس

حدود يجدر معرفتها قبل الشحن

المُصدِّر صريح بشأن نطاقه، ومعرفة حدوده مسبقًا أرخص من اكتشافها في الإنتاج. يصل اللون إلى جهاز SVG عبر العوامل rg وRG وg وG. أما التعبئات المُنشأة عبر مساحة لون مع scn، وهي الطريقة التي تُرسم بها ألوان Separation وDeviceN وICCBased، فلا تصل إلى الجهاز كثلاثية RGB مُحلَّلة، لذا فإن الصفحات التي تستخدم الألوان المُقطَّعة بهذه الطريقة تُصدّر هندستها لكن ليس تلك الألوان. وبالنسبة إلى المصادر الموجَّهة للطباعة، استخدم التحويل إلى صورة نقطية بدلًا من ذلك، أو سطّح الألوان المُقطَّعة أولًا؛ ونموذج التلوين نفسه مشروح في عرض ألوان Separation وDeviceN المُقطَّعة

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

ويقترن تصدير SVG بشكل طبيعي مع المقارنة البنيوية حين تحتاج إلى معرفة ما إذا كان مستند مُولَّد قد تغيّر فعليًا بين نُسختَي بناء. وتُوثَّق مجموعة الأدوات الأوسع المحيطة بالمستندات المحمّلة، من العرض إلى التحرير إلى التصدير، على صفحة مكوّن HotPDF لـ PDF في Delphi