Någon målar en svart ruta över ett namn, plattar inte ut något, levererar filen, och granskaren markerar rektangeln och klistrar in namnet i ett e-brev. PDFiumPas svarar på det med maskering på operatornivå: SaveAsRedacted tar bort endast de Unicode-skalärer vars teckenrutor rör en maskeringsrektangel, bygger om de överlevande från det ursprungliga typsnittet, storleken, matrisen, renderingsläget och färgen, och beskär axelriktade banor och bilder istället för att släppa dem helt
Varför en målad rektangel inte är en maskering
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 positionerings- och visningsoperatorer inuti BT och ET; en fylld rektangel ritad efteråt är helt enkelt ännu en operator i samma ström. Extraktionen går igenom operatorerna, inte bildpunkterna, så den täckta strängen kommer tillbaka intakt. Verklig maskering måste ta bort operanden, inte skymma utdatan
Den uppenbara säkra implementationen är brutal: hitta varje sidobjekt vars omslutande ruta skär en maskeringsrektangel och ta bort hela objektet. Det är vad tidigare PDFiumPas-utgåvor gjorde, och det är korrekt men dyrt. Ett enda Tj kan bära en hel tabellrad, så att svärta ut ett kontonummer tog datumet, beskrivningen och beloppet med sig. En rektangulär fyllning som råkade vara ett tabellband i full bredd försvann tvärs över hela sidan. En fakturalogotyp försvann eftersom maskeringen klippte ett av dess hörn. Version 3.101.0 flyttar beslutet en nivå ned, från sidobjektet till operanden
Vad tar maskering på operatornivå faktiskt bort?
PDFiumPas tar bort Unicode-skalärer, inte textobjekt. Under SaveAsRedacted bygger komponenten en mappning från tecken till sidobjekt ur den inlästa textsidan, och för varje tecken som ägs av objektet under prövning läser den teckenrutan och skär den rutan mot varje maskeringsrektangel. Tecken som rör en rektangel märks för borttagning; resten märks som överlevande. Om ingenting skär lämnas objektet helt i fred. Om varje tecken skär tas objektet bort helt, precis som förut. Bara det blandade fallet utlöser en uppdelning
Varje överlevande avges sedan som ett eget textobjekt byggt av det ursprungliga typsnittshandtaget, den ursprungliga teckenstorleken, textmatrisen per tecken, det ursprungliga textrenderingsläget, samt föräldraobjektets fyll- och strecktillstånd inklusive streckbredd, linjefog, linjeände och streckmönster. Att återanvända typsnittshandtaget istället för att lösa upp ett nytt är vad som håller glyferna metriskt identiska, och att återanvända matrisen per tecken är vad som håller kerning och ordmellanrum på plats utan att layouten körs om. Kostnaden är objektantalet: ett behållet tecken blir ett textobjekt, vilket är varför TPdfRedactionOptions.MaxSplitObjects finns som ett hårt tak på 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; // uppdelning på operatornivå (standard)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // tak på genererade fragment
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // fall stängt, leverera inte
finally
Pdf.Free;
end;
end;
Rektanglar beskärs, roterad geometri gör det inte
Banor delas bara när PDFiumPas kan bevisa att banan är en axelriktad rektangel. Beviset är medvetet 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 ett MOVETO och fortsätter med enbart LINETO, och de transformerade punkterna måste landa på alla fyra hörnen av objektets gränser inom en tolerans på 0,01. En bana som klarar den kontrollen reduceras genom successiv rektangelsubtraktion, där varje maskeringsrektangel skär ut de överlevande i remsor till vänster, till höger, under och över, och varje resulterande remsa återskapas med det ursprungliga fyllningsläget, streckflaggan och måltillståndet. Kurvor, trianglar, klippta former och allt roterat klarar inte 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 förvandla varje överlevande fragment i sidrymd tillbaka till normaliserade bildkoordinater, klämmer dem till enhetsintervallet, och konverterar sedan till bildpunktsindex genom att avrunda inåt: vänster och övre kanten går genom Ceil, höger och nedre genom Floor. Den riktningen spelar roll. Att avrunda utåt skulle låta en delvis kolumn av källbildpunkter från den maskerade sidan överleva vid fragmentkanten. De heltaliga bildpunktsgränserna konverteras sedan tillbaka till normaliserade koordinater och används för att härleda fragmentmatrisen, så att den beskurna bitmappen landar exakt på den bildpunktsgräns den skars vid. Själva beskärningen är en radkopiering med hänsyn till radbredden över formaten Gray, BGR, BGRx och BGRA. Liksom med banor tas en roterad eller skjuvad bild, eller en vars matris har en degenererad skalterm, bort i sin helhet
// Efter ett lyckat anrop till SaveAsRedacted
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 faller PDFiumPas stängt vid omappade tecken?
Eftersom en glyf som inte har någon reproducerbar Unicode-skalär inte kan byggas om ärligt. Att rekonstruera en överlevande innebär att anropa API:et för textsättning med en sträng, och det kräver en stabil kodpunkt för varje behållet tecken. Symboliska delmängdstypsnitt med trasig eller frånvarande ToUnicode-data kan ge en tom mappning, och omkodning på gissning skulle framställa utdata som ser korrekt ut på skärmen medan den bär ett annat tecken under. PDFiumPas vägrar: kontrollen av behållna tecken utlöser ett undantag, undantaget fångas inuti SaveAsRedacted, TPdfRedactionReport.Succeeded kommer tillbaka som False med meddelandet i ErrorMessage, och funktionen returnerar False. Samma regel gäller uppdelningsbudgeten, som utlöser ett undantag snarare än att tyst hugga av 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 räckvidder
Att dela objekt lämnar föräldralösa resurser efter sig, och att rensa dem är inte så enkelt som att jämföra sidans /Resources-ordbok. ISO 32000-1 §7.8.3 låter samma resursordbok refereras av flera sidor, av Form-XObject, av mönster och av annoteringars utseendeströmmar på en gång. Att ta bort ett typsnittsnamn för att en sida slutat använda det bryter en annan sida som fortfarande gör det. PruneUnusedPdfResources arbetar därför per räckvidd: den löser upp /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 XObject, 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 egenskaper i markerat innehåll, plus /CS-posten i inline-bilder. När en ordbok delas av flera räckvidder unioneras mängderna av använda namn per kategori innan något tas bort
Endast namn som bekräftats orefererade över varje räckvidd som pekar på ordboken släpps. En räckvidd som inte kan tolkas med säkerhet lämnas orörd, vilket är den försiktiga riktningen: en orensad fil är bara större, en felaktigt rensad är korrupt. De överlevande ordböckerna skrivs tillbaka som en gles inkrementell uppdatering som bär de exakta generationsnumren, och en omskrivning för nåbarhet sopar sedan undan de objekt som blev onåbara när namnen försvann. TPdfResourcePruneReport rapporterar ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, byteantalen och en Succeeded-flagga. SaveAsRedacted kör det här steget automatiskt på den sanerade utdatan, så maskeringsvägen inkluderar det redan, men funktionen är exporterad på strömnivå för pipelines 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
Maskeringsvägen muterar aldrig dokumentet du läste in. SaveAsRedacted fångar en isolerad ögonblicksbild, tillämpar /Redact-annoteringarna där, skalar bort bilagor, kör det saneringspass som tar bort öppningsåtgärden, katalogåtgärderna, namnträden, associerade filer, AcroForm och metadatan, rensar resurser, och skriver först då utdataströmmen. Att öppna den utdatan igen som ett självständigt dokument och extrahera texten på nytt är det verifieringssteg som är värt att behålla i din egen testsvit, eftersom det är den enda kontroll som besvarar den ursprungliga frågan — kan en läsare fortfarande komma åt strängen. En följd att planera för: uppdelning ersätter sidobjekt, så varje FPDF_PAGEOBJECT-handtag du höll är dött efteråt, samma livslängdsfälla som beskrivs i inaktuella sidobjekthandtag efter en transform
Två angränsande delar gör arbetsflödet komplett. Att bestämma var maskeringsrektanglarna ska ligga börjar oftast i extraherad geometri, och block- och läsordningsmodellen i strukturerade textblock och läsordning är en bättre källa till kandidatrutor än råa teckenkörningar. Att servera resultatet till en granskare hör till härdningsreglerna i att bygga en säker PDF-förhandsvisning, där formulärifyllning och JavaScript förblir avstängda som standard. Tillsammans täcker de den slinga de flesta efterlevnadsarbetsflöden behöver: lokalisera, maskera på operatornivå, verifiera genom att öppna om, förhandsvisa säkert. Den fullständiga API-ytan, testnedladdningen och licensvillkoren för komponenten finns på produktsidan för PDFium Delphi Component