Artigo Técnico

PDFlibPas: defaults de TrimBox, BleedBox e CropBox

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

BoxBoxType do PDFlibPasDefault quando ausenteHerdável de /Pages
MediaBox1Nenhum, a entrada é obrigatóriaSim
CropBox2MediaBoxSim
BleedBox3CropBoxNão
TrimBox4CropBoxNão
ArtBox5CropBoxNã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

Cadeia de defaults de page box no PDFlibPas em que o CropBox tem o MediaBox como default e BleedBox, TrimBox e ArtBox têm cada uma o CropBox como default, desenhada ao lado do miolo de um livro com MediaBox de 450 por 666 pontos e CropBox de 432 por 648 pontos que vira o trim efetivo quando não existe TrimBox
Nada além do CropBox tem o MediaBox como default direto, então uma página com só um MediaBox tem cinco caixas idênticas

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

Herança de page tree no PDFlibPas em que só Resources, MediaBox, CropBox e Rotate passam por um node Pages, então um TrimBox estacionado na raiz é uma chave não padrão que readers conformantes ignoram; antes da v3.539.44 dois caminhos de código independentes o herdavam e reportavam tamanhos de trim diferentes para um mesmo documento
O arquivo significa duas coisas dependendo de quem lê, e num pipeline de prepress essa ambiguidade pousa 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

Códigos de retorno do HasPageBox no PDFlibPas zero, um e dois com arrays diretos e indiretos ambos contando como herdados desde a v3.539.44, ao lado das opções zero a quatro do CapturePageEx em que BleedBox, TrimBox e ArtBox caem para o CropBox em vez do MediaBox desde a v3.539.42
Dois pontos de entrada de implementação para uma regra de spec são consertados juntos e testados como uma matriz de 18 cenários, com query e captura concordando em cada arquivo

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, CropBox e Rotate herdam de nodes /Pages (§7.7.3.4, Table 30); production boxes em nodes /Pages sã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 Bottom
  • HasPageBox(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