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
| Box | BoxType PDFlibPas | Default se assente | Ereditabile da /Pages |
|---|---|---|---|
| MediaBox | 1 | Nessuno, la voce è obbligatoria | Sì |
| CropBox | 2 | MediaBox | Sì |
| BleedBox | 3 | CropBox | No |
| TrimBox | 4 | CropBox | No |
| ArtBox | 5 | CropBox | No |
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
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
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
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,CropBoxeRotateereditano dai nodi/Pages(§7.7.3.4, Table 30); le production box su nodi/Pagesvengono 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 BottomHasPageBox(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