Noen maler en svart boks over et navn, flater ut ingenting, sender filen, og korrekturleseren velger rektangelet og limer navnet inn i en e-post. PDFiumPas svarer på det med redigering på operatornivå: SaveAsRedacted sletter bare Unicode-skalarene hvis tegnbokser berører et redigeringsrektangel, gjenoppbygger overlevende fra original font, størrelse, matrise, rendermodus og farge, og beskjærer aksejusterte baner og bilder i stedet for å slippe dem hele
Hvorfor et malt rektangel ikke er en redigering
En tegneoperasjon lagt til oppå en innholdsstrøm skjuler ingenting, fordi tekstvisingsoperatorene under den fortsatt er i strømmen og fortsatt mappes til kodepunkt. ISO 32000-1 §9.4 definerer et tekstobjekt som en sekvens av posisjonerings- og visingsoperatorer inne i BT og ET; et fylt rektangel tegnet etterpå er rett og slett en annen operator i samme strøm. Ekstraksjon går gjennom operatorene, ikke pikslene, så den dekkede strengen kommer tilbake intakt. Ekte redigering må fjerne operanden, ikke skjule utdataene
Den åpenbare trygge implementeringen er brutal: finn hvert sideobjekt hvis bounding box skjærer et redigeringsrektangel og slett hele objektet. Det er hva tidligere PDFiumPas-utgivelser gjorde, og det er korrekt men dyrt. En enkelt Tj kan bære en hel tabellrad, så å sverte ett kontonummer tok med seg datoen, beskrivelsen og beløpet. En rektangulær fylling som tilfeldigvis var et tabelbånd i full bredde, forsvant over hele siden. En fakturalogo forsvant fordi redigeringen klippet av ett hjørne av den. Versjon 3.101.0 flytter beslutningen ett nivå ned, fra sideobjektet til operanden
Hva sletter redigering på operatornivå egentlig?
PDFiumPas sletter Unicode-skalarer, ikke tekstobjekter. Under SaveAsRedacted bygger komponenten en tegn-til-sideobjekt-mapping fra den innlastede tekstsiden, deretter for hvert tegn eid av objektet under test leser den tegnboksen og skjærer den boksen mot hvert redigeringsrektangel. Tegn som berører et rektangel markeres for fjerning; resten markeres som overlevende. Hvis ingenting skjærer, blir objektet fullstendig i fred. Hvis hvert tegn skjærer, fjernes objektet helt, nøyaktig som før. Bare det blandede tilfellet utløser en splitting
Hver overlevende re-emitteres deretter som sitt eget tekstobjekt bygget fra original fonthåndtak, den opprinnelige fontstørrelsen, per-tegn tekstmatrisen, den opprinnelige tekstrendermodusen og fyll- og stryketilstanden til foreldreobjektet inkludert strekbredde, linjeskjøt, linjeende og stiplet array. Å gjenbruke fonthåndtaket i stedet for å løse et nytt, er det som holder glyfene metrisk identiske, og å gjenbruke per-tegn-matrisen er det som holder kerning og ordmellomrom på plass uten å kjøre oppsett på nytt. Kostnaden er objektantall: ett beholdt tegn blir ett tekstobjekt, og det er derfor TPdfRedactionOptions.MaxSplitObjects finnes som et hardt tak på genererte fragmenter
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // filen bærer allerede /Redact-annoteringer
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // operatornivå-splitting (standarden)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // tak på genererte fragmenter
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // feil lukket, ikke send
finally
Pdf.Free;
end;
end;
Rektangler beskjæres, rotert geometri gjør ikke
Baner splittes bare når PDFiumPas kan bevise at banen er et aksejustert rektangel. Beviset er bevisst smalt: objektmatrisen må ha begge shear-uttrykkene under 0.0001, banen må bestå av fire til seks segmenter som begynner med en MOVETO og fortsetter med bare LINETO, og de transformerte punktene må lande på alle fire hjørnene av objektgrensene innen en toleranse på 0.01. En bane som klarer den sjekken reduseres ved suksessiv rektangelsubtraksjon, hvert redigeringsrektangel utskjærer det overlevende settet i venstre, høyre, under og over striper, og hver resulterende stripe gjenskapes med original fyllmodus, strekflagg og malingstilstand. Kurver, trekanter, klypte former og alt rotert stryker sjekken og hele objektet fjernes
Bilder følger ISO 32000-1 §8.9, der bildeprøvene okkuperer enhetkvadratet mappet gjennom den gjeldende transformasjonsmatrisen. PDFiumPas inverterer den mappingen for å gjøre hvert overlevende sideområde-fragment tilbake til normaliserte bildekoordinater, clamper dem til enhetsintervallet, og konverterer deretter til pikselindekser ved avrunding innover: venstre og øvre kanter går gjennom Ceil, høyre og nedre gjennom Floor. Den retningen betyr noe. Avrunding utover ville la en delvis kolonne av kildepiksler fra den redigerte siden overleve ved fragmentkanten. De heltalls pikselgrensene konverteres deretter tilbake til normaliserte koordinater og brukes til å utlede fragmentmatrisen, så det beskjærte bitmapet lander nøyaktig på pikselgrensen det ble kuttet ved. Beskjæringen selv er en stride-bevisst radkopi på tvers av Gray-, BGR-, BGRx- og BGRA-formatene. Som med baner, fjernes et rotert eller skjevt bilde, eller ett hvis matrise har et degenerert skalauttrykk, i sin helhet
// Etter et vellykket SaveAsRedacted-kall
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
// ingenting kunne splittes: hvert skjøtende objekt ble sluppet helt
LogWholeObjectFallback(SourcePdf);
Hvorfor feiler PDFiumPas lukket på umappede tegn?
Fordi en glyf som ikke har noe reproduserbart Unicode-scalar ikke kan gjenoppbygges ærlig. Å rekonstruere en overlevende betyr å kalle tekstsette-API-et med en streng, og det krever et stabilt kodepunkt for hvert beholdt tegn. Symboliske subsettfonter med ødelagte eller fraværende ToUnicode-data kan gi en tom mapping, og re-encoding ved gjetting ville produsere utdata som ser riktige ut på skjermen mens de bærer et annet tegn under. PDFiumPas nekter: den beholdt-tegn-sjekken kaster, unntaket fanges inne i SaveAsRedacted, TPdfRedactionReport.Succeeded kommer tilbake False med meldingen i ErrorMessage, og funksjonen returnerer False. Samme regel gjelder splittbudsjettet, som kaster i stedet for stille å avkorte fragmentsettet. Når et dokument har fonter du ikke stoler på, og du vil ha den deterministiske gamle oppførselen, sett Options.PreservePartialObjects := False og hvert skjøtende objekt forsvinner helt
Ressursbeskjæring på tvers av delte scopes
Å splitte objekter etterlater foreldreløse, og å beskjære dem er ikke så enkelt som å diff-e side-nivå /Resources-ordlisten. ISO 32000-1 §7.8.3 lar samme ressursordliste refereres av flere sider, av Form XObjects, av mønstre og av annoteringsutseendestrømmer samtidig. Å slette et fontnavn fordi én side sluttet å bruke det, vil knekke en annen side som fortsatt gjør det. PruneUnusedPdfResources virker derfor per scope: den løser /Contents enten det er en direkte array, en indirekte referanse til en array eller en enkelt strøm, og samler deretter ressursbruk fra operatorene som faktisk navngir ressurser — Tf for fonter, Do for XObjects, gs for grafikktilstand, CS, cs, SCN og scn for fargerom og mønstre, sh for skyggelegginger, BDC og DP for marked-content-egenskaper, pluss /CS-oppføringen til inline-bilder. Når én ordliste deles av flere scopes, forbundes de brukte-navn-settene per kategori før noe fjernes
Bare navn bekreftet ureferert på tvers av hver scope som peker på ordlisten slippes. En scope som ikke kan parses med trygghet, blir i fred, som er den konservative retningen: en ubeskjært fil er bare større, en feil beskjært er korrupt. De overlevende ordlistene skrives tilbake som en sparsom inkrementell oppdatering som bærer de eksakte generasjonstallene, og en nåbarhet-omskrivning feier deretter objektene som ble utilgjengelige da navnene forsvant. TPdfResourcePruneReport rapporterer ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, bytetallene og et Succeeded-flagg. SaveAsRedacted kjører dette trinnet automatisk på den sanitiserte utdataen, så redigeringsstien inkluderer det allerede, men funksjonen er eksportert på strøm-nivå for pipelines som vil ha den på egen hånd
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 forblir False: en inkrementell omskriving ville
// ugyldiggjøre byte-områdene en signatur dekker
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;
Koble det inn i en dokumentpipeline
Redigeringsstien muterer aldri dokumentet du lastet inn. SaveAsRedacted fanger et isolert øyeblikksbilde, anvender /Redact-annoteringene der, striper vedlegg, kjører saniteringspasset som fjerner open action, kataloghandlinger, navnetrær, tilknyttede filer, AcroForm-en og metadataene, beskjærer ressurser, og skriver først da utdatastrømmen. Å gjenåpne den utdataen som et uavhengig dokument og trekke ut teksten på nytt, er verifikasjonstrinnet verdt å beholde i din egen testpakke, fordi det er den eneste sjekken som svarer på det opprinnelige spørsmålet — kan en leser fortsatt få strengen. En konsekvens å planlegge for: splitting erstatter sideobjekter, så ethvert FPDF_PAGEOBJECT-håndtak du holdt, er dødt etterpå, den samme levetid-fellen beskrevet i foreldede sideobjekt-håndtak etter en transform
To nabostykker gjør arbeidsflyten komplett. Å bestemme hvor redigeringsrektanglene går begynner vanligvis fra ekstrahert geometri, og blokk- og leserekkefølge-modellen i strukturerte tekstblokker og leserekkefølge er en bedre kilde til kandidatbokser enn rå tegnrekker. Å servere resultatet til en korrekturleser hører til herdningsreglene i bygge en sikker PDF-forhåndsvisning, der utfylling av skjema og JavaScript forblir av som standard. Sammen dekker de løkken de fleste compliance-arbeidsflyter trenger: lokalisere, redigere på operatornivå, verifisere ved gjenåpning, forhåndsvise trygt. Den fullstendige API-flaten, prøvenedlastingen og lisensvilkårene for komponenten bor på PDFium Delphi Component produktsiden