Bỏ bảy trang khỏi một cuốn cẩm nang 200 trang và mọi dấu trang sẽ rơi vào chỗ sai. Cách khắc phục không phải dựng lại đại cương từ một danh sách tiêu đề phẳng. PDFiumPas cung cấp TPdfOutlineEditor, thứ nạp cây đại cương thật, cho bạn di chuyển và đổi đích các mục, rồi chạy ApplyPageMap để chuyển mọi đích tường minh qua kế hoạch trang của bạn
Vì sao xóa trang lại làm hỏng mọi dấu trang?
Vì mục đại cương không lưu số trang. Nó lưu một tham chiếu tới page object, và khi các page object đổi, tham chiếu hoặc trỏ vào một trang đã dời chỗ hoặc trỏ vào không gì cả. ISO 32000-1 §12.3.2.2 định nghĩa đích tường minh là một mảng mà phần tử đầu là tham chiếu gián tiếp tới page dictionary, theo sau là một fit name như /Fit hay /XYZ. Xóa trang đi, bạn còn lại một tham chiếu đuối; đổi thứ tự trang, tham chiếu vẫn hợp lệ nhưng giờ mô tả một chương khác. PDFiumPas phân giải mảng đó về số trang lúc nạp, nên TPdfOutlineItem.PageNumber cho bạn chỉ số trang bắt đầu từ 1 khớp với API công khai TPdf thay vì số object. Đó là toàn bộ ý nghĩa của lớp trừu tượng này: logic ánh xạ lại của bạn làm việc trong cùng hệ tọa độ với kế hoạch trang bạn đã dựng khi tách, đổi thứ tự hay cấn trang tài liệu. Nếu bạn đang dựng kế hoạch đó, quy ước bắt đầu từ 1 này chạy xuyên suốt tách tài liệu PDF thành nhiều tệp và cấn trang n-up và đổi thứ tự trang
Đại cương là một cây liên kết đôi, không phải một danh sách
Lý do bạn không thể cứ thế serialize một mảng tiêu đề phẳng là ISO 32000-1 §12.3.3 mắc mỗi mục đại cương vào năm liên kết riêng: /Parent, /Prev, /Next, /First và /Last. Dời một subtree duy nhất vì thế phải viết lại parent cũ, parent mới, cả hai sibling láng giềng ở mỗi bên của điểm cắt và điểm chèn, cùng con trỏ parent của chính node được dời. Sai một trong số đó, các reader tuân thủ sẽ hiện cây bị cụt, hoặc lặp vòng. PDFiumPas giữ trạng thái chỉnh sửa như một mảng duyệt depth-first gồm các bản ghi TPdfOutlineItem với Id nguyên ổn định, nên một subtree là một lát liền kề và chuỗi sibling được suy ra chứ không giữ tay. TPdfOutlineEditor.Move nhấc lát đó lên, chèn lại dưới parent mới tại vị trí sibling được yêu cầu, và gán lại chỉ root của khối. Nó cũng từ chối hai kiểu di chuyển sẽ làm hỏng đồ thị: dời một mục vào chính subtree của nó, và khai báo một parent không tồn tại
Vì sao /Count có dấu?
Vì dấu mang trạng thái mở rộng, không phải kích thước. /Count dương nghĩa là mục đang mở và con số là bao nhiêu hậu duệ hiện đang hiển thị; /Count âm nghĩa là mục đã thu gọn. PDFiumPas ghi số hậu duệ cho mọi mục có con và đảo dấu khi IsOpen là False, và lúc nạp đọc trạng thái ngược lại thành IsOpen := HasCount and (CountValue > 0). Đây là bug tự viết tay phổ biến nhất trong các bộ viết đại cương: phát một count không dấu và lặng lẽ ép cả cây mở ra
var
Source, Dest: TMemoryStream;
Editor: TPdfOutlineEditor;
Options: TPdfOutlineEditOptions;
Report: TPdfOutlineValidationReport;
RootId, ChapterId: Integer;
begin
Source := TMemoryStream.Create;
Dest := TMemoryStream.Create;
Editor := nil;
try
Source.LoadFromFile('handbook.pdf');
Options := TPdfOutlineEditOptions.Default; // MaxItems 100000, MaxDepth 64
if not TPdfOutlineEditor.TryLoad(Source, Options, Editor, Report) then
raise Exception.Create(Report.ErrorMessage);
RootId := Editor[0].Id;
ChapterId := Editor[2].Id;
Editor.Move(ChapterId, RootId, 1); // trở thành con thứ hai của root
Editor.SetTitle(ChapterId, 'Appendix B');
Editor.SetStyle(ChapterId, [posBold, posItalic]);
Editor.SetColor(ChapterId, 0.25, 0.5, 0.75);
Editor.SetExpanded(RootId, False); // ghi một /Count âm
Editor.Retarget(ChapterId, 12, '/XYZ 10 20 1');
if not Editor.SaveIncremental(Source, Dest, Report) then
raise Exception.Create(Report.ErrorMessage);
Dest.SaveToFile('handbook-edited.pdf');
finally
Editor.Free;
Dest.Free;
Source.Free;
end;
end;
Retarget xử lý cả hai hình dạng mà đặc tả cho phép. Truyền DestinationInAction là False, PDFiumPas ghi một mảng /Dest trực tiếp; truyền True, nó ghi một action Go-To, /A << /S /GoTo /D [ page ref suffix ] >>, theo ISO 32000-1 §12.6.4.2. Dù kiểu nào, nó cũng xóa trước mọi /Dest và /A tồn tại khỏi mục để hai thứ không thể cùng tồn tại rồi mâu thuẫn. Hậu tố mặc định là /Fit và phải bắt đầu bằng một PDF name, vì vậy hậu tố rỗng hay dị dạng sẽ raise ngay lập tức thay vì tạo ra một mảng đích mà không reader nào parse nổi
ApplyPageMap tiêu thụ một kế hoạch trang thế nào?
ApplyPageMap nhận đúng cái mảng mà kế hoạch trang của bạn đã xác thực: NewPageNumbers, đánh chỉ số theo trang cũ trừ một, giữ số trang mới bắt đầu từ 1 hoặc số 0 khi trang đó không sống sót. Nó duyệt mảng mục ngược từ cuối để việc xóa một subtree không bao giờ làm vô hiệu một chỉ số nó chưa đến, và nó báo cáo những gì đã làm qua RemappedDestinationCount và RemovedDanglingItemCount
var
NewPageNumbers: array of Integer;
Report: TPdfOutlineValidationReport;
I: Integer;
begin
// Một entry cho mỗi trang của tài liệu GỐC
SetLength(NewPageNumbers, OriginalPageCount);
for I := 0 to OriginalPageCount - 1 do
NewPageNumbers[I] := 0; // 0 == trang này đã bị bỏ
NewPageNumbers[0] := 1; // trang cũ 1 -> trang mới 1
NewPageNumbers[1] := 2;
NewPageNumbers[9] := 3; // trang cũ 10 -> trang mới 3
// True: xóa cả subtree đuối. False: giữ mục, tước đích của nó
if not Editor.ApplyPageMap(NewPageNumbers, True, Report) then
raise Exception.Create(Report.ErrorMessage);
WriteLn(Format('%d remapped, %d dangling items removed',
[Report.RemappedDestinationCount, Report.RemovedDanglingItemCount]));
end;
Cờ DeleteDangling quyết định chính sách cho một đích được map về số 0, và cả hai nhánh đều là lựa chọn chủ đích. Với True, PDFiumPas xóa mục cùng toàn bộ subtree của nó, vì một node đại cương mà đích đã biến mất thường dẫn đầu một chương biến mất theo. Với False, mục sống sót với tiêu đề và phân cấp nguyên vẹn nhưng /Dest và /A bị bỏ, đúng thứ bạn cần khi một người sẽ retarget nó trong khâu rà soát. Đầu vào thực sự dị dạng vẫn fail ầm ĩ thay vì được vá: một phần tử âm hay một đích trỏ quá cuối map đã cung cấp trả về False với IssueKind đặt thành poviInvalidPageMap
Các mục mờ đục, và sự đánh đổi được nói thẳng
Không phải mục đại cương nào cũng có số trang mà PDFiumPas suy luận được. Ba loại được giữ nguyên trạng đưa qua: named destination, action không phải /S /GoTo, và các dictionary key không rõ do bất cứ thứ gì sinh ra tệp thêm vào. Chúng được nạp với PageNumber bằng 0, giữ nguyên byte gốc trong mục, và được ghi lại nguyên văn trừ khi bạn gọi rõ ràng Retarget lên chúng
- Named destination là một key vào name tree của tài liệu, nên ánh xạ lại đúng nghĩa là phân giải cây đó và viết lại entry đích, không phải đoán ở cấp đại cương
- Action
/URI,/Launchhay JavaScript không có ngữ nghĩa trang nào cả và không được lặng lẽ chuyển thành Go-To - Các key đặc thù của nhà cung cấp và structure destination được bảo toàn, vì việc vứt bỏ thứ bạn không hiểu chính là cách round-trip làm mất dữ liệu
Cái giá là thật và đáng nói thẳng: ApplyPageMap bỏ qua hoàn toàn các mục đó, nên một tài liệu mà mọi dấu trang đều dùng named destination sẽ đi qua một lần xóa trang với đại cương hợp lệ về cấu trúc nhưng lỗi thời về ngữ nghĩa. Đó là lựa chọn chủ đích — một liên kết lỗi thời mà người rà soát bắt được còn hơn một liên kết sai đầy tự tin mà không ai để ý. Nếu bạn phân loại tệp đến trước khi chỉnh sửa, một lượt kiểm kê trong PDF intake review workbench sẽ cho bạn biết tài liệu nào rơi vào nhóm đó
Lưu: revision tăng dần, rồi một lần nạp lại độc lập
TPdfOutlineEditor.SaveIncremental nối thêm một revision tăng dần thưa thay vì viết lại tệp. Các mục đã được nạp giữ nguyên tham chiếu indirect object gốc kể cả generation chính xác, nên các cross-reference hiện hữu vẫn hợp lệ; chỉ những mục bạn thêm mới lấy số mới, cấp phát từ một đơn vị sau số object lớn nhất của revision. Catalogue được cập nhật trong cùng revision đó, và entry /Outlines thiếu sẽ được thêm vào khi nguồn vốn không có đại cương
Chuyện gì xảy ra sau khi ghi mới là phần đáng copy. PDFiumPas mở lại stream đích bằng một editor hoàn toàn độc lập và so cây nạp lại với cây trong bộ nhớ — số mục, tiêu đề, số trang, hậu tố đích, dạng action hay đích trực tiếp, style, trạng thái mở rộng, và quan hệ parent. Bất kỳ lệch nào, hay bất kỳ lỗi nạp nào, đều xóa sạch stream đích và trả về poviVerificationFailure thay vì đưa bạn một tệp nhìn có vẻ hợp lý. Nguồn mã hóa bị từ chối ngay từ đầu với poviEncryptedInput, vì tiêu đề và đích mới tạo nội dung string mà không thể tạo ra bằng cách copy /Encrypt trailer về phía trước
if not Editor.SaveIncremental(Source, Dest, Report) then
case Report.IssueKind of
poviEncryptedInput:
Log('Source is encrypted; outline editing needs an unprotected copy');
poviInvalidDestination:
Log(Format('Item %d %d targets a missing page',
[Report.ObjectNumber, Report.Generation]));
poviVerificationFailure:
Log('Reload check rejected the written revision: ' + Report.ErrorMessage);
else
Log(Report.ErrorMessage);
end;
Hãy đối xử với đại cương như nó vốn vậy — một đồ thị object liên kết với các bất biến riêng — và việc xóa trang thôi là thảm họa dấu trang, trở thành một page map bạn đưa cho một lời gọi method. TPdfOutlineEditor, ApplyPageMap, và bộ ghi tăng dần đã kiểm chứng có trong PDFiumPas từ v3.98.0 cho Delphi, C++Builder và Lazarus; bạn có thể xem toàn bộ API và tải bản dùng thử trên trang sản phẩm PDFium Delphi Component