يعيد PDFlibPas، مكتبة مكوّن PDF الأصلية القائمة على VCL لـDelphi وC++Builder، تشغيل تدفق محتوى صفحة عبر فئة TPDFContentStateTracker الخاصة به دون لمس لوحة رسم على الإطلاق. تغذية المتتبع بعامل تشغيل مُحلَّل واحد في كل مرة تحافظ على سجل حالة رسم جارٍ — مصفوفة التحويل الحالية، ومصفوفة النص، وحدود القص، ومكدس الحفظ q/Q — متاح للقطة سريعة قبل أو بعد تنفيذ كل عامل تشغيل
اسأل أين يقع تشغيل نص فعليًا على الصفحة المطبوعة، وستضلّلك أرقام تدفق المحتوى الخام وحدها في كل مرة. تُعيد TPDFContentProgram.GetTextRuns بالفعل نقطة مرساة كل تعليمة عرض نص عبر حقلي OriginX وOriginY على TPDFTextRun، وتعليقات الحقول صريحة في أن هذه النقطة تقع في فضاء النص، مطويّة بالفعل عبر Tm، وTd، وTD، وT*. ما لا يزال مفقودًا، وما تقول تلك التعليقات إنه يجب على المستدعي توفيره، هو CTM النشطة عند تلك التعليمة تحديدًا — حاصل ضرب كل cm تراكمت حتى الآن، متداخلة داخل أيًّا كان عدد أزواج q/Q المفتوحة في تلك النقطة من التدفق
لماذا نعيد تشغيل تدفق محتوى بدلًا من رسمه؟
يحتفظ PDFlibPas بمفهومين منفصلين لحالة الرسم لمهمتين منفصلتين، والانقسام متعمد. يحمل سجل الحالة الداخلي للراسم مقبض لوحة جهاز حية، ومقبض منطقة قص، وذاكرات تخزين مؤقتة لترقيم الخطوط — موارد حقيقية مرتبطة بأيًّا كان السطح الذي يُرسَم عليه حاليًا، وبلا معنى بمجرد اختفاء ذلك السطح. لا تحمل TPDFContentGraphicsState شيئًا من ذلك: إنها سجل بسيط محصور بالقيم التي يعرّفها ISO 32000-1 §8.4 كقيم يمكن الوصول إليها من عوامل تدفق المحتوى وحدها — CTM، ونمط الخط، واللون، وحالة النص، وحدود القص والمسار المشتقة. ولأن السجل لا يحمل أي مرجع لوحة ولا مقبض ملف مفتوح، يمكن لمستدعٍ تحليل تدفق محتوى، واجتيازه بـTPDFContentStateTracker، والاستمرار في استخدام اللقطات الناتجة طويلًا بعد اختفاء أيًّا كان ما أنتج البايتات
كيف تبني TPDFContentStateTracker CTM
تربط TPDFContentStateTracker.Apply عوامل cm الستة في CTM الخاصة بالمتتبع باستخدام الضرب المسبق نفسه الذي يحدده PDF نفسه: تتحد المصفوفة الجديدة M2 مع CTM الحالية كـM2 × CTM، باصطلاح متجه الصف، حيث تتحول نقطة كـP′ = P × M (ISO 32000-1 §8.4). الجزء الذي يسهل إخطاؤه يقع في حد الإزاحة، لا الجزء الخطي: يجب أن تمر إزاحة M2 نفسها عبر مكوّن الدوران-والتحجيم لـCTM الحالية قبل إضافة إزاحة CTM الحالية فوقها. تخطَّ تلك الخطوة وثبّت تركيبًا ساذجًا حسب المكوّن بدلًا من ذلك، وستبدو أول cm معزولة تختبرها صحيحة بينما ينحرف بصمت كل إحداثي بعد cm ثانية أو ثالثة متداخلة، وهذا بالضبط نوع الخلل الذي ينجو من مراجعة الشيفرة لأن اختبار الوحدة الذي كان ليلتقطه يحتاج تحويلين متسلسلين على الأقل ليفشل
var
Prog: TPDFContentProgram;
Runs: TPDFTextRunArray;
States: TPDFContentGraphicsStateArray;
DeviceX, DeviceY: Double;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
try
Prog.Parse(ContentBytes);
Runs := Prog.GetTextRuns;
// One before-instruction snapshot per operator, computed in a single pass
States := Prog.TraceGraphicsStates(nil, False);
for I := 0 to High(Runs) do
begin
// OriginX/OriginY already fold in Tm/Td/TD/T*; only the CTM active
// at this instruction is still missing (ISO 32000-1 8.4)
with States[Runs[I].InstructionIndex].CTM do
begin
DeviceX := Runs[I].OriginX * M11 + Runs[I].OriginY * M21 + DX;
DeviceY := Runs[I].OriginX * M12 + Runs[I].OriginY * M22 + DY;
end;
LogTextOrigin(Runs[I].Text, DeviceX, DeviceY); // caller-supplied handler
end;
finally
Prog.Free;
end;
end;
تجيب الحلقة أعلاه عن نقطة الألم من الافتتاحية: تعيد TPDFContentProgram.GetTextRuns OriginX وOriginY مطويّتين بالفعل عبر Tm وTd وTD وT*، وتوفّر TraceGraphicsStates(nil, False) القطعة الواحدة المتبقية، CTM قبل-التعليمة عند الفهرس الدقيق الذي التُقط عنده كل تشغيل، في مرور خطي واحد عبر البرنامج بأكمله. تمرير nil يترك الطريقة تمتلك متتبعًا خاصًا للاستدعاء وتحرره داخليًا، وهو الخيار الصحيح لفحص لمرة واحدة؛ تمرير مثيل TPDFContentStateTracker موجود بدلًا من ذلك هو ما يبقي الحالة مستمرة عبر صفحة مجمَّعة من أكثر من تدفق محتوى واحد، بما أن ISO 32000-1 يعامل مصفوفة /Contents الخاصة بصفحة كتدفق منطقي واحد ويجب أن يتفق مكدس q/Q
مصفوفة النص تنجو من Q؛ حالة الرسم لا تنجو
يعرّف ISO 32000-1 §9.4.2 Td وTD وTm وT* كعوامل تبني مصفوفة النص ومصفوفة سطر النص داخل كتلة BT/ET، ويحافظ PDFlibPas على ذلك التمييز حادًا: تربط Td وTD إزاحة نقية بمصفوفة سطر النص، وتفعل T* الشيء نفسه باستخدام سالب المسافة البادئة (leading) الحالية، ولا تستبدل سوى Tm كلتا المصفوفتين كليًا بالأرقام الستة المُعطاة لها. تعيد BT ضبط كلتا المصفوفتين إلى الهوية، مرة واحدة بالضبط، عند بداية كائن النص — لكن q وQ لا تلمسانهما على الإطلاق. تعالج TPDFContentStateTracker.Apply حالة coRestoreState خاصة لهذا السبب بالضبط: قبل أن تُخرج الحالة المحفوظة من المكدس، تلتقط مصفوفة النص الحالية، ومصفوفة سطر النص، وعلم BT/ET، وتُعيد تطبيقها فوق أيًّا كانت الحالة المُخرَجة تحمله، لأن زوج q/Q ملفوفًا حول تشغيل نص لا يُفترض به تحريك موضع النص إلى الوراء
var
Tracker: TPDFContentStateTracker;
Prog: TPDFContentProgram;
I: Integer;
begin
Prog := TPDFContentProgram.Create;
Tracker := TPDFContentStateTracker.Create;
try
Prog.Parse('BT 100 700 Td q 2 0 0 2 0 0 cm (A) Tj Q (B) Tj ET');
for I := 0 to Prog.Count - 1 do
begin
Tracker.Apply(Prog[I]);
if Prog[I].Op in [coShowText, coRestoreState] then
LogState(Prog[I].OpName, Tracker.Snapshot); // caller-supplied handler
end;
finally
Tracker.Free;
Prog.Free;
end;
end;
شغّل ذلك التسلسل وستكون CTM المُبلَّغة عند Tj الثانية قد عادت إلى مقياس الهوية الذي كانت عليه قبل q — اختفت 2 0 0 2 0 0 cm داخل زوج الحفظ/الاستعادة، كما يتطلب q/Q. أما TextMatrix.DX عند التعليمة نفسها، فلا تزال 100: عملت Td التي ضبطتها قبل q، بحيث ليست حالة رسم كانت Q مؤهلة قط لمسّها، وأداة افترضت خلاف ذلك كانت لتبلّغ عن بدء تشغيل الحرف الثاني من الموضع الأفقي الخاطئ على الصفحة
ماذا يحدث عندما يعمل عامل مسار قص؟
لا يُصغِّر عامل W أو W* القص فورًا؛ فهو فقط يسجّل أي قاعدة تعبئة يُستخدم، وينتظر التقاطع الفعلي أيًّا كان عامل رسم مسار يليه، بما في ذلك راسم عدم-الفعل n الذي يستخدمه مؤلفو PDF روتينيًا بالضبط للقص دون رسم أي شيء. تعكس TPDFContentStateTracker ذلك التوقيت ذا الخطوتين بدقة: تضبط coClip وcoClipEvenOdd فقط علم قاعدة قص معلَّق، وEndCurrentPath — التي تستدعيها كل عوامل رسم المسار — هي ما يُقاطع فعليًا حدود المسار المعلَّق في ClipMinX وClipMinY وClipMaxX وClipMaxY. الحصول على هذا التدريج بشكل صحيح يهم لعقد لقطة قبل/بعد نفسه: لقطة-قبل تُلتقط بالضبط عند تعليمة W يجب أن تُظهر لا تزال القص القديم الأوسع، لأن القص لم يسرِ بعد في تلك النقطة من التدفق، وطي الخطوتين إلى واحدة كان سيكسر بصمت كل مستدعٍ يعتمد على أن حالة-قبل تعني ما تقوله
تخبر ClipBoundsExact مستدعيًا أيًّا من موقفين ينظر إليه، ولا تكون True إلا لمستطيل واحد محاذٍ للمحور مبني بـre على مسار فارغ من ناحية أخرى — الشكل الواحد الذي يمكن لـPDFlibPas تمثيله بدقة كأربعة أرقام. كل شيء آخر — مستطيل مُدار، مخطط منحنٍ، مسار مركّب بعدة مسارات فرعية، أو قص مبني من نمط عرض نص — لا يزال ينتج ClipMinX حتى ClipMaxY، لكن مع مسح ClipBoundsExact إلى False، إشارة صادقة إلى أن الأرقام الأربعة حد خارجي آمن لا شكل القص الحقيقي؛ المستدعون الذين يحتاجون فقط ذلك الحد، مثل عزل منطقة فرعية مستطيلة قبل التحويل النازل لنصف التظليل في GDI الموصوف في رسم صفحات PDF إلى أحادي اللون بت واحد، يمكنهم قراءته مباشرة بدلًا من إعادة اشتقاقه من هندسة الصفحة
منحنيات بيزييه: حد دقيق أو حد آمن
الطريقة الأرخص لتحديد قطعة بيزييه تكعيبية هي أخذ الغلاف المحدَّب لنقاط تحكمها الأربع، وذلك آمن دائمًا لأن المنحنى لا يغادره أبدًا — لكن منحنى ضحل وعريض يمكن أن يبلّغ عن صندوق حدود أكبر بكثير مما يشغله المنحنى فعليًا، ما يُضعف الترشيح القائم على القص بالضبط عندما يهم أكثر ما يهم، على مسارات زخرفية كبيرة. يحل PDFlibPas المشكلة الأدق بدلًا من ذلك: لكل محور، يحل مشتقة المنحنى التكعيبي بحثًا عن جذور داخل الفترة المفتوحة (0, 1) ويُقيّم المنحنى عند أي جذور يجدها، جنبًا إلى جنب مع كلتا نقطتي النهاية، وهي الطريقة الصيغية المغلقة القياسية للحصول على الامتداد الحقيقي المحاذي للمحور لمنحنى بدلًا من تقدير مبالغ فيه. الدقة لكل منحنى لا تنتقل مع ذلك إلى القص نفسه: بمجرد أن يصبح مخطط منحنٍ مسار قص، لا تزال ClipBoundsExact تسقط إلى False له، لأن صندوق حدود، مهما كان دقيقًا، لا يزال ليس الشكل نفسه للمنحنى الذي يحده، ويفضّل متتبع الحالة قول ذلك على ترك مستدعٍ يفترض مستطيلًا حيث يوجد منحنى فعليًا
قراءة الحالة قبل وبعد كل عامل تشغيل
ما إذا أراد مستدعٍ حالة-قبل أو حالة-بعد يعتمد كليًا على ما يفعله عامل التشغيل: سؤال رسم أو اختبار إصابة حول مسار أو تشغيل نص يريد الحالة كما وقفت لحظة قبل تشغيل ذلك العامل، بما أن ذلك ما حدد فعليًا كيف رسم العامل، بينما سؤال تشخيصي حول عامل ضابط حالة مثل gs يريد عادة رؤية ما غيّره للتو. تعرض TPDFContentProgram.TraceGraphicsStates(Tracker, AfterInstruction) ذلك الاختيار بالضبط كقيمة منطقية ثنائية واحدة، محسوبة TPDFContentGraphicsState واحدة لكل تعليمة في مرور خطي واحد عبر البرنامج بأكمله بصرف النظر عن أي لحظة مطلوبة. يعرض GetGraphicsState(InstructionIndex, AfterInstruction, State) الاختيار نفسه قبل/بعد لتعليمة واحدة بدلًا من البرنامج بأكمله، لكنه يعيد التشغيل من التعليمة صفر في كل استدعاء للوصول إلى هناك، بحيث يكلّف فحص فهارس كثيرة باستدعائه في حلقة O(n²) مقابل استدعاء O(n) واحد لـTraceGraphicsStates على البرنامج نفسه
var
Before, After: TPDFContentGraphicsState;
begin
// Same instruction index, two different instants: before vs. after it runs
Prog.GetGraphicsState(CmIndex, False, Before);
Prog.GetGraphicsState(CmIndex, True, After);
// Before.CTM reflects every earlier cm; After.CTM already folds in
// this instruction's own concatenation as well
end;
التعايش مع تدفقات محتوى مشوَّهة
نوعان من المدخلات المشوَّهة شائعان بما يكفي في منتجي PDF الحقيقيين بحيث يتعين على TPDFContentStateTracker التسامح معهما بدلًا من الفشل عليهما. الأول مسار يمتد عبر حد q/Q: المسار الحالي، والنقطة الحالية، وعدد المسارات الفرعية ليست معاملات حالة رسم — يغطي ISO 32000-1 §8.4 ما تحفظه وتستعيده q وQ، والمسار الحالي قيد البناء ليس من ضمنها — بحيث تتتبع TPDFContentStateTracker تلك البيانات خارج الحالة المحفوظة كليًا، ويظل مسار فرعي بدأ قبل q موجودًا هناك، غير مرسوم، فور Q المطابقة. الثاني Q مجردة دون q مطابقة في أي مكان قبلها في التدفق، وهو ليس نادرًا في مخرجات مولّدات تجمّع أجزاء تدفق محتوى بالتسلسل وتخطئ المحاسبة. تحسب TPDFContentStateTracker.RestoreUnderflowCount كل واحد من تلك الأحداث بدلًا من إطلاق استثناء أو إفساد الحالة: Q غير المطابقة تترك ببساطة حالة الرسم الحالية تمامًا كما كانت، كما لو كانت تلك التعليمة عملية عدم-فعل، بحيث يستمر بقية التدفق في إعادة التشغيل على حالة سليمة ويمكن لمستدعٍ لا يزال أن يقرر لاحقًا، من العدد، ما إذا كان المدخل يستحق الإبلاغ عنه مرة أخرى لمن أنتجه
تركيب CTM، واستقلال مصفوفة النص عن q/Q، والتحقق المرحلي لمسار قص لا يعتمد على كيفية أو ما إذا كان تدفق المحتوى سيُرسَم على الإطلاق، وهذا بالضبط بيت القصيد: لقطة TPDFContentStateTracker نفسها صحيحة سواء لم تُرسَم الصفحة أبدًا على الإطلاق أو كانت على وشك أن تُسلَّم إلى أيًّا كانت الخلفية التي يختارها PDFlibPas لذلك الملف، بما في ذلك تبديل محرك وقت التشغيل المشروح في دليل الرسم متعدد المحركات لـPDF في PDFlibPas. يمكن لتحليل المحتوى، وتخطيط الإحداثيات، وأدوات التنقيح أن تعمل جميعها بالكامل على مخرجات المتتبع، طويلًا قبل أو دون طلب تدخل راسم على الإطلاق
إعادة تشغيل تدفق المحتوى عبر TPDFContentStateTracker جزء من إطار عمل تحرير المحتوى المُهيكل المدمج في PDFlibPas، مكتبة مكوّن PDF الأصلية القائمة على VCL لـDelphi وC++Builder