Quando uma página de PDF não tem TrimBox, o TrimBox efetivo dela é o CropBox da página, e quando o CropBox também falta, é o MediaBox. BleedBox e ArtBox seguem a mesma regra. O PDFlibPas, a PDF Library para Delphi, aplica essa cadeia de defaults de forma consistente em GetPageBox, HasPageBox e CapturePageEx desde a v3.539.44, e ignora production boxes colocadas num node /Pages, porque a ISO 32000-1 não as deixa herdar
Isso parece uma nota de rodapé até você fazer imposition de um job. Imagine o miolo de um livro com MediaBox de 6.25 × 9.25 in, um CropBox setado para o trim de 6 × 9 in, e sem TrimBox, porque quem exportou nunca pensou em escrever um. Peça a trim box, receba a media box, e toda célula da sua folha de impressão arrasta um oitavo de polegada de bleed e slug para cima do vizinho. O PDFlibPas tinha defeitos exatamente nessa área, consertados na v3.539.42 e na v3.539.44, e o jeito como foram consertados diz algo sobre como a semântica de page boxes deve ser implementada em qualquer biblioteca de PDF
Qual caixa vale quando uma página não tem TrimBox?
A resposta é uma cadeia de defaults fixa da ISO 32000-1 §14.11.2: o CropBox tem o MediaBox como default, e BleedBox, TrimBox e ArtBox têm cada uma o CropBox como default. Nada além do CropBox tem o MediaBox como default direto. Uma página que define só um MediaBox tem portanto cinco caixas idênticas, e uma página que define um MediaBox mais um CropBox tem quatro caixas iguais ao CropBox
| Box | BoxType do PDFlibPas | Default quando ausente | Herdável de /Pages |
|---|---|---|---|
| MediaBox | 1 | Nenhum, a entrada é obrigatória | Sim |
| CropBox | 2 | MediaBox | Sim |
| BleedBox | 3 | CropBox | Não |
| TrimBox | 4 | CropBox | Não |
| ArtBox | 5 | CropBox | Não |
A cadeia de dois passos importa porque o CropBox pode ele mesmo ser herdado. O TrimBox efetivo de uma página que não tem nem TrimBox nem CropBox próprios é o CropBox do ancestral mais próximo que tenha um, e, na falta desse, o MediaBox herdado. A spec acrescenta mais uma regra fácil de esquecer: as caixas crop, bleed, trim e art não devem se estender além da media box, e se o fizerem, são efetivamente reduzidas à interseção delas com ela. O PDFlibPas reporta cada caixa como guardada no arquivo, então um validator que lida com input não confiável deve clampear contra o MediaBox por conta própria
Quais atributos de página um node /Pages pode passar para baixo?
Exatamente quatro: Resources, MediaBox, CropBox e Rotate. A ISO 32000-1 §7.7.3.4 define a herança de atributos, e a Table 30 marca só essas quatro entradas de page object como herdáveis. BleedBox, TrimBox e ArtBox pertencem à página folha. Um TrimBox escrito num node /Pages não é um valor herdado; é uma chave não padrão que um reader conformante ignora
Arquivos não padrão assim existem, tipicamente com um único TrimBox no node raiz da page tree como abreviação de "toda página tem esse trim". A abreviação parece certa em qualquer tool que caminha pelo /Parent para cada chave, e esse é o problema: o arquivo agora significa duas coisas dependendo de quem lê. Um reader que segue a spec não vê TrimBox e usa o CropBox, enquanto um reader que herda tudo vê o valor do pai. Num pipeline de prepress essa ambiguidade acaba na folha de impressão
Workflows PDF/X (ISO 15930) dependem do TrimBox para o tamanho final, e os perfis PDF/X exigem que cada página declare um TrimBox ou um ArtBox. Uma caixa estacionada num node /Pages não atende esse requisito, porque a chave nunca chega ao page object. Preflight deveria sinalizar tais arquivos em vez de lê-los silenciosamente de um jeito ou de outro
O que o PDFlibPas fazia de errado antes da v3.539.44?
O PDFlibPas tinha três defeitos separados, todos na lacuna entre o que a spec diz e o que dois caminhos de código independentes faziam. O primeiro foi consertado na v3.539.42, os outros dois na v3.539.44
Production boxes tinham o MediaBox como default durante a captura
Antes da v3.539.42, a rotina interna que prepara uma página para captura (ela copia entradas herdadas para a página e preenche caixas faltantes) dava às caixas BleedBox, TrimBox e ArtBox os valores do MediaBox quando elas estavam ausentes. O CapturePageEx com opções 2 a 4 lê o retângulo envolvente dele de exatamente essas entradas preenchidas, então numa página que define só um CropBox, pedir a trim box capturava a media box inteira. O GetPageBox já aplicava o default do CropBox, e a referência do CapturePageEx sempre tinha dito que a crop box é usada quando a caixa pedida falta; o código de captura discordava de ambos. Desde a v3.539.42 as três production boxes têm o CropBox da página como default, que nesse ponto já está na página (o próprio, copiado de um ancestral, ou preenchido a partir do MediaBox), e só o próprio CropBox cai para o MediaBox
Dois caminhos de herança, uma regra semântica
O segundo defeito era a própria herança não padrão, e a parte sutil era que o PDFlibPas resolvia caixas ao longo de dois caminhos independentes. As consultas de caixa (GetPageBox e HasPageBox) caminhavam pela cadeia /Parent através de um helper, e a captura caminhava por um helper local separado. Ambos herdavam toda chave, production boxes incluídas. Consertar só um deles teria produzido uma contradição dentro de um único documento: com um TrimBox de 180 pontos de largura no node /Pages e um CropBox de 380 pontos de largura na página, o GetPageBox ainda reportaria uma largura de trim de 180 enquanto o CapturePageEx construía um form de 380 de largura. Na v3.539.44 ambos os caminhos restringem a caminhada pelo /Parent às quatro chaves herdáveis, production boxes são lidas só da folha, e a entrada perdida no pai fica no arquivo intacta, nem deletada nem reescrita
HasPageBox perdia arrays diretos do pai
O HasPageBox retorna 0 quando a página não tem caixa do tipo pedido, 1 quando a página tem a caixa própria (guardada diretamente ou através de uma referência indireta), e 2 quando um MediaBox ou CropBox é herdado de um ancestral. O código antigo retornava 2 só quando o valor herdado era uma referência indireta, então um array direto herdado retornava 0. A correção separa desreferenciar do teste de array, e ambas as representações agora retornam 2. Desde a v3.539.44, o HasPageBox para uma BleedBox, TrimBox ou ArtBox só pode retornar 0 ou 1
A lição se generaliza bem além de page boxes. Quando uma fatia de semântica de spec tem dois pontos de entrada de implementação numa biblioteca, conserte-os juntos e teste-os como uma matriz em vez de com um único arquivo happy-path. O conjunto de regressão do PDFlibPas cruza duas representações de caixa do pai (array direto e indireto) com três estados de folha (ausente, array direto, array indireto) e três opções de captura (bleed, trim, art), dando 18 cenários, e cada um confere o resultado da query, os bounds capturados, a herança legítima de MediaBox e CropBox, e a entrada do pai intacta
Como leio o TrimBox efetivo em Delphi?
Chame GetPageBox(4, Dimension) na página selecionada. O PDFlibPas aplica a cadeia de defaults por você, então o resultado é o TrimBox efetivo haja ou não um na página. Combine com o HasPageBox quando precisar saber de onde veio o valor, o que um relatório de preflight costuma 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 = própria, 2 = herdada
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 o GetPageBox quanto o SetPageBox trabalham nas configurações de coordenadas atuais do documento. Os exemplos aqui rodam com os defaults: origem 0 (canto inferior esquerdo, igual ao user space do PDF) e pontos como unidade de medida, então a dimensão Top é a borda superior medida para cima a partir do fundo da página. Depois de SetOrigin(1) as dimensões Top e Bottom são medidas para baixo a partir do topo da página, e depois de SetMeasurementUnits(1) todo valor volta em milímetros. Largura e altura não dependem da origem
Achando production boxes abandonadas em nodes /Pages
Desde a v3.539.44 a API de caixas não vê mais um TrimBox num node /Pages, o que é correto, mas uma tool de preflight geralmente quer reportar tal arquivo em vez de lê-lo silenciosamente do jeito da spec. Nodes da page tree são objetos ordinários, então a API de objetos de baixo nível pode achá-los: caminhe pelos números de objeto até GetMaxObjectNumber, leia cada um com GetObjectToString, e procure um dicionário /Pages que carregue uma chave de production box. A segunda metade da checagem é o teste por página de que o PDF/X gosta, e o HasPageBox agora responde como um validator PDF/X responderia, porque um TrimBox no pai não conta mais
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 em nodes da page tree: não padrão e ignoradas
for ObjNum := 1 to Lib.GetMaxObjectNumber do
begin
Src := ''; // números livres não retornam 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: toda página precisa do próprio TrimBox ou 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. Reparo opcional: um trim de 6 x 9 in dentro de uma media box de 6.25 x 9.25 in
// (pontos, origem inferior esquerda: Left, Top, Width, Height)
if Missing > 0 then
Log.Add(Format('TrimBox written on %d pages',
[Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;
O match de texto é uma checagem pragmática, não um parser. Ele depende do PDFlibPas serializar cada entrada de dicionário como uma chave, um espaço e um valor, o que vale para objetos lidos de volta através do GetObjectToString. O passo de reparo merece uma decisão em vez de um reflexo: o valor perdido no pai pode muito bem ser o que o autor pretendia, mas confirme contra a ordem de serviço antes de oficializar. O SetPageBoxRange com uma range vazia aplica a caixa a toda página e retorna o número de páginas atualizadas. Quando a caixa existente de uma página é um array indireto, que outra página ou um node /Pages pode compartilhar, o SetPageBox dá a essa página um array direto novo em vez de reescrever o objeto compartilhado. Setar uma BleedBox, TrimBox ou ArtBox também eleva um documento destravado ao PDF 1.3, a versão que introduziu essas entradas
Impondo páginas no TrimBox com CapturePageEx
O CapturePageEx(Page, 3) transforma uma página num Form XObject cuja caixa envolvente é o TrimBox efetivo da página, e o DrawCapturedPage coloca esse form em outra página em qualquer tamanho. Desde a v3.539.42, a opção 3 numa página sem TrimBox te dá o CropBox, como a referência descreve, em vez do MediaBox com todo o slug dele
Duas propriedades da captura moldam o código. A captura é destrutiva: a página capturada é removida do documento, e o documento nunca pode cair a zero páginas, então anexe a primeira folha de saída antes de capturar qualquer coisa. A captura também funciona dentro de um documento só, então puxe todo input para um único documento primeiro; as técnicas de colar e intercalar fontes de PDF numa passada se aplicam direto
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;
// Tamanho de trim efetivo da página 1 (este layout assume um trim uniforme)
Lib.SelectPage(1);
TrimW := Lib.GetPageBox(4, 2);
TrimH := Lib.GetPageBox(4, 3);
// Anexe e dimensione a primeira folha; NewPage seleciona a página nova
Lib.NewPage;
Lib.SetPageDimensions(2 * TrimW, TrimH);
// Cada captura remove a página 1, então a próxima página de origem sobe
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;
// Só resta a folha: duas páginas aparadas por folha, 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; // mesmo tamanho da folha atual
// Origem default: Top é a borda superior, medida a partir de baixo
Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
end;
Lib.SaveToFile(OutFile);
finally
Lib.Free;
end;
end;
Uma captura baseada em trim recorta tudo fora do TrimBox, que é o que você quer para uma prova digital ou um layout cut-and-stack. Para uma folha de impressão que será aparada depois de imprimir, capture com a opção 2 para o bleed sobreviver, e espace as células pela largura do bleed. Como a captura remove as páginas de origem, bookmarks e links que apontavam para elas perdem os alvos deles, então imponha num arquivo de saída separado em vez de editar um documento cuja navegação você ainda precisa; substituir páginas sem quebrar bookmarks cobre esse lado da cirurgia de páginas
Quando a origem precisa ficar intacta, o ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) recebe os mesmos valores de opção 0 a 4 (passe Lib.SelectedDocument para o documento atual), deixa a page tree de origem inalterada, normaliza a rotação de página herdada na matriz do form, e retorna um handle que o DrawCapturedPage aceita. O CapturePageEx não desfaz /Rotate, então input rotacionado precisa desse passo primeiro, e achatar rotação de página sem quebrar page boxes mostra o que acontece com cada caixa quando você faz isso. Uma cautela para inputs que podem carregar production boxes em nodes /Pages: o caminho de import resolve a caixa dele através do próprio lookup de ancestrais dele, separado dos dois caminhos alinhados na v3.539.44, então confira HasPageBox(4) na página de origem primeiro e passe a opção 1 (CropBox) quando ela retornar 0. Isso mantém o resultado amarrado à spec em vez de ao jeito como o arquivo por acaso foi escrito
Referência rápida de page boxes
- CropBox efetivo: o CropBox próprio da página, senão o CropBox herdado mais próximo, senão o MediaBox efetivo (ISO 32000-1 §14.11.2)
- BleedBox, TrimBox e ArtBox efetivos: a entrada própria da página folha, senão o CropBox efetivo
- Só
Resources,MediaBox,CropBoxeRotateherdam de nodes/Pages(§7.7.3.4, Table 30); production boxes em nodes/Pagessão ignoradas 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 sem caixa, 1 a caixa própria da página (direta ou indireta), 2 um MediaBox ou CropBox herdado (direto ou indireto)CapturePageEx(Page, Options): 0 MediaBox, 1 CropBox com fallback para MediaBox, 2 a 4 BleedBox, TrimBox ou ArtBox com fallback para CropBox- Faça upgrade para a v3.539.44 ou posterior para defaults e herança consistentes entre consultas de caixa e captura
Page boxes são onde os defaults silenciosos do PDF encontram tolerâncias de prepress medidas em frações de milímetro, e uma biblioteca ou aplica esses defaults do mesmo jeito em todo lugar ou te entrega duas respostas para uma pergunta. A API completa de caixas, captura e Form XObject está documentada na página de produto do PDFlibPas PDF Library para Delphi