HotPDF Component có thể tìm kiếm và thay thế văn bản bên trong một tệp PDF hiện có từ Delphi và C++Builder. Các hàm SearchLoadedPageText và SearchLoadedDocumentText giúp xác định vị trí của mọi chuỗi xuất hiện với độ chính xác ở cấp độ ký tự đồ họa, và ReplaceLoadedPageText cùng ReplaceLoadedDocumentText sẽ ghi đè các byte khớp trực tiếp tại chỗ — miễn là mỗi ký tự thay thế đều có thể được mã hóa ngược thông qua phông chữ gốc, một ràng buộc vật lý mà bài viết này sẽ trình bày một cách trung thực thay vì che giấu ở phần chú thích cuối trang
Yêu cầu đằng sau tính năng này luôn rất thiết thực trong công việc hàng ngày. Một công ty thay đổi tên và ba nghìn hóa đơn lưu trữ vẫn mang tên cũ. Một mẫu hợp đồng được gửi đi mang ngày hết hạn của năm ngoái. Một mã sản phẩm đã ngừng hoạt động và mọi tài liệu kỹ thuật đề cập đến nó đều cần được thay thế bằng mã mới. Trong một trình soạn thảo văn bản, đây chỉ là công việc trong ba mươi giây. Nhưng trong một tệp PDF, đây thực sự là một bài toán khó, và hiểu rõ lý do tại sao là điểm mấu chốt để sử dụng API hiệu quả thay vì gửi một báo cáo lỗi vốn thực chất chỉ là một quy định của đặc tả kỹ thuật
Tại sao thay thế văn bản trong tệp PDF lại khó đến vậy?
Thay thế văn bản trong tệp PDF rất khó vì một trang PDF không chứa văn bản có thể chỉnh sửa trực tiếp — nó chứa các ký tự đồ họa đã được định vị. Theo mô hình hiển thị văn bản của ISO 32000-1 §9.4, một luồng nội dung điều khiển các toán tử như Tj và TJ để vẽ chuỗi mã ký tự tại các tọa độ được thiết lập bởi ma trận văn bản. Những mã đó không phải là Unicode; chúng là các chỉ số trong bất kỳ bảng mã nào mà phông chữ của trang khai báo, và việc ánh xạ ngược lại các ký tự có thể đọc được có thể nằm trong bảng /ToUnicode CMap, mảng khác biệt bảng mã (encoding difference array), hoặc chuỗi ánh xạ CID. Không có đối tượng đoạn văn, không có dòng chảy văn bản và không có gì đảm bảo rằng một từ hiển thị trực quan được lưu trữ như một chuỗi liền mạch
Việc thay thế bổ sung thêm một tầng khó khăn thứ hai bên cạnh việc giải mã: bạn phải biết chính xác byte nào của luồng gốc đã tạo ra mỗi ký tự đồ họa, để có thể chèn các byte mới vào đúng khoảng đó mà không ảnh hưởng đến phần khác. Một công cụ trích xuất văn bản có thể bỏ qua các vị trí byte sau khi lấy được Unicode. Nhưng công cụ thay thế thì không thể làm vậy. Đó là lý do tại sao HotPDF chia công việc này ra làm hai phiên bản phát hành — v2.251.0 xây dựng lớp theo dõi khoảng lệch (offset-tracking) và tìm kiếm, và v2.252.0 xây dựng lớp ghi đè lên trên nền tảng đó
Tìm kiếm văn bản: tìm kiếm cấp ký tự đồ họa với việc theo dõi khoảng lệch byte
Hàm SearchLoadedDocumentText của HotPDF tìm kiếm mọi chuỗi cần tìm bằng cách đối chiếu với chuỗi ký tự đồ họa Unicode đã được giải mã của từng trang, chứ không phải với các byte luồng thô, do đó kết quả khớp là hoàn toàn chính xác bất kể phông chữ mã hóa nó như thế nào. Cơ sở hạ tầng bên dưới được giới thiệu trong phiên bản v2.251.0: bộ phân tách luồng nội dung (content-stream tokenizer) ghi lại khoảng byte StartOfs/EndOfs cho mọi toán hạng chuỗi — bao gồm cả các dấu giới hạn của nó như ( ) hoặc < > — và mọi ký tự đồ họa đã giải mã đều mang một bộ ba thông tin TokenIndex/ItemIndex/ByteOffset trỏ ngược về chính xác toán hạng, phần tử mảng TJ và đơn vị mã đã tạo ra nó. Cùng một trình giải nghĩa ký tự đồ họa này hỗ trợ API trích xuất được mô tả trong bài viết về trích xuất văn bản từ tệp PDF đã tải trong Delphi; tính năng tìm kiếm chỉ đơn giản giữ lại nguồn gốc byte mà tính năng trích xuất đã bỏ qua
Mỗi kết quả khớp trả về dưới dạng bản ghi THPDFTextMatch mang chỉ số trang, phạm vi ký tự đồ họa, tọa độ gốc X/Y và chiều rộng trong không gian người dùng của điểm khớp, chỉ số phần tử và token nguồn, và chính văn bản khớp đó. Thông tin này là đủ để tạo các lớp tô sáng, giao diện xem lại hoặc thực hiện bước thay thế. Việc tìm kiếm không thấy kết quả sẽ trả về một mảng rỗng thay vì báo lỗi, giúp giữ cho mô hình gọi hàm luôn đơn giản
var
Pdf: THotPDF;
Matches: THPDFTextMatchArray;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoices-2025.pdf') > 0 then
begin
if Pdf.SearchLoadedDocumentText('Acme Corp', False, Matches) then
for I := 0 to Length(Matches) - 1 do
WriteLn(Format('page %d at (%.1f, %.1f): "%s"',
[Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
Matches[I].Text]));
end;
finally
Pdf.Free;
end;
end;
Có một quyết định thiết kế có chủ ý cần lưu ý. Khi tham số CaseSensitive là False, phép so sánh chỉ chuyển đổi chữ hoa chữ thường cho các ký tự ASCII: việc chuyển đổi chữ hoa chữ thường Unicode đầy đủ hoạt động khác nhau giữa các chuỗi công cụ từ Delphi 5 đến XE mà HotPDF hỗ trợ, và một API tìm kiếm trả về các kết quả khác nhau tùy thuộc vào trình biên dịch nào xây dựng ứng dụng của bạn sẽ tệ hơn một API có giới hạn rõ ràng và có thể dự đoán được. Đối với văn bản kinh doanh hệ chữ Latinh — tên, mã số, ngày tháng — việc chuyển đổi chữ hoa chữ thường cho ASCII đã đáp ứng hầu hết các trường hợp thực tế
Thay thế văn bản: mã hóa ngược và chèn ghép chính xác
Hàm ReplaceLoadedDocumentText, được thêm trong HotPDF v2.252.0, ghi lại mọi sự xuất hiện của từ khóa cần tìm bằng cách chạy ngược lại cơ chế giải mã. Hàm HPDFEncodeUnicode là nghịch đảo của bộ giải mã mã ký tự: nó duyệt ngược lại cùng một chuỗi chiến lược — tra cứu bfchar và bfrange của /ToUnicode, ánh xạ CID luồng mã hóa, ánh xạ identity Type0, và các bảng WinAnsi và MacRoman được định nghĩa trước — để chuyển đổi mỗi ký tự thay thế trở lại thành các byte mã ký tự mà phông chữ gốc mong đợi. Các byte được mã hóa ngược sau đó được tuần tự hóa thành một chuỗi literal hoặc chuỗi hex chuẩn, phản ánh các quy tắc thoát ký tự (escaping rules) của chính bộ phân tách từ để đảm bảo chu trình phân tích → tuần tự hóa lại luôn ổn định
Bản thân việc chèn ghép được thực hiện chính xác thay vì thay thế toàn bộ. Chỉ phạm vi byte mã nằm trong điểm khớp được thay thế bên trong toán hạng chuỗi; các byte không khớp trong cùng toán hạng đó, khoảng trắng giữa các token và mọi toán tử xung quanh đều được bảo toàn nguyên vẹn, từng byte một. Việc thay thế bca bên trong chuỗi abcabc sẽ tạo ra a + chuỗi thay thế + bc, chứ không phải làm hỏng toàn bộ toán hạng. Chuỗi thay thế có thể ngắn hơn hoặc dài hơn chuỗi gốc — chuỗi literal sẽ được tuần tự hóa lại và mục /Length của luồng được làm mới — và mỗi luồng /Contents của một trang có nhiều luồng được xử lý độc lập để đảm bảo cấu trúc trang luôn chuẩn chỉnh
var
Pdf: THotPDF;
ReplaceCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-draft.pdf') > 0 then
begin
if Pdf.ReplaceLoadedDocumentText('2025-12-31', '2026-12-31',
True, ReplaceCount) then
WriteLn(Format('%d operand rewrites performed', [ReplaceCount]));
Pdf.SaveLoadedDocument('contract-final.pdf');
end;
finally
Pdf.Free;
end;
end;
Lưu ý những điều mà API không thực hiện: nó không sắp chữ lại trang. PDF không có dòng chảy văn bản tự động (reflow), vì vậy một chuỗi thay thế rộng hơn về mặt trực quan so với chuỗi gốc sẽ chỉ đơn giản là chiếm nhiều không gian ngang hơn và có thể đè lên bất kỳ thứ gì được vẽ ở bên phải của nó. Các thay thế có độ dài tương đương hoặc gần tương đương — ngày tháng, chuỗi phiên bản, mã số bộ phận, sửa lỗi tên — là các trường hợp sử dụng phù hợp nhất. Việc thay đổi toàn bộ câu chữ nên được thực hiện trên tài liệu nguồn, chứ không phải trong tệp PDF
Tại sao bạn không thể thay thế văn bản bằng các ký tự mà tập hợp phông chữ rút gọn chưa từng bao gồm?
Bạn không thể thay thế văn bản bằng một ký tự mà phông chữ rút gọn được nhúng chưa từng chứa, bởi vì chuỗi byte để chọn ký tự đó chỉ đơn giản là không tồn tại trong các bảng ánh xạ của phông chữ. Khi một trình tạo PDF nhúng một phông chữ rút gọn, cấu trúc mã hóa và /ToUnicode CMap của nó chỉ bao gồm các ký tự đồ họa mà tài liệu gốc thực sự sử dụng. Hàm HPDFEncodeUnicode chỉ có thể đảo ngược một ánh xạ có sẵn: nếu tài liệu chưa từng chứa chữ cái E sử dụng phông chữ đó, sẽ không có mã ký tự nào cho chữ E để ánh xạ ngược lại. Đây là thuộc tính vật lý của tệp tin, chứ không phải là giới hạn của riêng thư viện nào — không công cụ nào có thể tạo ra một ánh xạ ký tự chưa từng được nhúng
HotPDF xử lý lỗi này một cách thận trọng. Nếu có bất kỳ ký tự đơn lẻ nào của chuỗi thay thế không thể được mã hóa ngược, toàn bộ vị trí khớp đó sẽ bị bỏ qua — không phát sinh ngoại lệ, không ghi văn bản rác một phần, và vị trí đó chỉ đơn giản là không được tính vào ReplaceCount. Hệ quả thực tế: hãy đối chiếu ReplaceCount với số lượng khớp từ kết quả tìm kiếm trước đó, và coi phần chênh lệch là một dấu hiệu cảnh báo. Trong ví dụ ngày tháng ở trên, chữ số 6 phải xuất hiện ở một vị trí nào đó trong văn bản của tài liệu sử dụng chính phông chữ đó để thao tác ghi đè thành công — điều này dễ có trong một hóa đơn, nhưng không bao giờ được đảm bảo nói chung. Khi các ký tự bạn cần đơn giản là không khả dụng và mục tiêu là loại bỏ thông tin nhạy cảm thay vì thay đổi câu chữ, việc xóa bỏ nội dung thực tế mới là công cụ tốt hơn; xem bài viết về xóa bỏ thông tin nhạy cảm và tái cấu trúc tệp PDF đã tải trong Delphi để tìm hiểu quy trình đó
var
Matches: THPDFTextMatchArray;
Expected, Replaced: Integer;
begin
Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
Expected := Length(Matches);
Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
if Replaced < Expected then
WriteLn(Format('%d occurrence(s) skipped: characters missing ' +
'from the font subset, or match spans multiple operands',
[Expected - Replaced]));
end;
Điều kiện bỏ qua thứ hai trong thông điệp đó là một giới hạn khác đã được ghi nhận: một từ khóa cần tìm nằm trải dài trên nhiều toán hạng chuỗi — ví dụ chữ Hello bị tách thành các phần tử [(He)(llo)] TJ — sẽ được tìm thấy bởi tính năng tìm kiếm (vì tìm kiếm đối chiếu trên chuỗi ký tự đồ họa đã giải mã), nhưng sẽ bị bỏ qua bởi tính năng thay thế (vì việc ghi lại trên các ranh giới toán hạng đòi hỏi phải ghép các phân đoạn byte liền kề). Quy trình tìm kiếm-sau đó-xác minh sẽ giúp hiển thị rõ cả hai giới hạn này thay vì bỏ qua trong âm thầm
Những thay đổi nào diễn ra trong tệp tin khi bạn lưu?
Một luồng /Contents được thay thế sẽ được lưu ở dạng không nén. Các luồng được nén bằng FlateDecode sẽ được giải nén để chỉnh sửa, và khi HotPDF ghi lại các byte đã dựng, nó sẽ loại bỏ mục nhập /Filter của luồng và làm mới mục /Length thay vì thực hiện nén lại. Tệp PDF kết quả hoàn toàn hợp lệ và hiển thị bình thường trong các trình xem phổ biến; sự đánh đổi là kích thước tệp lớn hơn cho mỗi luồng được chỉnh sửa. Đối với một đường ống xử lý hàng loạt hàng ngàn tài liệu, hãy chuẩn bị cho sự gia tăng kích thước này hoặc chạy một lượt nén riêng biệt ở hạ nguồn. Cách các đối tượng được ghi lại tương tác với cấu trúc tham chiếu chéo của tài liệu khi lưu là một chủ đề riêng biệt, được trình bày trong bài viết về các luồng đối tượng và cập nhật lũy tiến trong HotPDF
Mọi thành phần khác của tệp tin đều được giữ nguyên. Các luồng không bị tác động vẫn giữ nguyên trạng thái nén của chúng, các phông chữ và hình ảnh không được ghi lại, và việc ghép nối cấp toán hạng có nghĩa là ngay cả các luồng được chỉnh sửa cũng chỉ khác biệt so với tệp gốc tại đúng điểm khớp. Sự thận trọng này là có chủ ý: thư viện càng viết lại nhiều phần của tài liệu đã tải, càng có nhiều rủi ro làm hỏng các thiết lập đặc thù của trình tạo PDF gốc mà thư viện không lường trước được
Tính năng tìm kiếm và thay thế văn bản kết hợp cùng trích xuất, loại bỏ thông tin nhạy cảm và kết xuất trang trong bộ công cụ xử lý tài liệu đã tải của HotPDF, tất cả đều được vận hành bởi cùng một trình thông dịch luồng nội dung và khả dụng từ phiên bản Delphi 5 đến các phiên bản RAD Studio hiện tại mà không phụ thuộc vào thành phần bên ngoài. Tài liệu tham khảo API đầy đủ và bản tải về dùng thử có sẵn trên trang sản phẩm HotPDF Component