Technický článek

Redakce PDF a skládání N-up v Delphi s HotPDF

Na stůl vám přistane požadavek: vzít dávku už vyrenderovaných výpisů, začernit čísla účtů a vytisknout dvě stránky na jeden list, aby se ušetřil papír. Obě poloviny této úlohy jsou chirurgickým zásahem do content streamu PDF, které jste sami nevytvořili, takže tu není žádné přívětivé plátno stránky ani správce fontů, o který by šlo se opřít. Přímo upravujete objektový graf načteného dokumentu a připojujete k němu syrové kreslicí operátory na stránku, kterou vysázel nějaký jiný nástroj. HotPDF pro to nabízí přesně dva vstupní body a z těch dvou je nebezpečnější ten, který působí neškodně.

HotPDF je nativní VCL komponenta pro PDF v Delphi a C++Builderu. Její API pro načtené dokumenty v deváté sadě přidalo první metody, které vytvářejí zcela nový obsah na stránce otevřené z disku, nikoli na stránce, kterou jste postavili od nuly. Tady jsou dvě z nich: RedactLoadedRect, která překryje oblast neprůhledným obdélníkem, a StitchLoadedPage, která jednu stránku zmenší a vykreslí ji na jinou. Obě pracují tak, že do proudu /Contents stránky zapisují operátory content streamu podle ISO 32000-1 §8.5. Porozumět tomu, co tyto operátory dělají, a stejně důležitě tomu, co nedělají, je rozdíl mezi fungujícím nástrojem a únikem dat.

Připojování operátorů k načtené stránce

Když vytváříte stránku pomocí běžného API HotPDF, komponenta vlastní content stream a za vás serializuje volání TextOut i vektorové kreslení. U načtené stránky je to jiné: její /Contents je existující stream objekt, který může být sdílený, může být součástí pole contentů a vy do něj musíte vstoupit tak, abyste nepoškodili to, co už tam je. Devátá sada přidala tři malé pomocníky, kteří to dělají bezpečně. NewIndirectStream alokuje nový nepřímý THPDFStreamObject s prázdným buffrem a položkou /Length 0; ResolveLoadedStream sleduje nepřímou referenci až k podkladovému streamu; a AppendLoadedStream zapíše syrová data na konec streamu a přepíše /Length, aby uložený objekt zůstal správně tvarovaný.

Postup obou veřejných metod je stejný. Najdou /Contents stránky, rozřeší ho na stream a pokud žádný použitelný stream neexistuje, vytvoří nový a připojí ho. Pak přidají operátory. Protože nová data jdou na konec streamu, model kreslení zaručuje, že se vykreslí nad vším, co původní rozvržení nakreslilo. Právě tato posloupnost je celým mechanismem za redakčním obdélníkem a zároveň důvodem, proč ten obdélník není tím, co si o něm většina lidí myslí.

RedactLoadedRect: neprůhledné překrytí, ne smazání

RedactLoadedRect bere index stránky od nuly, čtyři souřadnice v uživatelském prostoru a tři barevné složky v rozsahu 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;

Uvnitř metoda vypíše do content streamu tři operátory: nastavení barvy výplně v DeviceRGB (r g b rg), cestu obdélníku (x y w h re) a výplň (f). Šířka a výška se odvozují jako X2 - X1 a Y2 - Y1, takže zadáte dva protilehlé rohy a necháte metodu spočítat rozsah. Když barvu zadáte jako 0, 0, 0, dostanete černý pruh; když zadáte 1, 1, 1, dostanete bílý pruh, který sedí na bílou stránku. Souřadnice patří do uživatelského prostoru načtené stránky, což znamená, že počátek leží vlevo dole a jednotky jsou body, a zároveň to znamená, že pro přesné umístění potřebujete /MediaBox stránky; získáte ho pomocí GetLoadedPageBox s pbMediaBox.

Čtěte to dvakrát: vyplněný obdélník obsah vizuálně zakryje, ale neodstraní ho. Text, obrázek nebo vektorová grafika pod obdélníkem v PDF pořád existuje, pořád je v objektovém grafu a pořád ji lze vytáhnout, ať už stránku někdo zkopíruje, spustí textový extraktor, nebo váš obdélník jednoduše smaže z content streamu. To je vizuální maskování, ne redakce v právním nebo bezpečnostním smyslu. Jestli skrýváte skutečně citlivá data - čísla účtů, zdravotní záznamy, identitu, cokoli regulovaného - zakrýt je černým blokem a soubor odeslat je pozvánka k úniku dat. Skutečná redakce vyžaduje smazání podkladových objektů obsahu, ne jejich přemalování.

Název metody říká "Redact" a je to užitečné varování před tím, jak bude výsledek pochopen, ne slib o tom, co se smaže. Implementace je v tom poctivá i ve svém komentáři: sama sebe nazývá "visual redaction primitive" a poznamenává, že redakce, která opravdu odstraňuje obsah, potřebuje interpret content streamu, jenž projde existující operátory a přepíše je. Cesta pro načtené dokumenty v HotPDF to zde nedělá. Bezpečné pravidlo je tedy úzké: RedactLoadedRect používejte jen pro nekonfidenční kosmetické maskování - skrytí návrhového vodoznaku, vyplnění oblasti před screenshotem, zakrytí zastaralého loga na interním nátisku. V okamžiku, kdy by na obsahu pod obdélníkem záleželo, kdyby unikl, je tato metoda špatný nástroj a správná odpověď je dokument znovu vygenerovat bez těch dat nebo použít skutečný pipeline pro odstranění obsahu.

StitchLoadedPage: škáluj, přesuň, vykresli

N-up impozice je přívětivější problém, protože se nic neskrývá, jen se přeuspořádává. StitchLoadedPage bere index cílové stránky, index zdrojové stránky, posun v ose X a Y a faktor měřítka, a vykreslí zdrojovou stránku na cíl v dané pozici a velikosti:

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

Řetězec operátorů, který připojí, je standardní sekvence transformace a kreslení: q pro uložení grafického stavu, matice cm, která nese měřítko na diagonále a posun v místech pro translaci, /StitchSrc Do pro vyvolání externího objektu a Q pro obnovení stavu. Dvojice q/Q je důležitá, protože izoluje transformaci tak, aby se souřadnicový systém přimontované stránky nerozlil do čehokoli, co se připojí potom. Metoda také hlídá zjevné chyby - indexy mimo rozsah, cíl shodný se zdrojem, nekladné měřítko, které omezí na 1.0 - a skončí potichu místo vyhození výjimky, takže si vstupy zkontrolujte, protože tichá no-op vypadá stejně jako úspěch.

StitchLoadedPageSideBySide je drobná zkratka nad obecnou metodou. Přečte šířku media boxu cíle, rozdělí ji na polovinu a zavolá StitchLoadedPage s touto poloviční šířkou jako posunem v ose X a pevným měřítkem 0.5, čímž umístí zdroj na pravou polovinu. Těch tvrdě zadaných 0.5 předpokládá, že zdroj i cíl mají stejnou šířku; pokud ne, zdroj svou polovinu nevyplní čistě a bude lepší použít obecné StitchLoadedPage s měřítkem, které si spočítáte z obou media boxů sami.

Zjednodušená strategie XObject a její kompromis vůči ISO

Tady se implementace dopouští záměrné zkratky, kterou potřebujete znát dřív, než začnete výstupu věřit napříč prohlížeči. Správná N-up impozice obalí obsah zdrojové stránky do Form XObjectu, tedy do samostatného vykreslitelného objektu, u něhož ISO 32000-1 §8.10.1 vyžaduje /Type /XObject, /Subtype /Form a vlastní ořezový box /BBox. Stitch v deváté sadě od HotPDF takový obal nevytváří. Místo toho registruje přímo samotný slovník stránky pod /Resources /XObject cílové stránky pod názvem StitchSrc a pak ho vykreslí pomocí Do. Slovník stránky a Form XObject sdílejí dost z modelu obsahu - oba odkazují na content stream a slovník zdrojů - takže výsledek mnoho čteček vykreslí.

Není to ale konformní Form XObject. Chybí mu značka /Subtype /Form i vlastní /BBox, což znamená, že přísný konzument má právo Do ignorovat nebo ho oříznout jinak, než čekáte. TechnicalNotes pro tuto sadu to říkají jasně: přístup "renders under most readers" je sice funkční, ale "not a strictly ISO-compliant Form XObject" a plná shoda vyžaduje samostatný krok, který skutečný Form XObject vytvoří. Chovejte se tedy k výstupu ze stitch stejným způsobem jako ke každé jiné nekonformní konstrukci: ověřte ho v konkrétních prohlížečích, které používají vaši zákazníci, ne jen v tom, který máte na počítači, a pokud potřebujete archivační nebo přísně validátorově čisté PDF, na tuto cestu nespoléhejte. Stejná disciplína platí pro cokoli, co stavíte nad načteným objektovým grafem, a právě proto si PDF preflight v Delphi zaslouží místo v release pipeline pokaždé, když dokumenty měníte programově.

Kde se tyto metody hodí a kde ne

Obě metody jsou nástroje pro content stream, takže mentální model je stejný jako při přímém kreslení. Jestli jste s komponentou stavěli stránky od nuly, vektorové a barevné operátory za těmito voláními vám budou povědomé z kreslení na plátno HotPDF v Delphi; rozdíl je jen v tom, že tady připojujete data ke streamu, který napsal někdo jiný, a ne vy sami. Mějte na paměti tři hranice:

  • Redakce je kosmetická. RedactLoadedRect content jen překryje a nikdy ho nesmaže. U čehokoli citlivého zdroj znovu vygenerujte nebo použijte skutečné odstranění obsahu - černý blok není bezpečnost.
  • Stitch je záměrně nekonformní. Zdrojová stránka je odkazována jako pseudo-XObject bez §8.10.1 /Subtype /Form a /BBox, takže vykreslení ověřte v cílových prohlížečích a vyhněte se tomu tam, kde je nutná přísná validace.
  • Souřadnice patří do uživatelského prostoru stránky. Počátek vlevo dole, jednotky v bodech, řízené vlastním media boxem stránky. Než něco umístíte, přečtěte si box pomocí GetLoadedPageBox, protože stránka, kterou jste načetli, nemusí mít velikost, kterou předpokládáte.

V těchto mezích dvojice pokrývá reálný pracovní postup: přeuspořádat stránky pro tisk, maskovat nekonfidenční oblasti a zapsat výsledek zpět pomocí SaveLoadedDocument - to vše bez úplného rerenderu. API pro načtené dokumenty, které tyto stitch a mask primitives obsahuje, přichází s HotPDF Component pro Delphi a C++Builder spolu s metodami pro formulářová pole, anotace a FDF z téže sady.