Odborný článok

Redakcia PDF a N-up skladanie v Delphi s HotPDF

Na stôl vám pristane požiadavka: vezmite dávku už vyrenderovaných výpisov, začiernite čísla účtov a pošlite dve stránky na jeden list, aby sa šetril papier. Obe polovice tejto úlohy sú chirurgia nad content streamom PDF, ktoré ste nevytvorili vy, takže nemáte k dispozícii žiadne priateľské page canvas na kreslenie ani žiadny font manager, o ktorý by sa dalo oprieť. Upravujete priamo graf objektov načítaného dokumentu a na stránku, ktorú rozložil iný nástroj, pripájate surové kresliace operátory. HotPDF na to vystavuje presne dva vstupné body a nebezpečnejší z nich je ten, ktorý vyzerá nevinne

HotPDF je natívny VCL PDF komponent pre Delphi a C++Builder. Jeho API pre načítané dokumenty z deviateho kola prinieslo prvé metódy, ktoré vytvárajú úplne nový obsah na stránke otvorenej z disku namiesto stránky vytvorenej od nuly. Dve z nich sú predmetom tohto článku: RedactLoadedRect, ktorá cez oblasť namaľuje nepriehľadný obdĺžnik, a StitchLoadedPage, ktorá jednu stránku zmenší a vykreslí ju na inú. Obe fungujú tak, že zapisujú operátory content streamu podľa ISO 32000-1 §8.5 do streamu /Contents danej stránky. Rozumieť tomu, čo tieto operátory robia a rovnako dôležito tomu, čo nerobia, je rozdiel medzi funkčným nástrojom a únikom dát

Pripájanie operátorov k načítanej stránke

Keď vytvárate stránku bežným API HotPDF, komponent vlastní content stream a za vás serializuje vaše volania TextOut aj vektorové volania. Načítaná stránka je iná: jej /Contents je existujúci stream objekt, možno zdieľaný, možno súčasť poľa obsahov, a vy sa doň musíte napojiť bez poškodenia toho, čo už obsahuje. Deviate kolo prinieslo tri malé helpery, ktoré to robia bezpečne. NewIndirectStream alokuje nový nepriamy objekt THPDFStreamObject s prázdnym bufferom a položkou /Length 0; ResolveLoadedStream sleduje nepriamu referenciu až k podkladovému streamu a AppendLoadedStream zapisuje surové bajty na koniec streamu a prepíše /Length, aby uložený objekt zostal dobre vytvorený

Obe verejné metódy sledujú rovnaký vzor. Nájdite /Contents, vyhodnoťte ho na stream, a ak neexistuje použiteľný stream, vytvorte ho a pripojte. Potom pripojte operátory. Pretože nové bajty idú na koniec streamu, maliarsky model zaručuje, že sa vyrenderujú nad všetkým, čo nakreslilo pôvodné rozloženie. Toto poradie je celý mechanizmus za redakčným obdĺžnikom a zároveň dôvod, prečo tento obdĺžnik nie je tým, čo si väčšina ľudí myslí

RedactLoadedRect: nepriehľadné prekrytie, nie zmazanie

RedactLoadedRect berie index stránky od nuly, štyri súradnice v user-space a tri zložky farby v rozsahu 0 až 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 kapotou metóda zapíše do content streamu tri operátory: nastavenie výplňovej farby v DeviceRGB (r g b rg), obdĺžnikovú cestu (x y w h re) a výplň (f). Šírka a výška sa odvodia ako X2 - X1 a Y2 - Y1, takže odovzdáte dva protiľahlé rohy a metóda si rozsah spočíta sama. Ak pre farbu zadáte 0, 0, 0, dostanete čierny pás; ak zadáte 1, 1, 1, dostanete biely, ktorý splynie s bielou stránkou. Súradnice patria do vlastného user-space načítanej stránky, čo znamená počiatok v ľavom dolnom rohu a jednotky v bodoch, a zároveň to znamená, že na presné umiestnenie potrebujete poznať /MediaBox stránky; GetLoadedPageBox spolu s pbMediaBox vám to poskytne

Prečítajte si to dvakrát: vyplnený obdĺžnik obsah vizuálne prekryje, ale neodstráni ho. Text, obrázok alebo vektorová grafika pod ním sú stále prítomné v PDF, stále v grafe objektov a stále ich môže ktokoľvek získať skopírovaním stránky, spustením extraktora textu alebo jednoduchým odstránením vášho obdĺžnika z content streamu. Toto je vizuálne maskovanie, nie redakcia v právnom alebo bezpečnostnom zmysle. Ak skrývate skutočne citlivé údaje, čísla účtov, zdravotné záznamy, identity, čokoľvek regulované, prekryť ich čiernou škatuľou a odoslať súbor je únik dát, ktorý len čaká na odhalenie. Skutočná redakcia vyžaduje odstránenie podkladových obsahových objektov, nie ich premaľovanie

Názov metódy hovorí "Redact" a to je užitočné varovanie o tom, ako sa bude výsledok nesprávne čítať, nie sľub o tom, čo sa vymaže. Implementácia je v tomto poctivá aj vo vlastnom komentári: nazýva samu seba "visual redaction primitive" a poznamenáva, že redakcia odstraňujúca obsah potrebuje interpret content streamu, ktorý prejde existujúce operátory a prepíše ich. Cesta HotPDF pre načítané dokumenty to tu nerobí. Bezpečné pravidlo je teda úzke: používajte RedactLoadedRect len na necitlivé kozmetické maskovanie, skrytie draft watermarku, zamazanie oblasti pred screenshotom, prekrytie zastaraného loga na internom nátlačku. V momente, keď by na tom, čo je pod obdĺžnikom, záležalo pri úniku, je táto metóda nesprávny nástroj a správnou odpoveďou je znovu vygenerovať dokument bez týchto dát alebo použiť skutočnú pipeline na odstránenie obsahu

StitchLoadedPage: zmenšiť, posunúť, vykresliť

Ukladanie N-up je priateľskejší problém, pretože nič neskrývate, iba preskupujete. StitchLoadedPage berie index cieľovej stránky, index zdrojovej stránky, posun X/Y a mierku a vykreslí zdrojovú stránku na cieľovej na danej pozícii a vo danej veľkosti:

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

Reťazec operátorov, ktorý pripojí, je štandardná sekvencia transformácie a kreslenia: q na uloženie grafického stavu, matica cm nesúca mierku na diagonále a posun v translačných poliach, /StitchSrc Do na vyvolanie externého objektu a Q na obnovenie stavu. Dvojica q/Q je dôležitá: izoluje transformáciu, aby zošitá stránka nepreniesla svoj súradnicový systém na nič, čo sa pripojí neskôr. Metóda tiež stráži zjavné chyby, indexy mimo rozsahu, cieľ zhodný so zdrojom, nekladnú mierku, ktorú zovrie na 1.0, a potichu skončí namiesto vyhodenia výnimky, takže si kontrolujte vstupy, pretože tichý no-op vyzerá presne ako úspech

StitchLoadedPageSideBySide je tenká pohodlná obálka nad všeobecnou metódou. Prečíta šírku media boxu cieľa, rozdelí ju na polovicu a zavolá StitchLoadedPage s touto polovičnou šírkou ako posunom X a pevnou mierkou 0.5, čím zdroj uloží do pravej polovice. Toto natvrdo zakódované 0.5 predpokladá, že zdroj aj cieľ majú rovnakú šírku; ak ju nemajú, zdroj svoju polovicu nevyplní čisto a budete chcieť všeobecnú StitchLoadedPage s mierkou, ktorú si spočítate sami z oboch media boxov

Zjednodušená stratégia XObjectu a jej ISO kompromis

Tu implementácia robí zámernú skratku, o ktorej musíte vedieť skôr, než budete výstupu dôverovať naprieč prehliadačmi. Správne uloženie N-up zabalí obsah zdrojovej stránky do Form XObjectu, teda samostatne kresliteľného objektu, ktorý podľa ISO 32000-1 §8.10.1 musí niesť /Type /XObject, /Subtype /Form a vlastný clipping box /BBox. HotPDF v deviatom kole pri stitchovaní tento obal netvorí. Namiesto toho zaregistruje samotný slovník zdrojovej stránky priamo pod /Resources /XObject cieľa pod menom StitchSrc a potom ho vykreslí pomocou Do. Slovník stránky a Form XObject zdieľajú dosť z obsahového modelu, obidve odkazujú na content stream aj resource dictionary, takže veľa čítačiek výsledok vyrenderuje

Nie je to však zhodný Form XObject. Chýba mu marker /Subtype /Form aj vlastný /BBox, čo znamená, že prísny konzument má plné právo ignorovať Do alebo ho orezávať inak, než očakávate. TechnicalNotes pre toto kolo to hovoria úplne priamo: tento prístup renderuje vo väčšine čítačiek, ale nie je striktne ISO-kompatibilný Form XObject, a úplná zhoda vyžaduje syntetizovať skutočný stream Form XObjectu ako samostatný krok. Preto sa k stitch výstupu správajte ako ku každému nevyhovujúcemu konštruktu: overte ho v konkrétnych prehliadačoch, ktoré používajú vaši zákazníci, nielen v tom na vašom stroji, a ak potrebujete archivačné PDF alebo PDF čisté pred prísnym validátorom, na túto cestu sa nespoliehajte. Rovnaká disciplína platí pre všetko, čo staviate nad načítaným grafom objektov, preto si aj PDF preflight v Delphi zaslúži miesto v release pipeline vždy, keď dokumenty meníte programovo

Kam sa tieto metódy hodia a kam nie

Obe metódy sú nástroje content streamu, takže mentálny model je ten istý ako pri priamom kreslení. Ak ste s komponentom stavali strany od nuly, vektorové a farebné operátory za týmito volaniami vám budú povedomé z kreslenia na canvas HotPDF v Delphi; rozdiel je len v tom, že tu pripájate obsah do streamu, ktorý vytvoril niekto iný, nie do streamu, ktorý vlastníte vy. Majte na pamäti tri hranice:

  • Redakcia je kozmetická. RedactLoadedRect prekresľuje obsah a nikdy ho nemaže. Pri čomkoľvek citlivom znovu vytvorte zdroj alebo použite skutočné odstránenie obsahu, čierny box nie je bezpečnosť
  • Stitch je zámerne nevyhovujúci. Zdrojová stránka sa odkazuje ako pseudo-XObject bez /Subtype /Form z §8.10.1 a /BBox, preto si overte vykreslenie vo svojich cieľových prehliadačoch a vyhnite sa tejto ceste tam, kde sa vyžaduje prísna validácia
  • Súradnice patria do user-space stránky. Počiatok vľavo dole, body, riadené vlastným media boxom stránky. Pred umiestnením čohokoľvek si box prečítajte pomocou GetLoadedPageBox, pretože načítaná stránka nemusí mať veľkosť, ktorú ste predpokladali

Ak tieto hranice rešpektujete, dvojica pokrýva skutočný workflow: preskupiť strany na tlač, zamaskovať nedôverné oblasti a výsledok zapísať späť pomocou SaveLoadedDocument bez plného rerenderu. API pre načítané dokumenty, ktoré obsahuje tieto primitíva pre stitch a maskovanie, sa dodáva v komponente HotPDF Component pre Delphi a C++Builder spolu s metódami pre formulárové polia, anotácie a FDF z toho istého kola