Technisch artikel

PDFlibPas paginaboxen: TrimBox, BleedBox en CropBox-defaults

Heeft een PDF-pagina geen TrimBox, dan is zijn effectieve TrimBox de CropBox van de pagina, en ontbreekt ook de CropBox, dan is het de MediaBox. BleedBox en ArtBox volgen dezelfde regel. PDFlibPas, de PDF Library for Delphi, past deze default-keten sinds v3.539.44 consistent toe in GetPageBox, HasPageBox en CapturePageEx, en hij negeert production boxes die op een /Pages-node staan, want ISO 32000-1 laat ze niet overerven

Dat klinkt als een voetnoot tot u een job imponeert. Denk aan een boekbinnenwerk met een MediaBox van 6,25 × 9,25 inch, een CropBox ingesteld op de trim van 6 × 9 inch, en geen TrimBox, want wie het exporteerde is nooit op het idee gekomen er een weg te schrijven. Vraag om de trimbox, krijg de mediabox, en elke cel op uw drukvel sleept een achtste inch bleed en slug mee richting buurman. PDFlibPas had op precies dit terrein gebreken, opgelost in v3.539.42 en v3.539.44, en de manier waarop ze zijn opgelost zegt iets over hoe paginabox-semantiek in elke PDF-library hoort te worden geïmplementeerd

Welke box geldt als een pagina geen TrimBox heeft?

Het antwoord is een vaste default-keten uit ISO 32000-1 §14.11.2: de CropBox valt terug op de MediaBox, en de BleedBox, TrimBox en ArtBox vallen elk terug op de CropBox. Niets behalve de CropBox valt rechtstreeks terug op de MediaBox. Een pagina die alleen een MediaBox definieert heeft dus vijf identieke boxes, en een pagina die een MediaBox plus een CropBox definieert heeft vier boxes gelijk aan de CropBox

BoxPDFlibPas BoxTypeDefault bij afwezigheidOvererfbaar van /Pages
MediaBox1Geen, de entry is vereistJa
CropBox2MediaBoxJa
BleedBox3CropBoxNee
TrimBox4CropBoxNee
ArtBox5CropBoxNee

De keten van twee stappen telt omdat de CropBox zelf kan overerven. De effectieve TrimBox van een pagina met geen eigen TrimBox en geen eigen CropBox is de CropBox van de dichtstbijzijnde voorouder die er een heeft, en bij gebreke daarvan de overgeërfde MediaBox. De spec voegt nog één regel toe die makkelijk te vergeten is: de crop-, bleed-, trim- en art-boxen horen niet voorbij de mediabox te reiken, en doen ze dat wel, dan worden ze in feite gereduceerd tot hun snijding ermee. PDFlibPas meldt elke box zoals hij in het bestand staat, dus een validator die niet-vertrouwde invoer verwerkt moet zelf tegen de MediaBox klemmen

PDFlibPas paginabox-default-keten waarin de CropBox terugvalt op de MediaBox en BleedBox, TrimBox en ArtBox elk op de CropBox, getekend naast een boekbinnenwerk met een MediaBox van 450 bij 666 punten en een CropBox van 432 bij 648 punten die de effectieve trim wordt zodra er geen TrimBox bestaat
Niets behalve de CropBox valt rechtstreeks terug op de MediaBox, dus een pagina met alleen een MediaBox heeft vijf identieke boxes

Welke pagina-attributen kan een /Pages-node doorgeven?

Precies vier: Resources, MediaBox, CropBox en Rotate. ISO 32000-1 §7.7.3.4 definieert attribuuterfenis, en Table 30 merkt alleen die vier page-object-entries als overerfbaar aan. BleedBox, TrimBox en ArtBox horen bij de leafpagina. Een TrimBox weggeschreven in een /Pages-node is geen overgeërfde waarde; het is een niet-standaard key die een conforming reader negeert

Zo'n niet-standaard bestand bestaat echt, doorgaans met één TrimBox op de root van de paginaboom als stenografie voor "elke pagina heeft deze trim". De stenografie ziet er goed uit in elke tool die voor elke key /Parent doorloopt, en dat is het probleem: het bestand betekent nu twee dingen naargelang wie het leest. Een lezer die de spec volgt ziet geen TrimBox en gebruikt de CropBox, terwijl een lezer die alles laat overerven de parentwaarde ziet. In een prepress-pijplijn eindigt die ambiguïteit op het drukvel

PDFlibPas paginaboom-erfenis waarin alleen Resources, MediaBox, CropBox en Rotate een Pages-node doorgaan, zodat een TrimBox op de root geparkeerd een niet-standaard key is die conforming readers negeren; vóór v3.539.44 erfden twee onafhankelijke codepaden haar over en meldden verschillende trimmaten voor één document
Het bestand betekent twee dingen naargelang wie het leest, en in een prepress-pijplijn landt die ambiguïteit op het drukvel

PDF/X-workflows (ISO 15930) rekenen op de TrimBox voor het eindformaat, en de PDF/X-profielen eisen dat elke pagina een TrimBox of een ArtBox verklaart. Een box geparkeerd op een /Pages-node voldoet niet aan die eis, want de key bereikt het paginaobject nooit. Preflight hoort zulke bestanden te vlaggen in plaats van ze stilletjes de ene of de andere kant op te lezen

Wat deed PDFlibPas verkeerd vóór v3.539.44?

PDFlibPas had drie aparte gebreken, alle drie in de kloof tussen wat de spec zegt en wat twee onafhankelijke codepaden deden. De eerste werd opgelost in v3.539.42, de andere twee in v3.539.44

Production boxes vielen bij capture terug op de MediaBox

Vóór v3.539.42 gaf de interne routine die een pagina klaarmaakt voor capture (ze kopieert overgeërfde entries op de pagina en vult ontbrekende boxes aan) de BleedBox, TrimBox en ArtBox de MediaBox-waarden zodra ze ontbraken. CapturePageEx met opties 2 tot 4 leest zijn begrenzende rechthoek uit precies die aangevulde entries, dus op een pagina die alleen een CropBox definieert, ving de vraag om de trimbox de hele mediabox in. GetPageBox paste de CropBox-default al toe, en de CapturePageEx-referentie zei altijd al dat de crop box wordt gebruikt zodra de gevraagde box ontbreekt; de capture-code vlooide met beide. Sinds v3.539.42 vallen de drie production boxes terug op de CropBox van de pagina, die op dat punt al op de pagina staat (eigen, gekopieerd van een voorouder, of aangevuld vanuit de MediaBox), en alleen de CropBox zelf valt terug op de MediaBox

Twee erfenispaden, één semantische regel

Het tweede gebrek was de niet-standaard erfenis zelf, en het subtiele was dat PDFlibPas boxes langs twee onafhankelijke paden resolveerde. Boxqueries (GetPageBox en HasPageBox) doorliepen de /Parent-keten via één helper, en capture via een aparte lokale helper. Beide lieten elke key overerven, production boxes inbegrepen. Er er maar één van fixen zou een contradictie binnen één document hebben opgeleverd: met een TrimBox van 180 punten breed op de /Pages-node en een CropBox van 380 punten breed op de pagina, zou GetPageBox nog steeds een trimbreedte van 180 melden terwijl CapturePageEx een form van 380 breed bouwde. In v3.539.44 beperken beide paden de /Parent-wandeling tot de vier overerfbare keys, worden production boxes alleen van de leaf gelezen, en blijft de verdwaalde parent-entry onaangetast in het bestand, noch verwijderd noch herschreven

PDFlibPas HasPageBox-retourcodes nul, één en twee waarbij directe en indirecte arrays sinds v3.539.44 allebei als overgeërfd tellen, naast de CapturePageEx-opties nul tot vier waarin BleedBox, TrimBox en ArtBox sinds v3.539.42 terugvallen op de CropBox in plaats van de MediaBox
Twee implementatie-ingangspunten voor één spec-regel worden samen gefixt en als matrix van 18 scenario's getest, met query en capture het eens over elk bestand

HasPageBox miste directe parent-arrays

HasPageBox geeft 0 terug als de pagina geen box van het gevraagde type heeft, 1 als de pagina een eigen box heeft (direct opgeslagen of via een indirecte verwijzing), en 2 als een MediaBox of CropBox van een voorouder erft. De oude code gaf 2 alleen terug als de overgeërfde waarde een indirecte verwijzing was, dus een overgeërfde directe array gaf 0. De fix scheidt dereferentiëren van de arraytest, en beide vormen geven nu 2. Sinds v3.539.44 kan HasPageBox voor een BleedBox, TrimBox of ArtBox alleen 0 of 1 teruggeven

De les generaliseert ruim voorbij paginaboxen. Wanneer één stuk spec-semantiek twee implementatie-ingangspunten heeft in een library, fix ze dan samen en test ze als matrix in plaats van met één happy-path-bestand. De PDFlibPas-regressieset kruist twee parent-box-vormen (directe en indirecte array) met drie leaf-states (afwezig, directe array, indirecte array) en drie capture-opties (bleed, trim, art), wat 18 scenario's oplevert, en elk daarvan controleert het queryresultaat, de gevangen grenzen, de legitieme MediaBox- en CropBox-erfenis, en de onaangeroerde parent-entry

Hoe lees ik de effectieve TrimBox in Delphi?

Roep GetPageBox(4, Dimension) aan op de geselecteerde pagina. PDFlibPas past de default-keten voor u toe, dus het resultaat is de effectieve TrimBox of de pagina er nu een heeft of niet. Koppel hem aan HasPageBox wanneer u moet weten waar de waarde vandaan kwam, wat een preflightrapport doorgaans wil

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 = eigen, 2 = overgeërfd
    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.

GetPageBox en SetPageBox werken allebei in de huidige coördinaatinstellingen van het document. De voorbeelden hier draaien met de defaults: origine 0 (linksonder, gelijk aan PDF user space) en punten als meeteenheid, dus de Top-dimensie is de bovenrand gemeten opwaarts vanaf de onderrand van de pagina. Na SetOrigin(1) worden de dimensies Top en Bottom neerwaarts vanaf de bovenkant van de pagina gemeten, en na SetMeasurementUnits(1) komt elke waarde terug in millimeters. Breedte en hoogte hangen niet van de origine af

Production boxes vinden die op /Pages-nodes zijn gestrand

Sinds v3.539.44 ziet de box-API geen TrimBox op een /Pages-node meer, wat klopt, maar een preflighttool wil zo'n bestand meestal melden in plaats van het stilletjes op de spec-manier te lezen. Paginaboom-nodes zijn gewone objecten, dus de low-level object-API kan ze vinden: loop de objectnummers op tot GetMaxObjectNumber, lees elk met GetObjectToString, en zoek naar een /Pages-dictionary die een production-box-key voert. De tweede helft van de controle is de per-paginatoets waar PDF/X om geeft, en HasPageBox beantwoordt die nu zoals een PDF/X-validator zou doen, want een parent-TrimBox telt niet meer

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 op paginaboom-nodes: niet-standaard en genegeerd
  for ObjNum := 1 to Lib.GetMaxObjectNumber do
  begin
    Src := '';                                // vrije nummers geven geen tekst terug
    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: elke pagina heeft een eigen TrimBox of ArtBox nodig
  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. Optionele reparatie: een trim van 6 x 9 inch in een mediabox van
  //    6.25 x 9.25 inch (punten, origine linksonder: Left, Top, Width, Height)
  if Missing > 0 then
    Log.Add(Format('TrimBox written on %d pages',
      [Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;

De tekstmatch is een pragmatische controle, geen parser. Hij vertrouwt erop dat PDFlibPas elke dictionary-entry serialiseert als key, één spatie en een waarde, wat opgaat voor objecten die via GetObjectToString worden teruggelezen. De reparatiestap verdient een beslissing in plaats van een reflex: de verdwaalde parentwaarde is misschien precies wat de auteur bedoelde, maar toets haar aan de jobticket voordat u haar officieel maakt. SetPageBoxRange met een lege range past de box op elke pagina toe en geeft het aantal bijgewerkte pagina's terug. Is de bestaande box van een pagina een indirecte array, die een andere pagina of een /Pages-node kan delen, dan geeft SetPageBox die pagina een nieuwe directe array in plaats van het gedeelde object te herschrijven. Het zetten van een BleedBox, TrimBox of ArtBox tilt een niet-vergrendeld document bovendien op naar PDF 1.3, de versie die die entries introduceerde

Pagina's imputeren op de TrimBox met CapturePageEx

CapturePageEx(Page, 3) maakt van een pagina een Form XObject waarvan de begrenzende box de effectieve TrimBox van de pagina is, en DrawCapturedPage zet die form op een andere pagina op elke gewenste maat. Sinds v3.539.42 geeft optie 3 op een pagina zonder TrimBox u de CropBox, zoals de referentie beschrijft, in plaats van de MediaBox met al zijn slug

Twee eigenschappen van capture vormen de code. Capture is destructief: de gevangen pagina wordt uit het document verwijderd, en het document mag nooit naar nul pagina's zakken, dus voeg het eerste uitvoervel toe voordat u iets vangt. Capture werkt bovendien alleen binnen één document, dus trekt u eerst elke invoer samen in één document; de technieken voor PDF-bronnen collateren en interleaven in één keer zijn direct toepasbaar

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;

    // Effectieve trimmaat van pagina 1 (deze layout veronderstelt een uniforme trim)
    Lib.SelectPage(1);
    TrimW := Lib.GetPageBox(4, 2);
    TrimH := Lib.GetPageBox(4, 3);

    // Voeg het eerste vel toe en maak het op maat; NewPage selecteert de nieuwe pagina
    Lib.NewPage;
    Lib.SetPageDimensions(2 * TrimW, TrimH);

    // Elke capture verwijdert pagina 1, dus de volgende bronpagina schuift op
    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;

    // Alleen het vel is over: twee bijgesneden pagina's per vel, naast elkaar
    Lib.SelectPage(1);
    for I := 0 to SourceCount - 1 do
    begin
      if (I > 0) and (I mod 2 = 0) then
        Lib.NewPage;                            // zelfde maat als het huidige vel
      // Default-origine: Top is de bovenrand, gemeten vanaf de onderkant
      Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
    end;
    Lib.SaveToFile(OutFile);
  finally
    Lib.Free;
  end;
end;

Een capture op basis van de trim knipt alles buiten de TrimBox af, en dat is precies wat u wilt voor een digitaal proefexemplaar of een cut-and-stack-layout. Voor een drukvel dat na het drukken wordt bijgesneden, capture met optie 2 zodat de bleed overleeft, en ruim de cellen op de bleedbreedte. Omdat capture de bronpagina's verwijdert, raken bookmarks en links die ernaar wezen hun doelwit kwijt, dus imputeer naar een apart uitvoerbestand in plaats van een document te bewerken waarvan u de navigatie nog nodig heeft; pagina's vervangen zonder bookmarks te breken behandelt die kant van pagina-chirurgie

Moet de bron intact blijven, dan neemt ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) dezelfde optiewaarden 0 tot 4 (geef Lib.SelectedDocument door voor het huidige document), laat de paginaboom van de bron onaangeroerd, normaliseert overgeërfde paginarotatie in de formmatrix, en geeft een handle terug die DrawCapturedPage accepteert. CapturePageEx draait /Rotate niet terug, dus gedraaide invoer heeft die stap eerst nodig, en paginarotatie afvlakken zonder pagina-boxen te breken toont wat er met elke box gebeurt als u dat doet. Eén waarschuwing voor invoer die production boxes op /Pages-nodes kan bevatten: het importpad resolveert zijn box via een eigen voorouder-opzoeking, los van de twee paden die in v3.539.44 op één lijn zijn gebracht, dus controleer eerst HasPageBox(4) op de bronpagina en geef optie 1 (CropBox) door als die 0 teruggeeft. Zo blijft het resultaat aan de spec vast in plaats van aan hoe het bestand toevallig was geschreven

Snelnaslag paginaboxen

  • Effectieve CropBox: de eigen CropBox van de pagina, anders de dichtstbijzijnde overgeërfde CropBox, anders de effectieve MediaBox (ISO 32000-1 §14.11.2)
  • Effectieve BleedBox, TrimBox en ArtBox: de eigen entry van de leafpagina, anders de effectieve CropBox
  • Alleen Resources, MediaBox, CropBox en Rotate erven van /Pages-nodes (§7.7.3.4, Table 30); production boxes op /Pages-nodes worden genegeerd
  • 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 geen box, 1 de eigen box van de pagina (direct of indirect), 2 een overgeërfde MediaBox of CropBox (direct of indirect)
  • CapturePageEx(Page, Options): 0 MediaBox, 1 CropBox met MediaBox-fallback, 2 tot 4 BleedBox, TrimBox of ArtBox met CropBox-fallback
  • Upgrade naar v3.539.44 of later voor consistente defaults en erfenis over boxqueries en capture heen

Paginaboxen zijn de plek waar de stille defaults van PDF prepress-toleranties op fracties van een millimeter ontmoeten, en een library past die defaults of overal op dezelfde manier toe, of geeft u twee antwoorden op één vraag. De volledige box-, capture- en Form XObject-API is gedocumenteerd op de productpagina van de PDFlibPas PDF Library for Delphi