Bài viết kỹ thuật

Đóng dấu trang có thể tái sử dụng thông qua Form XObjects với PDFium

Việc đóng một hình nền mờ (watermark) hoặc một logo lên mỗi trang của tài liệu nghe có vẻ giống như một công việc chỉ tốn 5 phút cho tới khi bạn mở kết quả ở trong một trình kiểm tra kích thước tệp (file-size inspector). Cách tiếp cận hiển nhiên là lặp qua tất cả các trang, và đối với mỗi trang, lại xây dựng chính văn bản hoặc hình ảnh giống nhau. Nó hoạt động về mặt hình ảnh, nhưng lại rất lãng phí theo một cách làm tăng cấp số nhân. Một hình nền mờ chữ "DRAFT" nằm chéo được vẽ trực tiếp vào một bản báo cáo dài một trăm trang đồng nghĩa với việc sẽ có một trăm bản sao của cùng một dữ liệu văn bản và đường dẫn (path) nằm trong các luồng nội dung (content streams), và khi lưu lại tệp sẽ chứa từng bản sao một

Một đối tượng Form XObject là một cấu trúc (construct) mà PDF cung cấp để tránh đúng việc này. Nó gói gọn một đoạn nội dung có thể tái sử dụng, có thể là một trang nguyên vẹn hay một mẫu (template) nhỏ, thành một đối tượng được đặt tên duy nhất, và đối tượng này có thể được vẽ lên ở nhiều vị trí và nhiều lần khác nhau. Nội dung chỉ tồn tại trong tệp một lần. Mỗi trang cần tới con dấu (stamp) này chỉ giữ một lệnh chỉ dẫn (instruction) ngắn gọn với nội dung là "hãy vẽ đối tượng XObject N ở đây, với phép biến đổi (transform) này." Một hình nền mờ (watermark) trên tài liệu dài một trăm trang thay vì phải mất đến một trăm đối tượng, giờ đây nó chỉ thêm một đối tượng vào tệp, và đó chính là điểm khác biệt giữa một tài liệu tăng kích thước tỉ lệ thuận với số lượng trang và một tài liệu không làm vậy. Các con dấu như hình nền mờ, hình dập logo, khuôn trang in số, và con dấu niêm phong (seals) đều có bản chất chung một vấn đề, và đối tượng Form XObject là công cụ thích hợp để xử lý cho từng trường hợp đó

Vì sao việc lưu trữ một đối tượng lại vượt trội hơn một trăm lần vẽ lại

Sự tiết kiệm mang tính cấu trúc (structural), chứ không phải là thẩm mỹ (cosmetic). Một trang PDF sẽ kết xuất (render) thông qua việc thi hành chuỗi toán tử vẽ trong luồng nội dung của nó. Khi bạn vẽ lại một con dấu (stamp) trên từng trang một, bạn đang nối thêm toàn bộ chuỗi toán tử của con dấu đó vào dòng (stream) của mỗi trang, và những byte dữ liệu bị sao chép với số lần tương đương với số lượng trang của bạn. Một Form XObject di dời các toán tử vẽ đó vào thành một dòng (stream) rồi lưu trữ một lần ở trong bản tài liệu (document). Cái tham chiếu mà một trang đơn lẻ giữ lại lại vô cùng bé: nó gọi lên một ma trận biến đổi (transformation matrix), cầu viện đến đối tượng XObject, và cuối cùng phục hồi lại trạng thái cũ (restore state). Số lượng trang không còn đóng vai trò như là nhân tố làm tăng lên chi phí cho các phần mỹ thuật (artwork)

Điều này trở nên rất quan trọng khi con dấu có dung lượng nặng. Một con dấu vector gồm hàng trăm đoạn vẽ đường dẫn (path segments), hoặc một bản vẽ dạng bitmap, thì đều tiêu tốn rất nhiều tài nguyên lưu trữ. Bằng việc lưu trữ chỉ một lần và dùng thao tác gọi tới (reference), chi phí đắt đỏ sẽ chỉ bị tính trong một lần và phí tổn cho mỗi trang (per-page overhead) chỉ còn là vài byte cho việc gọi (invocation). Thành phẩm trực quan ở trên trang sẽ hoàn toàn đồng nhất với vẽ lại trực tiếp, đây mới là điểm đáng chú ý. Ngay cả người đọc cũng chẳng thế nào nhận ra sự khác biệt; nhưng kích thước tệp (file size) thì hoàn toàn có thể

Ghi lại một trang vào XObject

PDFium sẽ xây dựng nên một đối tượng (object) có thể được sử dụng lại ngay từ một trang cho sẵn. Nguồn (source) này chính là một trang ở bên trong một số văn bản mà bạn đang cho mở, hay cũng có thể là một tệp PDF (một trang) nhỏ bé chỉ chứa thiết kế (artwork) là cái hình nền mờ của bạn, hoặc là một trang cụ thể ở trong một tệp (file) lớn hơn. CreateXObjectFromPage ghi lại phần nội dung của chính trang nguồn đó rồi chuyển vào thành một con trỏ (handle) tái sử dụng thuộc quyền sở hữu của bản tài liệu đích, tức là cái mà bạn đang định đặt dấu ấn vào

var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := 'Report.pdf';
    Dest.Active := True;
    Stamp.FileName := 'Watermark.pdf';   // one page of artwork
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 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) ...

Phần nguyên mẫu hàm (signature) có dạng CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Sẽ có ngoại lệ đưa ra nếu bản tài liệu gốc đang không được kích hoạt (not Active), và nó trả về nil chứ không phải tung ra ngoại lệ khi PDFium không thể tạo đối tượng (object) đó, vì vậy khâu xác nhận (check) rõ ràng ở bên trên là bắt buộc. Con trỏ (handle) mang về lại là một TPdfXObject do bạn sở hữu, đi kèm với đó là hai quy tắc bó buộc (lifetime constraints) với nó cũng lại chính là điều khiến mọi người nhầm lẫn ở trong cái cách thực hành này, vì vậy mà chúng sẽ cần có hẳn một phần nói riêng ở dưới đây

Đặt con dấu lên trang

Một XObject được ghi lại chẳng tự mình làm gì cả. Để làm cho nó xuất hiện, bạn phải tự mình chèn vô tài liệu của bạn ngay tại cái trang hiện hành, trang này được lựa theo một PageNumber đánh số bắt đầu từ 1, với hàm InsertFormObjectFromXObject. Lời gọi lệnh này sẽ mang trả cho một phần tử (object) trang gốc, một FPDF_PAGEOBJECT, và phần con trỏ (handle) trả về sẽ là cách mà bạn sẽ định vị chèn nó vào (position the placement). Nếu chưa qua biến đổi thì bản dấu đó (stamp) nó sẽ nằm nguyên ở tọa độ gốc, thứ mà lại hiếm khi là cái mà ta mong muốn

Do InsertFormObjectFromXObject chèn một bản sao (copy) vào trên mỗi lần được gọi lệnh và nó luôn truyền đi một đối tượng trang mới mỗi lúc, bạn có toàn quyền sơn lại (paint) cùng một đối tượng XObject nhiều lần trên cùng một trang với sự biến đổi khác nhau, nội dung bên trong được lưu cũng vẫn sẽ đếm làm một trong cái tệp tin. Một chiếc logo nằm góc và kèm thêm một cái hình chìm thật nhạt chiếm toàn bộ cả trang hoàn toàn có thể đến từ một đối tượng được bắt về duy nhất

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
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);
    RawM := M.Handle;
    if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
      raise Exception.Create('Cannot assign the stamp matrix');
  finally
    M.Free;
  end;
  Dest.UpdatePage;   // commit this page's edits to its content stream
  // if not Dest.SaveAs(...) then ... when every page is done.
end;

Hai thao tác dọn dẹp (housekeeping) khiến cho đoạn mã này an toàn. Điều đầu tiên, khi đã chèn (insert), thành phần trang ấy phụ thuộc về phía trang, chứ không phải nằm bên trong cái đối tượng XObject. Việc giải phóng cái XObject đó sau này không làm hỏng sự sắp đặt mà bạn vừa làm. Nhờ vậy mà khâu thứ tự tạo-đặt-giải phóng diễn tả ở phần bên dưới có thể có tác dụng. Cái thứ hai, thao tác chèn vô và thay đổi (inserting and positioning) chỉ thay được danh sách đối tượng (object list) của trang đó tại trong bộ nhớ; UpdatePage mới chính là bước chuỗi hóa (serialises) lại danh sách ấy trả lại luồng nội dung trang (page's content stream), nên một trang được sửa đổi khi quên chẳng gọi lệnh nó thì kết cục giống như là chưa bao giờ được chèn bản dấu vậy

Quy tắc vòng đời tay cầm (handle lifetime) làm khó mọi người

Có hai giới hạn áp đặt lên cái XObject, và hễ làm ngơ một trong hai thì lỗi (failure) phát sinh nhìn qua có vẻ vô can tới thủ phạm của nó. Số một, văn bản gốc phải Active ngay tại lúc gọi lệnh CreateXObjectFromPage. Quá trình bắt (capture) nội dung đọc thẳng nội dung trang của một gốc đang chạy thật, cho nên cái văn bản (document) và trang đó phải đang mở ra cũng như có giá trị ở lúc cấu trúc xong một tay cầm (handle). Số hai, và đây chính là điểm làm cho người ta hay ngạc nhiên, phải đi giải phóng (freed) tay cầm này trước thời điểm đóng trang, và ở thực tiễn là thời gian phải trước khi bạn đem đóng (close) hay dọn đi (free) bản văn bản lấy ra làm đầu vào

Lí lẽ là XObject thực ra chính là phần đối chiếu (reference) đưa vô chỗ cơ cấu bộ phận (structure) mà văn bản tài liệu đang chiếm cứ. Nó đâu phải là một cái được rũ độc lập, hoặc làm phần bản in (copy) được bao gọn có thể mang đem theo bất kỳ lúc nào nguồn gốc bị hủy mất. Hễ đi đem đi đóng phần khởi đầu sớm là cái cần cẩu sẽ chỉ vô một thứ giờ đây đã chịu tháo dỡ, nên giải quyết dọn sau nó đi chăng nữa, hay mọi cách lấy ra sử dụng, cũng đang là làm chạy trong vùng ký ức bộ nhớ (memory) không có hiệu lực. Những triệu chứng đặc thù dành cho một lỗi thả tự do tay cầm: lỗi cấm xâm phạm vùng truy cập (access violation) ngay lúc hệ thống phải đi xuống nước ngừng (shutdown), và lỗi gây hỏng thỉnh thoảng (intermittent corruption) di dời vị trí còn tuỳ vô tuần tự bố trí khởi tính cho biến nhớ (allocation order), có nguyên cái stack (cây theo dõi gọi lệnh) không ngó lại vị trí gây đổ vỡ mà luôn chỉ mặt vô chỗ lệnh đem đi gom dẹp (cleanup code). Giải pháp (fix) cũng như thứ tự sắp xếp (ordering), đây đâu phải là một phong cách rào đón lập trình an toàn (defensive coding). Bày ra bản mẫu (XObject), đem ấn nó tại hết tất thảy số tờ đang mong có nó, giải phóng XObject và mới kết cho đóng gốc tài liệu. Bộ khởi thoái (destructor) TPdfXObject sẽ phóng thả đi cái cán cơ bản ở trên thư viện (PDFium) hỗ trợ lại cho mình, nhờ vậy thả tự do (freeing) thành phẩm phần được bao đóng lại (wrapper) bằng đúng khoảng cần mới thực sự đã thỏa bổn phận riêng cho các bạn

Ma trận và ý nghĩa sáu con số của nó

Việc đặt vị trí là một phép biến đổi affine 2D (affine transform), cũng giống như cách PDF luôn sử dụng ở mọi nơi để chỉnh vị trí cho các nội dung (ISO 32000-1, section 8.3.4). Chứa đến những con số gồm a, b, c, d, e, f, mà tại đây thư viện đem hiển thị chúng làm ra kiểu dạng bản mẫu của FS_MATRIX. Chức năng chính là lấy chiếu sơ đồ điểm (map a point) ngay từ chính không gian vật thể qua thẳng lại chỗ phạm vi một tờ giấy in (page space):

// 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)

Các con chữ được cho điền đầy số ở đó vào bằng thủ công thế nhưng lắp chắp lại bằng cách thức tự tay là thứ gây loạn khi xoay (rotation), do hành động xoay đánh tráo toàn bộ của số a, b, c, d chen trộn cùng. Bản bọc (wrapper) bằng TPdfMatrix, từ khối lệnh mang tên FPdfMatrix, đem gộp những thao tác cơ bản lại với nhau giùm các bạn cũng như hậu nhân (post-multiplies) khi thao tác gọi xong, thế nên Translate, ScaleRotate xếp lại theo chuỗi tại ngay chính cái lệnh mà bạn định gọi. Chuyện tạo nền theo chiều chéo là quá trình theo cách xoay ngay sau có động thái chạy tịnh tiến (translate) và rồi quay vô về vị trí lại chính giữa (recentre); có góc biểu chưng cho hình huy là cái co kích cỡ có cái dời theo cùng lúc tới đó. Một lúc bảng trận mảng cấu hình có mặt sẵn sàng, sao lại đúng gốc thô (raw value) từ chỗ đó mang tên thành Handle theo phân lớp tên mã hóa là FS_MATRIX qua thẳng vùng nhớ biến đổi của lệnh, trao cho tới nơi lệnh là FPDFPageObj_SetMatrix; một cách gọi dẫn tới yêu cầu là nhập (import) lại đưa cái thuộc tính (matrix) biến định theo cấu mẫu dạng biến truyền là một var, bởi thế tính cách cấu phần (property) bị chặn đường không gửi được trực diện tại nó, nếu hỏng nhận phần tính không (0). Thứ thuộc tính theo hạng cấp nền FPDFPageObj_Transform, mà bắt phải dùng luôn cả lấy phần tử con số thông dưới mô hình hai chuẩn phần dư (doubles), có mặt trực diện để mà người lập trình hễ khi không còn ưng dùng trình bọc (wrapper)

Đóng dấu mỗi trang, theo đúng trình tự

Hình dạng kiểu mẫu đầy trọn đưa toàn tất số lượng mảnh vỡ tụ lại đi cùng thứ tự trình biên dựa vào những giới luật ở vòng đời yêu cầu. Khơi màn lên song hệ tờ gốc (documents), lấy chụp lại phần nền đóng dấu mộc cho ở một lần rồi chạy theo số những cái trang tài liệu với sự chuyển đổi cho nó bộ khung tham số gọi tên PageNumber đánh số khởi từ chỗ giá trị có 1 làm căn bản liên hoàn chèn cài với định thế (positioning) nguyên phần rập bản rồi gọi chuyển giao kết thành từng phần của trang với tay của UpdatePage, ngay từ đây rồi đi phóng giải con bộ tham mưu XObject, kết lại làm cứu với (SaveAs) rồi đi để mở cho cái nguồn sinh gốc của đoạn cuối mới dọn khép

procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
  I: Integer;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := ASource;
    Dest.Active := True;
    Stamp.FileName := AStamp;
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 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. PageNumber is 1-based.
      for I := 1 to Dest.PageCount do
      begin
        Dest.PageNumber := 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
          RawM := M.Handle;
          FPDFPageObj_SetMatrix(PageObj, RawM);
        finally
          M.Free;
        end;
        Dest.UpdatePage;                     // commit this page's edits
      end;
    finally
      XObject.Free;                          // 3. free BEFORE Stamp closes
    end;

    // 4. Write the result while Dest is still open.
    if not Dest.SaveAs(AOutput) then
      raise Exception.Create('Could not save ' + AOutput);
  finally
    Stamp.Free;                              // source closes last
    Dest.Free;
  end;
end;

Hình dạng kiểu tổ chức khối bao try này lo hết trách nhiệm khó nhất của cơ việc. Chỗ kết hàm vòng trong (inner finally) thả đi cái mẫu mã XObject cho kịp thời sớm tại mức nó chẳng có đường nào thoát nhảy ngoài biên finally chỗ ngoài đó để dọn đi cái Stamp cả, bởi đó bộ ngón đòn vung được cho tháo thoát một cái kiểu mà cái thân nguồn gốc vẫn đủ khỏe đang tại sống, chí ngay đến cả đoạn lúc mà đụng phải văng lỗi nổ súng ra ngoại vi ở đoạn vòng lặp lúc làm. Chăm giữ bộ nấp rào lồng này chuẩn chỉ đó thì quy chế tại vòng sinh mệnh đời (lifetime rule) nó cũng đã được để cho đi đúng đường lối của riêng

Chuyện đóng mộc (Stamping) chẳng qua thuộc một trong mảng nhỏ của bộ dụng cụ chức năng (toolkit) bao cấp đầy rẫy đủ đồ chơi nhằm làm cho chuyện ráp thành phẩm với tinh chỉnh cho phần khung bên trong trang tài liệu. Nhược bằng mẫu đồ chưng để ấn lên chính ngay gốc lại hiện ở thế ảnh thay vì thuộc một dòng văn trích riêng một tờ bản giấy, hãy ngó chuyển đổi tệp ảnh thành file gốc văn PDF thông qua sử dụng bản gốc chuẩn PDFium mà che cho khâu giúp làm cho hình đó bóc được qua vô trước rồi hẳn bàn. Còn lại hễ mà bản thể vật bạn đưa gán đính với phần nổi bản ảnh hiện thị không bằng ở mực viết in mặt trước thì đi mà lại thành file tệp gắn, xem cái phần nói đến việc cùng chung mảng với đồ tệp (attachments) PDF bên trong phạm hệ Delphi để nhìn đi phương phía dính với file tập tin ngầm bên (embedded-file side). Hết ráo bọn chúng xuất xưởng bằng bản mẫu mang tựa PDFium Component của cho phần Delphi và nhóm ứng C++Builder, luôn đính ở dọc hành các bộ cung đồ để làm việc kết xuất xuất ảnh hiện hiển, biến diễn thay thế chữ đồ, cũng như mảng đồ API bộ ứng dùng dành cho các gốc tài liệu, nói rõ khắp trang chốn ở ngay bài blog như bạn đọc