Łączenie stron (Page stitching) polega na nakładaniu zawartości istniejących stron na wspólny obszar roboczy (canvas), co różni się od operacji łączenia (mergowania) plików. Biblioteka losLab PDF Library implementuje łączenie stron w oparciu o obiekty Form XObject oraz modularne szablony znaków wodnych dla Delphi: procedury StitchPageOverlay, StitchPagesSideBySide i StitchPagesVertically umieszczają przechwycone strony w dowolnej skali z pełną dokładnością wektorową, a klasa TPDFlibCompositeWatermark nakłada tekstowe oraz obrazkowe znaki wodne na wybrany zakres stron za pomocą jednego wywołania. Jeśli w rzeczywistości potrzebujesz połączyć całe strony z kilku dokumentów w jeden plik, powinieneś skorzystać z metod łączenia opisanych w artykule o szybkim łączeniu plików PDF na poziomie bajtów; ten artykuł dotyczy łączenia zawartości stron w obrębie jednej przestrzeni współrzędnych
Wymagania, które prowadzą do tego rozwiązania, są prozaiczne i częste. Szkoła chce umieścić dwie strony egzaminu na jednym arkuszu poziomym, aby zmniejszyć zużycie papieru o połowę. Tłumacz weryfikujący lokalizację chce mieć stronę angielską i jej tłumaczenie obok siebie na jednej stronie, przewijane wspólnie. Zespół prawny wymaga, aby każdy projekt dokumentu był oznaczony trzema warstwami naraz: logo w rogu, ukośnym napisem CONFIDENTIAL na środku i małą stopką, wyłącznie na stronach od 1 do 3. Wszystkie te trzy przypadki sprowadzają się do tego samego problemu: umieszczenia już sformatowanej zawartości strony w innym miejscu i w innym rozmiarze bez degradowania jej do formy rastrowej
Dlaczego zwykłe łączenie strumieni zawartości PDF nie działa?
Naiwne łączenie polegające na skopiowaniu strumienia /Contents strony źródłowej do strony docelowej zawodzi z trzech powodów strukturalnych. Po pierwsze, kolidują ze sobą współrzędne: oba strumienie zakładają, że ich własny punkt początkowy znajduje się w lewym dolnym rogu ich własnego obszaru MediaBox, przez co wklejona zawartość trafia na istniejącą zawartość w niewłaściwym miejscu. Po drugie, kolidują nazwy zasobów: każda strona rozpoznaje operatory takie jak /F1 Tf oraz /Im0 Do w oparciu o własny słownik /Resources, a dwie strony mogą przypisywać tę samą nazwę do różnych czcionek lub obrazów, przez co jeden z elementów wyrenderuje się błędnie. Po trzecie, surowe kopiowanie strumienia nie daje możliwości skalowania; jedyne co można zrobić, to translacja (przesunięcie)
Obiekty Form XObject rozwiązują wszystkie te problemy jednocześnie i to na nich biblioteka losLab PDF Library opiera swoje mechanizmy łączenia. Standard ISO 32000-1 §8.10 definiuje obiekt Form XObject jako niezależny strumień zawartości z własnym słownikiem /Resources i własną przestrzenią współrzędnych, który jest umieszczany na stronie poprzez wywołanie jego nazwy przy użyciu aktualnej macierzy transformacji (transformation matrix). Metoda CapturePage pakuje całą istniejącą stronę — treść oraz zasoby — w obiekt XObject typu /Subtype /Form, a funkcje łączące pozycjonują go za pomocą macierzy cm: skalując, przesuwając i obracając w dowolny sposób. Ponieważ źródło pozostaje wektorowe, tekst nadal jest tekstem, a ścieżki pozostają ścieżkami przy każdym powiększeniu, co stanowi tę samą cechę, która nadaje niezależność od rozdzielczości grafice wektorowej i wzorcom cieniowania. Nic nie ulega rasteryzacji, a odizolowany słownik zasobów sprawia, że przechwycona strona nie koliduje ze stroną docelową o nazwę /F1
Jak połączyć dwie strony PDF na jednej stronie?
Bezpośrednią odpowiedzią jest funkcja StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight): przechwytuje ona obie strony źródłowe, tworzy nową, szerszą stronę i umieszcza oba przechwycone obrazy po lewej i prawej stronie z odstępem (gutter) między nimi, skalując każdy z nich do wysokości TargetHeight. Funkcja zwraca numer nowej strony (liczony od 1) lub 0 w przypadku niepowodzenia, a obie strony źródłowe są przechwytywane i ukrywane w ramach tej operacji. Wspomniany przypadek egzaminu z dwiema stronami na arkusz to zaledwie trzy linie kodu
var
Lib: TPDFlib;
NewPage: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('exam.pdf', '');
// Strony 1 i 2 na jednej nowej, szerszej stronie z odstępem 18 punktów,
// obie przeskalowane do wysokości wyjściowej 595 punktów (A4 poziomo)
NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
if NewPage > 0 then
Lib.SaveToFile('exam-2up.pdf');
finally
Lib.Free;
end;
end;
W przypadku kompozycji pionowej, funkcja StitchPagesVertically(PageRanges, Gap, TargetWidth) układa cały zakres stron w jedną długą stronę o układzie plakatowym. Parametr PageRanges przyjmuje standardową składnię zakresu, np. '1-3,5'. Każda strona źródłowa jest skalowana tak, aby jej szerokość odpowiadała TargetWidth, więc wysokość wnoszona przez każdą ze stron to jej własna wysokość pomnożona przez jej współczynnik skali, a wysokość strony wyjściowej to suma tych wartości oraz odstępów Gap między poszczególnymi parami. Jest to układ idealny dla dokumentów recenzenckich z ciągłym przewijaniem lub długich rachunków składanych z materiałów formatu Letter
Jak nałożyć jedną stronę na drugą?
Funkcja StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) nakłada jedną istniejącą stronę na inną istniejącą stronę, zamiast tworzyć nową. Strona źródłowa jest przechwytywana jako obiekt Form XObject i rysowana na stronie docelowej w podanym prostokącie z kryciem (opacity) od 0 do 1 (gdzie 1 oznacza pełne krycie). Szerokość i wysokość są parametrami swobodnymi, więc naniesiona treść rozciągnie się, jeśli proporcje będą różnić się od proporcji strony źródłowej; przekaż parę proporcjonalną, aby uzyskać niezniekształconą miniaturę źródła. Typowym zastosowaniem jest dokument, którego blok zatwierdzenia jest prowadzony jako osobna strona i nanoszony na okładkę
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('report.pdf', '');
// Strona 5 zawiera blok zatwierdzenia; umieść go na stronie 1
// w prostokącie 260 x 180 przy 45% krycia. Strona 5 zostaje skonsumowana:
// zostaje przechwycona i ukryta, so pages 6..n become 5..n-1
if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
Lib.SaveToFile('report-approved.pdf');
finally
Lib.Free;
end;
end;
Co dzieje się z numeracją stron po przechwyceniu?
Metoda CapturePage konsumuje stronę źródłową: jej zawartość zostaje przeniesiona do obiektu XObject, a pusta teraz strona zostaje ukryta, przez co numer każdej strony powyżej źródła natychmiast przesuwa się w dół o jeden. Jest to najważniejsza zasada semantyczna w całym API łączenia stron. Biblioteka losLab PDF Library wykonuje te obliczenia wewnątrz każdego wywołania łączenia stron — StitchPageOverlay koryguje indeks docelowy, gdy strona docelowa znajduje się powyżej źródłowej, a StitchPagesSideBySide przechwytuje najpierw stronę o wyższym numerze, dzięki czemu indeks drugiego przechwycenia pozostaje poprawny. Mimo to Twój własny kod musi uwzględniać te przesunięcia między wywołaniami. Dwie reguły zapewniają poprawność kodu przy wielokrotnym łączeniu: zapisz wymiary i numery stron, których będziesz potrzebować, zanim wykonasz pierwsze przechwycenie, ponieważ ulegną one zmianie. A gdy samodzielnie przechwytujesz kilka stron w pętli, wykonuj iterację w kolejności malejącej (od ostatniej do pierwszej), aby ukrycie późniejszej strony nie zaburzyło indeksu wcześniejszej, która nie została jeszcze przechwycona
Jak nałożyć wielowarstwowy znak wodny na zakres stron?
Warstwa szablonów znaków wodnych w module PDFlibWatermark.pas pozwala przekształcić zasadę „zdefiniuj raz, zastosuj na wielu stronach” w obiekty. Klasa TPDFlibWatermarkBase zawiera to, co współdzielą wszystkie znaki wodne: dziewięciopunktowe pozycjonowanie Position (od wpTopLeft do wpBottomRight, a także wpCustom dla surowych współrzędnych X/Y), kąt Angle w stopniach, krycie Opacity od 0 do 1 oraz zakres stron PageRange w formie ciągu znaków (gdzie brak wartości oznacza wszystkie strony). Klasa TPDFlibTextWatermark dodaje tekst, identyfikator czcionki (domyślnie Helvetica), rozmiar oraz kolor RGB; klasa TPDFlibImageWatermark dodaje identyfikator obrazu z uprzednio wczytanego pliku oraz szerokość i wysokość wstawienia. Kompozyt TPDFlibCompositeWatermark przechowuje listę podrzędnych znaków wodnych i stosuje je rekurencyjnie, a metoda TPDFlib.ApplyWatermark to jednostopniowy punkt wejścia, który uwzględnia zakres stron zdefiniowany w szablonie i zwraca liczbę ostemplowanych stron. Trzywarstwowy znak wodny wygląda następująco
var
Lib: TPDFlib;
Logo: TPDFlibImageWatermark;
Diagonal, Footer: TPDFlibTextWatermark;
Stack: TPDFlibCompositeWatermark;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('draft.pdf', '');
Stack := TPDFlibCompositeWatermark.Create;
try
Logo := TPDFlibImageWatermark.Create;
Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
Logo.Position := wpTopRight;
Logo.Width := 90;
Logo.Height := 32;
Stack.Add(Logo);
Diagonal := TPDFlibTextWatermark.Create;
Diagonal.Text := 'CONFIDENTIAL';
Diagonal.Position := wpCenter;
Diagonal.Angle := 45;
Diagonal.FontSize := 60;
Diagonal.Opacity := 0.15;
Stack.Add(Diagonal);
Footer := TPDFlibTextWatermark.Create;
Footer.Text := 'Internal review copy';
Footer.Position := wpBottomCenter;
Footer.FontSize := 9;
Footer.Opacity := 0.6;
Stack.Add(Footer);
Stack.PageRange := '1-3'; // elementy podrzędne dziedziczą ten zakres
Lib.ApplyWatermark(Stack);
Lib.SaveToFile('draft-marked.pdf');
finally
Stack.Free; // kompozyt zwalnia swoje elementy podrzędne
end;
finally
Lib.Free;
end;
end;
Warto doprecyzować dwa szczegóły kompozycji. Dziedziczenie zakresu działa w jedną stronę i jest nieinwazyjne: element podrzędny z pustym polem PageRange tymczasowo dziedziczy zakres kompozytu podczas wywołania Apply, natomiast element ze zdefiniowanym własnym zakresem zachowuje go, dzięki czemu można umieścić stopkę na każdej stronie, podczas gdy ukośny stempel pokryje tylko sekcję szkicu. Z kolei prawo własności podąża za kompozytem: destruktor TPDFlibCompositeWatermark.Destroy zwalnia swoje elementy podrzędne, więc zwalniasz kompozyt tylko raz, jak w powyższym przykładzie
Ograniczenia, o których warto wiedzieć
Obowiązują dwa istotne ograniczenia, wynikające bezpośrednio ze sposobu działania mechanizmu. Po pierwsze, przechwycenie obejmuje wyłącznie strumień zawartości strony oraz zasoby. Adnotacje, linki, pola formularzy i komentarze są zapisane jako osobne obiekty w słowniku strony, a nie w strumieniu treści, więc nie są przenoszone do obiektu XObject; połączony układ zachowuje widoczną treść strony, lecz nie interaktywną warstwę stron źródłowych. Po drugie, dziewięciopunktowe pozycjonowanie tekstu jest przybliżeniem: bez pełnego silnika składu tekstu biblioteka nie zna dokładnej szerokości wyrenderowanego tekstu, stąd szacuje szerokość pola tekstowego na 40% szerokości strony przy wyznaczaniu pozycji takich jak wpCenter. Jest to wystarczająco dokładne dla wyśrodkowania krótkiej etykiety, lecz dla precyzyjnego pozycjonowania długich ciągów znaków należy przełączyć się na wpCustom i samodzielnie obliczyć współrzędne
Skala nie stanowi tu praktycznego ograniczenia, ponieważ przechwytywanie i łączenie operują na referencjach obiektów, a nie na ponownym kodowaniu zawartości stron. Jeśli pliki źródłowe mają setki megabajtów, przed połączeniem obowiązują te same zasady opisane w przewodniku po łączeniu i dzieleniu dużych plików z dostępem bezpośrednim. Opisane tu funkcje łączenia stron oraz klasy szablonów znaków wodnych wchodzą w skład aktualnej wersji losLab PDF Library dla Delphi, C# i VB.NET, obok niskopoziomowych funkcji pierwotnych CapturePage, DrawCapturedPage oraz DrawCapturedPageMatrix na wypadek, gdyby potrzebny był układ niewspierany przez trzy gotowe wywołania