<?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: erich mayne</title>
    <description>The latest articles on DEV Community by erich mayne (@erich_mayne_a9cab13715b29).</description>
    <link>https://dev.to/erich_mayne_a9cab13715b29</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%2F4094790%2Fd7432798-097e-41f7-ac62-679f210edd8b.png</url>
      <title>DEV Community: erich mayne</title>
      <link>https://dev.to/erich_mayne_a9cab13715b29</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erich_mayne_a9cab13715b29"/>
    <language>en</language>
    <item>
      <title>Your AI image is 1024px. The canvas is 4 feet wide. Here's the math that saved me.</title>
      <dc:creator>erich mayne</dc:creator>
      <pubDate>Tue, 25 Aug 2026 22:09:37 +0000</pubDate>
      <link>https://dev.to/erich_mayne_a9cab13715b29/your-ai-image-is-1024px-the-canvas-is-4-feet-wide-heres-the-math-that-saved-me-1cc1</link>
      <guid>https://dev.to/erich_mayne_a9cab13715b29/your-ai-image-is-1024px-the-canvas-is-4-feet-wide-heres-the-math-that-saved-me-1cc1</guid>
      <description>&lt;p&gt;I build a tool that turns text prompts into wall art you can actually hang — canvas prints, posters, 4×6 ft tapestries. The generation part is the easy part. The part nobody warns you about is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SDXL hands you a 1024×1024 image. A four-foot tapestry needs roughly 3,500 pixels on the long edge. A poster needs more.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For about a month I brute-forced this by upscaling everything 4× and hoping. It was slow, it blew my function memory limits, and it produced 60 MB files for products that didn't need them. Then I actually did the math, and it turned out I'd been solving the wrong problem.&lt;/p&gt;

&lt;p&gt;Here's what I wish I'd read first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 300 DPI rule is for things you hold
&lt;/h2&gt;

&lt;p&gt;The "300 DPI or it's garbage" rule comes from offset printing — magazines, books, brochures. Things you hold about 12 inches from your face.&lt;/p&gt;

&lt;p&gt;Nobody holds a four-foot tapestry twelve inches from their face.&lt;/p&gt;

&lt;p&gt;Required resolution is a function of &lt;strong&gt;viewing distance&lt;/strong&gt;, not print size. The human eye resolves roughly one arcminute of detail. There are ~3438 arcminutes in a radian, which gives you a genuinely useful one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pixels per inch needed for a print to look "sharp"&lt;/span&gt;
&lt;span class="c1"&gt;// at a given viewing distance. 3438 = arcminutes per radian.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requiredPPI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;viewingDistanceInches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3438&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;viewingDistanceInches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;requiredPPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 286 PPI — a book in your hands. Hence "300 DPI".&lt;/span&gt;
&lt;span class="nf"&gt;requiredPPI&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="c1"&gt;// 143 PPI — a small canvas on a desk shelf.&lt;/span&gt;
&lt;span class="nf"&gt;requiredPPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 48  PPI — a big tapestry across the room.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last number is the one that changed my pipeline. A tapestry viewed from six feet away needs &lt;strong&gt;48 PPI&lt;/strong&gt;, not 300. That is a 6× difference in linear resolution and a &lt;strong&gt;36× difference in pixel count&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning that into actual pixel targets
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pixelsNeeded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;printInches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;viewingDistanceInches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;printInches&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;requiredPPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;viewingDistanceInches&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// A 4ft × 6ft tapestry, viewed from ~6ft&lt;/span&gt;
&lt;span class="nf"&gt;pixelsNeeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 3438px on the long edge&lt;/span&gt;

&lt;span class="c1"&gt;// A 12" × 16" canvas, viewed from ~2ft&lt;/span&gt;
&lt;span class="nf"&gt;pixelsNeeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&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="c1"&gt;// 2292px on the long edge&lt;/span&gt;

&lt;span class="c1"&gt;// A 24" × 36" poster, viewed from ~4ft&lt;/span&gt;
&lt;span class="nf"&gt;pixelsNeeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 2579px on the long edge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the thing that surprised me most: &lt;strong&gt;the giant tapestry and the small canvas need almost the same number of pixels.&lt;/strong&gt; The tapestry is 20× the surface area, but you stand 3× further back, and those cancel out almost exactly.&lt;/p&gt;

&lt;p&gt;So the naive rule — &lt;em&gt;bigger product, bigger upscale&lt;/em&gt; — is just wrong. I had it backwards for a month.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;Print size&lt;/th&gt;
&lt;th&gt;Typical viewing&lt;/th&gt;
&lt;th&gt;Required PPI&lt;/th&gt;
&lt;th&gt;Long-edge px&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small canvas&lt;/td&gt;
&lt;td&gt;12×16 in&lt;/td&gt;
&lt;td&gt;24 in&lt;/td&gt;
&lt;td&gt;143&lt;/td&gt;
&lt;td&gt;2292&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large canvas&lt;/td&gt;
&lt;td&gt;24×36 in&lt;/td&gt;
&lt;td&gt;48 in&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;2579&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poster&lt;/td&gt;
&lt;td&gt;24×36 in&lt;/td&gt;
&lt;td&gt;48 in&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;2579&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tapestry&lt;/td&gt;
&lt;td&gt;48×72 in&lt;/td&gt;
&lt;td&gt;72 in&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;3438&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One 4× upscale from 1024px gets me to 4096px, which clears &lt;strong&gt;every single row in that table&lt;/strong&gt;. I don't need a tiered pipeline at all. I need one upscale and a downsample.&lt;/p&gt;

&lt;h2&gt;
  
  
  The serverless part, where I actually got burned
&lt;/h2&gt;

&lt;p&gt;Knowing the target is half of it. The other half is not exploding.&lt;/p&gt;

&lt;p&gt;An RGBA buffer is &lt;code&gt;width × height × 4&lt;/code&gt; bytes, uncompressed, in memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bufferMB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;bufferMB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 4 MB    — fine&lt;/span&gt;
&lt;span class="nf"&gt;bufferMB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 64 MB   — fine, but...&lt;/span&gt;
&lt;span class="nf"&gt;bufferMB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 256 MB  — and sharp may hold 2-3 of these at once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one is what killed me. Image pipelines hold the source, the destination, and often an intermediate simultaneously. An 8192px "just to be safe" upscale would spike past 700 MB and get OOM-killed, intermittently, only on large orders — the worst kind of bug.&lt;/p&gt;

&lt;p&gt;Two things fixed it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Stop upscaling past what the table says.&lt;/strong&gt; Obvious in hindsight. 4096px covers everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Never do this in the request path.&lt;/strong&gt; Upscaling is a queue job, not an HTTP handler. The user gets their preview immediately from the raw generation; the print-resolution render happens after checkout, when you actually know which size they bought. No reason to burn GPU seconds upscaling art nobody purchased.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't render print files on generate. Render on purchase.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onCheckoutComplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PRINT_TARGETS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productType&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;   &lt;span class="c1"&gt;// from the table above&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;printQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;imageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;longEdgePx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;longEdgePx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bleedInches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bleed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bleed, which I forgot entirely
&lt;/h2&gt;

&lt;p&gt;Canvas wraps around a wooden frame. Tapestries get hemmed. Both eat your edges.&lt;/p&gt;

&lt;p&gt;A 12×16 canvas on a 1.5" stretcher bar loses 1.5 inches on &lt;strong&gt;every&lt;/strong&gt; side — so you're feeding a 15×19 inch image and only 12×16 survives on the front face. If you compose to the exact print size, your subject's head gets folded around the back of the frame.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withBleed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bleedInches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;inches&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;bleedInches&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;span class="nf"&gt;withBleed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 15&lt;/span&gt;
&lt;span class="nf"&gt;withBleed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// 19&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate wide, crop late. Outpainting is very useful here — it's often better to extend the AI image outward than to upscale-and-crop a composition that was framed tightly to begin with.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the pipeline looks like now
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Generate at native model resolution (1024²). Show it instantly.&lt;/li&gt;
&lt;li&gt;User picks a size. &lt;strong&gt;Now&lt;/strong&gt; you know the target.&lt;/li&gt;
&lt;li&gt;Queue job: outpaint for bleed → single 4× upscale → downsample to exact target → CMYK-ish soft proof → PDF/TIFF for the printer.&lt;/li&gt;
&lt;li&gt;Cap every intermediate at 4096px. Nothing needs more.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cut my per-order compute by roughly 70%, and the OOM errors went away entirely because nothing allocates a quarter-gigabyte buffer anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you're putting generated images onto physical objects, &lt;strong&gt;write down the viewing distance before you write any code.&lt;/strong&gt; It determines your resolution target, which determines your memory ceiling, which determines whether this runs in a serverless function or needs a real worker.&lt;/p&gt;

&lt;p&gt;I got this wrong in the most expensive possible direction — over-engineering for a quality level no human eye could resolve from across a room.&lt;/p&gt;

&lt;p&gt;If you want to see the output side of this, the generator is &lt;a href="https://art-ificial.tech/ai-art-generator" rel="noopener noreferrer"&gt;free to try here&lt;/a&gt; — the print-resolution pipeline above is what runs behind it. The &lt;a href="https://art-ificial.tech" rel="noopener noreferrer"&gt;tapestry sizing&lt;/a&gt; is the case where the math is most counterintuitive: biggest product, lowest PPI requirement.&lt;/p&gt;

&lt;p&gt;Happy to answer questions on the upscaling or the bleed math — the bleed thing in particular cost me a batch of misprints before I understood it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
