Artykuł techniczny

Reużywalne pieczątki stron poprzez Form XObjects za pomocą PDFium

Odbicie znaku wodnego lub logo na każdej stronie dokumentu wygląda jak 5-minutowe zadanie, dopóki nie otworzysz wyniku w inspektorze rozmiaru pliku. Oczywistym podejściem jest przejście przez strony i, na każdej z nich, ponowne zbudowanie tych samych obiektów tekstu lub obrazu. Działa to wizualnie, ale jest to marnotrawstwo w sposób, który się nawarstwia. Ukośny znak wodny „DRAFT” narysowany bezpośrednio w studwustronicowym raporcie to sto kopii tej samej ścieżki i danych tekstowych znajdujących się w strumieniach zawartości, a zapisany plik zawiera każdą z nich

Form XObject jest konstrukcją, którą format PDF dostarcza, aby dokładnie tego uniknąć. Opakowuje fragment zawartości wielokrotnego użytku, całą stronę lub mały szablon, w pojedynczy nazwany obiekt, który można namalować wielokrotnie w wielu miejscach. Zawartość żyje w pliku tylko raz. Każda strona, która chce mieć pieczątkę, przechowuje krótką instrukcję z komunikatem „namaluj tutaj XObject N, używając tej transformacji”. Stustronicowy znak wodny dodaje wtedy jeden obiekt zawartości do pliku zamiast stu, a to stanowi różnicę pomiędzy dokumentem rosnącym liniowo wraz ze stronami a takim, który nie rośnie. Znaki wodne, logo, szablony numerów stron i pieczęcie to ten sam kształt problemu, a Form XObject to właściwe narzędzie na każdy z nich

Dlaczego jeden przechowany obiekt przewyższa sto przerysowań

Oszczędność ma charakter strukturalny, a nie kosmetyczny. Strona PDF renderuje się poprzez wykonanie strumienia zawartości, czyli sekwencji operatorów rysowania. Kiedy przerysowujesz pieczątkę na każdej stronie, dołączasz pełną sekwencję operatorów dla tej pieczątki do strumienia każdej strony, a bajty duplikują się w zależności od liczby stron. Form XObject przenosi te operatory do jednego strumienia, raz zapisanego w dokumencie. Referencja, którą utrzymuje pojedyncza strona, jest niewielka: wprowadza ona macierz transformacji, wywołuje XObject i przywraca stan. Liczba stron nie mnoży już kosztów opracowania graficznego

Ma to największe znaczenie, gdy pieczątka jest ciężka. Pieczęć wektorowa z setkami segmentów ścieżki lub mapa bitowa z logo są drogie w przechowywaniu. Dzięki pojedynczemu przechowywaniu i referencji, za ciężką część płaci się tylko raz, a narzut na stronę to kilka bajtów wywołania. Efekt wizualny na stronie jest identyczny z bezpośrednim przerysowaniem, a o to właśnie chodzi. Czytelnik nie zauważy różnicy; wielkość pliku - wręcz przeciwnie

Przechwytywanie strony do XObject

PDFium buduje obiekt wielokrotnego użytku na podstawie istniejącej strony. Źródłem jest strona w aktualnie otwartym dokumencie, mały jednostronicowy plik PDF zawierający tylko znak wodny lub konkretna strona większego pliku. CreateXObjectFromPage przechwytuje zawartość tej strony źródłowej do uchwytu wielokrotnego użytku, który należy do dokumentu docelowego, tego, w którym nakładasz pieczątkę

var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := 'Report.pdf';
    Dest.Active := True;
    Stamp.FileName := 'Watermark.pdf';   // one page of artwork
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 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) ...

Sygnaturą jest CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metoda zgłasza wyjątek, jeśli dokument źródłowy nie jest Active, i zwraca nil zamiast zgłaszać wyjątek, gdy PDFium nie jest w stanie zbudować obiektu, więc jawne sprawdzenie powyżej nie jest opcjonalne. Uchwyt, który powraca, jest obiektem TPdfXObject, który posiadasz, a powiązane z nim dwa ograniczenia cyklu życia to ta część ćwiczenia, która łapie użytkowników w pułapkę, więc poświęcono im osobną sekcję poniżej

Umieszczanie pieczątki na stronie

Przechwycony obiekt XObject sam z siebie nic nie robi. Aby go wyświetlić, umieszczasz kopię tego obiektu na bieżącej stronie dokumentu (wybranej przez indeksowaną od 1 właściwość PageNumber), używając InsertFormObjectFromXObject. Wywołanie to zwraca obiekt podstawowy strony (underlying page object), czyli FPDF_PAGEOBJECT, i za pomocą zwróconego uchwytu ustalasz pozycję docelową. Bez przekształcenia, pieczątka ląduje w punkcie początkowym we własnych współrzędnych strony źródłowej, czyli rzadko w pożądanym miejscu

Ponieważ InsertFormObjectFromXObject wstawia jedną kopię podczas każdego wywołania i każdorazowo oddaje nowy obiekt strony, możesz namalować ten sam XObject kilka razy na jednej stronie używając różnych przekształceń, a w pamięci zawartość zliczana jest tylko raz. Logo w narożniku oraz półprzezroczysty, całostronicowy znak wodny mogą pochodzić z tego samego przechwyconego obiektu

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
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);
    RawM := M.Handle;
    if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
      raise Exception.Create('Cannot assign the stamp matrix');
  finally
    M.Free;
  end;
  Dest.UpdatePage;   // commit this page's edits to its content stream
  // if not Dest.SaveAs(...) then ... when every page is done.
end;

Dwa szczegóły administracyjne sprawiają, że jest to bezpieczne. Po pierwsze, raz wprowadzony obiekt (page object) przynależy do strony, a nie do XObject. Zwolnienie obiektu XObject nie unieważnia pozycjonowań wykonanych uprzednio. To pozwala na to, aby kolejność utwórz-umieść-zwolnij zaprezentowana niżej, faktycznie zadziałała. Po drugie, wstawianie i pozycjonowanie tylko zmienia listę obiektów strony w pamięci; to UpdatePage jest mechanizmem, który wykonuje serializację owej listy w strumień zawartości strony (page's content stream), więc strona poddana edycji bez tego wezwania, zostanie zachowana, jakby nigdy nie zastosowano żadnej z tych modyfikacji

Zasada cyklu życia uchwytów (handle), która stanowi problem

Uchwytem XObject rządzą dwa ograniczenia, a zignorowanie któregokolwiek z nich powoduje awarię o objawach na pozór niepowiązanych z podłożem problemu. Po pierwsze, dokument źródłowy musi być aktywny w momencie, gdy wywołujesz CreateXObjectFromPage. Przechwytywanie odczytuje zawartość strony źródłowej z aktywnego dokumentu źródłowego, tak że dany dokument i strona z nim muszą być cały czas otwarte i poprawne przy generowaniu uchwytu. Po drugie, z czym ludzie najczęściej mają kłopot, to że sam 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

Powodem jest to, że XObject jest referencją do struktury, którą nadal posiada dokument źródłowy. To nie jest oderwana, samowystarczalna kopia, którą można przenosić po usunięciu źródła. Zamknij najpierw źródło, a uchwyt pozostanie wskazując na zawartość, która została zniszczona, więc zwalnianie go później lub jakiekolwiek inne użycie operuje na pamięci, która nie jest już ważna. Objawem jest klasyczny symptom wiszącego wskaźnika (dangling handle): naruszenie dostępu (access violation) podczas zamykania lub sporadyczne uszkodzenie, które przenosi się w zależności od kolejności alokacji, ze stosem wskazującym na kod czyszczący, a nie na linię, która faktycznie spowodowała problem. Poprawką jest kolejność, a nie kodowanie defensywne. Zbuduj XObject, wstaw go na każdą stronę, która go potrzebuje, zwolnij XObject i dopiero wtedy zamknij dokument źródłowy. Destruktor TPdfXObject zwalnia leżący pod spodem uchwyt PDFium za ciebie, więc zwolnienie wrappera we właściwym czasie jest całym twoim obowiązkiem

Macierz i co oznaczają jej sześć liczb

Umieszczenie to transformacja afiniczna 2D, ta sama, której PDF używa wszędzie do pozycjonowania treś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 na przestrzeń 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 wypełnić te sześć wartości ręcznie, ale to przy ręcznym ich składaniu obrót idzie źle, ponieważ obraca on wszystkie cztery współczynniki `a, b, c, d`. Wrapper TPdfMatrix z modułu FPdfMatrix składa za ciebie popularne operacje i mnoży je prawostronnie (post-multiplies) w trakcie działania, więc Translate, Scale i Rotate łączą się w łańcuch w takiej kolejności, w jakiej je wywołujesz. Ukośny znak wodny to obrót z następującym przesunięciem, aby go wyśrodkować; logo w rogu to skalowanie, po którym następuje przesunięcie. Gdy macierz jest gotowa, skopiuj jej surową wartość, właściwość Handle typu FS_MATRIX, do zmiennej lokalnej i przekaż ją do FPDFPageObj_SetMatrix; deklaracja importu traktuje macierz jako parametr var, więc właściwość nie może być do niej bezpośrednio przekazana, a na skutek błędu zwraca wynik 0. Wywołanie niższego rzędu FPDFPageObj_Transform, które przyjmuje bezpośrednio jako double rzeczone wprost sześć wartości, jest dostępne gdy zamiast konstruowania wrapper'u po prostu wolisz podać parametry

Pieczętowanie każdej strony, w odpowiedniej kolejności

Pełny wzorzec składa elementy razem z kolejnością wymaganą przez zasadę cyklu życia. Otwórz oba dokumenty, przechwyć pieczątkę raz, przejdź przez strony docelowe, po kolei ustawiając opartą na jedynce właściwość PageNumber, wstawiając i pozycjonując kopię, zatwierdzając każdą stronę za pomocą UpdatePage, a następnie zwolnij XObject, potem zapisz za pomocą SaveAs i pozwól, aby dokument źródłowy zamknął się jako ostatni

procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
  I: Integer;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := ASource;
    Dest.Active := True;
    Stamp.FileName := AStamp;
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 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. PageNumber is 1-based.
      for I := 1 to Dest.PageCount do
      begin
        Dest.PageNumber := 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
          RawM := M.Handle;
          FPDFPageObj_SetMatrix(PageObj, RawM);
        finally
          M.Free;
        end;
        Dest.UpdatePage;                     // commit this page's edits
      end;
    finally
      XObject.Free;                          // 3. free BEFORE Stamp closes
    end;

    // 4. Write the result while Dest is still open.
    if not Dest.SaveAs(AOutput) then
      raise Exception.Create('Could not save ' + AOutput);
  finally
    Stamp.Free;                              // source closes last
    Dest.Free;
  end;
end;

Kształt bloków try wykonuje tu właściwą pracę. Wewnętrzne finally uwalnia XObject, zanim sterowanie będzie mogło dotrzeć do zewnętrznego finally, które zwalnia Stamp, więc uchwyt jest zawsze zwalniany, podczas gdy jego źródło wciąż żyje, nawet jeśli wyjątek zostanie rzucony w środku pętli. Zrób poprawnie to zagnieżdżenie, a reguła żywotności sama się ureguluje

Pieczętowanie to zaledwie skrawek większego zestawu narzędzi do tworzenia i edycji zawartości stron. Jeśli twoja pieczątka jest sama w sobie obrazem, a nie przechwyconą stroną, konwertowanie obrazów na dokumenty PDF z PDFium opisuje, jak najpierw umieścić tę mapę bitową w dokumencie. A kiedy tym, co chcesz nieść razem z widoczną pieczątką, jest plik, a nie atrament na stronie, praca z załącznikami PDF w Delphi pokazuje stronę osadzonych plików. Wszystko to jest dostarczane wraz z PDFium Component dla Delphi i C++Buildera, obok interfejsów API renderowania, edycji i dokumentów opisanych w innych miejscach tego bloga