English · Français

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.

Résultat PDF pdf17-secure-redaction : page 1 sur 2
Page 1 sur 2
Résultat PDF pdf17-secure-redaction : page 2 sur 2
Page 2 sur 2
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;

Source de ce guide au commit documenté ↗