Quelqu’un peint une boîte noire sur un nom, n’aplatis rien, livre le fichier, et le relecteur sélectionne le rectangle et colle le nom dans un courriel. PDFiumPas répond à cela avec le caviardage au niveau des opérateurs : SaveAsRedacted supprime uniquement les scalaires Unicode dont les boîtes de caractères touchent un rectangle de caviardage, reconstruit les survivants depuis la police, la taille, la matrice, le mode de rendu et la couleur d’origine, et rogne les chemins rectangulaires et les images alignées aux axes au lieu de les laisser tomber en entier
Pourquoi un rectangle peint n’est pas un caviardage
Une opération de dessin ajoutée par-dessus un flux de contenu ne cache rien, parce que les opérateurs d’affichage de texte au-dessous d’elle sont toujours dans le flux et continuent de correspondre à des points de code. L’ISO 32000-1 §9.4 définit un objet texte comme une séquence d’opérateurs de positionnement et d’affichage à l’intérieur de BT et ET ; un rectangle rempli dessiné ensuite n’est simplement qu’un autre opérateur du même flux. L’extraction parcourt les opérateurs, pas les pixels, donc la chaîne couverte revient intacte. Un vrai caviardage doit retirer l’opérande, pas obscurcir la sortie
L’implémentation sûre évidente est brutale : trouver chaque objet page dont la boîte englobante intersecte un rectangle de caviardage et supprimer l’objet entier. C’est ce que faisaient les versions antérieures de PDFiumPas, et c’est correct mais coûteux. Un seul Tj peut porter une rangée de tableau entière, si bien que masquer un numéro de compte emportait la date, la description et le montant avec lui. Un remplissage rectangulaire qui se trouve être une bande de tableau pleine largeur disparaissait sur toute la page. Un logo de facture disparaissait parce que le caviardage en coupait un coin. La version 3.101.0 descend la décision d’un niveau, de l’objet page vers l’opérande
Que supprime réellement le caviardage au niveau des opérateurs ?
PDFiumPas supprime des scalaires Unicode, pas des objets texte. Pendant SaveAsRedacted le composant construit une correspondance caractère vers objet page depuis la page texte chargée, puis pour chaque caractère possédé par l’objet en test il lit la boîte de caractère et intersecte cette boîte contre chaque rectangle de caviardage. Les caractères qui touchent un rectangle sont marqués pour suppression ; les autres sont marqués comme survivants. Si rien n’intersecte, l’objet est laissé complètement tranquille. Si chaque caractère intersecte, l’objet est supprimé en entier, exactement comme avant. Seul le cas mixte déclenche une scission
Chaque survivant est ensuite réémis comme son propre objet texte construit depuis le handle de police d’origine, la taille de police d’origine, la matrice de texte par caractère, le mode de rendu de texte d’origine, et l’état de remplissage et de trait de l’objet parent y compris largeur de trait, jointure de ligne, bout de ligne et tableau de tirets. Réutiliser le handle de police plutôt que résoudre une nouvelle est ce qui garde les glyphes métriquement identiques, et réutiliser la matrice par caractère est ce qui garde le crénage et l’espacement des mots en place sans relancer la mise en page. Le coût est le nombre d’objets : un caractère retenu devient un objet texte, voilà pourquoi TPdfRedactionOptions.MaxSplitObjects existe comme plafond strict des fragments générés
procedure RedactDocument(const SourcePdf, TargetPdf: string);
var
Pdf: TPdf;
Options: TPdfRedactionOptions;
Report: TPdfRedactionReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := SourcePdf; // the file already carries /Redact annotations
Pdf.Active := True;
Options := TPdfRedactionOptions.Default;
Options.PreservePartialObjects := True; // operator-level split (the default)
Options.RemoveIntersectingAnnotations := True;
Options.MaxSplitObjects := 20000; // ceiling on generated fragments
if not Pdf.SaveAsRedacted(TargetPdf, Options, Report) then
raise Exception.Create(Report.ErrorMessage); // fail closed, do not ship
finally
Pdf.Free;
end;
end;
Les rectangles rognent, la géométrie tournée non
Les chemins ne sont scindés que lorsque PDFiumPas peut prouver que le chemin est un rectangle aligné aux axes. La preuve est délibérément étroite : la matrice d’objet doit avoir les deux termes de cisaillement au-dessous de 0.0001, le chemin doit se composer de quatre à six segments commençant par un MOVETO et continuant avec LINETO seulement, et les points transformés doivent atterrir sur les quatre coins des bornes de l’objet dans une tolérance de 0.01. Un chemin qui franchit cette vérification est réduit par soustraction rectangulaire successive, chaque rectangle de caviardage découpant l’ensemble survivant en bandes gauche, droite, inférieure et supérieure, et chaque bande résultante est recréée avec le mode de remplissage, le drapeau de trait et l’état de peinture d’origine. Courbes, triangles, formes écrêtées et tout ce qui est tourné échouent à la vérification et l’objet entier est supprimé
Les images suivent l’ISO 32000-1 §8.9, où les échantillons d’image occupent le carré unité mappé à travers la matrice de transformation courante. PDFiumPas inverse ce mappage pour ramener chaque fragment survivant en espace page vers des coordonnées d’image normalisées, les borne à l’intervalle unité, puis convertit en indices de pixels en arrondissant vers l’intérieur : les bords gauche et haut passent par Ceil, les bords droit et bas par Floor. Cette direction compte. Arrondir vers l’extérieur laisserait une colonne partielle de pixels source du côté caviardé survivre au bord du fragment. Les bornes entières de pixels sont ensuite reconverties en coordonnées normalisées et servent à dériver la matrice du fragment, si bien que le bitmap rogné atterrit exactement sur la frontière de pixel où il a été coupé. Le rognage lui-même est une copie de lignes consciente du stride à travers les formats Gray, BGR, BGRx et BGRA. Comme pour les chemins, une image tournée ou biaisée, ou dont la matrice a un terme d’échelle dégénéré, est supprimée en entier
// After a successful SaveAsRedacted call
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
// nothing could be split: every intersecting object was dropped whole
LogWholeObjectFallback(SourcePdf);
Pourquoi PDFiumPas refuse-t-il en mode fail closed sur les caractères non mappés ?
Parce qu’un glyphe qui n’a aucun scalaire Unicode reproductible ne peut pas être reconstruit honnêtement. Reconstruire un survivant signifie appeler l’API de définition de texte avec une chaîne, et cela exige un point de code stable pour chaque caractère retenu. Les polices de sous-ensembles symboliques avec des données ToUnicode cassées ou absentes peuvent produire un mappage vide, et ré-encoder à la devinette produirait une sortie qui a l’air correcte à l’écran tout en portant un caractère différent en dessous. PDFiumPas refuse : la vérification des caractères retenus lève, l’exception est attrapée à l’intérieur de SaveAsRedacted, TPdfRedactionReport.Succeeded revient False avec le message dans ErrorMessage, et la fonction renvoie False. La même règle s’applique au budget de scission, qui lève plutôt que de tronquer silencieusement l’ensemble de fragments. Quand un document a des polices en lesquelles vous n’avez pas confiance et que vous voulez le vieux comportement déterministe, réglez Options.PreservePartialObjects := False et chaque objet intersectant disparaît en entier
Élagage de ressources à travers des portées partagées
Scinder des objets laisse des orphelins derrière, et les élaguer n’est pas aussi simple que de différer le dictionnaire /Resources au niveau de la page. L’ISO 32000-1 §7.8.3 laisse le même dictionnaire de ressources être référencé par plusieurs pages, par des Form XObjects, par des motifs, et par des flux d’apparence d’annotations à la fois. Supprimer un nom de police parce qu’une page a cessé de l’utiliser casserait une autre page qui l’utilise encore. PruneUnusedPdfResources fonctionne donc par portée : il résout /Contents qu’il soit un tableau direct, une référence indirecte vers un tableau, ou un flux unique, puis collecte l’usage de ressources depuis les opérateurs qui nomment réellement des ressources — Tf pour les polices, Do pour les XObjects, gs pour l’état graphique, CS, cs, SCN et scn pour les espaces colorimétriques et les motifs, sh pour les dégradés, BDC et DP pour les propriétés de contenu marqué, plus l’entrée /CS des images inline. Quand un dictionnaire est partagé par plusieurs portées, les ensembles de noms utilisés sont réunis par catégorie avant que quoi que ce soit soit retiré
Seuls les noms confirmés non référencés à travers chaque portée qui pointe vers le dictionnaire sont abandonnés. Une portée qui ne peut pas être analysée avec confiance est laissée intacte, ce qui est la direction conservatrice : un fichier non élagué est simplement plus gros, un fichier mal élagué est corrompu. Les dictionnaires survivants sont réécrits comme une mise à jour incrémentielle éparse portant les numéros de génération exacts, et une réécriture d’atteignabilité balaie ensuite les objets devenus inatteignables une fois les noms disparus. TPdfResourcePruneReport rapporte ScannedScopeCount, UpdatedScopeCount, RemovedNameCount, RemovedObjectCount, les comptes d’octets, et un drapeau Succeeded. SaveAsRedacted exécute cette étape automatiquement sur la sortie assainie, si bien que le chemin de caviardage l’inclut déjà, mais la fonction est exportée au niveau des flux pour les pipelines qui la veulent à part
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 stays False: an incremental rewrite would
// invalidate the byte ranges a signature covers
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;
Le câbler dans un pipeline de documents
Le chemin de caviardage ne mute jamais le document que vous avez chargé. SaveAsRedacted capture un instantané isolé, y applique les annotations /Redact, débarrasse les pièces jointes, exécute la passe d’assainissement qui retire l’action d’ouverture, les actions de catalogue, les arbres de noms, les fichiers associés, l’AcroForm et les métadonnées, élague les ressources, et seulement alors écrit le flux de sortie. Rouvrir cette sortie comme document indépendant et ré-extraire le texte est l’étape de vérification qui vaut la peine d’être gardée dans votre propre suite de tests, parce que c’est la seule vérification qui répond à la question d’origine — un lecteur peut-il encore obtenir la chaîne. Une conséquence à prévoir : la scission remplace les objets page, si bien que tout handle FPDF_PAGEOBJECT que vous déteniez est mort ensuite, le même piège de durée de vie décrit dans handles d’objets page périmés après une transformation
Deux pièces voisines complètent le flux de travail. Décider où vont les rectangles de caviardage commence d’ordinaire par la géométrie extraite, et le modèle de blocs et d’ordre de lecture de blocs de texte structuré et ordre de lecture est une meilleure source de boîtes candidates que des séquences de caractères brutes. Servir le résultat à un relecteur relève des règles de durcissement de construire un aperçu PDF sécurisé, où le remplissage de formulaires et le JavaScript restent désactivés par défaut. Ensemble ils couvrent la boucle dont la plupart des flux de conformité ont besoin : localiser, caviarder au niveau des opérateurs, vérifier en rouvrant, prévisualiser en sécurité. La surface API complète, le téléchargement d’essai et les conditions de licence du composant vivent sur la page produit PDFium Delphi Component