Tehnički članak

Operatorska redakcija PDF-a u Delphiju s PDFiumPasom

Netko nacrta crni okvir preko imena, ništa ne izravna, otpremi datoteku, a recenzent odabere pravokut i zalijepi ime u e-poštu. PDFiumPas na to odgovara redakcijom na razini operatora: SaveAsRedacted briše samo Unicode skalare čije se kućice znakova dotiču pravokuta redakcije, preživjele gradi izvornim fontom, veličinom, matricom, načinom iscrtavanja i bojom, te obreže putove i slike poravnate s osima umjesto da ih baca cijele

Zašto naslikani pravokut nije redakcija

Operacija crtanja dodana preko toka sadržaja ne sakriva ništa, jer operatorski znakovi za prikaz teksta ispod nje i dalje su u toku i dalje se mapiraju na točke koda. ISO 32000-1 §9.4 definira tekstualni objekt kao niz operatorskih znakova pozicioniranja i prikazivanja unutar BT i ET; puni pravokut nacrtan poslije je samo još jedan operator u istom toku. Izvlačenje prolazi operatore, ne piksele, pa pokriveni niz vraća netaknut. Prava redakcija mora ukloniti operand, a ne zamračiti izlaz

Očita sigurna izvedba je brutalna: nađi svaki objekt stranice čiji se okvir siječe s pravokutom redakcije i izbriši cijeli objekt. To su ranija izdanja PDFiumPasa radila, i točno je, ali skupo. Jedan Tj može nositi cijeli red tablice, pa je zamračivanje jednog broja računa povuklo datum, opis i iznos sa sobom. Pravokutna ispuna koja je slučajno bila traka pune širine nestala je preko cijele stranice. Logotip računa nestao je jer je redakcija odrezala jedan njegov kut. Izdanje 3.101.0 spušta odluku razinu niže, s objekta stranice na operand

Što redakcija na razini operatora stvarno briše?

PDFiumPas briše Unicode skalare, ne tekstualne objekte. Tijekom SaveAsRedacted komponenta gradi mapiranje znakova na objekte stranice iz učitane tekstualne stranice, a zatim za svaki znak u vlasništvu testiranog objekta čita kućicu znaka i provjerava njeno sjecište sa svakim pravokutom redakcije. Znaci koji dotiču pravokut označavaju se za uklanjanje; ostali se označavaju kao preživjeli. Ako se ništa ne siječe, objekt ostaje potpuno na miru. Ako se siječe svaki znak, objekt se uklanja cijeli, točno kao prije. Samo miješani slučaj pokreće rastavljanje

Redakcija na razini operatora u PDFiumPasu uspoređena s brisanjem cijelih objekata u Delphiju: stari put baca cijeli tekstualni objekt kad je pokriven jedan broj računa, dok put rastavljanja briše samo siječuće znakove i svakog preživjelog ponovno ispisuje kao vlastiti tekstualni objekt
Samo miješani slučaj pokreće rastavljanje: ništa siječuće ostavlja objekt na miru, sve siječuće uklanja ga cijelog

Svaki se preživjeli zatim ponovno ispisuje kao vlastiti tekstualni objekt građen iz izvornog rukovanja fontom, izvorne veličine fonta, matrice teksta po znaku, izvornog načina iscrtavanja teksta te stanja ispune i poteza roditeljskog objekta uključujući širinu poteza, spajanje linija, kraj linije i niz crtica. Ponovno korištenje rukovanja fontom umjesto razrješivanja novog je ono što znakovima čini metrike identičnima, a ponovno korištenje matrice po znaku ono što kerning i razmak riječi drži na mjestu bez ponovnog rasporeda. Cijena je broj objekata: jedan zadržani znak postaje jedan tekstualni objekt, zato TPdfRedactionOptions.MaxSplitObjects postoji kao tvrdi strop nad generiranim fragmentima

procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
  Pdf: TPdf;
  Options: TPdfRedactionOptions;
  Report: TPdfRedactionReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := SourcePdf;   // the file already carries /Redact annotations
    Pdf.Active := True;

    Options := TPdfRedactionOptions.Default;
    Options.PreservePartialObjects := True;    // operator-level split (the default)
    Options.RemoveIntersectingAnnotations := True;
    Options.MaxSplitObjects := 20000;          // ceiling on generated fragments

    if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
      raise Exception.Create(Report.ErrorMessage);   // fail closed, do not ship
  finally
    Pdf.Free;
  end;
end;

Pravokuti se obrezuju, rotirana geometrija ne

Putevi se rastavljaju samo kad PDFiumPas može dokazati da je put pravokut poravnat s osima. Dokaz je namjerno uzak: matrica objekta mora imati oba smična pojma ispod 0.0001, put mora imati četiri do šest segmenata koje počinju MOVETO i nastavljaju samo LINETO, a transformirane točke moraju sletjeti na sva četiri kuta granica objekta u toleranciji 0.01. Put koji prođe tu provjeru smanjuje se uzastopnim oduzimanjem pravokuta, svaki pravokut redakcije izreže skup preživjelih na lijeve, desne, donje i gornje trake, i svaka nastala traka ponovno se stvara s izvornim načinom ispune, zastavom poteza i stanjem boje. Krivulje, trokuti, oblici pod maskom i sve rotirano pada provjeru i cijeli se objekt uklanja

Slike slijede ISO 32000-1 §8.9, gdje uzorci slike zauzimaju jedinični kvadrat mapiran kroz trenutnu transformacijsku matricu. PDFiumPas to mapiranje invertira da svaki preživjeli fragment u prostoru stranice vrati u normalizirane koordinate slike, steže ih u jedinični interval, a zatim pretvara u pikselne indekse zaokruživanjem prema unutra: lijevi i gornji rub prolaze kroz Ceil, desni i donji kroz Floor. Smjer je bitan. Zaokruživanje prema van dopustilo bi da djelomični stupac izvornih piksela s rezane strane preživi na rubu fragmenta. Cjelobrojne pikselne granice zatim se vraćaju u normalizirane koordinate i služe za izvođenje matrice fragmenta, pa izrezana bitmapa sleti točno na pikselnu granicu na kojoj je rezana. Samo rezanje je kopija redova svjesna koraka preko formata Gray, BGR, BGRx i BGRA. Kao kod putova, rotirana ili smicana slika, ili slika čija matrica ima degenerirani pojam mjerila, uklanja se u cijelosti

Kako PDFiumPas u Delphiju obreže djelomično rezanu sliku: preživjeli fragment u prostoru stranice vraća se kroz invertirani CTM u normalizirane koordinate slike, steže u jedinični interval i zaokružuje prema unutra tako da nijedan stupac piksela iz redakcije ne preživi
Ceil lijevo i gore, Floor desno i dolje, pa rez slijeće na cjelobrojnu pikselnu granicu
// After a successful SaveAsRedacted call
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
  // nothing could be split: every intersecting object was dropped whole
  LogWholeObjectFallback(SourcePdf);

Zašto PDFiumPas propada zatvoreno na nemapiranim znakovima?

Jer znak bez reproducibilnog Unicode skalara ne može se pošteno obnoviti. Rekonstrukcija preživjelog znači pozivanje API-ja za postavljanje teksta s nizom, a to traži stabilnu točku koda za svaki zadržani znak. Simbolički podskupni fontovi s pokvarenim ili odsutnim podacima ToUnicode mogu dati prazno mapiranje, a ponovno kodiranje pogađanjem proizvelo bi izlaz koji na ekranu izgleda ispravno dok nosi drugi znak ispod. PDFiumPas odbija: provjera zadržanih znakova podiže iznimku, iznimka se hvata unutar SaveAsRedacted, TPdfRedactionReport.Succeeded vraća se False s porukom u ErrorMessage, a funkcija vraća False. Isto pravilo vrijedi za proračun rastavljanja, koji podiže iznimku umjesto da tiho odreže skup fragmenata. Kad dokument ima fontova kojima ne vjerujete i želite determinističko staro ponašanje, postavite Options.PreservePartialObjects := False i svaki sijecući objekt ode cijeli

Čišćenje resursa kroz dijeljene opsege

Rastavljanje objekata ostavlja siročad iza sebe, a njihovo čišćenje nije jednostavno kao usporedba rječnika /Resources na razini stranice. ISO 32000-1 §7.8.3 dopušta da isti rječnik resursa istovremeno referenciraju više stranica, Form XObjects, uzorci i tokovi izgleda anotacija. Brisanje imena fonta jer ga je jedna stranica prestala koristiti slomi drugu stranicu koja ga još koristi. PruneUnusedPdfResources zato radi po opsegu: razrješuje /Contents bilo da je izravni niz, neizravna referenca na niz ili jedan tok, a zatim prikuplja upotrebu resursa od operatora koji resurse stvarno imenuju — Tf za fontove, Do za XObjects, gs za grafičko stanje, CS, cs, SCN i scn za prostore boja i uzorke, sh za zasjenjenja, BDC i DP za svojstva označenog sadržaja, plus unos /CS ugrađenih slika. Kad jedan rječnik dijeli više opsega, skupovi korištenih imena spajaju se po kategoriji prije nego što se bilo što ukloni

Čišćenje resursa u PDFiumPasu: tri opsega referenciraju jedan dijeljeni rječnik resursa, njihovi skupovi korištenih imena spajaju se po kategoriji, i samo imena na koja nijedan opseg ne pokazuje uklanjaju se prije nego se nedostižni objekti pometu
Jedan rječnik može služiti više stranica, form XObjects i tokova izgleda, pa PDFiumPas spaja svaki skup korištenih imena prije nego baci ijedno ime

Bacaju se samo imena potvrđeno nereferencirana u svakom opsegu koji pokazuje na rječnik. Opseg koji se ne može pouzdano raščlaniti ostaje netaknut, a to je konzervativan smjer: neočišćena datoteka je samo veća, pogrešno očišćena je pokvarena. Preživjeli rječnici upisuju se natrag kao rijetka inkrementalna dopuna koja nosi točne brojeve generacija, a prepravka dostiživosti zatim pomete objekte koji su postali nedostižni kad su imena nestala. TPdfResourcePruneReport izvještava ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, brojeve bajtova i zastavicu Succeeded. SaveAsRedacted taj korak pokreće automatski nad očišćenim izlazom, pa ga put redakcije već uključuje, ali je funkcija izvezena na razini tokova za cjevovode koji je žele sami

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 stays False: an incremental rewrite would
      // invalidate the byte ranges a signature covers
      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;

Uklapanje u cjevovod dokumenata

Put redakcije nikad ne mijenja dokument koji ste učitali. SaveAsRedacted snimi izolirani snimak, tamo primijeni anotacije /Redact, oguli privitke, provede prolaz dezinfekcije koji uklanja otvorenu akciju, akcije kataloga, stabla imena, pridružene datoteke, AcroForm i metapodatke, očisti resurse i tek onda upiše izlazni tok. Ponovno otvaranje tog izlaza kao neovisnog dokumenta i ponovno izvlačenje teksta je korak provjere vrijedan vlastitog testnog skupa, jer je jedina provjera koja odgovara na izvorno pitanje — može li čitatelj još dobiti niz. Jedna posljedica za planiranje: rastavljanje zamjenjuje objekte stranice, pa je svako FPDF_PAGEOBJECT rukovanje koje ste držali mrtvo poslije, ista zamka životnog vijeka opisana u zastarjelim rukovanjima objekata stranice nakon transformacije

Dva susjedna komada dopunjuju tijek rada. Odluka gdje idu pravokuti redakcije obično polazi iz izvučene geometrije, a model blokova i redoslijeda čitanja u strukturiranim tekstualnim blokovima i redoslijedu čitanja je bolji izvor kandidatskih okvira od sirovih nizova znakova. Služenje rezultata recenzentu pripada pravilima očvršćivanja u građenju sigurnog PDF pregleda, gdje ispunjavanje obrasca i JavaScript ostaju po zadanom isključeni. Zajedno pokrivaju petlju koju većina tijekova usklađenosti treba: lociraj, izreži na razini operatora, provjeri ponovnim otvaranjem, pregledaj sigurno. Potpuna API površina, probno preuzimanje i uvjeti licenciranja komponente žive na stranici proizvoda PDFium Delphi komponente