Iemand tekent een zwart vlak over een naam, vlakt niets, verstuurt het bestand, en de reviewer selecteert de rechthoek en plakt de naam in een e-mail. PDFiumPas beantwoordt dat met operator-level redactie: SaveAsRedacted verwijdert alleen de Unicode-scalars waarvan de tekenkaders een redactierechthoek raken, bouwt de overlevenden op uit het originele lettertype, de grootte, de matrix, de rendermodus en de kleur, en snijdt uitgelijnde paden en afbeeldingen bij in plaats van ze heel te laten vallen
Waarom een getekend vlak geen redactie is
Een tekenoperator die bovenop een content stream wordt gezet verbergt niets, want de teksttonende operators eronder staan nog steeds in de stream en verwijzen nog steeds naar code points. ISO 32000-1 §9.4 definieert een tekstobject als een reeks positionerings- en toonoperatoren binnen BT en ET; een gevulde rechthoek die daarna wordt getekend is gewoon een andere operator in dezelfde stream. Extractie loopt over de operators, niet over de pixels, dus de bedekte tekenreeks komt intact terug. Echte redactie moet de operand verwijderen, niet de uitvoer verhullen
De voor de hand liggende veilige implementatie is bruut: vind elk paginaobject waarvan de begrenzingskader een redactierechthoek snijdt en verwijder het hele object. Dat is wat eerdere PDFiumPas-releases deden, en het is correct maar kostbaar. Eén enkele Tj kan een hele tabelrij bevatten, dus het zwartmaken van één rekeningnummer nam de datum, de omschrijving en het bedrag mee. Een rechthoekige vulling die toevallig een tabelband over de volle breedte was, verdween over de hele pagina. Een factuurlogo verdween omdat de redactie één hoek ervan raakte. Versie 3.101.0 schuift de beslissing één niveau omlaag, van het paginaobject naar de operand
Wat verwijdert operator-level redactie eigenlijk?
PDFiumPas verwijdert Unicode-scalars, geen tekstobjecten. Tijdens SaveAsRedacted bouwt de component een mapping van teken naar paginaobject op basis van de geladen tekstpagina, leest vervolgens voor elk teken dat aan het geteste object toebehoort het tekenkader en snijdt dat kader tegen elke redactierechthoek. Tekens die een rechthoek raken worden gemarkeerd voor verwijdering; de rest wordt als overlevende gemarkeerd. Snijdt niets, dan blijft het object volledig met rust. Raakt elk teken, dan wordt het object heel verwijderd, precies zoals vroeger. Alleen het gemengde geval triggert een splitsing
Elke overlevende wordt daarna opnieuw uitgegeven als eigen tekstobject, opgebouwd uit de originele font-handle, de originele fontgrootte, de tekstmatrix per teken, de originele tekst-rendermodus en de fill- en stroke-status van het ouderobject, inclusief streepdikte, line join, line cap en dash array. Het hergebruiken van de font-handle in plaats van een nieuwe te resolven houdt de glyphs metrisch identiek, en het hergebruiken van de matrix per teken houdt kerning en woordafstand op zijn plek zonder layout opnieuw te draaien. De prijs zit in het objectaantal: één behouden teken wordt één tekstobject, en daarom bestaat TPdfRedactionOptions.MaxSplitObjects als hard plafond op gegenereerde fragmenten
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // het bestand bevat al /Redact-annotaties
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // operator-level splitsing (de standaard)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // plafond op gegenereerde fragmenten
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // fail closed, niet versturen
finally
Pdf.Free;
end;
end;
Rechthoeken worden bijgesneden, gedraaide geometrie niet
Paden worden alleen gesplitst als PDFiumPas kan aantonen dat het pad een uitgelijnde rechthoek is. Het bewijs is opzettelijk smal: de objectmatrix moet beide schuiftermen onder 0.0001 hebben, het pad moet bestaan uit vier tot zes segmenten die beginnen met MOVETO en alleen verder gaan met LINETO, en de getransformeerde punten moeten binnen een tolerantie van 0.01 op alle vier de hoeken van de objectgrenzen landen. Een pad dat die controle haalt wordt gereduceerd door opeenvolgende rechthoeksubtractie, waarbij elke redactierechthoek de overlevende set verdeelt in stroken links, rechts, eronder en erboven, en elke ontstane strook wordt opnieuw aangemaakt met de originele vulmodus, de stroke-vlag en de verfstatus. Krommen, driehoeken, geclipte vormen en alles wat gedraaid is zakt voor de controle en het hele object wordt verwijderd
Afbeeldingen volgen ISO 32000-1 §8.9, waar de beeldsamples het eenheidsvierkant bezetten dat via de huidige transformatiematrix wordt gemapt. PDFiumPas keert die mapping om om elk overlevend fragment in paginaruimte terug te zetten naar genormaliseerde beeldcoördinaten, klemt ze vast op het eenheidsinterval en converteert ze daarna naar pixelindices door naar binnen te ronden: de linker- en bovenrand gaan via Ceil, de rechter- en onderrand via Floor. Die richting is belangrijk. Naar buiten ronden zou een gedeeltelijke kolom bronpixels van de geredigeerde kant aan de fragmentrand laten overleven. De gehele pixelgrenzen worden daarna teruggezet naar genormaliseerde coördinaten en gebruikt om de fragmentmatrix af te leiden, zodat de bijgesneden bitmap exact op de pixelgrens landt waar hij is afgesneden. De snede zelf is een rij-voor-rij kopie die rekening houdt met stride, over de formaten Gray, BGR, BGRx en BGRA. Zoals bij paden wordt een gedraaide of gescheefde afbeelding, of één waarvan de matrix een gedegenereerde schaalterm heeft, volledig verwijderd
// Na een geslaagde SaveAsRedacted-aanroep
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
// niets kon worden gesplitst: elk snijdend object werd als geheel laten vallen
LogWholeObjectFallback(SourcePdf);
Waarom PDFiumPas fail closed gaat bij niet-gemapte tekens?
Omdat een glyph zonder reproduceerbare Unicode-scalar niet eerlijk kan worden herbouwd. Een overlevende reconstrueren betekent de tekstinstel-API aanroepen met een string, en dat vereist een stabiel code point voor elk behouden teken. Symbolische subsetfonts met kapotte of ontbrekende ToUnicode-data kunnen een lege mapping opleveren, en hercoderen op gok zou uitvoer produceren die er op het scherm correct uitziet terwijl er onder water een ander teken in zit. PDFiumPas weigert: de controle op behouden tekens gooit een exception, de exception wordt gevangen binnen SaveAsRedacted, TPdfRedactionReport.Succeeded komt terug als False met de melding in ErrorMessage, en de functie geeft False terug. Dezelfde regel geldt voor het splitbudget, dat een exception gooit in plaats van de fragmentset geruisloos af te kappen. Heeft een document fonts waar u niet op vertrouwt en wilt u het deterministische oude gedrag, zet dan Options.PreservePartialObjects := False en elk snijdend object verdwijnt heel
Resource-snoei over gedeelde scopes
Objecten splitsen laat wezen achter, en die snoeien is niet zo eenvoudig als het diffen van de /Resources-dictionary op paginaniveau. ISO 32000-1 §7.8.3 laat dezelfde resource-dictionary tegelijk worden gerefereerd door meerdere pagina's, Form XObjects, patterns en annotation appearance streams. Een fontnaam verwijderen omdat één pagina ermee stopt breekt een andere pagina die er nog gebruik van maakt. PruneUnusedPdfResources werkt daarom per scope: het resolvet /Contents of het nu een directe array is, een indirecte referentie naar een array of een enkele stream, en verzamelt daarna resourcegebruik uit de operators die resources daadwerkelijk benoemen — Tf voor fonts, Do voor XObjects, gs voor graphics state, CS, cs, SCN en scn voor kleurruimten en patterns, sh voor shadings, BDC en DP voor marked-content properties, plus de /CS-entry van inline images. Wordt één dictionary door meerdere scopes gedeeld, dan worden de gebruikte-naamsets per categorie verenigd voordat er iets wordt verwijderd
Alleen namen waarvan is bevestigd dat geen enkele scope die naar de dictionary wijst ze nog refereert, vallen. Een scope die niet met zekerheid kan worden geparsed blijft onaangeroerd, en dat is de conservatieve richting: een ongesnoeid bestand is alleen groter, een fout gesnoeid bestand is corrupt. De overlevende dictionaries worden teruggeschreven als een sparse incrementele update met de exacte generation numbers, en een reachability-rewrite veegt daarna de objecten weg die onbereikbaar werden zodra de namen verdwenen. TPdfResourcePruneReport rapporteert ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, de byteaantallen en een Succeeded-vlag. SaveAsRedacted draait deze stap automatisch op de gesanificeerde uitvoer, dus het redactiepad bevat hem al, maar de functie is geëxporteerd op streamniveau voor pipelines die hem los willen
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 blijft False: een incrementele rewrite zou
// de byte ranges die een handtekening dekt ongeldig maken
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;
Inbedden in een documentpipeline
Het redactiepad muteert nooit het document dat u hebt geladen. SaveAsRedacted legt een geïsoleerde snapshot vast, past daar de /Redact-annotaties toe, stript bijlagen, draait de sanitatiepas die de open action, catalog actions, name trees, associated files, de AcroForm en de metadata verwijdert, snoeit resources en schrijft pas daarna de uitvoerstream. Die uitvoer opnieuw openen als onafhankelijk document en de tekst opnieuw extraheren is de verificatiestap die u in uw eigen testsuite wilt houden, want het is de enige controle die de oorspronkelijke vraag beantwoordt — kan een lezer de tekenreeks nog bemachtigen. Eén consequentie om rekening mee te houden: splitsen vervangt paginaobjecten, dus elke FPDF_PAGEOBJECT-handle die u vasthield is daarna dood, dezelfde lifetime-valkuil als beschreven in verouderde paginaobject-handles na een transformatie
Twee naburige stukken maken de workflow compleet. Bepalen waar de redactierechthoeken komen begint meestal bij geëxtraheerde geometrie, en het blok- en leesvolgtemodel in gestructureerde tekstblokken en leesvolgorde is een betere bron van kandidaatkaders dan rauwe tekenreeksen. Het resultaat aan een reviewer tonen hoort bij de hardening-regels in een veilige PDF-preview bouwen, waar formulierinvullen en JavaScript standaard uit blijven. Samen dekken ze de lus af die de meeste compliance-workflows nodig hebben: lokaliseren, redigeren op operatorniveau, verifiëren door opnieuw te openen, veilig previewen. De volledige API-oppervlakte, de proefdownload en de licentievoorwaarden voor de component staan op de productpagina van PDFium Delphi Component