Formats10 phút đọc

Cách chỉnh sửa văn bản trong PDF hoạt động: content stream, mã hóa font và vì sao chỉ sửa được từng dòng một

Một trang PDF không có đoạn văn hay khung văn bản, chỉ có các lệnh vẽ glyph bằng một font tại một vị trí. Sửa một dòng là xóa các lệnh cũ và ghi lệnh mới đúng chỗ đó, và điều này quyết định cái gì sửa được, cái gì không.

PDF123 · Updated 2026-10-14
Cách chỉnh sửa văn bản trong PDF hoạt động: content stream, mã hóa font và vì sao chỉ sửa được từng dòng một

Một trang PDF không có đoạn văn và không có khung văn bản. Nó có một danh sách lệnh: tại vị trí này, với font và cỡ chữ này, vẽ các glyph này. Đổi văn bản nghĩa là xóa các lệnh vẽ dòng đó và ghi lệnh mới tại cùng vị trí, với cùng font, cỡ chữ và màu. Vì vậy Sửa PDF sửa từng dòng một và không thể xuống dòng tự động: đó là hệ quả của cách văn bản được lưu.

Một dòng văn bản trông thế nào trong tệp

Nội dung của một trang nằm trong content stream, một chương trình nhỏ gồm các toán tử. Bốn toán tử làm phần lớn việc với văn bản: BT và ET bao quanh một khối văn bản, Tf chọn font và cỡ chữ, Td di chuyển tới vị trí tiếp theo, và Tj vẽ một chuỗi. Với font chuẩn, trường hợp đơn giản nhất trông như sau:

BT /F1 18 Tf 20 150 Td (Hello World) Tj ET

Nó có nghĩa là: chọn font F1 cỡ 18 point, di chuyển tới x 20, y 150, rồi vẽ Hello World. Một PDF in từ Chrome thường trông khác: trong ngoặc tròn không còn là ký tự mà là số hiệu glyph của font, và Tm đặt vị trí một lần. Dưới đây là dòng Hello World from Georgia từ một trang do Chrome xuất ra, trong đó cứ bốn chữ số thập lục phân là một số hiệu glyph:

BT /F4 24 Tf 1 0 0 -1 30 52 Tm <002b0048004f004f00520003003a00520055004f0047000300490055005200500003002a004800520055004a004c0044> Tj ET

002b là H, 0048 là e, 004f là l, 0003 là dấu cách, 003a là W, và cứ thế. Những con số này chỉ có nghĩa bên trong đúng font đó; chúng không phải Unicode. Khi bạn sao chép văn bản, trình xem đổi các số về ký tự bằng một bảng tra cứu được lưu ở chỗ khác trong tệp.

Từ đó có hai hệ quả. Thứ nhất, không có gì trong content stream nói "đây là một đoạn văn": ngắt dòng chỉ là thêm một Td hoặc Tm chuyển vị trí xuống dòng sau, và một đoạn có bao nhiêu dòng phải suy ra từ vị trí. Thứ hai, để ghi một ký tự mới vào dòng, bạn cần số hiệu của ký tự đó trong font đó, mà font thường chỉ chứa các glyph đã được dùng lúc tệp được ghi. Đó là kết quả của subset font.

Bạn có thể mở rộng các stream bằng qpdf:

qpdf --qdf --object-streams=disable in.pdf out.pdf

Mở out.pdf trong trình soạn thảo văn bản và tìm Tj để thấy mọi chuỗi được vẽ trên trang. Về những gì khác có trong một PDF, xem Bên trong một PDF: object, stream và bảng cross-reference.

Trình chỉnh sửa dựng lại một "dòng" từ các glyph như thế nào

Trước hết, trình chỉnh sửa phải gom các glyph rời thành một dòng mà người dùng bấm vào được. Các glyph cùng font, cùng cỡ chữ, cùng màu, và có khoảng cách tới glyph trước không quá một cỡ chữ thì thuộc cùng một cụm glyph (các glyph liền kề cùng font, cỡ chữ và màu); xuống dòng, đổi font hoặc đổi màu sẽ kết thúc cụm. Văn bản chạy từ phải sang trái, hoặc theo chiều dọc, không được gom nhóm và không mở ra để sửa được.

Có hai hệ quả quan sát được ngay. Một dòng trộn hai font, chẳng hạn dòng tiếng Trung có xen một từ tiếng Anh, bị tách thành hai cụm liền kề và được bấm riêng. Sau khi sửa cũng vậy: nếu văn bản mới dùng một font khác, dòng trở thành hai cụm, và sửa một cụm thì cụm kia giữ nguyên.

Sửa một dòng: xóa chữ cũ, rồi ghi chữ mới

Việc xóa dùng dạng bôi đen chỉ áp dụng cho văn bản: các glyph nằm trong vùng của dòng bị loại khỏi content stream, ảnh và đường kẻ được giữ nguyên, và không có ô màu nào được vẽ đè lên chữ cũ, nên sao chép và tìm kiếm không còn thấy nó. Nó chỉ đổi nội dung trang; nếu cùng đoạn văn bản đó còn xuất hiện trong một chú thích hoặc trong thuộc tính tài liệu thì những chỗ ấy không bị ảnh hưởng.

Việc ghi là thêm một lệnh văn bản mới vào cuối content stream của trang. Lệnh này bắt đầu từ vị trí đường cơ sở của dòng gốc và dùng lại cỡ chữ và màu của nó. Sau khi đổi dòng ở trên thành Hello World 2, phần được thêm vào cuối trang là:

q 1 0 0 1 22.5 148.92 cm 0 0 0 rg
/Span <</ActualText <feff00480065006c006c006f00200057006f0072006c006400200032>>> BDC
BT /F4 18 Tf 0 0 Td <002b0048004f004f00520003003a00520055004f00470003> Tj ET
BT /F9 18 Tf 102.18 0 Td (2) Tj ET
EMC
Q

BT đầu tiên dùng font gốc F4 và các số hiệu glyph gốc, nên Hello World được dùng lại y như cũ. Chữ "2" không có trong subset font gốc, nên BT thứ hai mở một font Helvetica chuẩn mới (F9) để vẽ nó. q và Q giới hạn phép biến đổi tọa độ và màu của khối này trong chính nó; trình chỉnh sửa cũng bọc nội dung gốc trong một cặp q/Q, để trạng thái còn dở dang ở cuối trang gốc không ảnh hưởng đến văn bản mới. Đó là hình dạng của một cụm; các con số thay đổi theo từng tệp.

Việc ghi thêm vào cuối kéo theo ba hệ quả:

  • Văn bản mới được vẽ sau cùng, đè lên mọi thứ khác trên trang. Văn bản từng nằm dưới một ảnh hoặc một khối màu sẽ hiện lên trên chúng sau khi sửa.
  • Công cụ trích xuất văn bản theo thứ tự content stream (như pdftotext -raw) đọc dòng đã sửa sau cùng trên trang đó, còn trích xuất theo vị trí (pdftotext -layout) thì không thấy thay đổi. Trang trông vẫn như cũ, nhưng thứ tự đọc đã đổi.
  • Trình chỉnh sửa gắn một ActualText vào văn bản mới, cho trình xem biết các glyph đại diện cho những ký tự thật nào. Một glyph có thể ứng với nhiều ký tự, và tra ngược chỉ từ số hiệu glyph đôi khi trả về một ký tự tương thích (ví dụ chữ 文 bị đọc thành bộ thủ Khang Hy ⽂); ActualText bảo đảm thứ bạn sao chép đúng là thứ bạn đã gõ.

Font nào vẽ văn bản mới

Dùng lại font gốc là lựa chọn an toàn nhất, vì hình dạng glyph, độ đậm và độ rộng khớp với phần bên cạnh. Có dùng lại được hay không tùy vào việc trình chỉnh sửa có biết một ký tự có mã nào trong font đó hay không:

  • Font phức hợp như font ở trên (Type0, số hiệu glyph hai byte, mã hóa Identity-H): tra ký tự trong font để lấy số hiệu glyph, và dùng nó nếu tìm thấy. Nếu không thấy, ký tự đó không nằm trong subset.
  • Font đơn giản một byte (TrueType, Type1) với mã hóa WinAnsi hoặc MacRoman: ánh xạ từ ký tự sang byte là cố định, và độ rộng lấy từ bảng độ rộng trong tệp.
  • Font có mã hóa tùy biến (Differences) hoặc dựa vào mã hóa dựng sẵn của font: không có ánh xạ đáng tin, nên trình chỉnh sửa không đoán mà chuyển sang font dự phòng, nếu không văn bản sẽ ra toàn ký tự rác.

Khi một ký tự không có trong font gốc, font dự phòng được dùng. Chữ cái Latin thường dùng font gần nhất trong Helvetica, Times và Courier, các font chuẩn mà trình xem nào cũng có, nên không cần nhúng gì. Mọi thứ còn lại, như chữ Hán, kana, Hangul, tiếng Thái, chữ Latin có dấu, Cyrillic và Hy Lạp, dùng một font Noto mã nguồn mở: máy chủ lấy các ký tự trong yêu cầu rồi cắt đúng các glyph đó khỏi font đầy đủ bằng thư viện tạo subset mã nguồn mở HarfBuzz, thường chỉ vài KB, và trình duyệt nhúng chúng vào tệp của bạn. Dạng chữ theo vùng của chữ Hán được chọn theo ngôn ngữ giao diện: giản thể, phồn thể, Nhật hay Hàn.

Các glyph được nhúng thay vì chỉ ghi tên font vì font chỉ được tham chiếu bằng tên sẽ bị mỗi trình xem thay bằng font dự phòng riêng của nó, và độ đậm có thể khác nhau giữa các trình xem; khi đã nhúng, mọi trình xem hiển thị cùng một hình dạng.

Vì sao chỉ sửa từng dòng một

Trình chỉnh sửa không có công cụ dàn trang. Mọi vị trí glyph đều cố định trong tệp; văn bản mới chỉ được xếp sang phải từ điểm bắt đầu theo độ rộng của font, không có logic "xuống dòng ở mép", và không có gì phía sau nó bị dời đi. Kéo dài một dòng thì các ký tự thừa chạy tiếp sang phải, và phần vượt quá mép trang thì không nhìn thấy được. Viết lại một đoạn hoặc thêm hàng vào một bảng cần dàn trang lại, nghĩa là quay về tệp nguồn mà PDF được tạo ra từ đó.

Độ rộng cũng chỉ là độ rộng tiến của riêng font (quãng con trỏ viết dịch sang phải sau mỗi glyph), không có kerning và không có thay thế glyph. Văn bản không cần shaping (xác định dạng và vị trí glyph theo ngữ cảnh: Latin, Cyrillic, Hy Lạp và chữ Hán) được dàn đúng. Dấu thanh tiếng Thái cần định vị glyph để nằm đúng chỗ, điều mà trình chỉnh sửa không làm, nên có thể bị lệch. Tiếng Ả Rập cần dạng glyph phụ thuộc ngữ cảnh và dàn trang phải sang trái, tiếng Do Thái cần dàn trang phải sang trái, còn các hệ chữ Ấn Độ cần ghép phụ âm và dấu nguyên âm thành glyph; tất cả đều cần shaping văn bản đầy đủ, điều mà trình chỉnh sửa hiện chưa làm, nên việc gõ chúng bị từ chối thay vì tạo ra ký tự sai.

Hai giới hạn nữa đến từ chính tệp. Nội dung của một trang quét là một ảnh duy nhất, không có toán tử văn bản, nên không có gì để xóa hay đổi. Sau khi một tệp có chữ ký số bị sửa, nội dung gốc mà chữ ký bảo vệ đã thay đổi, và chữ ký không còn hợp lệ.

Tự kiểm tra

Bạn có thể thấy những thay đổi vừa mô tả trong một tệp đã tải về bằng ba lệnh:

pdftotext -raw edited.pdf -      # trích xuất theo thứ tự content stream: chữ cũ đã mất, dòng đã sửa nằm cuối
pdffonts edited.pdf              # danh sách font: các font gốc được dùng lại và subset font mới được nhúng
qpdf --qdf --object-streams=disable edited.pdf edited.qdf   # mở rộng content stream; khối BT ... ET được thêm nằm ở cuối

Đọc tiếp

Open tool
Process in the browser — no watermark, files removed after the job.
Open tool