HotPDF thêm các bookmark vào một file PDF hiện có trong Delphi thông qua AddLoadedOutline, giúp tạo các mục outline ở bất kỳ cấp độ lồng nhau nào trên một tài liệu đã tải, đồng thời nó đọc và ghi các named destination thông qua ResolveLoadedNamedDestination và AddLoadedNamedDestination. Hãy tải file, xây dựng cây mục lục, gọi SaveLoadedDocument, và thanh điều hướng bên của trình đọc vốn trước đây trống trơn nay đã chứa một mục lục hoạt động hoàn chỉnh
Kịch bản thúc đẩy tất cả những điều này diễn ra rất phổ biến. Bạn gộp một tá hợp đồng vào một gói xem xét, hoặc ghép ba tài liệu hướng dẫn sản phẩm thành một file phân phối duy nhất, và kết quả đầu ra rất tốt về mặt cấu trúc: mọi trang đều có mặt, mọi font chữ đều nguyên vẹn. Nhưng khi ai đó mở nó trong Acrobat, bảng điều hướng bookmark lại trống trơn. Một tài liệu dài 400 trang không có điều hướng tuy hoàn chỉnh về mặt kỹ thuật nhưng thực tế lại không thể sử dụng được. Và cho đến phiên bản 2.347.0, HotPDF có thể đọc, sửa đổi và xóa bookmark trên các tài liệu đã tải nhưng không bao giờ có thể tạo mới chúng. Khoảng trống đó hiện đã được lấp đầy, và bài viết này sẽ hướng dẫn cách sử dụng bề mặt API mới cùng một chi tiết kỹ thuật PDF cũ kỹ — key /Count — thứ sẽ làm khó bạn nếu bạn coi nó như một bộ đếm số lượng mục đơn giản
Cấu trúc thực tế của cây outline
Một outline PDF là một cây liên kết kép gồm các dictionary, không phải một danh sách phẳng, và cấu trúc đó giải thích cho mọi tham số trong API. Tiêu chuẩn ISO 32000-1 §12.3.3 định nghĩa nó: catalog tài liệu trỏ đến một root /Outlines, root trỏ đến phần tử con đầu tiên và cuối cùng của nó thông qua /First và /Last, và các phần tử cùng cấp liên kết với nhau qua /Next và /Prev trong khi mỗi mục trỏ ngược lên qua /Parent. Mỗi mục mang một chuỗi /Title và thông thường là một /Dest chỉ ra nơi nhấp chuột sẽ đưa người đọc tới
Destination có hai dạng, và tiêu chuẩn ISO 32000-1 §12.3.2 phân biệt rõ ràng chúng. Một explicit destination (đích rõ ràng) là một mảng nội tuyến như [page /XYZ x y zoom]: một tham chiếu trực tiếp đến một page object cộng với một thông số hiển thị. Ngược lại, một named destination (đích được đặt tên) lưu trữ một chuỗi tên, và mục tiêu thực tế nằm trong catalog tài liệu dưới cây tên /Names /Dests, một mức gián tiếp (indirection) cho phép nhiều liên kết chia sẻ cùng một mục tiêu và giúp mục tiêu di chuyển mà không cần chỉnh sửa các liên kết. HotPDF ghi các explicit destination /XYZ khi tạo bookmark, và cung cấp các lệnh gọi riêng biệt để đọc và mở rộng cây tên
Làm thế nào để thêm bookmark vào một file PDF hiện có trong Delphi?
Gọi AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) sau LoadFromFile: truyền ParentIndex = -1 cho một mục cấp cao nhất, hoặc chỉ mục bắt đầu từ 0 của một mục cấp cao nhất hiện có để lồng xuống dưới nó. Hàm này tạo root /Outlines nếu tài liệu chưa từng có, xây dựng dictionary cho mục đó, ghi mảng /Dest [page /XYZ x y zoom] trỏ vào DestPageIndex bắt đầu từ 0, và liên kết mục mới vào chuỗi các phần tử cùng cấp. Nó trả về 0 nếu thành công và -1 nếu thất bại, ví dụ khi DestPageIndex nằm ngoài phạm vi
Một chi tiết liên kết quan trọng đối với thứ tự: một mục mới được chèn vào đầu chuỗi con của cha nó, dưới dạng một /First mới. Vì vậy, nếu bạn thêm "Chapter 1" rồi đến "Chapter 2" dưới dạng các mục cấp cao nhất, Chapter 2 xuất hiện phía trên Chapter 1 ở thanh bên, và Chapter 1 sẽ chuyển sang chỉ mục cấp cao nhất là 1. Mô hình thực tế là thêm các mục theo thứ tự đọc ngược lại, hoặc thêm từng mục cha và lập tức điền vào các mục con của nó khi nó vẫn nằm ở chỉ mục 0
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
begin
// Add chapters in reverse reading order: each new
// top-level entry becomes /First (index 0).
Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
// Chapter 1 is now top-level index 0; nest sections
// under it (children also prepend, so reverse order).
Pdf.AddLoadedOutline(0, 12, 'License activation', 0, 792, 0);
Pdf.AddLoadedOutline(0, 3, 'System requirements', 0, 792, 0);
Pdf.SaveLoadedDocument('merged-manual-toc.pdf');
end;
finally
Pdf.Free;
end;
end;
Các tham số X, Y, và Zoom ánh xạ trực tiếp lên đích /XYZ và tất cả đều mặc định là 0. Định dạng PDF đặt gốc tọa độ ở góc dưới bên trái, do đó Y = 792 sẽ hướng tới đầu trang US Letter, và theo tiêu chuẩn ISO 32000-1, một giá trị 0 trong một slot /XYZ sẽ yêu cầu trình xem giữ nguyên cài đặt hiện tại của nó, đây chính xác là những gì bạn muốn cho mức thu phóng trong hầu hết các trường hợp. Mọi thứ diễn ra trên đồ thị đối tượng đã tải trong bộ nhớ; không có gì ghi xuống đĩa cho đến khi SaveLoadedDocument chạy, cùng một mô hình chỉnh sửa-sau-đó-lưu mà API tài liệu đã tải sử dụng cho việc ghi lại tiêu đề, tác giả và siêu dữ liệu XMP trên các PDF đã tải
Định hướng lại bookmark: nhảy trang so với hành động URI
Các bookmark hiện có có thể được liên kết lại mà không cần tạo lại, và HotPDF cung cấp cho hai trường hợp này hai lệnh gọi riêng biệt vì các cấu trúc PDF bên dưới thực sự khác nhau. SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) thay thế /Dest của mục cấp cao nhất tại Index bằng một mảng /XYZ mới, công cụ để khắc phục lỗi gộp trang phổ biến khi mọi bookmark vẫn trỏ vào số trang của file gốc trước khi gộp. SetLoadedOutlineURI(Index, URI) thực hiện một cấu trúc khác: nó đính kèm một dictionary hành động /A với /S /URI, biến bookmark thành một liên kết web thay vì một lệnh nhảy bên trong tài liệu
// The merge shifted everything by 10 pages: repoint the
// third top-level bookmark at page 12 (zero-based 11).
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);
// Turn the last entry into an external link.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');
Pdf.SaveLoadedDocument('retargeted.pdf');
Hãy phân biệt rõ hai cơ chế này khi bạn thiết kế cây. Một /Dest là điều hướng thuần túy bên trong tài liệu; một hành động URI rời khỏi tài liệu hoàn toàn, và tiêu chuẩn ISO 32000-1 yêu cầu một mục outline điều hướng qua cơ chế này hoặc cơ chế kia, chứ không phải cả hai. Vì vậy, hãy dành SetLoadedOutlineURI cho các mục chưa mang đích trang, chẳng hạn như mục "Trang chủ sản phẩm" hoặc "Báo cáo sự cố" ở cuối cây thay vì một tiêu đề chương được chuyển đổi. Hành động URI được ghi ở đây là cùng một cấu trúc /S /URI được sử dụng cho các chú thích liên kết (link annotation) trên nội dung trang, được trình bày chi tiết hơn trong hướng dẫn tạo siêu liên kết trong tài liệu PDF với HotPDF
Named destination hoạt động như thế nào trên một PDF đã tải?
Named destination (đích được đặt tên) là cơ chế neo ổn định của tiêu chuẩn ISO 32000-1 §12.3.2.3: thay vì nhúng một tham chiếu trang vào mỗi liên kết, tài liệu giữ một bản đồ ánh xạ từ tên sang đích trong cây tên /Names /Dests của catalog, và các liên kết tham chiếu đến các mục theo tên. ResolveLoadedNamedDestination(Name) tìm kiếm một tên trong cây đó và trả về chỉ mục trang bắt đầu từ 0 mà nó trỏ tới, hoặc -1 nếu tên đó không tồn tại. Bộ phân giải xử lý cả hai dạng lưu trữ mà đặc tả cho phép đối với giá trị được ánh xạ: một mảng đích trần như [page /XYZ x y zoom] và dạng dictionary đóng gói mảng dưới một key /D
AddLoadedNamedDestination(Name, DestPageIndex, X, Y) thực hiện theo hướng ngược lại: nó đăng ký một neo mới trong cây /Names /Dests, tự động tạo cây nếu tài liệu chưa có, và trả về True nếu thành công. Cả hai tạo thành một cặp giải quyết quy trình làm việc nơi tài liệu gộp phải tuân theo các neo mà các công cụ trước đó đã ghi, và xuất bản các neo mới cho các liên kết tiếp theo hướng tới
var
PageIdx: Integer;
begin
// Honor an anchor another tool wrote into /Names /Dests.
PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
if PageIdx >= 0 then
Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);
// Publish a new anchor for other links to target.
if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
Pdf.SaveLoadedDocument('anchored.pdf');
end;
Các giới hạn thực tế: các đích mà HotPDF ghi, cho cả bookmark và neo được đặt tên, đều là các đích /XYZ. Các kiểu rõ ràng khác mà đặc tả định nghĩa như /Fit, /FitH, /FitB và các kiểu tương tự không được phát ra bởi các lệnh gọi này, mặc dù /XYZ với zoom bằng 0 đã bao quát được trường hợp sử dụng chính là "đi đến vị trí này, giữ nguyên zoom". Và GetLoadedBookmarkPageIndex(Title), lệnh tìm kiếm tiện lợi nhận tiêu đề bookmark và trả về trang đích của nó, quét cấp cao nhất của cây outline, vì vậy hãy sử dụng nó để xác minh các mục cấp chương bạn vừa tạo hơn là các mục con lồng sâu bên trong
Bẫy /Count: nó đếm các phần tử con cháu, không phải các mục
Key /Count trên outline root không có nghĩa như hầu hết các nhà phát triển vẫn lầm tưởng, và giả định sai lầm này tạo ra các lỗi kiểm thử trông giống như lỗi hỏng dữ liệu. Tiêu chuẩn ISO 32000-1 §12.3.3 định nghĩa /Count là tổng số phần tử con cháu hiển thị được ở mọi cấp độ, không phải số lượng mục cấp cao nhất. Trên mỗi mục riêng lẻ, dấu của giá trị cũng mang ý nghĩa: một giá trị dương có nghĩa là mục đó đang mở và số lượng con cháu đó hiển thị trên thanh bên, trong khi một giá trị âm có nghĩa là mục đó bị thu gọn và |N| con cháu bị ẩn đi bên trong nó
Điều này được phát hiện từ chính bộ test hồi quy của HotPDF trong quá trình xây dựng các API bookmark cho tài liệu đã tải. Một tài liệu test chứa ba bookmark cấp cao nhất, một trong số đó có con; sau khi xóa một mục cấp cao nhất, GetLoadedOutlineCount, hàm đọc root /Count, báo cáo kết quả là 1 thay vì 2 như kỳ vọng. Không có gì bị hỏng cả: số 3 ban đầu chưa từng là "ba mục cấp cao nhất", và việc tính toán lại chính xác phải duyệt qua chuỗi cấp cao nhất tích lũy từng node cộng với giá trị /Count dương của nó, bỏ qua các con cháu của các node bị thu gọn. Bài học cho code của chính bạn là: không bao giờ assert trên các delta của /Count sau khi chỉnh sửa cấu trúc, vì giá trị di chuyển phi tuyến tính theo số lượng con cháu hiển thị. Thay vào đó hãy xác minh cấu trúc bằng nội dung
// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Vị trí của tính năng này trong bộ công cụ tài liệu đã tải
Việc tạo bookmark và named destination hoàn thiện bức tranh mà API tài liệu đã tải đang xây dựng: cùng một chu trình tải-sửa-lưu đã ghi lại siêu dữ liệu tài liệu, thêm các chú thích liên kết, và chuyển đổi qua lại các chú thích thông qua việc nhập và xuất XFDF. Mô hình này là đồng nhất, LoadFromFile, một chuỗi các lệnh gọi *Loaded* đối với đồ thị đối tượng trong bộ nhớ, sau đó là một lệnh SaveLoadedDocument, giúp dễ dàng tích hợp việc xây dựng outline vào một quy trình gộp hoặc đóng dấu sẵn có như một bước trước khi lưu
Các API được thảo luận ở đây, AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination, và GetLoadedBookmarkPageIndex, đi kèm trong HotPDF Component tiêu chuẩn dành cho Delphi và C++Builder, không có sự phụ thuộc bên ngoài và không yêu cầu runtime trình xem riêng biệt