Nakładanie znaku wodnego lub logo na każdą stronę dokumentu wygląda na pięciominutowe zadanie, dopóki nie sprawdzisz rozmiaru wyjściowego pliku. Oczywistym podejściem jest przejście po stronach i na każdej z nich ponowne zbudowanie tych samych obiektów tekstowych lub graficznych. Wizualnie to zadziała, ale jest to bardzo nieekonomiczne. Ukośny znak wodny "DRAFT" narysowany bezpośrednio na stustronicowym raporcie to sto kopii tych samych danych ścieżki i tekstu w strumieniach zawartości, a zapisany plik będzie zawierał każdą z nich.
Obiekt Form XObject to struktura, którą format PDF udostępnia właśnie po to, by tego uniknąć. Opakowuje on fragment zawartości wielokrotnego użytku — całą stronę lub niewielki szablon — w pojedynczy, nazwany obiekt, który można rysować wielokrotnie w wielu miejscach. Zawartość zapisywana jest w pliku tylko raz. Każda strona wymagająca pieczątki przechowuje krótką instrukcję o treści "narysuj tutaj obiekt XObject N przy użyciu tej transformacji". Stustronicowy znak wodny dodaje wtedy do pliku jeden obiekt zawartości zamiast stu, co decyduje o tym, czy rozmiar dokumentu rośnie liniowo wraz z liczbą stron, czy nie. Znaki wodne, pieczątki z logo, szablony numeracji stron i pieczęcie to ten sam rodzaj problemu, a Form XObject jest odpowiednim narzędziem do każdego z nich.
Dlaczego jeden zapisany obiekt jest lepszy niż sto ponownych rysowań
Oszczędność ma charakter strukturalny, a nie tylko kosmetyczny. Strona PDF jest renderowana poprzez wykonanie jej strumienia zawartości — sekwencji operatorów rysowania. Rysując pieczątkę na każdej stronie osobno, dodajesz pełną sekwencję operatorów tej pieczątki do strumienia każdej strony, a bajty są duplikowane tyle razy, ile jest stron. Obiekt Form XObject przenosi te operatory do jednego strumienia zapisanego raz w dokumencie. Referencja zachowywana przez poszczególne strony jest niewielka: odkłada na stos macierz transformacji, wywołuje XObject i przywraca stan. Liczba stron nie mnoży już kosztów zapisu grafiki.
Ma to największe znaczenie, gdy pieczątka jest skomplikowana. Wektorowa pieczęć z setkami segmentów ścieżek lub mapa bitowa logo są kosztowne w przechowywaniu. Po jednokrotnym zapisaniu i powołaniu się na referencję, kosztowny element jest opłacany tylko raz, a narzut na stronę to zaledwie kilka bajtów wywołania. Wizualny efekt na stronie jest identyczny z bezpośrednim narysowaniem i o to właśnie chodzi. Czytelnik nie zauważy różnicy, natomiast rozmiar pliku jak najbardziej.
Przechwytywanie strony do obiektu XObject
PDFium buduje obiekt wielokrotnego użytku na podstawie istniejącej strony. Źródłem jest strona w otwartym dokumencie, niewielki jednostronicowy plik PDF zawierający tylko grafikę znaku wodnego lub konkretna strona większego pliku. Funkcja CreateXObjectFromPage przechwytuje zawartość tej strony źródłowej do uchwytu wielokrotnego użytku, który staje się własnością dokumentu docelowego, na który nakładasz pieczątkę.
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
Sygnatura to CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metoda ta w przypadku błędu zwraca nil zamiast zgłaszać wyjątek, więc jawne sprawdzenie nie jest opcjonalne. Zwracany uchwyt to obiekt TPdfXObject, którego stajesz się właścicielem, a dwa powiązane z nim ograniczenia dotyczące czasu życia to część tego zadania, która najczęściej sprawia problemy programistom — stąd poświęcono im osobną sekcję poniżej.
Umieszczanie pieczątki na stronie
Przechwycony obiekt XObject sam z siebie nic nie robi. Aby się pojawił, wstawiasz jego kopię na bieżącą stronę dokumentu za pomocą metody InsertFormObjectFromXObject. Wywołanie to zwraca niskopoziomowy obiekt strony, czyli FPDF_PAGEOBJECT, a zwrócony uchwyt służy do ustalenia pozycji. Bez transformacji pieczątka trafia na początek układu współrzędnych strony źródłowej, co rzadko jest pożądanym miejscem.
Ponieważ metoda InsertFormObjectFromXObject wstawia jedną kopię na wywołanie i za każdym razem zwraca nowy obiekt strony, możesz narysować ten sam XObject kilkukrotnie na jednej stronie przy różnych transformacjach, a zapisana zawartość nadal będzie liczona w pliku tylko raz. Logo w rogu strony i blady znak wodny na całej stronie mogą pochodzić z tego samego przechwyconego obiektu.
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;
Jeden szczegół dotyczący własności zapewnia bezpieczeństwo czyszczenia pamięci. Po wstawieniu obiekt strony należy do samej strony, a nie do obiektu XObject. Zwolnienie XObject w późniejszym czasie nie unieważnia wykonanych wcześniej rozmieszczeń. Dzięki temu opisywana poniżej kolejność twórz-rozmieść-zwolnij działa prawidłowo.
Reguła czasu życia uchwytu, która bywa pułapką
Uchwytem XObject rządzą dwa ograniczenia i zignorowanie każdego z nich powoduje błędy wyglądające na niezwiązane z przyczyną. Po pierwsze, dokument źródłowy musi być aktywny w momencie wywołania CreateXObjectFromPage. Przechwytywanie odczytuje zawartość strony źródłowej z aktywnego dokumentu źródłowego, więc ten dokument i jego strona muszą być otwarte i poprawne w momencie tworzenia uchwytu. Po drugie — i to jest szczegół, który zaskakuje programistów — uchwyt musi zostać zwolniony przed zamknięciem strony źródłowej, a w praktyce przed zamknięciem lub zwolnieniem dokumentu źródłowego, z którego pochodzi.
Wynika to z faktu, że XObject jest referencją do struktury, której właścicielem wciąż pozostaje dokument źródłowy. Nie jest to samodzielna, odizolowana kopia, którą można przenosić po zamknięciu źródła. Jeśli najpierw zamkniesz źródło, uchwyt będzie wskazywał na usuniętą zawartość, więc jego późniejsze zwolnienie lub dowolne inne użycie będzie operować na niepoprawnej pamięci. Objawem jest klasyczny błąd wiszącego uchwytu (dangling handle): naruszenie dostępu (access violation) przy zamykaniu programu lub nieregularne błędy pamięci przemieszczające się w zależności od kolejności alokacji, ze stosem wskazującym na kod czyszczący, a nie na linię, która rzeczywiście wywołała problem. Rozwiązaniem jest kolejność działań, a nie defensywne programowanie. Zbuduj XObject, wstaw go na każdą stronę, która go wymaga, zwolnij XObject i dopiero wtedy zamknij dokument źródłowy. Destruktor TPdfXObject zwalnia powiązany uchwyt PDFium za Ciebie, więc Twoim jedynym zadaniem jest zwolnienie wrappera we właściwym czasie.
Macierz i co oznacza jej sześć liczb
Rozmieszczenie to dwuwymiarowa transformacja afiniczna — ta sama, której format PDF używa w każdym miejscu do pozycjonowania zawartości (ISO 32000-1, sekcja 8.3.4). Składa się z sześciu liczb zapisywanych jako a, b, c, d, e, f, a PDFium udostępnia je jako rekord FS_MATRIX. Mapują one punkt z przestrzeni własnej obiektu do przestrzeni strony:
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
Możesz uzupełnić te sześć wartości ręcznie, ale samodzielne ich składanie to miejsce, w którym łatwo o błąd przy obracaniu (rotation), ponieważ obrót miesza ze sobą wszystkie cztery współczynniki a, b, c, d. Wrapper TPdfMatrix składa typowe operacje za Ciebie i mnoży je na bieżąco, dzięki czemu Translate, Scale i Rotate łączą się w kolejności ich wywoływania. Ukośny znak wodny to obrót, po którym następuje przesunięcie w celu wycentrowania; logo w rogu to skalowanie, po którym następuje przesunięcie. Gdy macierz jest gotowa, przekaż jej surową wartość do FPDFPageObj_SetMatrix(PageObj, M.Handle), gdzie M.Handle to niskopoziomowa struktura FS_MATRIX. Niskopoziomowa funkcja FPDFPageObj_Transform, przyjmująca sześć wartości bezpośrednio jako typ double, jest dostępna, jeśli wolisz przekazywać liczby zamiast tworzyć wrapper.
Stemplowanie każdej strony we właściwej kolejności
Pełny schemat łączy te elementy przy zachowaniu kolejności wymaganej przez regułę czasu życia. Otwierasz oba dokumenty, przechwytujesz pieczątkę raz, przechodzisz po stronach docelowych, wybierając każdą z nich po kolei, wstawiasz i pozycjonujesz kopię, następnie zwalniasz XObject, zapisujesz dokument docelowy i na samym końcu pozwalasz na zamknięcie dokumentu źródłowego.
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 1. Capture the artwork once. Stamp is active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := i; // make page i current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
Kluczową rolę odgrywa tutaj struktura bloków try. Wewnętrzna sekcja finally zwalnia obiekt XObject, zanim sterowanie dotrze do zewnętrznej sekcji finally zwalniającej Stamp, dzięki czemu uchwyt jest zawsze usuwany w momencie, gdy jego źródło wciąż żyje — nawet jeśli w pętli wystąpi wyjątek. Prawidłowe zagnieżdżenie sprawia, że reguła czasu życia realizuje się sama. (Użyj dowolnego selektora bieżącej strony udostępnianego przez Twój komponent; treść pętli pozostaje taka sama).
Nakładanie pieczątek to tylko jeden z elementów większego pakietu narzędzi do tworzenia i edycji zawartości stron. Jeśli Twoja pieczątka jest obrazem, a nie przechwyconą stroną, artykuł konwersja obrazów do dokumentów PDF z PDFium opisuje, jak najpierw umieścić tę mapę bitową w dokumencie. A gdy obok widocznej pieczątki chcesz dołączyć plik, poradnik praca z załącznikami PDF w Delphi przedstawia kwestie związane z osadzaniem plików. Wszystkie te funkcje są dostarczane wraz z pakietem PDFium Component dla Delphi i C++Builder obok interfejsów API do renderowania, edycji i obsługi dokumentów omówionych w innych miejscach na tym blogu.