مقاله فنی

افزودن لایه متن قابل‌جست‌وجو به PDFهای اسکن‌شده در دلفی

PDFium Component یک لایه متن قابل‌جست‌وجو را به صفحات PDF اسکن‌شده از دلفی از طریق ApplyOcrSearchLayer می‌افزاید. هر صفحه انتخاب‌شده را رندر می‌کند، پیکسل‌ها را به یک ارائه‌دهنده OCR که شما تأمین می‌کنید می‌سپارد، و واژه‌های تشخیص‌داده‌شده را به‌صورت اشیای متنی نامرئی که روی واژه‌های درون اسکن جای‌گذاری شده‌اند، دوباره می‌نویسد. تصویر اصلی صفحه هرگز رمزگشایی، دوباره‌رمزگذاری یا جایگزین نمی‌شود، بنابراین نتیجه بصری بیت به بیت همان صفحه‌ای است که با آن شروع کردید

موتور تشخیص به‌عمد بخشی از کتابخانه نیست. PDFium رندر صفحه، نگاشت مختصات، بارگذاری فونت، ساخت شیء متنی و حالت‌های رندر نامرئی را افشا می‌کند، اما هیچ موتور OCR‌ای ندارد، و وانمودکردن غیر از این به‌معنای بسته‌بندی محصول تشخیص شخص دیگری درون یک مؤلفه PDF بود. در عوض، تشخیص پشت رابط IPdfOcrProvider زندگی می‌کند: کتابخانه پیکسل‌های BGRA با چیدمان ثابت و مبدأ بالا را عبور می‌دهد، و ارائه‌دهنده متن یونیکد، مقادیر اطمینان و چهارضلعی‌های واژه را برمی‌گرداند

یک لایه متن قابل‌جست‌وجو دقیقاً چیست؟

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

جای‌گذاری همه‌چیز ماجراست. اگر متن نامرئی چند پوینت جابه‌جا بنشیند، هایلایت‌های انتخاب کنار واژه‌ها می‌نشینند نه روی آن‌ها، و کپی‌کردن یک پاراگراف متنی را با ترتیب نادرست تولید می‌کند. به همین دلیل هندسه باید از همان تبدیل‌هایی بیاید که PDFium برای رندر صفحه استفاده می‌کند، نه از یک حدس متناسب

پیاده‌سازی ارائه‌دهنده

قرارداد ارائه‌دهنده یک متد است. یک رکورد تصویر صفحه حامل ابعاد، گام (stride)، DPI، قالب پیکسل و خود بایت‌های پیکسلی، به‌علاوه یک نشانه لغو، دریافت می‌کند، و واژه‌ها یا یک پیام خطا برمی‌گرداند:

uses
  PDFium;

type
  TMyOcrProvider = class(TInterfacedObject, IPdfOcrProvider)
  public
    function RecognizePage(const Image: TPdfOcrImage;
      const CancellationToken: IPdfCancellationToken;
      out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
  end;

function TMyOcrProvider.RecognizePage(const Image: TPdfOcrImage;
  const CancellationToken: IPdfCancellationToken;
  out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
var
  I: Integer;
begin
  // Image.Pixels ردیف‌های BGRA با مبدأ بالا از Image.Stride بایت را نگه می‌دارد
  // آن‌ها را به موتور خود بسپارید، سپس یک ورودی به ازای هر واژه تشخیص‌داده‌شده پر کنید
  SetLength(Words, RecognisedCount);
  for I := 0 to RecognisedCount - 1 do
  begin
    Words[I].Text := EngineWordText(I);
    Words[I].Confidence := EngineWordConfidence(I);   // 0..1
    Words[I].Quad := TPdfOcrQuad.FromRectangle(
      EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
  end;
  ErrorMessage := '';
  Result := True;
end;

چهارضلعی به‌جای مستطیل، زیرا یک اسکن به‌ندرت نسبت به صفحه مربعی است. یک واژه روی صفحه‌ای که اندکی چرخیده، یک متوازی‌الاضلاع را اشغال می‌کند، و TPdfOcrQuad چهار نقطه گوشه را حمل می‌کند تا واژه‌های کج‌شده و چرخانده‌شده یک منطقه انتخاب دقیق را حفظ کنند. موتورهایی که فقط جعبه‌های هم‌راستا با محور را گزارش می‌دهند می‌توانند از FromRectangle استفاده کنند، که چهارضلعی منحط را می‌سازد

چرا موقعیت‌های واژه نمی‌توانند به‌طور متناسب مقیاس‌گذاری شوند؟

وسوسه‌انگیز است که یک مختصات پیکسلی را با تقسیم بر عرض رندر و ضرب در عرض صفحه به یک مختصات صفحه تبدیل کنید. این فقط برای صفحاتی بدون چرخش، یک CropBox یکسان با MediaBox، و یک مبدأ در صفر کار می‌کند، و بسیاری از اسناد اسکن‌شده دست‌کم یکی از آن شرایط را نقض می‌کنند

PDFium Component هر یک از چهار گوشه چهارضلعی را جداگانه از طریق FPDF_DeviceToPage نگاشت می‌کند، همان نگاشتی که رندرکننده برای تولید پیکسل‌ها استفاده کرده، بنابراین ورودی‌های /Rotate و کادرهای برش افست‌دار به‌طور طبیعی مدیریت می‌شوند. سپس ماتریس آفین برای شیء متنی از سه نقطه از نقاط نگاشت‌شده، گوشه‌های پایین‌چپ، پایین‌راست و بالا-چپ ساخته می‌شود، که دقیقاً برای بیان موقعیت، مقیاس، چرخش و برش کافی است

خود شیء متنی با اندازه فونت واحد ساخته می‌شود تا کران‌های فونت واقعی آن قابل‌اندازه‌گیری باشد، و سپس کران‌های شیء اندازه‌گیری‌شده روی چهارضلعی هدف نگاشت می‌شوند. اندازه‌دادن با یک اندازه پوینت حدسی و امیدواربودن به تطبیق آن با واژه اسکن‌شده، با هر جایگزینی فونت منحرف می‌شد؛ اندازه‌گیری اول، برازش را مستقل از اینکه لایه از کدام فونت استفاده می‌کند، می‌سازد

اجرای آن روی یک سند

رکورد گزینه‌ها وضوح، فیلترینگ و هر بودجه‌ای را کنترل می‌کند. فیلترینگ اطمینان بیش از آنچه به نظر می‌رسد اهمیت دارد: واژه‌های آشغال با اطمینان پایین، نتایج جست‌وجو را برای همیشه آلوده می‌کنند، و برخلاف یک رندر نادرست، هیچ‌کس متوجه نمی‌شود تا زمانی که یک جست‌وجو مزخرف برمی‌گرداند:

var
  Pdf: TPdf;
  Options: TPdfOcrOptions;
  Report: TPdfOcrReport;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'scanned-contract.pdf';
    Pdf.LoadDocument;

    Options := TPdfOcrOptions.Default;
    Options.Dpi := 300;                  // وضوح تشخیص
    Options.MinConfidence := 0.60;       // حذف واژه‌های نامطمئن
    Options.SkipPagesWithText := True;   // صفحات اصالتاً دیجیتال را دست‌نخورده رها کن
    Options.ContinueOnError := True;     // یک صفحه خراب نباید کار را متوقف کند
    Options.MaxPixelsPerPage := 40 * 1000 * 1000;

    if Pdf.ApplyOcrSearchLayer(TMyOcrProvider.Create, Options, Report) then
      Pdf.SaveAs('scanned-contract-searchable.pdf');

    for I := 0 to High(Report.Pages) do
      if Report.Pages[I].Status = popsFailed then
        Writeln(Format('page %d failed: %s',
          [Report.Pages[I].PageNumber, Report.Pages[I].ErrorMessage]));
    Writeln(Format('%d word(s) inserted, %d rejected, %d page(s) skipped',
      [Report.InsertedWordCount, Report.RejectedWordCount,
       Report.SkippedPageCount]));
  finally
    Pdf.Free;
  end;
end;

SkipPagesWithText در بایگانی‌های مختلط سزاوار تأکید است. یک PDF که از قبل متن واقعی حمل می‌کند، چه اصالتاً دیجیتال باشد چه پیش‌تر پردازش‌شده، اگر OCR را کورکورانه روی آن اجرا کنید یک لایه متن دوم می‌گیرد، و این تکرار باعث می‌شود استخراج هر واژه را دو بار برگرداند. وضعیت به‌ازای-هر-صفحه popsSkippedExistingText دقیقاً می‌گوید کدام صفحات دست‌نخورده رها شده‌اند

بودجه‌ها، لغو و مهار شکست

هر کمیتی که یک سند خصمانه یا صرفاً بسیار بزرگ می‌تواند متورم کند، یک سقف دارد: پیکسل به ازای هر صفحه و در مجموع، واژه به ازای هر صفحه و در مجموع، و نویسه به ازای هر واژه. همه آن‌ها پیش از نوشته‌شدن صفحه بررسی می‌شوند، نه پس از آن، و تخمین پیکسل پیش از تخصیص هر بیت‌مپی از ابعاد صفحه و DPI محاسبه می‌شود. افزایش DPI از ۱۵۰ به ۳۰۰ حافظه به ازای هر صفحه را چهار برابر می‌کند، بنابراین سقف به‌ازای-هر-صفحه اولین پارامتری است که باید تنظیم کرد وقتی یک کار دسته‌ای روی فرمت‌های بزرگ شروع به شکست‌خوردن می‌کند

نشانه لغو در سراسر کل مسیر رشته می‌شود: رندر تدریجی، فراخوانی ارائه‌دهنده و حلقه درج به‌ازای-هر-واژه. این یعنی کاربری که در حین تشخیص یک فایل ۴۰۰صفحه‌ای لغو می‌کند، درون یک صفحه متوقف می‌شود نه در انتهای سند، و همان الگوی نشانه‌ای که در جای دیگری از مؤلفه استفاده شده، که در رندر تدریجی قابل‌لغو شرح داده شده، اینجا بدون تغییر اعمال می‌شود

مهار شکست به‌ازای-هر-صفحه است. کتابخانه دسته‌های شیء‌ای را که روی یک صفحه درج کرده جمع می‌کند و FPDFPage_GenerateContent را یک‌بار، پس از جای‌گذاری همه واژه‌ها، فراخوانی می‌کند. اگر چیزی در میانه راه شکست بخورد، چه خطای ارائه‌دهنده باشد چه مشکل فونت، اشیای درج‌شده روی آن صفحه به‌ترتیب معکوس حذف می‌شوند و محتوای صفحه دوباره تولید می‌شود، بنابراین یک صفحه شکست‌خورده به حالت اصلی خود بازمی‌گردد، به‌جای نگه‌داشتن نیمی از یک لایه متن. سپس حلقه سند بر اساس ContinueOnError ادامه می‌یابد یا متوقف می‌شود، و صفحه فعال همیشه بازگردانده می‌شود

تأیید اینکه تصویر واقعاً دست‌نخورده باقی مانده

قوی‌ترین بررسی موجود، ساده‌ترین بررسی نیز هست: صفحه را پیش و پس از اعمال لایه با همان اندازه رندر کنید و بیت‌مپ‌ها را مقایسه کنید. آن‌ها باید بیت به بیت یکسان باشند، زیرا متن نامرئی چیزی ترسیم نمی‌کند و جریان تصویر هرگز رمزگشایی نشده. هر تفاوتی به این معناست که چیزی غیر از لایه متن، صفحه را تغییر داده

پس از آن، سمت متن را با استخراج از فایل پردازش‌شده و تأیید اینکه موقعیت‌های واژه روی اسکن می‌نشینند، تأیید کنید. مسیر استخراج همان مسیری است که در استخراج متن از اسناد PDF شرح داده شده، و برای یک بررسی بصری سریع از تراز، رندرکردن صفحات به تصاویر همان‌طور که در تبدیل صفحات PDF به JPEG شرح داده شده به شما اجازه می‌دهد جعبه‌های واژه را روی اسکن همپوشان کنید

لایه‌گذاری OCR، رندر، استخراج و ویرایش همگی روی همان شیء سند در دلفی، C++Builder و Lazarus اجرا می‌شوند؛ سطح کامل API در صفحه PDFium Component برای دلفی شرح داده شده