<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.2">Jekyll</generator><link href="http://0.0.0.0:8080/feed.xml" rel="self" type="application/atom+xml" /><link href="http://0.0.0.0:8080/" rel="alternate" type="text/html" /><updated>2026-02-17T13:56:32-06:00</updated><id>http://0.0.0.0:8080/feed.xml</id><title type="html">bopm.name</title><subtitle>Random thoughts on software development, technology, and everything else.</subtitle><entry><title type="html">WkhtmlToPDF is dead. Long live WkhtmlToPDF</title><link href="http://0.0.0.0:8080/2023/08/25/WkhtmlToPDF-is-dead-long-live-WkhtmlToPDF.html" rel="alternate" type="text/html" title="WkhtmlToPDF is dead. Long live WkhtmlToPDF" /><published>2023-08-25T00:00:00-05:00</published><updated>2023-08-25T00:00:00-05:00</updated><id>http://0.0.0.0:8080/2023/08/25/WkhtmlToPDF-is-dead-long-live-WkhtmlToPDF</id><content type="html" xml:base="http://0.0.0.0:8080/2023/08/25/WkhtmlToPDF-is-dead-long-live-WkhtmlToPDF.html"><![CDATA[<p>Let’s start from the obvious. If you using WickedPDF in 2023 it’s a little bit more problematic than just having issues with PDF generation from time to time. It means that you are building your PDF markups with tables, not using modern CSS abilities like flex and grids. It must be hard. But probably it’s because dealing with PDFs was difficult the last time you had to do it. But no matter what, the time has come. You need to up your PDF game. <a href="https://github.com/wkhtmltopdf/wkhtmltopdf">wkhtmltopdf</a> is deprecated. Let’s go over your options.</p>

<h1 id="wicked-pdf">Wicked PDF</h1>
<p>So for our example, let’s consider that your app PDF controller action looks more or less like this:</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">respond_to</span> <span class="k">do</span> <span class="o">|</span><span class="nb">format</span><span class="o">|</span>
  <span class="nb">format</span><span class="p">.</span><span class="nf">pdf</span> <span class="k">do</span>
    <span class="n">pdf_html</span> <span class="o">=</span> <span class="no">ApplicationController</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">render_to_string</span><span class="p">(</span>  
      <span class="ss">template: </span><span class="s1">'templates/pdfs/my_pdf'</span><span class="p">,</span>  
      <span class="ss">formats: </span><span class="p">[</span><span class="ss">:pdf</span><span class="p">],</span>  
      <span class="ss">layout: </span><span class="s1">'layouts/pdf'</span><span class="p">,</span>  
      <span class="ss">assigns: </span><span class="p">{</span> <span class="ss">model: </span><span class="vi">@model</span> <span class="p">},</span> 
      <span class="ss">encoding: </span><span class="s1">'UTF-8'</span>  
    <span class="p">)</span>  
      
    <span class="n">pdf</span> <span class="o">=</span> <span class="no">WickedPdf</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">pdf_from_string</span><span class="p">(</span>
      <span class="n">pdf_html</span><span class="p">,</span>  
      <span class="ss">page_size: </span><span class="s1">'Letter'</span><span class="p">,</span>  
      <span class="ss">dpi: </span><span class="mi">96</span><span class="p">,</span>  
      <span class="ss">margin: </span><span class="p">{</span>  
        <span class="ss">left: </span><span class="mi">0</span><span class="p">,</span>  
        <span class="ss">right: </span><span class="mi">0</span><span class="p">,</span>  
        <span class="ss">top: </span><span class="mi">40</span><span class="p">,</span>  
        <span class="ss">bottom: </span><span class="mi">20</span>  
      <span class="p">},</span>  
      <span class="ss">header: </span><span class="p">{</span>  
        <span class="ss">content: </span><span class="n">header_html</span><span class="p">,</span>  
        <span class="ss">spacing: </span><span class="mi">10</span>  
      <span class="p">},</span>  
      <span class="ss">footer: </span><span class="p">{</span>  
        <span class="ss">left: </span><span class="s2">""</span><span class="p">,</span>  
        <span class="ss">right: </span><span class="s1">'Page [page] of [topage]'</span><span class="p">,</span>  
        <span class="ss">font_size: </span><span class="mi">8</span><span class="p">,</span>  
        <span class="ss">spacing: </span><span class="mi">10</span>  
      <span class="p">}</span>
    <span class="p">)</span> 
    <span class="n">send_data</span><span class="p">(</span><span class="n">pdf</span><span class="p">,</span> <span class="ss">filename: </span><span class="s1">'test.pdf'</span><span class="p">,</span> <span class="ss">type: </span><span class="s1">'application/pdf'</span><span class="p">,</span> <span class="ss">disposition: </span><span class="s1">'inline'</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>  	</code></pre></figure>

<h1 id="grover">Grover</h1>
<p><a href="https://github.com/Studiosity/grover">grover</a> feels almost like drop-in replacement for your WickedPDF. We only need to replace pdf generation code with something like that:</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">pdf</span> <span class="o">=</span> <span class="no">Grover</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
  <span class="n">pdf_html</span><span class="p">,</span>  
  <span class="ss">format: </span><span class="s1">'Letter'</span><span class="p">,</span>  
  <span class="ss">margin: </span><span class="p">{</span>  
      <span class="ss">left: </span><span class="mi">0</span><span class="p">,</span>  
      <span class="ss">right: </span><span class="mi">0</span><span class="p">,</span>  
      <span class="ss">top: </span><span class="s1">'40px'</span><span class="p">,</span>  
      <span class="ss">bottom: </span><span class="s1">'10px'</span>  
  <span class="p">},</span>  
  <span class="ss">headerTemplate: </span><span class="s2">"Header"</span><span class="p">,</span>  
  <span class="ss">footerTemplate: </span><span class="s2">"Page [pageNumber] of [totalPages]"</span>
<span class="p">).</span><span class="nf">to_pdf</span></code></pre></figure>

<h1 id="ferrum">Ferrum</h1>
<p>Grover is good, but it require you to have <code class="language-plaintext highlighter-rouge">puppeteer</code> in the system. What if you using <code class="language-plaintext highlighter-rouge">importmaps</code> and it makes no sense to include npm in your project for <code class="language-plaintext highlighter-rouge">puppeteer</code> only.
That is where <a href="https://github.com/rubycdp/ferrum">Ferrum</a> shines. Let’s take a look at this approach:</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="no">Ferrum</span><span class="o">::</span><span class="no">Browser</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">timeout: </span><span class="mi">7</span><span class="p">).</span><span class="nf">tap</span> <span class="k">do</span> <span class="o">|</span><span class="n">browser</span><span class="o">|</span>  
  <span class="n">browser</span><span class="p">.</span><span class="nf">content</span> <span class="o">=</span> <span class="n">html</span>  
  <span class="k">begin</span>  
    <span class="n">browser</span><span class="p">.</span><span class="nf">evaluate_async</span><span class="p">(</span><span class="sx">%(addEventListener('load', arguments[0]);)</span><span class="p">,</span> <span class="n">browser</span><span class="p">.</span><span class="nf">timeout</span><span class="p">)</span>  
  <span class="k">rescue</span> <span class="no">StandardError</span>  
    <span class="kp">nil</span>  
  <span class="k">end</span>  
  <span class="n">browser</span><span class="p">.</span><span class="nf">network</span><span class="p">.</span><span class="nf">wait_for_idle</span>  
  <span class="n">pdf</span> <span class="o">=</span> <span class="n">browser</span><span class="p">.</span><span class="nf">pdf</span><span class="p">(</span>  
    <span class="ss">format: :letter</span><span class="p">,</span>  
    <span class="ss">encoding: :binary</span><span class="p">,</span>  
    <span class="ss">margin_left: </span><span class="mi">0</span><span class="p">,</span>  
    <span class="ss">margin_right: </span><span class="mi">0</span><span class="p">,</span>  
    <span class="ss">margin_top: </span><span class="mi">0</span><span class="p">,</span>  
    <span class="ss">margin_bottom: </span><span class="mi">0</span><span class="p">,</span>  
    <span class="ss">print_background: </span><span class="kp">true</span>  
  <span class="p">)</span>  
  <span class="n">browser</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">reset</span>  
  <span class="n">browser</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">quit</span>  
  <span class="n">send_data</span><span class="p">(</span><span class="n">pdf</span><span class="p">,</span> <span class="ss">filename: </span><span class="s1">'test.pdf'</span><span class="p">,</span> <span class="ss">type: </span><span class="s1">'application/pdf'</span><span class="p">,</span> <span class="ss">disposition: </span><span class="s1">'inline'</span><span class="p">)</span>  
<span class="k">end</span></code></pre></figure>

<p>One important thing to mention for all of these examples, is that if you are importing css into your pdf layouts, you probably still like to keep <code class="language-plaintext highlighter-rouge">wicked_pdf</code> gem in your gem file for a <a href="https://github.com/mileszs/wicked_pdf/blob/edcd80e7a96b5fe79476f7984e5ff8f3765f220d/lib/wicked_pdf/wicked_pdf_helper/assets.rb#L46">bunch of magic around that</a>. I honestly do not know, why Rails Core team keeps neglecting these things in Asset Pipeline API, that is obviously needed in both PDF and Mail complex scenarios, but only provided here and as is. Maybe I missing something.</p>

<p>There are also some half-cooked approach like this that can replace that code:</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">inline_asset</span><span class="p">(</span><span class="n">asset_name</span><span class="p">)</span>  
  <span class="s2">"&lt;style type='text/css'&gt;</span><span class="si">#{</span><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">assets</span><span class="p">.</span><span class="nf">find_asset</span><span class="p">(</span><span class="n">asset_name</span><span class="p">).</span><span class="nf">to_s</span><span class="si">}</span><span class="s2">&lt;/style&gt;"</span><span class="p">.</span><span class="nf">html_safe</span>  
<span class="k">end</span></code></pre></figure>

<p>Place it in <code class="language-plaintext highlighter-rouge">ApplicationHelper</code> and you good to go.</p>

<h2 id="lambda-function">Lambda Function</h2>
<p>If you already utilizing AWS Lambda you can go in a different direction, and try to spin your headless Chrome there:</p>

<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><span class="dl">"</span><span class="s2">use strict</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">chromium</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">chrome-aws-lambda</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">Sentry</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">@sentry/node</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">Sentry</span><span class="p">.</span><span class="nf">init</span><span class="p">({</span> <span class="na">dsn</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://...@sentry.io/...</span><span class="dl">'</span> <span class="p">});</span>

<span class="kr">module</span><span class="p">.</span><span class="nx">exports</span><span class="p">.</span><span class="nx">pdf</span> <span class="o">=</span> <span class="nf">async </span><span class="p">(</span><span class="nx">event</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">let</span> <span class="nx">browser</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
  <span class="kd">let</span> <span class="nx">response</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
  <span class="kd">const</span>  <span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">body</span><span class="p">);</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">html</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">body</span><span class="p">;</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span> <span class="na">format</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Letter</span><span class="dl">'</span><span class="p">,</span> <span class="na">margin</span><span class="p">:</span> <span class="p">{</span> <span class="na">top</span><span class="p">:</span> <span class="dl">'</span><span class="s1">0</span><span class="dl">'</span><span class="p">,</span> <span class="na">bottom</span><span class="p">:</span> <span class="dl">'</span><span class="s1">0</span><span class="dl">'</span><span class="p">,</span> <span class="na">left</span><span class="p">:</span> <span class="dl">'</span><span class="s1">0</span><span class="dl">'</span><span class="p">,</span> <span class="na">right</span><span class="p">:</span> <span class="dl">'</span><span class="s1">0</span><span class="dl">'</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">body</span><span class="p">;</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">remoteContent</span> <span class="o">=</span> <span class="kc">true</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">body</span><span class="p">;</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">emulateMedia</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">print</span><span class="dl">'</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">body</span><span class="p">;</span>

  <span class="k">try</span> <span class="p">{</span>
    <span class="nx">browser</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">chromium</span><span class="p">.</span><span class="nx">puppeteer</span><span class="p">.</span><span class="nf">launch</span><span class="p">({</span>
      <span class="na">args</span><span class="p">:</span> <span class="nx">chromium</span><span class="p">.</span><span class="nx">args</span><span class="p">,</span>
      <span class="na">defaultViewport</span><span class="p">:</span> <span class="nx">chromium</span><span class="p">.</span><span class="nx">defaultViewport</span><span class="p">,</span>
      <span class="na">executablePath</span><span class="p">:</span> <span class="k">await</span> <span class="nx">chromium</span><span class="p">.</span><span class="nx">executablePath</span><span class="p">,</span>
      <span class="na">headless</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
    <span class="p">});</span>
    <span class="kd">const</span> <span class="nx">page</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nf">newPage</span><span class="p">();</span>
    <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nf">emulateMedia</span><span class="p">(</span><span class="nx">emulateMedia</span><span class="p">);</span>
    
    <span class="nf">if </span><span class="p">(</span><span class="nx">remoteContent</span> <span class="o">===</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nf">goto</span><span class="p">(</span><span class="s2">`data:text/html;base64,</span><span class="p">${</span><span class="nx">Buffer</span><span class="p">.</span><span class="nf">from</span><span class="p">(</span><span class="nx">html</span><span class="p">).</span><span class="nf">toString</span><span class="p">(</span><span class="dl">'</span><span class="s1">base64</span><span class="dl">'</span><span class="p">)}</span><span class="s2">`</span><span class="p">,</span> <span class="p">{</span>
          <span class="na">waitUntil</span><span class="p">:</span> <span class="dl">'</span><span class="s1">networkidle0</span><span class="dl">'</span>
      <span class="p">});</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="c1">//page.setContent will be faster than page.goto if html is a static</span>
        <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nf">setContent</span><span class="p">(</span><span class="nx">html</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="kd">const</span> <span class="nx">pdfStream</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nf">pdf</span><span class="p">(</span><span class="nx">options</span><span class="p">);</span>
    <span class="nx">response</span> <span class="o">=</span> <span class="p">{</span>
      <span class="na">statusCode</span><span class="p">:</span> <span class="mi">200</span><span class="p">,</span>
      <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
        <span class="dl">"</span><span class="s2">Content-type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/pdf</span><span class="dl">"</span><span class="p">,</span>
        <span class="dl">'</span><span class="s1">content-disposition</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">attachment;</span><span class="dl">'</span>
      <span class="p">},</span>
      <span class="na">body</span><span class="p">:</span> <span class="nx">pdfStream</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="dl">"</span><span class="s2">base64</span><span class="dl">"</span><span class="p">),</span>
      <span class="na">isBase64Encoded</span><span class="p">:</span> <span class="kc">true</span>
    <span class="p">};</span>
  <span class="p">}</span> <span class="nf">catch </span><span class="p">(</span><span class="nx">error</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">Sentry</span><span class="p">.</span><span class="nf">captureException</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
    <span class="k">await</span> <span class="nx">Sentry</span><span class="p">.</span><span class="nf">flush</span><span class="p">(</span><span class="mi">2000</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">context</span><span class="p">.</span><span class="nf">fail</span><span class="p">(</span><span class="nx">error</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
    <span class="nf">if </span><span class="p">(</span><span class="nx">browser</span> <span class="o">!==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nf">close</span><span class="p">();</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">context</span><span class="p">.</span><span class="nf">succeed</span><span class="p">(</span><span class="nx">response</span><span class="p">);</span>
<span class="p">};</span></code></pre></figure>

<p>Additional details can be found on <a href="https://www.npmjs.com/package/chrome-aws-lambda">package site</a>.
And for Rails client to that:</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">PdfLambda</span>
  <span class="kp">include</span> <span class="no">HTTParty</span>
  <span class="n">base_uri</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s1">'PDF_LAMBDA_URL'</span><span class="p">)</span>
  <span class="n">debug_output</span> <span class="vg">$stderr</span> <span class="k">if</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s1">'DEBUG_PDF_LAMBDA'</span><span class="p">,</span> <span class="kp">false</span><span class="p">)</span>

  <span class="nb">attr_accessor</span> <span class="ss">:raw_html</span><span class="p">,</span> <span class="ss">:filename</span><span class="p">,</span> <span class="ss">:options</span><span class="p">,</span> <span class="ss">:remote_content</span><span class="p">,</span> <span class="ss">:wait_for_fonts</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">raw_html</span><span class="p">,</span> <span class="n">filename</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">raw_html</span> <span class="o">=</span> <span class="n">raw_html</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">filename</span> <span class="o">=</span> <span class="n">filename</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">options</span> <span class="o">=</span> <span class="n">options</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">remote_content</span> <span class="o">=</span> <span class="n">options</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="ss">:remote_content</span><span class="p">)</span> <span class="o">||</span> <span class="kp">false</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">wait_for_fonts</span> <span class="o">=</span> <span class="n">options</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="ss">:waitForFonts</span><span class="p">)</span> <span class="o">||</span> <span class="kp">false</span>
    <span class="k">return</span> <span class="k">if</span> <span class="no">File</span><span class="p">.</span><span class="nf">exist?</span><span class="p">(</span><span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">'tmp/pdfs'</span><span class="p">))</span>

    <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir</span><span class="p">(</span><span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">'tmp/pdfs'</span><span class="p">))</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">generate_file</span>
    <span class="n">response</span> <span class="o">=</span> <span class="kp">nil</span>
    <span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="s1">'wb'</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span>
      <span class="n">response</span> <span class="o">=</span> <span class="nb">self</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="s1">'/'</span><span class="p">,</span>
                                 <span class="ss">body: </span><span class="n">pdf_options</span><span class="p">,</span>
                                 <span class="ss">headers: </span><span class="n">headers</span><span class="p">,</span>
                                 <span class="ss">stream_body: </span><span class="kp">true</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">fragment</span><span class="o">|</span>
        <span class="k">if</span> <span class="n">fragment</span><span class="p">.</span><span class="nf">code</span> <span class="o">==</span> <span class="mi">200</span>
          <span class="n">file</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">fragment</span><span class="p">)</span>
        <span class="k">elsif</span> <span class="p">[</span><span class="mi">301</span><span class="p">,</span> <span class="mi">302</span><span class="p">].</span><span class="nf">exclude?</span><span class="p">(</span><span class="n">fragment</span><span class="p">.</span><span class="nf">code</span><span class="p">)</span>
          <span class="k">raise</span> <span class="no">StandardError</span><span class="p">,</span> <span class="s2">"Non-success status code while streaming </span><span class="si">#{</span><span class="n">fragment</span><span class="p">.</span><span class="nf">code</span><span class="si">}</span><span class="s2">"</span>
        <span class="k">end</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="n">response</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">success?</span> <span class="p">?</span> <span class="n">filepath</span> <span class="p">:</span> <span class="kp">false</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">filepath</span>
    <span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">'tmp/pdfs'</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">pdf_options</span>
    <span class="p">{</span>
      <span class="ss">html: </span><span class="n">raw_html</span><span class="p">,</span>
      <span class="ss">remoteContent: </span><span class="n">remote_content</span><span class="p">,</span>
      <span class="ss">waitForFonts: </span><span class="n">wait_for_fonts</span><span class="p">,</span>
      <span class="ss">options: </span><span class="p">{</span>
        <span class="ss">format: </span><span class="s1">'Letter'</span><span class="p">,</span> <span class="ss">margin: </span><span class="p">{</span> <span class="ss">top: </span><span class="s1">'0.1in'</span><span class="p">,</span> <span class="ss">bottom: </span><span class="s1">'70'</span><span class="p">,</span> <span class="ss">left: </span><span class="s1">'0.25in'</span><span class="p">,</span> <span class="ss">right: </span><span class="s1">'0.25in'</span> <span class="p">}</span>
      <span class="p">}.</span><span class="nf">merge</span><span class="p">(</span><span class="n">options</span><span class="p">)</span>
    <span class="p">}.</span><span class="nf">to_json</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">headers</span>
    <span class="p">{</span>
      <span class="s1">'Accept-Version'</span><span class="p">:</span> <span class="s1">''</span><span class="p">,</span>
      <span class="s1">'x-api-key'</span><span class="p">:</span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'PDF_LAMDBA_API_KEY'</span><span class="p">],</span>
      <span class="no">Accept</span><span class="p">:</span> <span class="s1">'application/pdf'</span>
    <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre></figure>

<p>and it can be used like that in our code:</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">format</span><span class="p">.</span><span class="nf">pdf</span> <span class="k">do</span>
  <span class="n">pdf_html</span> <span class="o">=</span> <span class="no">ApplicationController</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">render_to_string</span><span class="p">(</span>  
    <span class="ss">template: </span><span class="s1">'templates/pdfs/my_pdf'</span><span class="p">,</span>  
    <span class="ss">formats: </span><span class="p">[</span><span class="ss">:pdf</span><span class="p">],</span>  
    <span class="ss">layout: </span><span class="s1">'layouts/pdf'</span><span class="p">,</span>  
    <span class="ss">assigns: </span><span class="p">{</span> <span class="ss">model: </span><span class="vi">@model</span> <span class="p">},</span> 
    <span class="ss">encoding: </span><span class="s1">'UTF-8'</span>  
  <span class="p">)</span>
  <span class="n">pdf_lambda</span> <span class="o">=</span> <span class="no">PdfLambda</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span>
    <span class="n">pdf_html</span><span class="p">,</span> <span class="s1">'test.pdf'</span><span class="p">,</span>  
    <span class="p">{</span>  
      <span class="ss">displayHeaderFooter: </span><span class="kp">true</span><span class="p">,</span>  
      <span class="ss">printBackground: </span><span class="kp">true</span><span class="p">,</span>  
      <span class="ss">waitForFonts: </span><span class="kp">true</span><span class="p">,</span>  
      <span class="ss">footerTemplate: </span><span class="o">&lt;&lt;~</span><span class="no">TEMPLATE</span>  <span class="sh">
&lt;span class="pageNumber"&gt;&lt;/span&gt;/&lt;span class="totalPages"&gt;&lt;/span&gt;
</span><span class="no">TEMPLATE  </span>
	<span class="p">}</span>
<span class="p">)</span>  
  <span class="n">pdf_lambda</span><span class="p">.</span><span class="nf">generate_file</span>
  <span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">pdf_path</span><span class="p">,</span> <span class="s1">'r'</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">pdf</span><span class="o">|</span>  
    <span class="n">send_data</span> <span class="n">pdf</span><span class="p">.</span><span class="nf">read</span><span class="p">,</span>  
              <span class="ss">type: </span><span class="s1">'application/pdf'</span><span class="p">,</span>  
              <span class="ss">disposition: </span><span class="s1">'inline'</span><span class="p">,</span>  
              <span class="ss">filename: </span><span class="n">filename</span>  
  <span class="k">end</span>  
  <span class="no">File</span><span class="p">.</span><span class="nf">delete</span><span class="p">(</span><span class="n">pdf_path</span><span class="p">)</span>  
<span class="k">end</span></code></pre></figure>]]></content><author><name></name></author><summary type="html"><![CDATA[Let’s start from the obvious. If you using WickedPDF in 2023 it’s a little bit more problematic than just having issues with PDF generation from time to time. It means that you are building your PDF markups with tables, not using modern CSS abilities like flex and grids. It must be hard. But probably it’s because dealing with PDFs was difficult the last time you had to do it. But no matter what, the time has come. You need to up your PDF game. wkhtmltopdf is deprecated. Let’s go over your options.]]></summary></entry><entry><title type="html">Handling complex translation lookups in Rails</title><link href="http://0.0.0.0:8080/2023/04/18/handling-complex-translation-lookups-in-rails-copy.html" rel="alternate" type="text/html" title="Handling complex translation lookups in Rails" /><published>2023-04-18T00:00:00-05:00</published><updated>2023-04-18T00:00:00-05:00</updated><id>http://0.0.0.0:8080/2023/04/18/handling-complex-translation-lookups-in-rails%20copy</id><content type="html" xml:base="http://0.0.0.0:8080/2023/04/18/handling-complex-translation-lookups-in-rails-copy.html"><![CDATA[<p>Let’s say you need to get I18n translations from different scopes depending on the logic, and only some of them are present in such overrides, while others need to fallback to the default scope.
You build a logic like that:</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">complex_translate</span>
  <span class="n">some_scope</span> <span class="o">=</span> <span class="p">[</span><span class="ss">:first</span><span class="p">,</span> <span class="ss">:second</span><span class="p">]</span>
  <span class="n">fallback_scope</span> <span class="o">=</span> <span class="p">[</span><span class="ss">:first</span><span class="p">]</span>

  <span class="no">I18n</span><span class="p">.</span><span class="nf">exists?</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="ss">scope: </span><span class="n">some_scope</span><span class="p">)</span> <span class="p">?</span> <span class="no">I18n</span><span class="p">.</span><span class="nf">t</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="ss">scope: </span><span class="n">some_scope</span><span class="p">)</span> <span class="p">:</span> <span class="no">I18n</span><span class="p">.</span><span class="nf">t</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="ss">scope: </span><span class="n">fallback_scope</span><span class="p">)</span>
<span class="k">end</span></code></pre></figure>

<p>But then you find out, that <code class="language-plaintext highlighter-rouge">exists?</code> is not actually takes scope into the account at all (it always returning <code class="language-plaintext highlighter-rouge">false</code> for any combination of key and scope). As when you look for implementation (good thing you can do cmd+B in RubyMine and go directly for the implementation), you see this call:
<code class="language-plaintext highlighter-rouge">config.backend.exists?(locale, key, options)</code>. And when you go to the backend <code class="language-plaintext highlighter-rouge">exist?</code> definition you see next:</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">exists?</span><span class="p">(</span><span class="n">locale</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">options</span> <span class="o">=</span> <span class="no">EMPTY_HASH</span><span class="p">)</span>
  <span class="n">lookup</span><span class="p">(</span><span class="n">locale</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span> <span class="o">!=</span> <span class="kp">nil</span>
<span class="k">end</span></code></pre></figure>

<p>So what can be a way out? (Don’t say monkey patching, you are going to regret that later) It’s actually quite simple, you have to use bang version of translate and catch exception from it:</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">def</span> <span class="nf">complex_translate</span>
  <span class="n">some_scope</span> <span class="o">=</span> <span class="p">[</span><span class="ss">:first</span><span class="p">,</span> <span class="ss">:second</span><span class="p">]</span>
  <span class="n">fallback_scope</span> <span class="o">=</span> <span class="p">[</span><span class="ss">:first</span><span class="p">]</span>

  <span class="no">I18n</span><span class="p">.</span><span class="nf">t!</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="ss">scope: </span><span class="n">some_scope</span><span class="p">)</span>
<span class="k">rescue</span> <span class="no">I18n</span><span class="o">::</span><span class="no">MissingTranslationData</span>
  <span class="no">I18n</span><span class="p">.</span><span class="nf">t</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="ss">scope: </span><span class="n">fallback_scope</span><span class="p">)</span>
<span class="k">end</span></code></pre></figure>]]></content><author><name></name></author><summary type="html"><![CDATA[Let’s say you need to get I18n translations from different scopes depending on the logic, and only some of them are present in such overrides, while others need to fallback to the default scope. You build a logic like that:]]></summary></entry><entry><title type="html">Ruby on Rails Mailer previews for complex models</title><link href="http://0.0.0.0:8080/2023/04/13/rails-mailer-previews-with-complex-models.html" rel="alternate" type="text/html" title="Ruby on Rails Mailer previews for complex models" /><published>2023-04-13T00:00:00-05:00</published><updated>2023-04-13T00:00:00-05:00</updated><id>http://0.0.0.0:8080/2023/04/13/rails-mailer-previews-with-complex-models</id><content type="html" xml:base="http://0.0.0.0:8080/2023/04/13/rails-mailer-previews-with-complex-models.html"><![CDATA[<p>Sooner of later your Ruby on Rails application will get to the level of maturity, which will require you to implement email notifications for a really complex model layer changes. And that will rise a problems of both development database pollution and code duplication between specs and mailer previews.</p>

<p>Let’s see how we can solve both of the problems with few simple changes to the Mailer preview layer of your Rails application.</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">class</span> <span class="nc">OurNotifierPreview</span> <span class="o">&lt;</span> <span class="no">ActionMailer</span><span class="o">::</span><span class="no">Preview</span>
  <span class="c1"># ...</span>
  <span class="c1"># Wraps any of the above methods in a transaction,</span>
  <span class="c1"># so that the database changes are rolled back at the end of the render.</span>
  <span class="c1"># Which leads to links in emails being broken.</span>
  <span class="c1"># Due to it's nature, IDs sequences are increasing anyway.</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">call</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
    <span class="n">preview</span> <span class="o">=</span> <span class="kp">nil</span>
    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">transaction</span> <span class="k">do</span>
      <span class="n">preview</span> <span class="o">=</span> <span class="k">super</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
      <span class="k">raise</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Rollback</span>
    <span class="k">end</span>
    <span class="n">preview</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">welcome_new_account</span>
    <span class="no">OurNotifier</span><span class="p">.</span><span class="nf">something_changed</span><span class="p">(</span><span class="n">complex_model</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="kp">private</span>
    <span class="k">def</span> <span class="nf">complex_model</span>
      <span class="c1"># We are changing our factories list to the smaller/different scope</span>
      <span class="c1"># if needed</span>
      <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">factories</span><span class="p">.</span><span class="nf">clear</span>
      <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">definition_file_paths</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"spec/scope/factories"</span><span class="p">]</span>
      <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">find_definitions</span>
      <span class="c1"># These thee lines can be omitted if we are using wider default scope</span>
      <span class="no">FactoryBot</span><span class="p">.</span><span class="nf">create</span> <span class="ss">:model</span><span class="p">,</span> <span class="ss">:complex</span>
    <span class="k">end</span>
<span class="k">end</span></code></pre></figure>]]></content><author><name></name></author><summary type="html"><![CDATA[Sooner of later your Ruby on Rails application will get to the level of maturity, which will require you to implement email notifications for a really complex model layer changes. And that will rise a problems of both development database pollution and code duplication between specs and mailer previews.]]></summary></entry><entry><title type="html">Welcome!</title><link href="http://0.0.0.0:8080/jekyll/update/2023/03/20/welcome.html" rel="alternate" type="text/html" title="Welcome!" /><published>2023-03-20T10:54:09-05:00</published><updated>2023-03-20T10:54:09-05:00</updated><id>http://0.0.0.0:8080/jekyll/update/2023/03/20/welcome</id><content type="html" xml:base="http://0.0.0.0:8080/jekyll/update/2023/03/20/welcome.html"><![CDATA[<p>Let’s get moving. After a year of owing bopm.name I finally decided to host someting on this domain.</p>]]></content><author><name></name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[Let’s get moving. After a year of owing bopm.name I finally decided to host someting on this domain.]]></summary></entry></feed>