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