Artikel Teknis

Ekstraksi Teks PDF Terstruktur di Delphi dengan PDFium VCL

PDFiumPas mengembalikan teks halaman sebagai sebuah struktur, bukan sekadar string. GetStructuredText menghasilkan sebuah TPdfStructuredTextPage yang berisi block, masing-masing memuat line, masing-masing memuat styled span, dengan bounds page-space di setiap level dan index karakter sumber dipertahankan sehingga fragmen mana pun bisa dipetakan kembali ke text page yang mendasarinya

Ekstraksi flat-string yang menjadi titik awal kebanyakan kode masih ada di sana dan tetap benar untuk tujuannya. Ia berhenti menjadi cukup pada saat Anda perlu tahu kata mana yang merupakan heading, kata mana yang termasuk kolom kiri, atau di mana di halaman itu sebuah match sebenarnya berada

Mengapa flat string adalah output yang salah untuk kebanyakan pekerjaan?

Karena pertanyaan yang diajukan orang tentang teks yang diekstrak nyaris tidak pernah "karakter apa saja yang ada di halaman ini". Pertanyaannya adalah "apa judulnya", "apakah ini sebuah tabel", "apakah paragraf ini termasuk section 4", "di mana saya harus menggambar highlight-nya". Sebuah string tunggal tidak menjawab satu pun dari itu, dan setiap jawaban yang Anda rekonstruksi darinya adalah sebuah heuristik yang sekarang menjadi tanggung jawab Anda

Layout dua kolom membuat poin ini konkret. Ekstrak sebuah artikel dua kolom sebagai string dan, tergantung bagaimana producer menulis content stream-nya, Anda mungkin mendapatkan kolom satu diikuti kolom dua, atau Anda mungkin mendapatkan baris satu dari kolom satu, baris satu dari kolom dua, baris dua dari kolom satu, dan seterusnya menyusuri halaman. Keduanya bisa keluar dari sebuah PDF yang conforming. Tidak ada yang salah di level format, karena PDF mendeskripsikan tanda di sebuah halaman, bukan outline sebuah dokumen. Model berbasis block memungkinkan extractor membuat keputusan urutan itu secara eksplisit dan memberi tahu Anda keputusan mana yang diambilnya

Content order atau physical layout?

TPdfStructuredTextOptions.ReadingOrder memilih antara roContentOrder dan roPhysicalLayout, dan jawaban yang tepat bergantung pada mana yang lebih Anda percayai, producer-nya atau geometrinya

Content order mengembalikan teks dalam urutan content stream-nya menggambarnya. Itu cepat, dan untuk dokumen yang dihasilkan oleh producer yang berperilaku baik, biasanya itulah reading order yang dimaksudkan. Physical layout mengabaikan urutan stream dan merekonstruksi urutannya dari tempat karakter-karakter itu sebenarnya berada, mengelompokkannya menjadi line lalu menjadi column. Itulah yang Anda inginkan untuk halaman hasil scan-lalu-OCR, untuk output dari tool yang mengeluarkan teks dalam urutan font alih-alih reading order, dan untuk apa pun di mana hasil visual adalah satu-satunya hal yang bisa Anda andalkan

uses
  PDFium;

var
  Pdf: TPdf;
  Options: TPdfStructuredTextOptions;
  Page: TPdfStructuredTextPage;
  B, L: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'article.pdf';
    Pdf.LoadDocument;
    Pdf.PageNumber := 1;                     // 1-based

    Options := TPdfStructuredTextOptions.Default;
    Options.ReadingOrder := roPhysicalLayout;
    Options.IncludeFontInfo := True;
    Options.IncludeSemantics := True;
    Options.MaxCharacters := 200000;         // fail-closed budget

    Page := Pdf.GetStructuredText(Options);

    for B := 0 to High(Page.Blocks) do
    begin
      if Page.Blocks[B].Kind = cfHeading then
        Emit(Format('H%d: %s',
          [Page.Blocks[B].HeadingLevel, Page.Blocks[B].Text]))
      else
        for L := 0 to High(Page.Blocks[B].Lines) do
          Emit(Page.Blocks[B].Lines[L].Text);
    end;
  finally
    Pdf.Free;
  end;
end;

Apa yang ditambahkan tagging yang tidak bisa diberikan geometri?

Intent. Dengan IncludeSemantics diaktifkan, block dari sebuah PDF tagged membawa sebuah Kind yang diambil dari structure tree, sehingga sebuah heading adalah heading karena producer-nya bilang begitu, bukan karena font-nya lebih besar dari rata-rata. Kind-kind itu mencakup bentuk-bentuk yang penting untuk reuse: cfParagraph, cfHeading dengan sebuah HeadingLevel, cfListItem, cfTableCell, cfCaption, cfFigure, dan fallback untagged cfPlain

Field Source mencatat dari mana setiap klasifikasi berasal, rosStructure untuk structure tree dan rosHeuristic untuk inferensi, yang merupakan field yang perlu dicatat saat Anda memutuskan seberapa jauh mempercayai sebuah extraction pipeline di seluruh kumpulan dokumen. Figure adalah kasus khusus yang layak diketahui: untuk sebuah block cfFigure, teksnya berasal dari deskripsi alternatif, bukan dari glyph apa pun, karena sebuah figure tidak memiliki karakternya sendiri. Alternate text yang tidak cocok tetap direpresentasikan alih-alih dibuang, yang memungkinkan sebuah audit aksesibilitas melihat bahwa sebuah deskripsi memang ada bahkan saat tidak ada apa pun di halaman itu yang menggambarnya. Model tagging itu sendiri dibahas di validasi structure tree PDF/UA

Span membawa styling dan provenance

Setiap TPdfStructuredTextSpan menyimpan teksnya, bounds page-space-nya, FontName, FontSize, FontWeight, dan Angle, ditambah SourceStartIndex dan SourceCharacterCount. Span terpecah di tempat styling berubah, sehingga sebuah kalimat dengan tiga kata bold menjadi tiga span, dan membangun kembali emphasis di HTML atau Markdown menjadi soal membaca properti alih-alih menebak dari nama font

Kedua field source-index itulah yang mengubah ekstraksi menjadi sebuah fitur, bukan sekadar sebuah laporan. Keduanya menunjuk kembali ke urutan karakter halaman, yang berarti sebuah block yang Anda temukan dalam sebuah pencarian bisa diubah menjadi geometri selection level-karakter atau sebuah persegi panjang highlight tanpa pass kedua yang urutannya berbeda atas teks itu; mekanismenya dijelaskan di seleksi baris teks visual dengan character box. Field Angle lebih penting dari yang terlihat: teks yang diputar di dalam sebuah stamp atau watermark mendarat di ruang koordinat yang sama dengan teks body, dan sebuah pipeline yang mengabaikan angle akan dengan senang hati menggabungkan sebuah "DRAFT" diagonal ke tengah-tengah sebuah paragraf

var
  Page: TPdfStructuredTextPage;
  B, S, L: Integer;
  Emphasised: Boolean;
begin
  Page := Pdf.GetStructuredText(Options);

  if Page.UnmappedCharacterCount > 0 then
    Log(Format('page %d: %d characters without a Unicode mapping',
      [Page.PageNumber, Page.UnmappedCharacterCount]));
  if Page.GeometryFailureCount > 0 then
    Log(Format('page %d: %d characters without geometry',
      [Page.PageNumber, Page.GeometryFailureCount]));

  for B := 0 to High(Page.Blocks) do
    for L := 0 to High(Page.Blocks[B].Lines) do
      for S := 0 to High(Page.Blocks[B].Lines[L].Spans) do
      begin
        Emphasised := Page.Blocks[B].Lines[L].Spans[S].FontWeight >= 600;
        AppendRun(Page.Blocks[B].Lines[L].Spans[S].Text, Emphasised,
          Page.Blocks[B].Lines[L].Spans[S].SourceStartIndex);
      end;
end;

Budget, dan dua counter kualitas

MaxCharacters adalah sebuah budget fail-closed, bukan sebuah pengaturan truncation: sebuah halaman yang melampauinya akan berhenti alih-alih diam-diam mengembalikan sebagian kontennya. Pada sebuah intake path yang tidak tepercaya, itulah perilaku yang Anda inginkan, karena sebuah halaman dengan sejuta karakter entah adalah sebuah monster hasil mesin atau sebuah upaya untuk menjadikan extractor Anda bagian tersibuk dari sistem

Dua counter pada halaman yang dikembalikan mendeskripsikan kualitas ekstraksi secara langsung. UnmappedCharacterCount menghitung karakter tanpa Unicode mapping yang bisa dipakai, yang merupakan gejala klasik dari sebuah subset font yang di-embed tanpa CMap /ToUnicode; teks semacam itu dirender dengan sempurna namun diekstrak menjadi tidak berguna sama sekali. GeometryFailureCount menghitung karakter yang bounding box-nya tidak bisa ditentukan, yang menurunkan kualitas urutan physical-layout. Catat keduanya. Sebuah kumpulan dokumen di mana kedua angka itu konsisten mendekati nol bisa diindeks dengan percaya diri, dan yang tidak demikian memberi tahu Anda bahwa beberapa producer di dalam pipeline Anda membutuhkan perhatian sebelum hasil downstream apa pun bisa dipercaya

Performa pada halaman nyata

Ekstraksi physical-layout adalah mode yang mahal, dan implementasinya dibangun untuk halaman yang sungguh-sungguh besar: pengurutan karakter berjalan dalam O(n log n), bukan lewat pemindaian berulang, buffer line dan span tumbuh secara geometris alih-alih realokasi per karakter, teks Unicode dibangun di dalam buffer alih-alih lewat concatenation string, dan lookup font untuk text object yang bersebelahan di-cache. Kombinasi itulah yang menjaga sebuah halaman padat 5.000 karakter tetap predictable, bukannya kuadratik

Untuk pekerjaan yang berat dari sisi jumlah halaman, tetap ada gunanya memilih mode yang lebih murah di mana pun Anda bisa. Gunakan roContentOrder dengan semantics diaktifkan untuk dokumen tagged yang Anda percayai, dan cadangkan roPhysicalLayout untuk materi hasil scan dan legacy di mana geometri adalah satu-satunya sinyal. Jika yang Anda butuhkan hanyalah sebuah string biasa, API yang lebih sederhana yang dijelaskan di mengekstrak teks dari dokumen PDF tetap menjadi jalur yang lebih cepat, dan saat Anda perlu menelusuri teks kembali ke marked-content identifier, membaca dan menulis marked content BDC dan MCID membahas lapisan itu

Model block ini juga terpetakan dengan bersih ke apa yang diinginkan pipeline retrieval: sebuah heading beserta paragraf-paragrafnya adalah sebuah chunk dengan sebuah judul, dan bounds-nya memungkinkan sebuah citation menunjuk ke sebuah lokasi di sebuah halaman, bukan sekadar ke sebuah dokumen. PDFiumPas adalah komponen Delphi dan Lazarus di sekitar engine PDFium, didokumentasikan dengan contoh di halaman komponen Delphi PDFium