Artykuł techniczny

Redakcja PDF i układ N-up w Delphi z HotPDF

Na biurko trafia zadanie: weź paczkę już wyrenderowanych zestawień, zaczernij numery kont i wyślij po dwie strony na arkusz, żeby oszczędzić papier. Obie połowy tego zadania to chirurgia na strumieniach zawartości PDF, którego sam nie tworzyłeś, więc nie ma tu przyjaznego canvasu strony do rysowania ani menedżera fontów, na którym można się oprzeć. Edytujesz bezpośrednio graf obiektów załadowanego dokumentu, dopisując surowe operatory rysowania do strony rozłożonej przez inne narzędzie. HotPDF wystawia do tego dokładnie dwa punkty wejścia, a bardziej niebezpieczny z nich jest ten, który wygląda niewinnie

HotPDF to natywny komponent VCL do pracy z PDF dla Delphi i C++Builder. Dziewiąta tura API dla załadowanych dokumentów dodała pierwsze metody, które tworzą zupełnie nową zawartość na stronie otwartej z dysku zamiast zbudowanej od zera. Dwie z nich są tutaj tematem: RedactLoadedRect, która maluje nieprzezroczysty prostokąt na wybranym obszarze, oraz StitchLoadedPage, która skaluje jedną stronę i rysuje ją na drugiej. Obie działają przez zapis operatorów strumienia zawartości z ISO 32000-1 §8.5 do strumienia /Contents strony. Zrozumienie, co te operatory robią, a równie ważne czego nie robią, to różnica między działającym narzędziem a wyciekiem danych

Dopisywanie operatorów do wczytanej strony

Gdy budujesz stronę zwykłym API HotPDF, komponent sam jest właścicielem strumienia zawartości i serializuje za ciebie wywołania TextOut oraz wektorowe. Załadowana strona jest inna: jej /Contents jest istniejącym obiektem strumienia, być może współdzielonym, być może będącym częścią tablicy zawartości, i musisz wpiąć się w niego tak, by nie uszkodzić tego, co już tam jest. Dziewiąta tura wprowadziła trzy małe helpery, które robią to bezpiecznie. NewIndirectStream alokuje nowy pośredni THPDFStreamObject z pustym buforem i wpisem /Length 0; ResolveLoadedStream schodzi po referencji pośredniej do właściwego strumienia bazowego; a AppendLoadedStream dopisuje surowe bajty na końcu strumienia i przepisuje /Length, tak aby zapisany obiekt pozostał poprawnie uformowany

Schemat obu publicznych metod jest taki sam. Znajdź /Contents strony, rozwiąż je do strumienia, a jeśli nie ma użytecznego strumienia, utwórz go i podepnij. Następnie dopisz operatory. Ponieważ nowe bajty trafiają na koniec strumienia, model malarski gwarantuje, że zostaną wyrenderowane ponad wszystkim, co narysował pierwotny układ. Ta kolejność jest całym mechanizmem stojącym za prostokątem maskującym, i z tego samego powodu taki prostokąt nie jest tym, co większość ludzi zakłada

RedactLoadedRect: nieusuwająca osłona, nie kasowanie

RedactLoadedRect przyjmuje zerowy indeks strony, cztery współrzędne w przestrzeni użytkownika i trzy składowe koloru w zakresie 0-1:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('statement.pdf') > 0 then
    begin
      // Cover the account-number band on page 1 with solid black.
      // Coordinates are PDF user space: origin bottom-left, points.
      Pdf.RedactLoadedRect(0, 56, 690, 320, 706, 0, 0, 0);
      Pdf.SaveLoadedDocument('statement-covered.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Pod spodem metoda emituje do strumienia zawartości trzy operatory: ustawienie koloru wypełnienia w DeviceRGB (r g b rg), ścieżkę prostokąta (x y w h re) oraz wypełnienie (f). Szerokość i wysokość są wyliczane jako X2 - X1 oraz Y2 - Y1, więc przekazujesz dwa przeciwległe narożniki, a metoda sama oblicza rozmiar. Podaj 0, 0, 0 jako kolor, a dostaniesz czarny pasek. Podaj 1, 1, 1 dla białego, dopasowanego do białej strony. Współrzędne należą do własnej przestrzeni użytkownika załadowanej strony, co oznacza lewy dolny róg jako początek i punkty jako jednostki. Oznacza to też, że do precyzyjnego umieszczenia czegokolwiek potrzebujesz /MediaBox strony; GetLoadedPageBox razem z pbMediaBox daje ci te informacje

Przeczytaj to dwa razy: wypełniony prostokąt zakrywa zawartość wizualnie, ale jej nie usuwa. Tekst, obraz albo grafika wektorowa pod prostokątem nadal istnieją w PDF, nadal są w grafie obiektów, nadal można je wydobyć przez skopiowanie strony, uruchomienie ekstraktora tekstu albo zwykłe usunięcie twojego prostokąta ze strumienia zawartości. To maskowanie wizualne, a nie redakcja w sensie prawnym czy bezpieczeństwa. Jeżeli ukrywasz naprawdę wrażliwe dane, takie jak numery kont, dane medyczne, tożsamości czy cokolwiek regulowanego, zakrycie ich czarnym prostokątem i wysłanie pliku jest wyciekiem danych czekającym na odkrycie. Prawdziwa redakcja wymaga usunięcia bazowych obiektów zawartości, a nie malowania po nich

Nazwa metody zawiera słowo "Redact" i dobrze ostrzega przed tym, jak wynik będzie błędnie odczytywany, ale nie obiecuje, co zostanie usunięte. Sama implementacja uczciwie to komentuje: nazywa siebie "visual redaction primitive" i zaznacza, że redakcja usuwająca zawartość wymaga interpretera strumienia zawartości, który przejdzie po istniejących operatorach i przepisze je na nowo. Ścieżka załadowanego dokumentu w HotPDF tego tutaj nie robi. Dlatego bezpieczna zasada jest wąska: używaj RedactLoadedRect do niewrażliwego maskowania kosmetycznego, na przykład do ukrycia znaku wodnego wersji roboczej, wyczyszczenia fragmentu przed zrzutem ekranu albo zasłonięcia przestarzałego logo w wewnętrznej korekcie. W chwili, gdy to, co leży pod prostokątem, miałoby znaczenie po wycieku, ta metoda przestaje być właściwym narzędziem, a poprawną odpowiedzią jest ponowne wygenerowanie dokumentu bez tych danych albo użycie prawdziwego potoku usuwania zawartości

StitchLoadedPage: skaluj, przesuń, narysuj

Impozycja N-up jest przyjaźniejszym problemem, bo nic nie jest ukrywane, tylko przestawiane. StitchLoadedPage przyjmuje indeks strony docelowej, indeks strony źródłowej, przesunięcie X/Y i współczynnik skali, po czym rysuje stronę źródłową na docelowej w podanej pozycji i rozmiarze:

// Overlay page 2 (index 1) onto page 1 (index 0),
// scaled to 70% and nudged up-right.
Pdf.StitchLoadedPage(0, 1, 40, 380, 0.7);

// Convenience 2-up: source page on the right half of the target.
Pdf.StitchLoadedPageSideBySide(0, 1);

Dopisany ciąg operatorów to standardowa sekwencja transformacji i rysowania: q do zapisania stanu grafiki, macierz cm niosąca skalę na przekątnej i przesunięcie w polach translacji, /StitchSrc Do do wywołania obiektu zewnętrznego oraz Q do przywrócenia stanu. Para q/Q ma znaczenie: izoluje transformację, tak aby wklejona strona nie przeciekła swoim układem współrzędnych do czegokolwiek dopisanego później. Metoda pilnuje też oczywistych błędów, takich jak indeksy poza zakresem, strona docelowa równa źródłowej czy niedodatnia skala, którą zaciska do 1.0, i wychodzi po cichu zamiast zgłaszać wyjątek, więc sprawdzaj wejście, bo cichy no-op wygląda dokładnie jak sukces

StitchLoadedPageSideBySide jest cienką wygodną nakładką na metodę ogólną. Odczytuje szerokość media box strony docelowej, dzieli ją na pół i wywołuje StitchLoadedPage z tą połową szerokości jako przesunięciem X i ze stałą skalą 0.5, umieszczając źródło na prawej połowie. To zakodowane na sztywno 0.5 zakłada, że źródło i cel mają tę samą szerokość. Jeśli tak nie jest, źródło nie wypełni czysto swojej połowy i będziesz chciał użyć ogólnej StitchLoadedPage ze skalą obliczoną samodzielnie z obu media boxów

Uproszczona strategia XObject i jej kompromis względem ISO

W tym miejscu implementacja stosuje celowe uproszczenie, które musisz znać, zanim zaufasz wynikowi w różnych przeglądarkach. Poprawna impozycja N-up owija zawartość strony źródłowej w Form XObject, czyli samowystarczalny obiekt rysowalny, który zgodnie z ISO 32000-1 §8.10.1 musi zawierać /Type /XObject, /Subtype /Form oraz własny obszar przycinania /BBox. Dziewiąta wersja stitch w HotPDF nie buduje takiej otoczki. Zamiast tego rejestruje sam słownik strony źródłowej bezpośrednio pod /Resources /XObject strony docelowej z nazwą StitchSrc, a następnie rysuje go przez Do. Słownik strony i Form XObject mają na tyle wspólny model zawartości, bo oba wskazują strumień zawartości i słownik zasobów, że wiele czytników wyrenderuje taki wynik

Ale nie jest to zgodny Form XObject. Brakuje mu znacznika /Subtype /Form oraz własnego /BBox, co oznacza, że rygorystyczny odbiorca ma pełne prawo zignorować Do albo przyciąć je inaczej, niż oczekujesz. TechnicalNotes dla tej rundy mówią o tym wprost: podejście "renderuje się w większości czytników", ale "nie jest ściśle zgodnym z ISO Form XObject", a pełna zgodność wymaga osobnego syntetyzowania prawdziwego strumienia Form XObject. Traktuj więc wynik stitch tak samo, jak traktowałbyś każdą niezgodną konstrukcję: weryfikuj go w konkretnych przeglądarkach używanych przez klientów, a nie tylko w tej na własnej maszynie, i jeśli potrzebujesz PDF-ów archiwalnych albo czystych dla rygorystycznych walidatorów, nie polegaj na tej ścieżce. Ta sama dyscyplina dotyczy wszystkiego, co budujesz na grafie załadowanych obiektów, dlatego przebieg preflight PDF w Delphi zasłużenie trafia do pipeline'u wydaniowego zawsze, gdy programowo modyfikujesz dokumenty

Gdzie to pasuje, a gdzie nie

Obie metody są narzędziami strumienia zawartości, więc model mentalny jest ten sam, którego używasz przy rysowaniu bezpośrednim. Jeśli budowałeś strony od zera komponentem, operatory wektorowe i koloru stojące za tymi wywołaniami będą ci znane z artykułu o rysowaniu na kanwie HotPDF w Delphi; różnica polega tylko na tym, że tutaj dopisujesz do strumienia autorstwa kogoś innego, zamiast do strumienia, nad którym sam panujesz. Pamiętaj o trzech granicach:

  • Redaction jest kosmetyczne. RedactLoadedRect maluje po zawartości i nigdy jej nie usuwa. Dla wszystkiego wrażliwego wygeneruj źródło ponownie albo użyj prawdziwego usuwania zawartości. Czarny prostokąt nie jest zabezpieczeniem
  • Stitch jest z założenia niezgodny. Strona źródłowa jest referencjonowana jako pseudo-XObject bez wymaganych przez §8.10.1 /Subtype /Form i /BBox, więc potwierdź renderowanie w docelowych przeglądarkach i unikaj tej ścieżki tam, gdzie wymagana jest ścisła walidacja
  • Współrzędne należą do przestrzeni użytkownika strony. Lewy dolny róg jako początek, punkty jako jednostki, wszystko sterowane przez własny media box strony. Odczytaj go przez GetLoadedPageBox, zanim cokolwiek umieścisz, bo załadowana strona może nie mieć rozmiaru, który zakładałeś

Używana w tych granicach ta para metod pokrywa realny workflow: przestawianie stron do druku, maskowanie niepoufnych obszarów i zapis wyniku przez SaveLoadedDocument bez pełnego ponownego renderowania. API dla załadowanych dokumentów, które obejmuje te prymitywy stitch i maskowania, jest dostarczane wraz ze standardowym HotPDF Component dla Delphi i C++Builder, obok metod pól formularzy, adnotacji i FDF z tej samej rundy