Tehnički članak

Redakcija PDF-a na nivou operatora u Delphi-ju

Neko nacrta crni kvadrat preko imena, ništa ne spljošti, pošalje fajl, i recenzent selektuje pravougaonik i nalepi ime u mejl. PDFiumPas odgovara na to redakcijom na nivou operatora: SaveAsRedacted briše samo Unicode skalarne čije su kutije znakova dotakle pravougaonik redakcije, ponovo gradi preživele iz originalnog fonta, veličine, matrice, render režima i boje, i iseče ose poravnate putanje i slike umesto da ih ispusti cele

Zašto naslikani pravougaonik nije redakcija

Operacija crtanja dodata preko toka sadržaja ne krije ništa, jer operatori koji prikazuju tekst ispod nje su i dalje u toku i dalje se mapiraju na kodne tačke. ISO 32000-1 §9.4 definiše tekst objekt kao niz operatora pozicioniranja i prikazivanja unutar BT i ET; pun pravougaonik nacrtan posle je jednostavno još jedan operator u istom toku. Izvlačenje šeta operatore, ne piksele, pa pokriveni string dolazi nazad netaknut. Prava redakcija mora ukloniti operand, ne zamračiti izlaz

Očigledna bezbedna implementacija je brutalna: nađi svaki objekat stranice čiji bounding box seče pravougaonik redakcije i obriši ceo objekat. To su ranija PDFiumPas izdanja radila, i ispravno je ali skupo. Jedan Tj može nositi ceo red tabele, pa zamračivanje jednog broja računa odnelo je datum, opis i iznos sa sobom. Pravougaonsko punjenje koje slučajno jeste punoširina traka tabele nestalo je preko cele stranice. Logo fakture nestao je jer je redakcija odsekla jedan njegov ugao. Verzija 3.101.0 pomera odluku jedan nivo niže, sa objekta stranice na operand

Šta redakcija na nivou operatora stvarno briše?

PDFiumPas briše Unicode skalarne, ne tekst objekte. Tokom SaveAsRedacted komponenta gradi preslikavanje znak-u-objekat-stranice iz učitane tekstualne stranice, pa za svaki znak u vlasništvu objekta pod testom čita kutiju znaka i seče tu kutiju protiv svakog pravougaonika redakcije. Znaci koji dotaknu pravougaonik se označavaju za uklanjanje; ostali se označavaju kao preživeli. Ako se ništa ne seče, objekat se potpuno ostavlja na miru. Ako se svaki znak seče, objekat se uklanja ceo, baš kao pre. Samo mešani slučaj okida razdvajanje

PDFiumPas redakcija na nivou operatora upoređena sa brisanjem celog objekta u Delphi-ju: stara putanja ispušta ceo tekst objekt kad je jedan broj računa pokriven, dok putanja razdvajanja briše samo znakove koji seku i ponovo emituje svakog preživelog kao svoj tekst objekt
Samo mešani slučaj okida razdvajanje: ništa koje seče ostavlja objekat na miru, sve što seče ga uklanja cele

Svaki preživeli se zatim ponovo emituje kao svoj tekst objekt izgrađen iz originalnog rukovanca fonta, originalne veličine fonta, tekst matrice po znaku, originalnog režima renderovanja teksta, i fill i stroke stanja roditeljskog objekta uključujući širinu poteza, spajanje linija, kraj linije i niz crtica. Ponovna upotreba rukovanca fonta umesto rešavanja novog je ono što činke metrički identičnima, i ponovna upotreba matrice po znaku je ono što drži kerning i razmak rečeva na mestu bez ponovnog pokretanja rasporeda. Cena je broj objekata: jedan zadržani znak postaje jedan tekst objekt, po čemu TPdfRedactionOptions.MaxSplitObjects postoji kao kruta granica na generisane fragmente

procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
  Pdf: TPdf;
  Options: TPdfRedactionOptions;
  Report: TPdfRedactionReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := SourcePdf;   // fajl već nosi /Redact anotacije
    Pdf.Active := True;

    Options := TPdfRedactionOptions.Default;
    Options.PreservePartialObjects := True;    // razdvajanje na nivou operatora (podrazumevano)
    Options.RemoveIntersectingAnnotations := True;
    Options.MaxSplitObjects := 20000;          // granica na generisane fragmente

    if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
      raise Exception.Create(Report.ErrorMessage);   // padni zatvoreno, nemoj isporučiti
  finally
    Pdf.Free;
  end;
end;

Pravougaonici se iseču, rotirana geometrija ne

Putanje se razdvajaju tek kada PDFiumPas može dokazati da je putanja ose poravnat pravougaonik. Dokaz je namerno uzan: matrica objekta mora imati oba smična člana ispod 0.0001, putanja se mora sastojati od četiri do šest segmenata koja počinju MOVETO i nastavljaju samo LINETO, i transformisane tačke moraju sleteti na sva četiri ugla granica objekta unutar tolerancije 0.01. Putanja koja prođe tu proveru se smanjuje uzastopnim oduzimanjem pravougaonika, svaki pravougaonik redakcije reže skup preživelih u leve, desne, donje i gornje trake, i svaka nastala traka se ponovo stvara sa originalnim režimom punjenja, stroke flagom i stanjem bojanja. Krive, trouglovi, iseci oblici i išta rotirano pada tu proveru i ceo objekat se uklanja

Slike slede ISO 32000-1 §8.9, gde uzorci slike zauzimaju jedinični kvadrat preslikan kroz trenutnu matricu transformacije. PDFiumPas obrće to preslikavanje da bi svaki preživeli fragment u prostoru stranice vratio u normalizovane koordinate slike, sabija ih u jedinični interval, pa pretvara u piksel indekse zaokruživanjem unutra: leve i gornje ivice idu kroz Ceil, desne i donje kroz Floor. Taj smer biti. Zaokruživanje napolje bi dozvolilo parcijalnoj koloni izvornih piksela sa redakcione strane da preživi na ivici fragmenta. Celi broj piksel granice se zatim vraćaju u normalizovane koordinate i koriste za izvođenje matrice fragmenta, pa isečena bitmapa sleće baš na piksel granicu na kojoj je odsečena. Samo iseckanje je kopija redova svesna koraka preko Gray, BGR, BGRx i BGRA formata. Kao kod putanja, rotirana ili nakošena slika, ili jedna čija matrica ima degenerisan član razmere, uklanja se u celosti

Kako PDFiumPas iseče delimično redaktovanu sliku u Delphi-ju: preživeli fragment u prostoru stranice se preslikava nazad kroz obrnutu CTM u normalizovane koordinate slike, sabija u jedinični interval i zaokružuje unutra pa nijedna redaktovana piksel kolona ne preživi
Ceil levo i gore, Floor desno i dole, pa odsecanje sleće na celobrojnu piksel granicu
// Posle uspešnog SaveAsRedacted poziva
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šta se nije moglo razdvojiti: svaki objekat koji je sekao ispušten je ceo
  LogWholeObjectFallback(SourcePdf);

Zašto PDFiumPas pada zatvoreno na nemapirane znakove?

Zato što se glif bez reproduktivnog Unicode skalara ne može iskreno ponovo izgraditi. Rekonstrukcija preživelog znači pozivanje API-ja za postavljanje teksta sa stringom, a to zahteva stabilnu kodnu tačku za svaki zadržani znak. Simbolički podskup fontovi sa pokvarenim ili odsutnim ToUnicode podacima mogu dati prazno preslikavanje, a ponovno kodiranje nagađanjem bi dalo izlaz koji izgleda ispravno na ekranu dok ispod nosi drugi znak. PDFiumPas odbija: provera zadržanih znakova diže izuzetak, izuzetak se hvata unutar SaveAsRedacted, TPdfRedactionReport.Succeeded se vraća kao False sa porukom u ErrorMessage, i funkcija vraća False. Isto pravilo važi za budžet razdvajanja, koji diže izuzetak umesto da ušutke skraćuje skup fragmenata. Kada dokument ima fontove kojima ne verujete i želite determinističko staro ponašanje, postavite Options.PreservePartialObjects := False i svaki objekat koji seče nestaje ceo

Šišanje resursa kroz deljene opsege

Razdvajanje objekata ostavlja siročiđa iza sebe, a njihovo šišanje nije tako jednostavno kao poređenje rečnika /Resources nivoa stranice. ISO 32000-1 §7.8.3 dozvoljava da isti rečnik resursa bude referenciran od strane više stranica, Form XObjects, uzoraka i izglednih tokova anotacija istovremeno. Brisanje imena fonta zato što je jedna stranica prestala da ga koristi slomiće drugu stranicu koja ga i dalje koristi. PruneUnusedPdfResources zato radi po opsegu: rešava /Contents bilo da je direktan niz, indirektna referenca na niz ili jedan tok, pa sakuplja korišćenje resursa od operatora koji stvarno imenuju resurse — Tf za fontove, Do za XObjects, gs za grafičko stanje, CS, cs, SCN i scn za prostore boja i uzorke, sh za senčenja, BDC i DP za svojstva označenog sadržaja, plus /CS unos ugrađenih slika. Kada jedan rečnik deli više opsega, skupovi korišćenih imena se ujedinjavaju po kategoriji pre nego što se bilo šta ukloni

Šišanje resursa u PDFiumPas-u: tri opsega referenciraju jedan deljeni rečnik resursa, njihovi skupovi korišćenih imena se ujedinjavaju po kategoriji, i samo imena na koja nijedan opseg ne ukazuje se uklanjaju pre nego što se nedostižni objekti počiste
Jedan rečnik može opsluživati više stranica, form XObjects i izglednih tokova, pa PDFiumPas ujedinjava svaki skup korišćenih imena pre ispuštanja ijednog imena

Ispuštaju se samo imena potvrđeno nereferencirana u svakom opsegu koji ukazuje na rečnik. Opseg koji se ne može pouzdano raščlaniti ostavlja se netaknut, što je konzervativan smer: nešišan fajl je samo veći, pogrešno šišan je oštećen. Preživeli rečnici se upisuju nazad kao redak inkrementalno ažuriranje koje nosi tačne brojeve generacije, a preprava dostižnosti zatim počisti objekte koji su postali nedostižni čim su imena nestala. TPdfResourcePruneReport izveštava ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, brojeve bajtova i flag Succeeded. SaveAsRedacted pokreće ovaj korak automatski nad sanitizovanim izlazom, pa ga putanja redakcije već uključuje, ali je funkcija izvezena na nivou toka za pipeline-ove koji je žele odvojeno

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 ostaje False: inkrementalna preprava bi
      // poništila bajt opsege koje potpis pokriva
      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 dokument pipeline

Putanja redakcije nikada ne mutira dokument koji ste učitali. SaveAsRedacted beleži izolovani snimak, primenjuje /Redact anotacije na njemu, skida priloge, pokreće prolaz sanitizacije koji uklanja open action, akcije kataloga, stabla imena, pridružene fajlove, AcroForm i metapodatke, šiša resurse, i tek onda upisuje izlazni tok. Ponovno otvaranje tog izlaza kao nezavisnog dokumenta i ponovno izvlačenje teksta je korak provere vredan čuvanja u vašem test skupu, jer je to jedina provera koja odgovara na prvobitno pitanje — može li čitalac još doći do stringa. Jedna posledica koju treba planirati: razdvajanje zamenjuje objekte stranice, pa je svaki FPDF_PAGEOBJECT rukovanac koji ste držali mrtav posle toga, ista zamka životnog veka opisana u zastarelim rukovaocima objekata stranice posle transformacije

Dva susedna komada čine tok rada kompletanim. Odlučivanje gde pravougaoni redakcije idu obično polazi iz izvučene geometrije, i model blokova i redosleda čitanja u strukturiranim tekst blokovima i redosledu čitanja je bolji izvor kandidatskih kutija nego sirovi znakovni nizovi. Služenje rezultata recenzentu pripada pravilima očvršćavanja u izgradnji sigurnog PDF pregleda, gde popunjavanje formulara i JavaScript ostaju isključeni po podrazumevanom. Zajedno pokrivaju petlju koju većina compliance tokova treba: lociraj, redaktuj na nivou operatora, proveri ponovnim otvaranjem, pregledaj sigurno. Cela API površina, probno preuzimanje i licencni uslovi za komponentu žive na stranici proizvoda PDFium Delphi Component