<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jasper | PDFluent</title>
    <description>The latest articles on DEV Community by Jasper | PDFluent (@jasper_de_winter).</description>
    <link>https://dev.to/jasper_de_winter</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3781039%2Fe00e35a5-cffc-44f5-b077-2154a7d01a00.jpg</url>
      <title>DEV Community: Jasper | PDFluent</title>
      <link>https://dev.to/jasper_de_winter</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jasper_de_winter"/>
    <language>en</language>
    <item>
      <title>My Journey so far (beating Docling &amp; PyMuPDF4LLM)</title>
      <dc:creator>Jasper | PDFluent</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:31:17 +0000</pubDate>
      <link>https://dev.to/jasper_de_winter/my-journey-so-far-beating-docling-pymupdf4llm-4nl9</link>
      <guid>https://dev.to/jasper_de_winter/my-journey-so-far-beating-docling-pymupdf4llm-4nl9</guid>
      <description>&lt;p&gt;SDK 1.2 of PDFluent went out on 22 October. That's about nine months of work, by me, a solo founder in the Netherlands.&lt;/p&gt;

&lt;p&gt;The release post has the changelog and the numbers: &lt;a href="https://pdfluent.com/blog/sdk-1-2-0/" rel="noopener noreferrer"&gt;https://pdfluent.com/blog/sdk-1-2-0/&lt;/a&gt;. This post is about the other stuff, the things that never make it into release notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  how it started
&lt;/h2&gt;

&lt;p&gt;At the start I had no plan to write a PDF engine. The plan was lazier. There are a lot of good open source PDF tools out there, and I thought I could glue them together into one complete PDF product, call it done, and get to the part where people actually use it.&lt;/p&gt;

&lt;p&gt;That lasted [JASPER: how long the glue phase lasted, and the specific tool or bug that made you stop gluing and start writing]. After that I started writing most of the core myself. Not all of it. Some parts are forks of existing crates, and I'm glad those crates exist. But most of the core.&lt;/p&gt;

&lt;p&gt;The first thing I wanted to do properly was XFA. If you've never run into XFA, lucky you. It's Adobe's old dynamic form format, version 3.3, and it still lives inside tax and government systems where nobody is about to replace it. I wanted it working in Rust, properly. [JASPER: one real XFA moment, like the first form that calculated correctly, or the part of the spec that cost you a week]&lt;/p&gt;

&lt;p&gt;From there it kept growing, which is what happens when you're one person and nobody tells you to stop. It became an SDK in Rust with bindings for Python, Node.js, .NET, Java and WebAssembly. There's a free desktop PDF editor built on the same engine. The SDK is AGPLv3 or a commercial licence. Then it grew sideways into extraction: getting text and structure out of PDF and Office files, so layout, tables, Markdown, the stuff people want to feed into LLMs now. That's when I started measuring against Docling and pymupdf4llm, because those were the two I wanted to beat.&lt;/p&gt;

&lt;h2&gt;
  
  
  the things I got wrong
&lt;/h2&gt;

&lt;p&gt;I had written "pure Rust" everywhere. Website, README, probably in my sleep too. Then I found a C library in the build. QuickJS, for running form scripts. [JASPER: how you found out, e.g. a dependency tree, a licence scan, a build on a new machine] So it wasn't pure Rust, and I had been saying it was, in public. I pulled the claim from the website. In 1.2 form scripts run in a WebAssembly sandbox, and the copy says "written in Rust" now, which is true and boring, and I'll take that.&lt;/p&gt;

&lt;p&gt;The benchmarks were worse, in a way. My first numbers came from the same half of the data I had been tuning on. Of course they looked good. I was grading my own homework with the answer sheet open next to it. I threw those numbers out and replaced them with results on a held-out half, data I don't touch while building.&lt;/p&gt;

&lt;p&gt;Then the tests. I found functions that had been broken for months while tests for them existed the whole time. The tests were fine. Nothing in the CI pipeline ever ran them. A test file sitting in a folder feels nice and does nothing. So the rule now is boring: if a test doesn't run in CI, the work isn't done.&lt;/p&gt;

&lt;p&gt;After the pure Rust thing I got a bit paranoid, which I think is healthy. Every number on the website now has an ID in a claims file. No ID, it doesn't ship. It slows me down. Fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  where it is now
&lt;/h2&gt;

&lt;p&gt;This is the part I'm a bit nervous to write. I believe PDFluent is now state of the art at extracting text from PDF and Office documents, layout included. That's my belief, and you should check it yourself.&lt;/p&gt;

&lt;p&gt;Here are the numbers behind it. It's the average over 19 extraction measures on the holdout half, PDF only. PDFluent ran in balanced mode with the table model, which is still Preview. &lt;/p&gt;

&lt;p&gt;PDFluent 0.721 &lt;br&gt;
pymupdf4llm 0.693 &lt;br&gt;
Docling 0.672&lt;br&gt;
(Yes, I'm pretty proud of this result)&lt;/p&gt;

&lt;p&gt;The harness is public, pdfluent-bench on GitHub (github.com/pdfluent/pdfluent-bench), so if you think I set it up in my own favour, go and look, and tell me where.&lt;/p&gt;

&lt;p&gt;Traction is small. About 38 GitHub stars and a few hundred PyPI downloads a month. I'm not going to dress that up. After nine months, that's where it is.&lt;/p&gt;

&lt;p&gt;If any of this is useful to you, use it, and let me know. Feedback, ideas, someone who wants to work on it together, all welcome. The full list of what changed in 1.2 is at &lt;a href="https://pdfluent.com/blog/sdk-1-2-0/" rel="noopener noreferrer"&gt;https://pdfluent.com/blog/sdk-1-2-0/&lt;/a&gt; if you want the dry version;-)&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>softwaredevelopment</category>
      <category>startup</category>
    </item>
    <item>
      <title>XFA forms won't die, so I wrote a flattener for them in pure Rust</title>
      <dc:creator>Jasper | PDFluent</dc:creator>
      <pubDate>Wed, 23 Sep 2026 15:07:12 +0000</pubDate>
      <link>https://dev.to/jasper_de_winter/xfa-forms-wont-die-so-i-wrote-a-flattener-for-them-in-pure-rust-2oo6</link>
      <guid>https://dev.to/jasper_de_winter/xfa-forms-wont-die-so-i-wrote-a-flattener-for-them-in-pure-rust-2oo6</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Here's a rough sketch of what using it looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;pdfluent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;pdfluent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PdfDocument&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"input.pdf"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// simplified for illustration; check the docs for the exact&lt;/span&gt;
    &lt;span class="c1"&gt;// XFA-flatten call and its feature flag&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="nf"&gt;.has_xfa&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="nf"&gt;.flatten_xfa_forms&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="nf"&gt;.save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output.pdf"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm simplifying on purpose here. &lt;code&gt;PdfDocument&lt;/code&gt; and the &lt;code&gt;?&lt;/code&gt;-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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;If you're stuck with an XFA form right now, there's a WebAssembly build of the flattener running at the &lt;a href="https://pdfluent.com/playground/xfa" rel="noopener noreferrer"&gt;PDFluent playground&lt;/a&gt;. No signup, nothing uploaded anywhere: drag in a form and watch it flatten in the browser. For the Rust side, the &lt;a href="https://pdfluent.com/docs/rust" rel="noopener noreferrer"&gt;quickstart docs&lt;/a&gt; are one &lt;code&gt;cargo add pdfluent&lt;/code&gt; away.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>pdf</category>
      <category>webassembly</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building a PDF engine from scratch in Rust</title>
      <dc:creator>Jasper | PDFluent</dc:creator>
      <pubDate>Sat, 22 Aug 2026 20:30:11 +0000</pubDate>
      <link>https://dev.to/jasper_de_winter/building-a-pdf-engine-from-scratch-in-rust-4opk</link>
      <guid>https://dev.to/jasper_de_winter/building-a-pdf-engine-from-scratch-in-rust-4opk</guid>
      <description>&lt;h2&gt;
  
  
  Why build another PDF engine
&lt;/h2&gt;

&lt;p&gt;Hi Folks, my name is Jasper. I spent the last year writing a PDF engine from scratch in Rust. Not wrapping an existing C++ library, not forking an open source project - 48 pure Rust crates with zero C or C++ dependencies. The SDK is now solid, and a new version of the free desktop editor ships next week. Here's what broke along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The brutal parts
&lt;/h2&gt;

&lt;p&gt;XFA forms were the first obstacle. Adobe deprecated XFA in PDF 2.0, but deprecated doesn't mean gone. XFA has its own layout model, scripting language, and data binding - all documented in a 756-page specification. Implementing it meant writing a JavaScript interpreter (QuickJS) for form calculations while ensuring scripts in documents never execute.&lt;/p&gt;

&lt;p&gt;Rendering was another challenge. Existing Rust PDF crates only parsed files, so I had to build a rasterizer using vello_cpu. The first PDF/A conversions passed validation while quietly dropping content. The validator checked structure, not whether text was preserved. I caught it by counting words with mutool against the original - one escaped backslash cost an entire page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works now
&lt;/h2&gt;

&lt;p&gt;After processing 50,000 real PDFs with zero crashes, I measured rendering performance on a 5,000-document test set and PDF/A compliance on a separate 1,000-document holdout set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;94% render at SSIM 0.95 or better against mutool (mean SSIM 0.9869)&lt;/li&gt;
&lt;li&gt;0.16% crash rate (8 crashes), all on adversarial fuzzing input&lt;/li&gt;
&lt;li&gt;98.9% PDF/A-2b conformance on a 1,000-document holdout set&lt;/li&gt;
&lt;li&gt;Text retention median 100.0%, 5th percentile 99.3%, none below 95%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These numbers are reproducible using veraPDF and mutool against the published benchmarks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing traps I fell into
&lt;/h2&gt;

&lt;p&gt;A binding in the WebAssembly build had been broken since May. A test for it existed but wasn't running in CI - like 198 other method tests sitting unused. Now nothing ships without its test running in the pipeline.&lt;/p&gt;

&lt;p&gt;Three "passing" tests didn't actually test anything: one used ASCII fixtures that never hit Unicode paths, another counted uncompressed streams as zero, and a third positioned text where no blocks were detectable. The rule now: break the function on purpose and see if the suite notices.&lt;/p&gt;

&lt;p&gt;Silent test skips hid problems too. A duplicated character-mapping table survived because missing fixtures failed quietly. Now any skip must log to stderr.&lt;/p&gt;

&lt;h2&gt;
  
  
  The privacy approach
&lt;/h2&gt;

&lt;p&gt;The editor makes three outbound connections: update checks, opt-in crash reports, and opening links in your browser. Document content never leaves your machine. You can verify this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Monitor network traffic while using the tool&lt;/li&gt;
&lt;li&gt;Check that reqwest only appears in the updater (&lt;code&gt;cargo tree -i reqwest&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Compare against my published packet capture&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The limitation: TLS obscures content, and unpublished source can't be verified. You can run this same check on any PDF tool you use to compare the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The new editor version removes non-functional e-signature controls and fixes save-state indicators. It stays free for all uses, no accounts or telemetry. The SDK licenses start at €699 with a 30-day evaluation. The SDK runs with all features enabled but adds a watermark to the output until you apply a license key.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>rust</category>
      <category>software</category>
    </item>
  </channel>
</rss>
