Thay trang 3 của một hợp đồng đã ký duyệt không nên làm xê dịch mục lục. Xóa trang cũ, chèn trang mới, và mọi bookmark từng trỏ tới đó giờ rơi vào một nơi khác. Thư viện PDFlibPas Delphi PDF tránh được điều này bằng cách giữ nguyên chính đối tượng trang đích và chỉ chuyển các mục mang nội dung hiển thị
Vì sao bookmark hỏng sau khi thay trang PDF?
Bookmark hỏng vì một destination trong PDF nêu tên một trang bằng tham chiếu đối tượng gián tiếp, không phải bằng số trang. ISO 32000-1 §12.3.2.2 định nghĩa một explicit destination là một mảng mà phần tử đầu tiên là một tham chiếu gián tiếp tới đối tượng trang. Xóa đối tượng đó rồi nối thêm một trang thay thế, và tham chiếu trở nên treo lơ lửng: hầu hết trình xem phản ứng bằng cách đưa người đọc về trang 1, đây chính xác là triệu chứng mà mọi người báo cáo sau một lần thay thế theo kiểu xóa-rồi-chèn. Cây trang trông hoàn hảo, số trang đúng, việc kết xuất đúng, và toàn bộ lớp điều hướng thì âm thầm sai
Named destination cũng không cứu được bạn. §12.3.2.3 dẫn một tên qua name tree /Dests trong document catalogue, nhưng lá mà tên đó giải quyết tới vẫn là một mảng explicit destination chứa cùng tham chiếu trang đó. Việc đặt tên chỉ thêm một lớp gián tiếp phía trên tham chiếu trang, chứ không bao quanh nó. Cùng lý lẽ đó bao trùm phần còn lại của lớp tương tác được mô tả trong §12.5: một link annotation mang một /Dest hoặc một hành động GoTo /A có /D chính là mảng đó, mọi annotation đều có thể mang một mục /P là một tham chiếu gián tiếp tới trang của nó, và một widget trường form là một annotation trên đúng cùng nền tảng đó. Một lần đổi trang ngây thơ làm tách rời cùng lúc bốn hệ thống con, và nếu bạn muốn thấy chúng được liệt kê trên một tệp thật, cùng đồ thị đối tượng đó chính là thứ mà kiểm tra outline và annotation duyệt qua
Mục nào của trang mang tính danh tính, mục nào mang tính hiển thị
Một dictionary trang trộn lẫn hai loại mục, và một lần thay thế tại chỗ chỉ thành công khi bạn tách rời chúng. Phía hiển thị là hữu hạn và có thể liệt kê: /Contents, /Resources, năm khung trang /MediaBox, /CropBox, /BleedBox, /TrimBox và /ArtBox, cộng thêm /Rotate, /Group, /UserUnit và /BoxColorInfo. Mười một mục đó quyết định mọi thứ mà một bộ rasterise tạo ra cho trang, và không có gì khác trong tệp trỏ tới chúng bằng tên
Phía danh tính là những gì phần còn lại của tài liệu đã tự ràng buộc vào: số đối tượng và thế hệ của trang, liên kết ngược /Parent vào cây trang, và /Annots. PDFlibPas giữ nguyên từng thứ trong số đó. ReplacePageRanges xóa bỏ mười một mục hiển thị khỏi dictionary trang đích rồi thêm lại chúng từ trang nguồn đã nhập, nên đối tượng trang đích được chỉnh sửa tại chỗ chứ không phải bị thay thế. Cấu trúc cây trang mà §7.7.3 yêu cầu cũng giữ nguyên hình dạng từng byte: thứ tự /Kids, /Count, và mỗi /Parent còn sống sót đều giống hệt trước và sau, vì không có node nào từng bị gỡ liên kết
PDFlibPas thay một trang mà không đánh số lại đối tượng bằng cách nào?
Lệnh gọi nhận một tài liệu nguồn, một trang bắt đầu đích tính từ 1, một biểu thức dải trang nguồn, và một cờ tùy chọn. Cả hai tài liệu phải được mở trong cùng một thực thể, và tài liệu đích là tài liệu đang được chọn. Vì số trang đích không bao giờ thay đổi, dải bạn yêu cầu phải vừa khít trong tài liệu bắt đầu từ TargetStartPage, và điều đó được kiểm tra trước khi bất cứ thứ gì được tạo ra
var
Lib: TPDFlib;
TargetDoc, SourceDoc: Integer;
begin
Lib := TPDFlib.Create;
try
// The document whose bookmarks and links must survive
if Lib.LoadFromFile('contract-final.pdf', '') <> 1 then
Exit;
TargetDoc := Lib.SelectedDocument;
// The revised clause page, rendered by whatever produced it
if Lib.LoadFromFile('clause-7-revised.pdf', '') <> 1 then
Exit;
SourceDoc := Lib.SelectedDocument;
Lib.SelectDocument(TargetDoc);
// Source page 1 overwrites the visuals of target page 3.
// Page count, page 3 object number, bookmarks and annotations are kept.
if Lib.ReplacePageRanges(SourceDoc, 3, '1', 0) = 1 then
Lib.SaveToFile('contract-final.pdf');
finally
Lib.Free;
end;
end;
Bên trong, các trang nguồn không thể đơn giản được đọc xuyên qua ranh giới tài liệu, vì mọi tham chiếu gián tiếp bên trong chúng thuộc về hệ đánh số đối tượng của nguồn. Vì vậy dải nguồn trước hết được nhập vào theo cách thông thường, dưới dạng các trang tạm nối thêm sau trang thật cuối cùng, việc này chạy toàn bộ quá trình ánh xạ lại đồ thị đối tượng: content stream, font, XObject, shading và không gian màu đều được đánh số lại vào tài liệu đích. Chỉ sau đó mười một mục hiển thị mới được sao chép từ mỗi trang tạm sang trang đích của nó, và chỉ sau đó các trang tạm mới bị gỡ liên kết khỏi cây trang. Công việc ánh xạ lại diễn ra ở nơi nó rẻ và an toàn, còn thao tác mang tính phá hủy được rút gọn thành một lần hoán đổi ở mức dictionary trên những trang đã tồn tại sẵn
Đường xóa vốn sẽ phá hủy thứ bạn vừa chuyển sang
Gỡ bỏ các trang tạm đó là bước trông có vẻ tầm thường nhưng không phải vậy. Đường xóa trang thông thường trong thư viện làm nhiều hơn là gỡ liên kết một node: nó gộp các layer của mỗi trang đang bị xóa, làm rỗng content stream đầu tiên, và thu hồi các tài nguyên mà không trang nào khác dùng chung. Đó là hành vi đúng cho một lần xóa thật sự, và ở đây thì lại thảm họa, vì đến lúc các trang tạm bị gỡ bỏ thì các trang đích đã tham chiếu đúng chính những content stream và đối tượng tài nguyên đó. Làm rỗng chúng sẽ khiến trang bạn vừa thay thế trống trơn, và việc quét tài nguyên sẽ thu gom cả font và hình ảnh giờ đã có chủ sở hữu đang sống
Cách sửa là một chế độ bảo toàn-đối-tượng-được-tham-chiếu trên đường xóa nội bộ. Khi được bật, việc xóa bỏ qua cả bước quét tài nguyên không chia sẻ lẫn bước xóa content stream, và không làm gì khác ngoài việc tách các trang khỏi cây trang và sửa lại sổ sách của cây. Các đối tượng đã chuyển sang sống sót với một chủ sở hữu mới, và quyền sở hữu đối tượng sau thao tác này chính là điều bạn sẽ vẽ lên bảng trắng: một content stream, một trang sở hữu, một số đối tượng chưa từng dịch chuyển. Các quy tắc vòng đời liên quan cho việc tạo, xóa và sắp xếp lại trang được trình bày riêng trong ghi chú về các thao tác vòng đời tài liệu và trang
Thứ tự, trùng lặp, và thất bại kiểu tất-cả-hoặc-không-gì
Cờ tùy chọn chọn cách diễn giải dải nguồn. 0 sắp xếp các số trang đã phân tích và loại bỏ trùng lặp, đây là mặc định hợp lý khi bên gọi truyền vào thứ gì đó như '4-6,2' và đơn giản có nghĩa là bốn trang đó. 1 giữ nguyên thứ tự bạn đã viết và cho phép một trang lặp lại, nên '2,1,2' thực sự có nghĩa là ba lần thay thế lấy từ hai trang nguồn. Việc xác thực chạy trước và chạy đầy đủ: cú pháp dải, mọi số trang so với số trang nguồn, chính giá trị tùy chọn, và dung lượng đích đều được kiểm tra trước khi một đối tượng duy nhất được tạo ra. Một lệnh gọi bị từ chối đặt LastErrorCode thành 412, khôi phục trang đã chọn trước đó, và để tài liệu y hệt như trước
var
Replaced: Integer;
begin
Lib.SelectDocument(TargetDoc);
// Options = 1: source order is preserved and repeats are allowed, so
// target pages 5, 6 and 7 receive source pages 2, 1 and 2 respectively
Replaced := Lib.ReplacePageRanges(SourceDoc, 5, '2,1,2', 1);
if Replaced = 0 then
raise Exception.CreateFmt('Replacement rejected, LastErrorCode = %d',
[Lib.LastErrorCode]);
// On success the selection is the first replaced page
Assert(Lib.SelectedPage = 5);
end;
Tính nguyên tử (atomicity) kéo dài vượt qua cả bước xác thực vào tận bản thân quá trình chuyển đổi. Trước khi trang nguồn đầu tiên được nhập, mười một mục hiển thị của mọi trang đích trong dải được chụp nhanh (snapshot) dưới dạng giá trị đã mã hóa. Nếu việc nhập thất bại, hoặc số trang đã nhập không khớp với số được yêu cầu, các snapshot được giải mã trở lại lên các trang đích và các trang tạm bị gỡ bỏ, nên một thất bại giữa chừng vẫn để lại phần hiển thị gốc nguyên vẹn trên chính các đối tượng gốc của chúng. Điều đó quan trọng hơn vẻ ngoài của nó: một dải trang bị thay thế nửa chừng trong một hợp đồng còn tệ hơn cả một lệnh gọi thất bại, vì không có gì trong tệp đánh dấu nó là đang dang dở
// Post-conditions worth asserting in a regression test
Lib.SelectPage(3);
// Geometry now comes from the source page
WriteLn(Format('%.2f x %.2f', [Lib.PageWidth, Lib.PageHeight]));
// Annotations that were already on target page 3 are still attached
WriteLn(Lib.AnnotationCount);
// The bookmark created before the replacement still resolves to page 3
WriteLn(Lib.GetOutlinePage(OutlineID));
// And the document is still the same length
WriteLn(Lib.PageCount);
Thay thế tại chỗ vẫn chưa làm được điều gì cho bạn?
Annotation nguồn, trường form nguồn và outline nguồn cố tình không được nhập vào. Mang một widget sang mà không có mục trường /AcroForm của nó, hay một annotation mang marked-content mà không có quyền sở hữu structure tree, sẽ tạo ra một đối tượng tương tác nhập nửa vời mà không trình xem nào có thể suy luận được, nên thao tác này chỉ chuyển phần hiển thị mà thôi. Hệ quả thực tế là nếu trang thay thế lẽ ra phải mang các trường form mới hay liên kết mới, bạn thêm chúng vào trang đích sau đó, dựa trên đối tượng trang đích vẫn còn đang chờ ở đó
Còn hai ranh giới nữa đáng để kiểm tra trên tệp của riêng bạn. Thứ nhất, /Annots được bảo toàn nhưng hình học của trang thì không, nên thay một trang 220 mm bằng một trang 320 mm sẽ giữ nguyên các hình chữ nhật annotation ở tọa độ cũ bên trong một /MediaBox có kích thước khác; nếu hình học thay đổi, hãy định vị lại các annotation bạn đã giữ. Thứ hai, các mục nằm ngoài mười một khóa hiển thị vẫn ở lại với trang đích theo thiết kế, điều này đúng đối với /Trans hay /AA nhưng lại lỗi thời đối với /Thumb, nên hãy tái tạo lại thumbnail sau một lần thay thế. Các tài liệu được gắn thẻ (tagged) cần thêm một điều cần nghĩ tới: các phần tử structure vẫn trỏ tới đúng đối tượng trang qua /Pg, nhưng các định danh marked-content của chúng mô tả nội dung không còn ở đó nữa, nên việc đổi trang trong một quy trình PDF/UA vừa là một chỉnh sửa structure tree vừa là một chỉnh sửa nội dung. Nếu công việc của bạn thực chất là ghép lớp (compositing) chứ không phải đổi trang, xếp chồng hình ảnh lên các trang bạn giữ lại, thì cách tiếp cận ghép trang và mẫu (template) là công cụ rẻ hơn
Mọi thứ được mô tả ở đây, bao gồm cú pháp biểu thức dải, các giá trị tùy chọn và API thao tác trang xung quanh, được cung cấp trong PDFlibPas Delphi PDF Library tiêu chuẩn cho Delphi và C++Builder, tài liệu tham khảo của thư viện này có đầy đủ mục cho lệnh gọi thay thế trang và các mã lỗi của nó