addField($name)->addField($countries)->addField($consent)->addField($delivery)->addField($reset)->addField($signature); $tree->addRoot($person); // An appearance is a Form XObject whose BBox is local to the widget. Visible // bytes are distinct from /V: changing a value requires a new AP, // or an explicit NeedAppearances request to the reader. AP is supplied here. // The frame remains static. For /Tx and /Ch, /Tx BMC ... EMC tells // readers what to REPLACE during editing (ISO 12.7.3.3, p.444). // Without this section, Acrobat adds text and retains the old drawing. $drawAppearance = static function (ContentStreams $contents, int $width, int $height) use ($resources): FormAppearanceStream { return new FormAppearanceStream(new ContentStreams( new SetLineWidth(new NumericType(1)), new AppendRectangle(new NumericType(0.5), new NumericType(0.5), new NumericType($width - 1), new NumericType($height - 1)), new StrokePath(), ...$contents->operators, ), new Rectangle(new NumericType(0), new NumericType(0), new NumericType($width), new NumericType($height)), $resources); }; $label = static fn (string $text, int $height): ContentStreams => new ContentStreams( new BeginTextObject(), new SetTextFontAndSize(new NameType('Helv'), new NumericType(12)), new SetNonstrokingGray(new NumericType(0)), new MoveTextPosition(new NumericType(8), new NumericType(($height - 12) / 2)), new ShowText(new LiteralString($text)), new EndTextObject(), ); $rectangle = static fn (int $x, int $y, int $width, int $height): Rectangle => new Rectangle(new NumericType($x), new NumericType($y), new NumericType($x + $width), new NumericType($y + $height)); $print = new Entry(new NameType('F'), new IntegerType(4)); $nameAppearance = $drawAppearance(new ContentStreams(...[ new BeginMarkedContent(new NameType('Tx')), new SaveGraphicsState(), new AppendRectangle(new NumericType(1), new NumericType(1), new NumericType(318), new NumericType(30)), new ClipPathNonzero(), new EndPath(), ...$label('Ada Lovelace', 32)->operators, new RestoreGraphicsState(), new EndMarkedContent(), ]), 320, 32); // A list draws its /Opt labels line by line and both /I selections. // Export values FR/BE remain exclusively /V data. // Highlighting and lines are all inside the replaceable variable section. $countryAppearance = $drawAppearance(new ContentStreams( new BeginMarkedContent(new NameType('Tx')), new SaveGraphicsState(), new AppendRectangle(new NumericType(1), new NumericType(1), new NumericType(318), new NumericType(30)), new ClipPathNonzero(), new EndPath(), new SetNonstrokingRGBColor(new NumericType(0.85), new NumericType(0.9), new NumericType(0.97)), new AppendRectangle(new NumericType(1), new NumericType(16), new NumericType(318), new NumericType(14)), new FillPathNonzero(), new AppendRectangle(new NumericType(1), new NumericType(2), new NumericType(318), new NumericType(14)), new FillPathNonzero(), new BeginTextObject(), new SetTextFontAndSize(new NameType('Helv'), new NumericType(12)), new SetNonstrokingGray(new NumericType(0)), new MoveTextPosition(new NumericType(8), new NumericType(19)), new ShowText(new LiteralString('France')), new MoveTextPosition(new NumericType(0), new NumericType(-14)), new ShowText(new LiteralString('Belgique')), new EndTextObject(), new RestoreGraphicsState(), new EndMarkedContent(), ), 320, 32); $nameWidget = new WidgetAnnotation($rectangle(64, 620, 320, 32), new WidgetAppearance($nameAppearance), null, $print); $countryWidget = new WidgetAnnotation($rectangle(64, 538, 320, 32), new WidgetAppearance($countryAppearance), null, $print); // Buttons: /V names the selected state. Each widget retains its own // /AS and both /Off and /Yes appearances (or Email/Paper for radio buttons). $checkboxAppearances = new AppearanceStates(new AppearanceState(new NameType('Off'), $drawAppearance($label('', 24), 24, 24)), new AppearanceState(new NameType('Yes'), $drawAppearance($label('X', 24), 24, 24))); $consentWidget = new WidgetAnnotation($rectangle(64, 460, 24, 24), new WidgetAppearance($checkboxAppearances), new NameType('Yes'), $print); $emailAppearances = new AppearanceStates(new AppearanceState(new NameType('Off'), $drawAppearance($label('', 24), 24, 24)), new AppearanceState(new NameType('Email'), $drawAppearance($label('X', 24), 24, 24))); $paperAppearances = new AppearanceStates(new AppearanceState(new NameType('Off'), $drawAppearance($label('', 24), 24, 24)), new AppearanceState(new NameType('Paper'), $drawAppearance($label('X', 24), 24, 24))); $emailWidget = new WidgetAnnotation($rectangle(64, 382, 24, 24), new WidgetAppearance($emailAppearances), new NameType('Email'), $print); $paperWidget = new WidgetAnnotation($rectangle(210, 382, 24, 24), new WidgetAppearance($paperAppearances), new NameType('Off'), $print); $resetWidget = new WidgetAnnotation($rectangle(64, 300, 220, 32), new WidgetAppearance($drawAppearance($label('Reinitialiser les champs', 32), 220, 32)), null, $print, new Entry(new NameType('A'), new ResetFormActionTable238(new Entry(new NameType('S'), new NameType('ResetForm'))))); $signatureWidget = new WidgetAnnotation($rectangle(64, 218, 320, 32), new WidgetAppearance($drawAppearance($label('Signature non renseignee', 32), 320, 32)), null, $print); $name->addWidget($nameWidget); $countries->addWidget($countryWidget); $consent->addWidget($consentWidget); $delivery->addWidget($emailWidget)->addWidget($paperWidget); $reset->addWidget($resetWidget); $signature->addWidget($signatureWidget); $widgets = [$nameWidget, $countryWidget, $consentWidget, $emailWidget, $paperWidget, $resetWidget, $signatureWidget]; $contents = new ContentStreams(new BeginTextObject(), new SetTextFontAndSize(new NameType('Helv'), new NumericType(24)), new MoveTextPosition(new NumericType(64), new NumericType(728)), new ShowText(new LiteralString('PDFToolkit - formulaires objets')), new EndTextObject()); foreach ([['Nom : Person.Name', 674], ['Pays : choix multiples et valeurs exportees', 592], ['Consentement : case a cocher', 514], ['Livraison : groupe de boutons radio', 436], ['Bouton sans valeur permanente', 354], ['Champ de signature et verrou Adobe', 272], ['Ce fichier contient des apparences explicites.', 166], ['La signature reste vide : aucun CMS n\'est genere.', 142]] as [$label, $y]) { $contents = $contents->with(new BeginTextObject(), new SetTextFontAndSize(new NameType('Helv'), new NumericType(12)), new MoveTextPosition(new NumericType(64), new NumericType($y)), new ShowText(new LiteralString($label)), new EndTextObject()); } $contents = $contents->with(new BeginTextObject(), new SetTextFontAndSize(new NameType('Helv'), new NumericType(12)), new MoveTextPosition(new NumericType(100), new NumericType(388)), new ShowText(new LiteralString('Email')), new MoveTextPosition(new NumericType(146), new NumericType(0)), new ShowText(new LiteralString('Papier')), new EndTextObject()); // /Parent links the widget to its field; /P targets its actual page, which // references it in /Annots. These relationships are checked before writing. $page = new PageObject($rectangle(0, 0, 612, 792), $resources, new StreamObject($contents), new Entry(new NameType('Annots'), new ArrayType(...array_map(static fn (WidgetAnnotation $widget) => new IndirectReference($widget), $widgets)))); foreach ($widgets as $widget) { $widget->bindToPage($page); } $pages = (new PageTreeNode())->addKid($page); $form = new AcroForm($tree); $extensions = new DeveloperExtensions(new DeveloperExtensionEntry(new DeveloperPrefix(new NameType('ADBE')), new DeveloperExtension(new NameType('1.7'), new IntegerType(3)))); $catalog = new DocumentCatalog($pages, new AcroFormEntry(new IndirectReference($form)), new Extensions($extensions)); $file = new FileStructure(new FileHeader('1.7'), new FileBody($catalog), new IndirectReference($catalog)); $path = $argv[1] ?? __DIR__ . '/acroform-fields.pdf'; $file->writeTo($path); echo 'PDF written to ' . $path . PHP_EOL;