Artículo técnico

Cajas de página en PDFlibPas: TrimBox, BleedBox y CropBox

Cuando una página PDF no tiene TrimBox, su TrimBox efectivo es el CropBox de la página, y cuando tampoco hay CropBox, es el MediaBox. BleedBox y ArtBox siguen la misma regla. PDFlibPas, la PDF Library para Delphi, aplica esta cadena de defaults de forma consistente en GetPageBox, HasPageBox y CapturePageEx desde v3.539.44, e ignora las production boxes puestas sobre un nodo /Pages, porque ISO 32000-1 no las deja heredar

Eso suena a nota al pie hasta que usted arma un imposition. Imagine el interior de un libro con un MediaBox de 6.25 × 9.25 in, un CropBox puesto en el trim de 6 × 9 in, y sin TrimBox, porque quien lo exportó nunca se acordó de escribir uno. Pida el trim box y reciba el media box, y cada celda de su pliego arrastra un octavo de pulgada de bleed y slug hacia su vecina. PDFlibPas tuvo defectos justo en esta área, corregidos en v3.539.42 y v3.539.44, y la forma en que se corrigieron dice algo sobre cómo deberían implementarse las semánticas de page box en cualquier librería de PDF

¿Qué caja aplica cuando una página no tiene TrimBox?

La respuesta es una cadena de defaults fija de ISO 32000-1 §14.11.2: el CropBox hace default al MediaBox, y el BleedBox, el TrimBox y el ArtBox hacen default cada uno al CropBox. Nada excepto el CropBox hace default directo al MediaBox. Una página que define solo un MediaBox tiene entonces cinco cajas idénticas, y una que define MediaBox más CropBox tiene cuatro cajas iguales al CropBox

CajaBoxType de PDFlibPasDefault si está ausenteHeredable de /Pages
MediaBox1Ninguno, la entrada es obligatoriaSí
CropBox2MediaBoxSí
BleedBox3CropBoxNo
TrimBox4CropBoxNo
ArtBox5CropBoxNo

La cadena de dos pasos importa porque el CropBox puede a su vez estar heredado. El TrimBox efectivo de una página que no tiene ni TrimBox ni CropBox propios es el CropBox del ancestro más cercano que tenga uno, y fallando eso, el MediaBox heredado. La spec agrega una regla más que es fácil de olvidar: las cajas crop, bleed, trim y art no deberían extenderse más allá del media box, y si lo hacen, quedan efectivamente reducidas a su intersección con él. PDFlibPas reporta cada caja tal como está guardada en el archivo, así que un validador que procese input no confiable debería clampear contra el MediaBox por su cuenta

Cadena de defaults de page box de PDFlibPas donde el CropBox hace default al MediaBox y BleedBox, TrimBox y ArtBox hacen default cada uno al CropBox, dibujada junto al interior de un libro con un MediaBox de 450 por 666 puntos y un CropBox de 432 por 648 puntos que se vuelve el trim efectivo cuando no existe TrimBox
Nada salvo el CropBox hace default directo al MediaBox, así que una página con solo MediaBox tiene cinco cajas idénticas

¿Qué atributos de página puede transmitir un nodo /Pages?

Exactamente cuatro: Resources, MediaBox, CropBox y Rotate. ISO 32000-1 §7.7.3.4 define la herencia de atributos, y la Table 30 marca solo esas cuatro entradas del objeto de página como heredables. BleedBox, TrimBox y ArtBox pertenecen a la hoja. Un TrimBox escrito en un nodo /Pages no es un valor heredado; es una clave no estándar que un reader conforme ignora

Archivos no estándar así existen, típicamente con un único TrimBox en el nodo raíz del árbol de páginas como atajo de "toda página tiene este trim". El atajo se ve bien en cualquier herramienta que camine /Parent para cada clave, y ese es el problema: el archivo ahora significa dos cosas según quién lo lea. Un reader que sigue la spec no ve TrimBox y usa el CropBox, mientras que uno que hereda todo ve el valor del padre. En un pipeline de prepress esa ambigüedad termina en el pliego

Herencia del árbol de páginas en PDFlibPas donde solo Resources, MediaBox, CropBox y Rotate pasan por un nodo Pages, así que un TrimBox estacionado en el root es una clave no estándar que los readers conformes ignoran; antes de v3.539.44 dos caminos de código independientes lo heredaban y reportaban tamaños de trim distintos para un mismo documento
El archivo significa dos cosas según quién lo lea, y en un pipeline de prepress esa ambigüedad aterriza en el pliego

Los workflows PDF/X (ISO 15930) dependen del TrimBox para el tamaño final, y los perfiles PDF/X exigen que cada página declare un TrimBox o un ArtBox. Una caja estacionada en un nodo /Pages no cumple ese requisito, porque la clave nunca llega al objeto de página. El preflight debería marcar tales archivos en vez de leerlos en silencio de una u otra forma

¿Qué hizo mal PDFlibPas antes de v3.539.44?

PDFlibPas tenía tres defectos separados, todos en la brecha entre lo que dice la spec y lo que hacían dos caminos de código independientes. El primero se corrigió en v3.539.42, los otros dos en v3.539.44

Las production boxes hacían default al MediaBox durante la captura

Antes de v3.539.42, la rutina interna que prepara una página para captura (copia las entradas heredadas sobre la página y completa las cajas faltantes) le daba al BleedBox, al TrimBox y al ArtBox los valores del MediaBox cuando estaban ausentes. CapturePageEx con opciones 2 a 4 lee su rectángulo envolvente de justo esas entradas completadas, así que en una página que define solo un CropBox, pedir el trim box capturaba el media box entero. GetPageBox ya aplicaba el default del CropBox, y la referencia de CapturePageEx siempre había dicho que se usa el crop box cuando la caja pedida falta; el código de captura discrepan de ambos. Desde v3.539.42 las tres production boxes hacen default al CropBox de la página, que para ese punto ya está en la página (el suyo, copiado de un ancestro, o completado desde el MediaBox), y solo el propio CropBox recurre al MediaBox

Dos caminos de herencia, una regla semántica

El segundo defecto era la herencia no estándar en sí, y la parte sutil era que PDFlibPas resolvía cajas por dos caminos independientes. Las consultas de cajas (GetPageBox y HasPageBox) caminaban la cadena /Parent por un helper, y la captura la caminaba por un helper local separado. Ambos heredaban cada clave, production boxes incluidas. Corregir solo uno habría producido una contradicción dentro de un mismo documento: con un TrimBox de 180 puntos de ancho en el nodo /Pages y un CropBox de 380 puntos de ancho en la página, GetPageBox seguiría reportando un ancho de trim de 180 mientras CapturePageEx armaba un form de 380 de ancho. En v3.539.44 ambos caminos restringen el paseo por /Parent a las cuatro claves heredables, las production boxes se leen solo de la hoja, y la entrada perdida en el padre queda en el archivo intacta, ni borrada ni reescrita

Códigos de retorno de HasPageBox en PDFlibPas cero, uno y dos con arrays directos e indirectos contando ambos como heredados desde v3.539.44, junto a las opciones de CapturePageEx cero a cuatro donde BleedBox, TrimBox y ArtBox recurren al CropBox en vez del MediaBox desde v3.539.42
Dos puntos de entrada de implementación para una regla de spec se corrigen juntos y se testean como una matriz de 18 escenarios, con query y captura concordando en cada archivo

HasPageBox se perdía los arrays directos del padre

HasPageBox devuelve 0 cuando la página no tiene una caja del tipo pedido, 1 cuando tiene su propia caja (guardada directo o por una referencia indirecta), y 2 cuando un MediaBox o CropBox está heredado de un ancestro. El código viejo devolvía 2 solo cuando el valor heredado era una referencia indirecta, así que un array directo heredado devolvía 0. La corrección separa el desreferenciar del test de array, y ambas representaciones ahora devuelven 2. Desde v3.539.44, HasPageBox para un BleedBox, TrimBox o ArtBox solo puede devolver 0 o 1

La lección se generaliza mucho más allá de las cajas de página. Cuando una pieza de semántica de spec tiene dos puntos de entrada de implementación en una librería, corrijalos juntos y testéelos como una matriz en vez de con un solo archivo happy-path. El conjunto de regresión de PDFlibPas cruza dos representaciones de caja del padre (array directo e indirecto) con tres estados de hoja (ausente, array directo, array indirecto) y tres opciones de captura (bleed, trim, art), dando 18 escenarios, y cada uno chequea el resultado de la consulta, los límites capturados, la herencia legítima de MediaBox y CropBox, y la entrada del padre intacta

¿Cómo leo el TrimBox efectivo en Delphi?

Llame a GetPageBox(4, Dimension) sobre la página seleccionada. PDFlibPas aplica la cadena de defaults por usted, así que el resultado es el TrimBox efectivo tenga o no la página uno. Emparéjelo con HasPageBox cuando necesite saber de dónde vino el valor, cosa que un reporte de preflight normalmente quiere

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 = propia, 2 = heredada
    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.

Tanto GetPageBox como SetPageBox trabajan en los ajustes de coordenadas actuales del documento. Los ejemplos de aquí corren con los defaults: origen 0 (abajo a la izquierda, coincidiendo con el user space de PDF) y puntos como unidad de medida, así que la dimensión Top es el borde superior medido hacia arriba desde el fondo de la página. Después de SetOrigin(1) las dimensiones Top y Bottom se miden hacia abajo desde el tope de la página, y después de SetMeasurementUnits(1) cada valor vuelve en milímetros. Ancho y alto no dependen del origen

Encontrar production boxes varadas en nodos /Pages

Desde v3.539.44 la API de cajas ya no ve un TrimBox en un nodo /Pages, lo que es correcto, pero una herramienta de preflight normalmente quiere reportar tal archivo en vez de leerlo en silencio a la manera de la spec. Los nodos del árbol de páginas son objetos ordinarios, así que la API de objetos de bajo nivel puede encontrarlos: recorra los números de objeto hasta GetMaxObjectNumber, lea cada uno con GetObjectToString, y busque un diccionario /Pages que lleve una clave de production box. La segunda mitad del chequeo es el test por página que le importa a PDF/X, y HasPageBox ahora lo responde como lo respondería un validador PDF/X, porque un TrimBox del padre ya no cuenta

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 boxes en nodos del árbol de páginas: no estándar y se ignoran
  for ObjNum := 1 to Lib.GetMaxObjectNumber do
  begin
    Src := '';                                // los números libres no devuelven texto
    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: cada página necesita su propio TrimBox o ArtBox
  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. Reparación opcional: un trim de 6 x 9 in dentro de un media box de 6.25 x 9.25 in
  //    (puntos, origen abajo a la izquierda: Left, Top, Width, Height)
  if Missing > 0 then
    Log.Add(Format('TrimBox written on %d pages',
      [Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;

El match de texto es un chequeo pragmático, no un parser. Depende de que PDFlibPas serialice cada entrada de diccionario como una clave, un espacio y un valor, lo que se sostiene para objetos leídos de vuelta por GetObjectToString. El paso de reparación merece una decisión en vez de un reflejo: el valor perdido del padre bien puede ser lo que el autor pretendía, pero confírmelo contra el ticket del trabajo antes de volverlo oficial. SetPageBoxRange con un rango vacío aplica la caja a todas las páginas y devuelve el número de páginas actualizadas. Cuando la caja existente de una página es un array indirecto, que otra página o un nodo /Pages puede compartir, SetPageBox le da a esa página un array directo nuevo en vez de reescribir el objeto compartido. Poner un BleedBox, TrimBox o ArtBox además sube un documento desbloqueado a PDF 1.3, la versión que introdujo esas entradas

Imponer páginas sobre el TrimBox con CapturePageEx

CapturePageEx(Page, 3) convierte una página en un Form XObject cuya caja envolvente es el TrimBox efectivo de la página, y DrawCapturedPage coloca ese form sobre otra página a cualquier tamaño. Desde v3.539.42, la opción 3 sobre una página sin TrimBox le da el CropBox, como describe la referencia, en vez del MediaBox con todo su slug

Dos propiedades de la captura moldean el código. La captura es destructiva: la página capturada se elimina del documento, y el documento nunca puede bajar a cero páginas, así que agregue la primera hoja de salida antes de capturar cualquier cosa. La captura además funciona solo dentro de un documento, así que jale cada input a un solo documento primero; las técnicas para colacionar e intercalar fuentes PDF en una sola pasada aplican directo

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;

    // Tamaño de trim efectivo de la página 1 (este layout asume un trim uniforme)
    Lib.SelectPage(1);
    TrimW := Lib.GetPageBox(4, 2);
    TrimH := Lib.GetPageBox(4, 3);

    // Agregue y dimensione la primera hoja; NewPage selecciona la página nueva
    Lib.NewPage;
    Lib.SetPageDimensions(2 * TrimW, TrimH);

    // Cada captura elimina la página 1, así que la siguiente página fuente sube
    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;

    // Solo queda la hoja: dos páginas con trim por hoja, lado a lado
    Lib.SelectPage(1);
    for I := 0 to SourceCount - 1 do
    begin
      if (I > 0) and (I mod 2 = 0) then
        Lib.NewPage;                            // mismo tamaño que la hoja actual
      // Origen por defecto: Top es el borde superior, medido desde abajo
      Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
    end;
    Lib.SaveToFile(OutFile);
  finally
    Lib.Free;
  end;
end;

Una captura basada en trim recorta todo lo que queda fuera del TrimBox, que es lo que usted quiere para una prueba digital o un layout cut-and-stack. Para un pliego que se recorta después de imprimir, capture con la opción 2 así el bleed sobrevive, y separe las celdas por el ancho del bleed. Como la captura elimina las páginas fuente, los bookmarks y links que apuntaban a ellas pierden sus targets, así que imponga a un archivo de salida separado en vez de editar un documento cuya navegación todavía necesite; reemplazar páginas sin romper bookmarks cubre ese lado de la cirugía de páginas

Cuando el fuente tiene que quedar intacto, ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) toma los mismos valores de opción 0 a 4 (pase Lib.SelectedDocument para el documento actual), deja el árbol de páginas del fuente sin cambios, normaliza la rotación de página heredada en la matriz del form, y devuelve un handle que DrawCapturedPage acepta. CapturePageEx no deshace el /Rotate, así que el input rotado necesita ese paso primero, y aplanar la rotación de página sin romper las cajas de página muestra qué le pasa a cada caja cuando lo hace. Una advertencia para inputs que pueden traer production boxes en nodos /Pages: el camino de import resuelve su caja por su propia búsqueda de ancestros, separada de los dos caminos alineados en v3.539.44, así que chequee HasPageBox(4) en la página fuente primero y pase la opción 1 (CropBox) cuando devuelva 0. Eso mantiene el resultado atado a la spec y no a cómo le tocó ser escrito al archivo

Referencia rápida de cajas de página

  • CropBox efectivo: el CropBox propio de la página, si no el CropBox heredado más cercano, si no el MediaBox efectivo (ISO 32000-1 §14.11.2)
  • BleedBox, TrimBox y ArtBox efectivos: la entrada propia de la hoja, si no el CropBox efectivo
  • Solo Resources, MediaBox, CropBox y Rotate heredan de nodos /Pages (§7.7.3.4, Table 30); las production boxes en nodos /Pages se ignoran
  • 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 sin caja, 1 la caja propia de la página (directa o indirecta), 2 un MediaBox o CropBox heredado (directo o indirecto)
  • CapturePageEx(Page, Options): 0 MediaBox, 1 CropBox con fallback al MediaBox, 2 a 4 BleedBox, TrimBox o ArtBox con fallback al CropBox
  • Actualice a v3.539.44 o posterior para defaults y herencia consistentes entre consultas de cajas y captura

Las cajas de página son donde los defaults silenciosos de PDF se topan con tolerancias de prepress medidas en fracciones de milímetro, y una librería o aplica esos defaults igual en todos lados o le entrega dos respuestas a una pregunta. La API completa de cajas, captura y Form XObject está documentada en la página del producto PDFlibPas PDF Library for Delphi