يعيد HotPDF Delphi Component بناء مسافات الكلمات وفواصل الأسطر في THotPDF.ExtractLoadedPageText من هندسة glyphs لا من محارف الفراغ. تدخل مسافة حين تتجاوز الفجوة بعد عرض glyph نفسه 0.15 من ارتفاع النص، ويبدأ سطر جديد فقط حين تنقلب أصول النص عبر جهة الكتابة بأكثر من نصف ارتفاع النص. ومنذ v2.768.3 يشمل نص الصفحة أيضاً النص المرسوم عبر Form XObjects ويترك خارجاً glyphs خارج منطقة القص المرئية. وبقية هذه المقالة تشرح لماذا تبدو كل قاعدة كما هي، لأن كل واحدة منها حلّت محل قاعدة أبسط كانت تنتج مخرجاً معقولاً وخاطئاً على مستندات واقعية
الأعراض مألوفة لكل من صب نص PDF في فهرس بحث. صفحة غلاف تُستخرج PDFReferenceManualNovember4,1998، ونموذج ضريبي يتشظى إلى 156 سطراً، وعلامة مائية مائلة تصل محرفاً واحداً لكل سطر، وبروفة مقصوصة تبدأ بسطر الطابعة الذي لا يعرضه أي عارض. لا ملف من هذه مكسور. كل واحد يستخدم وسيلة قانونية تماماً لتموضع النص يسيء إليها مستخرج ساذج
لماذا يفقد النص المستخرج من PDF مسافاته؟
يفقد النص المستخرج مسافاته لأن PDF لا يُشترط أبداً أن يحويها. يمكن للمنتج أن يفصل الكلمات بعرض محرف فراغ، لكنه بقدر ذلك أن يحرك القلم برقم داخل مصفوفة TJ (ISO 32000-1 §9.4.3) أو بـ Td جديدة (§9.4.2)، ومخرجات TeX وكثير من ملفات Distiller ومعظم التخطيطات المضبوطة التبرير تفعل ذلك بالضبط. قبل v2.766.76 كانت HPDFAssemblePageText تنظر في الحركة الرأسية وحدها، فاختفى كسر كلمة صنعه التموضع. تقيس المجمّعة الآن، على طول جهة كتابة glyph السابق، المسافة من نهاية عرضه هو إلى أصل glyph الحالي، وتدخل مسافة واحدة حين تتجاوز المسافة 0.15 من ارتفاع صندوق glyph الحالي، مقاساً من صاعد إلى هابط في فضاء المستخدم. لا تضاف مسافة إذا كان أحد الجانبين فراغاً أصلاً، ولا بين محرفي CJK، لأن التبرير يمدد الأيديوغرامات دون أن يعني ذلك التمدد حد كلمة. وسجلات glyphs تكشف الهندسة نفسها، فيمكنك إعادة إنتاج القرار حين يحيرك ملف بعينه
uses
SysUtils, HPDFDoc, HPDFContentStream;
procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
Glyphs: THPDFGlyphArray;
I: Integer;
Height, Gap: Double;
begin
if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
Exit;
for I := 1 to High(Glyphs) do
begin
// ارتفاع صندوق الـ glyph من صاعد إلى هابط، في فضاء المستخدم
Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
// نص أفقي: الفجوة من نهاية عرض الـ glyph السابق نفسه
Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
if (Height > 0) and (Gap > 0.15 * Height) then
Writeln(Format('U+%.4x gap %.2f height %.2f: space',
[Glyphs[I].Unicode, Gap, Height]));
end;
end;
لماذا يُقاس من عرض الـ glyph نفسه لا من موضع القلم؟
تقيس HotPDF فجوات الكلمات من GlyphEndX / GlyphEndY لأن موضع القلم بعد glyph يتضمن أصلاً تباعداً ليس فجوة. تعرف ISO 32000-1 §9.4.4 الإزاحة الأفقية عرضَ glyph في حجم الخط، زائد تباعد المحارف Tc، زائد تباعد الكلمات Tw، كلها مقيسة بـ Tz. تحمل BaselineEndX / BaselineEndY تلك الإزاحة كاملة، بينما تحمل GlyphEndX / GlyphEndY التقدم الخطي و Tz فقط. والفرق يهم للمنتجين الذين يشدّون التتبع بـ Tc سالبة ثم يعيدون المساحة عبر ضبط TJ بعد كل glyph: بالقياس من موضع القلم بدت الإعادة فجوة، واستُخرج المصطلح الصيني «95后» على صورة «9 5 后». والعتبة مربوطة بارتفاع صندوق glyph لا بحجم Tf لسبب مشابه. تصديرات Word كثيراً ما تكتب 1 Tf وتحمل الحجم الحقيقي في Tm مقيسة، فـ Tfs تقول 1 بينما النص بارتفاع 10 نقاط، وقاعدة مفتاحها Tfs تعاملت تهجئتي الصفحة نفسها معاملة مختلفة
وللقاعدة حواف صادقة. عنوان مضبوط بتتبع فضفاض جداً، حيث يفتح Tc وحده أكثر من 0.15 من ارتفاع النص بين الحروف، يُستخرج بمسافة بين كل حرفين، وهو ما تبدو عليه الصفحة لكنه على الأرجح ليس ما أردت فهرسته. وقطع مرسومة بترتيب مخالف على خط أساس واحد تنتج فجوة سالبة وتلتحق دون مسافة. لا حالة من الحالتين شائعة في نص المتن، وعلى مجموعة اختبار رفعت التغيير مطابقات الكلمات ضد مستخرج مرجعي على 28 صفحة دون أن تهبط أي واحدة
متى يبدأ HotPDF سطراً جديداً في النص المستخرج؟
منذ v2.766.79 يبدأ سطر جديد حين تتجاوز الحركة من أصل glyph السابق إلى الحالي، المسقطة على عمودي جهة الكتابة السابقة، نصف الارتفاع الأكبر لصندوقي الـ glyphين. كانت القاعدة الأقدم تقارن الحركة Y الخام بنصف Tfs، وفشلت في اتجاهين. مع 1 Tf و Tm مقيسة انكمشت العتبة إلى نصف وحدة، فحرف مرتفع بارتفاع نصي 0.4 أو اهتزاز خط أساس عادي كسر السطر. وتجاهلت القاعدة الـ X كلياً أيضاً، فنص تحت Tm مدورة نزل الصفحة مع كل glyph وخرج glyphاً واحداً لكل سطر. والإسقاط على عمودي الجهة يجعل الجريات المدورة تتصرف كالأفقية، وأخذ الارتفاع الأكبر بين الاثنين يبقي كلمة عينة كبيرة وتعليقها الصغير على سطر واحد حين يتشاركان خط أساس. وفي نموذج الضريبة المذكور هبط عدد الأسطر من 156 إلى 97. والنص العمودي في وضع كتابة 1 (§9.7.4.3) يسلك مساراً منفصلاً: تلك الـ glyphs تُجمع أعمدة وتقرأ من اليمين إلى اليسار ومن أعلى إلى أسفل، بكسر سطر عند كل تغيير عمود
ما النص الذي يشمله ExtractLoadedPageText وأيهما يترك؟
يعيد ExtractLoadedPageText النص الذي يعرضه عارض. ومنذ v2.766.80 يعمل من glyphs المرئية فقط، مطيحاً بكل glyph يقع مركز صندوقه خارج GetLoadedPageVisibleBox، أي CropBox مقصوصة على MediaBox (§14.11.2). ذلك يطيح بأسطر الطابعة وعلاماتها الأخرى المنصوبة نصاً خارج منطقة القص. و ExtractLoadedPageGlyphs تستمر عمداً في إعادة كل glyph من تدفق محتوى الصفحة، فلا تزال تستطيع إيجاد تلك المواد حين تحتاجها. والمرشح اختبار صندوق لا اختبار رؤية: النص المخفي بمسار قص، أو المرسوم أبيض، أو المغطى بصورة، ما زال يُستخرج
var
Pdf: THotPDF;
Glyphs: THPDFGlyphArray;
PageText: UnicodeString;
L, B, R, T: Single;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('trimmed-proof.pdf');
if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
// كل glyph في تدفق محتوى الصفحة، وسطر الطابعة مشمول
if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
Writeln(Length(Glyphs), ' glyphs in the page content stream');
// ما تعرضه الصفحة فقط، مع دمج نص Form XObject داخله
if Pdf.ExtractLoadedPageText(0, PageText) then
Writeln(PageText);
finally
Pdf.Free;
end;
end;
والنص المرسوم عبر Form XObjects جزء من نص الصفحة منذ v2.768.3. الرؤوس والأختام والعلامات المائية تسكن الأشكال غالباً، وبعض مستندات المعايير فقدت 30 إلى 35 بالمئة من محارفها قبل التغيير. تسجل THotPDF.InterpretContentWithForms كل Do مع الـ CTM السارية، وتفسر الشكل عند /Matrix الخاصة به مضروبة في تلك CTM (§8.10.1)، وتدمج glyphs الشكل عند موضع الـ Do، متكررة داخل الأشكال المتداخلة. والشكل بلا /Resources خاصة يستعير موارد التدفق الذي يرسمه، كما يسمح §7.8.3. تحمل glyphs الأشكال TokenIndex = -1، وتعيد ExtractLoadedPageGlyphs glyphs التدفق الصفحي وحدها بعد، لأن البحث والاستبدال والحجب يكتبون التغييرات عبر TokenIndex وكانوا سيحررون بايتات خاطئة لو تسلل glyph شكل. وقبل مبسّطتان يستحقان المعرفة: نص الشكل لا يُقص على /BBox الخاص به، والتكرار يتوقف عند 12 مستوى لا بكشف دورات، فشكل مشوه يرسم نفسه يعيد نصه حتى يبلغ ذلك السقف
لماذا كان النص بعد معامل Q يفك ترميزه عبثاً؟
كان النص بعد Q قد يفك ترميزه خطأً قبل v2.766.73 لأن المستخرج كان يحفظ الـ CTM فقط عند q. ومعاملات حالة النص، أي الخط والحجم و Tc و Tw و Tz و TL ووضع العرض والارتفاع، تنتمي إلى حالة الرسوميات (§9.3.1)، فـ Q يجب أن تستعيدها مع كل شيء آخر على الرصة (§8.4.2). تقرير صناعي اختار خطاً ثنائي البايت من نوع Identity-H داخل q … Q ثم عرض نصاً أحادي البايت WinAnsi دون Tf خاصة. أبقى المستخرج الخط الداخلي، وقرأ الخطوط القائدة وكلمة «Adobe» في جدول المحتويات بوصفها رموزاً ثنائية البايت، وأطاح بـ 15% من محارف الصفحة. رصة q/Q للمفسر تحمل الآن حالة النص كاملة. وقواعد الاستخراج الموصوفة هنا تنطبق على كل صفحة، فيمكن لمستند كامل أن يذهب إلى ملف في استدعاء واحد
var
Output: TFileStream;
Pages: Integer;
begin
Output := TFileStream.Create('report.txt', fmCreate);
try
// نطاق فارغ = كل الصفحات؛ تغذية ورق بين الصفحات؛ BOM لـ UTF-8
Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
Writeln(Pages, ' pages extracted');
finally
Output.Free;
end;
end;
أي واجهة نص HotPDF تستخدم؟
تبقى ExtractLoadedPageText بترتيب تدفق المحتوى، وهو الافتراضي الصائب للبحث والفهرسة؛ وسلسلة فك الترميز تحتها مغطاة في استخراج النص من PDF محمّلة بـ HotPDF. وللمستندات الموسومة التي يهمها ترتيب التأليف يتجول استخراج النص بترتيب البنية على شجرة البنية بدل التخمين من الهندسة، وللبيانات المحبوسة في جداول تعيد استخراج الجداول المكتوبة النوع عبر فواصل الصفحات خلايا لا أسطراً. والمرجع الكامل للـ API وتحميل تجريبي على صفحة منتج HotPDF Delphi PDF Component