Technický článek

Page boxy PDFlibPas: výchozí TrimBox, BleedBox a CropBox

Když stránka PDF nemá TrimBox, její efektivní TrimBox je CropBox stránky a když chybí i CropBox, je to MediaBox. BleedBox a ArtBox drží stejné pravidlo. PDFlibPas, PDF Library for Delphi, aplikuje tenhle výchozí řetěz konzistentně v GetPageBox, HasPageBox a CapturePageEx od v3.539.44 a ignoruje produkční boxy položené na uzlu /Pages, protože ISO 32000-1 jim dědit nedovoluje

To zní jako poznámka pod čarou, dokud nezaimponujete zakázku. Představte si vnitřek knihy s MediaBoxem 6,25 × 9,25 palce, CropBoxem nastaveným na ořez 6 × 9 palců a bez TrimBoxu, protože ten, kdo ho exportoval, na napsání TrimBoxu ani nepomyslel. Zeptáte se na trim box, dostanete media box a každá buňka na vašem tiskovém archu vláčí osminu palce bleedu a slug do svého souseda. PDFlibPas měl závady přesně v téhle oblasti, opravené ve v3.539.42 a v3.539.44, a způsob, jakým byly opraveny, něco říká o tom, jak by se sémantika page boxů měla implementovat v jakékoli PDF knihovně

Který box platí, když stránka nemá TrimBox?

Odpovědí je pevný výchozí řetěz z ISO 32000-1 §14.11.2: CropBox se defaultně bere z MediaBoxu a BleedBox, TrimBox a ArtBox se každý bere z CropBoxu. Přímo z MediaBoxu se defaultně bere nic jiného než CropBox. Stránka definující jen MediaBox má proto pět identických boxů a stránka definující MediaBox plus CropBox má čtyři boxy rovné CropBoxu

BoxPDFlibPas BoxTypeVýchozí hodnota při absenciDěditelné z /Pages
MediaBox1Nic, položka je povinnáAno
CropBox2MediaBoxAno
BleedBox3CropBoxNe
TrimBox4CropBoxNe
ArtBox5CropBoxNe

Dvoustupňový řetěz má význam, protože i samotný CropBox může být zděděný. Efektivní TrimBox stránky, která nemá ani vlastní TrimBox, ani vlastní CropBox, je CropBox nejbližšího předka, který nějaký má, a když ani ten, pak zděděný MediaBox. Specifikace přidává ještě jedno pravidlo, které se snadno zapomíná: crop, bleed, trim a art boxy nemají přesahovat za media box a pokud přesáhnou, efektivně se redukují na svůj průnik s ním. PDFlibPas hlásí každý box tak, jak je uložený v souboru, takže validátor pracující s nedůvěryhodným vstupem by se měl sám seříznout proti MediaBoxu

Výchozí řetěz page boxů PDFlibPas, kde se CropBox defaultně bere z MediaBoxu a BleedBox, TrimBox a ArtBox každý z CropBoxu, nakreslený vedle vnitřku knihy s MediaBoxem 450 krát 666 bodů a CropBoxem 432 krát 648 bodů, který se při absenci TrimBoxu stává efektivním ořezem
Přímo z MediaBoxu se defaultně bere jen CropBox, takže stránka s pouhým MediaBoxem má pět identických boxů

Které atributy stránky může uzel /Pages předat dál?

Přesně čtyři: Resources, MediaBox, CropBox a Rotate. ISO 32000-1 §7.7.3.4 definuje dědičnost atributů a Table 30 označuje jako děditelné jen ty čtyři položky page objektu. BleedBox, TrimBox a ArtBox patří listové stránce. TrimBox zapsaný do uzlu /Pages není zděděná hodnota; je to nestandardní klíč, který korektní čtečka ignoruje

Takove nestandardní soubory existují, typicky s jediným TrimBoxem na kořenovém uzlu page tree jako zkratkou za „každá stránka má tento ořez“. Zkratka vypadá správně v jakémkoli nástroji, který prochází /Parent pro každý klíč, a v tom je problém: soubor teď znamená dvě věci podle toho, kdo ho čte. Čtečka jdoucí podle specifikace nevidí žádný TrimBox a použije CropBox, zatímco čtečka, která dědí všechno, vidí hodnotu rodiče. V prepress pipeline ta nejednoznačnost skončí na tiskovém archu

Dědičnost page tree v PDFlibPas, kde přes uzel Pages přechází jen Resources, MediaBox, CropBox a Rotate, takže TrimBox zaparkovaný na kořeni je nestandardní klíč, který korektní čtečky ignorují; před v3.539.44 dvě nezávislé kódové cesty dědily klíč a hlásily pro jeden dokument různé velikosti ořezu
Soubor znamená dvě věci podle toho, kdo ho čte, a v prepress pipeline ta nejednoznačnost dopadne na tiskový arch

Workflow PDF/X (ISO 15930) spoléhají na TrimBox jako na hotovou velikost a profily PDF/X vyžadují, aby každá stránka deklarovala TrimBox nebo ArtBox. Box zaparkovaný na uzlu /Pages ten požadavek nesplňuje, protože klíč se do page objektu nikdy nedostane. Preflight by měl takové soubory označit, místo aby je potichu četl jedním nebo druhým způsobem

Co dělal PDFlibPas před v3.539.44 špatně?

PDFlibPas měl tři oddělené závady, všechny v mezeře mezi tím, co říká specifikace, a tím, co dělaly dvě nezávislé kódové cesty. První se opravila ve v3.539.42, ty další dvě ve v3.539.44

Produkční boxy se při capture defaultně braly z MediaBoxu

Před v3.539.42 dávala interní rutina připravující stránku na capture (kopíruje zděděné položky na stránku a doplňuje chybějící boxy) BleedBoxu, TrimBoxu a ArtBoxu hodnoty MediaBoxu, když chyběly. CapturePageEx s options 2 až 4 čte svůj bounding rectangle přesně z těch doplněných položek, takže na stránce definující jen CropBox se dotaz na trim box chytil celý media box. GetPageBox už default CropBoxu aplikovala a reference CapturePageEx odjakživa říkala, že se při chybějícím boxu použije crop box; capture kód odporoval oběma. Od v3.539.42 se tři produkční boxy defaultně berou z CropBoxu stránky, který je v ten moment už na stránce (vlastní, zkopírovaný od předka, nebo doplněný z MediaBoxu) a jen samotný CropBox spadne zpátky na MediaBox

Dvě dědické cesty, jedno sémantické pravidlo

Druhou závadou byla ta nestandardní dědičnost sama a jemné na tom bylo, že PDFlibPas rozlišoval boxy po dvou nezávislých cestách. Dotazy na boxy (GetPageBox a HasPageBox) procházely řetěz /Parent přes jednoho pomocníka a capture jiným, místním pomocníkem. Obě dědily každý klíč, produkční boxy nevyjímaje. Oprava jen jedné by vytvořila rozpor uvnitř jediného dokumentu: s TrimBoxem širokým 180 bodů na uzlu /Pages a CropBoxem širokým 380 bodů na stránce by GetPageBox pořád hlásila šířku ořezu 180, zatímco CapturePageEx postavila form široký 380. Ve v3.539.44 obě cesty omezují procházení /Parent na čtyři děditelné klíče, produkční boxy se čtou jen z listu a zbloudilá rodičovská položka zůstává v souboru nedotčená, ani smazaná, ani přepsaná

Návratové kódy HasPageBox PDFlibPas nula, jedna a dvě, kde se od v3.539.44 přímé i nepřímé arrayy počítají jako zděděné, vedle options CapturePageEx nula až čtyři, kde se od v3.539.42 BleedBox, TrimBox a ArtBox opírají o CropBox místo MediaBoxu
Dva implementační vstupní body jednoho pravidla specifikace se opravují společně a testují jako matice 18 scénářů, přičemž dotaz a capture si rozumí na každém souboru

HasPageBox přehlížela přímé rodičovské arrayy

HasPageBox vrací 0, když stránka nemá box požadovaného typu, 1, když má vlastní box (uložený přímo nebo přes nepřímou referenci) a 2, když je MediaBox nebo CropBox zděděný od předka. Starý kód vracel 2 jen tehdy, když byla zděděná hodnota nepřímá reference, takže zděděný přímý array vrátil 0. Oprava odděluje dereferenci od testu array a obě podoby teď vracejí 2. Od v3.539.44 může HasPageBox pro BleedBox, TrimBox nebo ArtBox vrátit jen 0 nebo 1

Lekce se zobecňuje hodně dál než na page boxy. Když má jedna část sémantiky specifikace v knihovně dva implementační vstupní body, opravte je společně a testujte je jako matici, ne jedním happy-path souborem. Regresní sada PDFlibPas kříží dvě podoby rodičovského boxu (přímý a nepřímý array) se třemi stavy listu (absentní, přímý array, nepřímý array) a třemi capture options (bleed, trim, art), což dává 18 scénářů, a každá kontroluje výsledek dotazu, zachycené meze, legitimní dědičnost MediaBoxu a CropBoxu a nedotčenou rodičovskou položku

Jak v Delphi přečtu efektivní TrimBox?

Zavolejte GetPageBox(4, Dimension) na vybrané stránce. PDFlibPas vám výchozí řetěz aplikuje sám, takže výsledkem je efektivní TrimBox, ať stránka nějaký má nebo ne. Spárujte to s HasPageBox, když potřebujete vědět, odkud hodnota přišla — což obvykle chce preflight report

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 = vlastní, 2 = zděděné
    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.

Jak GetPageBox, tak SetPageBox pracují v aktuálním nastavení souřadnic dokumentu. Příklady tady běží s defaulty: počátek 0 (vlevo dole, odpovídající PDF user space) a body jako měrná jednotka, takže dimenze Top je horní hrana měřená odspodu stránky. Po SetOrigin(1) se dimenze Top a Bottom měří odshora dolů a po SetMeasurementUnits(1) přichází každá hodnota v milimetrech. Šířka a výška na počátku nezávisí

Hledání produkčních boxů uvízlých na uzlech /Pages

Od v3.539.44 box API už TrimBox na uzlu /Pages nevidí, což je správně, ale preflight nástroj obvykle takový soubor chce nahlásit, místo aby ho potichu četl specovým způsobem. Uzly page tree jsou obyčejné objekty, takže je nízkoúrovňové objektové API najde: projděte čísla objektů až do GetMaxObjectNumber, čtěte každý přes GetObjectToString a hledejte slovník /Pages nesoucí klíč produkčního boxu. Druhá půlka kontroly je test na stránku, na kterém záleží PDF/X, a HasPageBox na něj teď odpovídá tak, jak by odpovídal validátor PDF/X, protože rodičovský TrimBox se už nepočítá

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. Produkční boxy na uzlech page tree: nestandardní a ignorované
  for ObjNum := 1 to Lib.GetMaxObjectNumber do
  begin
    Src := '';                                // volná čísla nevrací žádný 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: každá stránka potřebuje vlastní TrimBox nebo 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. Volitelná oprava: ořez 6 x 9 palců uvnitř media boxu 6.25 x 9.25 palce
  //    (body, počátek vlevo dole: Left, Top, Width, Height)
  if Missing > 0 then
    Log.Add(Format('TrimBox written on %d pages',
      [Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;

Text match je pragmatická kontrola, ne parser. Spoléhá na to, že PDFlibPas serializuje každou položku slovníku jako klíč, jednu mezeru a hodnotu, což platí pro objekty čtené zpátky přes GetObjectToString. Krok opravy si zaslouží rozhodnutí, ne reflex: zbloudilá rodičovská hodnota může být klidně to, co autor zamýšlel, ale potvrďte ji proti zakázkovému listu, než z ní uděláte oficiální skutečnost. SetPageBoxRange s prázdným rozsahem aplikuje box na každou stránku a vrací počet aktualizovaných stránek. Když je stávající box stránky nepřímý array, který může sdílet jiná stránka nebo uzel /Pages, dá SetPageBox té stránce nový přímý array místo přepsání sdíleného objektu. Nastavení BleedBoxu, TrimBoxu nebo ArtBoxu navíc zvedne odemčený dokument na PDF 1.3, verzi, která ty položky zavedla

Impozice stránek na TrimBox přes CapturePageEx

CapturePageEx(Page, 3) změní stránku na Form XObject, jehož bounding box je efektivní TrimBox stránky, a DrawCapturedPage ten form umístí na jinou stránku v jakékoli velikosti. Od v3.539.42 dá option 3 na stránce bez TrimBoxu CropBox, jak popisuje reference, místo MediaBoxu se vším jeho slugem

Kód formují dvě vlastnosti capture. Capture je destruktivní: zachycená stránka se z dokumentu odstraní a dokument nikdy nesmí klesnout na nula stránek, takže připojte první výstupní arch, než začnete něco zachycovat. Capture navíc funguje jen v jednom dokumentu, takže nejdřív stáhněte každý vstup do jediného dokumentu; techniky z koládování a prokládání PDF zdrojů na jeden průchod se aplikují přímo

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;

    // Efektivní velikost ořezu stránky 1 (toto rozložení předpokládá jednotný ořez)
    Lib.SelectPage(1);
    TrimW := Lib.GetPageBox(4, 2);
    TrimH := Lib.GetPageBox(4, 3);

    // Připojte a rozměřte první arch; NewPage vybere novou stránku
    Lib.NewPage;
    Lib.SetPageDimensions(2 * TrimW, TrimH);

    // Každé zachycení odstraní stránku 1, takže další zdrojová stránka poskočí výš
    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;

    // Zbyl jen arch: dva ořezané stránky na arch, vedle sebe
    Lib.SelectPage(1);
    for I := 0 to SourceCount - 1 do
    begin
      if (I > 0) and (I mod 2 = 0) then
        Lib.NewPage;                            // stejná velikost jako aktuální arch
      // Výchozí počátek: Top je horní hrana, měřená odspodu
      Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
    end;
    Lib.SaveToFile(OutFile);
  finally
    Lib.Free;
  end;
end;

Capture podle ořezu stříhá všechno mimo TrimBox, což přesně chcete pro digitální korektur nebo rozložení cut-and-stack. Pro tiskový arch ořezávaný po tisku zachytávejte s option 2, aby bleed přežil, a rozestupte buňky po šířce bleedu. Protože capture odstraňuje zdrojové stránky, ztrácejí záložky a odkazy ukazující na ně své cíle, takže impozujte do odděleného výstupního souboru místo editace dokumentu, jehož navigaci ještě potřebujete; výměna stránek bez rozbitých záložek pokrývá tuhle stranu operací se stránkami

Když musí zdroj zůstat nedotčený, ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) bere stejné hodnoty options 0 až 4 (pro aktuální dokument pošlete Lib.SelectedDocument), nechává zdrojový page tree beze změny, normalizuje zděděnou rotaci stránky do matice formu a vrací handle, který DrawCapturedPage přijme. CapturePageEx nezruší /Rotate, takže otočený vstup tenhle krok potřebuje nejdřív a zploštění rotace stránky bez rozbitých page boxů ukazuje, co se stane s každým boxem, když to uděláte. Jedna opatrnost pro vstupy, které mohou nést produkční boxy na uzlech /Pages: importní cesta rozliší svůj box vlastním vyhledáním předka, odděleně od dvou cest srovnávaných ve v3.539.44, takže nejdřív zkontrolujte HasPageBox(4) na zdrojové stránce a pošlete option 1 (CropBox), když vrátí 0. Tím zůstane výsledek svázaný se specifikací, ne s tím, jak se náhodou soubor zapsal

Rychlá reference page boxů

  • Efektivní CropBox: vlastní CropBox stránky, jinak nejbližší zděděný CropBox, jinak efektivní MediaBox (ISO 32000-1 §14.11.2)
  • Efektivní BleedBox, TrimBox a ArtBox: vlastní položka listové stránky, jinak efektivní CropBox
  • Z uzlů /Pages dědí jen Resources, MediaBox, CropBox a Rotate (§7.7.3.4, Table 30); produkční boxy na uzlech /Pages se ignorují
  • 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 žádný box, 1 vlastní box stránky (přímý nebo nepřímý), 2 zděděný MediaBox nebo CropBox (přímý nebo nepřímý)
  • CapturePageEx(Page, Options): 0 MediaBox, 1 CropBox s oporou v MediaBoxu, 2 až 4 BleedBox, TrimBox nebo ArtBox s oporou v CropBoxu
  • Přejděte na v3.539.44 a novější pro konzistentní defaulty a dědičnost napříč dotazy na boxy a capture

Page boxy jsou místo, kde se tiché defaulty PDF potkávají s prepress tolerancemi měřenými na zlomky milimetru, a knihovna buď aplikuje ty defaulty stejně všude, nebo vám podá dvě odpovědi na jednu otázku. Kompletní API boxů, capture a Form XObject je zdokumentované na produktové stránce PDFlibPas PDF Library for Delphi