مقاله فنی

جستجو و جایگزینی متن در یک PDF موجود با دلفی

کامپوننت HotPDF می‌تواند متن داخل یک PDF موجود را از دلفی و C++Builder جستجو و جایگزین کند. متدهای SearchLoadedPageText و SearchLoadedDocumentText هر بار تکرار یک رشته را با دقت در سطح گلیف پیدا می‌کنند، و متدهای ReplaceLoadedPageText و ReplaceLoadedDocumentText بایت‌های مطابقت‌یافته را در همان محل بازنویسی می‌کنند — مشروط بر اینکه هر کاراکتر جایگزین بتواند از طریق فونت اصلی دوباره رمزگذاری شود، یک محدودیت فیزیکی که این مقاله به جای پنهان کردن در یک پاورقی، به طور صادقانه با آن برخورد می‌کند

درخواست پشت این ویژگی همیشه معمولی است. شرکتی نام خود را تغییر می‌دهد و سه هزار فاکتور آرشیو شده همچنان نام قدیمی را حمل می‌کنند. یک قالب قرارداد با تاریخ انقضای سال گذشته ارسال شده است. یک کد محصول بازنشسته شده و هر برگه داده‌ای که به آن اشاره می‌کند به کد جایگزین نیاز دارد. در یک واژه‌پرداز، هر یک از این‌ها کاری سی ثانیه‌ای است. اما در یک PDF این یک مسئله واقعاً سخت است، و درک دلیل آن، تفاوت بین استفاده درست از API و ثبت گزارش باگی است که در واقع ارجاع به مستندات مشخصات فایل است

چرا جایگزینی متن در یک PDF بسیار سخت است؟

جایگزینی متن در یک PDF سخت است زیرا یک صفحه PDF شامل متن قابل ویرایش نیست — بلکه شامل گلیف‌های موقعیت‌دهی شده است. تحت مدل نمایش متن استاندارد ISO 32000-1 §9.4، یک جریان محتوا عملگرهایی مانند Tj و TJ را هدایت می‌کند که توالی کدهای کاراکتر را در مختصات تعیین‌شده توسط ماتریس متن ترسیم می‌نمایند. آن کدها یونیکد نیستند؛ بلکه اندیس‌هایی در رمزگذاری اعلام‌شده توسط فونت صفحه هستند و نگاشت معکوس به کاراکترهای خوانا ممکن است در یک /ToUnicode CMap، یک آرایه تفاوت رمزگذاری یا یک زنجیره نگاشت CID قرار داشته باشد. هیچ شیء پاراگرافی، جریان متنی یا تضمینی وجود ندارد که یک کلمه بصری حتی به صورت یک رشته واحد ذخیره شده باشد

جایگزینی، لایه دومی از دشواری را به بالای فرآیند رمزگشایی اضافه می‌کند: شما باید دقیقاً بدانید کدام بایت‌های جریان اصلی هر گلیف را تولید کرده‌اند، به طوری که بتوانید بایت‌های جدید را دقیقاً در همان محدوده و نه جای دیگر پیوند بزنید. یک استخراج‌کننده متن می‌تواند پس از خروج یونیکد، موقعیت‌های بایت را دور بریزد. اما یک جایگزین‌کننده نمی‌تواند. به همین دلیل است که HotPDF کار را در دو انتشار تقسیم کرد — نسخه v2.251.0 لایه ردیابی آفست و جستجو را ساخت، و نسخه v2.252.0 لایه بازنویسی را بر روی آن بنا نهاد

یافتن متن: جستجو در سطح گلیف با ردیابی آفست بایت

متد SearchLoadedDocumentText در HotPDF هر تکرار از یک عبارت را با تطبیق در برابر توالی گلیف یونیکد رمزگشایی شده هر صفحه، و نه در برابر بایت‌های خام جریان، پیدا می‌کند، بنابراین بدون توجه به نحوه رمزگذاری فونت، یک تطابق واقعی حاصل می‌شود. زیرساخت زیرین در نسخه v2.251.0 معرفی شد: نشانه‌گذار (tokenizer) جریان محتوا یک محدوده بایت StartOfs/EndOfs را برای هر عملوند رشته‌ای — از جمله جداکننده‌های ( ) یا < > آن — ثبت می‌کند و هر گلیف رمزگشایی شده حاوی یک سه‌گانه TokenIndex/ItemIndex/ByteOffset است که به عملوند دقیق، آیتم آرایه TJ و واحد کدی که آن را تولید کرده اشاره می‌کند. همین مفسر گلیف، API استخراج شرح داده شده در استخراج متن از یک PDF بارگذاری‌شده در دلفی را تامین می‌کند؛ جستجو صرفاً منشا اطلاعاتی را که استخراج دور می‌اندازد، حفظ می‌نماید

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('صفحه %d در (%.1f, %.1f): "%s"',
            [Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
             Matches[I].Text]));
    end;
  finally
    Pdf.Free;
  end;
end;

یک تصمیم طراحی عمدی شایسته ذکر است. زمانی که CaseSensitive برابر False باشد، مقایسه بر اساس طراحی فقط برای کاراکترهای ASCII انجام می‌شود: تا کردن حروف (case folding) کامل یونیکد در ابزارهای دلفی 5 تا XE که HotPDF پشتیبانی می‌کند متفاوت رفتار می‌کند، و یک API جستجو که بسته به اینکه کدام کامپایلر برنامه شما را ساخته تطابق‌های متفاوتی پیدا کند، بدتر از یک API با محدودیت مستند و قابل پیش‌بینی است. برای متون تجاری لاتین — نام‌ها، کدها、تاریخ‌ها — تا کردن ASCII موارد عملی را پوشش می‌دهد

جایگزینی متن: رمزگذاری معکوس و پیوند جراحی

متد ReplaceLoadedDocumentText که در نسخه v2.252.0 اضافه شد، هر بار تکرار یک عبارت را با اجرای معکوس سازوکار رمزگشایی بازنویسی می‌کند. تابع HPDFEncodeUnicode معکوس رمزگشای کد کاراکتر است: همان زنجیره استراتژی را به صورت معکوس طی می‌کند — جستجوی bfchar و bfrange در /ToUnicode، نگاشت CID جریان رمزگذاری، نگاشت‌های هویت Type0 و جداول از پیش تعریف‌شده WinAnsi و MacRoman — تا هر کاراکتر جایگزین را به بایت‌های کد کاراکتر مورد انتظار فونت اصلی تبدیل کند. بایت‌های دوباره رمزگذاری‌شده سپس به یک رشته متنی قالب‌بندی‌شده یا رشته هگز سرال‌سازی می‌شوند که قوانین گریز (escaping) خود نشانه‌گذار را منعکس می‌کند تا رفت و برگشت تجزیه ← سریال‌سازی مجدد پایدار باشد

خود پیوند به جای کلی، جراحی است. فقط محدوده بایت کدی که توسط تطابق پوشش داده شده در داخل عملوند رشته جایگزین می‌شود؛ بایت‌های مطابقت‌نیافته در همان عملوند، فضای خالی بین نشانه‌ها و هر عملگر اطراف دقیقاً بایت به بایت حفظ می‌شوند. جایگزینی 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('انجام بازنویسی عملوندها صورت گرفت', [ReplaceCount]));
      Pdf.SaveLoadedDocument('contract-final.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

توجه داشته باشید که این API چه کاری انجام نمی‌دهد: صفحه را دوباره حروف‌چینی نمی‌کند. PDF فاقد جریان مجدد (reflow) متن است، بنابراین جایگزینی که از نظر بصری پهن‌تر از نسخه اصلی باشد، به سادگی فضای افقی بیشتری را اشغال خواهد کرد و ممکن است هر چیزی را که در سمت راست آن ترسیم شده شلوغ کند. جایگزینی‌های هم‌طول یا با طول نزدیک — تاریخ‌ها، رشته‌های نسخه، شماره قطعات، اصلاحات نام — نقطه قوت این متد هستند. بازنویسی‌های کلی متعلق به سند منبع است، نه فایل 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('مورد نادیده گرفته شد: کاراکترها در زیرمجموعه فونت وجود ندارند، یا تطابق شامل چند عملوند است',
      [Expected - Replaced]));
end;

شرط دوم نادیده‌انگاری در آن پیام، مرز مستند دیگری است: عبارتی که در چندین عملوند رشته پخش شده است — برای مثال Hello که در میان آیتم‌های [(He)(llo)] TJ تقسیم شده — توسط جستجو پیدا می‌شود، زیرا جستجو توالی گلیف رمزگشایی شده را تطبیق می‌دهد، اما در جایگزینی نادیده گرفته می‌شود، زیرا بازنویسی در مرزهای عملوند مستلزم ادغام محدوده‌های بایت مجاور است. جستجو و سپس تایید، هر دو محدودیت را به جای پنهان ماندن، آشکار می‌کند

چه تغییراتی در فایل در زمان ذخیره‌سازی رخ می‌دهد؟

یک جریان /Contents جایگزین شده به صورت غیرفشرده ذخیره می‌شود. جریان‌های فشرده‌شده با FlateDecode برای ویرایش از حالت فشرده خارج می‌شوند و وقتی HotPDF بایت‌های بازسازی‌شده را می‌نویسد، ورودی /Filter جریان را حذف کرده و /Length را به جای فشرده‌سازی مجدد، تازه‌سازی می‌کند. PDF حاصل کاملاً معتبر است و به طور عادی در نمایشگرهای رایج رندر می‌شود؛ هزینه آن، بزرگتر شدن حجم فایل برای هر جریان ویرایش‌شده است. برای یک خط لوله دسته‌ای که هزاران سند را پردازش می‌کند، باید این افزایش حجم را در نظر بگیرید یا یک مرحله فشرده‌سازی مجزا را در بخش‌های پایین‌دستی اجرا کنید. نحوه تعامل اشیاء بازنویسی‌شده با ساختار ارجاع متقابل سند در زمان ذخیره، موضوع جداگانه‌ای است که در مقاله جریان‌های شیء و به‌روزرسانی‌های افزایشی در HotPDF پوشش داده شده است

هر چیز دیگری در فایل دست‌نخورده باقی می‌ماند. جریان‌های دست‌نخورده فشرده‌سازی خود را حفظ می‌کنند، فونت‌ها و تصاویر بازنویسی نمی‌شوند، و پیوند در سطح عملوند به این معنی است که حتی جریان‌های ویرایش‌شده فقط در جایی که تطابق انجام شده با نسخه اصلی تفاوت دارند. این محافظه‌کاری عمدی است: هرچه یک کتابخانه بخش‌های بیشتری از سند بارگذاری‌شده را بازنویسی کند,فرصت‌های بیشتری برای خراب کردن ویژگی‌های خاص و پیش‌بینی‌نشده فایل ایجاد می‌شود

جستجو و جایگزینی متن به استخراج، ویرایش و رندر صفحات در مجموعه ابزارهای سند بارگذاری‌شده HotPDF می‌پیوندد که همگی توسط یک مفسر جریان محتوای یکسان هدایت می‌شوند و از دلفی 5 تا نسخه‌های فعلی RAD Studio بدون وابستگی‌های خارجی در دسترس هستند. مرجع کامل API و دانلود نسخه آزمایشی در صفحه محصول کامپوننت HotPDF قرار دارد