Technischer Artikel

PDFlibPas-Seitenbox-Defaults: TrimBox, BleedBox, CropBox

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

BoxPDFlibPas BoxTypeDefault bei AbwesenheitVererbbar von /Pages
MediaBox1Keiner, der Eintrag ist PflichtJa
CropBox2MediaBoxJa
BleedBox3CropBoxNein
TrimBox4CropBoxNein
ArtBox5CropBoxNein

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

PDFlibPas-Seitenbox-Default-Kette, bei der die CropBox auf die MediaBox zurückfällt und BleedBox, TrimBox und ArtBox jeweils auf die CropBox, gezeichnet neben einem Buchinnenteil mit 450 mal 666 Punkt MediaBox und einer 432 mal 648 Punkt CropBox, die zur effektiven TrimBox wird, wenn keine TrimBox existiert
Nichts außer der CropBox fällt direkt auf die MediaBox zurück, eine Seite mit nur einer MediaBox hat also fünf identische Boxen

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

PDFlibPas-Seitenbaum-Vererbung, bei der nur Resources, MediaBox, CropBox und Rotate einen Pages-Knoten hinabreichen, eine am Wurzelknoten geparkte TrimBox also ein nicht standardkonformer Schlüssel ist, den konforme Reader ignorieren; vor v3.539.44 erbten zwei unabhängige Codepfade sie und meldeten unterschiedliche Beschnittgrößen für ein Dokument
Die Datei bedeutet zwei Dinge, je nachdem, wer sie liest, und 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

PDFlibPas-HasPageBox-Rückgabecodes null, eins und zwei, bei denen direkte und indirekte Arrays seit v3.539.44 beide als geerbt zählen, daneben die CapturePageEx-Optionen null bis vier, bei denen BleedBox, TrimBox und ArtBox seit v3.539.42 auf die CropBox statt auf die MediaBox zurückfallen
Zwei Implementierungseinstiegspunkte für eine Spezifikationsregel werden zusammen gefixt und als Matrix aus 18 Szenarien getestet, mit Abfrage und Capture im Einvernehmen bei jeder Datei

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, CropBox und Rotate erben 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 Bottom
  • HasPageBox(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