Någon målar en svart ruta över ett namn, plattar ut ingenting, skeppar filen, och granskaren markerar rektangeln och klistrar in namnet i ett mejl. PDFiumPas svarar på det med redigering på operatornivå: SaveAsRedacted tar bara bort de Unicode-skalarärer vars teckenrutor vidrör en redigeringsrektangel, bygger om de överlevande från det ursprungliga typsnittet, storleken, matrisen, renderingsläget och färgen, och beskär axelriktade banor och bilder i stället för att släppa dem hela
Varför en målad rektangel inte är en redigering
En ritoperation som lagts ovanpå en innehållsström döljer ingenting, eftersom de textvisande operatorerna under den fortfarande finns i strömmen och fortfarande mappar till kodpunkter. ISO 32000-1 §9.4 definierar ett textobjekt som en sekvens av positionerande och visande operatorer inuti BT och ET; en fylld rektangel ritad efteråt är helt enkelt en annan operator i samma ström. Extrahering går igenom operatorerna, inte pixlarna, så den täckta strängen kommer tillbaka intakt. Riktig redigering måste ta bort operanden, inte skymma utdatat
Den uppenbara säkra implementationen är brutal: hitta varje sidobjekt vars begränsningsyta skär en redigeringsrektangel och ta bort hela objektet. Det var vad tidigare PDFiumPas-versioner gjorde, och det är korrekt men dyrt. En enda Tj kan bära en hel tabellrad, så att svärta ett kontonummer tog datumet, beskrivningen och beloppet med sig. En rektangulär fyllning som råkar vara ett helbrett tabellband försvann över hela sidan. En fakturalogotyp försvann eftersom redigeringen klippte ett hörn av den. Version 3.101.0 flyttar beslutet ett nivå ner, från sidobjektet till operanden
Vad tar redigering på operatornivå egentligen bort?
PDFiumPas tar bort Unicode-skalarärer, inte textobjekt. Under SaveAsRedacted bygger komponenten en tecken-till-sidobjekt-mappning från den laddade textsidan, och läser sedan för varje tecken som ägs av objektet under test teckenrutan och skär den rutan mot varje redigeringsrektangel. Tecken som vidrör en rektangel markeras för borttagning; resten markeras som överlevare. Om ingenting skär lämnas objektet helt orört. Om varje tecken skär tas objektet bort helt, precis som förut. Bara det blandade fallet utlöser en delning
Varje överlevare återsänds sedan som ett eget textobjekt byggt från det ursprungliga typsnittshandtaget, den ursprungliga teckenstorleken, den per-tecken-textmatrisen, det ursprungliga textrenderingsläget och fyllnings- och strecktillståndet hos föräldraobjektet inklusive streckbredd, linjefogning, linjeslut och streckmatris. Att återanvända typsnittshandtaget i stället för att lösa fram ett nytt är det som håller glyferna metriskt identiska, och att återanvända den per-tecken-matrisen är det som håller kerning och ordavstånd på plats utan att köra layout igen. Kostnaden är objektantal: ett bibehållet tecken blir ett textobjekt, vilket är skälet till att TPdfRedactionOptions.MaxSplitObjects finns som ett hårt tak för genererade fragment
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // filen bär redan /Redact-annoteringar
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // delning på operatornivå (standard)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // tak för genererade fragment
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // stäng vid fel, skeppa inte
finally
Pdf.Free;
end;
end;
Rektanglar beskär, roterad geometri gör det inte
Banor delas bara när PDFiumPas kan bevisa att banan är en axelriktad rektangel. Beviset är avsiktligt snävt: objektmatrisen måste ha båda skjuvtermerna under 0,0001, banan måste bestå av fyra till sex segment som börjar med en MOVETO och fortsätter med enbart LINETO, och de transformerade punkterna måste landa på alla fyra hörnen av objektbegränsningen inom en tolerans på 0,01. En bana som klarar den kontrollen reduceras genom successiv rektangelsubtraktion, där varje redigeringsrektangel hugger överlevarmängden i remsor till vänster, höger, under och ovanför, och varje resulterande remsa återskapas med det ursprungliga fyllningsläget, streckflaggan och målningstillståndet. Kurvor, trianglar, klippta former och allt som är roterat faller på kontrollen och hela objektet tas bort
Bilder följer ISO 32000-1 §8.9, där bildsamplen upptar enhetskvadraten mappad genom den aktuella transformationsmatrisen. PDFiumPas inverterar den mappningen för att vända varje överlevande sidrumsfragment tillbaka till normaliserade bildkoordinater, klämmer fast dem till enhetsintervallet och konverterar sedan till pixelindex genom att avrunda inåt: vänster- och överkanterna går genom Ceil, höger- och underkanterna genom Floor. Den riktningen spelar roll. Avrundning utåt skulle låta en partiell kolumn av källpixlar från den redigerade sidan överleva vid fragmentkanten. De heltalsgrundade pixelgränserna konverteras sedan tillbaka till normaliserade koordinater och används för att härleda fragmentmatrisen, så den beskurna bitmappen landar exakt på den pixelgräns den klipptes vid. Själva beskärningen är en stegdel-medveten radkopiering över formaten Gray, BGR, BGRx och BGRA. Liksom med banor tas en roterad eller snedställd bild, eller en vars matris har ett degenererat skaltermer, bort i sin helhet
// Efter ett lyckat SaveAsRedacted-anrop
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
// inget kunde delas: varje skärande objekt släpptes helt
LogWholeObjectFallback(SourcePdf);
Varför stänger PDFiumPas vid fel för omappade tecken?
Eftersom en glyf som saknar reproducerbar Unicode-skalarär inte kan byggas om ärligt. Att rekonstruera en överlevare innebär att anropa textsättnings-API:t med en sträng, och det kräver en stabil kodpunkt för varje bibehållet tecken. Symboliska delmängdstypsnitt med trasiga eller frånvarande ToUnicode-data kan ge en tom mappning, och omkodning på gissningar skulle ge utdata som ser korrekta ut på skärm medan de bär ett annat tecken under. PDFiumPas vägrar: bibehållen-tecken-kontrollen kastar, undantaget fångas inuti SaveAsRedacted, TPdfRedactionReport.Succeeded kommer tillbaka False med meddelandet i ErrorMessage, och funktionen returnerar False. Samma regel gäller för delningsbudgeten, som kastar i stället för att tyst trunkera fragmentmängden. När ett dokument har typsnitt du inte litar på och du vill ha det deterministiska gamla beteendet, sätt Options.PreservePartialObjects := False så försvinner varje skärande objekt helt
Resursrensning över delade scope
Att dela objekt lämnar föräldralösa kvar, och att rensa dem är inte så enkelt som att diffa den sidnivåbaserade /Resources-ordlistan. ISO 32000-1 §7.8.3 låter samma resursordlista refereras av flera sidor, av Form-XObjects, av mönster och av annoteringsutseendeströmmar på en gång. Att ta bort ett typsnittsnamn för att en sida slutat använda det kommer att bryta en annan sida som fortfarande gör det. PruneUnusedPdfResources arbetar därför per scope: den löser /Contents vare sig det är en direkt array, en indirekt referens till en array eller en enda ström, och samlar sedan resursanvändning från de operatorer som faktiskt namnger resurser — Tf för typsnitt, Do för XObjects, gs för grafiktillstånd, CS, cs, SCN och scn för färgrymder och mönster, sh för skuggningar, BDC och DP för märkt-innehåll-egenskaper, plus /CS-posten hos inlinerade bilder. När en ordlista delas av flera scope unioneras de använda-namn-mängderna per kategori innan någonting tas bort
Bara namn som bekräftats orefererade över varje scope som pekar på ordlistan släpps. Ett scope som inte kan parsas med tillförsikt lämnas orört, vilket är den konservativa riktningen: en orensad fil är bara större, en felrensad är korrupt. De överlevande ordlistorna skrivs tillbaka som en gles inkrementell uppdatering med de exakta generationsnumren, och en nåbarhetsomskrivning sopar sedan bort de objekt som blivit onåbara när namnen försvann. TPdfResourcePruneReport rapporterar ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, bytetalen och en Succeeded-flagga. SaveAsRedacted kör detta steg automatiskt på det sanerade utdatat, så redigeringsvägen redan inkluderar det, men funktionen exporteras på strömnivå för pipeliner som vill ha den för sig
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 förblir False: en inkrementell omskrivning skulle
// ogiltigförklara de byteintervall en signatur täcker
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;
Att koppla in det i en dokumentpipeline
Redigeringsvägen muterar aldrig dokumentet du laddade. SaveAsRedacted fångar en isolerad ögonblicksbild, tillämpar /Redact-annoteringarna där, river bort bilagor, kör saneringspasset som tar bort öppningsaktionen, katalogaktioner, namnträd, associerade filer, AcroForm och metadata, rensar resurser, och skriver först då utdataströmmen. Att öppna det utdatat på nytt som ett oberoende dokument och extrahera texten igen är det verifieringssteg värt att behålla i din egen testsvit, eftersom det är den enda kontroll som svarar på den ursprungliga frågan — kan en läsare fortfarande få tag på strängen. En konsekvens att planera för: delning ersätter sidobjekt, så varje FPDF_PAGEOBJECT-handtag du höll i är dött efteråt, samma livstidsfälla som beskrivs i inaktuella sidobjektshandtag efter en transform
Två angränsande delar gör arbetsflödet komplett. Att bestämma var redigeringsrektanglarna går börjar vanligtvis från extraherad geometri, och block- och läsordningsmodellen i strukturerade textblock och läsordning är en bättre källa till kandidatrutor än råa teckenlöp. Att servera resultatet till en granskare hör till härdningsreglerna i att bygga en säker PDF-förhandsvisning, där formulärfyllning och JavaScript förblir avstängda som standard. Tillsammans täcker de den loop som de flesta compliance-arbetsflöden behöver: lokalisera, redigera på operatornivå, verifiera genom att öppna på nytt, förhandsvisa säkert. Hela API-ytan, testnedladdningen och licensvillkoren för komponenten finns på PDFium Delphi Component-produktsidan