مقال تقني

البحث عن النصوص واستبدالها في ملف PDF موجود باستخدام دلفي

يمكن لمكون HotPDF البحث عن النصوص واستبدالها داخل ملف PDF موجود من دلفي و C++Builder. وتحدد SearchLoadedPageText و SearchLoadedDocumentText موقع كل تكرار لسلسلة نصية بدقة على مستوى الأشكال الرمزية، وتقوم ReplaceLoadedPageText و ReplaceLoadedDocumentText بإعادة كتابة البايتات المتطابقة في مكانها — بشرط إمكانية إعادة تشفير كل حرف بديل من خلال الخط الأصلي، وهو قيد مادي يتعامل معه هذا المقال بصدق بدلاً من إخفائه في حاشية سفلية

الطلب وراء هذه الميزة يكون دائمًا عاديًا. تغير شركة اسمها ولا تزال ثلاثة آلاف فاتورة مؤرشفة تحمل الاسم القديم. قالب عقد تم شحنه مع تاريخ انتهاء صلاحية العام الماضي. تم إلغاء رمز منتج ويحتاج كل جدول بيانات يذكره إلى رمز بديل بدلاً منه. في معالج النصوص، تعد كل من هذه المهام عملًا يستغرق ثلاثين ثانية. وفي ملف PDF، تعد هذه مشكلة صعبة حقًا، وفهم السبب يصنع الفارق بين استخدام واجهة برمجة التطبيقات بشكل جيد وتقديم تقرير عن خطأ هو في واقع الأمر اقتباس من المواصفات

لماذا يعد استبدال النصوص في ملف PDF صعبًا للغاية؟

يعد استبدال النصوص في ملف PDF صعبًا لأن صفحة PDF لا تحتوي على نص قابل للتحرير — بل تحتوي على أشكال رمزية محددة الموضع. وتحت نموذج إظهار النص الخاص بمعيار ISO 32000-1 §9.4، يوجه تدفق المحتوى معاملات مثل Tj و TJ التي ترسم سلاسل من رموز الأحرف عند إحداثيات تحددها مصفوفة النص. وتلك الرموز ليست Unicode؛ بل هي فهارس في أي ترميز يعلنه خط الصفحة، وقد تعيش المطابقة مرة أخرى إلى أحرف قابلة للقراءة في خريطة /ToUnicode CMap، أو مصفوفة فروق ترميز، أو سلسلة مطابقة CID. ولا يوجد كائن فقرة، ولا تدفق نص، ولا يوجد ضمان بأن كلمة مرئية واحدة مخزنة كسلسلة نصية واحدة

يضيف الاستبدال طبقة ثانية من الصعوبة فوق فك التشفير: يجب أن تعرف بالضبط أي بايتات من التدفق الأصلي أنتجت كل شكل رمزي، بحيث يمكنك دمج بايتات جديدة في هذا النطاق بدقة وليس في أي شيء آخر. ويمكن لمستخرج النصوص التخلص من مواضع البايت بمجرد حصوله على Unicode. ولا يستطيع المستبدل ذلك. ولهذا السبب قسم HotPDF العمل عبر إصدارين — بنى الإصدار v2.251.0 طبقة تتبع الإزاحة والبحث، وبنى الإصدار v2.252.0 طبقة إعادة الكتابة فوقها

العثور على النصوص: البحث على مستوى الأشكال الرمزية مع تتبع إزاحة البايت

تجد الدالة SearchLoadedDocumentText في HotPDF كل تكرار لهدف من خلال المطابقة مع تسلسل أشكال Unicode الرمزية المفككة لكل صفحة، وليس مع بايتات التدفق الخام، وبالتالي فإن المطابقة تكون صحيحة بغض النظر عن كيفية ترميز الخط لها. وتم تقديم البنية التحتية الأساسية في الإصدار v2.251.0: يسجل المحلل اللغوي لتدفق المحتوى نطاق بايت StartOfs/EndOfs لكل معامل سلسلة نصية — بما في ذلك محدداته ( ) أو < > — ويحمل كل شكل رمزي مفكك ثلاثية TokenIndex/ItemIndex/ByteOffset تشير إلى المعامل الدقيق، وعنصر مصفوفة TJ، ووحدة الرمز التي أنتجته. ويوجه مترجم الأشكال الرمزية نفسه واجهة برمجة تطبيقات الاستخراج الموضحة في استخراج النصوص من ملف PDF محمل في دلفي؛ ويحتفظ البحث ببساطة بأصل البيانات الذي يتخلص منه الاستخراج

كل مطابقة تأتي كسجل THPDFTextMatch يحمل فهرس الصفحة، ونطاق الأشكال الرمزية الشامل، وأصل X/Y لمساحة المستخدم وعرض المطابقة، وعنصر الرمز المصدر وفهرس العنصر، والنص المطابق نفسه. ويكفي ذلك لتوجيه تراكب التمييز، أو واجهة مستخدم المراجعة، أو خطوة الاستبدال. والبحث الذي لا يجد شيئًا يرجع مصفوفة فارغة بدلاً من الفشل، لذا يظل نمط الاستدعاء بسيطًا

var
  Pdf: THotPDF;
  Matches: THPDFTextMatchArray;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('invoices-2025.pdf') > 0 then
    begin
      if Pdf.SearchLoadedDocumentText('Acme Corp', False, Matches) then
        for I := 0 to Length(Matches) - 1 do
          WriteLn(Format('page %d at (%.1f, %.1f): "%s"',
            [Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
             Matches[I].Text]));
    end;
  finally
    Pdf.Free;
  end;
end;

يستحق خيار تصميم مقصود واحد ملاحظة. فعندما تكون CaseSensitive هي False، فإن المقارنة تطوي حالة الأحرف لأحرف ASCII فقط، حسب التصميم: يتصرف طي حالة الأحرف الكامل في Unicode بشكل مختلف عبر سلاسل أدوات Delphi 5 إلى XE التي يدعمها HotPDF، وواجهة برمجة تطبيقات بحث تجد مطابقات مختلفة اعتمادًا على المترجم (compiler) الذي بنى تطبيقك هي أسوأ من تلك التي لها حد موثق ويمكن التنبؤ به. بالنسبة لنصوص الأعمال اللاتينية — الأسماء والرموز والتواريخ — يغطي طي ASCII الحالات العملية

استبدال النصوص: الترميز العكسي والدمج الجراحي

تعيد ReplaceLoadedDocumentText، المضافة في HotPDF الإصدار v2.252.0، كتابة كل تكرار لهدف من خلال تشغيل آليات فك التشفير بشكل عكسي. والدالة HPDFEncodeUnicode هي عكس وحدة فك تشفير رمز الحرف: فهي تسير عبر نفس سلسلة الاستراتيجيات بشكل عكسي — بحث bfchar و bfrange في /ToUnicode، ومطابقة CID لتدفق الترميز، ومطابقات الهوية لـ Type0، وجداول WinAnsi و MacRoman المحددة مسبقًا — لتحويل كل حرف بديل مرة أخرى إلى بايتات رمز الحرف التي يتوقعها الخط الأصلي. ثم يتم تسلسل البايتات المعاد تشفيرها إلى سلسلة نصية حرفية جيدة التنسيق أو سلسلة ست عشرية، مما يعكس قواعد الهروب الخاصة بالمحلل اللغوي بحيث تكون دورة التحليل وإعادة التسلسل مستقرة

الدمج نفسه جراحي وليس كليًا. فيتم فقط استبدال نطاق بايت الرمز الذي تغطيه المطابقة داخل معامل السلسلة؛ ويتم الحفاظ على البايتات غير المتطابقة في نفس المعامل، والمساحة البيضاء بين الرموز، وكل معامل محيط حرفيًا، بايت ببايت. واستبدال bca داخل abcabc ينتج a + البديل + bc، وليس معاملاً تالفًا. وقد تكون البدائل أقصر أو أطول من الهدف — وتتم إعادة تسلسل الحرف وتحديث /Length الخاص بالتدفق — ويتم معالجة كل تدفق /Contents لصفحة متعددة التدفقات بشكل منفصل بحيث تظل الصفحة جيدة التنسيق

var
  Pdf: THotPDF;
  ReplaceCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract-draft.pdf') > 0 then
    begin
      if Pdf.ReplaceLoadedDocumentText('2025-12-31', '2026-12-31',
        True, ReplaceCount) then
        WriteLn(Format('%d operand rewrites performed', [ReplaceCount]));
      Pdf.SaveLoadedDocument('contract-final.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

لاحظ ما لا تفعله واجهة برمجة التطبيقات: فهي لا تعيد تنضيد الصفحة. فملف PDF لا يحتوي على إعادة تدفق، وبالتالي فإن البديل الذي يكون عرضه المرئي أكبر من الأصلي سيشغل ببساطة مساحة أفقية أكبر وقد يزاحم أي شيء مرسوم على يمينه. وتعد البدائل ذات الطول المماثل أو القريب — التواريخ وسلاسل الإصدارات وأرقام الأجزاء وتصحيحات الأسماء — هي الخيار الأمثل. وتغيير الكلمات بالكامل ينتمي إلى المستند المصدر، وليس إلى ملف PDF

لماذا لا يمكنك استبدال النصوص بأحرف لم تتضمنها تجزئة الخط أبدًا؟

لا يمكنك استبدال نص بحرف لم تتضمنه تجزئة الخط المضمنة أبدًا، لأن تسلسل البايت الذي سيحدد هذا الحرف ببساطة غير موجود في جداول مطابقة الخط. وعندما يضمن منتج PDF خطًا مجزأً، فإن خريطة /ToUnicode CMap وهياكل الترميز الخاصة به تغطي فقط الأشكال الرمزية التي استخدمها المستند الأصلي بالفعل. وتستطيع HPDFEncodeUnicode فقط عكس مطابقة موجودة: فإذا لم يحتوي المستند أبدًا على الحرف E بهذا الخط، فلا يوجد رمز حرف لـ E للعكس إليه. وهذه خاصية مادية للملف، وليست قصورًا في أي مكتبة معينة — فلا توجد أداة يمكنها استحضار مطابقة أشكال رمزية لم يتم تضمينها أبدًا

يتعامل HotPDF مع الفشل بشكل متحفظ. فإذا تعذر إعادة تشفير أي حرف فردي من البديل، يتم تخطي تكرار الهدف بالكامل — بدون استثناء، وبدون نصوص جزئية غير مفهومة، ولا يتم ببساطة احتساب التكرار في ReplaceCount. والنتيجة العملية: تحقق من ReplaceCount مقابل عدد المطابقات من بحث سابق، وتعامل مع النقص كإشارة. في مثال التاريخ أعلاه، يجب أن يظهر الرقم 6 في مكان ما في نص المستند بنفس الخط لتنجح إعادة الكتابة — وهو أمر محتمل في فاتورة، وغير مضمون بشكل عام. وعندما لا تتوفر الأحرف التي تحتاجها ببساطة ويكون الهدف هو إزالة النصوص الحساسة بدلاً من إعادة صياغتها، فإن إزالة المحتوى الحقيقي هي الأداة الأفضل على أي حال؛ انظر تحرير وإعادة هيكلة ملفات PDF المحملة في دلفي لهذا المسار

var
  Matches: THPDFTextMatchArray;
  Expected, Replaced: Integer;
begin
  Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
  Expected := Length(Matches);
  Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
  if Replaced < Expected then
    WriteLn(Format('%d occurrence(s) skipped: characters missing ' +
      'from the font subset, or match spans multiple operands',
      [Expected - Replaced]));
end;

شرط التخطي الثاني في تلك الرسالة هو الحد الموثق الآخر: فالهدف الذي يمتمد عبر معاملات سلسلة متعددة — مثل تقسيم Hello عبر عناصر [(He)(llo)] TJ — يتم العثور عليه بواسطة البحث، لأن البحث يطابق تسلسل الأشكال الرمزية المفككة، ولكن يتم تخطيه بواسطة الاستبدال، لأن إعادة الكتابة عبر حدود المعاملات تتطلب دمج نطاقات البايت المتجاورة. ويجعل البحث ثم التحقق كلا الحدين مرئيين بدلاً من كونهما صامتين

ما الذي يتغير في الملف عند الحفظ؟

يتم حفظ تدفق /Contents المستبدل دون ضغط. ويتم إلغاء ضغط التدفقات المضغوطة بـ FlateDecode للتحرير، وعندما يكتب HotPDF البايتات المعاد بناؤها فإنه يسقط إدخال /Filter الخاص بالتدفق ويحدث /Length بدلاً من إعادة الضغط. ويكون ملف PDF الناتج صالحًا تمامًا ويعرض بشكل طبيعي في أجهزة العرض الرئيسية؛ والمقايضة هي ملف أكبر لكل تدفق تم تحريره. بالنسبة لخط تدفق دفعي يعالج آلاف المستندات، ضع ميزانية لهذا النمو أو قم بتشغيل تمريرة ضغط منفصلة في المراحل اللاحقة. وكيفية تفاعل الكائنات المعاد كتابتها مع بنية المراجع الترافقية للمستند عند الحفظ هو موضوع خاص به، تمت تغطيته في تدفقات الكائنات والتحديثات التزايدية في HotPDF

يتم ترك كل شيء آخر في الملف كما هو. فتحتفظ التدفقات التي لم يتم مسها بضغطها، ولا يتم إعادة كتابة الخطوط والصور، والدمج على مستوى المعامل يعني أنه حتى التدفقات المحررة تختلف عن الأصلية فقط حيث استقرت المطابقة. وهذا التحيز مقصود: فكلما أعادت المكتبة كتابة المزيد من مستند محمل، زادت الفرص لكسر ميزة للمنتج لم تكن تتوقعها

ينضم البحث عن النصوص واستبدالها إلى استخراج النصوص وتحريرها وتمثيل الصفحات في مجموعة أدوات المستندات المحملة في HotPDF، وكلها موجهة بواسطة نفس مترجم تدفق المحتوى ومتاحة من Delphi 5 وحتى إصدارات RAD Studio الحالية دون تبعيات خارجية. ومرجع واجهة برمجة التطبيقات الكامل وتنزيل النسخة التجريبية متاحان على صفحة منتج مكون HotPDF