مقاله فنی

استخراج متن از فایل‌های PDF با PDFium Component در دلفی

استخراج متن PDF ساده به نظر می‌رسد تا زمانی که با سندی مواجه شوید که لایه متنی آن وجود ندارد، خراب است، یا به ده‌ها بخش کاراکتری بسیار کوچک و بدون ترتیب مشخص تقسیم شده است. کامپوننت PDFium دو نقطه ورود به شما می‌دهد: آرایه Character[] برای دسترسی خام و مبتنی بر ایندکس به هر گلیف در یک صفحه، و ReadablePageContent برای یک نمای ساختاریافته که پاراگراف‌ها و عناوین را از درخت تگ PDF یا تحلیل اکتشافی بازسازی می‌کند. هیچ‌کدام همیشه انتخاب مناسبی نیستند، بنابراین درک اینکه هر کدام چه چیزی را در معرض دید قرار می‌دهند اهمیت دارد

باز کردن سند و تله شکست بی سر و صدا

کنترل TPdf یک فایل را با تنظیم FileName و تغییر حالت به Active := True باز می‌کند. جزئیات مهم: عبارت Active := True هرگز استثنایی ایجاد نمی‌کند. اگر فایل گم شده باشد، با رمز عبور محافظت شده باشد یا خراب باشد، PDFium خطا را به طور داخلی کنترل می‌کند و ویژگی Active به سادگی روی False باقی می‌ماند. این بدان معناست که هر حلقه استخراج باید در برابر این موضوع محافظت کند:

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('Could not open PDF (damaged or wrong password)');
    Exit;
  end;
  // extraction follows here
finally
  Pdf.Active := False;
  Pdf.Free;
end;

فایل‌های محافظت شده با رمز عبور نیاز دارند که Pdf.Password := '...' قبل از Active := True تنظیم شود. هیچ شانس دومی وجود ندارد: به محض اینکه Active با شکست مواجه شود، شما فایل را می‌بندید و با رمز عبور صحیح دوباره باز می‌کنید

استخراج صفحه به صفحه با استفاده از Character[]

پایین‌ترین سطح رویکرد، بررسی تک‌تک کاراکترها در هر صفحه است. ویژگی Pdf.PageNumber را برای بارگذاری لایه متنی آن صفحه تنظیم کنید، سپس ورودی‌های CharacterCount را با استفاده از ویژگی Character[] پیمایش نمایید. دو پرچم روی هر ورودی ارزش بررسی دارند: ویژگی CharacterGenerated[i] گلیف‌های مصنوعی درج شده توسط رندرکننده را علامت‌گذاری می‌کند (مثلاً خط‌های فاصله نرم در شکستگی‌های خط) که فاقد مقدار واقعی یونیکد هستند، و CharacterMapError[i] سیگنال می‌دهد که PDFium نتوانسته گلیف را به یک نقطه کد نگاشت کند، که این اتفاق در انکودینگ‌های فونتی که فاقد جدول ToUnicode هستند رخ می‌دهد

procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
  Page, I: Integer;
  Line: string;
  Ch: WideChar;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    Line := '';
    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
        Continue;
      Ch := Pdf.Character[I];
      if Ch = #13 then
        Ch := #10;   // normalize CR to LF
      Line := Line + Ch;
    end;
    Output.Add(Line);
  end;
end;

نتیجه یک رشته مسطح از نقاط کد یونیکد به ترتیبی است که PDFium آنها را شمارش می‌کند، که همان ترتیبی است که در استریم محتوا ظاهر می‌شوند و لزوماً ترتیب خواندن چپ به راست نیست. برای اکثر اسناد با خط لاتین تولید شده توسط ابزارهای اداری استاندارد، این روش خوبی است. برای فایل‌های PDF اسکن شده که با توالی‌های گلیف غیرمعمول OCR شده‌اند، یا برای متن‌های راست به چپ، این ترتیب می‌تواند اشتباه باشد. اینجاست که ReadablePageContent مفیدتر واقع می‌شود

استخراج ساختاریافته با ReadablePageContent

متد ReadablePageContent یک سطح بالاتر می‌رود: این متد یک رکورد TPdfReadableContent را برمی‌گرداند که آرایه Fragments آن قطعات محتوای تگ‌شده را حمل می‌کند، که هر کدام دارای یک Kind هستند که پاراگراف‌ها، عناوین، آیتم‌های لیست، سلول‌های جدول و غیره را شناسایی می‌کند. هنگامی که PDF دارای یک درخت ساختار باشد (ویژگی Pdf.IsTagged را بررسی کنید)، منبع برابر rosStructure است و ترتیب خواندن معتبر می‌باشد. برای فایل‌های بدون تگ، PDFium به rosHeuristic متوسل می‌شود که کاراکترها را بر اساس جعبه‌های احاطه‌کننده‌شان به واحدهای خواندن منطقی گروه‌بندی می‌کند اما نمی‌تواند دقت آن را تضمین نماید

procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
  Page: Integer;
  Content: TPdfReadableContent;
  Fragment: TPdfContentFragment;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Content := Pdf.ReadablePageContent(Page);
    for Fragment in Content.Fragments do
    begin
      case Fragment.Kind of
        cfHeading   : Output.Add('# ' + Fragment.Text);
        cfParagraph : Output.Add(Fragment.Text);
        cfListItem  : Output.Add('- ' + Fragment.Text);
      else
        Output.Add(Fragment.Text);
      end;
    end;
  end;
end;

اگر مقدار Content.Source = rosHeuristic باشد و خروجی شما به هم ریخته به نظر برسد، لایه متنی سند احتمالاً با در نظر گرفتن ترتیب خواندن نوشته نشده است. در آن مرحله تنها راه حل مطمئن، خروجی گرفتن مجدد از برنامه منبع با تگ‌گذاری مناسب، یا اجرای یک مرحله پردازش ثانویه است که ریشه‌های کاراکتر را بر اساس Y و سپس X مرتب می‌کند

ویژگی‌های CharacterOrigin و CharacterRectangle چه چیزی به شما می‌دهند

هر دو ویژگی موقعیت یک کاراکتر را در فضای صفحه برمی‌گردانند (به امتیاز، نقطه شروع در گوشه پایین سمت چپ، با افزایش Y به سمت بالا). ویژگی CharacterOrigin[i] نقطه لنگر خط مبنای گلیف است؛ ویژگی CharacterRectangle[i] کادر احاطه‌کننده کامل است. اینها بلوک‌های سازنده برای هر چیزی فراتر از متن ساده هستند: شناسایی مرزهای ستون، گروه‌بندی کاراکترها در خطوط با مقایسه مختصات Y در یک محدوده تلرانس، یا ساختن یک نقشه تست برخورد برای انتخاب متن در یک نمایشگر. اگر نیاز دارید پیدا کنید کدام کاراکتر زیر کلیک موس قرار دارد، متد CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) آن جستجو را مستقیماً انجام می‌دهد بدون اینکه شما مجبور به پیمایش مستطیل‌ها باشید

قرار دادن DLL در محل مناسب

کامپوننت PDFium تمام تجزیه PDF را به یک DLL بومی واگذار می‌کند، یا pdfium32.dll یا pdfium64.dll بسته به پلتفرم هدف شما. این کامپوننت یک اسکریپت CopyDlls.bat را ارائه می‌دهد که فایل صحیح را در دایرکتوری سیستم ویندوز کپی می‌کند. اجرای یک‌باره آن به عنوان Administrator روی سیستم توسعه کافی است؛ برای استقرار، به جای آن DLL را در کنار فایل اجرایی برنامه کپی می‌کنید. نسخه‌های فعال مجهز به V8 (شامل pdfium32v8.dll و pdfium64v8.dll) به طور قابل توجهی بزرگتر هستند و تنها زمانی لازم می‌باشند که فایل‌های PDF شما حاوی جاوااسکریپتی باشد که باید اجرا شود. برای استخراج متن خالص، نسخه استاندارد انتخاب مناسبی است

اگر DLL در زمان اجرا وجود نداشته باشد، عبارت Active := True درست مانند یک فایل گم شده بی سر و صدا شکست خواهد خورد، زیرا کامپوننت خطای بارگذاری را به طور داخلی دریافت می‌کند. همیشه قبل از ارسال، روی یک سیستم تمیز تست کنید

استفاده از FontSize[] در کنار Character[] برای تحلیل چیدمان

فرآتر از متن ساده، API سطح کاراکتر ویژگی FontSize[i] را در معرض دید قرار می‌دهد که اندازه نقطه رندر شده هر گلیف را برمی‌گرداند. در ترکیب با CharacterOrigin[i] و CharacterRectangle[i]، این ویژگی به شما امکان می‌دهد متن بدنه را از عناوین بدون تکیه بر درخت ساختار تشخیص دهید. یک بخش کاراکتر که در آن اندازه فونت بالاتر از یک آستانه می‌پرد، تقریباً به طور قطع یک عنوان در یک سند بدون تگ است. همین روش برای شناسایی زیرنویس‌ها (متن کوچک زیر کادر احاطه‌کننده تصویر) یا پاورقی‌ها (متن کوچک در نزدیکی پایین صفحه) اعمال می‌شود. هیچ‌کدام از اینها نیازی به رندر کردن ندارند; هر سه ویژگی مستقیماً از لایه متنی که PDFium در طول Active := True می‌سازد خوانده می‌شوند

یک نکته ظریف: ویژگی FontSize[i] اندازه را پس از اعمال CTM (ماتریس تبدیل فعلی) صفحه منعکس می‌کند، بنابراین سندی که نویسنده کل صفحه را در آن مقیاس‌گذاری کرده است، اندازه‌ها را با تعدیل متناسب گزارش می‌دهد. اگر در حال مقایسه اندازه‌ها در صفحات با ابعاد متفاوت هستید، قبل از تصمیم‌گیری درباره مقادیر آستانه، مقادیر را با ارتفاع MediaBox هر صفحه نرمال‌سازی کنید

نوشتن خروجی در یک فایل

شیء TStringList دلفی از نسخه XE خروجی UTF-8 را به طور تمیز مدیریت می‌کند. اگر به یک فایل بدون BOM نیاز دارید (بسیاری از سیستم‌های پایین‌دستی با BOM پیشرو دچار مشکل می‌شوند)، مقدار WriteBOM := False را تنظیم کنید:

var
  Lines: TStringList;
begin
  Lines := TStringList.Create;
  try
    ExtractAllText(Pdf, Lines);
    Lines.WriteBOM := False;
    Lines.SaveToFile('output.txt', TEncoding.UTF8);
  finally
    Lines.Free;
  end;
end;

برای اسناد بسیار بزرگ که حافظه در آنها اهمیت دارد، به جای جمع‌آوری همه چیز در یک لیست، مستقیماً در یک TStreamWriter با انکودینگ TEncoding.UTF8 در داخل حلقه صفحه بنویسید

متدها و خصوصیات Character[]، CharacterCount، CharacterOrigin[]، CharacterRectangle[]، ReadablePageContent و CharacterIndexAtPos نشان داده شده در اینجا بخشی از PDFium Component برای دلفی و C++Builder هستند