صفحة نص PDF تعرض أحرفًا وصناديق، لا خطوطًا أبدًا. يبني PDFium Component خطًا مرئيًا بتجميع صناديق أحرف مراكزها الرأسية تقع ضمن نصف ارتفاع حرف البذرة، ماسحًا للخارج من الحرف المنقور عليه حتى يُتجاوز التسامح. كل مسار تحديد في العارض يستدعي ذلك المساعد الواحد، بحيث تتفق الماوس، ولوحة المفاتيح، والكود
العرض الذي يُرسلك للبحث عن هذا محدد وغير سار. مستخدم ينقر ثلاث نقرات على فقرة في تقرير عمودين ويحصل على نصف الصفحة. أو ينقر ثلاث نقرات على خلية جدول ويبتلع التحديد الصف بأكمله بالإضافة إلى رقم الصفحة في التذييل. العارض ليس معطوبًا؛ إنه يسأل سؤالًا لا يستطيع الملف الإجابة عنه. لا يوجد سطر في PDF لتحديده، وأي تنفيذ يتظاهر بخلاف ذلك يُخمِّن. هذا المقال عن جعل التخمين متعمدًا وجعله متسقًا. إذا كان ما تحتاجه فعليًا سحب نص من مستند، راجع استخراج النص من مستندات PDF بواسطة PDFium؛ إذا كنت تُنسّق نصًا وتحتاج عروضًا، راجع قياس النص والتفاف الكلمات. هنا الموضوع أضيق: تحديد أين يبدأ سطر مرئي وينتهي، وتحديد ذلك بالضبط
لماذا ليس لصفحة نص PDF كائنات سطر؟
لأن دفق محتوى PDF يصف الرسم، لا البنية. تُعرِّف الفقرة §9.4 من ISO 32000-1 كائن نص كزوج BT / ET يحتوي مُشغِّلات تموضع وعرض. مُشغِّلات التموضع في §9.4.2 (Td، TD، Tm، T*) تُحرِّك مصفوفة نص حول الصفحة، ومُشغِّلات العرض في §9.4.3 (Tj، TJ، '، ") تطلي الحروف أيًا كان ما تشير إليه تلك المصفوفة حاليًا. لا شيء في ذلك النموذج يقول "هذه السلسلة من الحروف سطر". السطر هو ما يراه الإنسان بعد انتهاء الطلاء
المنتجون يجعلون هذا أسوأ بطرق لا يمكنك التحكم بها. فقرة مُبررة قد تُصدَر كمصفوفة TJ واحدة لكل سطر، أو كـTj واحد لكل كلمة مع Tm صريحة قبل كل واحدة، أو كعملية عرض واحدة بتعديلات تباعد الأحرف تحمل التباعد. تخطيط عمودين قد يُصدر العمود الأيسر من الأعلى للأسفل ثم العمود الأيمن، أو قد يتشابكهما إن مشى المُنتِج في قائمة كائناته الداخلية بترتيب مختلف. تسلسل الأحرف الذي يُسلِّمه لك PDFium يتبع دفق المحتوى، ودفق المحتوى يتبع أيًا كان ما رغب التطبيق المُولِّد فعله. لذا الدالتان اللتان تحصل عليهما فعليًا هما FPDFText_CountChars، التي تُبلغ عن عدد الأحرف التي تحملها الصفحة، وFPDFText_GetCharBox، التي تُعيد صندوق حدود حرف واحد في مساحة الصفحة. هذا هو المفردات الخام بأكملها. كل شيء فوق ذلك، الكلمات، الأسطر، الفقرات، الأعمدة، استدلال تُجريه على الهندسة
لماذا يُعدّ كشف CR وLF الاختبار الخاطئ؟
لأن الأحرف التي كنت ستختبرها ليست موجودة بشكل موثوق، وعندما تكون موجودة فهي ليست ملكك بشكل موثوق. يُدرج PDFium أحرفًا اصطناعية في صفحة النص لجعل النص المُستخرَج قابلًا للقراءة: مسافة حيث تكون سلسلتان منفصلتين بصريًا، وCR أو LF حيث تبدأ السلسلة التالية على خط أساس جديد. توجد FPDFText_IsGenerated تحديدًا حتى تستطيع تمييز تلك عن الأحرف التي خرجت من الملف، ويعرضها PDFium Component كخاصية CharacterGenerated
انقسم عند تلك الأحرف وترث كل قرار حكمي اتخذه PDFium أثناء تصنيعها. فاصل سطر صلب داخل فقرة مُلتفَّة والتفاف ناعم يبدوان متطابقين بعد التصنيع. صف جدول أصدره المُنتِج خلية بخلية قد لا يحصل على أي فاصل إطلاقًا بين الخلية الأخيرة والخلية الأولى للصف التالي، لأن خطوط الأساس تصادف أنها قريبة بما يكفي. في الوقت نفسه عنوان يتبعه نص أساسي بحجم مختلف قد يحصل على فاصلين حيث يرى الإنسان واحدًا. الأحرف المُولَّدة راحة عرض للاستخراج على مستوى الصفحة بأكملها؛ إنها ليست نموذج سطر، وتتدهور بالضبط في المستندات التي يهم فيها التحديد أكثر
تجميع صناديق الأحرف بالمركز الرأسي
الإشارة الموثوقة هي الهندسة. خذ الحرف الذي نقر عليه المستخدم كبذرة، احسب المركز الرأسي لصندوقه، وامشِ للخارج في كلا الاتجاهين بينما تبقي الصناديق المجاورة مراكزها الرأسية ضمن التسامح. يستخدم PDFium Component نصف ارتفاع صندوق البذرة كذلك التسامح، بحد أدنى 0.5 وحدة صفحة بحيث لا تنهار الصناديق المتدنية، نقطة، مسافة رفيعة، حرف بصندوق ارتفاعه شبه صفري، التسامح إلى لا شيء وتقطع السطر بعد حرف واحد
function TPdfView.LineRangeAt(TxtPage: FPDF_TEXTPAGE; CharIndex: Integer;
out StartIndex, Count: Integer): Boolean;
var
Lo, Hi, Total: Integer;
SeedBox, Box: TPdfRectangle;
SeedYMid, BoxYMid, HalfH: Double;
begin
Result := False;
StartIndex := -1;
Count := 0;
Total := FPDFText_CountChars(TxtPage);
if (CharIndex < 0) or (CharIndex >= Total) then
Exit;
if FPDFText_GetCharBox(TxtPage, CharIndex, SeedBox.Left, SeedBox.Right,
SeedBox.Bottom, SeedBox.Top) = 0 then
Exit;
SeedYMid := (SeedBox.Top + SeedBox.Bottom) / 2;
HalfH := Abs(SeedBox.Top - SeedBox.Bottom) / 2;
if HalfH < 0.5 then // floor for degenerate boxes
HalfH := 0.5;
Lo := CharIndex;
Hi := CharIndex;
while Lo > 0 do
begin
if FPDFText_GetCharBox(TxtPage, Lo - 1, Box.Left, Box.Right,
Box.Bottom, Box.Top) = 0 then
Break;
BoxYMid := (Box.Top + Box.Bottom) / 2;
if Abs(BoxYMid - SeedYMid) > HalfH then
Break;
Dec(Lo);
end;
while Hi < Total - 1 do
begin
if FPDFText_GetCharBox(TxtPage, Hi + 1, Box.Left, Box.Right,
Box.Bottom, Box.Top) = 0 then
Break;
BoxYMid := (Box.Top + Box.Bottom) / 2;
if Abs(BoxYMid - SeedYMid) > HalfH then
Break;
Inc(Hi);
end;
StartIndex := Lo;
Count := Hi - Lo + 1;
Result := True;
end;
ثلاث تفصيلات في تلك الحلقة تكسب موضعها. التسامح مُشتَق من البذرة لا من ثابت، لذا عنوان بحجم 24 نقطة يحصل على نطاق واسع ونص هامش بحجم 7 نقاط يحصل على نطاق ضيق، ولا يسرق أي منهما أحرفًا من الآخر. المقارنة تستخدم المراكز الرأسية بدلًا من خطوط الأساس أو قمم الصناديق، مما يُبقي حرفًا مرتفعًا، أو سلسلة مضمّنة بحجم مختلف، أو جملة بخطوط مختلطة على نفس سطر جيرانها. واستدعاء FPDFText_GetCharBox فاشل يُنهي المسح بدلًا من تخطيه، لأن حرفًا بلا هندسة قابلة للاسترجاع لا يُعطيك دليلًا في أي اتجاه، والاستمرار بعده سيسمح للمشي بالقفز عبر حدود حقيقية بقوة حرف أبعد
لماذا يجب أن يُشارك كل مسار تحديد مساعدًا واحدًا؟
لأن ثلاثة مسارات كود ينفذ كل منها "السطر" ستنحرف، وستنحرف بهدوء. في PDFium Component، توسيع النقر الثلاثي، وShift+Home، وShift+End، وطريقة SelectLineAt العامة كلها تُحلّ حدودها عبر نفس استدعاء LineRangeAt. النقر الثلاثي يبذره من مرساة التحديد؛ مفاتيح الشِفت تبذره من مؤشر التحديد وتُحرِّك تلك النهاية فقط؛ SelectLineAt تبذره من فهرس حرف يُقدِّمه المستدعي وتُسلِّم النتيجة إلى SelectTextRange، نفس مُدقِّق النطاق الذي يستخدمه مسار الماوس. كرّر المنطق بدلًا من ذلك والفشل ليس انهيارًا، إنه انحراف بطيء. يضبط أحدهم تسامح النقر الثلاثي لإصلاح تقرير بتباعد أسطر ضيق، والآن يتوقف Shift+End عند حرف واحد قبل حيث يتوقف النقر الثلاثي على نفس الفقرة. مستخدم يُحدّد سطرًا بالماوس، ويُوسِّعه بلوحة المفاتيح، ويشاهد التحديد ينكمش. لأن SelectLineAt تُغذّي خط أنابيب التحديد العادي، التحديد البرمجي أيضًا يبقى مستقلًا عن ما إذا كان إدخال الماوس مُفعّلًا، ولا يزال يحصل على تحقق النطاق، وإعادة الرسم، وإشعار OnSelectionChange مجانًا
// Select the visual line under a client-space point, then read it back
procedure TForm1.SelectLineUnderCursor(X, Y: Integer);
var
CharIndex: Integer;
begin
CharIndex := PdfView1.CharacterIndexAtPos(X, Y, 6.0, 6.0);
if CharIndex < 0 then
Exit;
if PdfView1.SelectLineAt(PdfView1.CurrentPage, CharIndex) then
Memo1.Lines.Add(PdfView1.SelectedText);
end;
لاحظ وسيطتَي التسامح على CharacterIndexAtPos. اختبار الإصابة له ارتخاؤه الخاص، مُعبَّرًا عنه بوحدات الصفحة، وهو اهتمام منفصل عن تسامح السطر. نقرة تقع في التباعد بين سطرين تُحل إلى أيًا كان الحرف الأقرب ضمن ذلك الصندوق؛ ثم يُشغَّل مسح السطر من أيًا كان ما تبيّن أنه ذلك الحرف. تغذية تسامح إصابة سخي جدًا في البذرة إحدى أسهل طرق تحديد سطر لم يكن المستخدم يُشير إليه
مساحتا فهرس: فهرس الحرف وفهرس النص
بمجرد أن يكون لديك نطاق، قاوم الرغبة في استخدامه كإزاحة سلسلة. تُعيد FPDFText_GetText نص الصفحة كمخزن مؤقت UTF-16، لكن فهارسها ليست نفس مساحة الفهرسة التي تستخدمها فهارس الأحرف في FPDFText_GetCharBox وFPDFText_CountChars. الأحرف المُولَّدة المُناقَشة أعلاه تجلس في مخزن النص المؤقت بينما تشغل فتحات أحرف بلا هندسة قابلة للاستخدام، ويتباعد الترقيمان عبر الصفحة. الجسران هما FPDFText_GetTextIndexFromCharIndex وFPDFText_GetCharIndexFromTextIndex، مُغلَّفان بواسطة PDFium Component كـCharacterIndexToTextIndex وTextIndexToCharacterIndex
var
TextStart, TextEnd: Integer;
begin
// char-index range from LineRangeAt -> offsets into the page text buffer
TextStart := Pdf.CharacterIndexToTextIndex(StartIndex);
TextEnd := Pdf.CharacterIndexToTextIndex(StartIndex + Count - 1);
if (TextStart >= 0) and (TextEnd >= TextStart) then
Caption := Pdf.Text(TextStart, TextEnd - TextStart + 1);
end;
الاتجاه الذي يُوقِع بأشد قوة هو العكسي. بحث مُنفَّذ فوق السلسلة المُستخرَجة يُعطيك فهارس نص، وتمرير تلك مباشرةً إلى واجهة صندوق أو تحديد برمجية يُعنون بصمت الأحرف الخاطئة، بخطأ ينمو كلما نزلت أكثر في الصفحة. حوّل بـTextIndexToCharacterIndex قبل أن يلمس أي شيء هندسي الرقم. أزواج البدائل تُضيف مشكلة إزاحة ثانية مستقلة فوق هذا، مشروحة في مقال الإيموجي، وCJK، وأزواج البدائل
أين ينحني الاستدلال
كن صادقًا مع نفسك بشأن الحدود، لأنها حقيقية وقابلة للوصول إليها. النص المُدار أوضح حالة: صندوق حرف مستطيل محاذٍ للمحور في مساحة الصفحة، لذا لنص مُدار 90 درجة تتوزع مراكز صناديق سطر مرئي واحد رأسيًا عبر الصفحة، ويتوقف المسح تقريبًا فورًا. ما تحصل عليه تحديد قصير لا تحديد خاطئ، وهو نمط الفشل الأفضل، لكنه يظل فشلًا. أوضاع الكتابة الرأسية تتصرف بنفس الطريقة لنفس السبب. تخطيطات العمودين تعمل عندما يكون العمودان مُزاحين رأسيًا عن بعضهما وتنكسر عندما لا يكونان كذلك. إذا شارك العمودان شبكة خط أساس، تجلس أحرف العمود الأيمن ضمن تسامح سطر العمود الأيسر، وسيمشي المسح مباشرةً عبر الفاصل، لأنه في الهندسة الصرفة لا يوجد شيء هناك ليتوقف عنده. كشف ذلك يحتاج اختبار فجوة أفقية فوق التجميع الرأسي، واختيار عتبة الفجوة قرار حكمي خاص به حول أي المستندات أنت مستعد لأن تكون مخطئًا بشأنها. أحجام الخطوط المختلطة الحالة التي يتعامل معها التسامح النسبي للبذرة بشكل جيد: نطاق كود مضمّن بحجم 8 نقاط داخل نص أساسي بحجم 11 نقطة يُبقي مركزه داخل النطاق، وعنوان بحجم 24 نقطة على خط الأساس التالي لا يسحب السطر الأساسي إلى نفسه
دلالات تحديد السطر الموصوفة هنا تُشحن في PDFium Component لـDelphi وC++Builder، إلى جانب واجهات اختبار الإصابة، ونطاق التحديد، وفهرس النص البرمجية المستخدمة في الأمثلة؛ صفحة المنتج تحمل المرجع الكامل لصفحة النص ونموذج التحديد