استخرج رموزًا تعبيرية أو اسم سجل عائلي ياباني من ملف PDF كنص، وستظهر المخرجات مربعًا، أو علامة استفهام، أو لا شيء على الإطلاق حيث يجب أن يكون الحرف. خاصية Character[] في مكوّن PDFium هي عادة السبب: فهي تقرأ كل حرف عبر FPDFText_GetUnicode، التي تُعيد نقطة شيفرة يونيكودية كاملة كقيمة غير موقّعة من 32 بت، ثم تعرضها لـDelphi كـWideChar واحدة من 16 بت. لا يمكن لأي نقطة شيفرة تتجاوز U+FFFF إجراء تلك الرحلة كقطعة واحدة، ولا يظهر الفساد أبدًا بينما تنظر إلى الصفحة المرسومة، لأن الرسم واستخراج النص يمران عبر مسارات شيفرة منفصلة في PDFium — يمكن لمستند أن يعرض رموزه التعبيرية بشكل مثالي ويظل يسلّمك عبثًا بمجرد أن تقرأ Character[] في حلقة وتبني سلسلة منها
المستوى الأساسي متعدد اللغات ولماذا تتوقف WideChar عند U+FFFF
WideChar في Delphi نوع من 16 بت لا يمكنه حمل سوى وحدة شيفرة UTF-16 واحدة. يلائم المستوى الأساسي متعدد اللغات في يونيكود، النطاق من U+0000 إلى U+FFFF، ذلك بالضبط، وهذا سبب عودة اللاتينية، والسيريلية، واليونانية، وكتلة حروف CJK الصينية الموحَّدة الشائعة جميعها ذهابًا وإيابًا عبر WideChar واحدة دون حادث. تقع عائلتان من المحارف روتينيًا خارجه في مستندات حقيقية: الرموز التعبيرية، الكثير منها في كتلة الوجوه التعبيرية بادئة من U+1F600، وحروف CJK النادرة من امتداد B الصيني الموحَّد، النطاق من U+20000 إلى U+2A6DF المحجوز للحروف الصينية واليابانية والكورية الأقل شيوعًا بما في ذلك العديد من الأسماء الشخصية وأسماء الأماكن. يعالج UTF-16 أي شيء فوق U+FFFF بزوج بديل (surrogate pair) — وحدتا شيفرة من 16 بت، بديل عالٍ في النطاق $D800 إلى $DBFF متبوعًا ببديل منخفض في $DC00 إلى $DFFF، يشفّران معًا نقطة شيفرة واحدة — والحسابات خلف ذلك الاقتران ثابتة بما يكفي لعرضها مباشرة بلغة Pascal
function ToSurrogatePair(CodePoint: LongWord; out Hi, Lo: WideChar): Boolean;
var
V: LongWord;
begin
Result := CodePoint > $FFFF;
if Result then
begin
V := CodePoint - $10000;
Hi := WideChar($D800 + (V shr 10));
Lo := WideChar($DC00 + (V and $3FF));
end;
end;
غذِّ U+1F600، رمز الوجه المبتسم التعبيري، عبر تلك الدالة والنتيجة بديل عالٍ $D83D وبديل منخفض $DE00، قيمتان من 16 بت، لا واحدة. لا يعني أي نصف شيئًا بمفرده؛ بديل عالٍ منفرد $D83D يجلس في سلسلة دون $DE00 خلفه بديل معلَّق، ومعظم شيفرة معالجة النص التي تصادفه إما تُسقطه، أو تستبدله بحرف بديل، أو تُطلق خطأً
لماذا تُعيد FPDFText_GetUnicode قيمة لا تستطيع Character[] حملها؟
تُعيد FPDFText_GetUnicode قيمة LongWord، وهي قيمة كاملة من 32 بت، لأن ترميز نص PDF يحمل بالفعل القيمة العددية اليونيكودية الكاملة لكل حرف. تُخطِّط خريطة ToUnicode CMap الخاصة بملف PDF رموز الحروف إلى نص يونيكودي، وعندما يمثّل حرف ما يُسمّى بشكل غير رسمي حرفًا من المستوى النجمي (astral-plane) — أي شيء يتجاوز المستوى الأساسي متعدد اللغات — يكون ذلك التخطيط نقطة شيفرة كاملة، لا جزءًا من 16 بت. يفكّ PDFium ترميزها مرة أخرى إلى قيمة عددية داخليًا ويعيدها عبر حدود مكتبة DLL عبر FPDFText_GetUnicode، وذلك الحد هو بالضبط حيث يتعين على قيمة من 32 بت أن تصبح شيئًا يمكن لخاصية Delphi إعادته إلى شيفرتك
التنفيذ الواضح هو WideChar(FPDFText_GetUnicode(TextPage, Index))، وهو أيضًا الخاطئ. تحويل صارم من قيمة 32 بت إلى نوع 16 بت يحتفظ فقط بأدنى 16 بت ويُلقي بالباقي بصمت، دون استثناء ودون فحص نطاق. بالنسبة لـU+1F600 يعني ذلك الاحتفاظ بـ$F600 وفقدان حقيقة أن القيمة الحقيقية كانت فوق U+FFFF على الإطلاق، ما ينتج وحدة شيفرة ليست حتى بديلًا معلَّقًا صالحًا، مجرد حرف مستوى أساسي متعدد اللغات غير مرتبط صادف أن يتشارك تلك البتات المنخفضة. اربط بضعة آلاف من تلك في سلسلة ولن تملك الشيفرة اللاحقة أي طريقة متبقية للتمييز بين حرف فاسد وآخر شرعي
ما الذي تعيده Character[] وCharcode[] لنقاط شيفرة المستوى النجمي الآن
تُعيد خاصيتا Character[] وCharcode[] في مكوّن PDFium U+FFFD، حرف الاستبدال اليونيكودي، كلما تجاوزت نقطة الشيفرة الكامنة U+FFFF، بدلًا من اقتطاعها بصمت. يجلس ذلك الحارس مباشرة داخل جالب الخاصية خلف Character[]
function TPdf.GetCharacter(Index: Integer): WideChar;
var
Code: LongWord;
begin
LoadTextPage;
Code := FPDFText_GetUnicode(FTextPage, Index);
if Code > $FFFF then
Result := #$FFFD // astral-plane code point: cannot fit in one WideChar
else
Result := WideChar(Code);
end;
إعادة U+FFFD بدلًا من جزء مقتطَع إصلاح متعمد وضيق بدلًا من إعادة تصميم. تُعرَّف Character[] وCharcode[] بنوع WideChar على كل من TPdf وTPdfView، وتوسيع نوع الإرجاع ذلك لحمل نقطة شيفرة كاملة كان سيكسر كل مستدعٍ موجود يتوقع أن يعني حرف واحد لكل فهرس قيمة واحدة من 16 بت. U+FFFD هو حجز المكان المُعيَّن الخاص بمعيار يونيكود نفسه لهذا الموقف بالضبط، بحيث يحصل مستدعٍ يتحقق منه على إشارة مُعرَّفة وموثَّقة بدلًا من بيانات خاطئة بصمت. حالة حدّية واحدة تستحق المعرفة: U+FFFD حرف شرعي بحد ذاته أيضًا، بحيث في المستند النادر الذي يحتوي بالفعل حرف استبدال حقيقيًا، لا يمكن تمييز ذلك الفهرس عن حرف نجمي مقتطَع بالقيمة وحدها
كيف تستخرج نص الرموز التعبيرية وامتداد CJK B بشكل صحيح في Delphi؟
استدعِ Text بدلًا من اجتياز Character[] كلما كان المحتوى النصي الفعلي مهمًا، لأن Text تقرأ عبر FPDFText_GetText وتُعيد WString كاملة بأزواج بديلة صحيحة لكل حرف من المستوى النجمي في النطاق، بدلًا من قيمة ثابتة العرض لكل فهرس. تستخرج Pdf.Text(0, MaxInt)، أو الاختصار Pdf.Text، صفحة كاملة بشكل صحيح في استدعاء واحد، وتسحب Pdf.Text(StartIndex, Count) نطاقًا أصغر بالطريقة نفسها. لا تزال Character[] تستحق مكانها عندما تحتاج فقط بيانات موضع، أو خط، أو علم عند فهرس ولا تلمس نقطة الشيفرة نفسها أبدًا — لا تهتم CharacterOrigin[] وFontSize[] وCharacterMapError[] بما إذا كان الحرف الكامن نجميًا
function ExtractLineSafely(Pdf: TPdf): WString;
var
I: Integer;
begin
Result := '';
for I := 0 to Pdf.CharacterCount - 1 do
if not (Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I]) then
Result := Result + Pdf.Text(I, 1); // full code point, never a truncated WideChar
end;
فحص تخطي-المُولَّد-وغير-المُخطَّط في تلك الحلقة هو النمط نفسه المستخدَم لاستخراج النص العادي في استخراج النص من مستندات PDF عبر مكوّن PDFium؛ التغيير الوحيد هو السطر الأخير، الذي يبادل إلحاقًا مباشرًا لـCharacter[I] باستدعاء أحادي الفهرس إلى Text بحيث تصل الحروف النجمية كأزواج بديلة كاملة بدلًا من حروف استبدال نائبة
أين يعضّ هذا فعليًا: تصديرات محادثات، وأسماء شخصية، وخطوط CJK مضمَّنة
تظهر الرموز التعبيرية أينما يلتقط PDF اتصالًا غير رسمي: سجلات محادثات مصدَّرة، وتفريغات مراجعات متجر تطبيقات، ونصوص نظام تذاكر محفوظة إلى PDF لأرشيف امتثال. يظهر امتداد B لـCJK في مكان أضيق لكن أعلى المخاطر، الأسماء الشخصية وأسماء الأماكن، لأن السجلات العائلية اليابانية، وسجلات الأسر الصينية، ووثائق الهوية التايوانية مصادر كلاسيكية لحروف لم تصل قط إلى كتلة CJK الشائعة. خط أنابيب رواتب أو تحقق من الهوية يستخرج أسماء من أوراق حكومية ممسوحة ضوئيًا هو بالضبط نوع عبء العمل حيث يتحول حرف مشوَّه بصمت إلى تطابق فاشل بدلًا من عيب تجميلي
تميل حروف CJK النادرة أيضًا إلى السفر مع مشكلات خطوط، لا ترميز فقط، لأنه يتعين على خط حمل حرف لنقطة شيفرة في نطاق U+20000 قبل أن يمكن لأي شيء الرسم على الإطلاق، وقلة من خطوط النظام المثبَّتة تفعل ذلك. أي شخص يجتاز بالفعل FontIsEmbedded[] لكل حرف بالطريقة التي يصفها قراءة خصائص خط PDF عبر مكوّن PDFium يجب أن يتحقق من الفهرس نفسه لكلتا المشكلتين معًا: فهرس يُعيد U+FFFD من Character[] ويبلّغ عن خط غير مضمَّن هو مستند لن يستخرج ولن يطبع ذلك الحرف بشكل صحيح، والإصلاح ينتمي في المنبع إلى كيفية إنتاج ملف PDF، لا في شيفرة الاستخراج الخاصة بك
خصائص Character[] وCharcode[] وText الموصوفة هنا جزء من مكوّن PDFium القياسي لـDelphi وC++Builder