Artículo técnico

Cajas de página 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 también falta el CropBox, es el MediaBox. BleedBox y ArtBox siguen la misma regla. PDFlibPas, la PDF Library para Delphi, aplica esta cadena por defecto de forma consistente en GetPageBox, HasPageBox y CapturePageEx desde v3.539.44, e ignora las cajas de producción colocadas en un nodo /Pages, porque ISO 32000-1 no deja que hereden

Eso suena a nota al pie hasta que usted impone un trabajo. Imagine un interior de libro con un MediaBox de 6.25 × 9.25 pulg, un CropBox fijado al trim de 6 × 9 pulg, y sin TrimBox, porque quien lo exportó nunca se le ocurrió escribir uno. Pida la trim box y reciba la media box, y cada celda de su plancha de imprenta arrastra un octavo de pulgada de bleed y slug hacia su vecina. PDFlibPas tenía defectos justo en esta zona, arreglados en v3.539.42 y v3.539.44, y la manera de arreglarlos dice algo sobre cómo deberían implementarse las semánticas de page box en cualquier biblioteca PDF

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

La respuesta es una cadena de valores por defecto fija de ISO 32000-1 §14.11.2: el CropBox toma por defecto el MediaBox, y BleedBox, TrimBox y ArtBox toman cada uno por defecto el CropBox. Nada excepto el CropBox toma por defecto directamente el MediaBox. Una página que solo define un MediaBox tiene por tanto cinco cajas idénticas, y una que define MediaBox más CropBox tiene cuatro cajas iguales al CropBox

CajaBoxType de PDFlibPasPor defecto si faltaHeredable de /Pages
MediaBox1Ninguna, la entrada es obligatoriaSí
CropBox2MediaBoxSí
BleedBox3CropBoxNo
TrimBox4CropBoxNo
ArtBox5CropBoxNo

La cadena en dos pasos importa porque el CropBox puede estar a su vez 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 lo tenga, y en su defecto, el MediaBox heredado. La especificación añade una regla más que se olvida con facilidad: las cajas crop, bleed, trim y art no deberían extenderse más allá del media box, y si lo hacen, quedan en la práctica reducidas a su intersección con él. PDFlibPas reporta cada caja tal como está en el archivo, así que un validador que maneje entrada no fiable debería clampear contra el MediaBox por su cuenta

Cadena por defecto de page box en PDFlibPas donde el CropBox toma por defecto el MediaBox y BleedBox, TrimBox y ArtBox toman cada uno por defecto el CropBox, dibujada junto a un interior de libro con un MediaBox de 450 por 666 puntos y un CropBox de 432 por 648 puntos que se convierte en el trim efectivo cuando no existe TrimBox
Nada salvo el CropBox toma por defecto directamente el 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 tabla 30 marca solo esas cuatro entradas del objeto de página como heredables. BleedBox, TrimBox y ArtBox pertenecen a la página hoja. Un TrimBox escrito en un nodo /Pages no es un valor heredado; es una clave no estándar que un lector conforme ignora

Archivos no estándar así existen, normalmente con un único TrimBox en el nodo raíz del árbol de páginas a modo de atajo de "todas las páginas tienen este trim". El atajo parece correcto en cualquier herramienta que recorra /Parent para cada clave, y ahí está el problema: el archivo ahora significa dos cosas según quién lo lea. Un lector que sigue la especificación no ve TrimBox y usa el CropBox, mientras que uno que hereda todo ve el valor del padre. En un pipeline de preprensa esa ambigüedad acaba en la plancha

Herencia del árbol de páginas en PDFlibPas donde solo Resources, MediaBox, CropBox y Rotate bajan por un nodo Pages, así que un TrimBox aparcado en la raíz es una clave no estándar que los lectores 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 preprensa esa ambigüedad aterriza en la plancha

Los flujos PDF/X (ISO 15930) dependen del TrimBox para el tamaño acabado, y los perfiles PDF/X exigen que cada página declare un TrimBox o un ArtBox. Una caja aparcada 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 lugar de leerlos calladamente de una manera o de otra

¿Qué se le escapaba a PDFlibPas antes de v3.539.44?

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

Las cajas de producción tomaban el MediaBox por defecto 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 rellena las cajas ausentes) daba a BleedBox, TrimBox y ArtBox los valores del MediaBox cuando faltaban. CapturePageEx con opciones 2 a 4 lee su rectángulo envolvente de exactamente esas entradas rellenadas, así que en una página que solo define un CropBox, pedir la trim box capturaba el media box entero. GetPageBox ya aplicaba el valor por defecto 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 discrepaba de ambos. Desde v3.539.42 las tres cajas de producción toman por defecto el CropBox de la página, que a esas alturas ya está sobre la página (el propio, copiado de un ancestro, o rellenado desde el MediaBox), y solo el CropBox recae en el MediaBox

Dos caminos de herencia, una regla semántica

El segundo defecto era la propia herencia no estándar, y la parte sutil era que PDFlibPas resolvía las cajas por dos caminos independientes. Las consultas de caja (GetPageBox y HasPageBox) recorrían la cadena /Parent a través de un ayudante, y la captura lo hacía a través de otro ayudante local aparte. Ambos heredaban cada clave, cajas de producción incluidas. Arreglar 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 en la página, GetPageBox seguiría reportando un ancho de trim de 180 mientras CapturePageEx construía un formulario de 380 de ancho. En v3.539.44 ambos caminos restringen el recorrido de /Parent a las cuatro claves heredables, las cajas de producción se leen solo de la hoja, y la entrada huérfana del padre se queda en el archivo intacta, sin borrar ni reescribir

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 cero a cuatro de CapturePageEx donde BleedBox, TrimBox y ArtBox recaen en el CropBox en lugar del MediaBox desde v3.539.42
Dos puntos de entrada de implementación para una regla de especificación se arreglan juntos y se testean como una matriz de 18 escenarios, con consulta y captura concordando en cada archivo

HasPageBox no veía los arrays directos del padre

HasPageBox devuelve 0 cuando la página no tiene ninguna caja del tipo pedido, 1 cuando tiene una caja propia (guardada directamente o vía referencia indirecta), y 2 cuando un MediaBox o CropBox está heredado de un ancestro. El código antiguo devolvía 2 solo cuando el valor heredado era una referencia indirecta, así que un array directo heredado devolvía 0. El arreglo separa el desreferenciado del test de array, y ambas representaciones devuelven ahora 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 especificación tiene dos puntos de entrada de implementación en una biblioteca, arréglelos juntos y testéelos como una matriz en lugar de con un archivo de camino feliz. 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), lo que da 18 escenarios, y cada uno comprueba el resultado de la consulta, las cotas capturadas, 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 por defecto 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 informe de preflight suele querer

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 = propio, 2 = heredado
    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 la configuración de coordenadas en vigor del documento. Los ejemplos de aquí corren con los valores por defecto: origen 0 (abajo a la izquierda, igual que el user space de PDF) y puntos como unidad de medida, así que la dimensión Top es el borde superior medido desde abajo de la página. Tras SetOrigin(1) las dimensiones Top y Bottom se miden desde arriba de la página, y tras SetMeasurementUnits(1) cada valor vuelve en milímetros. Ancho y alto no dependen del origen

Encontrar cajas de producción abandonadas en nodos /Pages

Desde v3.539.44 la API de cajas ya no ve un TrimBox en un nodo /Pages, lo cual es lo correcto, pero una herramienta de preflight normalmente quiere reportar semejante archivo en lugar de leerlo calladamente a la manera de la especificación. Los nodos del árbol de páginas son objetos corrientes, 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 caja de producción. La segunda mitad del check es el test por página que le importa a PDF/X, y HasPageBox lo responde ahora 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. Cajas de producción en nodos del árbol de páginas: no estándar e ignoradas
  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 pulg dentro de un media box de 6.25 x 9.25 pulg
  //    (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 check pragmático, no un parser. Se apoya en que PDFlibPas serializa cada entrada de diccionario como clave, un espacio y un valor, cosa que se cumple en los objetos leídos de vuelta por GetObjectToString. El paso de reparación merece una decisión y no un reflejo: el valor huérfano del padre bien puede ser lo que el autor pretendía, pero confírmelo contra el parte de trabajo antes de hacerlo 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 lugar de reescribir el objeto compartido. Poner un BleedBox, TrimBox o ArtBox eleva además un documento sin bloquear a PDF 1.3, la versión que introdujo esas entradas

Imponer páginas al 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 formulario en otra página al tamaño que sea. Desde v3.539.42, la opción 3 sobre una página sin TrimBox le da el CropBox, como describe la referencia, en lugar del MediaBox con todo su slug

Dos propiedades de la captura moldean el código. La captura es destructiva: la página capturada se retira del documento, y el documento nunca puede bajar a cero páginas, así que añada la primera plancha de salida antes de capturar nada. La captura además solo funciona dentro de un documento, así que junte primero todas las entradas en un único documento; las técnicas de intercalar y entrelazar fuentes PDF en una sola pasada aplican directamente

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 (esta imposición asume un trim uniforme)
    Lib.SelectPage(1);
    TrimW := Lib.GetPageBox(4, 2);
    TrimH := Lib.GetPageBox(4, 3);

    // Añada y dimensione la primera plancha; 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 plancha: dos páginas recortadas por plancha, 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 plancha en curso
      // 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 el trim recorta todo lo que queda fuera del TrimBox, que es lo que usted quiere para una prueba digital o una imposición de corte y apilado. Para una plancha que se recorta tras imprimir, capture con la opción 2 para que el bleed sobreviva, y separe las celdas por el ancho del bleed. Como la captura retira las páginas fuente, los marcadores y enlaces que apuntaban a ellas pierden su destino, así que imponga a un archivo de salida separado en lugar de editar un documento cuya navegación aún necesita; reemplazar páginas sin romper marcadores 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 de 0 a 4 (pase Lib.SelectedDocument para el documento en curso), deja el árbol de páginas fuente sin tocar, normaliza la rotación de página heredada dentro de la matriz del formulario, y devuelve un handle que DrawCapturedPage acepta. CapturePageEx no deshace /Rotate, así que la entrada rotada 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 cautela para entradas que puedan llevar cajas de producción en nodos /Pages: el camino de importación resuelve su caja a través de su propia búsqueda de ancestros, separada de los dos caminos alineados en v3.539.44, así que compruebe 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 especificación y no a cómo le dio por escribir el 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 página hoja; si no, el CropBox efectivo
  • Solo Resources, MediaBox, CropBox y Rotate heredan de nodos /Pages (§7.7.3.4, tabla 30); las cajas de producción 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 recaída en MediaBox, 2 a 4 BleedBox, TrimBox o ArtBox con recaída en CropBox
  • Actualice a v3.539.44 o posterior para unos valores por defecto y una herencia consistentes entre consultas de caja y captura

Las cajas de página son donde los valores por defecto silenciosos de PDF se cruzan con tolerancias de preprensa medidas en fracciones de milímetro, y una biblioteca o aplica esos valores por defecto igual en todas partes o le entrega dos respuestas a una pregunta. La API completa de cajas, captura y Form XObject está documentada en la página de producto de PDFlibPas PDF Library for Delphi