Đóng dấu một hình mờ hoặc một logo lên mỗi trang của một tài liệu trông giống như một công việc kéo dài năm phút cho đến khi bạn mở kết quả trong một trình kiểm tra kích thước tệp. Cách tiếp cận rõ ràng là đi qua các trang và trên mỗi trang, xây dựng lại các đối tượng văn bản hoặc hình ảnh giống nhau. Điều đó hoạt động về mặt trực quan, nhưng nó lãng phí theo cách phức tạp dần. Một hình mờ "DRAFT" chéo được vẽ trực tiếp lên một báo cáo một trăm trang là một trăm bản sao của cùng dữ liệu văn bản và đường dẫn nằm trong các content stream, và tệp được lưu giữ mang theo từng bản sao đó.
Một Form XObject là cấu trúc mà PDF cung cấp để tránh chính xác điều này. Nó gói một phần nội dung có thể tái sử dụng, toàn bộ một trang hoặc một mẫu nhỏ, vào một đối tượng duy nhất được đặt tên mà có thể vẽ nhiều lần tại nhiều vị trí. Nội dung tồn tại trong tệp một lần duy nhất. Mỗi trang muốn có dấu đóng giữ một hướng dẫn ngắn nói rằng "vẽ XObject N ở đây, với chuyển đổi này." Một hình mờ trên một trăm trang sau đó sẽ thêm một đối tượng nội dung vào tệp thay vì một trăm đối tượng, và đó là sự khác biệt giữa một tài liệu tăng kích thước tuyến tính theo số lượng trang của nó và một tài liệu không như vậy. Hình mờ, con dấu logo, mẫu số trang, và con dấu niêm phong đều là cùng một dạng vấn đề, và Form XObject là công cụ thích hợp cho mỗi loại trong số chúng.
Tại sao một đối tượng được lưu trữ tốt hơn một trăm lần vẽ lại
Việc tiết kiệm mang tính cấu trúc, chứ không phải thẩm mỹ. Một trang PDF kết xuất bằng cách thực thi content stream của nó, một chuỗi các toán tử vẽ. Khi bạn vẽ lại một con dấu trên mỗi trang, bạn đang nối thêm toàn bộ chuỗi toán tử cho con dấu đó vào stream của mỗi trang, và các byte được nhân đôi nhiều lần bằng số lượng trang bạn có. Một Form XObject di chuyển các toán tử đó vào một stream được lưu trữ một lần duy nhất trong tài liệu. Tham chiếu mà một trang riêng lẻ giữ là rất nhỏ: nó đẩy một ma trận chuyển đổi (transformation matrix), gọi XObject, và khôi phục trạng thái. Số lượng trang không còn nhân chi phí của tác phẩm nghệ thuật lên.
Điều này quan trọng nhất khi con dấu nặng. Một con dấu vector với hàng trăm đoạn đường dẫn, hoặc một bitmap logo, rất tốn kém để lưu trữ. Được lưu trữ một lần và được tham chiếu, phần nặng nề chỉ được chi trả một lần duy nhất và chi phí bổ sung trên mỗi trang là một vài byte lệnh gọi. Kết quả trực quan trên trang là giống hệt với một lần vẽ lại trực tiếp, đó chính là điểm mấu chốt. Người đọc không thể nhận ra sự khác biệt; kích thước tệp rất có thể nhận ra.
Chụp một trang vào một XObject
PDFium xây dựng đối tượng có thể tái sử dụng từ một trang hiện có. Nguồn là một trang trong một số tài liệu bạn đang mở, một tệp PDF nhỏ một trang không chứa gì ngoài tác phẩm nghệ thuật hình mờ của bạn, hoặc một trang cụ thể của một tệp lớn hơn. CreateXObjectFromPage chụp nội dung của trang nguồn đó vào một handle có thể tái sử dụng thuộc sở hữu của tài liệu đích, tài liệu bạn đang đóng dấu.
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
Đặt con dấu lên một trang
Một captured XObject không tự làm gì cả. Để làm cho nó xuất hiện, bạn chèn một bản sao của nó lên trang hiện tại của tài liệu bằng InsertFormObjectFromXObject. Lệnh gọi đó trả về đối tượng trang bên dưới, một FPDF_PAGEOBJECT, và handle được trả về là cách bạn định vị vị trí đặt. Nếu không có chuyển đổi, con dấu sẽ rơi vào gốc tọa độ trong hệ tọa độ riêng của trang nguồn, đây hiếm khi là nơi bạn muốn.
Bởi vì InsertFormObjectFromXObject chèn một bản sao cho mỗi cuộc gọi và trả về một đối tượng trang mới mỗi lần, bạn có thể vẽ cùng một XObject vài lần trên một trang ở các chuyển đổi khác nhau, và nội dung được lưu trữ vẫn được tính một lần duy nhất trong tệp. Một logo góc và một hình mờ toàn trang mờ nhạt có thể đến từ cùng một đối tượng được chụp.
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;
Một chi tiết sở hữu giúp việc dọn dẹp an toàn
Một khi được chèn, đối tượng trang thuộc về trang, chứ không thuộc về XObject. Việc giải phóng XObject sau đó không làm mất hiệu lực của các vị trí đặt mà bạn đã thực hiện. Đó là điều cho phép thứ tự tạo-đặt-giải phóng được mô tả bên dưới hoạt động.
Quy tắc vòng đời handle gây rắc rối cho mọi người
Hai ràng buộc chi phối handle XObject, và việc bỏ qua một trong hai sẽ tạo ra một thất bại trông có vẻ không liên quan đến nguyên nhân của nó. Đầu tiên, tài liệu nguồn phải hoạt động tại thời điểm bạn gọi CreateXObjectFromPage. Việc chụp đọc nội dung của trang nguồn từ tài liệu nguồn trực tiếp, do đó tài liệu đó và trang của nó phải được mở và hợp lệ khi handle được xây dựng. Thứ hai, và đây là điều khiến mọi người ngạc nhiên, handle phải được giải phóng trước khi trang nguồn đóng lại, và trong thực tế là trước khi bạn đóng hoặc giải phóng tài liệu nguồn mà nó sinh ra.
Lý do là vì XObject là một tham chiếu vào cấu trúc mà tài liệu nguồn vẫn sở hữu. Nó không phải là một bản sao độc lập, tự chứa mà bạn có thể mang theo sau khi nguồn đã mất. Đóng nguồn trước và handle sẽ trỏ vào nội dung đã bị phá bỏ, vì vậy giải phóng nó sau đó, hoặc bất kỳ việc sử dụng nào khác, đều hoạt động trên vùng nhớ không còn hợp lệ. Triệu chứng là lỗi truy cập bộ nhớ điển hình khi tắt chương trình, hoặc lỗi hỏng bộ nhớ không liên tục di chuyển xung quanh tùy thuộc vào thứ tự cấp phát, với ngăn xếp cuộc gọi trỏ đến mã dọn dẹp thay vì dòng mã thực sự gây ra sự cố. Giải pháp khắc phục là thứ tự thực hiện, chứ không phải viết mã phòng thủ. Xây dựng XObject, chèn nó lên mọi trang cần thiết, giải phóng XObject, và chỉ sau đó mới đóng tài liệu nguồn. Hàm hủy của TPdfXObject giải phóng handle PDFium bên dưới cho bạn, do đó giải phóng lớp bọc đúng thời điểm là toàn bộ trách nhiệm của bạn.
Ma trận, và ý nghĩa của sáu con số của nó
Vị trí đặt là một chuyển đổi affine 2D, cùng một chuyển đổi mà PDF sử dụng ở mọi nơi để định vị nội dung (ISO 32000-1, mục 8.3.4). Nó gồm sáu số, được viết là a, b, c, d, e, f, và PDFium hiển thị chúng dưới dạng bản ghi FS_MATRIX. Chúng ánh xạ một điểm từ không gian của đối tượng sang không gian trang:
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
Bạn có thể tự điền sáu giá trị đó, nhưng việc tự ghép nối chúng là nơi phép quay dễ bị sai, vì phép quay trộn lẫn cả bốn số hạng a, b, c, d lại với nhau. Lớp bọc TPdfMatrix tự động ghép nối các phép toán thông thường cho bạn và nhân sau khi thực hiện, do đó Translate, Scale, và Rotate liên kết theo thứ tự bạn gọi. Một hình mờ chéo là một phép quay theo sau là một phép dịch chuyển để căn giữa lại; một logo góc là một phép tỷ lệ theo sau là một phép dịch chuyển. Khi ma trận đã sẵn sàng, hãy trao giá trị thô của nó cho FPDFPageObj_SetMatrix(PageObj, M.Handle), trong đó M.Handle là FS_MATRIX bên dưới. Hàm cấp thấp hơn FPDFPageObj_Transform, nhận trực tiếp sáu giá trị dưới dạng số thực double, có sẵn khi bạn muốn truyền các con số hơn là xây dựng một lớp bọc.
Đóng dấu mọi trang, theo thứ tự đúng
Mẫu đầy đủ kết hợp các mảnh ghép lại với nhau với thứ tự vòng đời yêu cầu. Mở cả hai tài liệu, chụp con dấu một lần, đi qua các trang đích để chọn từng trang và chèn cộng với định vị một bản sao, sau đó giải phóng XObject, lưu lại, và để tài liệu nguồn đóng sau cùng.
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 1. Capture the artwork once. Stamp is active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := i; // make page i current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
Hình dạng của các khối try đang thực hiện công việc thực sự. Khối finally bên trong giải phóng XObject trước khi quyền điều khiển có thể chạm tới khối finally bên ngoài giải phóng Stamp, do đó handle luôn được giải phóng trong khi nguồn của nó vẫn còn sống, ngay cả khi một ngoại lệ xảy ra giữa vòng lặp. Thiết lập cấu trúc lồng nhau đó đúng cách và quy tắc vòng đời sẽ tự được đảm bảo. (Sử dụng bất kỳ trình chọn trang hiện tại nào mà bản dựng của bạn hiển thị; thân vòng lặp là giống nhau cho cả hai.)
Stamping is one corner of a larger toolkit for building and editing page content. If your stamp is itself an image rather than a captured page, converting images to PDF documents with PDFium covers getting that bitmap into a document first. And when the thing you want to carry alongside the visible stamp is a file rather than ink on the page, working with PDF attachments in Delphi shows the embedded-file side. All of it ships with the PDFium Component for Delphi and C++Builder, alongside the rendering, editing, and document APIs covered elsewhere on this blog.