<turbo-stream action="append" target="posts_list"><template>    <div class="postbit" id="393155" data-post-id="393155">
  <section>
    <div class="post-wrap">


					<div class="post-header">
		        <div class="user-avatar">
		          <img alt="ManuelGarcia" src="/assets/icons/user-9f439610.png" width="120" height="120" />
		        </div>
					
						<div class="user-details">
		          <div class="user-name">
		            <h3>
                  ManuelGarcia
                    <span class="op-star" title="Thread Starter">
                      <img alt="OP" class="op-star-icon" src="/assets/thread-icons/thread-icon-thread-starter-df91e872.png" />
                    </span>
                  </h3>
		          </div>
						
						</div>
					
					</div>

	        <div class="thread-main">
	            <div class="post-body" data-turbo="false">
								<h3><a name="p-393155-paperforge-145-production-hardening-1" class="anchor" href="#p-393155-paperforge-145-production-hardening-1" aria-label="Heading link" rel="nofollow"></a>PaperForge 1.4.5 — Production Hardening</h3>
<p>I’ve just released <strong>PaperForge 1.4.5</strong>.</p>
<p>The last few releases have followed a fairly deliberate progression.</p>
<p>1.4.3 made rendering easier to understand through validation and diagnostics.</p>
<p>1.4.4 focused on doing less work, allocating less memory, and avoiding repeated computation.</p>
<p>1.4.5 asks the next question:</p>
<p><strong>What happens when PaperForge runs for a long time, under load, or receives input it should not trust?</strong></p>
<p>This release is focused on production hardening.</p>
<p>PDF parsing and writing now support configurable limits for object counts, source size, uncompressed stream data, and nested PDF value depth. Import, diagnostic rendering, and export operations can also run with bounded timeouts.</p>
<p>The goal is that an unexpectedly large, malformed, or hostile document should produce a structured failure rather than consume an unreasonable amount of CPU or memory.</p>
<p>PaperForge now has a stable <code>PF8xxx</code> family of runtime errors for:</p>
<ul>
<li>timeouts;</li>
<li>cancellation;</li>
<li>resource-limit violations;</li>
<li>isolated worker failures;</li>
<li>circuit-open resources;</li>
<li>concurrent job failures.</li>
</ul>
<p>These errors are structured and include operation metadata, so they can be consumed by application logs, APIs, monitoring systems, and eventually the Problems panel in PaperForge Studio.</p>
<p>I also added a pure-Elixir circuit breaker for resources that fail repeatedly during PDF processing.</p>
<p>It supports configurable failure thresholds and recovery windows, clears failures after healthy calls, expires inactive entries, and keeps its internal state bounded. Expected document-validation errors do not incorrectly trip the circuit.</p>
<p>Testing was another major part of this release.</p>
<p>PaperForge now has:</p>
<ul>
<li>writer → binary → parser round-trip tests;</li>
<li>StreamData properties for parser invariants;</li>
<li>declarative atom-safety properties;</li>
<li>mutated-PDF fuzz testing;</li>
<li>an opt-in <code>mix test.fuzz</code> task;</li>
<li>soak-test profiles;</li>
<li>timeout and cancellation tests;</li>
<li>crash-isolation coverage;</li>
<li>concurrent determinism checks;</li>
<li>temporary-file cleanup tests;</li>
<li>hostile-input and resource-limit tests.</li>
</ul>
<p>The current release suite includes <strong>298 regular tests</strong>, including <strong>3 property tests</strong>, plus <strong>2 dedicated fuzz-profile tests</strong>.</p>
<p>CI tests Elixir 1.20.2 on both OTP 28 and OTP 29, with warnings treated as errors.</p>
<p>One interesting issue appeared during the release.</p>
<p>The OTP 28 job found a failure after StreamData generated this mutation sequence:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">[:truncate, :truncate, :truncate, :truncate, :truncate, :truncate, :flip]
</code></pre>
<p>The parser itself was not failing incorrectly. The fuzz mutator had shortened the PDF until the following byte-flip operation selected a position outside the remaining binary.</p>
<p>That was a useful reminder that fuzz infrastructure must be just as defensive as the code it tests. The mutator is now total for every binary size, and the exact reduced counterexample is preserved as a deterministic regression test.</p>
<p>This release also includes a fix prompted by the first external issue report against PaperForge.</p>
<p>Text inside table cells could cross into the row above because one rendering path treated the top of the cell as if it were the PDF text baseline. Table rendering now calculates a font-aware baseline and consistently applies padding and vertical alignment for both supported coordinate origins.</p>
<p>A minimal reproduction from a real user made this much easier to isolate and verify. That is exactly the kind of feedback I hoped to receive by sharing PaperForge here.</p>
<p>Internally, I also refactored several large areas:</p>
<ul>
<li>classic PDF parsing;</li>
<li>object and stream resolution;</li>
<li>writer finalization;</li>
<li>declarative expansion;</li>
<li>CSS import;</li>
<li>TrueType cmap parsing;</li>
<li>EXIF orientation handling;</li>
<li>validation;</li>
<li>table rendering.</li>
</ul>
<p>Strict Credo findings were reduced from <strong>58 to 31</strong> without disabling checks or adding suppressions.</p>
<p>The documentation was consolidated as well. Several overlapping files were removed in favor of the README and a smaller set of focused guides.</p>
<p>The theme of 1.4.5 is:</p>
<p><strong>fail predictably, recover cleanly, and remain stable under pressure.</strong></p>
<h2><a name="p-393155-what-comes-next-paperforge-150-2" class="anchor" href="#p-393155-what-comes-next-paperforge-150-2" aria-label="Heading link" rel="nofollow"></a>What comes next: PaperForge 1.5.0</h2>
<p>The next release will take PaperForge in a new direction.</p>
<p>Until now, its main workflow has been:</p>
<pre><code class="lang-plaintext">Elixir → PaperForge → PDF
</code></pre>
<p>PaperForge 1.5.0 will begin establishing:</p>
<pre><code class="lang-plaintext">Existing PDF → PaperForge → Analysis or transformation → PDF
</code></pre>
<p>The working theme is:</p>
<p><strong>PDF Interoperability &amp; Transformation Engine.</strong></p>
<p>The goal is to expand the existing classic PDF parser into a deeper PDF object-graph engine capable of understanding documents produced by other systems.</p>
<p>The planned work includes:</p>
<ul>
<li>cross-reference streams;</li>
<li>compressed object streams;</li>
<li>hybrid-reference files;</li>
<li>incremental updates;</li>
<li>multiple object generations;</li>
<li>chained trailers;</li>
<li>inherited page resources;</li>
<li>additional stream filters;</li>
<li>supported encrypted PDFs when a password is provided.</li>
</ul>
<p>I also want to introduce an API along these lines:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">{:ok, pdf} = PaperForge.open("contract.pdf")

PaperForge.page_count(pdf)
PaperForge.metadata(pdf)
PaperForge.forms(pdf)
PaperForge.annotations(pdf)
PaperForge.attachments(pdf)
PaperForge.outlines(pdf)
PaperForge.signatures(pdf)
</code></pre>
<p>Another important part will be structured PDF analysis:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">{:ok, analysis} = PaperForge.analyze(pdf)
</code></pre>
<p>That analysis should be able to report:</p>
<ul>
<li>PDF version and file structure;</li>
<li>page and object counts;</li>
<li>cross-reference type;</li>
<li>compressed objects;</li>
<li>incremental revisions;</li>
<li>fonts and images;</li>
<li>duplicate resources;</li>
<li>broken or orphaned references;</li>
<li>forms and annotations;</li>
<li>attachments;</li>
<li>signatures;</li>
<li>encryption and permissions;</li>
<li>tagged-PDF information;</li>
<li>normalization opportunities.</li>
</ul>
<p>I’m also planning a command-line inspector:</p>
<pre data-code-wrap="shell"><code class="lang-shell">mix paper_forge.inspect document.pdf
</code></pre>
<p>The longer-term transformation API should make workflows like this possible:</p>
<pre data-code-wrap="elixir"><code class="lang-elixir">"input.pdf"
|&gt; PaperForge.open!()
|&gt; PaperForge.delete_pages(10..15)
|&gt; PaperForge.rotate_pages([2, 3], 90)
|&gt; PaperForge.add_watermark("CONFIDENTIAL")
|&gt; PaperForge.update_metadata(title: "Reviewed report")
|&gt; PaperForge.write!("output.pdf")
</code></pre>
<p>That creates a path toward merging, splitting, extracting, reordering, normalizing, and incrementally editing existing PDFs.</p>
<p>I don’t expect 1.5.0 to repair every malformed PDF ever produced. PDF compatibility is a very large problem, and real-world files will undoubtedly reveal assumptions that synthetic fixtures cannot.</p>
<p>The priority is to establish a reliable parser, object graph, inspection API, and transformation foundation.</p>
<p>So if you have unusual PDFs, compressed-object files, incrementally updated documents, old reports, forms, signed documents, or files generated by less common tools, I would be very interested in representative samples and compatibility reports.</p>
<p>As always, thank you for the feedback. PaperForge is becoming much better because people are trying it against real documents rather than only the examples I created for it.</p> 
	            </div>

	            <div class="base-line">
	                <div class="thread-counters">
	                    <span class="thread-count count-likes js-likers-trigger" title="Likes" data-post-id="393155" data-batch-url="/posts/batch_likers">
                        2
                      </span>
                      <!-- <span class="thread-count js-solved-indicator" title="Marked as solution"></span> -->
	                </div>
	                <div class="go-to-post">
	                  <a title="Go to post" alt="Go to post" href="https://forum.elixirforum.com/t/paperforge-a-pure-elixir-platform-for-building-securing-and-transforming-pdfs/76241/12">Post #11</a>
	                </div>
	            </div>
              <div id="likers-container-393155" 
                   class="likers-container"
                   data-first-post="false"
                   data-batch-url="/posts/batch_likers">
                   <div class="likers-placeholder" 
                     data-likers-post-id="393155"
                     data-batch-url="/posts/batch_likers">
                  <div class="post-likers"></div>
                </div>
              </div>
	        </div>
			

    </div>

    <div class="triangle-top-right type-last-post cat-last-post" title="Last post!"></div>
  </section>
</div>
</template></turbo-stream><turbo-stream action="replace" target="load-more-container"><template><div id="load-more-container" class="load-more-container">
    <span class="all-loaded">— All posts loaded —</span>
</div></template></turbo-stream>