Artikel Teknis

Memetakan Teks PDF ke Byte Content Stream di Delphi

Satu karakter keliru pada nomor invoice, tetapi satu-satunya primitive edit yang tersedia menulis ulang seluruh text run. PDF Library for Delphi menutup celah itu: GetTextBlockCharContentLocation memetakan setiap posisi UTF-16 hasil ekstraksi kembali ke instruksi content stream, operand, dan rentang byte terenkode yang menghasilkannya, lalu ReplaceTextBlockCharSourceBytes menimpa hanya rentang tersebut. Ekstraksi teks biasanya membuang semua informasi yang Anda perlukan. Anda mendapatkan Unicode, width, dan geometri, sementara provenance menghilang, sehingga karakter pada posisi 7 di block 3 tinggal sebuah karakter. Stream mana yang menghasilkannya, instruksi mana, operand mana, byte mana di dalam operand itu: semuanya hilang. Setiap strategi point-edit yang dibangun di atasnya harus menebak, biasanya dengan mencari substring di content yang sudah didekode dan berharap substring itu muncul tepat satu kali. Pada halaman nyata, harapan itu tidak terpenuhi

Mengapa menulis ulang seluruh text run merusak halaman?

Karena run bukan sekadar teks. Operator text-showing dalam ISO 32000-1 §9.4.3 mencakup TJ, yang operand-nya berupa array yang menyelang-nyelingkan string dengan numeric adjustment, dan angka-angka itu adalah typesetting. Baris yang ditata sebagai [(AB) -120 (CD)] TJ membawa kern 120 per seribu em di antara kedua string. Keluarkan Tj baru dengan teks yang sudah digabung, kern tersebut hilang, baris bergeser sedikit, dan pada form nilainya keluar dari kotak. Keberatan yang sama berlaku untuk font: byte operand adalah code dalam encoding apa pun yang dipilih Tf, bukan Unicode, dan untuk composite font byte tersebut dapat berupa CID dua-byte yang tidak memiliki hubungan dengan karakter yang Anda baca dari extractor. Regenerasi run mengharuskan Anda benar tentang encoding font, map /ToUnicode, dan glyph coverage-nya. Point editing menghindari semuanya dengan tetap berada di byte domain

Apa yang dikembalikan GetTextBlockCharContentLocation?

Method tersebut menyelesaikan satu karakter menjadi record sembilan field, dan setiap field merupakan address, bukan value. ContentLayer adalah index berbasis 1 ke array /Contents halaman, atau 0 ketika karakter berasal dari nested content. StreamObjectNumber dan StreamGeneration mengidentifikasi stream yang memuatnya. InstructionIndex adalah posisi berbasis 0 dalam content program yang sudah didekode, OperandIndex adalah operand text-string, dan ArrayElementIndex adalah elemen di dalam array TJ atau -1 untuk direct string operand. SourceByteOffset dan SourceByteLength kemudian menyebut rentang byte di dalam string yang sudah didekode itu

Var
  Lib: TPDFlib;
  ListID, Block, CharPos: Integer;
  ContentLayer, StreamObjectNumber, StreamGeneration: Integer;
  InstructionIndex, OperandIndex, ArrayElementIndex: Integer;
  SourceByteOffset, SourceByteLength, Flags: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    Lib.LoadFromFile('invoice.pdf', '');
    Lib.SelectPage(1);
    ListID:= Lib.ExtractPageTextBlocks(3);
    Try
      // Block dan CharPos berasal dari scan Anda sendiri atas GetTextBlockText
      If Lib.GetTextBlockCharContentLocation(ListID, Block, CharPos,
        ContentLayer, StreamObjectNumber, StreamGeneration,
        InstructionIndex, OperandIndex, ArrayElementIndex,
        SourceByteOffset, SourceByteLength, Flags)= 1 Then
      Begin
        // ContentLayer = 0 berarti glyph berada di nested Form XObject
        // ArrayElementIndex = -1 berarti operand Tj biasa, bukan array TJ
      End;
    Finally
      Lib.ReleaseTextBlocks(ListID);
    End;
  Finally
    Lib.Free;
  End;
End;

Lookup tidak menimbulkan biaya saat query. Ketika renderer mendekode setiap content layer, renderer mendaftarkan logical span yang sedang dilewatinya, sehingga query posisi menjadi binary search pada ordered interval list, bukan linear scan atas setiap content span untuk setiap karakter. Tidak ada yang diparse ulang ketika Anda bertanya; map sudah dibangun selama extraction pass yang memang sudah Anda bayar. Jika Anda sudah melakukan enumerasi hit dengan PDF text search yang mengembalikan koordinat hit, menambahkan content location untuk setiap hit hampir tidak menambah biaya

Mengedit byte, bukan Unicode

ReplaceTextBlockCharSourceBytes menerima AnsiString berisi raw replacement byte dalam active PDF font encoding. Itulah seluruh desainnya, dan itu disengaja. Tidak ada transcode, tidak ada re-encode, dan tidak ada tebakan tentang font. Library menyisipkan byte Anda di atas rentang yang disebutkan dari target string lalu menerbitkan ulang content layer yang memuatnya. String yang berdekatan dalam array TJ yang sama dan numeric kern di antaranya dibiarkan byte-identical. Ambil layout di atas: menemukan B dalam [(AB) -120 (CD)] TJ menghasilkan ArrayElementIndex 0, SourceByteOffset 1, dan SourceByteLength 1. Ganti dengan Z, maka content yang diterbitkan berisi (AZ), tetap diikuti -120 dan (CD), keduanya tidak disentuh. Regression suite menegaskan tepat hal itu, karena klaim "we preserved the kerning" adalah jenis klaim yang dapat diam-diam berhenti benar

Function EditableHere(Flags: Integer): Boolean;
Begin
  Result:= ((Flags and PDF_TEXT_CHAR_CONTENT_LOCATION_VALID)<> 0)and
    ((Flags and (PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED or
      PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT or
      PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED or
      PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER or
      PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED))= 0);
End;

// ...
If EditableHere(Flags) Then
Begin
  If Lib.ReplaceTextBlockCharSourceBytes(ListID, Block, CharPos, 'Z')= 1 Then
  Begin
    // Semua location dalam list lama kini stale. Lakukan extraction ulang
    Lib.ReleaseTextBlocks(ListID);
    ListID:= Lib.ExtractPageTextBlocks(3);
  End
  Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_STALE Then
    // Layer berubah sejak extraction dilakukan
  Else If Lib.LastErrorCode= PDFLIB_ERROR_TEXT_LOCATION_READ_ONLY Then
    // Ada flag yang gagal kita periksa, atau flag yang ditambahkan versi berikutnya
End;

Dua detail operasional perlu diingat. Pemanggilan ini sementara berpindah ke halaman tempat text list diekstrak, lalu mengembalikan halaman yang sebelumnya dipilih baik saat sukses maupun gagal, sehingga cursor Anda tidak bergeser diam-diam. Saat berhasil, pemanggilan juga menghapus page element snapshot, yang membuat semua handle dari pass enumerasi sebelumnya menjadi invalid

Karakter mana yang tidak dapat diedit?

Ada enam kategori, dan library menyebutkan masing-masing dalam bitmask Flags, bukan gagal secara samar. Ini lebih penting daripada happy path, karena pada dokumen nyata kasus yang tidak dapat dipetakan sering terjadi dan setiap kasus memiliki alasan yang berbeda

  • PDF_TEXT_CHAR_CONTENT_LOCATION_LIGATURE: beberapa posisi UTF-16 hasil ekstraksi berasal dari satu source glyph. Entry /ToUnicode yang memetakan satu code ke fi memberi Anda dua karakter yang berbagi satu rentang byte, jadi perlakukan keduanya sebagai satu source glyph dan edit rentangnya sekali
  • PDF_TEXT_CHAR_CONTENT_LOCATION_GENERATED: karakter disintesis saat layout. Inferred word space adalah kasus umum, dan tidak memiliki source byte sama sekali, sehingga SourceByteOffset bernilai -1 dan SourceByteLength 0
  • PDF_TEXT_CHAR_CONTENT_LOCATION_ACTUALTEXT: teks yang Anda baca berasal dari replacement /ActualText. Tidak ada reverse mapping unik dari string pengganti ke source byte, sehingga location hanya bersifat diagnostik
  • PDF_TEXT_CHAR_CONTENT_LOCATION_NESTED: glyph berada di dalam Form XObject. Byte-nya dapat di-address, tetapi Form dapat digambar oleh beberapa halaman, sehingga mengeditnya melalui high-level API berarti melakukan edit yang tidak Anda minta
  • PDF_TEXT_CHAR_CONTENT_LOCATION_TRANSCODED: operand berupa hex string yang membawa byte order mark UTF-16BE, dan extraction path yang ada mendekodenya sebelum font mapping. Offset pada hasil decode tidak lagi menunjuk byte asli, sehingga valid flag dihapus
  • PDF_TEXT_CHAR_CONTENT_LOCATION_CROSS_LAYER: string operand dan text-showing operator-nya berada di dua stream berbeda

Kasus terakhir layak mendapat kalimat sendiri karena engineer sering menganggapnya tidak mungkin. ISO 32000-1 §7.8.2 menyatakan bahwa stream dalam array halaman /Contents digabungkan, dan pembagiannya hanya diwajibkan jatuh pada lexical boundary. Jadi BT /F1 16 Tf 220 340 Td (CrossLayer) di satu stream dan Tj ET di stream berikutnya adalah halaman yang sepenuhnya legal. Mapping mempertahankan posisi diagnostik tetapi menandainya read-only, karena instruction index milik operator berada di layer berbeda dari byte operand, dan menggunakan salah satunya untuk meng-address yang lain akan merusak file

Bagaimana library tahu bahwa map masih valid?

Dengan fingerprint yang diperiksa tepat sebelum write. Setiap extraction list mencatat source page serta, untuk setiap content layer, panjang layer dan dua rolling hash independen: hash FNV-1a dan hash xor bergaya DJB2. Sebelum ReplaceTextBlockCharSourceBytes melakukan parsing apa pun, method tersebut membaca ulang target layer dan membandingkan ketiga nilai. Perubahan byte apa pun di mana pun pada layer itu mengembalikan PDFLIB_ERROR_TEXT_LOCATION_STALE dan write tidak dilakukan. Ini sengaja konservatif: pemeriksaan dilakukan per layer, bukan per instruction, sehingga edit yang tidak berhubungan di tempat lain pada content stream yang sama juga membatalkan location Anda. Itulah trade-off yang benar: offset pada stream yang bergeser bahkan satu byte bukan hampir benar, melainkan korupsi diam-diam. Disiplin yang sama mengatur permukaan editing lain, termasuk content-stream state tracker untuk CTM dan clipping. Setelah replacement berhasil, buang list dan lakukan extraction lagi

Mapping read-only melalui Direct Access

DAGetTextBlockCharContentLocation memberi record identik untuk halaman yang dibuka melalui Direct Access path, dengan vocabulary flag yang identik. Method ini diagnostik saja berdasarkan konstruksinya: ReplaceTextBlockCharSourceBytes bekerja pada dokumen editable yang dipilih, sedangkan Direct Access adalah read path. Data location tetap berada dalam text block list setelah file handle ditutup, sehingga dapat digunakan untuk offline auditing

FileHandle:= Lib.DAOpenFileReadOnly('audit.pdf', '');
Try
  PageRef:= Lib.DAFindPage(FileHandle, 1);
  DirectList:= Lib.DAExtractPageTextBlocks(FileHandle, PageRef, 3);
  Try
    Lib.DAGetTextBlockCharContentLocation(DirectList, Block, 1,
      ContentLayer, StreamObjectNumber, StreamGeneration,
      InstructionIndex, OperandIndex, ArrayElementIndex,
      SourceByteOffset, SourceByteLength, Flags);
    // Location tetap dapat dibaca setelah DACloseFile
  Finally
    Lib.DAReleaseTextBlocks(DirectList);
  End;
Finally
  Lib.DACloseFile(FileHandle);
End;

Gunakan untuk menjawab pertanyaan, bukan untuk mengubah sesuatu. Halaman mana yang membawa teks yang tidak pernah dapat Anda edit in-place? Berapa banyak corpus ini yang datang dengan override /ActualText? Output vendor mana yang memecah operator di antara content layer? Query tersebut murah setelah setiap karakter memiliki address, dan layak dijalankan sebelum Anda berkomitmen pada correction pipeline

Di mana point editing berhenti?

Point editing adalah scalpel, bukan text engine. Ia mengubah byte in-place, sehingga replacement text yang lebih lebar atau lebih sempit dari aslinya tidak akan reflow, tidak akan rewrap, dan tidak akan memperbarui kern di sekitarnya. Mengganti satu digit dengan digit lain dalam field monospaced adalah penggunaan yang tepat. Mengetik ulang paragraf bukan. Dan ini sama sekali bukan security tool: menimpa glyph byte membuat byte asli tetap dapat dipulihkan dari revision history file, sehingga segala sesuatu yang membutuhkan confidentiality harus memakai true redaction yang menghapus content, bukan sekadar menutupinya. Sebagai imbalan atas batasan tersebut, hasilnya jujur. Setiap karakter memiliki byte address yang dapat Anda tindak atau named flag yang menjelaskan mengapa tidak, dan fingerprint check menjadikan stale map sebagai hard error, bukan halaman yang korup. Character-to-content-byte mapping dan in-place source byte replacement tersedia sebagai bagian dari permukaan text extraction dan content editing PDF Library for Delphi, native Object Pascal PDF library untuk Delphi, C++Builder, dan Lazarus