مقاله فنی

بازچینش محتوای PDF به HTML واکنش‌گرا در دلفی

PDFium Component یک PDF با چیدمان ثابت را با استفاده از BuildReflowDocument به یک مدل معنایی قابل‌بازچینش تبدیل می‌کند، و آن مدل را به‌صورت HTML خودکفا از طریق ToHtml صادر می‌کند. عنوان‌ها عنوان می‌مانند، آیتم‌های فهرست، آیتم فهرست می‌مانند، و جدول‌های تشخیص‌داده‌شده روی صفحه به‌صورت نشانه‌گذاری واقعی جدول با سلول‌های سربرگ و اسپن‌های حفظ‌شده بیرون می‌آیند. هیچ چیزی در خروجی به یک اسکریپت یا شیوه‌نامه خارجی ارجاع نمی‌دهد

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

اطلاعات معنایی از کجا می‌آید؟

همه‌چیز از GetStructuredText شروع می‌شود، تنها منبع متن و معنا در مؤلفه. وقتی PDF یک درخت ساختار حمل می‌کند، PDF برچسب‌گذاری‌شده همان‌طور که در ISO 32000-1 بند ۱۴.۷ تعریف شده، مدل از سلسله‌مراتب منطقی‌ای که تولیدکننده ثبت کرده پیروی می‌کند. وقتی چنین نیست، و بیشتر PDFهای موجود در دنیای واقعی چنین نیستند، مدل به ترتیب چیدمان فیزیکی‌ای که از قبل برای اهداف ترتیب خواندن محاسبه شده بازمی‌گردد

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

هر گره ثبت می‌کند اطلاعاتش از کجا آمده، بنابراین یک مصرف‌کننده می‌تواند یک عنوانی را که سند اعلام کرده از عنوانی که ابتکارهای چیدمان استنتاج کرده‌اند تشخیص دهد. خط‌لوله‌های حساس به اطمینان باید آن فیلد را بخوانند به‌جای آنکه همه گره‌ها را به‌یک‌اندازه معتبر در نظر بگیرند

یک درخت مسطح، و چرا این یک درخت شیء نیست؟

مدل یک درخت مسطح‌شده پیش‌ترتیبی (pre-order) است: آرایه‌ای از گره‌ها که هر گره یک ParentIndex و یک Depth حمل می‌کند، نه یک رکورد بازگشتی یا یک گراف شیء با مالکیت. صفحات، عنوان‌ها، پاراگراف‌ها، فهرست‌ها، آیتم‌های فهرست، شکل‌ها، زیرنویس‌ها، جدول‌ها، ردیف‌ها و سلول‌ها همگی در آن یک آرایه خطی زندگی می‌کنند

دو منفعت به‌دنبال آن می‌آید. مصرف‌کننده‌ها می‌توانند آرایه را به‌ترتیب بدون بازگشت جریانی کنند، که تولید HTML، Markdown یا یک نمای درختی را یک حلقه ساده می‌کند. و چیدمان در سراسر دلفی، C++Builder و Free Pascal قابل‌حمل باقی می‌ماند، که در نحوه مدیریت انواع مدیریت‌شده بازگشتی در سراسر یک مرز ABI متفاوت‌اند. یک رکورد بازگشتی از آرایه‌های پویا دقیقاً همان نوع سازه‌ای است که همه‌جا کامپایل می‌شود و در هرکدام به‌شکل ظریفی متفاوت رفتار می‌کند

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfReflowOptions;
  Doc: TPdfReflowDocument;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.LoadDocument;

    Options := TPdfReflowOptions.Default;
    Options.FullDocument := True;
    Options.DetectTables := True;
    Options.IncludeCss := True;          // بلوک سبک درون‌خطی، بدون فایل خارجی
    Options.MaxNodes := 200000;          // بودجه fail-closed
    Options.MaxCharacters := 4000000;

    Doc := Pdf.BuildReflowDocument(Options);

    for I := 0 to High(Doc.Nodes) do
      case Doc.Nodes[I].Kind of
        prnkHeading:
          Writeln(Format('%sH%d: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Doc.Nodes[I].HeadingLevel, Doc.Nodes[I].Text]));
        prnkParagraph:
          Writeln(Format('%sp: %s', [StringOfChar(' ', Doc.Nodes[I].Depth),
            Copy(Doc.Nodes[I].Text, 1, 60)]));
        prnkTable:
          Writeln(Format('table on page %d', [Doc.Nodes[I].PageNumber]));
      end;

    Writeln(Format('%d node(s), %d table(s), %d character(s)',
      [Length(Doc.Nodes), Doc.TableCount, Doc.CharacterCount]));
  finally
    Pdf.Free;
  end;
end;

چگونه از دوباره‌ظاهرشدن جدول‌ها جلوگیری می‌شود؟

تشخیص جدول پس از جمع‌آوری متن ساختاریافته برای یک صفحه اجرا می‌شود، که یک خطر آشکار ایجاد می‌کند: همان محتوای سلول هم در بلوک‌های متن و هم در جدول تشخیص‌داده‌شده وجود دارد. انتشار هر دو، HTML‌ای تولید می‌کند که در آن هر جدول دوباره با محتوای خودش به‌صورت پاراگراف‌های شل دنبال می‌شود

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

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

صادرکردن HTML‌ای که خودکفا باقی می‌ماند

ToHtml مدلی را که از قبل ساخته شده می‌پیماید و هرگز دوباره به PDFium مراجعه نمی‌کند، بنابراین صادرکردن دوباره هیچ هزینه اضافی ندارد و نمی‌تواند نتیجه‌ای متفاوت از همان مدل تولید کند. متن و مقادیر ویژگی به‌طور یکنواخت اسکیپ می‌شوند، سطوح عنوان به بازه h1 تا h6 که HTML واقعاً تعریف می‌کند محدود می‌شوند، و سلول‌های سربرگ، RowSpan و ColumnSpan همان‌طور که نوشته شده‌اند عبور می‌کنند

CSS اختیاری یک بلوک سبک درون‌خطی ساده است. هیچ اسکریپت، هیچ وب‌فونت و هیچ منبع خارجی از هیچ نوعی وجود ندارد، که همان چیزی است که خروجی را برای تعبیه در یک ایمیل، یک نمایشگر راهنما یا یک کنترل مرورگر ایمن‌سازی‌شده (sandboxed) امن می‌کند:

var
  Html: WideString;
  Stream: TFileStream;
  Bytes: TBytes;
begin
  Options := TPdfReflowOptions.Default;
  Options.FullDocument := True;
  Options.IncludeCss := True;
  Options.IncludePageSections := True;   // حفظ مرزهای صفحه قابل‌مشاهده
  Options.PreserveLineBreaks := False;   // بگذارید مرورگر پاراگراف‌ها را بشکند

  Html := Pdf.BuildReflowDocument(Options).ToHtml;

  Bytes := TEncoding.UTF8.GetBytes(string(Html));
  Stream := TFileStream.Create('report.html', fmCreate);
  try
    if Length(Bytes) > 0 then
      Stream.WriteBuffer(Bytes[0], Length(Bytes));
  finally
    Stream.Free;
  end;
end;

PreserveLineBreaks گزینه‌ای است که بیشترین ارزش فکرکردن را دارد. یک شکست خط PDF یک تصمیم صفحه‌آرایی است که برای یک عرض صفحه ثابت گرفته شده، بنابراین حفظ آن روی یک صفحه باریک، همان مشکلی را بازتولید می‌کند که بازچینش برای حل آن وجود دارد. شکست‌ها را برای شعر، فهرست‌های کد و نشانی‌ها حفظ کنید؛ برای نثر آن‌ها را کنار بگذارید

بودجه‌ها، لغو و وضعیت صفحه

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

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

بازچینش برای چه چیزی خوب است، و برای چه چیزی نه؟

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

مشخصاً برای فناوری کمکی، مدل بازچینش با ویژگی‌های خواندن که در ساخت یک خواننده قابل‌دسترس شرح داده شده جفت می‌شود، و اسنادی که یک درخت ساختار واقعی حمل می‌کنند مدل‌های به‌طور محسوس بهتری تولید می‌کنند، که استدلال خوبی برای تأیید برچسب‌گذاری در بالادست است، همان‌طور که در تأیید درخت ساختار PDF/UA شرح داده شده

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