Bài viết kỹ thuật

Page box PDFlibPas: mặc định TrimBox, BleedBox, CropBox

Khi một trang PDF không có TrimBox, TrimBox hiệu lực của nó là CropBox của trang, và khi CropBox cũng vắng mặt, nó là MediaBox. BleedBox và ArtBox theo cùng quy tắc. PDFlibPas, PDF Library for Delphi, áp chuỗi mặc định này nhất quán trong GetPageBox, HasPageBox và CapturePageEx kể từ v3.539.44, đồng thời bỏ qua các production box đặt trên node /Pages, vì ISO 32000-1 không cho chúng kế thừa

Đọc thì như một chú thích chân trang, cho tới khi bạn ép một công việc in. Tưởng tượng một ruột sách với MediaBox 6.25 × 9.25 in, một CropBox đặt tại trim 6 × 9 in, và không có TrimBox, vì người export chẳng bao giờ nghĩ tới việc viết một cái. Hỏi lấy trim box, nhận về media box, và mỗi ô trên tờ in của bạn kéo theo một phần tám inch bleed và slug tràn vào ô bên cạnh. PDFlibPas từng có lỗi ngay đúng vùng này, được sửa trong v3.539.42 và v3.539.44, và cách chúng được sửa nói lên điều gì đó về việc ngữ nghĩa page box nên được hiện thực thế nào trong bất kỳ thư viện PDF nào

Box nào có hiệu lực khi một trang không có TrimBox?

Câu trả lời là một chuỗi mặc định cố định từ ISO 32000-1 §14.11.2: CropBox mặc định là MediaBox, còn BleedBox, TrimBox và ArtBox mỗi cái mặc định là CropBox. Chỉ mình CropBox là mặc định thẳng về MediaBox. Một trang chỉ định nghĩa MediaBox vì thế có năm box trùng hệt nhau, và một trang định nghĩa MediaBox cộng CropBox có bốn box bằng CropBox

BoxPDFlibPas BoxTypeMặc định khi vắng mặtKế thừa được từ /Pages
MediaBox1Không, entry là bắt buộcCó
CropBox2MediaBoxCó
BleedBox3CropBoxKhông
TrimBox4CropBoxKhông
ArtBox5CropBoxKhông

Chuỗi hai bước này quan trọng vì CropBox có thể bản thân nó là kế thừa. TrimBox hiệu lực của một trang không có TrimBox cũng không có CropBox riêng là CropBox của tổ tiên gần nhất có một cái, và nếu vẫn không có, là MediaBox được kế thừa. Đặc tả thêm một quy tắc nữa dễ quên: các box crop, bleed, trim và art không nên tràn ra ngoài media box, và nếu tràn, chúng bị giảm hiệu lực về phần giao với nó. PDFlibPas báo mỗi box như được lưu trong tệp, nên một validator xử lý input không đáng tin nên tự kẹp theo MediaBox

Chuỗi mặc định page box trong PDFlibPas, nơi CropBox mặc định là MediaBox còn BleedBox, TrimBox và ArtBox mỗi cái mặc định là CropBox, vẽ cạnh một ruột sách với MediaBox 450 nhân 666 point và CropBox 432 nhân 648 point trở thành trim hiệu lực khi không có TrimBox nào tồn tại
Chỉ mình CropBox mặc định thẳng về MediaBox, nên một trang chỉ có MediaBox có năm box trùng hệt nhau

Một node /Pages truyền xuống được những thuộc tính trang nào?

Đúng bốn cái: Resources, MediaBox, CropBox và Rotate. ISO 32000-1 §7.7.3.4 định nghĩa kế thừa thuộc tính, và Bảng 30 chỉ đánh dấu đúng bốn entry đó của page object là kế thừa được. BleedBox, TrimBox và ArtBox thuộc về trang leaf. Một TrimBox ghi vào node /Pages không phải một giá trị kế thừa; nó là một key phi chuẩn mà một reader tuân thủ sẽ bỏ qua

Những tệp phi chuẩn kiểu đó có thật, thường với một TrimBox duy nhất trên node gốc của page tree làm cách viết tắt cho "mọi trang có trim này". Cách viết tắt nhìn có vẻ đúng trong bất kỳ công cụ nào đi /Parent cho từng key, và đó chính là vấn đề: tệp giờ mang hai nghĩa tùy ai đọc nó. Một reader bám đặc tả sẽ thấy không có TrimBox và dùng CropBox, trong khi một reader kế thừa mọi thứ thấy giá trị của parent. Trong một pipeline prepress, sự nhập nhằng ấy cuối cùng đổ xuống tờ in

Kế thừa page tree trong PDFlibPas, nơi chỉ Resources, MediaBox, CropBox và Rotate truyền xuống qua một node Pages, nên một TrimBox đậu trên gốc là key phi chuẩn mà các reader tuân thủ bỏ qua; trước v3.539.44, hai đường code độc lập đã kế thừa nó và báo hai cỡ trim khác nhau cho cùng một tài liệu
Tệp mang hai nghĩa tùy ai đọc nó, và trong pipeline prepress, sự nhập nhằng ấy đáp thẳng xuống tờ in

Các workflow PDF/X (ISO 15930) dựa vào TrimBox cho kích thước thành phẩm, và các profile PDF/X đòi mỗi trang phải khai báo một TrimBox hay một ArtBox. Một box đậu trên node /Pages không đáp ứng yêu cầu ấy, vì key không bao giờ chạm tới page object. Preflight nên gắn cờ những tệp như vậy thay vì lặng lẽ đọc chúng theo kiểu này hay kiểu khác

PDFlibPas đã làm sai gì trước v3.539.44?

PDFlibPas có ba lỗi riêng biệt, tất cả nằm trong khe hở giữa điều đặc tả nói và điều hai đường code độc lập đã làm. Lỗi đầu được sửa trong v3.539.42, hai lỗi còn lại trong v3.539.44

Production box bị mặc định về MediaBox lúc capture

Trước v3.539.42, thủ tục nội bộ chuẩn bị một trang cho capture (nó chép các entry kế thừa lên trang và điền các box còn thiếu) đã cho BleedBox, TrimBox và ArtBox các giá trị MediaBox khi chúng vắng mặt. CapturePageEx với options 2 tới 4 đọc hình chữ nhật bao của nó từ đúng những entry được điền sẵn ấy, nên trên một trang chỉ định nghĩa CropBox, đòi trim box lại capture trọn media box. GetPageBox đã áp mặc định CropBox từ trước, và tài liệu tham khảo của CapturePageEx luôn nói crop box được dùng khi box được yêu cầu vắng mặt; code capture bất đồng với cả hai. Kể từ v3.539.42, ba production box mặc định là CropBox của trang — thứ tới thời điểm đó đã sẵn nằm trên trang (của riêng nó, chép từ tổ tiên, hay được điền từ MediaBox) — và chỉ mình CropBox mới lùi về MediaBox

Hai đường kế thừa, một quy tắc ngữ nghĩa

Lỗi thứ hai là chính việc kế thừa phi chuẩn ấy, và phần tinh vi là PDFlibPas phân giải box theo hai đường độc lập. Các truy vấn box (GetPageBox và HasPageBox) đi chuỗi /Parent qua một helper, còn capture đi qua một helper cục bộ riêng. Cả hai đều kế thừa mọi key, gồm cả production box. Sửa riêng một trong hai sẽ sinh ra mâu thuẫn ngay trong một tài liệu: với một TrimBox rộng 180 point trên node /Pages và một CropBox rộng 380 point trên trang, GetPageBox vẫn báo bề rộng trim 180 trong khi CapturePageEx dựng một form rộng 380. Ở v3.539.44, cả hai đường đều giới hạn phép đi /Parent trong bốn key kế thừa được, production box chỉ được đọc từ leaf, và entry lạc trên parent vẫn nằm nguyên trong tệp, không xóa cũng không ghi lại

Các mã trả về 0, 1 và 2 của HasPageBox trong PDFlibPas, với mảng direct lẫn indirect đều tính là kế thừa kể từ v3.539.44, cạnh các option 0 tới 4 của CapturePageEx nơi BleedBox, TrimBox và ArtBox lùi về CropBox thay vì MediaBox kể từ v3.539.42
Hai điểm vào hiện thực cho một quy tắc đặc tả được sửa cùng nhau và kiểm thử như một ma trận 18 kịch bản, với phần truy vấn và phần capture khớp nhau trên mọi tệp

HasPageBox bỏ sót mảng direct trên parent

HasPageBox trả về 0 khi trang không có box của loại được hỏi, 1 khi trang có box riêng (lưu trực tiếp hay qua tham chiếu indirect), và 2 khi một MediaBox hay CropBox được kế thừa từ tổ tiên. Code cũ chỉ trả về 2 khi giá trị kế thừa là tham chiếu indirect, nên một mảng direct được kế thừa cho ra 0. Bản sửa tách phần giải tham chiếu khỏi phép thử mảng, và cả hai dạng hiện diễn đều trả về 2. Kể từ v3.539.44, HasPageBox cho BleedBox, TrimBox hay ArtBox chỉ có thể trả về 0 hoặc 1

Bài học khái quát được xa hơn nhiều so với page box. Khi một mảnh ngữ nghĩa đặc tả có hai điểm vào hiện thực trong một thư viện, hãy sửa chúng cùng nhau và kiểm thử như một ma trận thay vì một tệp happy-path duy nhất. Bộ hồi quy của PDFlibPas giao hai dạng biểu diễn parent-box (mảng direct và indirect) với ba trạng thái leaf (vắng mặt, mảng direct, mảng indirect) và ba capture option (bleed, trim, art), cho ra 18 kịch bản, và mỗi kịch bản kiểm kết quả truy vấn, biên đã capture, kế thừa MediaBox và CropBox hợp lệ, cùng entry parent còn nguyên vẹn

Đọc TrimBox hiệu lực trong Delphi thế nào?

Gọi GetPageBox(4, Dimension) trên trang đang chọn. PDFlibPas áp chuỗi mặc định sẵn cho bạn, nên kết quả là TrimBox hiệu lực bất kể trang có hay không. Ghép với HasPageBox khi bạn cần biết giá trị đến từ đâu, điều mà một báo cáo preflight thường cần

uses
  System.SysUtils, PDFlibrary;

const
  BOX_CROP   = 2;
  BOX_TRIM   = 4;
  DIM_LEFT   = 0;
  DIM_WIDTH  = 2;
  DIM_HEIGHT = 3;
  DIM_BOTTOM = 5;

function DescribeTrim(Lib: TPDFlib; Page: Integer): string;
var
  Source: string;
begin
  Lib.SelectPage(Page);
  if Lib.HasPageBox(BOX_TRIM) = 1 then
    Source := 'own TrimBox'
  else if Lib.HasPageBox(BOX_CROP) <> 0 then   // 1 = của mình, 2 = kế thừa
    Source := 'defaulted to the CropBox'
  else
    Source := 'defaulted to the MediaBox';
  Result := Format('page %d: trim %.2f x %.2f pt at (%.2f, %.2f), %s',
    [Page,
     Lib.GetPageBox(BOX_TRIM, DIM_WIDTH),
     Lib.GetPageBox(BOX_TRIM, DIM_HEIGHT),
     Lib.GetPageBox(BOX_TRIM, DIM_LEFT),
     Lib.GetPageBox(BOX_TRIM, DIM_BOTTOM),
     Source]);
end;

var
  Lib: TPDFlib;
  Page: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('interior.pdf', '') = 1 then
      for Page := 1 to Lib.PageCount do
        Writeln(DescribeTrim(Lib, Page));
  finally
    Lib.Free;
  end;
end.

Cả GetPageBox lẫn SetPageBox đều làm việc theo thiết lập tọa độ hiện hành của tài liệu. Các ví dụ ở đây chạy với mặc định: gốc 0 (dưới-trái, khớp PDF user space) và đơn vị đo là point, nên chiều Top là cạnh trên tính từ đáy trang lên. Sau SetOrigin(1), các chiều Top và Bottom được tính từ đỉnh trang xuống, và sau SetMeasurementUnits(1) mọi giá trị trả về theo milimet. Bề rộng và chiều cao không phụ thuộc gốc

Tìm production box mắc kẹt trên các node /Pages

Kể từ v3.539.44, API box không còn nhìn thấy một TrimBox trên node /Pages, điều đúng đắn, nhưng một công cụ prepress thường muốn báo cáo tệp như vậy thay vì lặng lẽ đọc nó theo kiểu đặc tả. Các page tree node là những object thường, nên API object mức thấp tìm ra được chúng: đi các số object tới GetMaxObjectNumber, đọc từng cái bằng GetObjectToString, và soát các dictionary /Pages mang một key production box. Nửa sau của phép kiểm là phép thử từng trang mà PDF/X quan tâm, và HasPageBox giờ trả lời nó đúng cách một validator PDF/X sẽ làm, vì TrimBox trên parent không còn được tính

procedure PreflightTrim(Lib: TPDFlib; Log: TStrings);
const
  ProductionKeys: array[0..2] of string = ('/BleedBox', '/TrimBox', '/ArtBox');
var
  ObjNum, K, Page, Missing: Integer;
  Src: string;
begin
  // 1. Production box trên page tree node: phi chuẩn và bị bỏ qua
  for ObjNum := 1 to Lib.GetMaxObjectNumber do
  begin
    Src := '';                                // số trống không trả văn bản
    Src := string(Lib.GetObjectToString(ObjNum));
    if Pos('/Type /Pages', Src) = 0 then
      Continue;
    for K := Low(ProductionKeys) to High(ProductionKeys) do
      if Pos(ProductionKeys[K] + ' ', Src) > 0 then
        Log.Add(Format('object %d: %s on a /Pages node is not inheritable',
          [ObjNum, ProductionKeys[K]]));
  end;

  // 2. PDF/X: mỗi trang cần TrimBox hay ArtBox riêng
  Missing := 0;
  for Page := 1 to Lib.PageCount do
  begin
    Lib.SelectPage(Page);
    if (Lib.HasPageBox(4) = 0) and (Lib.HasPageBox(5) = 0) then
    begin
      Inc(Missing);
      Log.Add(Format('page %d: no TrimBox or ArtBox', [Page]));
    end;
  end;

  // 3. Sửa tùy chọn: trim 6 x 9 in trong media box 6.25 x 9.25 in
  //    (point, gốc dưới-trái: Left, Top, Width, Height)
  if Missing > 0 then
    Log.Add(Format('TrimBox written on %d pages',
      [Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;

Phép so khớp văn bản là một phép kiểm thực dụng, không phải một parser. Nó dựa vào việc PDFlibPas serialize mỗi entry dictionary thành một key, một dấu cách và một giá trị, điều đúng với các object đọc lại qua GetObjectToString. Bước sửa chữa xứng đáng với một quyết định chứ không phải một phản xạ: giá trị lạc trên parent hoàn toàn có thể là điều tác giả muốn, nhưng hãy đối chiếu với phiếu công việc trước khi biến nó thành chính thức. SetPageBoxRange với dải rỗng áp box cho mọi trang và trả về số trang đã cập nhật. Khi box hiện có của một trang là mảng indirect — thứ một trang khác hay một node /Pages có thể đang dùng chung — SetPageBox trao trang ấy một mảng direct mới thay vì ghi đè object dùng chung. Đặt BleedBox, TrimBox hay ArtBox còn nâng một tài liệu chưa khóa lên PDF 1.3, phiên bản đưa các entry ấy vào

Ép trang theo TrimBox bằng CapturePageEx

CapturePageEx(Page, 3) biến một trang thành Form XObject với hình chữ nhật bao là TrimBox hiệu lực của trang, còn DrawCapturedPage đặt form ấy lên một trang khác với bất kỳ cỡ nào. Kể từ v3.539.42, option 3 trên một trang không có TrimBox đưa cho bạn CropBox, đúng như tài liệu mô tả, thay vì MediaBox cùng mọi slug của nó

Hai tính chất của capture định hình code. Capture mang tính phá hủy: trang bị capture bị lấy khỏi tài liệu, và tài liệu không bao giờ được về 0 trang, nên hãy nối tờ đầu ra đầu tiên trước khi capture bất cứ thứ gì. Capture cũng chỉ hoạt động trong một tài liệu, nên hãy kéo mọi input vào cùng một tài liệu trước; các kỹ thuật xáo và đan các nguồn PDF trong một lượt áp dụng trực tiếp được

procedure ImposeTwoUp(const InFile, OutFile: string);
var
  Lib: TPDFlib;
  Captures: array of Integer;
  SourceCount, I: Integer;
  TrimW, TrimH: Double;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(InFile, '') <> 1 then
      raise Exception.Create('Cannot open ' + InFile);
    SourceCount := Lib.PageCount;

    // Cỡ trim hiệu lực của trang 1 (layout này giả sử trim đồng nhất)
    Lib.SelectPage(1);
    TrimW := Lib.GetPageBox(4, 2);
    TrimH := Lib.GetPageBox(4, 3);

    // Nối và đặt cỡ tờ đầu tiên; NewPage chọn trang mới
    Lib.NewPage;
    Lib.SetPageDimensions(2 * TrimW, TrimH);

    // Mỗi lần capture bỏ trang 1, nên trang nguồn kế trồi lên
    SetLength(Captures, SourceCount);
    for I := 0 to SourceCount - 1 do
    begin
      Captures[I] := Lib.CapturePageEx(1, 3);   // 3 = TrimBox
      if Captures[I] = 0 then
        raise Exception.CreateFmt('Capture of source page %d failed', [I + 1]);
    end;

    // Chỉ còn lại tờ giấy: hai trang trim trên mỗi tờ, nằm cạnh nhau
    Lib.SelectPage(1);
    for I := 0 to SourceCount - 1 do
    begin
      if (I > 0) and (I mod 2 = 0) then
        Lib.NewPage;                            // cùng cỡ với tờ hiện tại
      // Gốc mặc định: Top là cạnh trên, tính từ đáy
      Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
    end;
    Lib.SaveToFile(OutFile);
  finally
    Lib.Free;
  end;
end;

Một capture dựa trên trim cắt mọi thứ ngoài TrimBox, đúng điều bạn muốn cho một bản duyệt số hay một layout cắt-rồi-ép. Với tờ in sẽ bị trim sau khi in, hãy capture với option 2 để bleed sống sót, và giãn các ô theo bề rộng bleed. Vì capture lấy đi các trang nguồn, bookmark và link trỏ vào chúng mất đích, nên hãy ép vào một tệp đầu ra riêng thay vì sửa một tài liệu mà bạn vẫn cần phần điều hướng của nó; thay trang mà không phá bookmark bàn về phía phẫu thuật trang đó

Khi nguồn phải giữ nguyên vẹn, ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) nhận cùng các giá trị option 0 tới 4 (truyền Lib.SelectedDocument cho tài liệu hiện hành), giữ page tree của nguồn không đổi, chuẩn hóa góc xoay trang kế thừa vào ma trận của form, và trả về một handle mà DrawCapturedPage nhận. CapturePageEx không hoàn tác /Rotate, nên input bị xoay cần bước ấy trước, còn làm phẳng góc xoay trang mà không phá page box cho thấy từng box đổi ra sao khi bạn làm. Một lưu ý cho input có thể mang production box trên các node /Pages: đường import phân giải box của nó qua phép tra tổ tiên riêng, tách biệt với hai đường đã được canh chỉnh trong v3.539.44, nên hãy kiểm HasPageBox(4) trên trang nguồn trước và truyền option 1 (CropBox) khi nó trả về 0. Điều đó giữ kết quả bám đặc tả thay vì bám cách tệp tình cờ được viết

Tra nhanh page box

  • CropBox hiệu lực: CropBox riêng của trang, không thì CropBox kế thừa gần nhất, không thì MediaBox hiệu lực (ISO 32000-1 §14.11.2)
  • BleedBox, TrimBox và ArtBox hiệu lực: entry riêng của trang leaf, không thì CropBox hiệu lực
  • Chỉ Resources, MediaBox, CropBox và Rotate kế thừa từ node /Pages (§7.7.3.4, Bảng 30); production box trên node /Pages bị bỏ qua
  • GetPageBox(BoxType, Dimension): BoxType 1 MediaBox, 2 CropBox, 3 BleedBox, 4 TrimBox, 5 ArtBox; Dimension 0 Left, 1 Top, 2 Width, 3 Height, 4 Right, 5 Bottom
  • HasPageBox(BoxType): 0 không có box, 1 box riêng của trang (direct hay indirect), 2 MediaBox hay CropBox kế thừa (direct hay indirect)
  • CapturePageEx(Page, Options): 0 MediaBox, 1 CropBox lùi về MediaBox, 2 tới 4 BleedBox, TrimBox hay ArtBox lùi về CropBox
  • Nâng lên v3.539.44 trở lên để có mặc định và kế thừa nhất quán trên cả truy vấn box lẫn capture

Page box là chỗ những mặc định êm thầm của PDF va vào dung sai prepress tính theo phần nghìn milimet, và một thư viện hoặc áp những mặc định ấy cùng một kiểu ở mọi nơi, hoặc đưa bạn hai câu trả lời cho một câu hỏi. Toàn bộ API box, capture và Form XObject được ghi trong trang sản phẩm PDFlibPas PDF Library for Delphi