Bạn có mười nghìn tệp PDF hợp đồng từ hàng chục nguồn tạo khác nhau, và bộ phận pháp lý muốn mỗi tệp trong số đó phải mang thông tin Author chính xác, chuỗi Producer được sửa đổi và chế độ đọc mở bảng dấu trang khi khởi chạy. Cách khắc phục đơn giản nhất là nạp từng tệp, định dạng lại các trang và ghi ra một tài liệu mới. Làm như vậy có nghĩa là bạn vừa loại bỏ mọi số đối tượng hiện có, lịch sử cập nhật gia tăng (incremental-update), bất kỳ chữ ký số nào và bảng tham chiếu chéo (xref) được tinh chỉnh cẩn thận mà công cụ ban đầu đã tạo ra. Các trang trông có vẻ giống hệt nhau nhưng về mặt cấu trúc, tệp đó đã hoàn toàn thay đổi. Đối với việc chỉnh sửa siêu dữ liệu, đây hoàn toàn là một sự đánh đổi sai lầm
Giải pháp đúng đắn là xử lý tài liệu đã nạp như một đồ thị đối tượng mà bạn đột biến tại chỗ: can thiệp vào từ điển Info, luồng /Metadata và Catalog, thay đổi một vài mục mà bạn quan tâm rồi ghi lại kết quả. HotPDF, thành phần PDF VCL gốc dành cho Delphi và C++Builder, cung cấp chính xác bề mặt đó thông qua API ghi tài liệu đã nạp của nó. Bài viết này hướng dẫn cách sử dụng API đó một cách chính xác, đồng thời chỉ ra một sai lầm mà hầu như ai cũng mắc phải: chỉnh sửa từ điển Info nhưng lại quên rằng một bản sao thứ hai của cùng siêu dữ liệu đó cũng nằm trong XMP
Hai nơi lưu cùng một siêu dữ liệu nhưng lại không khớp
PDF lưu trữ thông tin tài liệu ở hai vị trí song song, và đây là nguyên nhân gốc rễ của hầu hết các phản hồi lỗi kiểu "Tôi đã thay đổi tiêu đề nhưng Acrobat vẫn hiển thị tiêu đề cũ". Vị trí đầu tiên là từ điển thông tin tài liệu, đối tượng /Info cổ điển với các khóa /Title, /Author, /Subject, /Keywords, /Creator và /Producer, được định nghĩa trong ISO 32000-1 §14.3.3. Vị trí thứ hai là một gói XMP, một tài liệu XML được lưu trữ dưới dạng một luồng (stream) gắn liền với Catalog dưới khóa /Metadata, được định nghĩa trong §14.3.2 và được xây dựng trên mô hình dữ liệu Adobe XMP
Cả hai nơi đều có thể chứa tiêu đề. Không có điều khoản nào trong đặc tả bắt buộc chúng phải thống nhất với nhau. Các trình xem hiện đại và hầu hết các trình xác thực PDF/A đều ưu tiên gói XMP khi nó tồn tại và chỉ quay lại sử dụng từ điển Info khi không có XMP. Vì vậy, nếu bạn chỉ cập nhật đối tượng /Info — điều mà phần lớn các đoạn mã "thiết lập siêu dữ liệu PDF" thực hiện — một trình đọc tin cậy XMP sẽ tiếp tục hiển thị giá trị cũ, và trình kiểm tra PDF/A sẽ gắn cờ cảnh báo sự không khớp này. Thao tác chính xác trên bất kỳ tệp nào đã có gói XMP là ghi kép (double write): thay đổi mục Info và tạo lại XMP để cả hai luôn nhất quán. HotPDF cung cấp cho bạn cả hai phần; việc phối hợp sử dụng chúng một cách kỷ luật là tùy thuộc vào bạn
Chỉnh sửa từ điển Info
Các hàm hỗ trợ phía Info hoạt động rất đơn giản và dễ đoán. Các hàm SetLoadedTitle, SetLoadedAuthor, SetLoadedSubject, SetLoadedKeywords, SetLoadedCreator và SetLoadedProducer mỗi hàm nhận một tham số kiểu AnsiString duy nhất và ghi khóa tương ứng vào từ điển Info đã nạp, thay thế giá trị nếu khóa đã tồn tại hoặc thêm mới nếu khóa chưa có. Để loại bỏ hoàn toàn một khóa — ví dụ như khóa /Creator làm lộ tên công cụ nội bộ của bạn — hãy gọi hàm RemoveLoadedInfoKey với tên khóa trần. Không có hàm nào trong số này tác động đến XMP; chúng chỉ hoạt động thuần túy trên đối tượng /Info mà hàm LoadFromFile đã xác định được khi phân tích cú pháp tệp
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-in.pdf', '') > 0 then
begin
Pdf.SetLoadedTitle('Master Services Agreement 2026');
Pdf.SetLoadedAuthor('Legal Department');
Pdf.SetLoadedSubject('Executed contract, retention 7 years');
Pdf.SetLoadedKeywords('contract; MSA; 2026; executed');
Pdf.SetLoadedProducer('Acme Document Pipeline');
Pdf.RemoveLoadedInfoKey('Creator'); // drop the originating tool name
Pdf.SaveLoadedDocument('contract-out.pdf');
end;
finally
Pdf.Free;
end;
end;
Một chi tiết cần lưu ý: các hàm này nhận kiểu dữ liệu AnsiString. Đối với các tiêu đề ASCII thì đây không phải là vấn đề, nhưng các chuỗi văn bản PDF cần ký tự không phải Latinh phải được mã hóa theo yêu cầu của đặc tả — UTF-16BE kèm theo ký tự đánh dấu thứ tự byte (BOM), hoặc mã hóa PDFDocEncoding — trước khi bạn truyền chúng vào. Thư viện ghi trực tiếp các byte bạn cung cấp vào đối tượng chuỗi; nó không tự động đoán bảng mã cho bạn. Nếu tiêu đề của bạn chỉ là tiếng Anh thuần túy, bạn có thể bỏ qua điều này. Nếu chúng chứa ký tự có dấu hoặc ký tự CJK, hãy mã hóa một cách chủ động và kiểm tra trên một trình xem thực tế
Ghi lại gói XMP
SetLoadedXMPMetadata là nửa còn lại của thao tác ghi kép. Truyền toàn bộ gói XMP cho hàm dưới dạng AnsiString và nó sẽ thực hiện một trong hai việc: nếu Catalog đã tham chiếu đến một luồng /Metadata, nó sẽ thay thế nội dung luồng đó tại chỗ mà vẫn giữ nguyên số đối tượng; nếu chưa có luồng siêu dữ liệu, nó sẽ tạo mới một luồng, đánh dấu nó là /Type /Metadata và /Subtype /XML, cấp phát một số đối tượng và liên kết nó từ Catalog. Dù bằng cách nào, bạn cũng sẽ có một đối tượng siêu dữ liệu hợp lệ mà các trình xem có thể đọc được
Bạn cung cấp dữ liệu XML, điều này có nghĩa là bạn kiểm soát lược đồ — dc:title, dc:creator, xmp:CreatorTool, vân vân. Đó vừa là quyền hạn vừa là trách nhiệm: thư viện không phân tích cú pháp hoặc xác thực gói của bạn, và nó ghi các byte ở dạng không nén, không áp dụng bất kỳ bộ lọc luồng nào. Một gói XML bị lỗi định dạng sẽ vẫn được thực hiện qua lệnh gọi này và sau đó sẽ xuất hiện dưới dạng lỗi siêu dữ liệu bị hỏng. Hãy xây dựng XML một cách cẩn thiện và phản ánh chính xác các giá trị bạn đã ghi vào từ điển Info để hai chế độ xem không bao giờ mâu thuẫn với nhau
const
XMP_TEMPLATE =
'<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>' +
'<x:xmpmeta xmlns:x="adobe:ns:meta/">' +
'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">' +
'<rdf:Description rdf:about="" xmlns:dc="http://purl.org/dc/elements/1.1/">' +
'<dc:title><rdf:Alt><rdf:li xml:lang="x-default">%s</rdf:li></rdf:Alt></dc:title>' +
'<dc:creator><rdf:Seq><rdf:li>%s</rdf:li></rdf:Seq></dc:creator>' +
'</rdf:Description></rdf:RDF></x:xmpmeta><?xpacket end="w"?>';
begin
// After setting the Info dictionary, mirror the same values into XMP:
Pdf.SetLoadedTitle('Master Services Agreement 2026');
Pdf.SetLoadedAuthor('Legal Department');
Pdf.SetLoadedXMPMetadata(
AnsiString(Format(XMP_TEMPLATE,
['Master Services Agreement 2026', 'Legal Department'])));
Pdf.SaveLoadedDocument('contract-out.pdf');
end;
Trình tự đó — Info trước, XMP sau, rồi lưu — là quy trình cần ghi nhớ. Hai lệnh gọi này hoàn toàn độc lập; sự nhất quán chỉ tồn tại vì bạn đã truyền cho chúng cùng một chuỗi dữ liệu. Nếu bạn bỏ qua lệnh gọi XMP trên một tệp đã có gói XMP, bạn sẽ gặp lại lỗi siêu dữ liệu cũ hiển thị âm thầm mà toàn bộ phần này đang cố gắng ngăn chặn

Điều khiển cách trình xem mở tệp
Ba mục nhập Catalog quyết định những gì người đọc nhìn thấy ngay khi tài liệu được mở, và cả ba đều là các chỉnh sửa chỉ bằng một dòng lệnh trên đồ thị đã nạp. Hàm SetLoadedPageMode ghi khóa /PageMode dưới dạng một đối tượng tên (name object): truyền giá trị 'UseOutlines' để mở bảng dấu trang (bookmark), 'UseThumbs' cho thanh hình thu nhỏ (thumbnail), 'FullScreen' cho chế độ trình chiếu, hoặc 'UseAttachments' để hiển thị khung đính kèm (ISO 32000-1 §7.7.3.1, Bảng 28). Hàm SetLoadedPageLayout ghi khóa /PageLayout theo cách tương tự — 'SinglePage', 'OneColumn', 'TwoColumnLeft', và các giá trị khác. Cả hai đều nhận tên mà không có dấu gạch chéo đi đầu; thư viện sẽ tự động thêm dấu gạch chéo khi xuất dữ liệu
Hàm SetLoadedLanguage ghi mục /Lang của Catalog, thẻ ngôn ngữ tự nhiên cho toàn bộ tài liệu — ví dụ 'en-US', 'de-DE', hoặc một thẻ BCP 47. Lưu ý sự khác biệt về loại dữ liệu thường gây nhầm lẫn: /PageMode và /PageLayout là các đối tượng tên (name) trong PDF, trong khi /Lang là một đối tượng chuỗi (string). HotPDF xử lý chính xác điều này bên trong thư viện, nhưng nếu bạn tự mình kiểm tra đầu ra, bạn sẽ thấy /PageMode /UseOutlines đối lập với /Lang (en-US), và bây giờ bạn đã biết lý do tại sao. Mục nhập /Lang quan trọng hơn vẻ ngoài của nó: đó là thông tin mà công nghệ hỗ trợ đọc để chọn cách phát âm, và là một yêu cầu bắt buộc để tuân thủ khả năng tiếp cận PDF/UA
if Pdf.LoadFromFile('handbook.pdf', '') > 0 then
begin
Pdf.SetLoadedPageMode('UseOutlines'); // /PageMode, a name
Pdf.SetLoadedPageLayout('TwoColumnLeft'); // /PageLayout, a name
Pdf.SetLoadedLanguage('en-US'); // /Lang, a string
Pdf.SaveLoadedDocument('handbook-tagged.pdf');
end;
Đổi tên dấu trang mà không làm xáo trộn cây
Việc đổi tên dấu trang là một hoạt động dọn dẹp thường gặp — sửa một lỗi đánh máy trong tiêu đề, hoặc một chương được đánh lại số sau khi cây phác thảo đã được xây dựng. Hàm SetLoadedOutlineTitle nhận một chỉ số dựa trên 0 (zero-based index) của các mục phác thảo cấp cao nhất và một tiêu đề mới, duyệt qua chuỗi Catalog → /Outlines → /First → /Next đến vị trí đó và thay thế chuỗi /Title của mục nhập. Nó chỉ thay đổi tiêu đề; đích đến, trạng thái mở/đóng và cấu trúc con đều được giữ nguyên
if Pdf.LoadFromFile('report.pdf', '') > 0 then
begin
Pdf.SetLoadedOutlineTitle(0, 'Executive Summary');
Pdf.SetLoadedOutlineTitle(1, 'Financial Results');
Pdf.SaveLoadedDocument('report-renamed.pdf');
end;
Đổi tên là an toàn chính xác vì nó không bao giờ chạm đến các bộ đếm cấu trúc. Xóa một mục phác thảo là trường hợp phức tạp, và điều này rất đáng để hiểu ngay cả khi bạn chỉ đổi tên, bởi vì nó cho bạn biết những gì không nên tự chỉnh sửa thủ công. Mỗi nút phác thảo mang một thuộc tính /Count, và — theo ISO 32000-1 §12.3.3 — số lượng đó không phải là số lượng nút con trực tiếp. Đó là tổng số các nút con cháu hiển thị: một thuộc tính /Count dương bằng N nghĩa là N nút con cháu hiện đang được mở rộng, trong khi một giá trị âm có nghĩa là nút đó có con cháu nhưng đang được thu gọn. Khi một mục cấp cao nhất bị xóa, số lượng gốc của /Outlines không thể chỉ đơn giản là giảm đi một; nó phải được tính toán lại bằng cách cộng tổng, trên mỗi nút cấp cao nhất còn lại, "một cho chính nút đó cộng với thuộc tính /Count dương của nó," bỏ qua các nút con cháu của bất kỳ nút nào đang thu gọn (số đếm âm). Nếu làm sai điều đó, tổng số dấu trang hiển thị trên trình đọc sẽ bị lệch — nó có thể nhảy nhiều hơn một đơn vị cho mỗi lần xóa. Việc đổi tên giúp tránh được tất cả những điều này, đó là một lý do nữa để bạn nên ưu tiên sử dụng hàm hỗ trợ có mục tiêu hơn là tự can thiệp vào từ điển
Cách lưu vẫn giữ nguyên cấu trúc
Mọi chỉnh sửa ở trên chỉ thay đổi các đối tượng trong bộ nhớ; không có gì ghi xuống đĩa cho đến khi SaveLoadedDocument chạy. Lý do phương pháp này có chi phí thấp là vì thao tác lưu không tạo lại tài liệu — nó bảo toàn các số đối tượng hiện có và cấu trúc mà HotPDF đã phân tích khi nạp, ghi lại cùng một đồ thị cùng với một số ít các đối tượng đã thay đổi và mới được cấp phát của bạn. Đó là điều giúp cho việc cập nhật siêu dữ liệu tránh phải ghi lại toàn bộ tệp, và đó cũng là cơ chế cập nhật tại chỗ tương tự giúp cho luồng đối tượng (object stream) và cập nhật gia tăng (incremental update) hoạt động. Nếu các tệp nguồn của bạn được tạo ra từ Word hoặc một bộ ứng dụng văn phòng khác, bố cục đối tượng của chúng có những đặc thù riêng cần biết trước khi bạn chỉnh sửa; bài viết về luồng tham chiếu chéo hỗn hợp trong các tệp PDF Office sẽ trình bày cách cấu trúc các tệp đó và những gì được bảo toàn sau khi ghi
Có hai ranh giới cần tôn trọng. Thứ nhất, đây là mô hình chỉnh sửa tại chỗ, không phải là công cụ che giấu thông tin hay làm sạch tài liệu: việc xóa một khóa Info chỉ loại bỏ khóa đó, chứ không xóa bỏ các giá trị cũ hơn có thể vẫn còn tồn tại trong thế hệ cập nhật gia tăng trước đó của cùng một tệp. Nếu yêu cầu của bạn là loại bỏ hoàn toàn siêu dữ liệu nhạy cảm, đó là một hoạt động khác nặng nề hơn. Thứ hai, việc ghi XMP là nguyên bản — thư viện tin tưởng hoàn toàn vào XML của bạn và không xác thực nó — vì vậy đối với bất kỳ tài liệu nào hướng tới PDF/A hoặc trình xác thực nghiêm ngặt, hãy tạo gói XML từ một mẫu đã được biết là tốt và xác minh kết quả đầu ra. Được sử dụng trong phạm vi đó, chỉnh sửa siêu dữ liệu tại chỗ là một công cụ có kích cỡ phù hợp: nó sửa một vài byte bị sai và giữ nguyên chín mươi chín phần trăm tài liệu vốn đã chính xác như những gì nguồn tạo ban đầu đã ghi
API ghi tài liệu đã nạp được hiển thị ở đây đi kèm với bản tiêu chuẩn của Thành phần HotPDF dành cho Delphi và C++Builder, cùng với tập hợp đầy đủ các phương thức chỉnh sửa siêu dữ liệu, phác thảo dấu trang và Catalog