English · Français

A Redact annotation identifies content to remove. Painting an opaque rectangle does not remove the underlying text or images. The Adobe addendum, physical pages 2–4, requires destruction of the affected data and a replacement appearance. Core 0.8.0 introduces SecureRedaction for an explicitly normalized object graph.

Run the actual example

composer require "roukmoute/pdf:^0.9.1"
php examples/pdf17-secure-redaction.php redacted.pdf --source=original.pdf

The optional second argument writes the original to a separate file for comparison. The redacted result never embeds that original or appends a revision to it. The complete executable PHP source and the two resulting pages are below.

Understand the input

The example constructs two small pages sharing the same image. The second page contains secret text, an ActualText replacement and a redaction annotation over the secret text and image. Catalog XMP metadata also contains a secret. A green rectangle outside the selected area provides a visible preservation check.

The page ends with a deliberately persistent translation matrix. The processor must isolate the replacement mark from that source graphics state so the mark appears in the annotation's coordinates.

Make the operation explicit

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 and $annotation are the actual objects assembled by the application. The plan binds a selected annotation to its page; it does not search the filesystem or silently import an external document. The immutable result is written as a fresh file with a new reachable graph and no previous revisions.

Follow what is removed

The default processor has no trusted font-outline geometry backend. It therefore removes every text object on the selected page conservatively. Font Widths or FontBBox declarations alone cannot prove that text lies outside the area. An explicit RedactionFontGeometry implementation can provide actual geometry when the application needs finer preservation.

An affected image or Form is indivisible. The tainted shared image is removed from both pages, including its earlier use outside the selected page. The green vector rectangles survive. This intentional excess deletion prevents a shared resource from retaining affected payloads elsewhere in the result. The same rule follows reverse dependencies through Mask, SMask and nested Forms before rebuilding any final page. A surviving image cannot retain a deleted image indirectly as its transparency mask.

The fresh catalog drops annotations, metadata, forms/XFA, attachments, actions, outlines, structure and alternate-text channels. Unused rendering resources are pruned. The result does not preserve the source's accessibility structure. Retained fonts lose ancillary descriptor data such as CharSet, FontFamily and Style. Context-specific dictionaries validate the types of retained values: a string concealed under a numeric field is rejected. Core 0.8.1 also validates the actual font domain and subtype. A simple Type1 or TrueType font cannot retain superfluous CIDSystemInfo or DescendantFonts entries. A Type0 font requires its actual CID descendant and compatible encoding; legitimate CID identifiers remain in that domain. Invalid combinations are rejected before any result, rather than carrying unused strings into the output.

Produce the replacement appearance

QuadPoints takes precedence over Rect for the selected regions. Path bounds use conservative control-point hulls, transformed coordinates and stroke extent. Rectangle bounds include all four transformed corners under rotation or shear. An unpainted path terminated by n is also removed when its recoverable geometry crosses a region. The example paints a black replacement with repeated, centred white REDACTED text using an explicit default appearance. If RO is supplied, that replacement Form takes precedence over colour and overlay-text entries.

The processor retains only supported normalized data. The default image path accepts exact device-colour sample rows. Retained opaque image encodings, colour profiles or complex font programs need an explicit RedactionResourceSanitizer that parses and rebuilds those resources. Opaque affected images can instead be deleted without decoding.

Respect the accepted input boundary

Unsupported opaque content, unknown operators, unnormalized graphics states, patterns, shadings, optional content, external XObjects, text clipping and crossing clipping paths are rejected. Instruction, graph, nesting, image and overlay-repeat budgets are explicit. This processor is not a general editor for arbitrary external PDFs, and it cannot detect steganography in retained content.

The independent validator runs qpdf, inspects reachable objects and decoded streams with pypdf, and renders source/result pages with Poppler. It checks the absence of secret text, image and metadata payloads, the survival of green vectors, the placement of the replacement and the single output revision. Those checks demonstrate this normalized example's destruction and rendering; applications must still validate their own input domain and publication process. The regression oracle renders sixteen pages in total, including four transformed rectangle cases and two transitive Mask/SMask dependency cases. Dependency work also counts repeated graph edges against the instruction budget.

Source audits · Security · Tutorials

View the PDF result

This file was generated with the code in this documentation. All pages appear below; open the PDF to use its links and bookmarks.

PDF result pdf17-secure-redaction: page 1 of 2
Page 1 of 2
PDF result pdf17-secure-redaction: page 2 of 2
Page 2 of 2
Use the browser PDF viewer

This viewer depends on your browser. If the document does not appear, use “Open the PDF” or “Download”.

Complete source code

Code from roukmoute/pdf · View the source at the documented commit ↗

Read the commented script: 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;

Guide source at the documented commit ↗