Teknisk artikel

PDFlibPas sidboxar: TrimBox, BleedBox och CropBox

När en PDF-sida saknar TrimBox är dess effektiva TrimBox sidans CropBox, och när CropBox också saknas är det MediaBox. BleedBox och ArtBox följer samma regel. PDFlibPas, PDF Library for Delphi, tillämpar denna standardkedja konsekvent i GetPageBox, HasPageBox och CapturePageEx sedan v3.539.44, och den ignorerar produktionsboxar placerade på en /Pages-nod, för ISO 32000-1 låter dem inte ärva

Det låter som en fotnot tills du imponerar ett jobb. Föreställ dig en bokinsida med en MediaBox på 6,25 × 9,25 tum, en CropBox satt till trimsnittet 6 × 9 tum, och ingen TrimBox, för den som exporterade den tänkte aldrig på att skriva en. Be om trimboxen, få mediaboxen i stället, och varje cell på ditt tryckark släpar med en åttondels tum av utblekning och anslag in i sin granne. PDFlibPas hade defekter i exakt det här området, fixade i v3.539.42 och v3.539.44, och sättet de fixades på säger något om hur sidboxsemantik bör implementeras i vilken PDF-library som helst

Vilken box gäller när en sida saknar TrimBox?

Svaret är en fast standardkedja från ISO 32000-1 §14.11.2: CropBox faller tillbaka på MediaBox, och BleedBox, TrimBox och ArtBox faller var och en tillbaka på CropBox. Inget utom CropBox faller direkt tillbaka på MediaBox. En sida som definierar bara en MediaBox har alltså fem identiska boxar, och en sida som definierar en MediaBox plus en CropBox har fyra boxar lika med CropBox

BoxPDFlibPas BoxTypeStandard vid frånvaroÄrvbar från /Pages
MediaBox1Ingen, posten krävsJa
CropBox2MediaBoxJa
BleedBox3CropBoxNej
TrimBox4CropBoxNej
ArtBox5CropBoxNej

Tvåstegskedjan spelar roll för att CropBox själv kan vara ärvd. Den effektiva TrimBox för en sida utan varken TrimBox eller CropBox av egen är CropBox hos närmaste förfader som har en, och om den misslyckas, den ärvda MediaBox. Specen lägger till ytterligare en regel som är lätt att glömma: crop-, bleed-, trim- och art-boxarna ska inte sträcka sig förbi mediaboxen, och gör de det är de i praktiken reducerade till sin skärning med den. PDFlibPas rapporterar varje box som lagrad i filen, så en validator som hanterar opålitlig indata bör klamra mot MediaBox själv

PDFlibPas sidbox-standardkedja där CropBox faller tillbaka på MediaBox och BleedBox, TrimBox och ArtBox var och en faller tillbaka på CropBox, ritad bredvid en bokinsida med en MediaBox på 450 gånger 666 punkter och en CropBox på 432 gånger 648 punkter som blir den effektiva trimsnittet när ingen TrimBox finns
Inget utom CropBox faller direkt tillbaka på MediaBox, så en sida med bara en MediaBox har fem identiska boxar

Vilka sidattribut kan en /Pages-nod föra vidare?

Exakt fyra: Resources, MediaBox, CropBox och Rotate. ISO 32000-1 §7.7.3.4 definierar attributärftning, och Table 30 märker bara de fyra sidobjektsposterna som ärvbara. BleedBox, TrimBox och ArtBox tillhör lövsidan. En TrimBox skriven in i en /Pages-nod är inte ett ärvt värde; den är en icke-standardnyckel som en normföljande läsare ignorerar

Icke-standardfiler som den finns, typiskt med en enda TrimBox på rotsidträdsnoden som förkortning för "varje sida har denna trim". Förkortningen ser rätt ut i vilket verktyg som helst som stegar /Parent för varje nyckel, och det är problemet: filen betyder nu två saker beroende på vem som läser den. En läsare som följer specen ser ingen TrimBox och använder CropBox, medan en läsare som ärver allt ser föräldervärdet. I en repropipeline hamnar den tvetydigheten på tryckarket

PDFlibPas sidträdärftning där bara Resources, MediaBox, CropBox och Rotate förs vidare över en Pages-nod, så att en TrimBox parkerad på roten är en icke-standardnyckel som normföljande läsare ignorerar; före v3.539.44 ärvde två oberoende kodvägar den och rapporterade olika trimstorlekar för ett dokument
Filen betyder två saker beroende på vem som läser den, och i en repropipeline landar den tvetydigheten på tryckarket

PDF/X-arbetsflöden (ISO 15930) förlitar sig på TrimBox för färdig storlek, och PDF/X-profilerna kräver att varje sida deklarerar en TrimBox eller en ArtBox. En box parkerad på en /Pages-nod uppfyller inte det kravet, för nyckeln når aldrig sidobjektet. Preflight bör flagga sådana filer i stället för att tyst läsa dem ena eller andra vägen

Vad gjorde PDFlibPas fel före v3.539.44?

PDFlibPas hade tre separata defekter, alla i gapet mellan vad specen säger och vad två oberoende kodvägar gjorde. Den första fixades i v3.539.42, de andra två i v3.539.44

Produktionsboxar fick MediaBox som standard vid fångst

Före v3.539.42 gav den interna rutinen som förbereder en sida för fångst (den kopierar ärvda poster på sidan och fyller i saknade boxar) BleedBox, TrimBox och ArtBox MediaBox-värdena när de saknades. CapturePageEx med alternativ 2 till 4 läser sin avgränsande rektangel från exakt de ifyllda posterna, så på en sida som definierar bara en CropBox fångade en förfrågan om trimboxen hela mediaboxen. GetPageBox tillämpade redan CropBox-standarden, och CapturePageEx-referensen hade alltid sagt att crop box används när den begärda boxen saknas; fångstkoden motsade båda. Sedan v3.539.42 får de tre produktionsboxarna sidans CropBox som standard, som vid den punkten redan ligger på sidan (dess egen, kopierad från en förfader, eller ifylld från MediaBox), och bara CropBox själv faller tillbaka på MediaBox

Två ärftningsvägar, en semantisk regel

Den andra defekten var den icke-standardmässiga ärftningen själv, och det subtila var att PDFlibPas löste upp boxar längs två oberoende vägar. Boxfrågor (GetPageBox och HasPageBox) stegade /Parent-kedjan genom en hjälpare, och fångsten stegade den genom en separat lokal hjälpare. Båda ärvde varje nyckel, produktionsboxar inkluderade. Att fixa bara en av dem skulle ha framställt en motsägelse inuti ett enda dokument: med en TrimBox 180 punkter bred på /Pages-noden och en CropBox 380 punkter bred på sidan skulle GetPageBox fortfarande rapportera en trimbredd på 180 medan CapturePageEx byggde en 380 bred form. I v3.539.44 begränsar båda vägarna /Parent-stegningen till de fyra ärvbara nycklarna, produktionsboxar läses bara från lövet, och den vilsekomna föräldraposten stannar i filen orörd, vare sig raderad eller omskriven

PDFlibPas HasPageBox-returkoder noll, ett och två med direkta och indirekta arrayer båda räknade som ärvda sedan v3.539.44, bredvid CapturePageEx-alternativen noll till fyra där BleedBox, TrimBox och ArtBox faller tillbaka på CropBox i stället för MediaBox sedan v3.539.42
Två implementeringsingångar för en specregel fixas tillsammans och testas som en matris av 18 scenarier, med fråga och fångst överens om varje fil

HasPageBox missade direkta förälderarrayer

HasPageBox returnerar 0 när sidan saknar en box av begärd typ, 1 när sidan har en egen box (lagrad direkt eller via en indirekt referens), och 2 när en MediaBox eller CropBox ärvs från en förfader. Gamla koden returnerade 2 bara när det ärvda värdet var en indirekt referens, så en ärvd direkt array returnerade 0. Fixen skiljer dereferering från arraytestet, och båda representationerna returnerar nu 2. Sedan v3.539.44 kan HasPageBox för en BleedBox, TrimBox eller ArtBox bara returnera 0 eller 1

Lärdomen generaliserar långt bortom sidboxar. När en bit specsemantik har två implementeringsingångar i ett bibliotek, fixa dem tillsammans och testa dem som en matris i stället för med en gladvägsfil. PDFlibPas regressionsmängd korsar två förälderboxrepresentationer (direkt och indirekt array) med tre lövtillstånd (frånvarande, direkt array, indirekt array) och tre fångstalternativ (bleed, trim, art), vilket ger 18 scenarier, och var och en kontrollerar frågeresultatet, de fångade gränserna, legitim MediaBox- och CropBox-ärftning, och den orörda föräldraposten

Hur läser jag den effektiva TrimBox i Delphi?

Anropa GetPageBox(4, Dimension) på den valda sidan. PDFlibPas tillämpar standardkedjan åt dig, så resultatet är den effektiva TrimBox oavsett om sidan har en eller inte. Para den med HasPageBox när du behöver veta var värdet kom ifrån, vilket en preflightrapport vanligen gör

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 = egen, 2 = ärvd
    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.

Både GetPageBox och SetPageBox arbetar i dokumentets aktuella koordinatinställningar. Exemplen här kör med standarderna: origo 0 (nere till vänster, matchande PDF user space) och punkter som mätenhet, så dimensionen Top är den övre kanten mätt uppåt från sidans nederkant. Efter SetOrigin(1) mäts dimensionerna Top och Bottom nedåt från sidans topp i stället, och efter SetMeasurementUnits(1) kommer varje värde tillbaka i millimeter. Bredd och höjd beror inte på origo

Hitta produktionsboxar strandsatta på /Pages-noder

Sedan v3.539.44 ser box-API:t inte längre en TrimBox på en /Pages-nod, vilket är korrekt, men ett preflight-verktyg vill vanligen rapportera en sådan fil i stället för att tyst läsa den på specens sätt. Sidträdsnoder är vanliga objekt, så lågnivå-objekt-API:et kan hitta dem: stega objektnummer upp till GetMaxObjectNumber, läs varje med GetObjectToString, och leta efter en /Pages-ordbok som bär en produktionsboxnyckel. Andra halvan av kontrollen är sidvis-testet PDF/X bryr sig om, och HasPageBox svarar den nu som en PDF/X-validator skulle, för en förälder-TrimBox räknas inte längre

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. Produktionsboxar på sidträdsnoder: icke-standard och ignoreras
  for ObjNum := 1 to Lib.GetMaxObjectNumber do
  begin
    Src := '';                                // lediga nummer returnerar ingen 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: varje sida behöver en egen TrimBox eller 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. Valfri reparation: en trim på 6 x 9 tum inuti en mediabox på 6.25 x 9.25 tum
  //    (punkter, ursprung nere vänster: Left, Top, Width, Height)
  if Missing > 0 then
    Log.Add(Format('TrimBox written on %d pages',
      [Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;

Textmatchningen är en pragmatisk kontroll, inte en parser. Den förlitar sig på att PDFlibPas serialiserar varje ordbokspost som en nyckel, ett mellanslag och ett värde, vilket gäller objekt lästa tillbaka via GetObjectToString. Reparationssteget förtjänar ett beslut i stället för en reflex: det vilsekomna föräldervärdet kan mycket väl vara vad upphovsmannen avsåg, men bekräfta det mot jobbiljetten innan du gör det officiellt. SetPageBoxRange med ett tomt intervall tillämpar boxen på varje sida och returnerar antalet uppdaterade sidor. När en sidas befintliga box är en indirekt array, som en annan sida eller en /Pages-nod kan dela, ger SetPageBox den sidan en ny direkt array i stället för att skriva om det delade objektet. Att sätta en BleedBox, TrimBox eller ArtBox höjer också ett olåst dokument till PDF 1.3, versionen som introducerade de posterna

Imponecera sidor på TrimBox med CapturePageEx

CapturePageEx(Page, 3) gör en sida till en Form XObject vars avgränsande box är sidans effektiva TrimBox, och DrawCapturedPage placerar den formen på en annan sida i valfri storlek. Sedan v3.539.42 ger alternativ 3 på en sida utan TrimBox dig CropBox, som referensen beskriver, i stället för MediaBox med allt sitt anslag

Två egenskaper hos fångsten formar koden. Fångst är destruktiv: den fångade sidan tas bort ur dokumentet, och dokumentet kan aldrig sjunka till noll sidor, så lägg till första utdataarket innan du fångar något. Fångst fungerar dessutom bara inom ett dokument, så dra in all indata i ett enda dokument först; teknikerna för att sammanfoga och fläta PDF-källor i ett pass tillämpas 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;

    // Effektiv trimstorlek för sida 1 (denna layout antar en enhetlig trim)
    Lib.SelectPage(1);
    TrimW := Lib.GetPageBox(4, 2);
    TrimH := Lib.GetPageBox(4, 3);

    // Lägg till och storleksätt första arket; NewPage väljer den nya sidan
    Lib.NewPage;
    Lib.SetPageDimensions(2 * TrimW, TrimH);

    // Varje fångst tar bort sida 1, så nästa källsida flyttar upp
    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;

    // Bara arket är kvar: två trimmade sidor per ark, sida vid sida
    Lib.SelectPage(1);
    for I := 0 to SourceCount - 1 do
    begin
      if (I > 0) and (I mod 2 = 0) then
        Lib.NewPage;                            // samma storlek som aktuellt ark
      // Standardursprung: Top är övre kant, mätt från nederkanten
      Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
    end;
    Lib.SaveToFile(OutFile);
  finally
    Lib.Free;
  end;
end;

En trimbaserad fångst klipper allt utanför TrimBox, vilket är vad du vill ha för ett digitalt korrektur eller en cut-and-stack-layout. För ett tryckark som trimmas efter tryck, fånga med alternativ 2 så att utblekningen överlever, och avstå cellerna med utblekningsbredden. Eftersom fångsten tar bort källsidorna tappar bokmärken och länkar som pekade på dem sina mål, så imponecera till en separat utdatafil i stället för att redigera ett dokument vars navigation du fortfarande behöver; att byta ut sidor utan att bryta bokmärken täcker den sidan av sidkirurgi

När källan måste stanna intakt tar ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) samma alternativvärden 0 till 4 (skicka Lib.SelectedDocument för aktuellt dokument), lämnar källans sidträd orört, normaliserar ärvd sidrotation in i formmatrisen, och returnerar ett handtag som DrawCapturedPage accepterar. CapturePageEx gör inte ogjort /Rotate, så roterad indata behöver det steget först, och att flattena sidrotation utan att bryta sidboxar visar vad som händer med varje box när du gör det. En varning för indata som kan bära produktionsboxar på /Pages-noder: importvägen löser upp sin box via sin egen förfaderuppslagning, skild från de två vägarna riktade i v3.539.44, så kontrollera HasPageBox(4) på källsidan först och skicka alternativ 1 (CropBox) när den returnerar 0. Det håller resultatet knutet till specen i stället för till hur filen råkade vara skriven

Snabbreferens för sidboxar

  • Effektiv CropBox: sidans egen CropBox, annars närmaste ärvda CropBox, annars den effektiva MediaBox (ISO 32000-1 §14.11.2)
  • Effektiv BleedBox, TrimBox och ArtBox: lövsidans egen post, annars den effektiva CropBox
  • Bara Resources, MediaBox, CropBox och Rotate ärver från /Pages-noder (§7.7.3.4, Table 30); produktionsboxar på /Pages-noder ignoreras
  • 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 ingen box, 1 sidans egen box (direkt eller indirekt), 2 en ärvd MediaBox eller CropBox (direkt eller indirekt)
  • CapturePageEx(Page, Options): 0 MediaBox, 1 CropBox med MediaBox-fallback, 2 till 4 BleedBox, TrimBox eller ArtBox med CropBox-fallback
  • Uppgradera till v3.539.44 eller senare för konsekventa standarder och ärftning över boxfrågor och fångst

Sidboxar är där PDF:s tysta standarder möter reproductionstoleranser mätta i bråkdelar av en millimeter, och ett bibliotek tillämpar antingen de standarderna på samma sätt överallt eller ger dig två svar på en fråga. Det fullständiga box-, fångst- och Form XObject-API:et är dokumenterat på produktsidan för PDFlibPas PDF Library for Delphi