Quando uma página PDF não tem TrimBox, o seu TrimBox efetivo é o CropBox da página, e quando também falta o CropBox, é o MediaBox. BleedBox e ArtBox seguem a mesma regra. O PDFlibPas, a PDF Library para Delphi, aplica esta cadeia de predefinições de forma consistente no GetPageBox, no HasPageBox e no CapturePageEx desde a v3.539.44, e ignora caixas de produção colocadas num nó /Pages, porque a ISO 32000-1 não as deixa herdar
Isto parece uma nota de rodapé até preparar uma imposição. Imagine um miolo de livro com um MediaBox de 6,25 × 9,25 in, um CropBox posto no trim de 6 × 9 in, e nenhum TrimBox, porque quem o exportou nunca pensou em escrever um. Peça a caixa de trim, receba a media box em vez disso, e cada célula da sua folha de impressão arrasta um oitavo de polegada de bleed e slug para o vizinho. O PDFlibPas tinha defeitos exatamente nesta área, corrigidos na v3.539.42 e na v3.539.44, e a forma como foram corrigidos diz algo sobre como a semântica de caixas de página deve ser implementada em qualquer biblioteca PDF
Que caixa se aplica quando uma página não tem TrimBox?
A resposta é uma cadeia de predefinições fixa da ISO 32000-1 §14.11.2: o CropBox por omissão é o MediaBox, e a BleedBox, a TrimBox e a ArtBox têm cada uma por omissão o CropBox. Nada além do CropBox tem por omissão diretamente o MediaBox. Uma página que defina só um MediaBox tem por isso cinco caixas idênticas, e uma página que defina um MediaBox mais um CropBox tem quatro caixas iguais ao CropBox
| Caixa | BoxType do PDFlibPas | Predefinição quando ausente | Herdável de /Pages |
|---|---|---|---|
| MediaBox | 1 | Nenhuma, 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 em dois passos interessa porque o CropBox pode ele próprio ser herdado. O TrimBox efetivo de uma página sem TrimBox nem CropBox próprios é o CropBox do ancestral mais próximo que o tenha, e na falta dele, o MediaBox herdado. A norma acrescenta mais uma regra fácil de esquecer: as caixas crop, bleed, trim e art não devem ultrapassar a media box, e se o fizerem, são efetivamente reduzidas à sua interseção com ela. O PDFlibPas reporta cada caixa como está guardada no ficheiro, por isso um validador que trate input não confiável deve amarrá-las ao próprio MediaBox
Que atributos de página pode um nó /Pages transmitir?
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 do objeto de página como herdáveis. BleedBox, TrimBox e ArtBox pertencem à página folha. Um TrimBox escrito num nó /Pages não é um valor herdado; é uma chave não padrão que um leitor conforme ignora
Ficheiros não padrão assim existem, tipicamente com um único TrimBox no nó raiz da árvore de páginas como abreviatura para "todas as páginas têm este trim". A abreviatura parece certa em qualquer ferramenta que percorra o /Parent para cada chave, e é esse o problema: o ficheiro agora significa duas coisas consoante quem o lê. Um leitor que siga a norma não vê TrimBox e usa o CropBox, enquanto um leitor que herde tudo vê o valor do pai. Numa pipeline de pré-impressão essa ambiguidade acaba na folha de impressão
Os fluxos de trabalho PDF/X (ISO 15930) dependem do TrimBox para o tamanho acabado, e os perfis PDF/X exigem que cada página declare um TrimBox ou um ArtBox. Uma caixa estacionada num nó /Pages não cumpre esse requisito, porque a chave nunca chega ao objeto de página. O preflight devia marcar tais ficheiros em vez de os ler em silêncio de uma ou de outra maneira
O que é que o PDFlibPas fazia mal antes da v3.539.44?
O PDFlibPas tinha três defeitos separados, todos na distância entre o que a norma diz e o que dois caminhos de código independentes faziam. O primeiro foi corrigido na v3.539.42, os outros dois na v3.539.44
As caixas de produção tinham por omissão o MediaBox durante a captura
Antes da v3.539.42, a rotina interna que prepara uma página para captura (copia as entradas herdadas para a página e preenche as caixas em falta) dava à BleedBox, à TrimBox e à ArtBox os valores do MediaBox quando estavam ausentes. O CapturePageEx com opções 2 a 4 lê o seu retângulo envolvente exatamente dessas entradas preenchidas, por isso numa página que defina só um CropBox, pedir a caixa de trim capturava a media box inteira. O GetPageBox já aplicava a predefinição do CropBox, e a referência do CapturePageEx sempre dissera 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 caixas de produção têm por omissão o CropBox da página, que a essa altura já está na página (o seu próprio, copiado de um ancestral, ou preenchido a partir do MediaBox), e só o próprio CropBox recua 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 subtil era que o PDFlibPas resolvia caixas ao longo de dois caminhos independentes. As consultas de caixas (GetPageBox e HasPageBox) percorriam a cadeia /Parent através de um helper, e a captura percorria-a através de um helper local separado. Ambos herdavam todas as chaves, caixas de produção incluídas. Corrigir só um deles teria produzido uma contradição dentro de um único documento: com um TrimBox de 180 pontos de largura no nó /Pages e um CropBox de 380 pontos de largura na página, o GetPageBox continuaria a reportar 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 o percurso do /Parent às quatro chaves herdáveis, as caixas de produção são lidas só da folha, e a entrada perdida no pai fica no ficheiro intocada, nem apagada nem reescrita
O HasPageBox perdia arrays diretos do pai
O HasPageBox devolve 0 quando a página não tem caixa do tipo pedido, 1 quando a página tem a sua própria caixa (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 devolvia 2 só quando o valor herdado era uma referência indireta, por isso um array direto herdado devolvia 0. A correção separa a desreferenciação do teste do array, e ambas as representações devolvem agora 2. Desde a v3.539.44, o HasPageBox para uma BleedBox, TrimBox ou ArtBox só pode devolver 0 ou 1
A lição generaliza bem para além das caixas de página. Quando uma parte da semântica da norma tem dois pontos de entrada de implementação numa biblioteca, corrija-os em conjunto e teste-os como uma matriz em vez de com um ficheiro do caminho feliz. O conjunto de regressão do PDFlibPas cruza duas representações de caixa do pai (array direto e indireto) com três estados da folha (ausente, array direto, array indireto) e três opções de captura (bleed, trim, art), dando 18 cenários, e cada um verifica o resultado da consulta, os limites capturados, a herança legítima de MediaBox e CropBox, e a entrada do pai intocada
Como leio o TrimBox efetivo em Delphi?
Chame GetPageBox(4, Dimension) na página selecionada. O PDFlibPas aplica a cadeia de predefinições por você, por isso o resultado é o TrimBox efetivo quer a página tenha um quer não. Emparelhe-o com o HasPageBox quando precisar de saber de onde veio o valor, o que um relatório de preflight normalmente quer
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 como o SetPageBox trabalham nas definições de coordenadas atuais do documento. Os exemplos aqui correm com as predefinições: origem 0 (inferior esquerda, a condizer com o user space do PDF) e pontos como unidade de medida, por isso a dimensão Top é a margem superior medida a partir do fundo da página. Depois de SetOrigin(1) as dimensões Top e Bottom são medidas a partir do topo da página para baixo, e depois de SetMeasurementUnits(1) cada valor volta em milímetros. Largura e altura não dependem da origem
Encontrar caixas de produção abandonadas em nós /Pages
Desde a v3.539.44 a API de caixas já não vê um TrimBox num nó /Pages, o que está certo, mas uma ferramenta de preflight normalmente quer reportar tal ficheiro em vez de o ler em silêncio à maneira da norma. Os nós da árvore de páginas são objetos comuns, por isso a API de objetos de baixo nível os consegue encontrar: percorra os números de objeto até GetMaxObjectNumber, leia cada um com GetObjectToString, e procure um dicionário /Pages que traga uma chave de caixa de produção. A segunda metade da verificação é o teste por página que interessa ao PDF/X, e o HasPageBox responde agora como responderia um validador PDF/X, porque um TrimBox do pai já não conta
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. Caixas de produção em nós da árvore de páginas: não padrão e ignoradas
for ObjNum := 1 to Lib.GetMaxObjectNumber do
begin
Src := ''; // números livres não devolvem 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 precisa do seu 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. Reparação 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;
A correspondência de texto é uma verificação pragmática, não um parser. Apoia-se no PDFlibPas serializar cada entrada de dicionário como uma chave, um espaço e um valor, o que se mantém para objetos lidos de volta através do GetObjectToString. O passo de reparação 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-o contra a ordem de trabalho antes de o tornar oficial. O SetPageBoxRange com um intervalo vazio aplica a caixa a todas as páginas e devolve o número de páginas atualizadas. Quando a caixa existente de uma página é um array indireto, que outra página ou um nó /Pages pode partilhar, o SetPageBox dá a essa página um novo array direto em vez de reescrever o objeto partilhado. Definir uma BleedBox, TrimBox ou ArtBox também eleva um documento destrancado ao PDF 1.3, a versão que introduziu essas entradas
Impor páginas sobre o TrimBox com CapturePageEx
O CapturePageEx(Page, 3) transforma uma página num Form XObject cuja bounding box é o TrimBox efetivo da página, e o DrawCapturedPage coloca esse form noutra página a qualquer tamanho. Desde a v3.539.42, a opção 3 numa página sem TrimBox dá-lhe o CropBox, como a referência descreve, em vez do MediaBox com todo o seu slug
Duas propriedades da captura moldam o código. A captura é destrutiva: a página capturada é removida do documento, e o documento nunca pode descer a zero páginas, por isso acrescente a primeira folha de saída antes de capturar qualquer coisa. A captura também funciona dentro de um único documento, por isso junte primeiro todos os inputs num único documento; as técnicas para agregar e intercalar fontes PDF numa passagem aplicam-se diretamente
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);
// Acrescente e dimensione a primeira folha; NewPage seleciona a nova página
Lib.NewPage;
Lib.SetPageDimensions(2 * TrimW, TrimH);
// Cada captura remove a página 1, por isso a página de origem seguinte 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 que a folha atual
// Origem por omissão: Top é a margem superior, medida a partir do fundo
Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
end;
Lib.SaveToFile(OutFile);
finally
Lib.Free;
end;
end;
Uma captura baseada no trim corta tudo o que está fora do TrimBox, que é o que se quer numa prova digital ou num layout cut-and-stack. Para uma folha de impressão que é aparada depois da impressão, 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, os bookmarks e links que apontavam para elas perdem os alvos, por isso imponha para um ficheiro de saída separado em vez de editar um documento cuja navegação ainda precisa; substituir páginas sem partir bookmarks cobre esse lado da cirurgia de páginas
Quando a origem tem de ficar intacta, o ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) recebe os mesmos valores de opção de 0 a 4 (passe Lib.SelectedDocument para o documento atual), deixa a árvore de páginas de origem inalterada, normaliza a rotação de página herdada na matriz do form, e devolve um handle que o DrawCapturedPage aceita. O CapturePageEx não desfaz o /Rotate, por isso input rodado precisa desse passo primeiro, e achatar a rotação de página sem partir caixas de página mostra o que acontece a cada caixa quando o faz. Uma cautela para inputs que possam trazer caixas de produção em nós /Pages: o caminho de importação resolve a sua caixa através da sua própria procura nos ancestrais, separada dos dois caminhos alinhados na v3.539.44, por isso verifique primeiro o HasPageBox(4) na página de origem e passe a opção 1 (CropBox) quando devolver 0. Isso mantém o resultado ligado à norma e não à forma como o ficheiro por acaso foi escrito
Referência rápida de caixas de página
- 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 efetivas: a entrada própria da página folha, senão o CropBox efetivo
- Só
Resources,MediaBox,CropBoxeRotateherdam de nós/Pages(§7.7.3.4, Table 30); caixas de produção em nós/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 recuo ao MediaBox, 2 a 4 BleedBox, TrimBox ou ArtBox com recuo ao CropBox- Faça upgrade para a v3.539.44 ou posterior para predefinições e herança consistentes entre consultas de caixas e captura
As caixas de página são onde as predefinições silenciosas do PDF encontram tolerâncias de pré-impressão medidas em frações de milímetro, e uma biblioteca ou aplica essas predefinições da mesma maneira em todo o lado ou entrega-lhe duas respostas a uma pergunta. A API completa de caixas, captura e Form XObject está documentada na página de produto da PDF Library para Delphi PDFlibPas