مقال تقني

تقدم الحروف واستعادة مقص q/Q في عارض PDF مع Delphi

يعرض عارض صفحات HotPDF Delphi Component النص الآن بحساب إزاحة كل حرف في فضاء النص، tx = ((w0 − Tj/1000) × Tfs + Tc + Tw) × Th كما يعرفها §9.4.4 من ISO 32000-1، ثم تحريك مصفوفة النص عبر جزئها الخطي بـ HPDFTranslateTextMatrix. والقص يحفظ عن كل إطار q ويستعاد على Q، لكن منطقة GDI تُلتقط فقط حين يغير ذلك الإطار المقص فعلاً. الإصلاحان هبطا في HotPDF 2.754.0، وكلاهما جاء من صفحات واقعية عُرضت بكلمات منهارة أو بمناطق مقص تتسرب بعد Q خاصتها. العيب الأول حساب يبدو صائباً حتى يكتب منتج حجم خطه في المصفوفة. والثاني إصلاح صواب كاد يكلفنا تسريع العرض المتوازي، وطريقة استرداد السرعة تستحق المعرفة إن كنت تكتب أي جهاز PDF مدعوم بـ GDI

لماذا تنهار الكلمات كتلةً واحدة حين يستخدم PDF‏ Tf 1؟

لأن كود التقدم القديم كان يضيف مسافة في فضاء النص مباشرة إلى مركبة النقل في Tm، وكأن فضاء النص وفضاء المستخدم يحملان المقياس نفسه دائماً. منتجون واقعيون كثيرون يضبطون حجم الخط على 1 بـ Tf ويحملون الحجم الحقيقي في مصفوفة النص. مع /F1 1 Tf و 12 0 0 12 72 700 Tm، رمز بعرض 500 وحدة يتقدم 0.5 في فضاء النص، أي 6 نقاط على الصفحة بعد أن يقيسه Tm. وكان العارض القديم ينفذ Tm.e := Tm.e + Adv فيتحرك القلم 0.5 نقطة. هبط كل حرف بعد سابقه باثني عشر من الحرف، فعُرض سطر نص المتن بقعة داكنة عند الهامش الأيسر بينما بدا الملف نفسه تاماً في كل عارض آخر

لماذا تنهار الكلمات تحت Tf 1 في عارض HotPDF: مع 12 0 0 12 72 700 Tm يجب أن يتقدم رمز من 500 وحدة 0.5 وحدة في فضاء النص، وهو ما يقيسه Tm إلى 6 نقاط، بينما أضاف الكود القديم 0.5 مباشرة إلى Tm.e فعرض سطر نص المتن بقعة تقدم فيها كل حرف باثني عشر من الحرف
المنتجون الذين يرمزون حجم الخط في مصفوفة النص جعلوا كل حرف يهبط بعد سابقه باثني عشر من الحرف، عيب غير مرئي على مخرجات المكتبة ذاتها
// تدفق محتوى من منتج يرمز الحجم في Tm لا Tf:
//   BT
//   /F1 1 Tf
//   12 0 0 12 72 700 Tm
//   [(Hel) 30 (lo) -250 (world)] TJ
//   ET

// التقدم القديم (مبسط): مسافة تضاف إلى Tm.e وكأنها فضاء المستخدم
Adv := W * FontSize / 1000;                  // ‏0.5 لرمز من 500 وحدة
if (HorizScale <> 0) and (HorizScale <> 100) then
  Adv := Adv * HorizScale / 100;             // ‏Th على العرض فقط
Adv := Adv + CharSpace;                      // ‏Tc لا يقاس بـ Th
if Code = 32 then
  Adv := Adv + WordSpace * FontSize / 1000;  // ‏Tw يقاس بـ Tfs خطأً
Tm.e := Tm.e + Adv;                          // يتجاهل Tm.a و Tm.b و Tm.c و Tm.d

// ضبط TJ القديم: بلا Th، ومرة أخرى Tm.e فقط
Tm.e := Tm.e - NumValue * FontSize / 1000;

لم يكن اختصار Tm.e العيب الوحيد في تلك الكتلة. تباعد الكلمات Tw يعبَّر بوحدات فضاء النص غير المقاسة، ومع ذلك ضرب الكود القديم في FontSize / 1000، فتحت Tf 12 خسر سطر مضبوط التبرير كل تقريباً من فراغه بين الكلمات. والقياس الأفقي Th انطبق على عرض الحرف لا على Tc أو Tw، وتجاوزه ضبط تقطيع TJ كلياً. ومسار عدم الرسم الذي يقدم نص وضع العرض 3 غير المرئي، وهو ما تستخدمه طبقات نص OCR، والنص داخل محتوى اختياري مخفي حمل نسخة خاصة من الحساب نفسه، فكل ما رُسم بعد جري غير مرئي بدأ من موضع خطأ. عيوب حالة النص في عارض نادراً ما تفشل بصوت عال: مثل عيوب فهرس المعامل واسم المورد التي صفّرت يوماً Tc و Tw و Tz دون خطأ واحد، أنتجت تلك صفحات مقنعة على مخرجات المكتبة ذاتها ولم تنكسر إلا على ملفات من منتجين آخرين

كيف يعرف §9.4.4 من ISO 32000-1 تقدم الحرف؟

يعرف §9.4.4 من ISO 32000-1 التقدم كلياً في فضاء النص ويطبقه على مصفوفة النص مصفوفةَ نقل، فالجواب أن تحسب tx أولاً وتترك Tm تقيس وتدير وتميل. للكتابة الأفقية يساوي tx ‏((w0 − Tj/1000) × Tfs + Tc + Tw) × Th، حيث w0 عرض الحرف بأجزاء الألف من em، و Tj ضبط TJ، و Th هي Tz على 100. و Tm الجديدة هي [1 0 0 1 tx 0] × Tm، وهي في HotPDF المساعدة HPDFTranslateTextMatrix: تضيف X و Y عبر معاملات المصفوفة a و b و c و d بدل الكتابة إلى e و f مباشرة. وفق §9.3.3 ينطبق Tw على كود المحرف أحادي البايت 32 فقط، فأكواد CID متعددة البايتات لا تلتقط تباعد كلمات أبداً على المسار الأفقي. والمساعدة نفسها تقود الآن Td و TD و T* ومعاملي ' و "، وضبطات TJ ومسار النص المخفي، أي أن دالة واحدة تملك القاعدة

تقدم الحرف وفق ISO 32000-1 9.4.4 في عارض HotPDF: يُحسب tx في فضاء النص من w0 و Tj و Tfs و Tc و Tw و Th، ثم يطبق عبر HPDFTranslateTextMatrix فيمرت الإزاحة على معاملات المصفوفة a و b و c و d وتتشارك Td و TD و TJ ومسار النص المخفي قاعدة واحدة
إضافة التقدم إلى Tm.e مباشرة لا تعمل إلا حين يتساوى فضاء النص وفضاء المستخدم؛ وتوجيهه عبر معاملات المصفوفة يبقي النص المقاس والمدير والمائل صحيحاً
procedure HPDFTranslateTextMatrix(var Matrix: THPDFAffineMatrix; X, Y: Double);
begin
  Matrix.e := Matrix.e + Matrix.a * X + Matrix.c * Y;
  Matrix.f := Matrix.f + Matrix.b * X + Matrix.d * Y;
end;

// تقدم الحرف الأفقي، ISO 32000-1 9.4.4
W   := HPDFFontCharWidth(F, Code);
Adv := W * State.Text.FontSize / 1000 + State.Text.CharSpace;
if (Code = 32) and not F.CID2Byte then
  Adv := Adv + State.Text.WordSpace;         // ‏Tw في فضاء النص، بلا قياس
Adv := Adv * State.Text.HorizScale / 100;    // ‏Th ينطبق على المجموع كله
HPDFTranslateTextMatrix(Tm, Adv, 0);

// عنصر عدد TJ: الفضاء نفسه، و Th نفسه
Adjustment := -Items[I].NumValue * State.Text.FontSize / 1000;
HPDFTranslateTextMatrix(State.Text.Tm, Adjustment * State.Text.HorizScale / 100, 0);

وكان لا بد لموضعة الحروف أن تتبع المنطق نفسه. حين لا محيط مضمّن متاح ويعود العارض إلى GDI ‏TextOutW، يبني الآن مصفوفة الحرف كاملة من CTM × Tm × الارتفاع × مقياس em، بما فيها Th، ويثبتها بـ SetWorldTransform في وضع GM_ADVANCED داخل زوج SaveDC / RestoreDC. وينشأ خط GDI بارتفاع ثابت 1000 وحدة والتحويل يقوم بالتحجيم، فيحافظ النص المدير والمائل على توجهه بدل أن يُرسم معتدلاً عند نقطة أصل محولة. ووضع الكتابة العمودي هو عدم التماثل المتعمد الوحيد: خط WMode 1 يتقدم أسفل محور y بمقياسه العمودي، ولا ينطبق القياس الأفقي على ذلك المحور

ماذا يحفظ q/Q فعلاً في حالة رسوميات PDF؟

يسرد §8.4.2 من ISO 32000-1 مسار القص الحالي جزءاً من حالة الرسوميات، فيجب أن يستعيد Q المقص كما كان تماماً عند q المطابقة، لا المعاملات العددية وحدها. كان HotPDF يحتفظ أصلاً برصة حالة رسوميات بالـ CTM والألوان ومعاملات الخط وحالة النص، لكن GDI تحفظ المقص في سياق الجهاز، خارج تلك الرصة. فنسخة الحالة العددية استعادت كل شيء إلا المقص، ومقص ثبت بـ W n داخل كتلة q ... Q ظل يقص كل عملية لاحقة على الصفحة. وأضافت Form XObjects طريقاً ثانياً إلى الفشل نفسه، لأن §8.10 يعطي النموذج حفظاً واستعادة ضمنيين حول محتواه، ومحتوى النماذج الواقعي أحياناً يترك معاملي q غير متوازنين رغم أن المواصفة تشترط توأمهما. يستدعي العارض الآن CaptureClipBeforeChange و SaveDC قبل تشغيل نموذج، ثم بعد انتهاء النموذج يرمي أي مناطق محفوظة أعمق من عمق الدخول ويستدعي RestoreDC، فلكل HRGN محفوظ مسار تحرير واحد بالضبط

التقاط مقص كسول مع THPDFSavedClipState

الإصلاح الذي شحن يحفظ سجل THPDFSavedClipState واحداً عن كل q، لكنه يرجئ الجزء المكلف حتى أول مرة يعدّل فيها الإطار المقص. يحمل السجل مقبض المنطقة، وعمق الرصة الذي يخصها، وسياق الجهاز الذي أُخذ منه، وعلم Captured. يملأ DevPushState العمق و DC فقط وينمي مصفوفة الإطارات بالمضاعفة من 16، فتدفق محتوى مليء بـ q 1 0 0 1 x y cm ... Q لا يخصص أي كائن GDI إطلاقاً. والمعاملات التي ستغير القص، أي رسم مسار بـ W أو W* معلقة، أو معامل n، أو تعبئات الأنماط، أو دخول نموذج، تستدعي CaptureClipBeforeChange أولاً

التقاط مقص GDI الكسول في عارض HotPDF: يسجل DevPushState العمق و DC فقط عن كل q، ويقرأ CaptureClipBeforeChange المنطقة قبل أن يغير W أو n أو دخول نموذج القص مباشرة، ويستعاد ويحذف في DevPopState على Q، بينما النسخة المتعجلة التي تلتقط عن كل q أسقطت معدل النقل المتوازي إلى نحو 1.15 ضعف أحادي الخيط
إنشاء منطقة GDI عن كل q أجاع خيوط العرض، فالالتقاط الآن يحدث فقط حين يوشك معامل على تغيير القص وتجتاز بوابة التسريع 1.5 ضعف مجدداً
procedure THPDFPageRenderer.CaptureClipBeforeChange;
var
  Index, ClipResult: Integer;
  Region: HRGN;
begin
  ClearSavedClipRegions(FGSStack.Count);
  Index := FSavedClipCount - 1;
  if (Index < 0) or FSavedClips[Index].Captured or
     (FSavedClips[Index].StackDepth <> FGSStack.Count) or
     (FSavedClips[Index].DC <> FDC) then Exit;   // محفوظ أصلاً، أو ليس لنا
  Region := CreateRectRgn(0, 0, 0, 0);
  if Region = 0 then RaiseLastOSError;
  ClipResult := GetClipRgn(FDC, Region);         // ‏0 تعني لا مقص إطلاقاً
  if ClipResult <= 0 then
  begin
    DeleteObject(Region);
    Region := 0;
    if ClipResult < 0 then RaiseLastOSError;
  end;
  FSavedClips[Index].Region := Region;
  FSavedClips[Index].Captured := True;
end;

procedure THPDFPageRenderer.DevPopState;
var
  Index: Integer;
begin
  ClearSavedClipRegions(FGSStack.Count);
  Index := FSavedClipCount - 1;
  if (Index >= 0) and (FSavedClips[Index].StackDepth = FGSStack.Count) then
  begin
    if FSavedClips[Index].Captured and (FSavedClips[Index].DC = FDC) then
      SelectClipRgn(FDC, FSavedClips[Index].Region);  // المنطقة 0 تزيل المقص
    if FSavedClips[Index].Region <> 0 then
      DeleteObject(FSavedClips[Index].Region);
    Dec(FSavedClipCount);
  end;
  FGSStack.Pop;
end;

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

أين حدود هذا الأسلوب؟

المقص المحفوظ منطقة GDI ببكسلات الجهاز، فهو دقيق للصورة النقطية المعروضة ولا معنى له لأي هدف آخر. لهذا يسجل كل إطار سياق جهازه ويتخطى DevPopState الاستعادة حين يكون الـ DC قد تغير، مثلاً أثناء عرض مجموعة شفافية في صورة نقطية طبقية خاصة بها. وإعادة GetClipRgn صفراً نتيجة مشروعة تعني لا مقص، واستعادته بـ SelectClipRgn(FDC, 0) هو ما يزيل بشكل صحيح مقصاً لم يكن موجوداً عند q المطابقة. وعلى جانب النص، يصحح الإصلاح أين يذهب كل حرف، لكنه لا يخترع الأعراض: إن ترك خط مصفوفة /Widths خاصته وكان البرنامج المضمّن غير متاح، فالتقدم ما زال بجودة احتياطي العرض فقط. وحين تفحص هذه المنطقة بالانحدار، احتفظ على الأقل بتركيبة واحدة بـ Tf 1 و Tm مقاسة، وواحدة بـ Tz و Tw غير صفريين، وواحدة بمقص داخل q ... Q يليه محتوى خارجه، لأن لا واحدة من تلك تظهر في مستندات أنتجتها المكتبة نفسها

وإن كنت تقود العارض من كود تطبيق، فلا شيء يتغير في نمط الاستدعاء الموصوف في عرض صفحة PDF إلى صورة نقطية، والصفحات التي كانت تظهر سطوراً مموهة أو محتوى مقصوصاً ينبغي أن تعرض ببساطة صحيحة على 2.754.0 وما بعده. وتفاصيل المكون وإصدارات Delphi و C++Builder المدعومة والترخيص على صفحة منتج HotPDF Delphi PDF Component