Wenn eine PDF-Seite keine TrimBox hat, ist ihre effektive TrimBox die CropBox der Seite, und fehlt auch die CropBox, ist es die MediaBox. BleedBox und ArtBox folgen derselben Regel. PDFlibPas, die PDF Library for Delphi, wendet diese Default-Kette seit v3.539.44 konsistent in GetPageBox, HasPageBox und CapturePageEx an und ignoriert Produktionsboxen, die auf einem /Pages-Knoten sitzen, denn ISO 32000-1 lässt sie nicht erben
Das liest sich wie eine Fußnote, bis Sie einen Job ausimponieren. Stellen Sie sich ein Buchinnenteil mit 6,25 × 9,25 Zoll MediaBox vor, eine CropBox auf dem 6 × 9 Zoll Beschnitt und keine TrimBox, weil wer auch immer exportiert hat nie daran dachte, eine zu schreiben. Fragen Sie nach der TrimBox, bekommen Sie die MediaBox, und jede Zelle auf Ihrem Druckbogen schleppt ein Achtel Zoll Anschnitt und Beschnittzutat in den Nachbarn. PDFlibPas hatte ausgerechnet hier Defekte, gefixt in v3.539.42 und v3.539.44, und die Art, wie sie gefixt wurden, sagt etwas darüber, wie Seitenbox-Semantik in jeder PDF-Bibliothek umgesetzt werden sollte
Welche Box greift, wenn eine Seite keine TrimBox hat?
Die Antwort ist eine feste Default-Kette aus ISO 32000-1 §14.11.2: Die CropBox fällt auf die MediaBox zurück, und BleedBox, TrimBox und ArtBox fallen jeweils auf die CropBox zurück. Nichts außer der CropBox fällt direkt auf die MediaBox zurück. Eine Seite, die nur eine MediaBox definiert, hat also fünf identische Boxen, und eine Seite mit MediaBox plus CropBox hat vier Boxen gleich der CropBox
| Box | PDFlibPas BoxType | Default bei Abwesenheit | Vererbbar von /Pages |
|---|---|---|---|
| MediaBox | 1 | Keiner, der Eintrag ist Pflicht | Ja |
| CropBox | 2 | MediaBox | Ja |
| BleedBox | 3 | CropBox | Nein |
| TrimBox | 4 | CropBox | Nein |
| ArtBox | 5 | CropBox | Nein |
Die zweistufige Kette ist wichtig, weil die CropBox selbst geerbt sein kann. Die effektive TrimBox einer Seite, die weder eine TrimBox noch eine eigene CropBox hat, ist die CropBox des nächsten Ahnen, der eine hat, und sonst die geerbte MediaBox. Die Spezifikation fügt eine Regel hinzu, die leicht vergessen wird: Crop-, Bleed-, Trim- und ArtBox sollten nicht über die MediaBox hinausragen, und tun sie es doch, werden sie effektiv auf ihre Schnittmenge mit ihr reduziert. PDFlibPas meldet jede Box so, wie sie in der Datei steht, ein Validator für nicht vertrauenswürdige Eingaben sollte also selbst gegen die MediaBox klemmen
Welche Seitenattribute kann ein /Pages-Knoten weiterreichen?
Genau vier: Resources, MediaBox, CropBox und Rotate. ISO 32000-1 §7.7.3.4 definiert die Attributvererbung, und Tabelle 30 markiert nur diese vier Seitenobjekt-Einträge als vererbbar. BleedBox, TrimBox und ArtBox gehören zum Blatt. Eine in einen /Pages-Knoten geschriebene TrimBox ist kein geerbter Wert; sie ist ein nicht standardkonformer Schlüssel, den ein konformer Reader ignoriert
Nicht standardkonforme Dateien dieser Art existieren, typischerweise mit einer einzigen TrimBox am Wurzel-Seitenbaum-Knoten als Kurzschrift für „jede Seite hat diesen Beschnitt“. Die Kurzschrift sieht in jedem Tool richtig aus, das für jeden Schlüssel /Parent abläuft, und das ist das Problem: Die Datei bedeutet jetzt zwei Dinge, je nachdem, wer sie liest. Ein Reader, der der Spezifikation folgt, sieht keine TrimBox und nutzt die CropBox, während ein Reader, der alles erbt, den Parent-Wert sieht. In einer Prepress-Pipeline landet diese Mehrdeutigkeit auf dem Druckbogen
PDF/X-Workflows (ISO 15930) hängen am Beschnittformat, und die PDF/X-Profile verlangen von jeder Seite eine TrimBox oder eine ArtBox. Eine Box, die auf einem /Pages-Knoten parkt, erfüllt diese Anforderung nicht, denn der Schlüssel erreicht nie das Seitenobjekt. Preflight sollte solche Dateien markieren, statt sie stillschweigend auf die eine oder andere Weise zu lesen
Was hat PDFlibPas vor v3.539.44 falsch gemacht?
PDFlibPas hatte drei getrennte Defekte, alle in der Lücke zwischen dem, was die Spezifikation sagt, und dem, was zwei unabhängige Codepfade taten. Der erste wurde in v3.539.42 gefixt, die anderen beiden in v3.539.44
Produktionsboxen fielen beim Capture auf die MediaBox zurück
Vor v3.539.42 gab die interne Routine, die eine Seite für den Capture vorbereitet (sie kopiert geerbte Einträge auf die Seite und füllt fehlende Boxen auf), der BleedBox, TrimBox und ArtBox bei Abwesenheit die MediaBox-Werte. CapturePageEx mit Optionen 2 bis 4 liest sein Begrenzungsrechteck aus exakt diesen aufgefüllten Einträgen, auf einer Seite, die nur eine CropBox definiert, erfasste die Anfrage nach der TrimBox also die ganze MediaBox. GetPageBox wandte den CropBox-Default bereits an, und die CapturePageEx-Referenz sagte stets, dass bei fehlender angefragter Box die CropBox benutzt wird; der Capture-Code widersprach beidem. Seit v3.539.42 fallen die drei Produktionsboxen auf die CropBox der Seite zurück, die zu diesem Zeitpunkt bereits auf der Seite liegt (ihre eigene, von einem Ahnen kopiert oder aus der MediaBox aufgefüllt), und nur die CropBox selbst fällt auf die MediaBox zurück
Zwei Vererbungspfade, eine semantische Regel
Der zweite Defekt war die nicht standardkonforme Vererbung selbst, und der subtile Teil war, dass PDFlibPas Boxen entlang zweier unabhängiger Pfade auflöste. Box-Abfragen (GetPageBox und HasPageBox) liefen die /Parent-Kette über einen Helfer entlang, Capture über einen separaten lokalen Helfer. Beide erbten jeden Schlüssel, Produktionsboxen eingeschlossen. Nur einen davon zu fixen hätte einen Widerspruch innerhalb eines einzelnen Dokuments erzeugt: mit einer 180 Punkt breiten TrimBox am /Pages-Knoten und einer 380 Punkt breiten CropBox auf der Seite hätte GetPageBox weiterhin eine Beschnittbreite von 180 gemeldet, während CapturePageEx ein 380 breites Form baute. In v3.539.44 beschränken beide Pfade den /Parent-Walk auf die vier vererbbaren Schlüssel, Produktionsboxen werden nur am Blatt gelesen, und der verirrte Parent-Eintrag bleibt unangetastet in der Datei, weder gelöscht noch umgeschrieben
HasPageBox übersah direkte Parent-Arrays
HasPageBox liefert 0, wenn die Seite keine Box des angefragten Typs hat, 1, wenn die Seite ihre eigene Box hat (direkt gespeichert oder über eine indirekte Referenz), und 2, wenn eine MediaBox oder CropBox von einem Ahnen geerbt ist. Der alte Code lieferte 2 nur, wenn der geerbte Wert eine indirekte Referenz war, ein geerbtes direktes Array lieferte also 0. Der Fix trennt das Dereferenzieren vom Array-Test, beide Repräsentationen liefern jetzt 2. Seit v3.539.44 kann HasPageBox für eine BleedBox, TrimBox oder ArtBox nur noch 0 oder 1 liefern
Die Lektion verallgemeinert sich weit über Seitenboxen hinaus. Wenn eine Spezifikationssemantik zwei Implementierungseinstiegspunkte in einer Bibliothek hat, fixe sie zusammen und teste sie als Matrix statt mit einer einzigen Happy-Path-Datei. Der PDFlibPas-Regressionsset kreuzt zwei Parent-Box-Repräsentationen (direktes und indirektes Array) mit drei Blattzuständen (abwesend, direktes Array, indirektes Array) und drei Capture-Optionen (Bleed, Trim, Art), das ergibt 18 Szenarien, und jedes prüft das Abfrageergebnis, die erfassten Grenzen, legitime MediaBox- und CropBox-Vererbung und den unangetasteten Parent-Eintrag
Wie lese ich die effektive TrimBox in Delphi?
Rufen Sie GetPageBox(4, Dimension) auf der gewählten Seite auf. PDFlibPas wendet die Default-Kette für Sie an, das Ergebnis ist also die effektive TrimBox, ob die Seite nun eine hat oder nicht. Kombinieren Sie es mit HasPageBox, wenn Sie wissen müssen, woher der Wert kommt, was ein Preflight-Report üblicherweise will
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 = eigene, 2 = geerbt
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.
Sowohl GetPageBox als auch SetPageBox arbeiten in den aktuellen Koordinateneinstellungen des Dokuments. Die Beispiele hier laufen mit den Defaults: Ursprung 0 (unten links, passend zum PDF User Space) und Punkte als Maßeinheit, die Top-Dimension ist also die obere Kante, von der Unterseite der Seite aufwärts gemessen. Nach SetOrigin(1) werden die Dimensionen Top und Bottom stattdessen von der Oberseite der Seite abwärts gemessen, und nach SetMeasurementUnits(1) kommt jeder Wert in Millimetern zurück. Breite und Höhe hängen nicht vom Ursprung ab
Auf /Pages-Knoten gestrandete Produktionsboxen finden
Seit v3.539.44 sieht die Box-API eine TrimBox auf einem /Pages-Knoten nicht mehr, was korrekt ist, aber ein Preflight-Tool will so eine Datei üblicherweise melden, statt sie stillschweigend auf die Spezifikationsart zu lesen. Seitenbaum-Knoten sind gewöhnliche Objekte, die Low-Level-Objekt-API findet sie also: Objektnummern bis GetMaxObjectNumber ablaufen, jede mit GetObjectToString lesen und nach einem /Pages-Dictionary suchen, das einen Produktionsbox-Schlüssel trägt. Die zweite Hälfte der Prüfung ist der pro-Seite-Test, der PDF/X interessiert, und HasPageBox beantwortet ihn jetzt so, wie ein PDF/X-Validator es täte, denn eine Parent-TrimBox zählt nicht mehr
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. Produktionsboxen auf Seitenbaum-Knoten: nicht standardkonform und ignoriert
for ObjNum := 1 to Lib.GetMaxObjectNumber do
begin
Src := ''; // freie Nummern liefern keinen Text
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: jede Seite braucht ihre eigene TrimBox oder 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. Optionale Reparatur: eine 6 x 9 Zoll TrimBox innerhalb einer 6.25 x 9.25 Zoll MediaBox
// (Punkte, Ursprung unten links: Left, Top, Width, Height)
if Missing > 0 then
Log.Add(Format('TrimBox written on %d pages',
[Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;
Der Textabgleich ist eine pragmatische Prüfung, kein Parser. Er verlässt sich darauf, dass PDFlibPas jeden Dictionary-Eintrag als Schlüssel, ein Leerzeichen und ein Wert serialisiert, was für über GetObjectToString zurückgelesene Objekte gilt. Der Reparaturschritt verdient eine Entscheidung statt eines Reflexes: Der verirrte Parent-Wert mag durchaus das sein, was der Autor beabsichtigte, aber gleichen Sie ihn mit dem Auftrag ab, bevor Sie ihn offiziell machen. SetPageBoxRange mit leerer Range wendet die Box auf jede Seite an und liefert die Anzahl aktualisierter Seiten zurück. Ist die bestehende Box einer Seite ein indirektes Array, das eine andere Seite oder ein /Pages-Knoten teilen mag, gibt SetPageBox dieser Seite ein neues direktes Array, statt das geteilte Objekt umzuschreiben. Das Setzen einer BleedBox, TrimBox oder ArtBox hebt ein nicht gesperrtes Dokument auch auf PDF 1.3, die Version, die diese Einträge eingeführt hat
Seiten auf der TrimBox imponieren mit CapturePageEx
CapturePageEx(Page, 3) verwandelt eine Seite in ein Form XObject, dessen Begrenzungsbox die effektive TrimBox der Seite ist, und DrawCapturedPage platziert dieses Form in beliebiger Größe auf einer anderen Seite. Seit v3.539.42 liefert Option 3 auf einer Seite ohne TrimBox die CropBox, wie die Referenz es beschreibt, statt der MediaBox samt ihrem gesamten Beschnittzutat
Zwei Eigenschaften des Capture prägen den Code. Capture ist destruktiv: Die erfasste Seite wird aus dem Dokument entfernt, und das Dokument darf nie auf null Seiten fallen, hängen Sie also das erste Ausgabeblatt an, bevor Sie irgendetwas erfassen. Capture funktioniert zudem nur innerhalb eines Dokuments, ziehen Sie also jede Eingabe zuerst in ein einziges Dokument; die Techniken aus PDF-Quellen in einem Durchlauf sortieren und verschachteln passen direkt
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;
// Effektive TrimBox-Größe von Seite 1 (dieses Layout nimmt eine einheitliche TrimBox an)
Lib.SelectPage(1);
TrimW := Lib.GetPageBox(4, 2);
TrimH := Lib.GetPageBox(4, 3);
// Erstes Blatt anhängen und bemessen; NewPage wählt die neue Seite
Lib.NewPage;
Lib.SetPageDimensions(2 * TrimW, TrimH);
// Jeder Capture entfernt Seite 1, die nächste Quellseite rückt also nach oben
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;
// Nur noch das Blatt: zwei zugeschnittene Seiten pro Blatt, nebeneinander
Lib.SelectPage(1);
for I := 0 to SourceCount - 1 do
begin
if (I > 0) and (I mod 2 = 0) then
Lib.NewPage; // gleiche Größe wie das aktuelle Blatt
// Default-Ursprung: Top ist die obere Kante, von unten gemessen
Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
end;
Lib.SaveToFile(OutFile);
finally
Lib.Free;
end;
end;
Ein Trim-basierter Capture schneidet alles außerhalb der TrimBox ab, genau das wollen Sie für einen Digitalproof oder ein Cut-and-Stack-Layout. Für einen Druckbogen, der nach dem Druck beschnitten wird, erfassen Sie mit Option 2, damit der Anschnitt überlebt, und staffeln die Zellen um die Anschnittbreite. Weil Capture die Quellseiten entfernt, verlieren Bookmarks und Links, die auf sie zeigten, ihre Ziele, imponieren Sie also in eine separate Ausgabedatei, statt ein Dokument zu editieren, dessen Navigation Sie noch brauchen; Seiten ersetzen, ohne Bookmarks zu brechen behandelt jene Seite der Seiten-Operationen
Muss die Quelle intakt bleiben, nimmt ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) dieselben Optionswerte 0 bis 4 (übergeben Sie Lib.SelectedDocument für das aktuelle Dokument), lässt den Quell-Seitenbaum unverändert, normalisiert geerbte Seitenrotation in die Form-Matrix und liefert ein Handle zurück, das DrawCapturedPage akzeptiert. CapturePageEx macht /Rotate nicht rückgängig, rotierte Eingabe braucht also diesen Schritt zuerst, und Seitenrotation glätten, ohne Seitenboxen zu brechen zeigt, was mit jeder Box passiert, wenn Sie das tun. Eine Warnung für Eingaben, die Produktionsboxen auf /Pages-Knoten tragen können: Der Importpfad löst seine Box über seine eigene Ahnen-Nachschlag auf, getrennt von den beiden in v3.539.44 ausgerichteten Pfaden, prüfen Sie also zuerst HasPageBox(4) auf der Quellseite und übergeben Sie bei 0 die Option 1 (CropBox). Das hält das Ergebnis an der Spezifikation fest statt daran, wie die Datei zufällig geschrieben war
Seitenbox-Kurzreferenz
- Effektive CropBox: die eigene CropBox der Seite, sonst die nächste geerbte CropBox, sonst die effektive MediaBox (ISO 32000-1 §14.11.2)
- Effektive BleedBox, TrimBox und ArtBox: der eigene Eintrag der Blattseite, sonst die effektive CropBox
- Nur
Resources,MediaBox,CropBoxundRotateerben von/Pages-Knoten (§7.7.3.4, Tabelle 30); Produktionsboxen auf/Pages-Knoten werden ignoriert 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 keine Box, 1 die eigene Box der Seite (direkt oder indirekt), 2 eine geerbte MediaBox oder CropBox (direkt oder indirekt)CapturePageEx(Page, Options): 0 MediaBox, 1 CropBox mit MediaBox-Fallback, 2 bis 4 BleedBox, TrimBox oder ArtBox mit CropBox-Fallback- Auf v3.539.44 oder später aktualisieren für konsistente Defaults und Vererbung über Box-Abfragen und Capture hinweg
Seitenboxen sind der Ort, an dem PDFs stille Defaults auf Prepress-Toleranzen treffen, die in Bruchteilen eines Millimeters gemessen werden, und eine Bibliothek wendet diese Defaults entweder überall auf dieselbe Weise an oder liefert Ihnen zwei Antworten auf eine Frage. Die vollständige Box-, Capture- und Form-XObject-API ist auf der Produktseite der PDFlibPas PDF Library for Delphi dokumentiert