Articolo tecnico

Page box PDFlibPas: default di TrimBox, BleedBox e CropBox

Quando una pagina PDF non ha TrimBox, il suo TrimBox effettivo è il CropBox della pagina, e quando manca anche il CropBox è il MediaBox. BleedBox e ArtBox seguono la stessa regola. PDFlibPas, la PDF Library per Delphi, applica questa catena di default in modo coerente in GetPageBox, HasPageBox e CapturePageEx dalla v3.539.44, e ignora le production box appoggiate su un nodo /Pages, perché ISO 32000-1 non le lascia ereditare

Sembra una nota a piè di pagina finché non imponi un lavoro. Immagina un interno di libro con un MediaBox di 6,25 × 9,25 pollici, un CropBox impostato al trim di 6 × 9 pollici, e nessun TrimBox, perché chi l'ha esportato non ha mai pensato di scriverne uno. Chiedi il trim box, ti arriva il media box, e ogni cella sul tuo foglio di stampa trascina un ottavo di pollice di bleed e slug nel vicino. PDFlibPas aveva difetti proprio in quest'area, corretti nella v3.539.42 e v3.539.44, e il modo in cui sono stati corretti dice qualcosa su come la semantica delle page box andrebbe implementata in qualsiasi libreria PDF

Quale box vale quando una pagina non ha TrimBox?

La risposta è una catena di default fissa da ISO 32000-1 §14.11.2: il CropBox ha per default il MediaBox, e BleedBox, TrimBox e ArtBox hanno per default ciascuna il CropBox. Nulla eccetto il CropBox ha per default direttamente il MediaBox. Una pagina che definisce solo un MediaBox ha quindi cinque box identiche, e una pagina che definisce un MediaBox più un CropBox ha quattro box uguali al CropBox

BoxBoxType PDFlibPasDefault se assenteEreditabile da /Pages
MediaBox1Nessuno, la voce è obbligatoriaSì
CropBox2MediaBoxSì
BleedBox3CropBoxNo
TrimBox4CropBoxNo
ArtBox5CropBoxNo

La catena a due passi conta perché il CropBox può a sua volta essere ereditato. Il TrimBox effettivo di una pagina che non ha né un TrimBox né un CropBox propri è il CropBox dell'antenato più vicino che ne ha uno, e in mancanza, il MediaBox ereditato. La specifica aggiunge una regola in più facile da dimenticare: le box crop, bleed, trim e art non dovrebbero estendersi oltre il media box, e se lo fanno vengono di fatto ridotte alla loro intersezione con lui. PDFlibPas riporta ogni box così com'è conservata nel file, quindi un validatore che gestisce input non fidato dovrebbe fare il clamp contro il MediaBox da solo

Catena di default delle page box in PDFlibPas dove il CropBox ha per default il MediaBox e BleedBox, TrimBox e ArtBox hanno per default ciascuna il CropBox, disegnata accanto a un interno di libro con un MediaBox di 450 per 666 punti e un CropBox di 432 per 648 punti che diventa il trim effettivo quando non esiste un TrimBox
Nulla eccetto il CropBox ha per default direttamente il MediaBox, quindi una pagina con solo un MediaBox ha cinque box identiche

Quali attributi di pagina può trasmettere un nodo /Pages?

Esattamente quattro: Resources, MediaBox, CropBox e Rotate. ISO 32000-1 §7.7.3.4 definisce l'ereditarietà degli attributi, e la Table 30 marca solo quelle quattro voci dell'oggetto pagina come ereditabili. BleedBox, TrimBox e ArtBox appartengono alla pagina foglia. Un TrimBox scritto in un nodo /Pages non è un valore ereditato; è una chiave non standard che un lettore conforme ignora

File non standard così esistono, tipicamente con un unico TrimBox sul nodo radice dell'albero pagine come abbreviazione di "ogni pagina ha questo trim". L'abbreviazione sembra giusta in qualsiasi tool che percorre /Parent per ogni chiave, ed è questo il problema: il file ora significa due cose a seconda di chi lo legge. Un lettore che segue la specifica non vede alcun TrimBox e usa il CropBox, mentre un lettore che eredita tutto vede il valore del genitore. In una pipeline di prestampa quell'ambiguità finisce sul foglio di stampa

Ereditarietà dell'albero pagine in PDFlibPas dove solo Resources, MediaBox, CropBox e Rotate scendono lungo un nodo Pages, così un TrimBox parcheggiato sulla radice è una chiave non standard che i lettori conformi ignorano; prima della v3.539.44 due percorsi di codice indipendenti lo ereditavano e riportavano dimensioni di trim diverse per un solo documento
Il file significa due cose a seconda di chi lo legge, e in una pipeline di prestampa quell'ambiguità atterra sul foglio di stampa

I flussi di lavoro PDF/X (ISO 15930) contano sul TrimBox per la dimensione a finito, e i profili PDF/X richiedono che ogni pagina dichiari un TrimBox o un ArtBox. Una box parcheggiata su un nodo /Pages non soddisfa quel requisito, perché la chiave non raggiunge mai l'oggetto pagina. Il preflight dovrebbe segnalare tali file invece di leggerli in silenzio in un modo o nell'altro

Che cosa sbagliava PDFlibPas prima della v3.539.44?

PDFlibPas aveva tre difetti distinti, tutti nel varco tra ciò che la specifica dice e ciò che due percorsi di codice indipendenti facevano. Il primo è stato corretto nella v3.539.42, gli altri due nella v3.539.44

Le production box avevano per default il MediaBox durante la cattura

Prima della v3.539.42, la routine interna che prepara una pagina alla cattura (copia le voci ereditate sulla pagina e riempie le box mancanti) dava a BleedBox, TrimBox e ArtBox i valori del MediaBox quando erano assenti. CapturePageEx con opzioni da 2 a 4 legge il proprio rettangolo di delimitazione da esattamente quelle voci riempite, quindi su una pagina che definisce solo un CropBox, chiedere il trim box catturava l'intero media box. GetPageBox applicava già il default dal CropBox, e il riferimento di CapturePageEx diceva da sempre che si usa il crop box quando la box richiesta manca; il codice di cattura era in disaccordo con entrambi. Dalla v3.539.42 le tre production box hanno per default il CropBox della pagina, che a quel punto è già sulla pagina (proprio, copiato da un antenato, o riempito dal MediaBox), e solo il CropBox stesso ripiega sul MediaBox

Due percorsi di ereditarietà, una regola semantica

Il secondo difetto era l'ereditarietà non standard in sé, e la parte sottile era che PDFlibPas risolveva le box lungo due percorsi indipendenti. Le interrogazioni delle box (GetPageBox e HasPageBox) percorrevano la catena /Parent attraverso un helper, e la cattura la percorreva attraverso un helper locale separato. Entrambi ereditavano ogni chiave, production box incluse. Correggerne uno solo avrebbe prodotto una contraddizione dentro un singolo documento: con un TrimBox largo 180 punti sul nodo /Pages e un CropBox largo 380 punti sulla pagina, GetPageBox avrebbe continuato a riportare un trim largo 180 mentre CapturePageEx costruiva un form largo 380. Nella v3.539.44 entrambi i percorsi restringono il giro lungo /Parent alle quattro chiavi ereditabili, le production box si leggono solo dalla foglia, e la voce dispersa sul genitore resta nel file intoccata, né cancellata né riscritta

Codici di ritorno di HasPageBox in PDFlibPas zero, uno e due con array diretti e indiretti che contano entrambi come ereditati dalla v3.539.44, accanto alle opzioni di CapturePageEx da zero a quattro dove BleedBox, TrimBox e ArtBox ripiegano sul CropBox invece che sul MediaBox dalla v3.539.42
Due punti d'ingresso implementativi per una regola di specifica vengono corretti insieme e testati come matrice di 18 scenari, con interrogazione e cattura d'accordo su ogni file

HasPageBox perdeva gli array diretti sul genitore

HasPageBox restituisce 0 quando la pagina non ha una box del tipo richiesto, 1 quando la pagina ha una box propria (conservata direttamente o attraverso un riferimento indiretto), e 2 quando un MediaBox o CropBox viene ereditato da un antenato. Il vecchio codice restituiva 2 solo quando il valore ereditato era un riferimento indiretto, quindi un array diretto ereditato restituiva 0. La correzione separa la dereferenziazione dal test dell'array, e entrambe le rappresentazioni ora restituiscono 2. Dalla v3.539.44, HasPageBox per una BleedBox, TrimBox o ArtBox può restituire solo 0 o 1

La lezione si generalizza ben oltre le page box. Quando una singola semantica di specifica ha due punti d'ingresso implementativi in una libreria, correggili insieme e provali come matrice invece che con un unico file happy-path. L'insieme di regressione di PDFlibPas incrocia due rappresentazioni della box sul genitore (array diretto e indiretto) con tre stati della foglia (assente, array diretto, array indiretto) e tre opzioni di cattura (bleed, trim, art), dando 18 scenari, e ognuno controlla il risultato dell'interrogazione, i limiti catturati, la legittima ereditarietà di MediaBox e CropBox, e la voce sul genitore intoccata

Come leggo il TrimBox effettivo in Delphi?

Chiama GetPageBox(4, Dimension) sulla pagina selezionata. PDFlibPas applica la catena di default per te, quindi il risultato è il TrimBox effettivo che la pagina abbia o no il proprio. Abbinalo a HasPageBox quando ti serve sapere da dove viene il valore, cosa che di solito fa un rapporto di preflight

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 = propria, 2 = ereditata
    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.

Sia GetPageBox sia SetPageBox lavorano nelle impostazioni di coordinate correnti del documento. Gli esempi qui girano con i default: origine 0 (in basso a sinistra, come lo user space PDF) e punti come unità di misura, così la dimensione Top è il bordo superiore misurato dal fondo della pagina. Dopo SetOrigin(1) le dimensioni Top e Bottom si misurano invece dall'alto della pagina, e dopo SetMeasurementUnits(1) ogni valore torna in millimetri. Larghezza e altezza non dipendono dall'origine

Trovare production box arenate su nodi /Pages

Dalla v3.539.44 l'API delle box non vede più un TrimBox su un nodo /Pages, il che è corretto, ma un tool di preflight di solito vuole segnalare un tale file invece di leggerlo in silenzio alla maniera della specifica. I nodi dell'albero pagine sono oggetti ordinari, quindi l'API a basso livello degli oggetti può trovarli: percorri i numeri di oggetto fino a GetMaxObjectNumber, leggi ciascuno con GetObjectToString, e cerca un dizionario /Pages che porti una chiave di production box. La seconda metà del controllo è il test per pagina a cui tiene PDF/X, e HasPageBox ora vi risponde come farebbe un validatore PDF/X, perché un TrimBox sul genitore non conta più

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 box su nodi dell'albero pagine: non standard e ignorate
  for ObjNum := 1 to Lib.GetMaxObjectNumber do
  begin
    Src := '';                                // i numeri liberi non restituiscono testo
    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: ogni pagina ha bisogno di un proprio 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. Riparazione opzionale: un trim 6 x 9 in dentro un media box 6.25 x 9.25 in
  //    (punti, origine in basso a sinistra: Left, Top, Width, Height)
  if Missing > 0 then
    Log.Add(Format('TrimBox written on %d pages',
      [Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;

La corrispondenza testuale è un controllo pragmatico, non un parser. Si affida al fatto che PDFlibPas serializza ogni voce di dizionario come chiave, uno spazio e un valore, cosa che vale per gli oggetti riletti attraverso GetObjectToString. Il passo di riparazione merita una decisione più che un riflesso: il valore disperso sul genitore può ben essere ciò che l'autore intendeva, ma confermalo contro la commessa prima di renderlo ufficiale. SetPageBoxRange con un intervallo vuoto applica la box a ogni pagina e restituisce il numero di pagine aggiornate. Quando la box esistente di una pagina è un array indiretto, che un'altra pagina o un nodo /Pages può condividere, SetPageBox dà a quella pagina un nuovo array diretto invece di riscrivere l'oggetto condiviso. Impostare una BleedBox, TrimBox o ArtBox porta anche un documento non bloccato al PDF 1.3, la versione che introdusse quelle voci

Imporre le pagine sul TrimBox con CapturePageEx

CapturePageEx(Page, 3) trasforma una pagina in un Form XObject il cui riquadro di delimitazione è il TrimBox effettivo della pagina, e DrawCapturedPage colloca quel form su un'altra pagina a qualsiasi dimensione. Dalla v3.539.42, l'opzione 3 su una pagina senza TrimBox ti dà il CropBox, come descrive il riferimento, invece del MediaBox con tutto il suo slug

Due proprietà della cattura modellano il codice. La cattura è distruttiva: la pagina catturata viene rimossa dal documento, e il documento non può mai scendere a zero pagine, quindi accoda il primo foglio di uscita prima di catturare qualsiasi cosa. La cattura inoltre lavora solo dentro un documento, quindi riuni prima ogni ingresso in un unico documento; le tecniche per assemblare e intrecciare sorgenti PDF in un solo passaggio si applicano direttamente

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;

    // Dimensione trim effettiva della pagina 1 (questo layout presume un trim uniforme)
    Lib.SelectPage(1);
    TrimW := Lib.GetPageBox(4, 2);
    TrimH := Lib.GetPageBox(4, 3);

    // Aggiungi e dimensiona il primo foglio; NewPage seleziona la nuova pagina
    Lib.NewPage;
    Lib.SetPageDimensions(2 * TrimW, TrimH);

    // Ogni cattura rimuove la pagina 1, quindi la successiva pagina sorgente sale
    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;

    // Resta solo il foglio: due pagine rifilate per foglio, fianco a fianco
    Lib.SelectPage(1);
    for I := 0 to SourceCount - 1 do
    begin
      if (I > 0) and (I mod 2 = 0) then
        Lib.NewPage;                            // stessa dimensione del foglio corrente
      // Origine di default: Top è il bordo superiore, misurato dal basso
      Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
    end;
    Lib.SaveToFile(OutFile);
  finally
    Lib.Free;
  end;
end;

Una cattura basata sul trim taglia tutto ciò che sta fuori dal TrimBox, che è ciò che vuoi per una prova digitale o un layout cut-and-stack. Per un foglio di stampa che viene rifilato dopo la stampa, cattura con l'opzione 2 così il bleed sopravvive, e spazia le celle della larghezza del bleed. Poiché la cattura rimuove le pagine sorgente, i segnalibri e i link che puntavano a loro perdono i bersagli, quindi imponi in un file di uscita separato invece di modificare un documento la cui navigazione ti serve ancora; sostituire pagine senza rompere i segnalibri copre quel lato della chirurgia di pagine

Quando la sorgente deve restare intatta, ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) prende gli stessi valori di opzione da 0 a 4 (passa Lib.SelectedDocument per il documento corrente), lascia invariato l'albero pagine della sorgente, normalizza la rotazione di pagina ereditata nella matrice del form, e restituisce un handle che DrawCapturedPage accetta. CapturePageEx non annulla /Rotate, quindi un ingresso ruotato ha bisogno prima di quel passo, e appiattire la rotazione di pagina senza rompere le page box mostra cosa succede a ogni box quando lo fai. Una cautela per gli ingressi che possono portare production box su nodi /Pages: il percorso di import risolve la propria box attraverso la propria risalita degli antenati, separata dai due percorsi allineati nella v3.539.44, quindi controlla prima HasPageBox(4) sulla pagina sorgente e passa l'opzione 1 (CropBox) quando restituisce 0. Così il risultato resta legato alla specifica invece che a come il file per caso è stato scritto

Riferimento rapido alle page box

  • CropBox effettivo: il CropBox proprio della pagina, altrimenti il CropBox ereditato più vicino, altrimenti il MediaBox effettivo (ISO 32000-1 §14.11.2)
  • BleedBox, TrimBox e ArtBox effettive: la voce propria della pagina foglia, altrimenti il CropBox effettivo
  • Solo Resources, MediaBox, CropBox e Rotate ereditano dai nodi /Pages (§7.7.3.4, Table 30); le production box su nodi /Pages vengono ignorate
  • 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 nessuna box, 1 la box propria della pagina (diretta o indiretta), 2 un MediaBox o CropBox ereditato (diretto o indiretto)
  • CapturePageEx(Page, Options): 0 MediaBox, 1 CropBox con ripiego sul MediaBox, da 2 a 4 BleedBox, TrimBox o ArtBox con ripiego sul CropBox
  • Passa alla v3.539.44 o successiva per default ed ereditarietà coerenti tra interrogazioni delle box e cattura

Le page box sono dove i default silenziosi del PDF incontrano tolleranze di prestampa misurate in frazioni di millimetro, e una libreria o applica quei default allo stesso modo ovunque o ti consegna due risposte a una domanda. L'API completa di box, cattura e Form XObject è documentata sulla pagina prodotto di PDFlibPas PDF Library for Delphi