Skip to main content

M17 — HTML forms

State: to do — Depends on: M12, M13, M14, M16 — Fields through M16's generation-side builder; tagged through M13's structure writer, which precedes it; no script produced, by ADR 37; nothing loaded, by ADR 38

Goal​

Render a template written as an HTML form into a PDF whose controls are real, fillable, accessible fields — named, typed, valued, and drawn exactly as the page shows them — and whose signature blocks are empty signature fields placed where the layout put them.

Fillable contracts, onboarding packs, direct-debit mandates, client questionnaires: a business writes them once as templates and sends them to be completed. M12 prints form controls statically, as a browser prints a form, so today a caller who wants fields must position them afterwards by coordinates, and move every one of them each time the template changes. The HTML engines that do this — WeasyPrint's pdf_forms, Prince's --pdf-forms, iText pdfHTML's AcroForm option — show the two failures to avoid: fields whose look in the viewer differs from the page around them, because the viewer regenerates what the engine did not draw; and fields a screen reader cannot name, because nothing tied the widget to its label. Here the appearance of each field is what the static print would have shown, painted by the same painter, and every field is tagged with its accessible name.

Scope​

In:

  • opt-in, twice: PdfRenderOptions.FormFields (Static, the default and M12's behavior, or Interactive), and the CSS property -adc-pdf-field on an element (auto, static, interactive, signature);
  • the mapping of <input> — text-like types, password, checkbox, radio, submit, reset, button —, <textarea>, <select> and <button> to M16's text, check box, radio group, combo box, list box and push button fields, through M16's PdfFormBuilder on M08's PdfDocumentBuilder.Form;
  • names from name, their hierarchy, their collisions, several <form> elements and the form attribute; values, defaults and flags from value, checked, selected, required, readonly, disabled, maxlength and spellcheck;
  • appearances painted by M12's painter from the computed style, and the /DA, /Q, /MK and /BS entries a viewer needs to redraw a field once someone types in it;
  • ResetForm and SubmitForm actions for reset and submit buttons, opt-in; never a script;
  • signature placeholders from any element the stylesheet marks, with /Lock from attributes and seed values from the options;
  • controls in running elements, margin boxes and fixed boxes, repeated on every page;
  • tagging with M13: a Form element per widget, /TU from the accessible name, /Tabs /S; and the Form element with its PrintField attributes for controls printed statically, which M13 left here; PDF/UA-1 conflicts under M09's policy;
  • conformance under M14's PDF/A-3 targets and M20's PDF/A-2 ones when they exist: appearances, embedded fonts, no NeedAppearances, forbidden actions refused;
  • batch generation (M12.6) with fields: unique names per record in one volume;
  • the command-line tool's html2pdf --forms.

Out, explicitly:

  • generating JavaScript, of any kind — never (ADR 37): HTML's constraint validation (pattern, min, max, step, what type="email" checks), event handlers, and Acrobat's format actions — which are JavaScript actions, so not written even for type="number" or type="date", though M16 recognizes them when it reads a form; each reported once per template, never enforced;
  • <input> of type file, color, range, image and hidden, <datalist>, <output>, <meter>, <progress>, contenteditable — printed statically as M12 prints them, and reported;
  • filling, flattening, exchanging and batch-filling the forms we generate — M16's operations, which apply to M17's output unchanged;
  • signing the placeholders — M26;
  • fields in PDF 2.0's structure namespace and under PDF/UA-2 — M28;
  • rich-text fields: a <textarea> holds text, and its markup is not a field's /RV;
  • XFA — never generated;
  • a form described in a markup of its own (XForms, a JSON schema) — not planned: HTML is the template language.

Design​

Where it lives​

In AdCodicem.Pdf.Html, a layer between layout and painting, which consumes the fragment tree and hands M16's builder what it needs:

Forms/ FormControlMapper an element and its computed style -> a field specification, or none
FieldNaming names, hierarchy, collisions, form prefixes, record prefixes
FieldPainter a control's fragment -> its appearance XObjects, through Paint/
FormStructure the Form elements, OBJR and PrintField attributes, through M13's sink

The core gains nothing. M16's PdfFormBuilder on PdfDocumentBuilder.Form writes widgets with their page and the field tree at Finish, and takes an appearance the caller supplies for every field type — per state for buttons — rather than generating one (M16 slice 10); M13's PdfStructureBuilder.Reference(element, annotation) places each widget in the structure; M11's annotation writer writes the widget dictionaries as it writes links.

Turning it on​

  • PdfRenderOptions.FormFields — Static keeps M12's output, tagged as below; Interactive makes fields of every mapped control.
  • -adc-pdf-field (not inherited; initial auto): auto follows the option; static prints the control even under Interactive; interactive makes a field of it even under Static — the per-element opt-in; signature makes any element a signature placeholder (below).
  • -adc-pdf-field-scope: document | page for controls that repeat on every page (below); -adc-pdf-field-font-size: auto | <length>, whose auto writes size 0 in /DA so that a viewer fits the text a user types.
  • The three properties are in the declared CSS level of M12.1, with the -adc-pdf-tag family M13 added, and reported when misspelled as any unknown property is.

The mapping​

HTMLFieldNotes
input text, search, email, tel, urlTextmaxlength → /MaxLen; spellcheck="false" → DoNotSpellCheck; size sets the width only
input number, date, time, datetime-local, month, weekTextThe value as the attribute writes it; no format action; html-form.type-as-text once per type
input passwordText, PasswordIts value is never written — M16's rule —, reported when the template had one
textareaText, MultilineIts text content as the value; rows, cols and wrap are layout only
input checkboxCheck boxvalue → the on-state name, Yes when absent (Acrobat's convention, not HTML's on); checked → /V and /AS
input radioOne radio group per nameEach control a kid widget whose on-state is its value, /Opt carrying the exact text when the value is not a plain name; NoToggleToOff, since an HTML radio cannot be unchecked; RadiosInUnison when two values repeat
select, neither multiple nor a size above 1Combo boxEach option's value and text as an /Opt pair; selected → /V; optgroup labels dropped from the list and reported
select multiple, or a size above 1List boxMultiSelect with multiple; /I from the selected options; /TI 0
button or input of type resetPush button, ResetForm over its form's fieldsWith PdfRenderOptions.FormActions; printed statically otherwise
button or input of type submit, in a form with an actionPush button, SubmitForm to that URL resolved against the base, HTML format, GET when method says so, its form's fieldsWith FormActions; the URL is written, never fetched; a javascript: URL gives no action, reported
button or input of type buttonPush button without actionOnly under -adc-pdf-field: interactive: an inert button is otherwise a picture
Any element under -adc-pdf-field: signatureSignature fieldBelow
input file, color, range, image, hidden; datalist, output, meter, progressNonePrinted as M12 prints them; html-form.control-unsupported once per kind
  • Flags: required → Required; readonly → ReadOnly; disabled, or inside a disabled fieldset → ReadOnly and NoExport, since HTML does not submit a disabled control.
  • Default values: /DV is the initial value, so that ResetForm restores what the template said.
  • Check boxes sharing a name with different values — HTML's way of submitting several choices — become one non-terminal field holding one check box per value, named by the value, rather than check boxes that would toggle together as a PDF viewer makes same-named boxes do.

Names​

  • From name, split at periods: client.nom is nom under a non-terminal client — useful and documented, since a PDF partial name may not contain a period. With no name, the id; with neither, a generated field-n in document order (html-form.name-generated).
  • Collisions, by M06's definition — equal names, or one the other plus a period-separated tail: radio buttons sharing a name are one group; text-like controls of one type and the same attributes become one field with a widget each, sharing a value (html-form.name-shared, information); anything else renames the later control with the smallest free suffix _2, _3 (html-form.name-collision, warning). A value used as a name loses the characters PDF forbids in a partial name, replaced by _, reported.
  • Several <form> elements, controls associated by containment or the form attribute: names prefixed by the form's name or id only when names collide across forms (html-form.form-prefixed).
  • Batch generation: one file per record needs nothing; one volume for the run nests each record's fields under its key or record-n, as M06's Nest policy does, so that a thousand subscription forms keep a thousand sets of values.

Geometry​

  • The widget's rectangle is the control's border box on its page, in PDF space; padding and borders are drawn inside the appearance. With ADR 39's two passes, the rectangle is the final pass's.
  • A control is atomic: never fragmented across pages or columns. One taller than the page area is clipped at the page's edge as an image is, its widget covering what shows (html-form.control-clipped).
  • Transforms: a rotation by a multiple of 90 degrees becomes /MK /R with the appearance drawn upright, as viewers expect; a uniform scale scales the rectangle; any other transform — a skew, another angle — prints the control statically (html-form.transform-unsupported). A control clipped by an ancestor's overflow or clip-path prints statically too: a widget cannot be clipped.
  • Repeated controls — in a running element, in a margin box's element(), under position: fixed — appear on every page. -adc-pdf-field-scope: document, the initial value, makes one field with a widget per page, so that initials typed once show everywhere; page makes one field per page, named name.pN from the physical page number. A signature placeholder is always per page: one signature signs once.

Appearances​

  • Painted by M12's painter from the control's fragment: exactly what the static print shows — background, borders with their radii and shadows, padding, the value's text shaped by HarfBuzz in the computed font, the user-agent stylesheet's check mark and radio dot in accent-color, an author's own drawing under appearance: none — into one form XObject per state: /N for text and choice fields; /N with the on-state and /Off for check boxes and radio buttons, both painted, the checked and the unchecked look; no /D. The page itself does not paint the control a second time.
  • For a viewer that redraws after a user types: /DA names the computed font's /DR entry, its size — or 0 under auto — and its color; /Q comes from text-align resolved against direction; /MK /BG from background-color; /MK /BC and /BS from the top border's color, width and style — solid, dashed and dotted as dashed, inset, outset as beveled, a bottom border alone as underline. What those entries cannot say — gradients, radii, shadows, four border colors, a web font's features — a viewer loses when it redraws; reported once per template (html-form.style-not-regenerable, information).
  • Text is set by the engine, so a value in Arabic or Hebrew, which M16's core generator draws in logical order and reports, is shaped and ordered here. text-transform changes the appearance but not the value: a viewer that redraws shows the value as it is, reported (html-form.text-transform-appearance-only).
  • Choice fields: a combo box shows its selected option with the user-agent's arrow; a list box its visible rows, the selected ones as the user-agent stylesheet's option:checked paints them.
  • Fonts: the appearance uses the subsets M12 embeds for the page. /DR must name a face a viewer can type with: PdfRenderOptions.FormFieldFonts is Subset (the default: the page's subset — a character outside it shows the viewer's fallback), Full (the face embedded whole in /DR, once per face, its cost measured and documented), or Standard (a standard 14 font named in /DR, refused under a PDF/A target). Slice 2 checks with veraPDF that a subset in /DR breaks no PDF/A claim, and records the verdict in status.md.

Signature placeholders​

  • Any element under -adc-pdf-field: signature — a div framing "Signature du client, précédée de la mention « Lu et approuvé »" — becomes an unsigned signature field whose widget is its border box, and whose appearance is the element's own painting, taken off the page so it is not drawn twice. The box shows in every viewer before signing; M26, or any signing service, signs the field by its name.
  • Name from data-adc-field-name, then the id, then a generated one; /TU from the accessible name.
  • /Lock from data-adc-lock: all, include: a b, or exclude: a b, naming fields as the PDF names them after mapping; a name that matches no field is reported, not guessed.
  • Seed values (/SV) from PdfRenderOptions.SignatureFields, a map from field name to M16's signature-field options: they are policy — digest methods, reasons, a certification level — not presentation.
  • An element with no box makes no field (html-form.signature-without-box); a box of zero size makes an invisible one.

Tagging​

With M13's structure writer, tagging on by default in the HTML engine:

  • Interactive: each widget in a Form structure element holding its OBJR, placed at the control's position in document order; /TU on the field from the accessible name, computed as HTML-AAM computes it, in this order — aria-labelledby, aria-label, the associated <label> by for or containment, title, placeholder; a radio group's /TU from its fieldset's legend, or its first radio's label; the label's text stays in the page's structure where M13 put it. /Tabs /S on every page with a widget.
  • Repeated controls live in running content, which is an artifact, while a widget must be structure: each page's widget gets a Form element after the page's last element (html-form.running-field-tagged, information), as M11 places annotations it cannot place better.
  • Static: a control printed statically becomes a Form element with a PrintField attribute object — /Role tv for text-like controls and selects, cb, rb, pb; /checked on or off; /Desc the accessible name — holding the painted control: the non-interactive form of ISO 32000-1 §14.8.5.6, which M13 left to this milestone.
  • Under the PDF/UA-1 target: a field without an accessible name is a conflict (html-form.accessible-name-missing) under M09's PdfConformancePolicy, collected with M13's so that a template is fixed in one pass, each named by its source position.

Conformance and output​

  • Under M14's PDF/A-3 targets, and M20's PDF/A-2 ones: every widget has its /AP /N, no NeedAppearances is written, every font is embedded, and each action is checked against the part's forbidden actions as veraPDF's rules state them — a conflict under the same policy (html-form.action-refused).
  • In PDF 2.0 output (ADR 40) fields work as in 1.7; M16's version rows apply.
  • Determinism: fields in document order, names derived, appearances by content, dates none; two renders give the same bytes, alone or in a batch.
  • Memory: widgets leave with their page; the field tree — a few hundred bytes a field — is written at Finish. A report of a thousand pages with a field on each holds memory proportional to its fields, measured.

Diagnostics and the tool​

In the HTML engine's diagnostics, disjoint from validation rule identifiers (ADR 36), each documented: html-form.control-unsupported, type-as-text, constraint-not-enforced, name-generated, name-shared (information); name-collision, form-prefixed, password-value-dropped, optgroup-flattened, control-clipped, transform-unsupported, text-transform-appearance-only, signature-without-box, action-refused (warning); style-not-regenerable, running-field-tagged (information); accessible-name-missing (warning, a conflict under the PDF/UA-1 target).

html2pdf gains --forms (Interactive), --form-actions, --form-fonts subset|full|standard, and its JSON report lists the fields made, with each control's source line and column.

Slices​

Each slice ends on a green commit, with its acceptance rows passing, the diagnostics it introduces documented, and the referee harness it adds running in CI.

  1. Text fields end to end. Delivers the option and -adc-pdf-field, the Forms/ layer, text-like inputs, password and textarea, names and their collisions, values and flags, the widget at the border box, the appearance painted by M12's painter and handed to M16's builder as the caller's, and the derived /DA, /Q, /MK and /BS. Proven by unit tests — the mapping row by row, every naming case, the appearance XObject's content equal to what the static paint writes for the same box — and by qpdf's --json, pikepdf, pdf.js's getFieldObjects() and pdftk's dump_data_fields_utf8 listing names, types, values and flags as the template declares them; the subscription template's fields named and typed as documents/form/reportlab-subscription-form.pdf's; MuPDF and pdf.js rendering the interactive output as M12's static one, within M12's threshold. Leaves buttons and choices.
  2. Check boxes, radio groups, combo and list boxes; fonts for typing. Delivers the button and choice rows, both states painted, /Opt, NoToggleToOff, multi-select, FormFieldFonts, and the veraPDF check of a subset in /DR. Proven by unit tests (non-ASCII radio values, repeated values, check boxes sharing a name, optgroup) and by pdf.js's getFieldObjects() export values, pdftk's fill_form switching states that pdf.js and MuPDF then draw as the checked static paint, and veraPDF's PDF/A-3b profile on the onboarding pack. Leaves actions and repetition.
  3. Buttons, actions, repeated controls, awkward geometry. Delivers reset and submit buttons under FormActions, running, margin-box and fixed controls with both scopes, rotations, clipping, controls taller than the page, several forms, record prefixes in batch generation. Proven by unit tests (each geometry case, each scope, a javascript: action) and by qpdf's --json and pikepdf showing each action, widget and page, pdf.js resolving the submit URL without fetching it, and a one-volume batch whose names pikepdf finds unique. Leaves signatures.
  4. Signature placeholders. Delivers -adc-pdf-field: signature, the appearance taken off the page, names, /Lock, seed values from the options, per-page placeholders. Proven by unit tests and by pyHanko, in a container, signing each placeholder of the contract template: the signature is intact, the lock applied, the widget's rectangle the box M12 laid out, and the page showing the block once — MuPDF's rendering before and after signing identical outside the box. Leaves tagging.
  5. Tagging, PDF/UA-1 and PDF/A. Delivers the Form elements, OBJR, /TU by the accessible-name order, PrintField for static controls, repeated controls' elements, /Tabs /S, the conflicts, the PDF/A checks. Proven by unit tests (every accessible-name source, fieldsets, a control with none) and by veraPDF's PDF/UA-1 profile on every template, interactive and static, and its PDF/A-3a profile with the dual claim of M14; pikepdf finding each widget's Form element shaped as vendor/pdf-association/indesign15-pdfua1-form.pdf's are. Leaves round trips and budgets.
  6. Round trips, determinism, budgets, the tool. Delivers html2pdf --forms, HtmlFormBenchmarks with MemoryDiagnoser, the batch and memory rows. Proven by pdftk filling our output from an XFDF and M16 flattening it, pdftotext finding the values and veraPDF's PDF/UA-1 profile still passing; determinism across renders and batches; CorpusToolTests.

Tests required​

Unit — tests/AdCodicem.Pdf.Tests:

  • Mapping: every row of the table, every attribute that changes a field, every unsupported control reported once per kind; -adc-pdf-field in each value against each option.
  • Names: periods, missing names, every collision case, several forms and the form attribute, record prefixes, values that make illegal names.
  • Geometry: border boxes in PDF space under each page size and orientation, rotations by 90, 180 and 270, a skew, a scale, clipping by overflow and clip-path, a control taller than the page, both scopes of repeated controls, the final pass's rectangle under ADR 39.
  • Appearances: each state's XObject equal, by content bytes, to the static paint of the same box; /DA, /Q, /MK and /BS derived from every border style, alignment and direction; the three font policies.
  • Tagging: the accessible-name order, radio groups and fieldsets, PrintField for each role, the placement of repeated controls' elements, each conflict.
  • Hostile templates: a hundred thousand controls, a select of a million options, a name of a megabyte, a thousand nested fieldsets — each within M12's render limits, bounded in time and memory, or reported under their limit.* code.
  • Determinism: every template rendered twice, under the invariant culture and fr-FR, identical bytes.

Integration — tests/AdCodicem.Pdf.IntegrationTests, every referee in a container (ADR 27):

  • qpdf — --check, --json (acroform); pikepdf — fields, widgets, actions, the structure tree; pdf.js — getFieldObjects() and rendering with forms enabled; PyMuPDF — widgets as a second listing; pdftk-java — dump_data_fields_utf8 and fill_form; MuPDF — rendering against M12's static references; pyHanko — signing each placeholder; veraPDF — PDF/UA-1, PDF/A-3a and 3b.
  • WeasyPrint with pdf_forms, informative: the same templates' fields — names, types, rectangles — compared with ours, each difference recorded rather than failed, since WeasyPrint is not the reference.

Acceptance conditions​

"The templates" are the three sources this milestone adds — not in the corpus (below): the subscription form, the onboarding pack and the fillable contract.

DocumentsBehaviorVerified by
The subscription template, and documents/form/reportlab-subscription-form.pdf, which carries the same formRendered Interactive: qpdf's --json, pikepdf, pdf.js and pdftk list nom, prenom, societe, courriel as text fields and newsletter as a check box — the names and types they list on ReportLab's form —, with the template's values and flagsHtmlFormRefereeTests.A_form_template_renders_to_fields_referees_list
The onboarding packEvery mapped control a field of the table's type, with its name, value, flags, options and actions as the template declares them, in every referee's listing; every unsupported control printed statically and reported exactly once per kind; no JavaScript anywhere in the file, as pikepdf's walk of every action findsHtmlFormRefereeTests.Every_control_maps_to_its_field
The three templates, Interactive and StaticMuPDF and pdf.js render the interactive output as M12 renders the static one, page by page, within M12's threshold; after pdftk sets new values, pdf.js and MuPDF draw them in the template's font, size and alignmentHtmlFormRefereeTests.Fields_look_like_the_page_they_were_printed_on
The contract template, from tests/corpus/sources/contract-fr.html with two signature blocks and initials in its footerTwo unsigned signature fields at the blocks' boxes, within 0.01 point of M12's layout; pyHanko signs each in turn, finds each signature intact and its lock applied; the initials one field with a widget on every pagePyHankoHtmlFormTests.Signature_blocks_become_fields_that_sign
The three templates under the PDF/UA-1 target, interactive and staticveraPDF's PDF/UA-1 profile reports no failure; each widget in a Form element with its OBJR, each field with its /TU, each static control in a Form element with its PrintField attributes — the shape pikepdf finds in vendor/pdf-association/indesign15-pdfua1-form.pdf, whose claim veraPDF upholdsVeraPdfHtmlFormTests.Form_templates_pass_pdf_ua_1
The onboarding pack with a control stripped of its labelUnder Refuse, PdfConformanceException naming the control's source position and the clause; under RemoveClaim, the file without pdfuaid and the loss reportedCorpusHtmlFormTests.A_field_without_a_name_is_a_pdf_ua_conflict
The onboarding pack under M14's PDF/A-3a and PDF/UA-1 dual targetveraPDF upholds both; a reset button under FormActions is refused as a conflict where the part forbids the action, and its field printed statically under RemoveClaimVeraPdfHtmlFormTests.Forms_keep_the_pdf_a_3_claim
The three templates rendered Interactive, then filled by pdftk from a recorded XFDF and flattened by M16pdftotext finds every value in the page content; veraPDF's PDF/UA-1 profile still passes; pikepdf finds no fieldPdftkHtmlFormTests.Generated_forms_fill_and_flatten
The subscription template as a batch of a thousand records, one volumeEach record's fields under its own name, a thousand sets pikepdf finds distinct; memory flat as records grow and throughput recorded in status.md; one file per record gives, for any record, the bytes the volume's record would have aloneCorpusHtmlFormTests.A_thousand_forms_hold_their_budget, HtmlFormBenchmarks
A report of a thousand pages with an initials control in its running footer — M12's long report, not in the corpus, M12's needOne field, a thousand widgets, each tagged after its page's content; memory proportional to the fields, measuredCorpusHtmlFormTests.Repeated_controls_hold_their_budget
Every render aboveTwo renders give identical bytesCorpusHtmlFormTests.Html_forms_are_deterministic
The templates through the toolhtml2pdf --forms produces the API's bytesCorpusToolTests.Html2pdf_forms_matches_the_api

Corpus​

What the corpus holds​

  • Sources: sources/contract-fr.html, eight articles under h2, which the contract template extends; sources/invoice-fr.html and report-fr.html, without a single form control.
  • The same form from another producer: documents/form/reportlab-subscription-form.pdf — four text fields and a check box with tooltips, /Helv in /DR, whose names and types the subscription template must reproduce.
  • A PDF/UA-1 form: vendor/pdf-association/indesign15-pdfua1-form.pdf — text, combo, check box and radio fields tagged under Form elements with OBJR, field tooltips, a merged field and widget —, whose claim veraPDF upholds: the shape our tagging is compared with.
  • Other producers' conventions: M16's AcroForms — Acrobat's, LiveCycle's, LibreOffice's, InDesign's, OmniForm's — for /DA, /MK and /BS as viewers expect them.
  • M12's reference renderings (documents/*/adcodicem-*-fr.pdf), which M12 commits — none with a control.

What it lacks​

NeedWhyPriorityLikely source
Three HTML form templates: sources/subscription-form-fr.html (the ReportLab form's fields and labels); sources/onboarding-pack-fr.html (every mapped control; every unsupported one; fieldset and legend; labels by for and by containment, aria-label, title, placeholder; required, readonly, disabled, maxlength; a password with a value; radios with non-ASCII values; a multi-select; optgroup; reset and submit buttons; a rotated control; a textarea taller than a page; a control without a label for the conflict row); the contract template, sources/contract-fr.html with two signature blocks and initials in a running footerEvery acceptance row is written against them; no source in the corpus has a single form control1Generated here, our own sources, MIT, recorded in build_corpus.py
Their static renderings by our engine, as approved references, and their interactive renderings committedThe visual rows compare the two; M12's references cover no control1Generated here, by M12 and then this milestone, recorded
The same templates through WeasyPrint's pdf_formsA second HTML engine's fields, to compare with ours and for M16 to read2Generated here: WeasyPrint from pypi, already M12's referee
A PDF/UA-1 form with a list box, a push button and a signature field, from an authoring tool, whose claim veraPDF upholdsThe tagging shape of those three types is checked on our output only; the InDesign form has none of them2A contribution (W08); a public source if one is found with an attribution-only license
Chromium's print of the same templatesHow a browser prints the same controls statically, beside M12's rendering3Generated here with Chromium, already in the container
A template with right-to-left values and dir="rtl" controlsAlignment, /Q and shaped values in Arabic and Hebrew are checked on unit fixtures only3Generated here, with M13's accessibility reference's Arabic passage

Traps​

  • HTML names allow periods; PDF partial names do not. Splitting them is a choice, documented, and a beside a.b is a collision M06 already defines.
  • Same-named check boxes toggle together in a PDF viewer, while HTML submits each. Group them under a parent and name each by its value.
  • HTML's default check-box value is on, Acrobat's Yes; radio values are arbitrary text, on-states are names. /Opt keeps the text.
  • Disabled controls are not submitted, read-only ones are: NoExport for the first.
  • A widget cannot be clipped, skewed or split. Print the control statically rather than draw a field that lies about its extent.
  • /DA cannot say what CSS says. The first redraw in a viewer loses radii, gradients and shadows; say so once rather than pretend.
  • The appearance must not be painted twice: once on the page and once as the widget, the two drift apart the moment someone types.
  • A subset cannot type new characters. Whatever /DR names is what a user's keystrokes are drawn in.
  • A password field's value is never written, even when the template carries one.
  • text-transform is presentation: the value is what the attribute says, and a redraw shows it so.
  • Running content is an artifact, and a widget is structure. A repeated control needs a Form element on every page it appears on.
  • A static control is still a form to a screen reader: PrintField says what it was.
  • A submit URL is a destination for someone's data. It is opt-in, never fetched by us, and never a javascript: URL.
  • Batch volumes share one field namespace: a thousand records named nom are one field unless nested.
  • The second layout pass moves boxes (ADR 39): a widget's rectangle is taken from the final pass, or the field sits beside the control it belongs to.

Documentation​

  • docs/website/docs/guides/html-forms.md — turning fields on, naming, values and flags, appearances and fonts, buttons and actions, signature placeholders, repeated controls, batches, what a viewer's redraw keeps.
  • docs/website/docs/reference/html-form-mapping.md — the mapping table, every attribute, every unsupported control, the diagnostics.
  • docs/website/docs/reference/css-support.md — -adc-pdf-field, -adc-pdf-field-scope, -adc-pdf-field-font-size.
  • docs/website/docs/guides/accessible-documents.md — accessible names of fields, Form and PrintField, the PDF/UA-1 conflicts of forms.
  • docs/website/docs/guides/forms.md (M16's) — filling and flattening the forms the engine generates.
  • docs/website/docs/reference/diagnostics.md and docs/website/docs/reference/tool/ — the html-form.* codes and html2pdf --forms.
  • docs/website/docs/introduction.md and docs/features/features.json — the html-forms feature delivered.
  • docs/architecture.md — the Forms/ layer of AdCodicem.Pdf.Html, between layout and painting.

Exit criteria​

  • Every mapped control becomes its field, with its name, value, flags, options and appearance; every unsupported one is printed and reported; no script is ever written.
  • Signature placeholders are placed from the layout, with their locks and seed values, and sign in pyHanko.
  • Repeated controls, rotations, clipping and batches behave as designed.
  • Fields are tagged with their accessible names, static controls with PrintField; the templates pass PDF/UA-1, and PDF/A-3a with the dual claim.
  • The priority-1 gaps above are filled; each remaining gap is recorded in docs/corpus-contributions.md.
  • The acceptance conditions above pass on the corpus, in CI, with no document skipped.
  • Unit tests cover each behavior, its degenerate cases and its hostile ones.
  • Integration tests confirm fields, rendering, fills, flattening, signatures and conformance through qpdf, pikepdf, pdf.js, PyMuPDF, pdftk-java, MuPDF, pyHanko and veraPDF, each in a container.
  • HtmlFormBenchmarks measures rendering with fields and batches with MemoryDiagnoser; status.md records the budgets.
  • The documentation site publishes the guide, the mapping reference and the CSS properties.
  • Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).