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.
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;

