Niekto nakreslí čierny rámik cez meno, nič neslúči, odošle súbor a recenzent vyberie obdĺžnik a vloží meno do e-mailu. PDFiumPas na to odpovedá redakciou na úrovni operátorov: SaveAsRedacted zmaže len Unicode skaláry, ktorých rámečky znakov sa dotýkajú redakčného obdĺžnika, prebuduje preživších z pôvodného písma, veľkosti, matice, render režimu a farby a oreže cesty a obrázky zarovnané k osiam namiesto toho, aby ich zahodil celé
Prečo nakreslený obdĺžnik nie je redakcia
Kresliaca operácia pridaná navrch obsahového prúdu nič neskrýva, pretože operácie zobrazujúce text pod ňou sú stále v prúde a stále sa mapujú na kódové body. ISO 32000-1 §9.4 definuje textový objekt ako postupnosť operácií umiestňovania a zobrazovania vnútri BT a ET; vyplnený obdĺžnik nakreslený potom je jednoducho ďalšia operácia v tom istom prúde. Extrakcia prechádza operáciami, nie pixelmi, takže zakrytý reťazec sa vráti nedotknutý. Skutočná redakcia musí odstrániť operand, nie zatemniť výstup
Zjavná bezpečná implementácia je brutálna: nájsť každý objekt stránky, ktorého ohraničujúci rámec pretína redakčný obdĺžnik, a zmazať celý objekt. Presne to robili skoršie vydania PDFiumPas a je to správne, ale drahé. Jediný Tj môže niesť celý riadok tabuľky, takže zatienenie jedného čísla účtu vzalo so sebou dátum, popis aj sumu. Obdĺžniková výplň, ktorá náhodou tvorí pásmo tabuľky na celú šírku, zmizla po celej stránke. Logo faktúry zmizlo, pretože redakcia odstrihla jeden jeho roh. Verzia 3.101.0 posúva rozhodnutie o úroveň nižšie, z objektu stránky na operand
Čo redakcia na úrovni operátorov skutočne maže?
PDFiumPas maže Unicode skaláry, nie textové objekty. Počas SaveAsRedacted komponent vybuduje mapovanie znakov na objekty stránky z načítanej textovej stránky, potom pre každý znak vlastnený testovaným objektom prečíta jeho rámeček a pretína tento rámec s každým redakčným obdĺžnikom. Znaky, ktoré sa dotknú obdĺžnika, sa označia na odstránenie; ostatné sa označia ako preživší. Ak nič nepretína, objekt sa úplne nechá pokojne. Ak pretína každý znak, objekt sa zmaže celý, presne ako predtým. Len zmiešaný prípad spustí rozdelenie
Každý preživší sa potom znovu vydá ako vlastný textový objekt postavený z pôvodného handle písma, pôvodnej veľkosti písma, textovej matice pre každý znak, pôvodného render režimu textu a stavu výplne a ťahu rodičovského objektu vrátane šírky ťahu, line join, line cap a dash array. Znovupoužitie handle písma namiesto vyriešenia nového je to, čo drží glyfy metricky identické, a znovupoužitie matice pre každý znak je to, čo drží kerning a medzery slov na mieste bez opätovného behu layoutu. Cena je počet objektov: jeden ponechaný znak sa stane jedným textovým objektom, a preto TPdfRedactionOptions.MaxSplitObjects existuje ako tvrdý strop počtu generovaných fragmentov
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // súbor už nesie anotácie /Redact
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // delenie na úrovni operátorov (predvolené)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // strop počtu generovaných fragmentov
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // fail closed, neodosielať
finally
Pdf.Free;
end;
end;
Obdĺžniky sa orežú, otočená geometria nie
Cesty sa delia len vtedy, keď PDFiumPas dokáže, že cesta je obdĺžnik zarovnaný k osiam. Dôkaz je zámerne úzky: objektová matica musí mať oba šmykové členy pod 0.0001, cesta musí pozostávať zo štyroch až šiestich segmentov začínajúcich MOVETO a pokračujúcich len LINETO a transformované body musia dopadnúť na všetky štyri rohy ohraničenia objektu v tolerancii 0.01. Cesta, ktorá kontrolu prejde, sa redukuje postupným odčítaním obdĺžnikov, pričom každý redakčný obdĺžnik vyrubuje zo súboru preživších pásy vľavo, vpravo, pod a nad a každý výsledný pás sa znovu vytvorí s pôvodným režimom výplne, príznakom ťahu a stavom farbenia. Krivky, trojuholníky, vystrihnuté tvary a čokoľvek otočené kontrolu nezvládne a celý objekt sa odstráni
Obrázky sa riadia ISO 32000-1 §8.9, kde vzorky obrázku obsadzujú jednotkový štvorec zmapovaný cez aktuálnu transformačnú maticu. PDFiumPas toto mapovanie invertuje, aby každý preživší fragment v priestore stránky vrátil do normalizovaných súradníc obrázku, pritlačí ich do jednotkového intervalu a potom konvertuje na pixelové indexy zaokrúhlením dovnútra: ľavý a horný okraj prechádzajú cez Ceil, pravý a dolný cez Floor. Tento smer je dôležitý. Zaokrúhlenie smerom von by dovolilo, aby sa na okraji fragmentu zachovala čiastočná stĺpcová zložka zdrojových pixelov z redakčnej strany. Celočíselné pixelové medze sa potom prevedú späť na normalizované súradnice a použijú na odvodenie matice fragmentu, takže orezaná bitmapa dopadne presne na pixelovú hranicu, pri ktorej bola narezaná. Samotný rez je kópia riadkov s ohľadom na stride cez formáty Gray, BGR, BGRx a BGRA. Rovnako ako pri cestách sa otočený alebo zošikmený obrázok, alebo obrázok, ktorého matica má degenerovaný člen mierky, odstráni celý
// Po úspešnom volaní SaveAsRedacted
Writeln(Format('applied %d redaction(s) on %d page(s)',
[Report.RedactionCount, Report.RedactedPageCount]));
Writeln(Format('scanned %d object(s), removed %d',
[Report.ScannedObjectCount, Report.RemovedObjectCount]));
Writeln(Format('split text/path/image: %d / %d / %d',
[Report.SplitTextObjectCount, Report.SplitPathObjectCount,
Report.SplitImageObjectCount]));
Writeln(Format('preserved %d fragment(s)', [Report.PreservedFragmentCount]));
Writeln(Format('pruned %d resource name(s), swept %d object(s)',
[Report.ResourcePruneReport.RemovedNameCount,
Report.ResourcePruneReport.RemovedObjectCount]));
if Report.PreservedFragmentCount = 0 then
// nič sa nedalo rozdeliť: každý pretínajúci objekt bol zahodený celý
LogWholeObjectFallback(SourcePdf);
Prečo PDFiumPas zlyháva bezpečne pri nenamapovaných znakoch?
Pretože glyf bez reprodukovateľného Unicode skaláru sa nedá čestne prebudovať. Rekonštrukcia preživšieho znamená zavolať API nastavujúce text s reťazcom a to vyžaduje stabilný kódový bod pre každý ponechaný znak. Symbolické podmnožinové písma s pokazenými alebo chýbajúcimi dátami ToUnicode môžu dať prázdne mapovanie a rekódovanie odhadom by vyrobilo výstup, ktorý na obrazovke vyzerá správne, ale v skutočnosti nesie iný znak. PDFiumPas odmietne: kontrola ponechaných znakov vyvolá výnimku, výnimka sa zachytí vnútri SaveAsRedacted, TPdfRedactionReport.Succeeded sa vráti ako False so správou v ErrorMessage a funkcia vráti False. Toto isté pravidlo platí pre rozpočet delenia, ktorý vyvolá výnimku namiesto tichého skrátenia sady fragmentov. Ak dokument obsahuje písma, ktorým neveríte, a chcete deterministické staré správanie, nastavte Options.PreservePartialObjects := False a každý pretínajúci objekt zmizne celý
Orezávanie zdrojov cez zdieľané oblasti
Delenie objektov zanecháva siroty a ich orezávanie nie je také jednoduché ako porovnanie slovníka /Resources na úrovni stránky. ISO 32000-1 §7.8.3 dovoľuje, aby sa ten istý slovník zdrojov odkazoval z viacerých stránok, Form XObjects, vzorov aj prúdov vzhľadu anotácií naraz. Zmazanie názvu písma preto, že jedna stránka ho prestala používať, rozbije inú stránku, ktorá ho ešte používa. PruneUnusedPdfResources preto pracuje po oblastiach: vyrieši /Contents či už je to priame pole, nepriamy odkaz na pole alebo jediný prúd, potom zbiera použitie zdrojov z operácií, ktoré zdroje reálne menujú — Tf pre písma, Do pre XObjects, gs pre grafický stav, CS, cs, SCN a scn pre farebné priestory a vzory, sh pre tienenia, BDC a DP pre vlastnosti marked-content, plus položku /CS inline obrázkov. Keď jeden slovník zdieľa viac oblastí, množiny použitých názvov sa zjednotia po kategóriách skôr, než sa čokoľvek odstráni
Zahodia sa len názvy potvrdené ako neodkazované naprieč každou oblasťou, ktorá na slovník ukazuje. Oblasť, ktorú nemožno spoľahlivo vyparsovať, zostáva nedotknutá, a to je konzervatívny smer: neorezaný súbor je len väčší, zle orezaný je poškodený. Preživšie slovníky sa zapíšu späť ako riedka prírastková aktualizácia nesúca presné čísla generácií a prepis dosiahnuteľnosti potom zamestá objekty, ktoré sa po zmiznutí názvov stali nedosiahnuteľnými. TPdfResourcePruneReport hlási ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, počty bajtov a príznak Succeeded. SaveAsRedacted spúšťa tento krok automaticky na sanitovanom výstupe, takže redakčná cesta ho už obsahuje, ale funkcia je exportovaná na úrovni prúdov pre pipeline, ktoré ju chcú samostatne
uses
FPdfCompress;
procedure PruneResourceNames(const SourcePdf, TargetPdf: string);
var
Source, Dest: TFileStream;
Report: TPdfResourcePruneReport;
begin
Source := TFileStream.Create(SourcePdf, fmOpenRead or fmShareDenyWrite);
try
Dest := TFileStream.Create(TargetPdf, fmCreate);
try
// AllowSignedDocument ostáva False: prírastkový prepis by
// zneplatnil bajtové rozsahy, ktoré podpis kryje
PruneUnusedPdfResources(Source, Dest, Report);
if not Report.Succeeded then
raise Exception.Create(Report.ErrorMessage);
Writeln(Format('%d name(s) removed from %d scope(s), %d -> %d bytes',
[Report.RemovedNameCount, Report.UpdatedScopeCount,
Report.SourceByteCount, Report.OutputByteCount]));
finally
Dest.Free;
end;
finally
Source.Free;
end;
end;
Zapojenie do pipeline dokumentov
Redakčná cesta nikdy nemutuje dokument, ktorý ste načítali. SaveAsRedacted zachytí izolovanú snímku, aplikuje na nej anotácie /Redact, odstráni prílohy, spustí sanitizačný priechod odstraňujúci open action, akcie katalógu, stromy mien, pridružené súbory, AcroForm a metadata, oreže zdroje a až potom zapíše výstupný prúd. Znovuotvorenie tohto výstupu ako nezávislého dokumentu a opätovná extrakcia textu je overovací krok, ktorý stojí za to držať vo vlastnej testovacej sade, pretože je to jediná kontrola, ktorá odpovie na pôvodnú otázku — či sa čitateľ ešte môže k reťazcu dopracovať. Jedna následnosť, na ktorú sa treba pripraviť: delenie nahradzuje objekty stránky, takže každý handle FPDF_PAGEOBJECT, ktorý ste držali, je potom mŕtvy; to isté životné pasce popisuje zastarané handly objektov stránky po transformácii
Dva susediace kusy robia pracovný postup úplným. Rozhodnutie, kam redakčné obdĺžniky sadia, zvykne vychádzať z extrahovanej geometrie a model blokov a poradia čítania v štruktúrovaných textových blokoch a poradí čítania je lepším zdrojom kandidátskych rámečkov než surové behy znakov. Servírovanie výsledku recenzentovi patrí k pravidlám spevnenia v stavbe bezpečnej PDF ukážky, kde vypĺňanie formulárov a JavaScript zostávajú v predvolenom stave vypnuté. Spolu pokrývajú slučku, ktorú väčšina compliance pracovných postupov potrebuje: nájsť, redigovať na úrovni operátorov, overiť znovuotvorením, ukázať bezpečne. Úplný povrch API, skúšobné stiahnutie a licenčné podmienky komponentu nájdete na produktovej stránke PDFium Delphi Component