$line) { $operators[] = new Op\BeginTextObject(); $operators[] = new Op\SetTextFontAndSize(new NameType('F1'), new NumericType($index === 0 ? 20 : 12)); $operators[] = new Op\MoveTextPosition(new NumericType(48), new NumericType(780 - 32 * $index)); $operators[] = new Op\ShowText(new LiteralString($line)); $operators[] = new Op\EndTextObject(); } return (new PageTreeNode())->addKid(new PageObject(new Rectangle(new NumericType(0), new NumericType(0), new NumericType(595), new NumericType(842)), $resources, new \PDF\Syntax\Object\StreamObject(new ContentStreams(...$operators)))); }; // This is a real encrypted PDF, not an EP label on clear bytes. Demo passwords // are intentionally public. File keys, salts and IVs come from SystemRandomSource. $random = new Security\SystemRandomSource(); $security = (new Security\ModernStandardSecurityHandler(new Security\OpenSslAes256Backend(), $random))->create(new Security\PreparedPassword('demo-reader'), new Security\PreparedPassword('demo-owner'), new Security\Pdf20Permissions(-4), new Security\EncryptMetadata(true)); $identifier = new HexadecimalString($random->bytes(16)); $inner = new FileStructure(new FileHeader('2.0'), new FileBody(new DocumentCatalog($createPage('Encrypted payload', 'This text becomes available after password authentication.'))), trailerEntries: new DictionaryType(new Entry(new NameType('ID'), new ArrayType($identifier, $identifier)))); $encryptedBytes = (new FileWriter(encryption: $security))->serialize($inner); $payload = new Files\EmbeddedFileStream($encryptedBytes, new NameType('application/pdf'), modificationDate: new Date(new DateTimeImmutable('2026-10-14T00:00:00Z')), filters: new FlateDecode()); $specification = new Files\FileSpecification(new LiteralString('payload.pdf'), new Files\FileSpecificationOptions(embedded: new Files\EmbeddedFileBindings(new Files\EmbeddedFileBinding(Files\EmbeddedFileKey::File, $payload))), new Files\AssociatedFileRelationship(Files\AFRelationship::EncryptedPayload), encryptedPayload: new Files\EncryptedPayload(new NameType('Standard'))); $attachments = new Files\EmbeddedFiles(new Files\EmbeddedFileAttachment(new LiteralString('payload.pdf'), $specification)); $collection = new Collection(view: CollectionView::Hidden, initialDocument: new LiteralString('payload.pdf')); // ISO 32000-2 7.6.7 requires exactly one EmbeddedFiles entry, a catalog AF // association and a hidden collection selecting that same name. The wrapper // stays unencrypted so a processor can display these instructions. $catalog = new DocumentCatalog($createPage('Encrypted PDF payload', 'This unencrypted wrapper contains one encrypted PDF.', 'Extract payload.pdf from the attachments, then open it.', 'Demonstration reader password: demo-reader', 'The payload uses the Standard security handler with AES-256.', 'Some viewers open the selected payload automatically.'), new Entry(new NameType('Names'), new DictionaryType($attachments->nameDictionaryEntry())), new Files\AssociatedFilesEntry(new Files\AssociatedFiles(new IndirectReference($specification))), $collection->catalogEntry()); $output = $argv[1] ?? __DIR__ . '/pdf20-encrypted-payload.pdf'; (new FileStructure(new FileHeader('2.0'), new FileBody($catalog)))->writeTo($output); echo $output . PHP_EOL;