Artykuł techniczny

Boxy stron PDFlibPas: domyślne TrimBox, BleedBox i CropBox

Gdy strona PDF nie ma TrimBoxa, jej efektywny TrimBox to CropBox strony, a gdy brakuje też CropBoxa — MediaBox. BleedBox i ArtBox trzymają się tej samej reguły. PDFlibPas, biblioteka PDF dla Delphi, stosuje ten domyślny łańcuch konsekwentnie w GetPageBox, HasPageBox i CapturePageEx od v3.539.44 i ignoruje boxy produkcyjne postawione na węźle /Pages, bo ISO 32000-1 nie pozwala im dziedziczyć

Brzmi jak przypis, dopóki nie złożysz zadania na maszynie. Wyobraź sobie wnętrze książki z MediaBoxem 6,25 × 9,25 cala, CropBoxem ustawionym na trim 6 × 9 cala i bez TrimBoxa, bo ktoś, kto to eksportował, nie pomyślał, żeby go napisać. Poprosisz o trim box, dostaniesz media box, a każda komórka na arkuszu drukarskim wlecze ósemkę cala spadów i pól do sąsiadki. PDFlibPas miał usterki dokładnie w tym obszarze, naprawione w v3.539.42 i v3.539.44, a sposób ich naprawy mówi sporo o tym, jak semantykę boxów stron powinno się implementować w dowolnej bibliotece PDF

Który box obowiązuje, gdy strona nie ma TrimBoxa?

Odpowiedzią jest stały domyślny łańcuch z ISO 32000-1 §14.11.2: CropBox domyśla się MediaBoxa, a BleedBox, TrimBox i ArtBox domyślają się każdy CropBoxa. Nic poza CropBoxem nie domyśla się prosto do MediaBoxa. Strona definiująca tylko MediaBox ma więc pięć identycznych boxów, a strona definiująca MediaBox plus CropBox ma cztery boxy równe CropBoxowi

BoxBoxType w PDFlibPasDomyślnie przy brakuDziedziczne z /Pages
MediaBox1Brak, wpis jest wymaganyTak
CropBox2MediaBoxTak
BleedBox3CropBoxNie
TrimBox4CropBoxNie
ArtBox5CropBoxNie

Dwustopniowy łańcuch ma znaczenie, bo sam CropBox może być odziedziczony. Efektywny TrimBox strony, która nie ma ani TrimBoxa, ani własnego CropBoxa, to CropBox najbliższego przodka, który go ma, a gdy i to zawiedzie — odziedziczony MediaBox. Specyfikacja dodaje jeszcze jedną łatwą do zapomnienia regułę: boxy crop, bleed, trim i art nie powinny wystawać poza media box, a jeśli wystają, są w praktyce zredukowane do przecięcia z nim. PDFlibPas raportuje każdy box tak, jak jest zapisany w pliku, więc walidator obsługujący dane niezaufane powinien sam dociskać do MediaBoxa

Domyślny łańcuch boxów stron w PDFlibPas, gdzie CropBox domyśla się MediaBoxa, a BleedBox, TrimBox i ArtBox domyślają się każdy CropBoxa, narysowany obok wnętrza książki z MediaBoxem 450 na 666 punktów i CropBoxem 432 na 648 punktów, który staje się efektywnym trimem, gdy TrimBoxa nie ma
Nic poza CropBoxem nie domyśla się prosto do MediaBoxa, więc strona mająca tylko MediaBox ma pięć identycznych boxów

Które atrybuty strony może przekazać w dół węzeł /Pages?

Dokładnie cztery: Resources, MediaBox, CropBox i Rotate. ISO 32000-1 §7.7.3.4 definiuje dziedziczenie atrybutów, a tabela 30 oznacza jako dziedziczne tylko te cztery wpisy obiektu strony. BleedBox, TrimBox i ArtBox należą do liścia strony. TrimBox zapisany w węźle /Pages nie jest wartością dziedziczoną; to niestandardowy klucz, który czytelnik zgodny ze specyfikacją ignoruje

Takie niestandardowe pliki istnieją, zwykle z pojedynczym TrimBoxem na korzeniu drzewa stron jako skrótem myślowym za „każda strona ma ten trim". Skrót wygląda dobrze w każdym narzędziu, które chodzi po /Parent dla każdego klucza, i w tym jest problem: plik znaczy teraz dwie rzeczy, zależnie od tego, kto go czyta. Czytelnik trzymający się specyfikacji nie widzi TrimBoxa i używa CropBoxa, a czytelnik dziedziczący wszystko widzi wartość rodzica. W potoku przeddrukowym ta dwuznaczność ląduje na arkuszu drukarskim

Dziedziczenie w drzewie stron w PDFlibPas, gdzie tylko Resources, MediaBox, CropBox i Rotate przechodzą w dół przez węzeł Pages, więc TrimBox odstawiony na korzeniu to niestandardowy klucz, który czytelnicy zgodni ze specyfikacją ignorują; przed v3.539.44 dwie niezależne ścieżki kodu go dziedziczyły i raportowały różne rozmiary trimu dla jednego dokumentu
Plik znaczy dwie rzeczy, zależnie od tego, kto go czyta, a w potoku przeddrukowym ta dwuznaczność ląduje na arkuszu drukarskim

Workflow PDF/X (ISO 15930) polegają na TrimBoxie co do formatu zamkniętego, a profile PDF/X wymagają od każdej strony deklaracji TrimBoxa albo ArtBoxa. Box odstawiony na węźle /Pages tego wymagania nie spełnia, bo klucz nigdy nie dociera do obiektu strony. Preflight powinien takie pliki flagować, a nie po cichu czytać je w którąś z dwu stron

Co PDFlibPas robił źle przed v3.539.44?

PDFlibPas miał trzy osobne usterki, wszystkie w szczelinie między tym, co mówi specyfikacja, a tym, co robiły dwie niezależne ścieżki kodu. Pierwszą naprawiono w v3.539.42, pozostałe dwie w v3.539.44

Boxy produkcyjne domyślały się MediaBoxa przy przechwytywaniu

Przed v3.539.42 wewnętrzna rutyna przygotowująca stronę do przechwycenia (kopiuje dziedziczone wpisy na stronę i uzupełnia brakujące boxy) dawała BleedBoxowi, TrimBoxowi i ArtBoxowi wartości MediaBoxa, gdy ich brakowało. CapturePageEx z opcjami od 2 do 4 czyta swój prostokąt ograniczający dokładnie z tych uzupełnionych wpisów, więc na stronie definiującej tylko CropBox prośba o trim box przechwytywała cały media box. GetPageBox stosował już domyślenie do CropBoxa, a referencja CapturePageEx od zawsze mówiła, że przy braku żądanego boxa używany jest crop box; kod przechwytywania był w sprzeczności z oboma. Od v3.539.42 trzy boxy produkcyjne domyślają się CropBoxa strony, który w tym momencie już na stronie jest (własny, skopiowany od przodka albo uzupełniony z MediaBoxa), a do MediaBoxa cofa się tylko sam CropBox

Dwie ścieżki dziedziczenia, jedna reguła semantyczna

Drugą usterką było samo niestandardowe dziedziczenie, a subtelność polegała na tym, że PDFlibPas rozwiązywał boxy dwiema niezależnymi ścieżkami. Zapytania o boxy (GetPageBox i HasPageBox) chodziły po łańcuchu /Parent jednym helperem, a przechwytywanie — osobnym, lokalnym helperem. Oba dziedziczyły każdy klucz, boxy produkcyjne włącznie. Naprawa tylko jednej z nich wytworzyłaby sprzeczność wewnątrz jednego dokumentu: przy TrimBoxie o szerokości 180 punktów na węźle /Pages i CropBoxie o szerokości 380 punktów na stronie GetPageBox wciąż raportowałby szerokość trimu 180, podczas gdy CapturePageEx budowałby formę o szerokości 380. W v3.539.44 obie ścieżki ograniczają chodzenie po /Parent do czterech kluczy dziedzicznych, boxy produkcyjne są czytane wyłącznie z liścia, a zabłąkany wpis rodzica zostaje w pliku nietknięty — ani usunięty, ani przepisany

Kody zwrotne HasPageBox w PDFlibPas: zero, jeden i dwa, gdzie tablice bezpośrednie i pośrednie liczą się od v3.539.44 jako odziedziczone, obok opcji CapturePageEx od zera do czterech, gdzie BleedBox, TrimBox i ArtBox cofają się od v3.539.42 do CropBoxa zamiast do MediaBoxa
Dwa punkty wejściowe implementacji dla jednej reguły specyfikacji naprawia się razem i testuje jako macierz 18 scenariuszy, z zapytaniem i przechwytywaniem zgodnymi na każdym pliku

HasPageBox gubiło bezpośrednie tablice rodzica

HasPageBox zwraca 0, gdy strona nie ma boxa żądanego typu, 1, gdy ma własny box (zapisany bezpośrednio albo przez referencję pośrednią), i 2, gdy MediaBox albo CropBox jest odziedziczony po przodku. Stary kod zwracał 2 tylko wtedy, gdy odziedziczona wartość była referencją pośrednią, więc odziedziczona tablica bezpośrednia zwracała 0. Poprawka oddziela dereferencję od testu tablicy i obie reprezentacje zwracają teraz 2. Od v3.539.44 HasPageBox dla BleedBoxa, TrimBoxa albo ArtBoxa może zwrócić tylko 0 albo 1

Lekcja uogólnia się daleko poza boxy stron. Gdy jeden kawałek semantyki specyfikacji ma w bibliotece dwa punkty wejściowe implementacji, naprawia się je razem i testuje jako macierz, a nie jednym plikiem z sunny day. Zestaw regresyjny PDFlibPas krzyżuje dwie reprezentacje boxa rodzica (tablica bezpośrednia i pośrednia) z trzema stanami liścia (brak, tablica bezpośrednia, tablica pośrednia) i trzema opcjami przechwytywania (bleed, trim, art), co daje 18 scenariuszy, a każdy sprawdza wynik zapytania, przechwycone granice, legalne dziedziczenie MediaBoxa i CropBoxa oraz nietknięty wpis rodzica

Jak odczytać efektywny TrimBox w Delphi?

Wołaj GetPageBox(4, Dimension) na wybranej stronie. PDFlibPas stosuje domyślny łańcuch za ciebie, więc wynikiem jest efektywny TrimBox niezależnie od tego, czy strona ma własny. Dołóż do tego HasPageBox, gdy musisz wiedzieć, skąd wartość przyszła — zwykle robi to raport 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 = własny, 2 = odziedziczony
    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.

I GetPageBox, i SetPageBox pracują w bieżących ustawieniach współrzędnych dokumentu. Przykłady tutaj jadą na domyślnych: początek 0 (lewy dolny, zgodny z PDF user space) i punkty jako jednostka miary, więc wymiar Top to górna krawędź liczona w górę od dołu strony. Po SetOrigin(1) wymiary Top i Bottom są liczone zamiast tego w dół od góry strony, a po SetMeasurementUnits(1) każda wartość wraca w milimetrach. Szerokość i wysokość nie zależą od początku

Wyłapywanie boxów produkcyjnych utknących na węzłach /Pages

Od v3.539.44 API boxów nie widzi już TrimBoxa na węźle /Pages, co jest poprawne, ale narzędzie preflight zwykle chce taki plik zaraportować, a nie po cichu czytać go po specyfikacyjnemu. Węzły drzewa stron to zwykłe obiekty, więc niskopoziomowe API obiektów potrafi je znaleźć: przejdź numery obiektów aż do GetMaxObjectNumber, czytaj każdy przez GetObjectToString i szukaj słownika /Pages niosącego klucz boxa produkcyjnego. Druga połowa sprawdzenia to test per strona, na którym zależy PDF/X, a HasPageBox odpowiada na niego teraz tak, jak odpowiedziałby walidator PDF/X, bo rodzicielski TrimBox już się nie liczy

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. Boxy produkcyjne na węzłach drzewa stron: niestandardowe i ignorowane
  for ObjNum := 1 to Lib.GetMaxObjectNumber do
  begin
    Src := '';                                // wolne numery nie zwracają tekstu
    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żda strona potrzebuje własnego TrimBoxa albo ArtBoxa
  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. Opcjonalna naprawa: trim 6 x 9 in wewnątrz media box 6.25 x 9.25 in
  //    (punkty, początek w lewym dolnym rogu: Left, Top, Width, Height)
  if Missing > 0 then
    Log.Add(Format('TrimBox written on %d pages',
      [Lib.SetPageBoxRange('', 4, 9, 657, 432, 648)]));
end;

Dopasowanie tekstowe to pragmatyczny test, nie parser. Polega na tym, że PDFlibPas serializuje każdy wpis słownika jako klucz, jedną spację i wartość, co trzyma się dla obiektów czytanych z powrotem przez GetObjectToString. Krok naprawy zasługuje na decyzję, a nie na odruch: zabłąkana wartość rodzica może być dokładnie tym, co zamierzał autor, ale potwierdź to z zleceniem drukarskim, zanim zrobisz z tego oficjał. SetPageBoxRange z pustym zakresem stosuje box do każdej strony i zwraca liczbę zaktualizowanych stron. Gdy istniejący box strony jest tablicą pośrednią, którą może dzielić inna strona albo węzeł /Pages, SetPageBox daje tej stronie nową tablicę bezpośrednią zamiast przepisywać współdzielony obiekt. Ustawienie BleedBoxa, TrimBoxa albo ArtBoxa podbija też niezablokowany dokument do PDF 1.3, wersji, która te wpisy wprowadziła

Impozycja stron na TrimBoxie przez CapturePageEx

CapturePageEx(Page, 3) zamienia stronę w Form XObject, którego box ograniczający to efektywny TrimBox strony, a DrawCapturedPage kładzie tę formę na innej stronie w dowolnym rozmiarze. Od v3.539.42 opcja 3 na stronie bez TrimBoxa daje ci CropBox, tak jak opisuje referencja, zamiast MediaBoxa ze wszystkimi jego polami

Dwie własności przechwytywania kształtują kod. Przechwycenie jest destrukcyjne: przechwycona strona jest usuwana z dokumentu, a dokument nigdy nie może spaść do zera stron, więc dołącz pierwszy arkusz wyjściowy, zanim cokolwiek przechwycisz. Przechwytywanie działa też tylko wewnątrz jednego dokumentu, więc najpierw ściągnij wszystkie wejścia do jednego dokumentu; techniki z kolacjonowania i przeplatania źródeł PDF w jednym przebiegu zastosujesz tu wprost

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;

    // Efektywny rozmiar trimu strony 1 (ten layout zakłada jednolity trim)
    Lib.SelectPage(1);
    TrimW := Lib.GetPageBox(4, 2);
    TrimH := Lib.GetPageBox(4, 3);

    // Dołącz i wymiaruj pierwszy arkusz; NewPage wybiera nową stronę
    Lib.NewPage;
    Lib.SetPageDimensions(2 * TrimW, TrimH);

    // Każde przechwycenie usuwa stronę 1, więc następna strona źródłowa idzie w górę
    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;

    // Został tylko arkusz: dwie przycięte strony na arkusz, obok siebie
    Lib.SelectPage(1);
    for I := 0 to SourceCount - 1 do
    begin
      if (I > 0) and (I mod 2 = 0) then
        Lib.NewPage;                            // ten sam rozmiar co bieżący arkusz
      // Domyślny początek: Top to górna krawędź, mierzona od dołu
      Lib.DrawCapturedPage(Captures[I], (I mod 2) * TrimW, TrimH, TrimW, TrimH);
    end;
    Lib.SaveToFile(OutFile);
  finally
    Lib.Free;
  end;
end;

Przechwycenie po trimie obcina wszystko poza TrimBoxem, czego chcesz przy cyfrowej próbie albo layoucie cut-and-stack. Dla arkusza drukarskiego przycinanego po druku przechwytuj opcją 2, żeby przeżył spad, i rozstawiaj komórki o szerokość spadu. Ponieważ przechwycenie usuwa strony źródłowe, zakładki i linki wskazujące na nie tracą swoje cele, więc składaj do osobnego pliku wyjściowego zamiast edytować dokument, którego nawigacji wciąż potrzebujesz; wymiana stron bez psucia zakładek opisuje tę stronę operacji na stronach

Gdy źródło ma pozostać nietknięte, ImportPageAsFormXObject(SourceDocumentID, SourcePage, Options) przyjmuje te same wartości opcji od 0 do 4 (podaj Lib.SelectedDocument dla bieżącego dokumentu), zostawia drzewo stron źródła bez zmian, normalizuje dziedziczony obrót strony do macierzy formy i zwraca uchwyt, który DrawCapturedPage przyjmie. CapturePageEx nie cofa /Rotate, więc obrócone wejście potrzebuje najpierw tego kroku, a spłaszczanie obrotu strony bez psucia boxów stron pokazuje, co dzieje się z każdym boxem, gdy to robisz. Jedno ostrzeżenie dla wejść mogących nieść boxy produkcyjne na węzłach /Pages: ścieżka importu rozwiązuje swój box przez własne odszukiwanie przodków, osobne od dwóch ścieżek ustawionych w v3.539.44, więc sprawdź najpierw HasPageBox(4) na stronie źródłowej i podaj opcję 1 (CropBox), gdy zwróci 0. To trzyma wynik przy specyfikacji, a nie przy tym, jak akurat zapisano plik

Ściąga boxów stron

  • Efektywny CropBox: własny CropBox strony, inaczej najbliższy odziedziczony CropBox, inaczej efektywny MediaBox (ISO 32000-1 §14.11.2)
  • Efektywny BleedBox, TrimBox i ArtBox: własny wpis strony liściowej, inaczej efektywny CropBox
  • Tylko Resources, MediaBox, CropBox i Rotate dziedziczą z węzłów /Pages (§7.7.3.4, tabela 30); boxy produkcyjne na węzłach /Pages są ignorowane
  • 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 brak boxa, 1 własny box strony (bezpośredni albo pośredni), 2 odziedziczony MediaBox albo CropBox (bezpośredni albo pośredni)
  • CapturePageEx(Page, Options): 0 MediaBox, 1 CropBox z fallbackiem do MediaBoxa, od 2 do 4 BleedBox, TrimBox albo ArtBox z fallbackiem do CropBoxa
  • Zaktualizuj do v3.539.44 lub nowszego dla spójnych domyśleń i dziedziczenia w zapytaniach o boxy i przechwytywaniu

Boxy stron to miejsce, gdzie ciche domyślenia PDF spotykają tolerancje przeddruku mierzone ułamkami milimetra, a biblioteka albo stosuje te domyślenia tak samo wszędzie, albo wręcza ci dwie odpowiedzi na jedno pytanie. Pełne API boxów, przechwytywania i Form XObject jest udokumentowane na stronie produktu PDFlibPas PDF Library for Delphi