مقاله فنی

استخراج متن PDF در Delphi: فاصله‌های کلمه و شکست خط

HotPDF Delphi Component فاصله‌های کلمه و شکست خط را در THotPDF.ExtractLoadedPageText از هندسهٔ گلایف بازسازی می‌کند، نه از نویسه‌های فاصله. یک فاصله وقتی می‌آید که فاصلهٔ بعد از عرض خود گلایف از 0.15 ارتفاع متن بیشتر باشد، و خط جدید فقط وقتی شروع می‌شود که مبدأ متن در جهت نوشتاری بیش از نصف ارتفاع متن جابه‌جا شود. از v2.768.3 متن صفحه همچنین متنی را که از طریق Form XObjectها رسم شده شامل می‌شود و گلایف‌های بیرون از ناحیهٔ برش قابل‌مشاهده را کنار می‌گذارد. بقیهٔ این نوشته توضیح می‌دهد چرا هر قاعده شکلی که دارد را دارد، چون هر کدام یک قاعدهٔ ساده‌تر را جایگزین کرده که روی اسناد واقعی خروجی محتمل اما غلط تولید می‌کرد

علائم برای هر کسی که متن PDF به یک ایندکس جست‌وجو داده آشناست. یک صفحهٔ جلد به‌شکل PDFReferenceManualNovember4,1998 استخراج می‌شود، یک فرم مالیاتی به 156 خط می‌شکند، یک واترمارک مورب به‌شکل یک نویسه در هر خط می‌رسد، و یک پیش‌نویس برش‌خورده با خط slug پرینتر شروع می‌شود که هیچ viewer ای نشانش نمی‌دهد. هیچ‌کدام از این فایل‌ها خراب نیستند. هر کدام یک راه کاملاً قانونی برای جاگذاری متن استفاده می‌کنند که یک استخراج‌کنندهٔ ساده‌لوح اشتباه می‌خواند

چرا متن استخراج‌شده از PDF فاصله‌های کلمه‌اش را از دست می‌دهد؟

متن استخراج‌شده فاصله‌های کلمه را از دست می‌دهد چون هیچ‌وقت PDF ملزم به داشتنشان نیست. یک تولیدکننده می‌تواند کلمه‌ها را با نمایش یک نویسهٔ فاصله جدا کند، اما به همان اندازه می‌تواند قلم را با یک عدد داخل آرایهٔ TJ (ISO 32000-1 §9.4.3) یا با یک Td تازه (§9.4.2) جابه‌جا کند، و خروجی TeX و خیلی از فایل‌های Distiller و بیشتر چیدمان‌های justify شده دقیقاً همین را می‌کنند. قبل از v2.766.76، HPDFAssemblePageText فقط به حرکت عمودی نگاه می‌کرد، پس یک شکست کلمه‌ای که با موقعیت‌دهی ساخته شده بود به‌سادگی ناپدید می‌شد. اسمبلر حالا در جهت نوشتاری گلایف قبلی، فاصلهٔ از انتهای عرض خود آن گلایف تا مبدأ گلایف جاری را اندازه می‌گیرد و وقتی فاصله از 0.15 ارتفاع جعبهٔ گلایف جاری، از صعود تا نزول در user space، بیشتر باشد یک فاصله می‌گذارد. وقتی هر طرف از قبل خالی است فاصله‌ای اضافه نمی‌شود، و بین دو نویسهٔ CJK هم نه، چون justify کردن ایدئوگراف‌ها را از هم باز می‌کند بدون اینکه آن باز شدن مرز کلمه باشد. رکوردهای گلایف همان هندسه را در دسترس می‌گذارند، پس می‌توانی تصمیم را وقتی یک فایل خاص سرت را درد می‌گیرد دوباره تولید کنی

uses
  SysUtils, HPDFDoc, HPDFContentStream;

procedure DumpWordGaps(Pdf: THotPDF; PageIndex: Integer);
var
  Glyphs: THPDFGlyphArray;
  I: Integer;
  Height, Gap: Double;
begin
  if not Pdf.ExtractLoadedPageGlyphs(PageIndex, Glyphs) then
    Exit;
  for I := 1 to High(Glyphs) do
  begin
    // ارتفاع صعود-تا-نزول جعبهٔ گلایف، در user space
    Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
      Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
    // متن افقی: فاصله از انتهای عرض خود گلایف قبلی
    Gap := Glyphs[I].BaselineStartX - Glyphs[I - 1].GlyphEndX;
    if (Height > 0) and (Gap > 0.15 * Height) then
      Writeln(Format('U+%.4x gap %.2f height %.2f: space',
        [Glyphs[I].Unicode, Gap, Height]));
  end;
end;

چرا از عرض خود گلایف اندازه می‌گیریم نه از موقعیت قلم؟

HotPDF فاصله‌های کلمه را از GlyphEndX / GlyphEndY اندازه می‌گیرد چون موقعیت قلم بعد از یک گلایف از قبل فاصله‌گذاری‌ای را حمل می‌کند که فاصله نیست. ISO 32000-1 §9.4.4 جابه‌جایی افقی را عرض گلایف ضربدر اندازهٔ فونت به‌علاوهٔ فاصلهٔ نویسه یعنی Tc به‌علاوهٔ فاصلهٔ کلمه یعنی Tw تعریف می‌کند، همه مقیاس‌شده با Tz. BaselineEndX / BaselineEndY همان جابه‌جایی کامل را نگه می‌دارند، در حالی که GlyphEndX / GlyphEndY فقط پیشروی فونت و Tz را. تفاوت برای تولیدکننده‌هایی مهم است که با یک Tc منفی tracking را تنگ می‌کنند و بعد فاصله را با یک تنظیم TJ بعد از هر گلایف پس می‌دهند: اندازه‌گیری از موقعیت قلم، آن پس دادن شبیه یک فاصله به نظر می‌رسد و اصطلاح چینی «95后» به‌شکل «9 5 后» استخراج می‌شد. آستانه به ارتفاع جعبهٔ گلایف بسته شده نه به اندازهٔ Tf به دلیل مشابهی. خروجی‌های Word اغلب می‌نویسند 1 Tf و اندازهٔ واقعی را در یک Tm مقیاس‌شده حمل می‌کنند، پس Tfs می‌گوید 1 در حالی که متن 10 نقطه بلند است، و قاعده‌ای که به Tfs کلید خورده باشد دو املا از یک صفحهٔ واحد را متفاوت قضاوت می‌کرد

قاعدهٔ فاصلهٔ کلمه در HotPDF برای ExtractLoadedPageText در Delphi: فاصله فقط وقتی درج می‌شود که فاصلهٔ از GlyphEndX گلایف قبلی تا BaselineStartX گلایف بعدی از 0.15 ارتفاع جعبهٔ صعود-تا-نزول بیشتر باشد، چون موقعیت قلم در BaselineEndX از قبل Tc و Tw و Tz را دارد و پس دادن‌های tracking جاستیفای‌شده را به فاصله‌های کاذب مثل 9 5 后 تبدیل می‌کند
هندسه تصمیم می‌گیرد کلمه‌ها کجا می‌شکنند، نه نویسه‌های فاصله؛ رکوردهای گلایف همان اندازه‌گیری‌ها را در دسترس می‌گذارند، پس می‌توانی تصمیم را برای هر فایل گیج‌کننده‌ای دوباره اجرا کنی

قاعده لبه‌های صادقانه‌ای دارد. عنوانی با tracking خیلی باز، جایی که Tc به‌تنهایی بیش از 0.15 ارتفاع متن بین حروف باز می‌کند، با یک فاصله بین تک‌تک حروفش استخراج می‌شود، که همان چیزی است که صفحه نشان می‌دهد ولی احتمالاً آن چیزی نیست که می‌خواستی ایندکس کنی. تکه‌هایی که روی یک خط پایه از ترتیب خارج رسم شده‌اند یک فاصلهٔ منفی تولید می‌کنند و بدون فاصله به هم می‌چسبند. هیچ‌کدام در متن بدنه رایج نیست، و روی یک سویت تست این تغییر تطبیق‌های کلمه را در برابر یک استخراج‌کنندهٔ مرجع روی 28 صفحه بالا برد بدون اینکه هیچ‌کدام پایین بیاید

HotPDF چه موقع در متن استخراج‌شده خط جدید شروع می‌کند؟

از v2.766.79، خط جدید وقتی شروع می‌شود که حرکت از مبدأ گلایف قبلی به مبدأ فعلی، تصویرشده روی نرمالِ جهت نوشتاری قبلی، از نصف ارتفاع جعبهٔ بزرگ‌ترِ آن دو گلایف بیشتر باشد. قاعدهٔ قبلی حرکت خام Y را با نصف Tfs مقایسه می‌کرد که در دو جهت شکست می‌خورد. با 1 Tf و یک Tm مقیاس‌شده آستانه به نصف واحد می‌چسبید، پس یک بالانویس که با text rise برابر 0.4 بالا رفته بود یا لرزش عادی خط پایه خط را می‌شکست. قاعده همچنین X را کلاً نادیده می‌گرفت، پس متن زیر یک Tm چرخیده با هر گلایف روی صفحه پایین می‌رفت و یک نویسه در هر خط بیرون می‌آمد. تصویر کردن روی نرمال جهت باعث می‌شود ران‌های چرخیده مثل ران‌های افقی رفتار کنند، و گرفتن بزرگ‌ترِ دو ارتفاع باعث می‌شود یک کلمهٔ نمونهٔ بزرگ و کپشن کوچکش وقتی خط پایه مشترک دارند در یک خط بمانند. روی فرم مالیاتی که بالا گفته شد شمار خط از 156 به 97 افت کرد. متن عمودی در writing mode 1 (§9.7.4.3) مسیر جداگانه‌ای می‌رود: آن گلایف‌ها در ستون‌ها گروه می‌شوند، از راست به چپ و از بالا به پایین خوانده می‌شوند، با یک شکست خط در هر تغییر ستون

چگونه ExtractLoadedPageText در HotPDF در Delphi شکست خط‌ها را تصمیم می‌گیرد: حرکت بین مبدأهای گلایف روی نرمالِ جهت نوشتاری تصویر می‌شود و با نصف ارتفاع جعبهٔ بزرگ‌تر مقایسه می‌شود، پس بالانویسی که با یک text rise کوچک زیر فونت 1 Tf بالا رفته و متنی که زیر یک Tm چرخیده روی صفحه پایین می‌رود دیگر به یک نویسه در هر خط نمی‌شکنند
تصویر کردن باعث می‌شود ران‌های چرخیده مثل ران‌های افقی رفتار کنند و گرفتن بزرگ‌ترِ دو ارتفاع جعبه باعث می‌شود یک کلمهٔ نمونهٔ بزرگ و کپشن کوچکش در یک خط بمانند

ExtractLoadedPageText چه متنی را شامل می‌شود و چه متنی را کنار می‌گذارد؟

ExtractLoadedPageText متنی را برمی‌گرداند که viewer نشان می‌دهد. از v2.766.80 فقط از گلایف‌های قابل‌مشاهده کار می‌کند و هر گلایفی را که مرکز جعبه‌اش بیرون از GetLoadedPageVisibleBox بیفتد کنار می‌گذارد، یعنی CropBox بریده‌شده به MediaBox (§14.11.2). این خطوط slug و دیگر نشان‌های پرینتر را که به‌شکل متن بیرون از ناحیهٔ برش ست شده‌اند حذف می‌کند. ExtractLoadedPageGlyphs عمداً همچنان هر گلایف content stream صفحه را برمی‌گرداند، پس هنوز هم می‌توانی وقتی لازمش داری آن مطالب را پیدا کنی. فیلتر یک تست جعبه است نه تست دید: متن پنهان‌شده با یک مسیر برش، رسم‌شده به رنگ سفید یا پوشیده‌شده زیر یک تصویر همچنان استخراج می‌شود

var
  Pdf: THotPDF;
  Glyphs: THPDFGlyphArray;
  PageText: UnicodeString;
  L, B, R, T: Single;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('trimmed-proof.pdf');
    if Pdf.GetLoadedPageVisibleBox(0, L, B, R, T) then
      Writeln(Format('Visible box: %.1f %.1f %.1f %.1f', [L, B, R, T]));
    // هر گلایف content stream صفحه، شامل خط slug
    if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
      Writeln(Length(Glyphs), ' glyphs in the page content stream');
    // فقط چیزی که صفحه نشان می‌دهد، با متن Form XObject درج شده
    if Pdf.ExtractLoadedPageText(0, PageText) then
      Writeln(PageText);
  finally
    Pdf.Free;
  end;
end;

متنی که از طریق Form XObjectها رسم شده از v2.768.3 بخشی از متن صفحه است. سرصفحه‌ها و مهرها و واترمارک‌ها خیلی وقت‌ها داخل فرم‌ها زندگی می‌کنند و بعضی اسناد استاندارد قبل از این تغییر 30 تا 35 درصد نویسه‌هایشان را از دست می‌دادند. THotPDF.InterpretContentWithForms هر Do را همراه با CTM جاری ثبت می‌کند، فرم را در /Matrix اش ضربدر همان CTM (§8.10.1) تفسیر می‌کند و گلایف‌های فرم را در جایگاه Do درج می‌کند، با بازگشت به فرم‌های تودرتو. فرمی که /Resources خودش را ندارد از آن stream ای که آن را رسم می‌کند قرض می‌گیرد، همان‌طور که §7.8.3 اجازه می‌دهد. گلایف‌های فرم TokenIndex = -1 حمل می‌کنند و ExtractLoadedPageGlyphs همچنان فقط گلایف‌های page-stream را برمی‌گرداند، چون جست‌وجو و جایگزینی و redaction تغییرات را از طریق TokenIndex به عقب می‌نویسند و اگر یک گلایف فرم داخل شود بایت‌های اشتباه را ویرایش می‌کردند. دو ساده‌سازی ارزش دانستن دارند: متن فرم به /BBox فرم بریده نمی‌شود، و بازگشت در 12 سطح متوقف می‌شود به‌جای تشخیص چرخه، پس فرم بدشکلی که خودش را رسم می‌کند متنش را تا رسیدن به همان سقف تکرار می‌کند

کدام گلایف‌ها وقتی HotPDF در Delphi متن صفحهٔ PDF را استخراج می‌کند شامل می‌شوند: ExtractLoadedPageText فقط گلایف‌هایی را نگه می‌دارد که مرکز جعبه‌شان داخل GetLoadedPageVisibleBox بیفتد، یعنی CropBox بریده‌شده به MediaBox، پس خطوط slug پرینتر ناپدید می‌شوند، در حالی که InterpretContentWithForms گلایف‌های Form XObject را در هر جایگاه Do با TokenIndex روی 1- درج می‌کند و API سطح گلایف همچنان همه چیز را برمی‌گرداند
تست جعبه روی مرکز گلایف تست دید نیست؛ متن سفید و متن بریده و متن پوشیده همچنان بیرون می‌آیند و متن فرم از v2.768.3 حساب می‌شود

چرا متن بعد از عملگر Q به‌شکل آشغال دیکود می‌شد؟

متن بعد از Q قبل از v2.766.73 می‌توانست اشتباه دیکود شود چون استخراج‌کننده فقط CTM را روی q ذخیره می‌کرد. پارامترهای وضعیت متن، یعنی فونت، اندازه، Tc، Tw، Tz، TL، حالت رندر و rise، بخشی از وضعیت گرافیک‌اند (§9.3.1)، پس Q باید آن‌ها را همراه همهٔ چیزهای دیگر روی پشته بازیابی کند (§8.4.2). یک گزارش صنعتی یک فونت دوبایتی Identity-H داخل q … Q انتخاب می‌کرد و بعد متن تک‌بایتی WinAnsi را بدون Tf مخصوص خودش نشان می‌داد. استخراج‌کننده فونت داخلی را نگه می‌داشت، سرنخ‌ها و کلمهٔ «Adobe» را در فهرست مطالب به‌شکل کدهای دوبایتی می‌خواند و 15% نویسه‌های صفحه را دور می‌انداخت. پشتهٔ q/Q مفسر حالا وضعیت متن کامل را نگه می‌دارد. قواعد استخراج توصیف‌شده در اینجا روی هر صفحه اعمال می‌شوند، پس یک سند کامل می‌تواند در یک فراخوانی به فایل برود

var
  Output: TFileStream;
  Pages: Integer;
begin
  Output := TFileStream.Create('report.txt', fmCreate);
  try
    // بازهٔ خالی = همهٔ صفحات؛ form feed بین صفحات؛ BOM یو‌تی‌اف-8
    Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
    Writeln(Pages, ' pages extracted');
  finally
    Output.Free;
  end;
end;

کدام API متن HotPDF را باید استفاده کنی؟

ExtractLoadedPageText در ترتیب content stream می‌ماند، که پیش‌فرض درست برای جست‌وجو و ایندکس است؛ زنجیرهٔ دیکود زیرش در استخراج متن از PDFهای بارگذاری‌شده با HotPDF پوشش داده شده. برای اسناد تگ‌شده‌ای که ترتیب نگارش‌شان مهم است، استخراج متن به ترتیب ساختار به‌جای حدس زدن از روی هندسه درخت ساختار را می‌پیماید، و برای داده‌های قفل‌شده در جدول‌ها، استخراج جدول تایپ‌دار در طول شکست صفحات به‌جای خط‌ها سلول برمی‌گرداند. مرجع کامل API و دانلود آزمایشی در صفحهٔ محصول HotPDF Delphi PDF Component است