XFA is Adobe's old dynamic form format. Adobe deprecated it in PDF 2.0, and it still refuses to go away. Tax authorities and immigration offices built entire form systems on it years ago, and nobody is rewriting those systems just because a spec moved on. Most PDF tools, open source or commercial, dropped XFA support a while back. I didn't, because a decent chunk of the forms real users need to open are still XFA.
XFA has its own layout model (subforms, dynamic instances that can appear or repeat at runtime) and its own scripting language, FormCalc, for form calculations. That's what makes it dynamic: a field can resize itself, a section can add a row, a calculation can run before you even click save. It's also why so few tools bother with it. Most PDF readers just show a blank page or a wall of unrendered XML when you open one.
The approach that actually ships today is flattening: run the form's layout and its FormCalc scripts once, then bake the result into a plain static PDF. You lose the interactivity, but you get a document any PDF reader can open, print, or archive without complaint. Full interactive XFA, live and editable inside a viewer, is still experimental on my end and I wouldn't put it in front of production users yet. Flattening is the boring, reliable part, and boring and reliable is what you want for a tax form.
Getting there in Rust took longer than I expected. The layout model has enough edge cases that "just parse the XML" gets you maybe halfway, and FormCalc scripts show up with quirks nobody documents. I ended up writing and re-writing the calculation engine more times than I'd like to admit before it stopped falling over on real-world forms, not just the clean examples in the spec.
Here's a rough sketch of what using it looks like:
use pdfluent::prelude::*;
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("input.pdf")?;
// simplified for illustration; check the docs for the exact
// XFA-flatten call and its feature flag
if doc.has_xfa() {
doc.flatten_xfa_forms()?;
}
doc.save("output.pdf")?;
Ok(())
}
I'm simplifying on purpose here. PdfDocument and the ?-based error handling are real and match the actual API, but I shortened the XFA-specific method names for readability. Check the Rust quickstart linked below if you want the exact calls.
This is one piece of PDFluent, the PDF SDK I've been building in Rust, with bindings for Python, Node.js, .NET, Java, and WASM. It's AGPLv3, with a commercial license for teams that can't take on the copyleft.
If you're stuck with an XFA form right now, there's a WebAssembly build of the flattener running at the PDFluent playground. No signup, nothing uploaded anywhere: drag in a form and watch it flatten in the browser. For the Rust side, the quickstart docs are one cargo add pdfluent away.
XFA isn't disappearing on its own timeline. It disappears when someone bothers to support it properly, and until then, flattening is the honest answer.
Top comments (0)