Cineva pictează un pătrat negru peste un nume, nu aplatizează nimic, expediază fișierul, iar reviewer-ul selectează dreptunghiul și lipește numele într-un email. PDFiumPas răspunde la asta cu redactare la nivel de operator: SaveAsRedacted șterge doar scalarii Unicode ale căror casete de caracter ating un dreptunghi de redactare, reconstruiește supraviețuitorii din fontul, dimensiunea, matricea, modul de randare și culoarea originale, și decupează traseele și imaginile aliniate la axă în loc să le arunce întregi
De ce un dreptunghi pictat nu e o redactare
O operație de desen adăugată peste un content stream nu ascunde nimic, pentru că operatorii de afișare a textului de sub ea sunt încă în stream și încă mapează către code points. ISO 32000-1 §9.4 definește un text object ca o secvență de operatori de poziționare și afișare în interiorul BT și ET; un dreptunghi umplut desenat după e pur și simplu un alt operator în același stream. Extragerea parcurge operatorii, nu pixelii, deci șirul acoperit se întoarce intact. Redactarea adevărată trebuie să elimine operandul, nu să ascundă ieșirea
Implementarea sigură evidentă e brutală: găsește fiecare page object a cărui bounding box intersectează un dreptunghi de redactare și șterge obiectul întreg. Asta făceau versiunile mai vechi de PDFiumPas, corect dar scump. Un singur Tj poate purta un rând întreg de tabel, deci mascarea unui număr de cont lua cu el data, descrierea și suma. O umplutură dreptunghiulară care se întâmpla să fie o bandă de tabel pe toată lățimea dispărea pe toată pagina. Logo-ul unei facturi dispărea pentru că redactarea tăia un colț al lui. Versiunea 3.101.0 mută decizia un nivel mai jos, de la page object la operand
Ce șterge de fapt redactarea la nivel de operator?
PDFiumPas șterge scalari Unicode, nu text objects. În timpul SaveAsRedacted componenta construiește o mapare caracter-page object din pagina de text încărcată, apoi pentru fiecare caracter deținut de obiectul testat citește caseta caracterului și intersectează caseta cu fiecare dreptunghi de redactare. Caracterele care ating un dreptunghi sunt marcate pentru ștergere; restul sunt marcate ca supraviețuitori. Dacă nimic nu intersectează, obiectul e lăsat complet în pace. Dacă fiecare caracter intersectează, obiectul e șters întreg, exact ca înainte. Doar cazul mixt declanșează o scindare
Fiecare supraviețuitor e apoi reemis ca propriul text object construit din font handle-ul original, dimensiunea originală a fontului, matricea de text per caracter, modul de randare a textului original, și starea de umplere și contur a obiectului părinte inclusiv lățimea conturului, line join, line cap și dash array. Reutilizarea font handle-ului în loc să rezolvați unul nou e ce păstrează glifele metric identice, iar reutilizarea matricei per caracter e ce păstrează kerning-ul și spațierea cuvintelor la loc fără a rerula layout-ul. Prețul e numărul de obiecte: un caracter păstrat devine un text object, de aceea TPdfRedactionOptions.MaxSplitObjects există ca plafon dur al fragmentelor generate
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // fișierul poartă deja adnotările /Redact
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // scindare la nivel de operator (implicit)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // plafon pentru fragmentele generate
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // fail closed, nu expedia
finally
Pdf.Free;
end;
end;
Dreptunghiurile se decupează, geometria rotită nu
Traseele sunt scindate doar când PDFiumPas poate demonstra că traseul e un dreptunghi aliniat la axă. Demonstrarea e intenționat îngustă: matricea obiectului trebuie să aibă ambii termeni de shear sub 0.0001, traseul trebuie să conste din patru până la șase segmente care încep cu MOVETO și continuă doar cu LINETO, iar punctele transformate trebuie să cadă pe toate cele patru colțuri ale limitelor obiectului în interiorul unei toleranțe de 0.01. Un traseu care trece verificarea e redus prin scădere succesivă de dreptunghiuri, fiecare dreptunghi de redactare sculptând mulțimea supraviețuitorilor în benzi stânga, dreapta, dedesubt și deasupra, iar fiecare bandă rezultată e recreată cu modul de umplere, flag-ul de contur și starea de vopsea originale. Curbele, triunghiurile, formele decupate și orice e rotit pică verificarea și obiectul întreg e eliminat
Imaginile urmează ISO 32000-1 §8.9, unde mostrele imaginii ocupă pătratul unitar mapat prin matricea de transformare curentă. PDFiumPas inversează maparea asta ca să întoarcă fiecare fragment supraviețuitor din spațiul paginii în coordonate normalizate ale imaginii, le fixează în intervalul unitar, apoi convertește în indici de pixeli rotunjind spre interior: marginile stânga și sus trec prin Ceil, dreapta și jos prin Floor. Direcția contează. Rotunjirea spre exterior ar lăsa o coloană parțială de pixeli sursă de partea redactată să supraviețuiască la marginea fragmentului. Limitele întregi de pixeli sunt apoi convertite înapoi în coordonate normalizate și folosite să deriveze matricea fragmentului, deci bitmap-ul decupat aterizează exact pe frontiera de pixel la care a fost tăiat. Decuparea propriu-zisă e o copiere de rând conștientă de stride prin formatele Gray, BGR, BGRx și BGRA. Ca la trasee, o imagine rotită sau înclinată, sau una a cărei matrice are un termen de scală degenerat, e eliminată în întregime
// După un apel reușit 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
// nimic n-a putut fi scindat: fiecare obiect care intersecta a fost aruncat întreg
LogWholeObjectFallback(SourcePdf);
De ce dă PDFiumPas fail closed la caractere nemapate?
Pentru că o glifă fără scalar Unicode reproductibil nu poate fi reconstruită onest. Reconstruirea unui supraviețuitor înseamnă a apela API-ul de setare a textului cu un șir, iar asta cere un code point stabil pentru fiecare caracter păstrat. Fonturile subset simbolice cu date ToUnicode rupte sau absente pot da o mapare goală, iar recodificarea prin ghicire ar produce o ieșire care arată corect pe ecran în timp ce poartă un alt caracter dedesubt. PDFiumPas refuză: verificarea caracterelor păstrate ridică excepție, excepția e prinsă în interiorul SaveAsRedacted, TPdfRedactionReport.Succeeded se întoarce False cu mesajul în ErrorMessage, iar funcția întoarce False. Aceeași regulă se aplică bugetului de scindare, care ridică excepție în loc să trunchieze tăcut mulțimea de fragmente. Când un document are fonturi în care nu aveți încredere și vreți comportamentul vechi determinist, setați Options.PreservePartialObjects := False și fiecare obiect care intersectează dispare întreg
Tăierea resurselor între scope-uri partajate
Scindarea obiectelor lasă orfani în urmă, iar tăierea lor nu e la fel de simplă ca diferența dicționarului /Resources de nivel de pagină. ISO 32000-1 §7.8.3 lasă același dicționar de resurse să fie referențiat de mai multe pagini, de Form XObjects, de patterns și de appearance streams de adnotare deodată. Ștergerea numelui unui font pentru că o pagină a încetat să-l folosească va rupe altă pagină care încă îl folosește. PruneUnusedPdfResources funcționează prin urmare per scope: rezolvă /Contents indiferent dacă e un array direct, o referință indirectă către un array sau un stream unic, apoi colectează utilizarea resurselor din operatorii care numesc de fapt resurse — Tf pentru fonturi, Do pentru XObjects, gs pentru starea grafică, CS, cs, SCN și scn pentru spații de culoare și patterns, sh pentru shadings, BDC și DP pentru proprietăți de marked-content, plus intrarea /CS a imaginilor inline. Când un dicționar e partajat de mai multe scope-uri, mulțimile de nume folosite sunt reunite per categorie înainte ca ceva să fie eliminat
Doar numele confirmate ca nereferențiate în fiecare scope care arată către dicționar sunt eliminate. Un scope care nu poate fi parsat cu încredere e lăsat neatins, ceea ce e direcția conservatoare: un fișier netăiat e doar mai mare, unul tăiat greșit e corupt. Dicționarele supraviețuitoare sunt scrise înapoi ca un update incremental spars care poartă numerele exacte de generație, iar o rescriere de accesibilitate mătură apoi obiectele care au devenit inaccesibile odată ce numele au dispărut. TPdfResourcePruneReport raportează ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, numărătorile de bytes și un flag Succeeded. SaveAsRedacted rulează pasul acesta automat pe ieșirea sanitarizată, deci drumul de redactare îl include deja, dar funcția e exportată la nivel de stream pentru pipeline-urile care o vor separat
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 rămâne False: o rescriere incrementală
// invalidează intervalele de bytes pe care o semnătură le acoperă
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;
Integrarea într-un pipeline de documente
Drumul de redactare nu mutează niciodată documentul pe care l-ați încărcat. SaveAsRedacted capturează un snapshot izolat, aplică acolo adnotările /Redact, dezbracă atașamentele, rulează pasul de sanitizare care elimină open action-ul, acțiunile de catalog, name trees, fișierele asociate, AcroForm-ul și metadata, taie resursele, și abia apoi scrie stream-ul de ieșire. Redeschiderea ieșirii ca document independent și reextragerea textului e pasul de verificare care merită păstrat în propriul dumneavoastră test suite, pentru că e singura verificare care răspunde întrebării originale — poate un cititor să obțină încă șirul. O consecință de planificat: scindarea înlocuiește page objects, deci orice handle FPDF_PAGEOBJECT pe care îl țineați e mort după, capcana aceeași de ciclu de viață descrisă în handle-uri de page object întârziate după o transformare
Două piese învecinate fac workflow-ul complet. Decizia unde merg dreptunghiurile de redactare pornește de regulă din geometria extrasă, iar modelul de bloc și ordine de citire din blocuri de text structurat și ordine de citire e o sursă mai bună de casete candidate decât rulările brute de caractere. Servirea rezultatului unui reviewer ține de regulile de întărire din construirea unui preview PDF securizat, unde form filling și JavaScript rămân oprite implicit. Împreună acoperă bucla de care au nevoie majoritatea workflow-urilor de compliance: localizează, redactează la nivel de operator, verifică prin redeschidere, previzualizează în siguranță. Suprafața completă de API, descărcarea de probă și termenii de licențiere ai componentei stau pe pagina de produs PDFium Delphi Component