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
| Caja | BoxType de PDFlibPas | Default si está ausente | Heredable de /Pages |
|---|---|---|---|
| MediaBox | 1 | Ninguno, la entrada es obligatoria | Sí |
| CropBox | 2 | MediaBox | Sí |
| BleedBox | 3 | CropBox | No |
| TrimBox | 4 | CropBox | No |
| ArtBox | 5 | CropBox | No |
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
¿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
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
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,CropBoxyRotateheredan de nodos/Pages(§7.7.3.4, Table 30); las production boxes en nodos/Pagesse 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 BottomHasPageBox(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