BoxLang 🚀 A New JVM Dynamic Language Learn More...
|:------------------------------------------------------: |
| ⚡︎ B o x L a n g ⚡︎
| Dynamic : Modular : Productive
|:------------------------------------------------------: |
Copyright Since 2023 by Ortus Solutions, Corp
www.boxlang.io | www.ortussolutions.com
This module provides comprehensive PDF manipulation and generation functionality for BoxLang, including:
{% hint style="danger" %} This module is only available to
+/++ subscribers only but
can be installed in conjunction with the bx-plus
Module with a limited trial. {% endhint %}
# For Operating Systems using our Quick Installer
install-bx-module bx-pdf
# Using CommandBox to install for web servers
box install bx-pdf
This module contributes the following components to the BoxLang runtime:
document
- Main component for creating PDF documents from HTMLdocumentitem
- Specifies headers, footers, and page breaksdocumentsection
- Divides documents into sections with unique propertiespdf
- Advanced PDF operations (merge, split, watermark, etc.)pdfparam
- Parameters for PDF operations like attachmentspdfform
- Manipulate PDF forms (populate/read form fields)pdfformparam
- Specify individual form field valuesThe document component creates PDF documents from HTML
content with extensive customization options.
When rendering remote HTTP/HTTPS images, the module uses bounded connect/read timeouts so unavailable endpoints fail instead of hanging PDF generation.
| Attribute | Type | Default | Description |
|---|---|---|---|
format
| String | "pdf" | Document format (PDF only) |
filename
| String | - | Output file path |
variable
| String | - | Variable name to store PDF binary |
encryption
| String | "none" | Encryption level: "128-bit", "40-bit", "none" |
orientation
| String | "portrait" | Page orientation: "portrait", "landscape" |
pageType
| String | "A4" | Page size (A4, LETTER, LEGAL, etc.) |
pageWidth
| Numeric | - | Custom page width in inches |
pageHeight
| Numeric | - | Custom page height in inches |
marginTop
| Numeric | - | Top margin |
marginBottom
| Numeric | - | Bottom margin |
marginLeft
| Numeric | - | Left margin |
marginRight
| Numeric | - | Right margin |
scale
| Numeric | 100 | Scaling percentage (≤100) |
backgroundVisible
| Boolean | true | Show background elements |
bookmark
| Boolean | true | Generate bookmarks |
htmlBookmark
| Boolean | false | Convert HTML anchors to bookmarks |
fontEmbed
| Boolean | true | Embed fonts in document |
fontDirectory
| String | - | Custom font directory |
localUrl
| Boolean | false | Allow local file URLs/paths in HTML assets (supports Windows paths, file:/// URIs, and relative paths) |
openpassword
| String | - | Password to open document |
ownerPassword
| String | - | Owner password for restrictions |
pdfa
| Boolean | false | Generate PDF/A compliant document |
saveAsName
| String | - | Browser save filename |
overwrite
| Boolean | false | Overwrite existing files |
src
| String | - | URL or path to HTML content |
srcfile
| String | - | Absolute path to HTML file |
mimeType
| String | "text/html" | Source content MIME type |
unit
| String | "inches" | Measurement unit: "in", "cm" |
The following attributes are not currently implemented and will throw an error if used:
permissions - Granular permissibility is not yet supportedpermissionspassword - Granular permissibility is not
yet supporteduserPassword - Granular permissibility is not yet supportedauthPassword - Authentication password not supportedauthUser - Authentication user not supporteduserAgent - HTTP user agent identifier not supportedproxyHost - Proxy server configuration not supportedproxyPassword - Proxy authentication not supportedproxyPort - Proxy port configuration not supportedproxyUser - Proxy user authentication not supportedtagged - ACF OpenOffice integration not supportedformfields - Form field attributes not implemented in
standard moduleformsType - Form type specification not implemented in
standard moduleSpecifies headers, footers, and page breaks within PDF documents.
| Attribute | Type | Required | Description |
|---|---|---|---|
type
| String | Yes | Item type: "header", "footer", "pagebreak" |
evalAtPrint
| Boolean | No | ⚠️ Deprecated - content always evaluated |
Divides PDF documents into sections with unique headers, footers, and page numbering.
| Attribute | Type | Description |
|---|---|---|
name
| String | Section name (used for bookmarks) |
marginTop
| Numeric | Section top margin |
marginBottom
| Numeric | Section bottom margin |
marginLeft
| Numeric | Section left margin |
marginRight
| Numeric | Section right margin |
src
| String | URL or relative path to content |
srcfile
| String | Absolute path to content file |
mimeType
| String | Content MIME type |
The following attributes are not currently implemented and will throw an error if used:
userAgent - HTTP user agent identifier for URL fetchingauthPassword - Authentication password for URL contentauthUser - Authentication username for URL contentManipulates PDF forms created in Adobe Acrobat and Adobe LiveCycle Designer. Supports both populating form fields and extracting form data.
| Attribute | Type | Required | Description |
|---|---|---|---|
action
| String | Yes | "populate" or "read" |
source
| Any | Yes | Source PDF form (file path, byte array, or variable) |
destination
| String | No | Output file path (populate action) |
result
| String | No | Variable name for extracted data (read action) |
overwrite
| Boolean | No | Overwrite existing files (default: false) |
overwriteData
| Boolean | No | Overwrite existing form data (default: false) |
fdfdata
| String | No | FDF file path for import/export |
XMLdata
| String | No | ⏳ XML data (planned feature) |
<bx:pdfform action="populate"
source="/forms/employee-form.pdf"
destination="/completed/employee-123.pdf"
overwrite="true">
<bx:pdfformparam name="employeeId" value="EMP-123" />
<bx:pdfformparam name="firstName" value="John" />
<bx:pdfformparam name="lastName" value="Smith" />
<bx:pdfformparam name="department" value="Engineering" />
<bx:pdfformparam name="salary" value="$85,000" />
<bx:pdfformparam name="startDate" value="2024-01-15" />
</bx:pdfform>
<bx:pdfform action="read"
source="/submitted/employee-form-filled.pdf"
result="employeeData" />
<!-- Access extracted data -->
<bx:output>
Employee: #employeeData.firstName# #employeeData.lastName#<br>
ID: #employeeData.employeeId#<br>
Department: #employeeData.department#
</bx:output>
<bx:dump var="#employeeData#" />
<!-- Export form data to FDF -->
<bx:pdfform action="read"
source="/filled-forms/survey.pdf"
fdfdata="/exported-data/survey-data.fdf" />
<!-- Import FDF data to populate form -->
<bx:pdfform action="populate"
source="/templates/survey-template.pdf"
destination="/completed/survey-filled.pdf"
fdfdata="/data/survey-responses.fdf" />
Specifies individual form field values when populating PDF forms.
Must be nested within a pdfform component.
| Attribute | Type | Required | Description |
|---|---|---|---|
name
| String | Yes | Form field name |
value
| String | Yes | Value to assign to the field |
index
| Integer | No | Field index for LiveCycle forms (default: 1) |
index is not applicableindex to distinguish them<bx:pdfform action="populate" source="registration-form.pdf" destination="completed-form.pdf">
<!-- Text fields -->
<bx:pdfformparam name="fullName" value="Jane Doe" />
<bx:pdfformparam name="email" value="[email protected]" />
<!-- Checkbox (requires specific values like "Yes"/"No" or "On"/"Off") -->
<bx:pdfformparam name="agreeToTerms" value="Yes" />
<!-- Dropdown selection -->
<bx:pdfformparam name="country" value="United States" />
<!-- Radio button -->
<bx:pdfformparam name="gender" value="Female" />
<!-- Date field -->
<bx:pdfformparam name="birthDate" value="1985-03-15" />
<!-- Numeric field -->
<bx:pdfformparam name="yearsExperience" value="8" />
</bx:pdfform>
Performs advanced PDF operations like merging, splitting, watermarking, and form manipulation.
| Action | Description |
|---|---|
addAttachments
| Add file attachments to PDF |
addHeader
| Add headers to pages |
addFooter
| Add footers to pages |
addWatermark
| Add watermark to pages |
deletePages
| Remove specific pages |
export
| Export form data (FDF/XFDF) |
extractText
| Extract text content |
extractimage
| Extract embedded images |
getInfo
| Get PDF metadata |
import
| Import form data |
merge
| Combine multiple PDFs |
protect
| Add password protection |
removePassword
| Remove password protection |
removeWatermark
| Remove watermarks |
removeHeaderFooter
| Remove headers/footers |
thumbnail
| Generate page thumbnails |
transform
| Transform/manipulate pages |
unsign
| Remove digital signatures |
validatesignature
| Validate signatures |
write
| Save PDF to file |
When using addHeader and addFooter, the
stamped content is written as PDF Artifact marked content
where supported, which aligns with standard PDF semantics for non-body
content. The removeHeaderFooter action removes
header/footer output for selected pages and is also compatible with
document-rendered header/footer flows (for example
documentitem header/footer output) by clearing the
header/footer render bands.
| Attribute | Type | Description |
|---|---|---|
action
| String | Operation to perform (see actions above) |
source
| Any | Source PDF(s) - file path, binary data, or variable |
destination
| String | Output file path |
pages
| String | Page selection: "1-5", "1,3,5", "*" |
overwrite
| Boolean | Overwrite existing files |
password
| String | Password for encrypted PDFs |
When using addHeader or addFooter actions,
the following placeholders can be used in the text attribute:
| Placeholder | Description | Example |
|---|---|---|
_PAGENUMBER
| Replaced with the current page number | "Page _PAGENUMBER" → "Page 1", "Page 2", etc. |
_LASTPAGENUMBER
| Replaced with the total number of pages | "Page _LASTPAGENUMBER" → "Page 10" (if 10 pages total) |
Combined Example: "Page _PAGENUMBER of _LASTPAGENUMBER" becomes "Page 1 of 10", "Page 2 of 10", etc.
Note: Placeholders are only supported in
addHeader and addFooter actions. They are
not available in other PDF operations like watermarks or stamps.
<bx:pdf action="merge"
destination="/combined/merged-document.pdf"
overwrite="true">
<bx:pdfparam source="/docs/doc1.pdf" />
<bx:pdfparam source="/docs/doc2.pdf" />
<bx:pdfparam source="/docs/doc3.pdf" />
</bx:pdf>
<bx:pdf action="addWatermark"
source="/documents/report.pdf"
destination="/documents/watermarked-report.pdf"
image="/images/confidential.png"
opacity="0.3"
position="center"
rotation="45">
</bx:pdf>
<bx:pdf action="extractText"
source="/documents/report.pdf"
destination="/extracted/report.txt"
pages="1-10">
</bx:pdf>
<bx:pdf action="protect"
source="/documents/sensitive.pdf"
destination="/documents/protected.pdf"
newOwnerPassword="admin123"
newUserPassword="user123"
permissions="print,copy">
</bx:pdf>
<bx:pdf action="addFooter"
source="/documents/report.pdf"
destination="/documents/report-paginated.pdf"
text="Page _PAGENUMBER of _LASTPAGENUMBER"
align="center"
pages="*">
</bx:pdf>
<bx:pdf action="addHeader"
source="/documents/report.pdf"
destination="/documents/report-with-header.pdf"
text="CONFIDENTIAL - Page _PAGENUMBER"
align="right"
pages="*">
</bx:pdf>
Specifies parameters for PDF operations, particularly for merge and attachment operations.
| Attribute | Type | Description |
|---|---|---|
source
| String | Source file path |
filename
| String | Attachment filename |
encoding
| String | File encoding (default: UTF-8) |
mimeType
| String | File MIME type |
description
| String | Attachment description |
password
| String | Password for encrypted files |
<bx:pdf action="addAttachments"
source="/base/document.pdf"
destination="/enhanced/document.pdf">
<bx:pdfparam source="/files/spreadsheet.xlsx"
filename="data.xlsx"
mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
description="Supporting data analysis" />
<bx:pdfparam source="/images/chart.png"
filename="sales-chart.png"
mimeType="image/png"
description="Q4 Sales Performance Chart" />
</bx:pdf>
The PDF renderer has two header/footer paths. The main HTML document is laid out by Flying Saucer, while document-section headers and footers are rendered separately and imported as PDF form content. This distinction matters when adjusting vertical spacing.
PDF.java, the non-explicit, non-table-header branch
controls the top page reservation through
effectiveMarginTop. The current compatibility value is
0.50in, followed by the renderer's normal compatibility
inset. Increase it to move ordinary body content down; decrease it
to move the body up.PDFBoxUtil.java,
DOCSECTION_HEADER_ZONE_HEIGHT_PT controls the compact
form zone for ordinary headers. Table headers are detected
separately and use a 72pt zone so multi-line table
labels are not clipped. Do not reduce the table zone to fix a plain
one-line header: that breaks reports such as the franchise-code report.DOCSECTION_HEADER_FOOTER_FONT_SIZE_PT controls the
default inherited document-section header/footer text size. Authored
inline font-size values should remain in the fragment
and should not be replaced by a tag-specific or italic-specific rule.DOCSECTION_HEADER_TABLE_FONT_SIZE_PT controls table
header cells in the separately rendered header form. It affects the
table header row, not the body table.padding-top: 6pt for the inherited typography path.
This changes where the header text sits inside its form; it is
separate from the page's body reservation.DOCSECTION_FOOTER_BOTTOM_EDGE_INSET and
DOCSECTION_FOOTER_Y_OFFSET_PT control the
document-section footer's bottom placement. Adjust these for footer
position rather than changing the body top margin..bx-pdf-content, .bx-pdf-section, or
body are not reliable fixes for this renderer path.-fs-transform experiment used
during calibration did not reliably move the section content and
should not be used as the primary body-spacing control.testDefaultMarginWithHeaderFooterUsingDocumentSection
and inspect the rendered PNG against its golden image.effectiveMarginTop in PDF.java and rerender.PDFBoxUtil.java; do not use the
plain-header margin branch.testFranchiseCodeReport after every header-zone
change. It is the control for multi-line table-header clipping.The focused visual tests are in
src/test/java/ortus/boxlang/modules/pdf/ai/PDFRenderingComparisonTest.java.
The PNGs use different render scales in some comparison paths, so
compare normalized page coordinates rather than raw pixel coordinates alone.
The Gradle build has subtle pitfalls that waste hours if you don't know about them. Run these commands in order before testing any source change:
# 1. Generate META-INF/services entries (component/BIF registration)
.\gradlew serviceLoaderBuild
# 2. Build the regular JAR (NOT shadowJar — it strips the service files)
.\gradlew clean jar
# 3. Copy the JAR into the module directory the test loads from
.\gradlew createModuleStructure
# 4. Verify the service file is in the module JAR
jar tf build\module\libs\bx-pdf-1.6.4-snapshot.jar | Select-String "ortus.boxlang.runtime.components"
# Expected: META-INF/services/ortus.boxlang.runtime.components.Component
Why the regular jar task, not shadowJar:
shadowJar produces
build/libs/bx-pdf-1.6.4-snapshot.jar but does
not include the META-INF/services files generated
by serviceLoaderBuild. Without those files,
cfdocument and other components throw Component
[document] could not be found at runtime.jar task picks up the service files from
build/resources/main/META-INF/services/ correctly.createModuleStructure copies the JAR from
build/libs/ into build/module/libs/ where
the test loads it from.
Compile only is NOT enough. Running only
.\gradlew compileJava updates
build/classes/ but the test runtime loads from
build/module/libs/. Without rebuilding and re-copying the
JAR, your code changes will not run.
Symptoms of a stale build:
passed=2 failed=0 but the image is clearly wrong — AI
judge is unreliable; always visually compare the actual PNG to the goldenVerifying the build is current:
# Check JAR build time matches source change time
Get-Item build\module\libs\bx-pdf-1.6.4-snapshot.jar | Select-Object LastWriteTime
Get-Item build\classes\java\main\ortus\boxlang\modules\pdf\types\PDF.class | Select-Object LastWriteTime
Working test cycle:
# Edit source
# Then build:
.\gradlew clean jar
.\gradlew createModuleStructure
# Then run test via VS Code's runTests tool
The AI judge in the visual test is unreliable — sometimes it reports
pass=false even when the image matches the golden. Always
visually compare the rendered PNG to the golden yourself before making changes.
<bx:set testImage = "https://ortus-public.s3.amazonaws.com/logos/ortus-medium.jpg"/>
<bx:document format="pdf"
filename="/reports/annual-report.pdf"
pageType="A4"
orientation="portrait"
encryption="128-bit"
openpassword="company2024"
fontEmbed="true">
<!-- Global Header -->
<bx:documentitem type="header">
<div style="text-align: center; border-bottom: 2px solid #333; padding: 10px;">
<h2>Annual Business Report 2024</h2>
</div>
</bx:documentitem>
<!-- Global Footer -->
<bx:documentitem type="footer">
<div style="text-align: center; font-size: 10px;">
Page #bxdocument.currentpagenumber# of #bxdocument.totalpages#
</div>
</bx:documentitem>
<!-- Executive Summary Section -->
<bx:documentsection name="Executive Summary">
<h1>Executive Summary</h1>
<p>This report provides an overview of our performance...</p>
</bx:documentsection>
<!-- Charts Section -->
<bx:documentsection name="Performance Charts" src="#testImage#" />
</bx:document>
<!-- Step 1: Merge quarterly reports -->
<bx:pdf action="merge" destination="/temp/combined-quarterly.pdf">
<bx:pdfparam source="/reports/q1-2024.pdf" />
<bx:pdfparam source="/reports/q2-2024.pdf" />
<bx:pdfparam source="/reports/q3-2024.pdf" />
<bx:pdfparam source="/reports/q4-2024.pdf" />
</bx:pdf>
<!-- Step 2: Add company watermark -->
<bx:pdf action="addWatermark"
source="/temp/combined-quarterly.pdf"
destination="/temp/watermarked-report.pdf"
image="/assets/company-watermark.png"
opacity="0.2"
pages="*">
</bx:pdf>
<!-- Step 3: Add password protection -->
<bx:pdf action="protect"
source="/temp/watermarked-report.pdf"
destination="/final/annual-report-protected.pdf"
newOwnerPassword="admin2024"
newUserPassword="view2024"
permissions="print">
</bx:pdf>
This repo includes a test-only GitHub Models integration stub for comparing two static images:
src/test/java/ortus/boxlang/modules/pdf/ai/BaseAIIntegrationTest.java
src/test/java/ortus/boxlang/modules/pdf/ai/PDFRenderingComparisonTest.java
The test is opt-in and skipped by default.
For VS Code test runs, the test helper now auto-loads a repo-root
.env file if it exists.
BX_PDF_AI_TESTS=true to enable the AI integration testsBX_PDF_AI_TOKEN (preferred) or BX_PDF_AI_GITHUB_TOKEN
Optional:
BX_PDF_AI_MODEL (default: gpt-4.1)BX_PDF_AI_ENDPOINT (default: https://models.inference.ai.azure.com/chat/completions)For LM Studio (OpenAI-compatible local server), set:
BX_PDF_AI_ENDPOINT=http://localhost:1234/v1/chat/completions
If /chat/completions is configured by mistake, the test
helper now retries once against /v1/chat/completions when
the server reports an endpoint mismatch.
.env.example to .env
BX_PDF_AI_TOKEN
BX_PDF_AI_TESTS=true
No shell export is required for VS Code because the test helper reads
the local .env file automatically.
$env:BX_PDF_AI_TESTS = "true"
$env:BX_PDF_AI_TOKEN = "<your-token>"
./gradlew compileTestJava
This shell-based approach is optional now; it is only needed if you do not want to use the local properties file.
Set the BX_PDF_AI_TOKEN secret in CI and run the same
test suite. BX_PDF_AI_TESTS is already enabled in the
reusable tests workflow for CI. The API call path is identical between
local and CI; only the credential source changes.
<!-- Process submitted form data -->
<bx:pdfform action="read"
source="/submissions/application-#url.id#.pdf"
result="applicationData" />
<!-- Validate and process data -->
<bx:if applicationData.status eq "pending">
<!-- Update form with approval stamp -->
<bx:pdfform action="populate"
source="/submissions/application-#url.id#.pdf"
destination="/processed/approved-application-#url.id#.pdf"
overwriteData="true">
<bx:pdfformparam name="status" value="APPROVED" />
<bx:pdfformparam name="approvedBy" value="#session.user.name#" />
<bx:pdfformparam name="approvalDate" value="#dateFormat(now(), 'yyyy-mm-dd')#" />
</bx:pdfform>
</bx:if>
This module provides full compatibility with CFML PDF tags:
cfdocument → bx:document
cfdocumentitem → bx:documentitem
cfdocumentsection → bx:documentsection
cfpdf → bx:pdf
cfpdfparam → bx:pdfparam
cfpdfform → bx:pdfform
cfpdfformparam → bx:pdfformparam
Migration from ColdFusion requires only prefix changes
(cf → bx).
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
<bx:document>/<cfdocument> content no longer abort PDF generation. Broken local file paths, unresolvable remote hosts, HTTP 4xx/5xx responses, and malformed image URIs are now replaced with a small red-X placeholder (matching ColdFusion's behaviour) and the rest of the document renders normally.sign, processddx and removeWatermarksource attribute now accepts an in-memory PDFDocument reference or raw byte[], and resolves bare variable names to a PDFDocument or byte[] in scope before treating the string as a file path. Mutations made to an in-memory PDFDocument are preserved across chained actions (no disk round-trip).top, bottom, middle) in the align attribute of the PDF component for backwards compatibility with Adobe ColdFusionpages attribute is no longer required for the addHeader and addFooter PDF component actionsPDFDocument no longer throws an NPE when extracting document info from an in-memory document with no file metadataPDFDocument instances handed off to a caller variable via the name attribute are no longer closed by the component, so they can be reused in subsequent actionsisPDFFile and isPDFObject validation BIFsPDF Component - Boxlang+ license requiredPDFForm Component - Boxlang+ license requiredPDFFormParam Component - Boxlang+ license required<<<<<<< HEAD
=======
development
$
box install bx-pdf