Nogen maler en sort boks over et navn, flader intet ud, afskiber filen, og anmelderen markerer rektanglet og indsætter navnet i en mail. PDFiumPas besvarer det med redigering på operatorniveau: SaveAsRedacted sletter kun de Unicode-skalarer, hvis tegnbokse rører et redigeringsrektangel, genopbygger de overlevende ud fra den originale font, størrelse, matrix, render-tilstand og farve og beskærer aksejusterede stier og billeder frem for at kassere dem hele
Hvorfor et malet rektangel ikke er en redigering
En tegneoperation tilføjet oven på en indholdsstream skjuler ingenting, fordi tekstvisningsoperatorerne under den stadig er i streamen og stadig afbilder til kodepunkter. ISO 32000-1 §9.4 definerer et tekstobjekt som en sekvens af positionerings- og visningsoperatorer inde i BT og ET; et udfyldt rektangel tegnet bagefter er blot endnu en operator i samme stream. Udtrækning går operatorerne igennem, ikke pixels, så den dækkede streng kommer tilbage intakt. Ægte redigering er nødt til at fjerne operanden, ikke sløre outputtet
Den indlysende sikre implementering er brutal: find hvert sideobjekt, hvis bounding box skærer et redigeringsrektangel, og slet hele objektet. Det var, hvad tidligere PDFiumPas-udgivelser gjorde, og det er korrekt, men dyrt. En enkelt Tj kan bære en hel tabellerække, så at sortlægge ét kontonummer tog datoen, beskrivelsen og beløbet med. En rektangulær udfyldning, der tilfældigvis var et fuldbredde-tabelbånd, forsvandt over hele siden. Et fakturalogo forsvandt, fordi redigeringen beskar et hjørne af det. Version 3.101.0 flytter beslutningen ét niveau ned, fra sideobjektet til operanden
Hvad sletter redigering på operatorniveau reelt?
PDFiumPas sletter Unicode-skalarer, ikke tekstobjekter. Under SaveAsRedacted bygger komponenten en tegn-til-sideobjekt-tilknytning fra den indlæste tekstside og læser for hvert tegn, der ejes af det testede objekt, tegnboksen og skærer den boks mod hvert redigeringsrektangel. Tegn, der rører et rektangel, markeres til fjernelse; resten markeres som overlevende. Skærer intet, efterlades objektet helt i fred. Skærer hvert tegn, fjernes objektet helt, præcis som før. Kun det blandede tilfælde udløser en opdeling
Hver overlevende genudsendes derefter som sit eget tekstobjekt bygget fra det originale font-handle, den originale fontstørrelse, tegn-for-tegn-tekstmatricen, den originale tekst-render-tilstand og forældreobjektets udfyldnings- og stregtilstand inklusive stregbredde, line join, line cap og dash-array. Genbrug af font-handlet frem for at opløse et nyt er det, der holder glyferne metrisk identiske, og genbrug af tegnmatricen er det, der holder kerning og ordmellemrum på plads uden at køre layout igen. Prisen er objektantal: ét bevaret tegn bliver ét tekstobjekt, hvilket er derfor, TPdfRedactionOptions.MaxSplitObjects findes som et hårdt loft over genererede 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; // operatorniveau-opdeling (standarden)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // loft over genererede fragmenter
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // fail closed, afskib ikke
finally
Pdf.Free;
end;
end;
Rektangler beskæres, roteret geometri gør ikke
Stier opdeles kun, når PDFiumPas kan bevise, at stien er et aksejusteret rektangel. Beviset er bevidst smalt: objektmatricen skal have begge forskydningsled under 0,0001, stien skal bestå af fire til seks segmenter, der begynder med en MOVETO og fortsætter med kun LINETO, og de transformerede punkter skal lande på alle fire hjørner af objektgrænserne inden for en tolerance på 0,01. En sti, der består den kontrol, reduceres ved succesiv rektangelsubtraktion, hvert redigeringsrektangel udskærer overlevende-sættet i venstre, højre, under og over striber, og hver resulterende stibe genoprettes med den originale fyldtilstand, stregflag og malestilstand. Kurver, trekanter, klippede former og alt roteret består ikke kontrollen, og hele objektet fjernes
Billeder følger ISO 32000-1 §8.9, hvor billedprøverne optager enhedskvadratet afbildet gennem den aktuelle transformationsmatrix. PDFiumPas inverterer den tilknytning for at vende hvert overlevende siderumsfragment tilbage til normaliserede billedekoordinater, klemmer dem til enhedsintervallet og konverterer derefter til pixelindeks ved at runde indad: venstre og øverste kanter går gennem Ceil, højre og nederste gennem Floor. Den retning tæller. At runde udad ville lade en delvis kolonne af kildepixels fra den redigerede side overleve ved fragmentkanten. Heltalspixelgrænserne konverteres derefter tilbage til normaliserede koordinater og bruges til at udlede fragmentmatricen, så den beskårne bitmap lander præcis på den pixelgrænse, den blev skåret ved. Beskæringen selv er en stride-bevidst rækkekopi på tværs af formaterne Gray, BGR, BGRx og BGRA. Som med stier fjernes et roteret eller skævt billede, eller et, hvis matrix har et degenereret skalaled, i sin helhed
// Efter et vellykket SaveAsRedacted-kald
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
// intet kunne opdeles: hvert skærende objekt blev kastet hele
LogWholeObjectFallback(SourcePdf);
Hvorfor fejler PDFiumPas lukket på tegn uden tilknytning?
Fordi en glyf uden reproduserbar Unicode-scalar ikke kan genopbygges ærligt. At rekonstruere en overlevende betyder at kalde det tekstindstillende API med en streng, og det kræver et stabilt kodepunkt for hvert bevaret tegn. Symboliske subset-fonts med ødelagte eller fraværende ToUnicode-data kan give en tom tilknytning, og omkodning efter gæt ville producere output, der ser korrekt ud på skærmen, mens det bærer et andet tegn nedenunder. PDFiumPas nægter: den bevarede-tegn-kontrol rejser, exception fanges inde i SaveAsRedacted, TPdfRedactionReport.Succeeded kommer tilbage som False med beskeden i ErrorMessage, og funktionen returnerer False. Samme regel gælder opdelingsbudgettet, som rejser i stedet for stille at afkorte fragmentsættet. Når et dokument har fonts, du ikke stoler på, og du vil have den deterministiske gamle adfærd, sæt Options.PreservePartialObjects := False, og hvert skærende objekt forsvinder helt
Ressourcebeskæring på tværs af delte scopes
At opdele objekter efterlader forældreløse, og at beskære dem er ikke så enkelt som at diff-e ordbogen /Resources på sideniveau. ISO 32000-1 §7.8.3 lader samme ressourceordbog refereres af flere sider, af Form XObjects, af mønstre og af annoteringens appearance-streams på én gang. At slette et fontnavn, fordi én side holdt op med at bruge det, vil ødelægge en anden side, der stadig gør. PruneUnusedPdfResources arbejder derfor pr. scope: den opløser /Contents, uanset om det er et direkte array, en indirekte reference til et array eller en enkelt stream, og samler derefter ressourceforbrug fra de operatorer, der reelt navngiver ressourcer — Tf for fonts, Do for XObjects, gs for grafiktilstand, CS, cs, SCN og scn for farverum og mønstre, sh for shadings, BDC og DP for marked-content-egenskaber plus /CS-posten af inline-billeder. Når én ordbog deles af flere scopes, forenes de brugte-navne-sæt pr. kategori, før noget fjernes
Kun navne, der er bekræftet ureferencede på tværs af hvert scope, der peger på ordbogen, droppes. Et scope, der ikke kan parses med sikkerhed, efterlades urørt, hvilket er den konservative retning: en ubeskåret fil er blot større, en forkert beskåret er korrupt. De overlevende ordbøger skrives tilbage som en sparsom inkrementel opdatering med de nøjagtige generationsnumre, og en reachability-omskrivning svaber derefter de objekter, der blev utilgængelige, da navnene forsvandt. TPdfResourcePruneReport rapporterer ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, byteantallene og et Succeeded-flag. SaveAsRedacted kører dette trin automatisk på det rensede output, så redigeringsstien allerede inkluderer det, men funktionen eksporteres på streamniveau for pipelines, der vil have den for sig selv
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 forbliver False: en inkrementel omskrivning ville
// invalidere de byteintervaller, en signatur dækker
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;
At koble det ind i en dokumentpipeline
Redigeringsstien muterer aldrig dokumentet, du indlæste. SaveAsRedacted fanger et isoleret snapshot, anvender /Redact-annoteringerne der, fjerner vedhæftninger, kører det rensende trin, der fjerner den åbne handling, kataloghandlinger, navnetræer, tilknyttede filer, AcroForm og metadata, beskærer ressourcer og skriver først derefter outputstream. At genåbne det output som et uafhængigt dokument og udtrække teksten igen er verifikationstrinnet, der er værd at beholde i din egen testpakke, fordi det er den eneste kontrol, der besvarer det oprindelige spørgsmål — kan en læser stadig få strengen. En konsekvens at planlægge for: opdeling erstatter sideobjekter, så ethvert FPDF_PAGEOBJECT-handle, du holdt, er dødt bagefter, samme levetidsgyde som beskrevet i forældede sideobjekt-handles efter en transform
To nabobidrag gør arbejdsgangen komplet. At beslutte hvor redigeringsrektanglerne placeres begynder som regel fra udtrukket geometri, og blok- og læserækkefølge-modellen i strukturerede tekstblokke og læserækkefølge er en bedre kilde til kandidatbokse end rå tegnkørsler. At servere resultatet til en anmelder hører til hærkningsreglerne i at bygge en sikker PDF-forhåndsvisning, hvor udfyldning af formularer og JavaScript holder sig slukket som standard. Sammen dækker de den løkke, de fleste compliance-arbejdsgange har brug for: lokalisér, redigér på operatorniveau, verificér ved genåbning, forhåndsvis sikkert. Den fulde API-overflade, prøvedownloaden og licensvilkårene for komponenten ligger på PDFium Delphi Component-produktsiden