Article technique

Boxes de page PDFlibPas : défauts TrimBox, BleedBox, CropBox

Quand une page PDF n’a pas de TrimBox, son TrimBox effectif est le CropBox de la page, et quand le CropBox manque aussi, c’est le MediaBox. BleedBox et ArtBox suivent la même règle. PDFlibPas, la PDF Library for Delphi, applique cette chaîne de défauts de façon cohérente dans GetPageBox, HasPageBox et CapturePageEx depuis v3.539.44, et il ignore les boxes de production posées sur un nœud /Pages, parce que ISO 32000-1 ne les laisse pas hériter

Cela ressemble à une note de bas de page, jusqu’à ce que vous imposiez un travail. Imaginez un intérieur de livre avec un MediaBox de 6,25 × 9,25 pouces, un CropBox réglé sur le rogne de 6 × 9 pouces, et pas de TrimBox, parce que celui qui l’a exporté n’a jamais pensé à en écrire un. Demandez la boîte de rogne, recevez la boîte média à la place, et chaque cellule de votre forme de presse traîne un huitième de pouce de fond perdu et de recettes dans sa voisine. PDFlibPas avait des défauts exactement dans cette zone, corrigés en v3.539.42 et v3.539.44, et la façon de les corriger en dit long sur la façon dont la sémantique des boxes de page devrait être implémentée dans n’importe quelle bibliothèque PDF

Quelle box s’applique quand une page n’a pas de TrimBox ?

La réponse est une chaîne de défauts fixée par ISO 32000-1 §14.11.2 : le CropBox prend le MediaBox par défaut, et la BleedBox, la TrimBox et l’ArtBox prennent chacune le CropBox par défaut. Rien d’autre que le CropBox ne prend le MediaBox directement par défaut. Une page qui ne définit qu’un MediaBox a donc cinq boxes identiques, et une page qui définit un MediaBox plus un CropBox a quatre boxes égales au CropBox

BoxPDFlibPas BoxTypeDéfaut si absenteHéritable depuis /Pages
MediaBox1Aucun, l’entrée est requiseOui
CropBox2MediaBoxOui
BleedBox3CropBoxNon
TrimBox4CropBoxNon
ArtBox5CropBoxNon

La chaîne en deux temps compte parce que le CropBox peut lui-même être hérité. Le TrimBox effectif d’une page qui n’a ni TrimBox ni CropBox propre est le CropBox de l’ancêtre le plus proche qui en a un, et à défaut, le MediaBox hérité. La spec ajoute une règle de plus, facile à oublier : les boxes crop, bleed, trim et art ne devraient pas dépasser la media box, et si c’est le cas, elles sont effectivement réduites à leur intersection avec elle. PDFlibPas rapporte chaque box telle que stockée dans le fichier, donc un validateur qui traite des entrées non fiables devrait écrêter contre le MediaBox lui-même

Chaîne de défauts des boxes de page PDFlibPas où le CropBox prend le MediaBox par défaut et BleedBox, TrimBox et ArtBox prennent chacune le CropBox, dessinée à côté d’un intérieur de livre avec un MediaBox de 450 par 666 points et un CropBox de 432 par 648 points qui devient le rogne effectif quand aucun TrimBox n’existe
Rien d’autre que le CropBox ne prend le MediaBox directement par défaut, donc une page avec seulement un MediaBox a cinq boxes identiques

Quels attributs de page un nœud /Pages peut-il transmettre ?

Exactement quatre : Resources, MediaBox, CropBox et Rotate. ISO 32000-1 §7.7.3.4 définit l’héritage d’attributs, et la Table 30 ne marque que ces quatre entrées d’objet page comme héritables. BleedBox, TrimBox et ArtBox appartiennent à la page feuille. Un TrimBox écrit dans un nœud /Pages n’est pas une valeur héritée ; c’est une clé non standard qu’un lecteur conforme ignore

De tels fichiers non standard existent, typiquement avec un unique TrimBox sur le nœud racine de l’arbre de pages en raccourci de « chaque page a ce rogne ». Le raccourci paraît bon dans tout outil qui remonte /Parent pour chaque clé, et c’est là le problème : le fichier veut maintenant dire deux choses selon qui le lit. Un lecteur qui suit la spec ne voit pas de TrimBox et utilise le CropBox, tandis qu’un lecteur qui hérite tout voit la valeur du parent. Dans un pipeline de prépresse, cette ambiguïté finit sur la forme de presse

Héritage d’arbre de pages PDFlibPas où seuls Resources, MediaBox, CropBox et Rotate descendent un nœud Pages, si bien qu’un TrimBox parqué sur la racine est une clé non standard que les lecteurs conformes ignorent ; avant v3.539.44 deux chemins de code indépendants l’héritaient et rapportaient des rognes de tailles différentes pour un même document
Le fichier veut dire deux choses selon qui le lit, et dans un pipeline de prépresse cette ambiguïté atterrit sur la forme de presse

Les workflows PDF/X (ISO 15930) dépendent du TrimBox pour le format fini, et les profils PDF/X exigent que chaque page déclare un TrimBox ou un ArtBox. Une box parquée sur un nœud /Pages ne satisfait pas cette exigence, parce que la clé n’atteint jamais l’objet page. Le preflight devrait signaler de tels fichiers plutôt que les lire tranquillement dans un sens ou dans l’autre

Qu’est-ce que PDFlibPas faisait de travers avant v3.539.44 ?

PDFlibPas avait trois défauts distincts, tous dans l’écart entre ce que dit la spec et ce que faisaient deux chemins de code indépendants. Le premier a été corrigé en v3.539.42, les deux autres en v3.539.44

Les boxes de production prenaient le MediaBox par défaut à la capture

Avant v3.539.42, la routine interne qui prépare une page à la capture (elle copie les entrées héritées sur la page et comble les boxes manquantes) donnait à la BleedBox, à la TrimBox et à l’ArtBox les valeurs du MediaBox quand elles étaient absentes. CapturePageEx avec les options 2 à 4 lit son rectangle englobant dans exactement ces entrées comblées, donc sur une page qui ne définit qu’un CropBox, demander la boîte de rogne capturait toute la media box. GetPageBox appliquait déjà le défaut CropBox, et la référence de CapturePageEx disait depuis toujours que la crop box est utilisée quand la box demandée manque ; le code de capture était en désaccord avec les deux. Depuis v3.539.42, les trois boxes de production prennent le CropBox de la page par défaut, qui à ce stade est déjà sur la page (le sien, copié d’un ancêtre, ou comblé depuis le MediaBox), et seul le CropBox lui-même retombe sur le MediaBox

Deux chemins d’héritage, une règle sémantique

Le second défaut était l’héritage non standard lui-même, et la partie subtile était que PDFlibPas résolvait les boxes le long de deux chemins indépendants. Les requêtes de box (GetPageBox et HasPageBox) remontaient la chaîne /Parent par un helper, et la capture la remontait par un helper local séparé. Les deux héritaient de chaque clé, boxes de production comprises. N’en corriger qu’un aurait produit une contradiction à l’intérieur d’un même document : avec un TrimBox de 180 points de large sur le nœud /Pages et un CropBox de 380 points de large sur la page, GetPageBox aurait continué de rapporter un rogne de 180 pendant que CapturePageEx construisait un formulaire de 380. En v3.539.44, les deux chemins restreignent la marche /Parent aux quatre clés héritables, les boxes de production se lisent dans la feuille seule, et l’entrée parente égarée reste dans le fichier intacte, ni supprimée ni réécrite

Codes de retour de HasPageBox dans PDFlibPas, zéro, un et deux, où tableaux directs et indirects comptent tous deux comme hérités depuis v3.539.44, à côté des options de CapturePageEx de zéro à quatre où BleedBox, TrimBox et ArtBox retombent sur le CropBox au lieu du MediaBox depuis v3.539.42
Deux points d’entrée d’implémentation pour une règle de spec se corrigent ensemble et se testent comme une matrice de 18 scénarios, requête et capture d’accord sur chaque fichier

HasPageBox ratait les tableaux parent directs

HasPageBox renvoie 0 quand la page n’a pas de box du type demandé, 1 quand la page a sa propre box (stockée directement ou via une référence indirecte), et 2 quand un MediaBox ou un CropBox est hérité d’un ancêtre. L’ancien code renvoyait 2 seulement quand la valeur héritée était une référence indirecte, si bien qu’un tableau direct hérité renvoyait 0. La correction sépare le déréférencement du test de tableau, et les deux représentations renvoient désormais 2. Depuis v3.539.44, HasPageBox pour une BleedBox, une TrimBox ou une ArtBox ne peut renvoyer que 0 ou 1

La leçon se généralise bien au-delà des boxes de page. Quand un morceau de sémantique de spec a deux points d’entrée d’implémentation dans une bibliothèque, corrigez-les ensemble et testez-les comme une matrice plutôt qu’avec un seul fichier du cas idéal. Le jeu de régression de PDFlibPas croise deux représentations de box parente (tableau direct et indirect) avec trois états de feuille (absente, tableau direct, tableau indirect) et trois options de capture (bleed, trim, art), soit 18 scénarios, et chacun vérifie le résultat de la requête, les bornes capturées, l’héritage légitime de MediaBox et CropBox, et l’entrée parente intacte

Comment lire le TrimBox effectif en Delphi ?

Appelez GetPageBox(4, Dimension) sur la page sélectionnée. PDFlibPas applique la chaîne de défauts pour vous, donc le résultat est le TrimBox effectif que la page en ait un ou non. Accouplez-le à HasPageBox quand vous devez savoir d’où vient la valeur, ce que fait d’habitude un rapport de preflight

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 = propre, 2 = hérité
    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 et SetPageBox travaillent tous deux dans les réglages de coordonnées courants du document. Les exemples ici tournent avec les défauts : origine 0 (en bas à gauche, comme l’espace utilisateur PDF) et le point comme unité de mesure, si bien que la dimension Top est le bord supérieur mesuré depuis le bas de la page. Après SetOrigin(1), les dimensions Top et Bottom se mesurent plutôt vers le bas depuis le haut de la page, et après SetMeasurementUnits(1) chaque valeur revient en millimètres. Largeur et hauteur ne dépendent pas de l’origine

Trouver les boxes de production échouées sur des nœuds /Pages

Depuis v3.539.44, l’API de box ne voit plus un TrimBox sur un nœud /Pages, ce qui est correct, mais un outil de preflight veut d’ordinaire signaler un tel fichier plutôt que le lire en silence à la façon de la spec. Les nœuds de l’arbre de pages sont des objets ordinaires, donc l’API objet de bas niveau peut les trouver : parcourez les numéros d’objet jusqu’à GetMaxObjectNumber, lisez chacun avec GetObjectToString, et cherchez un dictionnaire /Pages qui porte une clé de box de production. La seconde moitié du contrôle est le test par page auquel PDF/X tient, et HasPageBox y répond désormais comme le ferait un validateur PDF/X, parce qu’un TrimBox parent ne compte plus

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. Boxes de production sur nœuds de l’arbre de pages : non standard, ignorées
  for ObjNum := 1 to Lib.GetMaxObjectNumber do
  begin
    Src := '';                                // les numéros libres ne renvoient pas de texte
    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 : chaque page a besoin de son propre TrimBox ou 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. Réparation optionnelle : un rogne 6 x 9 po dans une media box 6.25 x 9.25 po
  //    (points, origine bas-gauche : Left, Top, Width, Height)
  if Missing > 0 then
    Log.Add(Format('TrimBox written on %d pages',
      [Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;

La correspondance de texte est un contrôle pragmatique, pas un parseur. Elle repose sur le fait que PDFlibPas sérialise chaque entrée de dictionnaire en une clé, une espace et une valeur, ce qui vaut pour les objets relus via GetObjectToString. L’étape de réparation mérite une décision plutôt qu’un réflexe : la valeur parente égarée est peut-être bien ce que l’auteur voulait, mais confirmez-la contre le bon de commande avant de l’officialiser. SetPageBoxRange avec une plage vide applique la box à chaque page et renvoie le nombre de pages mises à jour. Quand la box existante d’une page est un tableau indirect, qu’une autre page ou un nœud /Pages peut partager, SetPageBox donne à cette page un nouveau tableau direct au lieu de réécrire l’objet partagé. Poser une BleedBox, une TrimBox ou une ArtBox fait aussi passer un document non verrouillé en PDF 1.3, la version qui a introduit ces entrées

Imposer des pages sur le TrimBox avec CapturePageEx

CapturePageEx(Page, 3) transforme une page en Form XObject dont la boîte englobante est le TrimBox effectif de la page, et DrawCapturedPage place ce formulaire sur une autre page à n’importe quelle taille. Depuis v3.539.42, l’option 3 sur une page sans TrimBox vous donne le CropBox, comme la référence le décrit, au lieu du MediaBox avec toute sa zone morte

Deux propriétés de la capture façonnent le code. La capture est destructive : la page capturée est retirée du document, et le document ne peut jamais tomber à zéro pages, donc ajoutez la première feuille de sortie avant de capturer quoi que ce soit. La capture ne marche aussi qu’à l’intérieur d’un seul document, donc ramenez d’abord chaque entrée dans un document unique ; les techniques de collationnement et d’entrelacement de sources PDF en une passe s’appliquent directement

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;

    // Taille de rogne effective de la page 1 (ce layout suppose un rogne uniforme)
    Lib.SelectPage(1);
    TrimW := Lib.GetPageBox(4, 2);
    TrimH := Lib.GetPageBox(4, 3);

    // Ajoutez et dimensionnez la première feuille ; NewPage sélectionne la nouvelle page
    Lib.NewPage;
    Lib.SetPageDimensions(2 * TrimW, TrimH);

    // Chaque capture retire la page 1, donc la page source suivante remonte
    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;

    // Ne reste que la feuille : deux pages rognées par feuille, côte à côte
    Lib.SelectPage(1);
    for I := 0 to SourceCount - 1 do
    begin
      if (I > 0) and (I mod 2 = 0) then
        Lib.NewPage;                            // même taille que la feuille courante
      // Origine par défaut : Top est le bord supérieur, mesuré depuis le bas
      Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
    end;
    Lib.SaveToFile(OutFile);
  finally
    Lib.Free;
  end;
end;

Une capture basée sur le rogne coupe tout ce qui est hors du TrimBox, ce qu’on veut pour une épreuve numérique ou un layout cut-and-stack. Pour une forme de presse rognée après impression, capturez avec l’option 2 pour que le fond perdu survive, et espacez les cellules de la largeur du fond perdu. Comme la capture retire les pages sources, les signets et liens qui pointaient vers eux perdent leurs cibles, donc imposez vers un fichier de sortie séparé plutôt que d’éditer un document dont vous avez encore besoin pour la navigation ; remplacer des pages sans casser les signets couvre ce côté de la chirurgie de pages

Quand la source doit rester intacte, ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) prend les mêmes valeurs d’option de 0 à 4 (passez Lib.SelectedDocument pour le document courant), laisse l’arbre de pages source inchangé, normalise la rotation de page héritée dans la matrice du formulaire, et renvoie un handle que DrawCapturedPage accepte. CapturePageEx n’annule pas /Rotate, donc une entrée tournée a besoin de cette étape d’abord, et aplatir la rotation de page sans casser les boxes de page montre ce que devient chaque box quand vous le faites. Une précaution pour les entrées qui peuvent porter des boxes de production sur des nœuds /Pages : le chemin d’import résout sa box par sa propre remontée d’ancêtres, séparée des deux chemins alignés en v3.539.44, donc vérifiez d’abord HasPageBox(4) sur la page source et passez l’option 1 (CropBox) quand elle renvoie 0. Ça garde le résultat lié à la spec plutôt qu’à la façon dont le fichier s’est trouvé écrit

Aide-mémoire des boxes de page

  • CropBox effectif : le CropBox propre de la page, sinon le CropBox hérité le plus proche, sinon le MediaBox effectif (ISO 32000-1 §14.11.2)
  • BleedBox, TrimBox et ArtBox effectives : l’entrée propre de la page feuille, sinon le CropBox effectif
  • Seuls Resources, MediaBox, CropBox et Rotate héritent des nœuds /Pages (§7.7.3.4, Table 30) ; les boxes de production sur des nœuds /Pages sont ignorées
  • 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 pas de box, 1 la box propre de la page (directe ou indirecte), 2 un MediaBox ou CropBox hérité (direct ou indirect)
  • CapturePageEx(Page, Options) : 0 MediaBox, 1 CropBox avec repli MediaBox, 2 à 4 BleedBox, TrimBox ou ArtBox avec repli CropBox
  • Passez à v3.539.44 ou ultérieure pour des défauts et un héritage cohérents entre requêtes de box et capture

Les boxes de page sont l’endroit où les défauts discrets de PDF rencontrent les tolérances de prépresse mesurées en fractions de millimètre, et une bibliothèque applique ces défauts de la même façon partout ou vous remet deux réponses à une question. L’API complète des boxes, de la capture et des Form XObject est documentée sur la page produit PDFlibPas PDF Library for Delphi