new Rectangle(new NumericType($x), new NumericType($y), new NumericType($x + $width), new NumericType($y + $height)); $text = static fn (string $value, int $x, int $y, int $size, string $font = 'F1'): ContentStreams => new ContentStreams( new BeginTextObject(), new SetTextFontAndSize(new NameType($font), new NumericType($size)), new MoveTextPosition(new NumericType($x), new NumericType($y)), new ShowText(new LiteralString($value)), new EndTextObject(), ); $referenceAppearance = new FormAppearanceStream(new ContentStreams(...[ new SetStrokingGray(new NumericType(0.7)), new SetLineWidth(new NumericType(1)), new AppendRectangle(new NumericType(0.5), new NumericType(0.5), new NumericType(498), new NumericType(31)), new StrokePath(), // ISO 12.7.3.3, p.444: readers replace only variable text while // preserving the frame. This appearance stays distinct from the signature. new BeginMarkedContent(new NameType('Tx')), new SaveGraphicsState(), new AppendRectangle(new NumericType(1), new NumericType(1), new NumericType(497), new NumericType(30)), new ClipPathNonzero(), new EndPath(), new SetNonstrokingGray(new NumericType(0)), ...$text('DOSSIER-2026-010', 12, 10, 12)->operators, new RestoreGraphicsState(), new EndMarkedContent(), ]), $rectangle(0, 0, 499, 32), $resources); $signatureAppearance = new FormAppearanceStream(new ContentStreams( new SetNonstrokingGray(new NumericType(0.95)), new AppendRectangle(new NumericType(0), new NumericType(0), new NumericType(499), new NumericType(80)), new FillPathNonzero(), new SetNonstrokingGray(new NumericType(0)), ...$text('Signature mathematiquement verifiee', 16, 54, 15, 'Title')->operators, ...$text('CMS detache / SHA-256 / certificat public de test', 16, 31, 11)->operators, ...$text('Apparence fournie explicitement par ce script', 16, 13, 9)->operators, ), $rectangle(0, 0, 499, 80), $resources); // 3. The seed defines obligations. Ff bits require Filter, the first // supported SubFilter, DigestMethod and the reason actually written. // Reference: ISO Table234, PDF32000_2008.pdf, pages455-457. $reason = new TextString('Étudié et approuvé'); $seed = new SignatureSeedValue( new SignatureSeedFlags(SignatureSeedFlag::Filter, SignatureSeedFlag::SubFilter, SignatureSeedFlag::DigestMethod, SignatureSeedFlag::Reasons), new Entry(new NameType('Filter'), new NameType('Adobe.PPKLite')), new Entry(new NameType('SubFilter'), new ArrayType(new NameType('adbe.pkcs7.detached'))), new Entry(new NameType('DigestMethod'), new ArrayType(new NameType('SHA256'))), new Entry(new NameType('Reasons'), new ArrayType($reason)), ); $referenceName = new PartialFieldName(new TextString('Reference')); $reference = new TextField($referenceName, value: new TextString('DOSSIER-2026-010')); $lock = new SignatureFieldLock(SignatureLockAction::Include, new ArrayType($referenceName->value)); $signing = new SignatureFieldSigning( $backend, new PartialFieldName(new TextString('Approval')), new SignatureCapacity(8192), new SignatureSigningIntent(reason: $reason, appearance: new SignatureAppearanceSelection(new TextString('Approbation pédagogique'), new IndirectReference($signatureAppearance))), $seed, $lock, ); // 4. Field Approval's /V designates the reservation. Its /Lock selects // Reference. SignatureFieldSigning copies Action/Fields into FieldMDP // without inventing coordinates or changing the target field for the application. $defaultAppearance = new DefaultAppearance(new SetTextFontAndSize(new NameType('F1'), new NumericType(12)), new SetNonstrokingGray(new NumericType(0))); $fields = (new FieldTree($defaultAppearance, $resources))->addRoot($reference)->addRoot($signing->field); $referenceWidget = new WidgetAnnotation($rectangle(48, 610, 499, 32), new WidgetAppearance($referenceAppearance), entries: new Entry(new NameType('F'), new IntegerType(4))); $signatureWidget = new WidgetAnnotation($rectangle(48, 353, 499, 80), new WidgetAppearance($signatureAppearance), entries: new Entry(new NameType('F'), new IntegerType(4))); $reference->addWidget($referenceWidget); $signing->field->addWidget($signatureWidget); $content = new ContentStreams(...[ ...$text('Signer un champ sous conditions', 48, 760, 22, 'Title')->operators, ...$text('Des objets explicites, un contrat applique aux octets ecrits.', 48, 721, 11)->operators, ...$text('1 Le formulaire conserve la reference du dossier', 48, 672, 14, 'Title')->operators, ...$text('Ce champ est selectionne par le verrou FieldMDP de Approval.', 48, 589, 10)->operators, ...$text('2 Le seed impose le format, la raison et le digest', 48, 553, 14, 'Title')->operators, ...$text('Filter : Adobe.PPKLite | SubFilter : adbe.pkcs7.detached', 48, 526, 11)->operators, ...$text('DigestMethod : SHA256 | Reason : Etudie et approuve', 48, 504, 11)->operators, ...$text('3 Le resultat verifie remplit la reservation Contents', 48, 461, 14, 'Title')->operators, ...$text('Le CMS signe la revision entiere, sauf la valeur Contents.', 48, 310, 11)->operators, ...$text('Le champ, son widget et le verrou appartiennent au meme catalogue.', 48, 289, 10)->operators, new SetNonstrokingGray(new NumericType(0.97)), new AppendRectangle(new NumericType(48), new NumericType(128), new NumericType(499), new NumericType(128)), new FillPathNonzero(), new SetNonstrokingGray(new NumericType(0)), ...$text('Ce que cette signature confirme', 60, 234, 12, 'Title')->operators, ...$text('Les octets couverts correspondent au certificat et au CMS generes.', 60, 210, 10)->operators, ...$text('Ce certificat et sa cle sont publics et reserves a la demonstration.', 60, 193, 10)->operators, ...$text('Aucune identite PKI, aucun horodatage TSA ni preuve OCSP/CRL.', 60, 176, 10)->operators, ...$text('Le verrou est une declaration PDF ; la comparaison generale des', 60, 159, 10)->operators, ...$text('revisions DocMDP/FieldMDP/UR reste un mecanisme distinct.', 60, 142, 10)->operators, new SetNonstrokingGray(new NumericType(0.5)), ...$text('PDFToolkit / Tutoriel 26 / PHP 8.3+', 48, 60, 9)->operators, ]); $page = new PageObject($rectangle(0, 0, 595, 842), $resources, new StreamObject($content), new Entry(new NameType('Annots'), new ArrayType(new IndirectReference($referenceWidget), new IndirectReference($signatureWidget)))); $referenceWidget->bindToPage($page); $signatureWidget->bindToPage($page); $catalog = new DocumentCatalog((new PageTreeNode())->addKid($page), new Entry(new NameType('AcroForm'), new IndirectReference(new AcroForm($fields, entries: new Entry(new NameType('SigFlags'), new IntegerType(3)))))); // 5. Data explicitly closes the circular connection to this catalog. prepare() // writes the file and fixes ByteRange. The contract rereads actually emitted // snapshots, then verifies the result's mathematical signature, digest and // certificate before filling Contents. It grants no PKI trust. $signing->bindCatalog(new IndirectReference($catalog)); $prepared = (new SignatureWriter())->prepare(new FileStructure(new FileHeader('1.7'), new FileBody($catalog)), $signing->signature); $signed = $signing->sign($prepared); $path = $argv[1] ?? __DIR__ . '/signature-field-policy.pdf'; $signed->writeTo($path); echo 'PDF written to ' . $path . PHP_EOL;