cd /news/generative-ai/your-ai-image-is-1024px-the-canvas-i… Β· home β€Ί topics β€Ί generative-ai β€Ί article
[ARTICLE Β· art-110975] src=dev.to β†— pub= topic=generative-ai verified=true sentiment=Β· neutral

Your AI image is 1024px. The canvas is 4 feet wide. Here's the math that saved me.

A developer built a tool that turns text prompts into wall art, including canvas prints and large tapestries. They discovered that the required resolution for prints depends on viewing distance, not print size, and that a 4Γ— upscale from SDXL's 1024Γ—1024 output is sufficient for all product sizes. The developer also optimized the pipeline by moving upscaling out of the request path and into a queue job.

read5 min views1 publishedAug 25, 2026

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:

SDXL hands you a 1024Γ—1024 image. A four-foot tapestry needs roughly 3,500 pixels on the long edge. A poster needs more.

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.

Here's what I wish I'd read first.

The "300 DPI or it's garbage" rule comes from offset printing β€” magazines, books, brochures. Things you hold about 12 inches from your face.

Nobody holds a four-foot tapestry twelve inches from their face.

Required resolution is a function of viewing distance, 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:

// Pixels per inch needed for a print to look "sharp"
// at a given viewing distance. 3438 = arcminutes per radian.
const requiredPPI = (viewingDistanceInches) => 3438 / viewingDistanceInches;

requiredPPI(12);  // 286 PPI β€” a book in your hands. Hence "300 DPI".
requiredPPI(24);  // 143 PPI β€” a small canvas on a desk shelf.
requiredPPI(72);  // 48  PPI β€” a big tapestry across the room.

That last number is the one that changed my pipeline. A tapestry viewed from six feet away needs 48 PPI, not 300. That is a 6Γ— difference in linear resolution and a 36Γ— difference in pixel count.

const pixelsNeeded = (printInches, viewingDistanceInches) =>
  Math.ceil(printInches * requiredPPI(viewingDistanceInches));

// A 4ft Γ— 6ft tapestry, viewed from ~6ft
pixelsNeeded(72, 72);   // 3438px on the long edge

// A 12" Γ— 16" canvas, viewed from ~2ft
pixelsNeeded(16, 24);   // 2292px on the long edge

// A 24" Γ— 36" poster, viewed from ~4ft
pixelsNeeded(36, 48);   // 2579px on the long edge

Here's the thing that surprised me most: the giant tapestry and the small canvas need almost the same number of pixels. The tapestry is 20Γ— the surface area, but you stand 3Γ— further back, and those cancel out almost exactly.

So the naive rule β€” bigger product, bigger upscale β€” is just wrong. I had it backwards for a month.

Product Print size Typical viewing Required PPI Long-edge px
Small canvas 12Γ—16 in 24 in 143 2292
Large canvas 24Γ—36 in 48 in 72 2579
Poster 24Γ—36 in 48 in 72 2579
Tapestry 48Γ—72 in 72 in 48 3438

One 4Γ— upscale from 1024px gets me to 4096px, which clears every single row in that table. I don't need a tiered pipeline at all. I need one upscale and a downsample.

Knowing the target is half of it. The other half is not exploding.

An RGBA buffer is width Γ— height Γ— 4

bytes, uncompressed, in memory:

const bufferMB = (w, h) => (w * h * 4) / 1024 / 1024;

bufferMB(1024, 1024);  // 4 MB    β€” fine
bufferMB(4096, 4096);  // 64 MB   β€” fine, but...
bufferMB(8192, 8192);  // 256 MB  β€” and sharp may hold 2-3 of these at once

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.

Two things fixed it:

1. Stop upscaling past what the table says. Obvious in hindsight. 4096px covers everything.

2. Never do this in the request path. 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.

// Don't render print files on generate. Render on purchase.
async function onCheckoutComplete(order) {
  const target = PRINT_TARGETS[order.productType];   // from the table above
  await printQueue.enqueue({
    imageId: order.imageId,
    longEdgePx: target.longEdgePx,
    bleedInches: target.bleed,
  });
}

Canvas wraps around a wooden frame. Tapestries get hemmed. Both eat your edges.

A 12Γ—16 canvas on a 1.5" stretcher bar loses 1.5 inches on every 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.

const withBleed = (inches, bleedInches) => inches + bleedInches * 2;
withBleed(12, 1.5);  // 15
withBleed(16, 1.5);  // 19

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.

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

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

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.

If you want to see the output side of this, the generator is free to try here β€” the print-resolution pipeline above is what runs behind it. The tapestry sizing is the case where the math is most counterintuitive: biggest product, lowest PPI requirement.

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.

── more in #generative-ai 4 stories Β· sorted by recency
── more on @sdxl 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/your-ai-image-is-102…] indexed:0 read:5min 2026-08-25 Β· β€”