يدمج مكوّن PDFium لدلفي خطوط النظام التي يستخدمها TPdf.AddText بوصفها خطوط CID مفهرسة برمز لكل نقطة ترميز Unicode، فيحمل كل CID مطابقة ToUnicode واحدة بالضبط. ذلك ما يمنع المسافات المستخرجة من العودة U+00A0 (مسافة غير فاصلة) والواصلات U+00AD (واصلة لينة)، من المستند الحي ومن الملف المحفوظ معًا
العرَض قبيح لأنه غير مرئي. فهرس بحث يفوت «two-x» لأن السلسلة المخزنة تحوي واصلة لينة، وتصدير CSV يقسم على نحو مختلف، وأداة diff تعلّم أسطرًا تبدو متطابقة في كل عارض. لا شيء في الصفحة المرسومة خاطئ؛ الخاطئ هو Unicode خلف الحروف الرسمية
لماذا تعود المسافات المستخرجة U+00A0؟
تتحول المسافات المستخرجة إلى U+00A0 لأن خريطة ToUnicode CMap التي يولدها PDFium في FPDFText_LoadFont مفهرسة بالحرف الرسمي، والحرف الرسمي الواحد يبلغ إليه من نقطتي ترميز. في Arial يخدم الحرف الرسمي 3 كلا U+0020 وU+00A0، ويخدم حرف الواصلة كلا U+002D وU+00AD. لذا يربط CMap المولد CID نفسه مرتين، مرة عبر مدخل bfchar ومرة عبر bfrange بصيغة مصفوفة، ويربح المدخل الذي يفضله قانون أسبقية القارئ ويصير النص المستخرج
1 beginbfchar
<0003> <0020>
endbfchar
1 beginbfrange
<0003> <0010> [<00A0> ...]
endbfrange
ظل هذا التناقض بلا ضرر طويلًا، لأن قارئ PDFium كان يدع الأدنى من المطابقات يفوز. وقد قلب تغيير في المستوى الأعلى القارئ إلى الأعلى يفوز، ومن تلك البنية فصاعدًا استخرجت كل مسافة تكتب بـ AddText بوصفها NBSP وكل واصلة بوصفها واصلة لينة. ولاحظ النمط في الأزواج: تختلف 0x20/0xA0 و0x2D/0xAD في البت العالي فقط، وهو بالضبط ما تتوقعه من خط يرسل له الـ cmap شبيهات Latin-1 إلى المخطط التفصيلي نفسه. وإن كان كود الاستخراج لديك سليمًا أمس وفاشل اليوم على محارف غير مرئية، فأفرغ نقاط الترميز بدل الثقة بعرض المصحح؛ وأساسيات سحب النص مغطاة في استخراج النص من مستندات PDF عبر PDFium في دلفي
uses
SysUtils, PDFium;
const
// تشترك المسافة/U+00A0 والواصلة/U+00AD في حرف Arial رسمي واحد، وكذلك
// حرف Omega اليوناني (U+03A9) وإشارة Ohm (U+2126)
Sample: WString = 'two-x'#$00A0'y'#$00AD'z '#$03A9#$2126;
function CodePoints(const S: WString): string;
var
I: Integer;
begin
Result := '';
for I := 1 to Length(S) do
Result := Result + 'U+' + IntToHex(Ord(S[I]), 4) + ' ';
end;
var
Pdf: TPdf;
Live, Reloaded: WString;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
Pdf.AddPage(1, 595, 842);
Pdf.AddText(Sample, 'Arial', 12, 72, 770);
Live := Pdf.Text; // مستند حي غير محفوظ
Pdf.SaveAs('codepoints.pdf');
finally
Pdf.Free;
end;
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'codepoints.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
Reloaded := Pdf.Text; // بعد حفظ كامل وإعادة تحميل
finally
Pdf.Free;
end;
if (Live <> Sample) or (Reloaded <> Sample) then
Writeln('Mismatch: ', CodePoints(Live), '/ ', CodePoints(Reloaded));
end;
لماذا لم يكف ترقيع CMap بعد الحفظ
ترقيع الملف المحفوظ يصلح الملف المحفوظ فقط، وفقط إن أبقى الترقيع بنية CMap سليمة بايتًا ببايت. الإصلاح الأول، RepairSubsetToUnicodeCMaps في وحدة FPdfCompress، يجري بعد كل TPdf.SaveAs غير تزايدي ويحسم كل CID متضارب: مدخل bfchar يفوز، والزوج الذي يختلف في البت العالي فقط يحسم إلى أصغر نقطة ترميز Latin أساسية، وكل ما سوى ذلك يبقي مطابقته الأولى
الجزء المثير هو النتيجة السلبية. بني CMap المتضارب من جديد بنظافة، بصيغة start-code أو المصفوفة، بدا الخيار البديهي، ورفض PDFium كل CMap أعيد بناؤه رفضًا قاطعًا عائدًا إلى Identity. والناتج الوحيد الذي قبله القارئ الأصلي هو استبدال في الموضع بطول متساو للقيم الست عشرية المتضاربة، مع ترك تخطيط الكتلة وتغطية CID بلا مساس. والدرس الثاني كان أشد تواضعًا: لامحت ملاحظتنا حينها حالة الذاكرة على أن المستند الحي لا يملك تدفق ToUnicode أصلًا. وقد نبذ الاستدعاء المباشر للـ DLL ذلك، إذ يحمل المستند الحي التدفق الغامض نفسه، أي أن الإصلاح الحقيقي كان لا بد أن يقع قبل أن يولد PDFium الـ CMap أصلًا. وتبقى روتينية الإصلاح في المكتبة دفاعًا عن ملفات PDF أنتجتها أدوات أخرى مبنية على PDFium
uses
Classes, FPdfCompress;
var
Source, Dest: TFileStream;
begin
Source := TFileStream.Create('from-other-tool.pdf',
fmOpenRead or fmShareDenyWrite);
try
Dest := TFileStream.Create('repaired.pdf', fmCreate);
try
// تعديلات بطول متساو فقط؛ والملفات بلا تضارب قابل للإصلاح،
// وملفات تدفقات الإشارات المرجعية أو تدفقات الكائنات، تنسخ كما هي
RepairSubsetToUnicodeCMaps(Source, Dest);
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
فهرسة الخط بنقطة الترميز بدل الحرف الرسمي
الإصلاح الجذري هو التوقف عن طلب توليد CMap من PDFium كليًا. يسلم TPdf.LoadCachedFont الآن بايتات خط النظام إلى TPdf.LoadUnicodeKeyedCidFont، التي تقرأ جدول sfnt cmap الخاص بالخط، مفضلة جدولًا فرعيًا بالصيغة 12 وتعود إلى الصيغة 4 عند غيابه. تعود نقاط الترميز مرتبة ومزالة التكرار، ويسند CID k+1 إلى نقطة الترميز k، ويبقى CID 0 بوصفه .notdef. وترسل CIDToGIDMap صريحة كل CID إلى حرفه الرسمي، فتنال U+0020 وU+00A0 رسميَّي CID مختلفين يرسمان المخطط التفصيلي نفسه، وتربط خريطة ToUnicode CMap كل CID بنقطة ترميز واحدة فقط. ثم يحمَّل الخط عبر FPDFText_LoadCidType2Font، نقطة الدخول نفسها خلف الكتابة على مستوى الحروف الرسمية في تضمين خطوط CID Type 2 بخرائط CID إلى GID صريحة
// مختصرة من TPdf.LoadUnicodeKeyedCidFont
SetLength(CidToGidMap, (Length(Entries) + 1) * 2); // CID 0 = .notdef
for I := 0 to High(Entries) do
begin
CidToGidMap[(I + 1) * 2] := Byte(Entries[I].GlyphID shr 8);
CidToGidMap[(I + 1) * 2 + 1] := Byte(Entries[I].GlyphID and $FF);
end;
ToUnicode := BuildUnicodeKeyedCidCMap(Entries); // CID واحد، نقطة ترميز واحدة
Result := FPDFText_LoadCidType2Font(Document, @Data[0], Length(Data),
PAnsiChar(ToUnicode), @CidToGidMap[0], Length(CidToGidMap));
حين يكتب FPDFText_SetText سلسلة لاحقًا، يهبط البحث العكسي على CID واحد لكل محرف، فتنجو NBSP والواصلة اللينة وإشارة Ohm كل كما هي تحت أي قانون أسبقية، في الذاكرة وبعد أي حفظ. ولأن الملف المحفوظ يحمل تدفق ToUnicode الخاص بالمكوّن لا المولد من المحرك، لا يجد RepairSubsetToUnicodeCMaps ما يصلحه فيه
لماذا يستطيع مدخل bfrange واحد محو كتلة كاملة؟
bfrange واحد تمتد سلسلة CID فيه عبر حد xxFF يجعل PDFium يرمي الكتلة كلها التي يجلس فيها. لا تجيز ISO 32000-1 §9.10.3 إلا تغاير البايت الأخير من الوجهة داخل مدى، لكن جانب CID له فخه الخاص: يشتق HandleBeginBFRange في PDFium الـ CID الأعلى بوصفه (low and $FFFFFF00) or (high and $FF). فتقرأ سلسلة من CID 00FE إلى 0101 بوصفها 00FE إلى 0001، والأدنى أكبر من الأعلى، وتوسم الكتلة كلها غير صحيحة. والفشل صامت: ينجح SetText، وترسم الصفحة بشكل تام، ويعيد الاستخراج U+0000 لكل محرف في تلك الكتلة
تنهي BuildUnicodeKeyedCidCMap كل سلسلة قبل أن يبلغ نقطة الترميز أو الـ CID بايتًا منخفضًا يساوي FF، وتبقي كل كتلة داخل حد المداخيل البالغ 100 في قواعد CMap، وتكتب نقاط الترميز في المستوى التكميلي مداخيل bfchar فردية وجهاتها أزواج بديلة UTF-16، لأن زيادة زوج بديل داخل مدى لا معنى معرفًا له؛ وجانب الأزواج البديلة من تلك القصة في معالجة emoji وCJK وأزواج الاستبدال في دلفي. وCMap من bfchar وحده كانت ستتجاوز مشكلة الحدود كليًا، على حجم يفوق بعدة مرات
ماذا لا يغطيه الخط المفهرس بنقطة الترميز؟
يغطي المسار المفهرس بنقطة الترميز كل خط يعرض جدول cmap فرعيًا بترميز Unicode، ويعود إلى السلوك القديم المفهرس بالحروف الرسمية للبقية. والحدود التي تستحق المعرفة قبل الاعتماد عليه:
- خطوط Symbol ذات cmap من (3,0) فقط، وأي خط يعجز مسار CID عن تحميله، تمر عبر
FPDFText_LoadFontكما قبل، فقد يستخرج هناك حرف رسمي مشترك بين نقطتي ترميز بغموض بعد - بلا جدول فرعي بالصيغة 12 تقتصر الخريطة على BMP، وسقف عدد المداخيل 65535 كي يتسع كل CID في بايتين فوق الصفر
- الحفظ التزايدي (
saIncremental) يتخطىRepairSubsetToUnicodeCMapsعمدًا، لأن المراجعة التزايدية يجب أن تبقى إلحاقًا فقط؛ وخطوط الفهرسة بنقطة الترميز تجعل ذلك بلا موضوع للنص الذي يكتبه المكوّن بنفسه - مجموعات TrueType تحتاج عناية إضافية: يعيد GDI
GetFontDataملف .ttc كاملًا، وFPDFText_LoadCidType2Fontلا يملك معامل مؤشر وجه، فطلب NSimSun من simsun.ttc كان يضم ويرسم SimSun، الوجه 0. ويطابق المكوّن الآن اسم العائلة ضد جدول الاسم (nameID 1 و16) ويستخرج الوجه المطلوب sfnt مستقلًا قبل تحليل cmap؛ وإن فشل التحليل، عبرت بايتات المجموعة كما هي وعاد السلوك إلى الوجه 0
تتقاسم كتابة النص وتضمين الخطوط والاستخراج نموذج صفحة واحد عبر Delphi وC++Builder وLazarus، وAPI كامل موصوف في صفحة منتج مكوّن PDFium لدلفي