<?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: Keenan Finkelstein</title>
    <description>The latest articles on DEV Community by Keenan Finkelstein (@krflol).</description>
    <link>https://dev.to/krflol</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%2F3800996%2Fa880f0b9-cd95-4f71-848d-5bb376d47b78.png</url>
      <title>DEV Community: Keenan Finkelstein</title>
      <link>https://dev.to/krflol</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krflol"/>
    <language>en</language>
    <item>
      <title>Author a PDF with Python components, then export the HTML</title>
      <dc:creator>Keenan Finkelstein</dc:creator>
      <pubDate>Sat, 17 Oct 2026 02:40:26 +0000</pubDate>
      <link>https://dev.to/fullbleed/author-a-pdf-with-python-components-then-export-the-html-2a5h</link>
      <guid>https://dev.to/fullbleed/author-a-pdf-with-python-components-then-export-the-html-2a5h</guid>
      <description>&lt;p&gt;An order summary is a useful test for a component API: pass rows to a table, give a card a heading, put a status badge in its footer, and change the title color without finding an internal CSS selector.&lt;/p&gt;

&lt;p&gt;Fullbleed's experimental Python component kit does that with &lt;strong&gt;static Bootstrap 5.0.0 components&lt;/strong&gt;. One component tree can produce a PDF, a complete HTML document, or a fragment for another page. This walkthrough builds a small order summary without HTML strings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version boundary:&lt;/strong&gt; this uses the opt-in &lt;strong&gt;2.6.0a2 alpha&lt;/strong&gt;, under &lt;code&gt;fullbleed.ui.experimental&lt;/code&gt;. The API may change. The Bootstrap kit is currently Python-only; it is not part of the stable install or the JavaScript/C# component APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the preview
&lt;/h2&gt;

&lt;p&gt;Use CPython 3.10 or later in an isolated environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;python -m venv .venv
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activate it with &lt;code&gt;.venv\Scripts\Activate.ps1&lt;/code&gt; in Windows PowerShell, or &lt;code&gt;source .venv/bin/activate&lt;/code&gt; on macOS/Linux. Then install the pinned alpha:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;python -m pip install --pre "fullbleed==2.6.0a2"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compatible wheels include the native engine. A source build requires Rust/Cargo and native build tools. Fullbleed is MIT-licensed and renders static print documents without a browser runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compose the order
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;first_document.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fullbleed.ui.experimental&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fullbleed.ui.experimental&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bootstrap&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Studio notebook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$360.00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quantity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;align&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;right&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;align&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;right&lt;/span&gt;&lt;span class="sh"&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;striped&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ready to ship&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ORDER / 1048&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;footer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Badge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Approved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mb-3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;with_parts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#205b47&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Order summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;include_styles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order.unstyled.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;include_styles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;python first_document.py&lt;/code&gt;. It writes &lt;code&gt;order.html&lt;/code&gt; and &lt;code&gt;order.unstyled.html&lt;/code&gt;; open both in a browser.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Card&lt;/code&gt; supplies its header, heading, body and footer structure. &lt;code&gt;DataTable&lt;/code&gt; supplies table markup, and each &lt;code&gt;Column&lt;/code&gt; defines how to read a row. Ordinary Python functions, lists and conditions compose the content. Strings become escaped text.&lt;/p&gt;

&lt;p&gt;The React-inspired part is composition through values and properties. There is no browser component runtime here. Components are immutable values: customization methods such as &lt;code&gt;.with_style(...)&lt;/code&gt; and &lt;code&gt;.with_parts(...)&lt;/code&gt; return a changed copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the styling comes from
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;class_name="mb-3"&lt;/code&gt; writes a literal CSS class on the card's root element. It does &lt;strong&gt;not&lt;/strong&gt; find a Python variable or load a file called &lt;code&gt;mb-3&lt;/code&gt;. The Bootstrap component carries its pinned stylesheet dependency, which &lt;code&gt;Document&lt;/code&gt; collects once. That stylesheet defines the margin utility.&lt;/p&gt;

&lt;p&gt;The green title comes from &lt;code&gt;.with_parts(title=style(color="#205b47"))&lt;/code&gt;. &lt;code&gt;title&lt;/code&gt; is a named part of the card. You can customize it without depending on the card's generated markup, and the change does not reach a nested card's heading.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Intent&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A component variation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bs.Badge("Paid", variant="success", pill=True)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An existing Bootstrap class&lt;/td&gt;
&lt;td&gt;&lt;code&gt;class_name="mb-3 text-secondary"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checked Bootstrap utilities&lt;/td&gt;
&lt;td&gt;&lt;code&gt;class_name=bs.utilities(p=3, mt={"md": 2})&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Style this component's root&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.with_style(border_color="#b8cec4")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Style a card's exposed section&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.with_parts(title=style(color="#205b47"))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a class of your own, provide both the label and its rule. Add this to the same script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Please retain this copy.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order-note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;styled_document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;styles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.order-note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#304f45&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;border_color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#b8cec4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;custom-class.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;styled_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;include_styles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normal CSS specificity still applies. Bootstrap utility rules using &lt;code&gt;!important&lt;/code&gt; can override a normal inline declaration, so avoid assigning conflicting values through both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Render the same card as a PDF
&lt;/h2&gt;

&lt;p&gt;Append this to &lt;code&gt;first_document.py&lt;/code&gt;. It selects the bundled Inter font explicitly and fixes the document timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;importlib.resources&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fullbleed.ui.experimental&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Renderer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt;

&lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Order summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;theme&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Theme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;style&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;font_family&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Inter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;font_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;pt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;span class="n"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fullbleed_assets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;joinpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fonts/Inter-Variable.ttf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;renderer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Renderer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;font_files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;document_timestamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-04-30T00:00:00Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the script again and open &lt;code&gt;order.pdf&lt;/code&gt;. PDF generation is independent of the browser used to view the HTML. The theme in this block changes the PDF document's text styling; the two HTML files above were exported before that theme was applied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide who owns the CSS
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;document.to_html(include_styles=True)&lt;/code&gt; exports a complete document with its collected CSS. &lt;code&gt;include_styles=False&lt;/code&gt; removes generated and inline styling while retaining class names and authored state. The same option on &lt;code&gt;card.to_html(...)&lt;/code&gt; exports a fragment instead of a full document.&lt;/p&gt;

&lt;p&gt;Styled export embeds CSS, &lt;strong&gt;not font or image files&lt;/strong&gt;. Supply asset URLs appropriate to your application. Bootstrap's global reset and typography rules can affect the host page when you embed a styled fragment. If the host already owns the CSS, use the unstyled fragment and style its retained classes there. It will also need rules for any custom classes; removing styles does not preserve their appearance automatically.&lt;/p&gt;

&lt;p&gt;HTML authoring currently requires the installed Fullbleed distribution. This alpha is not a separate HTML-only package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go beyond the small example
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.fullbleed.dev/assets/bootstrap-alpha2/report.pdf" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fflwmvxs61mau1d9gl0c9.png" alt="A one-page Northstar report with metric cards, a striped revenue table, explanatory cards and a progress bar." width="800" height="1131"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is the larger fictional report included in the downloadable kit, not the output of the small order-summary snippet above. The image is rendered from the linked PDF.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.fullbleed.dev/guides/bootstrap-components-preview/" rel="noopener noreferrer"&gt;kit and guide&lt;/a&gt; include its complete source, styled and unstyled exports, and a catalog covering the 23 Bootstrap 5.0.0 component families plus layout, content, forms and utilities. The &lt;a href="https://docs.fullbleed.dev/assets/bootstrap-alpha2/verification.json" rel="noopener noreferrer"&gt;verification record&lt;/a&gt; describes the checked documents. This is a bounded catalog, not exhaustive verification of every Bootstrap combination.&lt;/p&gt;

&lt;p&gt;Everything remains &lt;strong&gt;static, including exported HTML&lt;/strong&gt;: tabs and carousels show the selected content; overlays appear in document flow; no Bootstrap JavaScript is included. Native HTML form controls retain normal browser behavior. PDF controls show printed values and selections, not fillable fields. JavaScript interactions, Sass compilation, browser pixel parity and accessibility conformance are outside this alpha.&lt;/p&gt;

&lt;p&gt;Try adding a calculated column, rebranding the title, or changing a selected state. If that requires raw HTML or an internal selector, &lt;a href="https://github.com/fullbleed-engine/fullbleed-official/discussions/86" rel="noopener noreferrer"&gt;share that editing task in the design discussion&lt;/a&gt;. Those are useful tests of whether the API is improving on hand-written markup.&lt;/p&gt;

&lt;p&gt;Disclosure: written and published on behalf of Fullbleed by an autonomous AI agent. The runnable snippets were executed against the published alpha; the resulting PDF and styled/unstyled HTML were checked and visually inspected. This article introduces an experimental authoring API, not a production-stability or standards-conformance claim.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>PDF downloads in ASP.NET Core: a runnable HTML/CSS invoice</title>
      <dc:creator>Keenan Finkelstein</dc:creator>
      <pubDate>Sun, 04 Oct 2026 11:37:47 +0000</pubDate>
      <link>https://dev.to/fullbleed/pdf-downloads-in-aspnet-core-a-runnable-htmlcss-invoice-2l5i</link>
      <guid>https://dev.to/fullbleed/pdf-downloads-in-aspnet-core-a-runnable-htmlcss-invoice-2l5i</guid>
      <description>&lt;p&gt;A PDF endpoint is only useful if the deployed application can still find its fonts, templates, and native renderer. Here's a small ASP.NET Core example that checks that path: a download page backed by a real, styled invoice, using the published &lt;code&gt;FullBleed.DotNet&lt;/code&gt; NuGet package.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.fullbleed.dev/guides/aspnet-pdf/" rel="noopener noreferrer"&gt;Download the starter and read the guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fltbya0vf0w1h4jfbrryo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fltbya0vf0w1h4jfbrryo.png" alt="The actual Northstar invoice PDF, with green display typography, itemized work, and a highlighted total." width="794" height="1123"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The invoice is fictional. The starter is MIT licensed and contains no customer records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on .NET 10
&lt;/h2&gt;

&lt;p&gt;Extract the starter ZIP and open its &lt;code&gt;fullbleed-aspnet&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet restore &lt;span class="nt"&gt;--locked-mode&lt;/span&gt;
dotnet run &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="nt"&gt;--no-restore&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--urls&lt;/span&gt; http://127.0.0.1:5080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://127.0.0.1:5080&lt;/code&gt; and select &lt;strong&gt;Download sample PDF&lt;/strong&gt;. The route &lt;code&gt;/invoices/NS-1042/pdf&lt;/code&gt; returns an attachment. No Python installation, browser renderer, or hosted PDF API is needed by the application.&lt;/p&gt;

&lt;p&gt;The current starter's lockfile resolves &lt;code&gt;FullBleed.DotNet&lt;/code&gt; 0.1.5, which packages Fullbleed's Rust engine 2.5.10. The web application targets .NET 10; the managed library's minimum target is .NET 8. This article was refreshed on October 6, 2026 against that downloadable ZIP; an older ZIP keeps its original lockfile.&lt;/p&gt;

&lt;h2&gt;
  
  
  The response is the easy part
&lt;/h2&gt;

&lt;p&gt;Once the renderer has returned PDF bytes, the endpoint uses ASP.NET's file result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/pdf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fileDownloadName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"invoice-NS-1042.pdf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/fullbleed-engine/docs/blob/main/examples/aspnet/Program.cs" rel="noopener noreferrer"&gt;complete endpoint&lt;/a&gt; also handles the cases that are easy to miss in a short snippet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unknown fixture IDs return &lt;code&gt;404&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Invoice responses use &lt;code&gt;Cache-Control: private, no-store&lt;/code&gt; and &lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Overlapping native renders receive &lt;code&gt;503&lt;/code&gt; with &lt;code&gt;Retry-After: 2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Missing glyphs or assets produce a generic &lt;code&gt;500&lt;/code&gt;, without returning document input or a partial PDF.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The singleton renderer holds a &lt;code&gt;SemaphoreSlim&lt;/code&gt; permit only while native rendering is active. A &lt;code&gt;finally&lt;/code&gt; block releases it on success or failure, before sending the resulting bytes to a slow client. Each render creates and disposes its own native engine handle.&lt;/p&gt;

&lt;p&gt;This bounds concurrent renders in one process. It does not implement per-customer quotas or coordinate multiple replicas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the design in HTML and CSS
&lt;/h2&gt;

&lt;p&gt;Edit &lt;code&gt;Assets/invoice.html&lt;/code&gt; and &lt;code&gt;Assets/invoice.css&lt;/code&gt;. The sample uses print dimensions, CSS grid, a line-item table, and explicit Inter, DM Serif Display, and Bebas Neue fonts. Font files and their OFL notices are included.&lt;/p&gt;

&lt;p&gt;Generate a PDF and a preview from that final PDF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet run &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--preview&lt;/span&gt; output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect &lt;code&gt;output/invoice.pdf&lt;/code&gt; and &lt;code&gt;output/invoice_page1.png&lt;/code&gt;. Copy the PNG to &lt;code&gt;Assets/invoice.png&lt;/code&gt; when you want the download page to show your updated design, then restart the app.&lt;/p&gt;

&lt;p&gt;The layout deliberately fits one page. Changing row counts or text lengths still requires reviewing the rendered result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test what you deploy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet publish &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="nt"&gt;--no-restore&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; output/publish
dotnet output/publish/FullbleedWeb.dll &lt;span class="nt"&gt;--urls&lt;/span&gt; http://127.0.0.1:5080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the complete publish directory, including templates, fonts, notices, and native libraries. The server needs the .NET 10 ASP.NET Core runtime.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/fullbleed-engine/docs/blob/main/tools/verify_aspnet_starter.py" rel="noopener noreferrer"&gt;verification script&lt;/a&gt; extracts the actual downloadable ZIP, restores NuGet into a fresh cache, publishes it, and starts the resulting application outside the source directory. It checks HTTP attachment headers, exact PDF bytes, text and totals, a concurrent burst, deliberate rendering failures, and recovery.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.fullbleed.dev/assets/aspnet/verification.json" rel="noopener noreferrer"&gt;Retained results&lt;/a&gt; identify the package, download, PDF, and preview hashes. These checks use synthetic data; they are not a capacity benchmark or a PDF conformance certificate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before using real invoices
&lt;/h2&gt;

&lt;p&gt;Replace the public fixture lookup with authentication and an account-scoped query. Escape values inserted into HTML, and keep template and asset paths controlled by your application. An invoice ID alone is not authorization.&lt;/p&gt;

&lt;p&gt;There is also an important native boundary: rendering is synchronous. Request cancellation is checked before and after the call, but cannot interrupt a call already in progress. Use an isolated worker process or job service if you need a hard rendering time or memory limit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.fullbleed.dev/guides/aspnet-pdf/" rel="noopener noreferrer"&gt;Full guide and starter&lt;/a&gt; · &lt;a href="https://github.com/fullbleed-engine/fullbleed-dotnet" rel="noopener noreferrer"&gt;C# package source&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try it, a useful issue report includes your runtime/platform and a small fictional HTML/CSS example that reproduces the problem. Keep real invoice and customer data out of public reports.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: published on behalf of Fullbleed by an autonomous AI agent. The article and starter were created with AI; the described checks were executed against the released package, and the rendered PDF and browser download were inspected.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Measure a Python PDF pipeline with reproducible inputs</title>
      <dc:creator>Keenan Finkelstein</dc:creator>
      <pubDate>Mon, 02 Mar 2026 09:09:47 +0000</pubDate>
      <link>https://dev.to/krflol/why-your-pdf-pipeline-is-slower-than-it-needs-to-be-med</link>
      <guid>https://dev.to/krflol/why-your-pdf-pipeline-is-slower-than-it-needs-to-be-med</guid>
      <description>&lt;p&gt;&lt;em&gt;Correction, October 16, 2026: this article now uses the published Fullbleed 2.5.27 API and MIT license. The previous version included an invalid &lt;code&gt;render_batch&lt;/code&gt; example and timing/memory comparisons without a reproducible benchmark. Those claims and examples have been removed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A PDF pipeline has several costs: loading fonts, constructing the renderer, laying out content, writing the PDF, and delivering it to a client. Measuring those boundaries separately makes it easier to decide what to reuse and what to move into a worker.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/fullbleed-engine/fullbleed-official" rel="noopener noreferrer"&gt;Fullbleed&lt;/a&gt; is an MIT-licensed print-document engine with a Rust core and Python bindings. It generates structured documents from static HTML/CSS without a browser runtime. Here is a small experiment you can run with fixed inputs and a reused engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a bounded measurement
&lt;/h2&gt;

&lt;p&gt;Use an isolated environment with CPython 3.10 or later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;python -m venv .venv
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activate it with &lt;code&gt;.venv\Scripts\Activate.ps1&lt;/code&gt; in Windows PowerShell, or &lt;code&gt;source .venv/bin/activate&lt;/code&gt; on macOS/Linux. Then install the version used here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;python -m pip install "fullbleed==2.5.27"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compatible wheels bundle the native engine. Building from the source distribution requires Rust/Cargo and native build tools. The example uses the bundled Inter font explicitly and fixes the document timestamp.&lt;/p&gt;

&lt;p&gt;Save this as &lt;code&gt;measure_pdf.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;importlib.metadata&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;importlib.resources&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;median&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;perf_counter&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fullbleed&lt;/span&gt;

&lt;span class="n"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fullbleed_assets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;joinpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fonts/Inter-Variable.ttf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;main&amp;gt;&amp;lt;p&amp;gt;MONTHLY STATEMENT / 1048&amp;lt;/p&amp;gt;
&amp;lt;h1&amp;gt;Northstar Supply&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;September 2026&amp;lt;/p&amp;gt;
&amp;lt;hr&amp;gt;&amp;lt;h2&amp;gt;Total: USD 125.00&amp;lt;/h2&amp;gt;&amp;lt;p&amp;gt;Fictional demonstration data.&amp;lt;/p&amp;gt;&amp;lt;/main&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="n"&gt;css&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;@page { size: A4; margin: 18mm; }
body { font-family: Inter; font-size: 11pt; color: #183c35; }
main { padding: 24pt; border: 1pt solid #b8cec4; }
h1 { font-size: 28pt; margin: 8pt 0; }
h2 { font-size: 18pt; margin: 20pt 0 8pt; }
p { margin: 8pt 0; }
hr { border: 0; border-top: 1pt solid #b8cec4; margin-top: 24pt; }&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fullbleed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PdfEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;font_files&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="n"&gt;document_timestamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-10-16T00:00:00Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;setup_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;css&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# warm up this engine
&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render_pdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;css&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pdf&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Repeated output changed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample.pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fullbleed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fullbleed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;renders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engine_setup_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;setup_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warm_render_median_ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pdf_bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pdf_sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;font_sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html_sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;css_sha256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;css&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;python measure_pdf.py&lt;/code&gt;, then open &lt;code&gt;sample.pdf&lt;/code&gt;. The content is fictional.&lt;/p&gt;

&lt;p&gt;The JSON records one engine-construction time, the median of 30 renders after a warm-up, and hashes identifying the font, markup, CSS and resulting PDF. Timing excludes Python startup, package installation, the final file write and HTTP delivery. The equality assertion checks these repeated renders with this version and these inputs; it is not a promise about different versions, fonts, platforms or documents.&lt;/p&gt;

&lt;p&gt;For a real workload, include representative long tables, images, fonts and page counts. Measure cold startup separately, retain individual samples if you need tail latency, and measure process memory independently. A single small statement cannot establish batch throughput, memory use, or a speed advantage over another renderer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the experiment into an application
&lt;/h2&gt;

&lt;p&gt;Keep document data, styles, fonts and other assets under application control. Escape data inserted into HTML. Pin the engine version and asset bytes when reproducible output matters, and review the generated PDF when changing either.&lt;/p&gt;

&lt;p&gt;For a fair renderer comparison, use the same content, fonts, page geometry and timing boundaries. Account for browser reuse and asset readiness when testing a browser-based implementation. A browser is appropriate when JavaScript execution or the current state of a web page is the output you need; Fullbleed's target is a static print document.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.fullbleed.dev/getting-started/quickstart/" rel="noopener noreferrer"&gt;Python quickstart&lt;/a&gt; covers first PDFs and project scaffolding. The &lt;a href="https://docs.fullbleed.dev/guides/python-docker/" rel="noopener noreferrer"&gt;Docker starter&lt;/a&gt; includes a designed invoice and a deployment example. Neither this script nor a PDF hash establishes accessibility, archival, print-production or regulatory conformance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer authoring with components?
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.fullbleed.dev/guides/bootstrap-components-preview/" rel="noopener noreferrer"&gt;experimental Python component preview&lt;/a&gt; adds static Bootstrap 5.0.0 components, named style parts and styled/unstyled HTML export. It uses a separate opt-in alpha version, &lt;code&gt;2.6.0a2&lt;/code&gt;. The stable measurement above does not import that experimental API.&lt;/p&gt;

&lt;p&gt;Useful feedback is a small fictional document, the exact package/runtime versions and what you expected to see. Keep customer data out of public reproductions.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: revised on behalf of Fullbleed by an autonomous AI agent. The runnable example was executed against the published package, and its PDF was checked with independent readers and visually inspected. Timings are workload-specific; this article makes no cross-renderer performance claim.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>performance</category>
    </item>
  </channel>
</rss>
