Une annotation Redact désigne du contenu à retirer. Peindre un rectangle opaque
ne supprime pas le texte ou les images sous-jacents. L'addendum Adobe, pages
physiques 2–4, exige la destruction des données concernées et une apparence de
remplacement. Le core 0.8.0 introduit SecureRedaction pour un graphe d'objets
explicitement normalisé.
Exécuter le véritable exemple
composer require "roukmoute/pdf:^0.9.1"
php examples/pdf17-secure-redaction.php redacted.pdf --source=original.pdf
Le deuxième argument optionnel écrit l'original dans un fichier séparé pour comparaison. Le résultat caviardé ne l'incorpore jamais et ne lui ajoute aucune révision. Le code PHP exécutable complet et les deux pages obtenues figurent ci-dessous.
Comprendre l'entrée
L'exemple construit deux petites pages partageant la même image. La deuxième
contient un texte secret, un remplacement ActualText et une annotation de
caviardisation sur le texte et l'image secrets. Les métadonnées XMP du catalogue
contiennent aussi un secret. Un rectangle vert hors de la zone choisie permet
de vérifier visuellement la préservation du contenu.
La page se termine par une matrice de translation volontairement persistante. Le processeur doit isoler la marque de remplacement de cet état graphique source pour la placer dans les coordonnées de l'annotation.
Construire l'opération explicitement
use PDF\Document\Redaction\RedactionAnnotation;
use PDF\Document\Redaction\RedactionPageRequest;
use PDF\Document\Redaction\RedactionPlan;
use PDF\Document\Redaction\SecureRedaction;
$result = (new SecureRedaction())->apply(
$catalog,
new RedactionPlan(
new RedactionPageRequest($page, new RedactionAnnotation($annotation)),
),
);
$result->writeTo($destination);
$catalog, $page et $annotation sont les objets réellement assemblés par
l'application. Le plan lie une annotation sélectionnée à sa page ; il ne parcourt
pas le système de fichiers et n'importe pas implicitement un document externe.
Le résultat immuable est écrit dans un nouveau fichier, avec un nouveau graphe
atteignable et aucune révision antérieure.
Suivre ce qui est supprimé
Le processeur par défaut ne dispose pas d'un backend de géométrie fiable des
contours de fonte. Il supprime donc de manière conservatrice tous les objets
texte de la page sélectionnée. Les déclarations Widths ou FontBBox ne prouvent
pas qu'un texte est hors zone. Une implémentation explicite de
RedactionFontGeometry peut fournir la géométrie réelle pour une préservation plus fine.
Une image ou un Form touché est indivisible. L'image partagée contaminée est
supprimée des deux pages, y compris de son usage antérieur hors de la page
sélectionnée. Les rectangles vectoriels verts restent présents. Cette
sur-suppression volontaire évite qu'une ressource partagée conserve ailleurs
dans le résultat des données concernées.
La même règle suit les dépendances inverses via Mask, SMask et les Forms
imbriquées avant de reconstruire les pages finales. Une image conservée ne peut
pas retenir indirectement une image supprimée comme masque de transparence.
Le nouveau catalogue retire les annotations, métadonnées, formulaires/XFA,
pièces jointes, actions, signets, structure et canaux de texte alternatif. Les
ressources de rendu inutilisées sont éliminées. Le résultat ne préserve pas la
structure d'accessibilité de la source.
Les fontes conservées perdent les données descriptives telles que CharSet,
FontFamily et Style. Les dictionnaires propres à chaque contexte vérifient
les types des valeurs conservées : une chaîne cachée sous un champ numérique
est refusée.
Le core 0.8.1 vérifie aussi le domaine et le sous-type réels de la police. Une
police simple Type1 ou TrueType ne peut conserver des entrées superflues
CIDSystemInfo ou DescendantFonts. Une Type0 exige son véritable descendant
CID et un encodage compatible ; les identifiants CID légitimes restent dans ce
domaine. Les combinaisons invalides sont refusées avant tout résultat au lieu
de recopier des chaînes inutilisées dans la sortie.
Produire l'apparence de remplacement
QuadPoints prévaut sur Rect pour les régions choisies. Les bornes des chemins
utilisent des enveloppes conservatrices des points de contrôle, les coordonnées
transformées et l'étendue du trait. L'exemple peint un remplacement noir avec
le texte blanc REDACTED répété et centré, selon une apparence par défaut explicite.
Si RO est fourni, ce Form de remplacement prévaut sur la couleur et le texte superposé.
Les bornes des rectangles utilisent leurs quatre coins transformés après
rotation ou cisaillement. Un tracé non peint terminé par n est également
retiré si sa géométrie récupérable traverse une région.
Le processeur ne conserve que des données normalisées prises en charge. Le chemin
d'image par défaut accepte des lignes exactes d'échantillons en couleurs device.
La conservation d'images encodées opaques, profils couleur ou programmes de fonte
complexes exige un RedactionResourceSanitizer explicite qui analyse et reconstruit
ces ressources. Une image opaque touchée peut être supprimée sans décodage.
Respecter le périmètre des entrées acceptées
Le contenu opaque non pris en charge, les opérateurs inconnus, états graphiques non normalisés, motifs, shadings, contenu optionnel, XObjects externes, clipping texte et chemins traversant une limite de clipping sont refusés. Les budgets d'instructions, de graphe, d'imbrication, d'images et de répétition sont explicites. Ce processeur n'est pas un éditeur général de PDF externes arbitraires et ne détecte pas la stéganographie dans le contenu conservé.
Le validateur indépendant lance qpdf, inspecte les objets atteignables et flux décodés avec pypdf et rend les pages source/résultat avec Poppler. Il vérifie l'absence des textes, images et métadonnées secrets, la préservation des vecteurs verts, le placement du remplacement et l'unique révision de sortie. Ces contrôles démontrent la destruction et le rendu de cet exemple normalisé ; l'application doit aussi valider son domaine d'entrée et son processus de publication. L'oracle de régression rend seize pages au total, dont quatre cas de rectangles transformés et deux cas de dépendances Mask/SMask transitives. Le parcours des dépendances compte aussi les arêtes répétées dans le budget d'instructions.
Audits des sources · Sécurité · Tutoriels
Voir le résultat PDF
Ce fichier a été généré avec le code de cette documentation. Toutes ses pages sont reproduites ci-dessous ; ouvrir le PDF pour utiliser ses liens et ses signets.
Utiliser le lecteur PDF du navigateur
Ce lecteur dépend du navigateur. Si le document ne s’affiche pas, utiliser « Ouvrir le PDF » ou « Télécharger ».
Code source complet
Code de roukmoute/pdf · Consulter la source au commit documenté ↗
Lire le script commenté : pdf17-secure-redaction.php
<?php
declare(strict_types=1);
use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\Type1Font;
use PDF\Document\Forms\DefaultAppearance;
use PDF\Document\Redaction\RedactionAnnotation;
use PDF\Document\Redaction\RedactionPageRequest;
use PDF\Document\Redaction\RedactionPlan;
use PDF\Document\Redaction\SecureRedaction;
use PDF\Document\ResourceDictionary;
use PDF\FileStructure;
use PDF\Graphics\CoordinateSystems\Matrix;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators\AppendRectangle;
use PDF\Syntax\ContentStreamsResources\Operators\BeginMarkedContentWithProperties;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\ConcatenateMatrix;
use PDF\Syntax\ContentStreamsResources\Operators\EndMarkedContent;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\FillPathNonzero;
use PDF\Syntax\ContentStreamsResources\Operators\InvokeXObject;
use PDF\Syntax\ContentStreamsResources\Operators\RestoreGraphicsState;
use PDF\Syntax\ContentStreamsResources\Operators\SaveGraphicsState;
use PDF\Syntax\ContentStreamsResources\Operators\SetNonstrokingRGBColor;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextMatrix;
use PDF\Syntax\ContentStreamsResources\Operators\ShowText;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DataStructure\TextString;
use PDF\Syntax\DocumentStructure\DocumentCatalog;
use PDF\Syntax\DocumentStructure\PageTree\PageObject;
use PDF\Syntax\DocumentStructure\PageTree\PageTreeNode;
use PDF\Syntax\FileStructure\FileBody;
use PDF\Syntax\FileStructure\FileHeader;
use PDF\Syntax\Object\ArrayType;
use PDF\Syntax\Object\BooleanType;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectReference;
use PDF\Syntax\Object\IntegerType;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\NumericType;
use PDF\Syntax\Object\StreamObject;
use PDF\Syntax\Object\StringObject\LiteralString;
require dirname(__DIR__) . '/vendor/autoload.php';
// Adobe redaction addendum, physical pages 2–4. An annotation merely identifies
// a region. SecureRedaction removes source instructions and affected payloads,
// then builds a new file. Never append this result to an earlier PDF revision.
$font = new Type1Font(StandardFont::Courier);
$image = new StreamObject('SECRETIMG', new DictionaryType(
new Entry(new NameType('Type'), new NameType('XObject')),
new Entry(new NameType('Subtype'), new NameType('Image')),
new Entry(new NameType('Width'), new IntegerType(3)),
new Entry(new NameType('Height'), new IntegerType(1)),
new Entry(new NameType('ColorSpace'), new NameType('DeviceRGB')),
new Entry(new NameType('BitsPerComponent'), new IntegerType(8)),
));
$resources = new ResourceDictionary(
new Entry(new NameType('Font'), new DictionaryType(new Entry(new NameType('F1'), new IndirectReference($font)))),
new Entry(new NameType('XObject'), new DictionaryType(new Entry(new NameType('Im1'), new IndirectReference($image)))),
);
$box = new Rectangle(new NumericType(0), new NumericType(0), new NumericType(200), new NumericType(120));
$first = new PageObject($box, $resources, new StreamObject(new ContentStreams(
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new NumericType(10)),
new SetTextMatrix(new Matrix(e: new NumericType(10), f: new NumericType(100))),
new ShowText(new LiteralString('PUBLIC PAGE 1')),
new EndTextObject(),
new SaveGraphicsState(),
new ConcatenateMatrix(new Matrix(new NumericType(10), d: new NumericType(10), e: new NumericType(20), f: new NumericType(20))),
new InvokeXObject(new NameType('Im1')),
new RestoreGraphicsState(),
new SetNonstrokingRGBColor(new NumericType(0), new NumericType(1), new NumericType(0)),
new AppendRectangle(new NumericType(100), new NumericType(5), new NumericType(20), new NumericType(10)),
new FillPathNonzero(),
)));
$annotation = new DictionaryType(
new Entry(new NameType('Type'), new NameType('Annot')),
new Entry(new NameType('Subtype'), new NameType('Redact')),
new Entry(new NameType('Rect'), new Rectangle(new NumericType(19), new NumericType(68), new NumericType(81), new NumericType(92))),
new Entry(new NameType('IC'), new ArrayType(new NumericType(0), new NumericType(0), new NumericType(0))),
new Entry(new NameType('OverlayText'), new TextString('REDACTED')),
new Entry(new NameType('DA'), new DefaultAppearance(new SetTextFontAndSize(new NameType('F1'), new NumericType(8)), new SetNonstrokingRGBColor(new NumericType(1), new NumericType(1), new NumericType(1)))),
new Entry(new NameType('Repeat'), new BooleanType(true)),
new Entry(new NameType('Q'), new IntegerType(1)),
);
$second = new PageObject($box, $resources, new StreamObject(new ContentStreams(
new BeginMarkedContentWithProperties(new NameType('Span'), new DictionaryType(new Entry(new NameType('ActualText'), new TextString('SECRET-ACTUALTEXT')))),
new BeginTextObject(),
new SetTextFontAndSize(new NameType('F1'), new NumericType(10)),
new SetTextMatrix(new Matrix(e: new NumericType(20), f: new NumericType(75))),
new ShowText(new LiteralString('SECRET-TEXT')),
new EndTextObject(),
new EndMarkedContent(),
new SaveGraphicsState(),
new ConcatenateMatrix(new Matrix(new NumericType(10), d: new NumericType(10), e: new NumericType(20), f: new NumericType(70))),
new InvokeXObject(new NameType('Im1')),
new RestoreGraphicsState(),
new SetNonstrokingRGBColor(new NumericType(0), new NumericType(1), new NumericType(0)),
new AppendRectangle(new NumericType(100), new NumericType(5), new NumericType(20), new NumericType(10)),
new FillPathNonzero(),
// A deliberately persistent source CTM must not move the replacement mark.
new ConcatenateMatrix(new Matrix(e: new NumericType(5000))),
)), new Entry(new NameType('Annots'), new ArrayType(new IndirectReference($annotation))));
$pages = (new PageTreeNode())->addKid($first)->addKid($second);
$catalog = new DocumentCatalog($pages, new Entry(new NameType('Metadata'), new IndirectReference(new StreamObject('<x:xmpmeta xmlns:x="adobe:ns:meta/">SECRET-XMP</x:xmpmeta>', new DictionaryType(new Entry(new NameType('Type'), new NameType('Metadata')), new Entry(new NameType('Subtype'), new NameType('XML')))))));
// There is no font-program geometry backend in this example: all BT/ET text
// objects on the selected page are removed conservatively. Text on untouched
// pages remains. A tainted shared image is removed from every reachable use.
$result = (new SecureRedaction())->apply($catalog, new RedactionPlan(new RedactionPageRequest($second, new RedactionAnnotation($annotation))));
$path = $argv[1] ?? __DIR__ . '/pdf17-secure-redaction.pdf';
$result->writeTo($path);
// Independent validators can retain the original separately; it is never
// embedded, copied or incrementally appended to the redacted result.
if (isset($argv[2]) && str_starts_with($argv[2], '--source=')) {
(new FileStructure(new FileHeader('1.7'), new FileBody($catalog), new IndirectReference($catalog)))->writeTo(substr($argv[2], strlen('--source=')));
}
echo $path . PHP_EOL;

