Artikel Teknis

Ekstraksi Teks PDF di Delphi: Spasi Kata dan Line Break

HotPDF Delphi Component membangun ulang spasi kata dan line break di THotPDF.ExtractLoadedPageText dari geometri glyph, bukan dari karakter spasi. Spasi masuk ketika celah setelah lebar glyph sendiri melebihi 0,15 dari tinggi teks, dan baris baru hanya mulai ketika origin teks bergerak melintasi arah tulisan lebih dari setengah tinggi teks. Sejak v2.768.3 teks halaman juga menyertakan teks yang digambar lewat Form XObject dan membuang glyph di luar area crop yang terlihat. Sisa tulisan ini menjelaskan kenapa tiap aturan berbentuk seperti sekarang, karena semuanya menggantikan aturan yang lebih sederhana yang menghasilkan output terlihat masuk akal tapi salah pada dokumen nyata

Gejalanya familier bagi siapa pun yang pernah memasukkan teks PDF ke search index. Halaman sampul terekstrak sebagai PDFReferenceManualNovember4,1998, formulir pajak terpecah menjadi 156 baris, watermark diagonal datang satu karakter per baris, dan proof yang dipangkas dipimpin slug line printer yang tak pernah ditampilkan viewer mana pun. Tak satu pun dari file-file itu rusak. Masing-masing memakai cara penempatan teks yang sepenuhnya legal yang salah dibaca oleh extractor naif

Kenapa teks PDF hasil ekstraksi kehilangan spasi katanya?

Teks hasil ekstraksi kehilangan spasi kata karena PDF tak pernah diwajibkan memuatnya. Produser bisa memisahkan kata dengan menampilkan karakter spasi, tapi sama mudahnya menggerakkan pena dengan angka di dalam array TJ (ISO 32000-1 §9.4.3) atau dengan Td segar (§9.4.2), dan output TeX, banyak file Distiller, serta sebagian besar layout justified memang melakukan persis itu. Sebelum v2.766.76, HPDFAssemblePageText hanya melirik pergerakan vertikal, jadi pemisah kata yang dibuat oleh positioning begitu saja lenyap. Assembler kini mengukur, sepanjang arah tulisan glyph sebelumnya, jarak dari ujung lebar glyph itu sendiri ke origin glyph saat ini, dan menyisipkan satu spasi ketika jaraknya melebihi 0,15 dari tinggi kotak glyph saat ini, diukur dari ascent ke descent di user space. Tak ada spasi ditambahkan ketika salah satu sisi sudah kosong, dan tidak pula di antara dua karakter CJK, karena justification merenggangkan ideograf tanpa renggangan itu berarti batas kata. Record glyph memaparkan geometri yang sama, jadi Anda bisa mereproduksi keputusannya ketika file tertentu membuat Anda bingung

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
    // tinggi kotak glyph dari ascent ke descent, di user space
    Height := Sqrt(Sqr(Glyphs[I].QuadX[3] - Glyphs[I].QuadX[0]) +
      Sqr(Glyphs[I].QuadY[3] - Glyphs[I].QuadY[0]));
    // teks horizontal: celah dari ujung lebar glyph sebelumnya sendiri
    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;

Kenapa mengukur dari lebar glyph sendiri alih-alih posisi pena?

HotPDF mengukur celah kata dari GlyphEndX / GlyphEndY karena posisi pena setelah sebuah glyph sudah memuat spacing yang bukan celah. ISO 32000-1 §9.4.4 mendefinisikan perpindahan horizontal sebagai lebar glyph kali ukuran font, plus character spacing Tc, plus word spacing Tw, semuanya diskalakan oleh Tz. BaselineEndX / BaselineEndY menyimpan perpindahan penuh itu, sedangkan GlyphEndX / GlyphEndY hanya menyimpan advance font dan Tz. Pembedaannya penting untuk produser yang mengencangkan tracking dengan Tc negatif lalu mengembalikan ruangnya lewat penyesuaian TJ setelah setiap glyph: diukur dari posisi pena, pengembaliannya tampak seperti celah, dan istilah Mandarin “95后” terekstrak sebagai “9 5 后”. Threshold-nya dikaitkan ke tinggi kotak glyph alih-alih ke ukuran Tf dengan alasan serupa. Export Word sering menulis 1 Tf dan membawa ukuran sebenarnya di Tm yang diskalakan, jadi Tfs mengatakan 1 sementara teksnya setinggi 10 poin, dan aturan yang dikunci ke Tfs akan memperlakukan kedua ejaan halaman yang sama secara berbeda

Aturan spasi kata HotPDF untuk ExtractLoadedPageText di Delphi: spasi hanya disisipkan ketika jarak dari GlyphEndX glyph sebelumnya ke BaselineStartX glyph berikutnya melebihi 0,15 dari tinggi kotak ascent-ke-descent, karena posisi pena di BaselineEndX sudah memuat Tc, Tw dan Tz serta mengubah pengembalian tracking justified menjadi celah palsu seperti 9 5 后
Geometri, bukan karakter spasi, yang memutuskan di mana kata terpecah — record glyph memaparkan pengukuran yang sama, jadi Anda bisa memutar ulang keputusannya untuk file membingungkan mana pun

Aturan ini punya tepi yang jujur. Judul yang disetel dengan tracking sangat renggang, yang Tc-nya saja membuka lebih dari 0,15 tinggi teks di antara huruf, terekstrak dengan spasi di antara setiap huruf — itulah yang tampak di halaman, tapi mungkin bukan yang ingin Anda indeks. Potongan yang digambar tak berurutan pada satu baseline menghasilkan celah negatif dan menyatu tanpa spasi. Kedua kasus tak umum di teks badan, dan di corpus test perubahan ini menaikkan kecocokan kata terhadap extractor referensi di 28 halaman tanpa menurunkan satu pun

Kapan HotPDF memulai baris baru di teks hasil ekstraksi?

Sejak v2.766.79, baris baru dimulai ketika pergerakan dari origin glyph sebelumnya ke yang sekarang, diproyeksikan ke normal arah tulisan sebelumnya, melebihi setengah dari tinggi kotak yang lebih besar di antara kedua glyph. Aturan lama membandingkan pergerakan Y mentah dengan setengah Tfs, yang gagal ke dua arah. Dengan 1 Tf dan Tm yang diskalakan, threshold menyusut menjadi setengah unit, sehingga superscript yang dinaikkan text rise 0,4 atau jitter baseline yang biasa saja memecah baris. Aturan itu juga mengabaikan X sepenuhnya, sehingga teks di bawah Tm terotasi turun melintasi halaman dengan setiap glyph dan keluar satu glyph per baris. Proyeksi ke normal arah membuat run terotasi berperilaku seperti yang horizontal, dan mengambil tinggi yang lebih besar dari keduanya menjaga kata sampel besar beserta keterangannya yang kecil di satu baris ketika mereka berbagi baseline. Di formulir pajak yang disebut di atas, jumlah baris turun dari 156 menjadi 97. Teks vertikal dalam writing mode 1 (§9.7.4.3) mengikuti jalur terpisah: glyph-glyph itu dikelompokkan menjadi kolom, dibaca kanan ke kiri dan atas ke bawah, dengan line break di setiap pergantian kolom

Cara ExtractLoadedPageText milik HotPDF memutuskan line break di Delphi: pergerakan antar origin glyph diproyeksikan ke normal arah tulisan dan dibandingkan dengan setengah tinggi kotak yang lebih besar, sehingga superscript yang dinaikkan text rise kecil di bawah font 1 Tf dan teks yang turun melintasi halaman di bawah Tm terotasi tak lagi terpecah satu glyph per baris
Proyeksi membuat run terotasi berperilaku seperti yang horizontal, dan mengambil tinggi kotak yang lebih besar menjaga kata sampel besar beserta keterangannya yang kecil di satu baris

Teks mana yang disertakan dan ditinggalkan ExtractLoadedPageText?

ExtractLoadedPageText mengembalikan teks yang ditampilkan viewer. Sejak v2.766.80 ia bekerja hanya dari glyph yang terlihat, membuang setiap glyph yang pusat kotaknya jatuh di luar GetLoadedPageVisibleBox, yaitu CropBox yang di-clip ke MediaBox (§14.11.2). Itu menghilangkan slug line dan tanda printer lain yang disetel sebagai teks di luar area trim. ExtractLoadedPageGlyphs sengaja tetap mengembalikan setiap glyph dari content stream halaman, jadi Anda masih bisa menemukan materi itu ketika membutuhkannya. Filternya tes kotak, bukan tes keterlihatan: teks yang disembunyikan clipping path, digambar putih, atau tertutup gambar tetap terekstrak

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]));
    // setiap glyph dari content stream halaman, slug line termasuk
    if Pdf.ExtractLoadedPageGlyphs(0, Glyphs) then
      Writeln(Length(Glyphs), ' glyphs in the page content stream');
    // hanya yang ditampilkan halaman, dengan teks Form XObject disisipkan
    if Pdf.ExtractLoadedPageText(0, PageText) then
      Writeln(PageText);
  finally
    Pdf.Free;
  end;
end;

Teks yang digambar lewat Form XObject menjadi bagian teks halaman sejak v2.768.3. Header, stamp, dan watermark sangat sering tinggal di form, dan beberapa dokumen standar kehilangan 30 sampai 35 persen karakternya sebelum perubahan ini. THotPDF.InterpretContentWithForms mencatat setiap Do bersama CTM yang berlaku, menginterpretasi form pada /Matrix-nya kali CTM itu (§8.10.1), dan menyisipkan glyph milik form di posisi Do tersebut, berekursi ke form bersarang. Form tanpa /Resources sendiri meminjam milik stream yang menggambarnya, seperti diizinkan §7.8.3. Glyph form membawa TokenIndex = -1, dan ExtractLoadedPageGlyphs tetap hanya mengembalikan glyph page-stream, karena search, replace, dan redaksi menulis perubahan kembali lewat TokenIndex dan akan menyunting byte yang salah kalau glyph form menyelinap masuk. Dua penyederhanaan layak diketahui: teks form tak di-clip ke /BBox form, dan rekursi berhenti di 12 level alih-alih lewat deteksi siklus, jadi form malformed yang menggambar dirinya sendiri mengulang teksnya sampai mencapai batas itu

Glyph mana yang disertakan HotPDF saat mengekstrak teks halaman PDF di Delphi: ExtractLoadedPageText hanya menyimpan glyph yang pusat kotaknya jatuh di dalam GetLoadedPageVisibleBox, CropBox yang di-clip ke MediaBox, sehingga slug line printer lenyap, sementara InterpretContentWithForms menyisipkan glyph Form XObject di setiap posisi Do dengan TokenIndex disetel -1 dan API level glyph tetap mengembalikan semuanya
Tes kotak pada pusat glyph bukan tes keterlihatan — teks putih, teks ter-clip, dan teks tertutup tetap keluar, dan teks form ikut dihitung sejak v2.768.3

Kenapa teks setelah operator Q terdekode sebagai sampah?

Teks setelah Q bisa terdekode salah sebelum v2.766.73 karena extractor hanya menyimpan CTM saat q. Parameter text state — font, ukuran, Tc, Tw, Tz, TL, rendering mode, dan rise — adalah bagian graphics state (§9.3.1), jadi Q harus memulihkannya bersama semua yang lain di stack (§8.4.2). Satu laporan industri memilih font Identity-H dua byte di dalam q … Q lalu menampilkan teks WinAnsi satu byte tanpa Tf miliknya sendiri. Extractor menyimpan font bagian dalam itu, membaca leader dan kata “Adobe” di daftar isi sebagai kode dua byte, dan membuang 15% karakter halamannya. Stack q/Q milik interpreter kini menyimpan text state penuh. Aturan ekstraksi yang dijelaskan di sini berlaku untuk setiap halaman, jadi satu dokumen utuh bisa masuk ke file dalam satu panggilan

var
  Output: TFileStream;
  Pages: Integer;
begin
  Output := TFileStream.Create('report.txt', fmCreate);
  try
    // rentang kosong = semua halaman; form feed antar halaman; BOM UTF-8
    Pages := Pdf.ExtractLoadedPagesTextToStream(Output, '', #12, True);
    Writeln(Pages, ' pages extracted');
  finally
    Output.Free;
  end;
end;

API teks HotPDF mana yang sebaiknya Anda pakai?

ExtractLoadedPageText tetap dalam urutan content stream, yang merupakan default yang tepat untuk search dan indexing; rantai decoding di bawahnya dibahas di mengekstrak teks dari PDF termuat dengan HotPDF. Untuk dokumen bertag yang urutan pembuatannya penting, ekstraksi teks urutan struktur menelusuri structure tree alih-alih menebak dari geometri, dan untuk data yang terkunci di tabel, ekstraksi tabel bertipe lintas pergantian halaman mengembalikan sel alih-alih baris. Referensi API lengkap dan unduhan trial ada di halaman produk HotPDF Delphi PDF Component