Joku maalaa mustan laatikon nimen päälle, tasoittaa mitään, toimittaa tiedoston, ja katselmoija valitsee suorakulmion ja liittää nimen sähköpostiviestiin. PDFiumPas vastaa tähän operaattoritason mustoituksella: SaveAsRedacted poistaa vain ne Unicode-skaalarit, joiden merkkilaatikot koskettavat mustoitussuorakulmiota, rakentaa jäljelle jääneet uudelleen alkuperäisestä fontista, koosta, matriisista, renderöintitavasta ja väristä sekä rajaa akselisuuntaiset polut ja kuvat sen sijaan että hylkäisi ne kokonaisuudessaan
Miksi maalattu suorakulmio ei ole mustoitus
Sisältövirran päälle lisätty piirto-operaatio ei piilota mitään, koska sen alla olevat tekstiä näyttävät operaattorit ovat yhä virrassa ja mapautuvat yhä koodipisteisiin. ISO 32000-1 §9.4 määrittelee tekstiobjektin sijoitus- ja näyttöoperaatioiden jonoksi BT- ja ET-merkkien sisällä; sen jälkeen piirretty täytetty suorakulmio on vain toinen operaattori samassa virrassa. Poiminta kulkee operaattorien läpi, ei pikselien, joten katettu merkkijono palaa koskemattomana. Aito mustoitus edellyttää operandin poistamista, ei tulosteen hämärtämistä
Ilmeinen turvallinen toteutus on brutaali: etsi jokainen sivuobjekti, jonka rajauslaatikko leikkaa mustoitussuorakulmiota, ja poista koko objekti. Sen aiemmat PDFiumPas-julkaisut tekivät, ja se on oikein mutta kallis. Yksittäinen Tj voi kantaa kokonaisen taulukkorivin, joten yhden tilinumeron mustaaminen vei mukanaan päivämäärän, kuvauksen ja summan. Suorakulmainen täyttö, joka sattui olemaan täysleveä taulukkokaista, katosi koko sivun laajuisesti. Laskulogo katosi, koska mustoitus leikkasi siitä yhden kulman. Versio 3.101.0 siirtää päätöksen yhden tason alemmas, sivuobjektista operandiin
Mitä operaattoritason mustoitus oikeasti poistaa?
PDFiumPas poistaa Unicode-skaalareita, ei tekstiobjekteja. SaveAsRedactedin aikana komponentti rakentaa merkki-sivuobjekti-mappauksen ladatusta tekstisivusta, lukee sitten jokaiselle testattavan objektin omistamalle merkille merkkilaatikon ja leikkaa kyseisen laatikon jokaista mustoitussuorakulmiota vasten. Suorakulmioon koskettavat merkit merkitään poistettaviksi; loput merkitään jäljelle jääviksi. Jos mikään ei leikkaa, objekti jätetään täysin rauhaan. Jos jokainen merkki leikkaa, objekti poistetaan kokonaisena, aivan kuten ennenkin. Vain sekatapaus laukaisee jaon
Jokainen jäljelle jäänyt lähetetään sitten uudelleen omana tekstiobjektinaan, joka on rakennettu alkuperäisestä fonttikahvasta, alkuperäisestä fonttikoosta, merkikohtaisesta tekstimatriisista, alkuperäisestä tekstin renderöintitavasta sekä isäobjektin täyttö- ja viivaustilasta mukaan lukien viivanleveys, viivanliitos, viivan pääte ja viivakuvio. Fonttikahvan uudelleenkäyttö uuden selvittämisen sijaan pitää glyyfit metrisesti identtisinä, ja merkikohtaisen matriisin uudelleenkäyttö pitää parituksen ja sanavälit paikoillaan ilman asettelun uudelleenajoa. Hintana on objektilukumäärä: yksi säilytetty merkki muuttuu yhdeksi tekstiobjektiksi, minkä vuoksi TPdfRedactionOptions.MaxSplitObjects on olemassa kovana kattona generoiduille fragmenteille
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // tiedosto sisältää jo /Redact-annotaatiot
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // operaattoritason jako (oletus)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // katto generoiduille fragmenteille
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // fail closed, älä toimita
finally
Pdf.Free;
end;
end;
Suorakulmiot rajataan, kierretty geometria ei
Polut jaetaan vain, kun PDFiumPas pystyy todistamaan polun olevan akselisuuntainen suorakulmio. Todistus on tahallaan kapea: objektimatriisilla on oltava molemmat leikkaustermit alle 0.0001, polun on koostuttava neljästä kuuteen segmenttiin, jotka alkavat MOVETO- ja jatkavat vain LINETO-segmenteillä, ja muunnettujen pisteiden on osuttava objektin rajojen kaikkiin neljään kulmaan 0.01:n toleranssin sisällä. Tarkistuksen läpäisevä polku pienennetään peräkkäisellä suorakulmiovähennyslaskulla, jossa jokainen mustoitussuorakulmio veistää jäljelle jääneen joukon vasemmaksi, oikeaksi, alapuoliseksi ja yläpuoliseksi kaistaksi, ja jokainen syntyvä kaista luodaan uudelleen alkuperäisellä täyttötavalla, viivauslipulla ja maalaustilalla. Käyrät, kolmiot, leikatut muodot ja kaikki kierretty epäonnistuvat tarkistuksessa ja koko objekti poistetaan
Kuvat noudattavat standardia ISO 32000-1 §8.9, jossa kuvanäytteet vievät yksikköneliön, joka mapautuu nykyisen transformaatiomatriisin kautta. PDFiumPas kääntää kyseisen mappauksen kääntääkseen jokaisen jäljelle jääneen sivutilan fragmentin takaisin normalisoituihin kuvakoordinaatteihin, puristaa ne yksikköväliin ja muuntaa sitten pikseli-indekseiksi pyöristämällä sisäänpäin: vasen ja yläreuna kulkevat Ceilin kautta, oikea ja alareuna Floorin kautta. Suunnalla on merkitystä. Ulospäin pyöristäminen päästäisi osittaisen lähdepikselisarakkeen mustoitetulta puolelta henkiin fragmentin reunalla. Kokonaislukupikselirajat muunnetaan sitten takaisin normalisoituihin koordinaatteihin ja niistä johdetaan fragmentin matriisi, joten rajattu bittikartta osuu täsmälleen sille pikselirajalle, jolla se leikattiin. Itse rajaus on stride-tietoinen rivikopio Gray-, BGR-, BGRx- ja BGRA-muotojen yli. Kuten poluilla, kierretty tai vino kuva, tai kuva jonka matriisissa on degeneroitunut skaalaustermi, poistetaan kokonaan
// Onnistuneen SaveAsRedacted-kutsun jälkeen
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
// mitään ei voitu jakaa: jokainen leikkaava objekti pudotettiin kokonaisena
LogWholeObjectFallback(SourcePdf);
Miksi PDFiumPas toimii fail closed -periaatteella mappaamattomilla merkeillä?
Koska glyyfiä, jolla ei ole toistettavaa Unicode-skaalaria, ei voi rakentaa rehellisesti uudelleen. Jäljelle jääneen rakentaminen uudelleen tarkoittaa tekstiä asettavan API:n kutsumista merkkijonolla, ja se edellyttää vakaata koodipistettä jokaiselle säilytetylle merkille. Symboliset osajoukkofontit, joiden ToUnicode-tieto on rikki tai puuttuu, voivat tuottaa tyhjän mappauksen, ja arvaukseen perustuva uudelleenkoodaus tuottaisi tulosteen, joka näyttää oikealta ruudulla mutta kantaa allaan eri merkkiä. PDFiumPas kieltäytyy: säilytettyjen merkkien tarkistus nostaa poikkeuksen, poikkeus tarkistetaan SaveAsRedactedin sisällä, TPdfRedactionReport.Succeeded palaa arvolla False ja viesti on ErrorMessage-kentässä, ja funktio palauttaa arvon False. Sama sääntö pätee jakobudjettiin, joka nostaa poikkeuksen sen sijaan että hiljaisesti typistäisi fragmenttijoukon. Kun asiakirjassa on fontteja, joihin et luota, ja haluat deterministisen vanhan käyttäytymisen, aseta Options.PreservePartialObjects := False ja jokainen leikkaava objekti poistuu kokonaisena
Resurssien karsinta jaetuissa laajuuksissa
Objektien jakaminen jättää orpoja jälkeensä, eikä niiden karsinta ole yhtä yksinkertaista kuin sivutason /Resources-sanakirjan vertailu. ISO 32000-1 §7.8.3 sallii saman resurssisanakirjan olevan useiden sivujen, Form XObjectien, kuvioiden ja annotaatioiden ulkoasuvirtojen viittaama yhtä aikaa. Fonttinimen poistaminen siksi, että yksi sivu lopetti sen käytön, rikkoo toisen sivun, joka käyttää sitä yhä. PruneUnusedPdfResources toimii siksi laajuutta kohden: se selvittää /Contents-merkinnän riippumatta siitä, onko se suora taulukko, epäsuora viittaus taulukkoon vai yksittäinen virta, ja kerää sitten resurssien käytön operaattoreista, jotka oikeasti nimeävät resursseja — Tf fonteille, Do XObjecteille, gs grafiikkatilalle, CS, cs, SCN ja scn väriavaruuksille ja kuvioille, sh varjostuksille, BDC ja DP merkityn sisällön ominaisuuksille sekä upotuskuvien /CS-merkintä. Kun yksi sanakirja on usean laajuuden jaettava, käytettyjen nimien joukot yhdistetään kategorioittain ennen mitään poistamista
Pudotetaan vain ne nimet, joiden on vahvistettu olevan viittaamattomia jokaisessa sanakirjaan osoittavassa laajuudessa. Laajuus, jota ei voi jäsentää luotettavasti, jätetään koskemattomaksi, ja se on konservatiivinen suunta: karsimaton tiedosto on vain suurempi, väärin karsittu on korruptoitunut. Selvinneet sanakirjat kirjoitetaan takaisin harvana inkrementaalisena päivityksenä, joka kantaa tarkkoja sukupolvilukuja, ja saavutettavuusuudelleenkirjoitus poistaa sitten ne objektit, joista tuli tavoittamattomia nimien kadottua. TPdfResourcePruneReport raportoi arvot ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, tavumäärät ja lipun Succeeded. SaveAsRedacted suorittaa tämän vaiheen automaattisesti sanitoituun tulokseen, joten mustoituspolku sisältää sen jo valmiiksi, mutta funktio on viety myös virtatasolla putkia varten, jotka haluavat sen itsenäisenä
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 pysyy arvossa False: inkrementaalinen uudelleenkirjoitus
// mitätöisi allekirjoituksen kattamat tavualueet
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;
Kytkeminen asiakirjaputkeen
Mustoituspolku ei koskaan muokkaa lataamaasi asiakirjaa. SaveAsRedacted ottaa eristetyn tilannekuvan, soveltaa /Redact-annotaatiot siihen, poistaa liitteet, suorittaa sanitointikierroksen, joka poistaa avautumistoiminnon, katalogitoiminnot, nimipuut, assosioidut tiedostot, AcroFormin ja metatiedot, karsii resurssit ja vasta sen jälkeen kirjoittaa tulostevirran. Kyseisen tulosteen avaaminen uudelleen itsenäisenä asiakirjana ja tekstin uudelleenpoiminta on varmistusvaihe, joka kannattaa pitää omassa testipakettissa, koska se on ainoa tarkistus, joka vastaa alkuperäiseen kysymykseen — saako lukija merkkijonon yhä. Yksi suunniteltava seuraus: jako korvaa sivuobjektit, joten jokainen hallussa ollut FPDF_PAGEOBJECT-kahva on sen jälkeen kuollut, sama elinkaarasieppo, joka kuvataan artikkelissa vanhentuneet sivuobjektien kahvat transformoinnin jälkeen
Kaksi viereistä palasta tekee työnkulusta kokonaisen. Päätös siitä, minne mustoitussuorakulmiot sijoitetaan, lähtee yleensä poimitusta geometriasta, ja lohko- ja lukujärjestysmalli artikkelissa jäsennellyt tekstilohkot ja lukujärjestys on parempi lähde ehdokaslaatikoille kuin raakat merkkijonot. Tuloksen tarjoaminen katselmoijalle kuuluu kovennussääntöihin artikkelissa turvallisen PDF-esikatselun rakentaminen, jossa lomakkeiden täyttö ja JavaScript pysyvät oletuksena pois päältä. Yhdessä ne kattavat silmukan, jota useimmat vaatimustenmukaisuustyönkulut tarvitsevat: paikanna, mustoita operaattoritasolla, varmista avaamalla uudelleen, esikatsele turvallisesti. Koko API-pinta, kokeilulataus ja komponentin lisensointiehdot ovat PDFium Delphi Component -tuotesivulla