Generates an AcroForm PDF template from a JSON spec using iText 9.5.
- .NET 9 SDK
From the workspace root:
-
Build/run (project):
dotnet run --project src/PdfTemplateBuilder/PdfTemplateBuilder.csproj
-
Build/run (solution):
- Build solution:
dotnet build PdfTemplateBuilder.sln - Run CLI:
dotnet run --project src/PdfTemplateBuilder.Cli/PdfTemplateBuilder.Cli.csproj - Or use the convenience scripts below:
./build.ps1/./build.shand./run-cli.ps1/./run-cli.sh
- Build solution:
Publishing / CI
- This repository includes a GitHub Actions workflow at
.github/workflows/ci.ymlthat builds the solution on push and pull requests and produces a NuGet package artifact. - To create a NuGet package locally:
dotnet pack src/PdfTemplateBuilder/PdfTemplateBuilder.csproj -c Release -o ./artifactsLibrary API additions: PdfTemplateRenderer.Build(Stream outputStream, TemplateSpec spec)— write PDF to a provided stream.PdfTemplateRenderer.BuildToBytes(TemplateSpec spec)— returns PDF bytes.PdfTemplateRenderer.BuildToStream(TemplateSpec spec)— returns a seekableStreampositioned at 0 (caller disposes).
Examples:
- Write to file (existing behavior):
PdfTemplateRenderer.Build("out.pdf", spec, rows);
- Get bytes (e.g., for HTTP response):
var pdfBytes = PdfTemplateRenderer.BuildToBytes(spec, rows);
- Get a stream (e.g., return from ASP.NET action):
using var stream = PdfTemplateRenderer.BuildToStream(spec, rows);The output PDF is written to:
src/bin/Debug/net9.0/document-template-YYYYMMDD-HHMMSS.pdf
The generator resolves the template spec in this order:
./template-spec.json./src/PdfTemplateBuilder/template-spec.json./src/PdfTemplateBuilder/bin/Debug/net9.0/template-spec.json
Top-level:
unit:"mm" | "cm" | "pt"(defaultmm)origin:"top-left" | "bottom-left"(defaulttop-left)page: page size and marginsfonts: font paths for Unicode renderingstaticTexts[],fields[],checkboxes[],signatures[],tables[],subforms[]
{
"page": {
"size": "A4",
"margins": { "left": 10, "right": 10, "top": 10, "bottom": 10 }
}
}
You can also specify a custom page size by providing explicit width and height values (numbers in the configured unit, default mm). When both width and height are present they take precedence over size.
Example (A4 via explicit dimensions):
{
"page": {
"width": 210,
"height": 297,
"margins": { "left": 10, "right": 10, "top": 10, "bottom": 10 }
}
}
{
"fonts": {
"regular": "C:/Windows/Fonts/segoeui.ttf",
"bold": "C:/Windows/Fonts/segoeuib.ttf"
}
}
x,y: absolute coordinates inunitbelow: name of an element to place below (or"$prev"for previous element)gap: vertical gap inunitapplied whenbelowis used
If below is provided, y is ignored. gap is only considered when the referenced anchor can be resolved (i.e., the named element exists in the layout); it is converted to points (using unit) and used as the vertical spacing between the bottom of the anchor and the top of the current element.
Where gap is valid:
staticTexts[](e.g., put text below another element)fields[](text fields)checkboxes[]signatures[]tables[](table'sbelow/gapcontrols top-of-table placement)subforms[](subform positioning when usingbelow)
Behavior notes:
gapis measured in the configuredunit(defaultmm).- For top-based layouts (
origin: "top-left"), the resolved Y is calculated as:anchorBottomY - gap - elementHeightfor elements that need the element's top to be placed below an anchor (this ensures the specified gap is the space between the anchor and the element). - If a referenced anchor is on a previous page,
gapis still used, but the element may be moved to the next page according to normal flow and margin rules.
Example:
- If anchor bottom is at 50mm from the page bottom,
gap: 5(mm), and element height is 10mm, the element's Y will be placed so there is 5mm between the anchor and the element and the element occupies 10mm below that gap.
Examples:
- Simple
below+gap:
{ "name": "label", "x": 15, "y": 100, "height": 10 }
{ "name": "value", "below": "label", "gap": 5, "x": 15, "height": 8 }
- Using
"$prev"to anchor to the previous element:
{ "name": "first", "x": 15, "y": 200, "height": 10 }
{ "name": "second", "below": "$prev", "gap": 4, "x": 15, "height": 10 }
- Anchor on previous page (note): If the anchor resolves to an element on a previous page,
gapis still applied but the dependent element may be moved to the next page and positioned at the pageflowTop(page top minus top margin).
{
"name": "title",
"value": "DOCUMENT DE FUNDAMENTARE",
"x": 70,
"y": 35,
"width": "auto",
"fontSize": 11,
"bold": true
}
width: number (units) or'auto'to fill remaining horizontal space within the container.
Notes:
- When
widthis'auto', the element (field or static text) expands to the available horizontal space inside its container (page or subform), respecting the element'sx, the container width, and the right margin. - For
staticTexts,'auto'enables simple wrapping within the computed width.
{
"name": "institutie_publica",
"x": 50,
"y": 17,
"width": "auto",
"height": 7,
"fontSize": 9,
"borderWidth": 0.5,
"align": "left",
"multiline": false,
"dataType": "date|decimal|string",
"format": "yyyy-MM-dd|0.00",
"value": "",
"sampleValue": false
}
width: number (units) or'auto'to fill remaining horizontal space within the container.
Notes:
- When
widthis'auto', the element (field or static text) expands to the available horizontal space inside its container (page or subform), respecting the element'sx, the container width, and the right margin. - For text fields,
'auto'will cause the field widget to be created using the computed width.
{
"name": "angajament_legal",
"x": 150,
"y": 50,
"size": 4,
"borderWidth": 0.5,
"checked": false,
"checkType": "check"
}
Fields, checkboxes, tables and table columns support visibility options to keep AcroForm fields present in the PDF while hiding their on-page widget annotation.
-
visibleacceptstrue|falseor string values"Invisible"|"Hidden"|"NoView".- boolean
true(default) — visible, widgets not flagged. - boolean
false— behavior depends on context for backwards-compatibility: forfield/checkboxafalsewill hide the widget (field remains in AcroForm); fortable/columnafalsewill omit the table/column from rendering. - string
"Hidden"|"Invisible"|"NoView"or array["Hidden","NoView"]— render the element (table/column/header/etc.) and set the corresponding annotation flag(s) on the widget(s). Example semantics:"Hidden"sets the Hidden annotation bit on widgets"Invisible"sets the Invisible annotation bit on widgets"NoView"sets the NoView annotation bit on widgets- Combined values like
"Hidden|NoView"or["Hidden","NoView"]set multiple annotation bits (ORed)
- boolean
-
Table.RowsVisible(bool, defaulttrue) — whenfalsethe table header will be drawn but no data rows will be generated.
Notes:
- When a string flag is used (e.g.
"Hidden") the element is still drawn (lines, header, etc.) but the underlying widgets are flagged so they won't be shown/printed according to viewer behavior. - This preserves backwards compatibility: using
"visible": falseon tables/columns continues to omit them completely; using"visible": falseon fields/checkboxes hides the widget but keeps the field in the AcroForm.
Implementation note: the renderer hides widgets by setting the widget annotation F flags (the Hidden bit is set). The underlying form field remains present in the AcroForm so you can set or read its value with normal iText APIs or other tools.
Examples:
- Hide a text field (field remains in AcroForm but not visible):
{
"name": "institutie_publica",
"x": 50,
"y": 17,
"width": "auto",
"height": 7,
"fontSize": 9,
"borderWidth": 0.5,
"align": "left",
"visible": false
}
- Hide a checkbox widget:
{
"name": "angajament_legal",
"x": 150,
"y": 50,
"size": 4,
"borderWidth": 0.5,
"checked": false,
"checkType": "check",
"visible": false
}
- Table-level and column-level examples (hide rows and a column):
{
"name": "valoaAng",
"below": "valoare_angajamente",
"x": 25,
"rowHeight": 7,
"headerHeight": 8,
"sampleRowCount": 3,
"rowsVisible": false,
"columns": [
{ "name": "element", "header": "Element", "width": 35, "align": "left" },
{ "name": "cod_ssi", "header": "Cod SSI", "width": 15, "align": "left", "visible": false }
]
}
{
"name": "Sig_S1",
"x": 15,
"y": 260,
"width": 70,
"height": 12,
"borderWidth": 0.5
}
{
"name": "valoare_angajamente",
"x": 15,
"yStart": 175,
"rowHeight": 7,
"headerHeight": 8,
"bottomLimit": 20,
"headerFontSize": 8,
"bodyFontSize": 8,
"sampleRowCount": 5,
"rowNamePrefix": "row_",
"fitWidth": true,
"fitToSpace": false,
"headerWrap": true,
"headerAlign": "center",
"headerAutoFit": false,
"columns": [
{ "name": "element", "header": "Element", "width": 45, "align": "left" },
{ "name": "program", "header": "Program", "width": 25, "align": "center", "headerWrap": true, "headerAlign": "center", "headerAutoFit": false }
]
}
Notes:
headerWrap/headerAlign/headerAutoFitcan be set at table level or per column to override.headerAutoFit: falsekeeps header font size fixed even when columns are fit.headerWrapaccepts boolean or string values (e.g.trueor"true").
Field naming for table cells:
tableName_rowName_columnName
Subforms are positioned containers that can include all element types. They also support flow-style placement with below and optional enclosure borders.
{
"name": "main_form",
"x": 0,
"y": 0,
"width": 210,
"height": 297,
"staticTexts": [ ... ],
"fields": [ ... ],
"checkboxes": [ ... ],
"signatures": [ ... ],
"tables": [ ... ],
"subforms": [ ... ]
}
Subform placement and border options:
{
"name": "subform2",
"x": 0,
"y": 0,
"width": 210,
"height": 200,
"below": "main_form",
"gap": 6,
"borderWidth": 0.6,
"borderColor": "#6E6E6E",
"borderStyle": "dashed"
}
Notes:
belowcan reference another element (or"$prev") and offsets the subform usinggap.- When
belowis used, the subform's top is computed from the last element of the referenced container. - If there isn't enough space, the subform flows to the next page.
- If
widthorheightis omitted or set to0, the subform size expands to the available page area within margins. borderStylesupportssolid(default),dashed,dotted.borderColoraccepts named colors (red,green,blue,gray,lightgray) or hex like#RRGGBB.
-
Margins: Page margins define content flow limits (left/right/top/bottom). Element coordinates (
x,y) are absolute distances from the page edge in the configuredunit(e.g.,mm). Margins are used for flow and page-break calculations and are no longer applied as an extra offset to every explicitycoordinate on the first page. -
ysemantics: A numericyvalue is measured from the page top when usingorigin: "top-left". If you specifyy: 0, the element aligns with the page top; to place content inside the page safe area, usepage.margins.topfor flow and page-break control rather than offsetting everyyvalue. -
Subforms:
- Subforms without an explicit
y(or withy <= 0) and without abelowanchor start at the flow top (page height minus top margin). - Child elements inside a flow-style subform use their own
ycoordinates relative to the page top; a flow subform does not add the top margin again to its children. - Subforms without explicit
heightexpand to the available page area within margins; ifheightis set and doesn't fit on the current page, the subform is moved to the next page.
- Subforms without an explicit
-
Tables & page breaks: Tables compute a
flowTop(page height minus top margin) and will repeat headers on new pages.bottomLimitandmarginBottomare respected when deciding when to break rows onto the next page. -
Signatures: Signatures that share the same
belowanchor are handled as a group and will be moved as a unit to the next page if needed, avoiding multiple page breaks that would separate paired signatures. -
Fields pagination: Fields now support page breaks: when a field would be placed below the bottom margin it is moved to the next page and positioned at the flow top.
-
Migration note: If you adjusted
yvalues to compensate for previous behavior (where the top margin was applied as an offset on the first page), please review and update those values. Recommended practice: useymeasured from the page top and rely onpage.marginsfor flow and page-break behavior.
- Keep the PDF open? Use the timestamped output to avoid file locks.
- Use
origin: "top-left"for coordinates measured from the top edge. page.marginsshifts all coordinates inward (left/right/top/bottom).