<?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: Gaven</title>
    <description>The latest articles on DEV Community by Gaven (@gaven).</description>
    <link>https://dev.to/gaven</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%2F3759404%2F8f3c6e5d-e6e9-4b53-9dd9-609c7fa3f9ec.jpg</url>
      <title>DEV Community: Gaven</title>
      <link>https://dev.to/gaven</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gaven"/>
    <language>en</language>
    <item>
      <title>I Thought Better Models Would Fix AI Image Editing. I Was Wrong.</title>
      <dc:creator>Gaven</dc:creator>
      <pubDate>Sun, 09 Aug 2026 04:18:08 +0000</pubDate>
      <link>https://dev.to/gaven/i-thought-better-models-would-fix-ai-image-editing-i-was-wrong-5ak</link>
      <guid>https://dev.to/gaven/i-thought-better-models-would-fix-ai-image-editing-i-was-wrong-5ak</guid>
      <description>&lt;p&gt;For a while, I assumed the biggest problem in AI image editing was obvious:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the model just wasn’t good enough yet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the output changed a face too much, use a better model. If the product logo became distorted, use a better model. If the lighting looked strange, wait for the next model release.&lt;/p&gt;

&lt;p&gt;That explanation is comforting because it turns every failure into a model-quality problem.&lt;/p&gt;

&lt;p&gt;But while working on an AI photo editing workflow, I kept running into a different conclusion:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;the harder problem is expressing what is allowed to change — and what is not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That sounds like a UX detail. I now think it is the core abstraction of AI image editing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Generation and editing are not the same task
&lt;/h2&gt;

&lt;p&gt;Text-to-image generation is wonderfully forgiving.&lt;/p&gt;

&lt;p&gt;If I ask for:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A cinematic photo of a red sneaker on a concrete pedestal, soft studio light, dark gray background.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;there are thousands of outputs that could reasonably satisfy me.&lt;/p&gt;

&lt;p&gt;Editing is different.&lt;/p&gt;

&lt;p&gt;Imagine I already have a product photo. I want to keep the exact shoe, its proportions, material, logo, color, and camera angle — but replace the background with a cleaner studio scene.&lt;/p&gt;

&lt;p&gt;Now the request is not simply:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generate a better product photo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is closer to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Change the environment, &lt;strong&gt;but preserve the product identity&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That second half is where a surprising amount of the difficulty lives.&lt;/p&gt;

&lt;p&gt;You can see the difference clearly in a before/after product edit:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cliplumi.com/" 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%2Fyqqumyd9pe5d5zgohfms.webp" alt="AI product photo editing example" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The user is not asking the system to be creative everywhere. They are granting creativity inside a boundary.&lt;/p&gt;

&lt;p&gt;That led me to a simple mental model:&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;// Conceptual model — not an API contract&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;editRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;originalImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;change&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Replace the background with a clean studio setup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;preserve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;product shape&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;logo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;material&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;color&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;camera angle&lt;/span&gt;&lt;span class="dl"&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;p&gt;The more I thought about it, the more this looked less like prompting and more like &lt;strong&gt;constraint management&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  “Change this” is only half of the prompt
&lt;/h2&gt;

&lt;p&gt;Most people naturally describe the desired change first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remove the people in the background&lt;/li&gt;
&lt;li&gt;make the room Scandinavian&lt;/li&gt;
&lt;li&gt;turn daytime into night&lt;/li&gt;
&lt;li&gt;change the jacket to black&lt;/li&gt;
&lt;li&gt;add warm cinematic lighting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for editing, the preservation requirements are often equally important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep the face unchanged&lt;/li&gt;
&lt;li&gt;keep the original pose&lt;/li&gt;
&lt;li&gt;preserve the product logo&lt;/li&gt;
&lt;li&gt;do not move the furniture&lt;/li&gt;
&lt;li&gt;keep the camera perspective&lt;/li&gt;
&lt;li&gt;keep the typography intact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the pattern I now encourage in the interface and in prompts:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Change X. Preserve Y.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is almost embarrassingly simple, but it produces a much clearer contract between the user and the model.&lt;/p&gt;

&lt;p&gt;A room redesign is a good example. “Make this room more modern” is underspecified. Does the user want to replace the sofa? Move the windows? Change the floor plan?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cliplumi.com/" 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%2Ftvxw3huyr39ezt05cd4i.webp" alt="AI room redesign example" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A better request is something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Redesign the room in a warm minimalist style. Keep the room layout, windows, camera angle, and major furniture positions unchanged.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is much closer to how a human art director would brief an editor.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reference images are really another kind of constraint
&lt;/h2&gt;

&lt;p&gt;Another thing I initially thought about too narrowly was reference images.&lt;/p&gt;

&lt;p&gt;It is easy to treat them as “more prompt.” In practice, they are more useful as a &lt;strong&gt;visual contract&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A reference can answer questions that are annoying to describe in text:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What shade of blue?&lt;/li&gt;
&lt;li&gt;What kind of lighting?&lt;/li&gt;
&lt;li&gt;How dense should the texture be?&lt;/li&gt;
&lt;li&gt;What does “minimalist” mean in this specific project?&lt;/li&gt;
&lt;li&gt;Which composition should the edit move toward?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the workflow I’m building with &lt;a href="https://cliplumi.com/" rel="noopener noreferrer"&gt;ClipLumi&lt;/a&gt;, users can add up to three reference images alongside the main editing instruction.&lt;/p&gt;

&lt;p&gt;The important part is not the number three. The important part is that the UI acknowledges something developers already know from multimodal systems:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;language is not always the best representation of intent.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes the most precise instruction is another image.&lt;/p&gt;




&lt;h2&gt;
  
  
  Natural language does not eliminate the need for controls
&lt;/h2&gt;

&lt;p&gt;There is a tempting product idea in generative AI:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the model is smart enough, every setting should disappear.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I don’t think that is always true.&lt;/p&gt;

&lt;p&gt;Some choices are not semantic. They are production constraints.&lt;/p&gt;

&lt;p&gt;A user may know exactly what they want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;16:9 for a website hero&lt;/li&gt;
&lt;li&gt;9:16 for a mobile story&lt;/li&gt;
&lt;li&gt;four variants instead of one&lt;/li&gt;
&lt;li&gt;a faster draft before spending time on quality&lt;/li&gt;
&lt;li&gt;a specific model because it behaves better for a certain task&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not failures of natural language. They are legitimate parameters.&lt;/p&gt;

&lt;p&gt;So the current ClipLumi editor keeps explicit controls for things such as model selection, aspect ratio, output count, resolution, quality, fast mode, and style.&lt;/p&gt;

&lt;p&gt;The design question becomes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which choices belong in prose, and which choices deserve a control?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My current rule of thumb is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use language for &lt;em&gt;intent&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Use references for &lt;em&gt;visual direction&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Use controls for &lt;em&gt;production constraints&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation has been more useful to me than trying to turn everything into one giant prompt box.&lt;/p&gt;




&lt;h2&gt;
  
  
  The goal was not “Photoshop with AI buttons”
&lt;/h2&gt;

&lt;p&gt;This also changed how I thought about the product itself.&lt;/p&gt;

&lt;p&gt;I did not want to rebuild a traditional editor in the browser and then add an AI button to the toolbar.&lt;/p&gt;

&lt;p&gt;Traditional image editors are powerful because they expose the mechanics directly: selections, masks, layers, blend modes, brushes, curves, transforms.&lt;/p&gt;

&lt;p&gt;For many everyday edits, though, the user does not care about the mechanics.&lt;/p&gt;

&lt;p&gt;They care about the outcome:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remove those people, but keep the subject and the scene intact.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://cliplumi.com/" 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%2F2syuuo0ja5ii9vqcde32.webp" alt="AI background people removal example" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole point of a prompt-driven editor is that the user should not need to manually paint a mask just to communicate that intention.&lt;/p&gt;

&lt;p&gt;That is why the product direction became less about “AI features inside an editor” and more about &lt;strong&gt;editing through constraints, references, and natural language&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Better models still matter — just not in the way I expected
&lt;/h2&gt;

&lt;p&gt;None of this means model quality is irrelevant.&lt;/p&gt;

&lt;p&gt;It matters a lot.&lt;/p&gt;

&lt;p&gt;There are still cases where generative editing is frustrating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;small text and logos can deform&lt;/li&gt;
&lt;li&gt;hands and fine geometry can change unexpectedly&lt;/li&gt;
&lt;li&gt;multiple edits in one request can interfere with one another&lt;/li&gt;
&lt;li&gt;preserving identity across aggressive transformations is difficult&lt;/li&gt;
&lt;li&gt;the same prompt can behave differently across models&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A stronger model can reduce those failures.&lt;/p&gt;

&lt;p&gt;But a better model cannot fully rescue a badly specified task.&lt;/p&gt;

&lt;p&gt;If the interface gives the model permission to reinterpret the entire image when the user only wanted one small change, the product has already made the job harder than necessary.&lt;/p&gt;

&lt;p&gt;This is why I increasingly think the next improvement in AI creative tools will not come only from larger models.&lt;/p&gt;

&lt;p&gt;Some of it will come from &lt;strong&gt;better ways to represent user intent&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bigger question
&lt;/h2&gt;

&lt;p&gt;So here is the question I’m curious about from other developers building with generative models:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should future AI editors expose more model controls, or should they hide more of the model behind higher-level constraints?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In other words, are we moving toward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;increasingly sophisticated prompt boxes,&lt;/li&gt;
&lt;li&gt;increasingly sophisticated traditional editors with AI features,&lt;/li&gt;
&lt;li&gt;or a new kind of interface where users mainly define what may change, what must remain stable, and what references the system should follow?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I’m currently betting on the third direction.&lt;/p&gt;

&lt;p&gt;That is the idea I’m exploring with &lt;a href="https://cliplumi.com/" rel="noopener noreferrer"&gt;ClipLumi&lt;/a&gt;: upload an image, describe the change, state what must remain intact, optionally add references, and keep enough explicit controls to make the output usable in a real workflow.&lt;/p&gt;

&lt;p&gt;It is still imperfect, and that is exactly why I find the problem interesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you were designing an AI image editor today, what would you expose to the user — and what would you hide?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I’d genuinely like to hear how other people here are thinking about this boundary.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>discuss</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Modeling Margins and Overlap in a Browser-Based PDF Layout Engine</title>
      <dc:creator>Gaven</dc:creator>
      <pubDate>Sun, 26 Jul 2026 08:52:45 +0000</pubDate>
      <link>https://dev.to/gaven/modeling-margins-and-overlap-in-a-browser-based-pdf-layout-engine-4ene</link>
      <guid>https://dev.to/gaven/modeling-margins-and-overlap-in-a-browser-based-pdf-layout-engine-4ene</guid>
      <description>&lt;p&gt;The hardest part of a tiled-poster generator is not cutting an image into rectangles. The hard part is deciding what each rectangle means in physical space.&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%2Fn8v1nc59okfv04hv8bxb.jpg" 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%2Fn8v1nc59okfv04hv8bxb.jpg" alt=" " width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A browser gives us pixels. A PDF library gives us points. Paper presets are usually described in millimetres or inches. The printer adds a non-printable border. The user may request overlap between neighbouring sheets. The responsive preview then compresses the whole result into a few hundred CSS pixels.&lt;/p&gt;

&lt;p&gt;If those measurements leak into one another, the preview can look correct while the exported poster is wrong.&lt;/p&gt;

&lt;p&gt;This article explains the layout model I used while building &lt;a href="https://rasterbator.app/" rel="noopener noreferrer"&gt;Rasterbator.app&lt;/a&gt;, a browser-based tool that turns one image into a printable multi-page PDF. The goal is not to present one implementation as universal. It is to show the decisions that kept margins, overlap, source cropping, and preview rendering consistent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a physical poster model
&lt;/h2&gt;

&lt;p&gt;A tempting implementation begins with the preview container:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;read its pixel width;&lt;/li&gt;
&lt;li&gt;divide it into columns and rows;&lt;/li&gt;
&lt;li&gt;reuse those rectangles when generating the PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That approach couples print output to responsive UI. Resize the browser, open the tool on a phone, or change the device pixel ratio, and the calculation can change.&lt;/p&gt;

&lt;p&gt;Instead, the layout engine should first produce a device-independent model. A simplified version might contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PosterLayout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;paperWidthMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;paperHeightMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;marginMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overlapMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;printableWidthMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;printableHeightMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;stepXmm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;stepYmm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;posterWidthMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;posterHeightMm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;p&gt;The preview and PDF exporter both consume this object. Neither is allowed to invent its own geometry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Printable size and page advance are different
&lt;/h2&gt;

&lt;p&gt;Suppose an A4 portrait page is 210 × 297 millimetres and the user selects a 10-millimetre margin on every side.&lt;/p&gt;

&lt;p&gt;The printable dimensions are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;printable width  = 210 - 2 × 10 = 190 mm
printable height = 297 - 2 × 10 = 277 mm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without overlap, each new column advances 190 millimetres across the final poster.&lt;/p&gt;

&lt;p&gt;With a 10-millimetre overlap, the printed region is still 190 millimetres wide, but the next logical page begins only 180 millimetres later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stepX = printableWidth - overlap
stepY = printableHeight - overlap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That difference is the centre of the model.&lt;/p&gt;

&lt;p&gt;The printable region describes how much image appears on one sheet. The page advance describes how much unique poster area that sheet adds. Confusing the two causes one of two failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicated strips make the assembled poster larger than expected;&lt;/li&gt;
&lt;li&gt;the overlap setting appears visually but does not affect source cropping.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Calculating final poster dimensions
&lt;/h2&gt;

&lt;p&gt;For a grid with &lt;code&gt;columns&lt;/code&gt; and &lt;code&gt;rows&lt;/code&gt;, the physical extent is not simply &lt;code&gt;columns × printableWidth&lt;/code&gt; when overlap is active.&lt;/p&gt;

&lt;p&gt;A useful formula is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;posterWidth  = printableWidth  + (columns - 1) × stepX
posterHeight = printableHeight + (rows - 1) × stepY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first page contributes its complete printable region. Each additional page contributes only the logical advance because part of it repeats the previous page.&lt;/p&gt;

&lt;p&gt;This formula also provides a helpful user-facing number. The application can display the assembled poster size before the user generates or prints anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert units only at boundaries
&lt;/h2&gt;

&lt;p&gt;Internally, I prefer to keep physical layout calculations in millimetres. Paper presets are easy to represent, custom dimensions remain readable, and the values shown in the UI match the values used by the engine.&lt;/p&gt;

&lt;p&gt;Conversion to PDF points happens at the export boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mmToPt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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;mm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;25.4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The preview has a separate conversion. It receives the poster dimensions in millimetres and computes a display-only scale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;previewScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;containerWidthPx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;posterWidthMm&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;displayX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;physicalXmm&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;previewScale&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CSS pixels never flow back into the physical model.&lt;/p&gt;

&lt;p&gt;This separation makes it possible to render the same poster at different preview sizes while generating an identical PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  Map page rectangles to source-image coordinates
&lt;/h2&gt;

&lt;p&gt;The poster model describes physical windows. The source image uses pixels. The next task is to map each page’s physical bounds to a crop rectangle in the source image.&lt;/p&gt;

&lt;p&gt;Assume the image is fitted to the poster while preserving its aspect ratio. Once the effective poster image dimensions are known, calculate a normalized page window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;normalizedX = pagePhysicalX / posterWidth
normalizedY = pagePhysicalY / posterHeight
normalizedW = printableWidth / posterWidth
normalizedH = printableHeight / posterHeight
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then map those normalized values to image pixels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sourceX = normalizedX × imageWidthPx
sourceY = normalizedY × imageHeightPx
sourceW = normalizedW × imageWidthPx
sourceH = normalizedH × imageHeightPx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;pagePhysicalX&lt;/code&gt; advances by &lt;code&gt;stepX&lt;/code&gt;, neighbouring pages naturally include the repeated overlap strip.&lt;/p&gt;

&lt;p&gt;Rounding should be handled carefully. Rounding every intermediate value can create one-pixel seams or drift across many pages. Keep floating-point values through the mapping stage and round only where a Canvas or encoder requires integers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partial pages still count as pages
&lt;/h2&gt;

&lt;p&gt;Users may specify a physical width or height rather than an exact grid. If a poster needs 3.1 printable page widths, it requires four sheets.&lt;/p&gt;

&lt;p&gt;That means page counts are calculated with &lt;code&gt;Math.ceil&lt;/code&gt;, not ordinary rounding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&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;requiredWidthMm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;stepXmm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final column may use only part of its printable area. The exported PDF page is still a complete physical sheet, but the image content may occupy a smaller window or require controlled clipping.&lt;/p&gt;

&lt;p&gt;This is also why a preview should show complete page boundaries rather than presenting the poster as one uninterrupted rectangle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Margins belong to the page, not the source crop
&lt;/h2&gt;

&lt;p&gt;Margins are physical empty areas around the printable content. They should not reduce the amount of source image represented by the poster model in an accidental way.&lt;/p&gt;

&lt;p&gt;A clean export sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;create a PDF page using the full paper dimensions;&lt;/li&gt;
&lt;li&gt;define a printable rectangle offset by the margin;&lt;/li&gt;
&lt;li&gt;crop or draw the source image for that page window;&lt;/li&gt;
&lt;li&gt;place the result inside the printable rectangle;&lt;/li&gt;
&lt;li&gt;add crop marks and labels in controlled page coordinates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In no-effects mode, each tile can be rendered to a temporary Canvas and encoded as JPEG before being embedded. In vector halftone modes, sampled marks can be drawn directly into the printable rectangle.&lt;/p&gt;

&lt;p&gt;A clipping mask is useful for large dots or line effects because their geometry may otherwise spill into the margin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Page labels need a stable coordinate convention
&lt;/h2&gt;

&lt;p&gt;Labels such as A1, B1, and A2 are more than decoration. They are part of the assembly model.&lt;/p&gt;

&lt;p&gt;Choose one convention and keep it consistent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;columns use spreadsheet-style letters;&lt;/li&gt;
&lt;li&gt;rows use one-based numbers;&lt;/li&gt;
&lt;li&gt;the top-left page is A1;&lt;/li&gt;
&lt;li&gt;columns increase from left to right;&lt;/li&gt;
&lt;li&gt;rows increase from top to bottom.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more than 26 columns, a spreadsheet-style conversion avoids arbitrary limits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;columnLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&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;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&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;p&gt;Even if the application’s safety limits make extremely wide grids unlikely, using a complete convention keeps the model predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate workload before rendering
&lt;/h2&gt;

&lt;p&gt;A physically valid poster can still be too expensive for a browser. A high-resolution source image expands into an uncompressed pixel buffer. Canvas operations, halftone sampling, JPEG encoding, and PDF assembly can all exist in memory at the same time.&lt;/p&gt;

&lt;p&gt;Rasterbator.app estimates both page count and sampled-cell workload before starting expensive work. Desktop and mobile use different practical thresholds. The important design principle is not the exact numbers; it is rejecting an unreasonable job before allocating large buffers.&lt;/p&gt;

&lt;p&gt;A good validation error should explain what the user can change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reduce the page grid;&lt;/li&gt;
&lt;li&gt;use a desktop browser;&lt;/li&gt;
&lt;li&gt;choose a larger paper size;&lt;/li&gt;
&lt;li&gt;reduce halftone density;&lt;/li&gt;
&lt;li&gt;resize the source image.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One model, multiple renderers
&lt;/h2&gt;

&lt;p&gt;The most valuable architectural decision was making the poster layout an explicit intermediate representation.&lt;/p&gt;

&lt;p&gt;The preview renderer converts physical units into display pixels. The PDF renderer converts the same physical units into points. The source-crop mapper converts them into image pixels. Labels, crop marks, masks, and overlap all derive from the same page rectangles.&lt;/p&gt;

&lt;p&gt;That structure prevents a common class of “almost correct” bugs: a preview computed one way and an export computed another way.&lt;/p&gt;

&lt;p&gt;When a web application produces a physical object, model the physical object first. Renderers should translate the model; they should not define it.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a Browser-Based Tiled Poster PDF Generator</title>
      <dc:creator>Gaven</dc:creator>
      <pubDate>Thu, 23 Jul 2026 01:05:06 +0000</pubDate>
      <link>https://dev.to/gaven/building-a-browser-based-tiled-poster-pdf-generator-426g</link>
      <guid>https://dev.to/gaven/building-a-browser-based-tiled-poster-pdf-generator-426g</guid>
      <description>&lt;p&gt;A tiled poster generator looks simple from the outside: load an image, choose a paper size, and download a PDF. The implementation becomes more interesting when the entire pipeline has to run in the browser, preserve physical dimensions, support overlapping pages, render a halftone effect, and avoid exhausting mobile memory.&lt;/p&gt;

&lt;p&gt;I recently worked through those constraints while building &lt;a href="https://rasterbator.app/" rel="noopener noreferrer"&gt;Rasterbator.app&lt;/a&gt;. This article focuses on the engineering model behind the tool rather than the physical printing workflow.&lt;/p&gt;

&lt;p&gt;The pipeline has five main stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode the source image locally.&lt;/li&gt;
&lt;li&gt;Convert paper settings into one consistent coordinate system.&lt;/li&gt;
&lt;li&gt;Calculate a deterministic page grid while preserving aspect ratio.&lt;/li&gt;
&lt;li&gt;Generate a preview or halftone representation.&lt;/li&gt;
&lt;li&gt;Render each page into a downloadable PDF.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Use PDF points as the layout coordinate system
&lt;/h2&gt;

&lt;p&gt;The UI exposes familiar paper units such as millimeters and inches, but the PDF renderer works in points. Converting everything early prevents the layout code from mixing units.&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;MM_TO_PTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;25.4&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;PAPER_SIZES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;A4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;210&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;MM_TO_PTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;297&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;MM_TO_PTS&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;A3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;297&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;MM_TO_PTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;420&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;MM_TO_PTS&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;LETTER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.5&lt;/span&gt; &lt;span class="o"&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;11&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;LEGAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.5&lt;/span&gt; &lt;span class="o"&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;14&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;72&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;p&gt;The paper rectangle is not the same as the usable poster rectangle. A page can have an outer margin and an overlap region shared with the next tile.&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;printableWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;paperWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;margin&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;printableHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;paperHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;margin&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;logicalWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;printableWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;overlap&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;logicalHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;printableHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That distinction matters. The printable width is what appears on one sheet. The logical width is how far the poster advances before the next sheet begins. When overlap is enabled, adjacent sheets intentionally cover some of the same image area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the image ratio before rounding the page count
&lt;/h2&gt;

&lt;p&gt;Users may define the poster by width, height, or a manual page grid. For a width-driven layout, the physical poster width is derived from the requested number of logical pages. The height follows from the image ratio.&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;imageAspect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;imageHeight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;imageWidth&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;physicalWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pagesWide&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;logicalWidth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;overlap&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;physicalHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;physicalWidth&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;imageAspect&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;pagesHighFloat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;physicalHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;logicalHeight&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;pagesHigh&lt;/span&gt; &lt;span class="o"&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;pagesHighFloat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final &lt;code&gt;Math.ceil&lt;/code&gt; is unavoidable. A narrow partial strip at the edge still requires a complete physical sheet.&lt;/p&gt;

&lt;p&gt;Rounding creates another problem: the rounded page grid is usually slightly larger than the exact poster dimensions. I center the image horizontally inside that grid and keep it top-aligned vertically. The offset becomes part of every later coordinate calculation.&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;trueGridWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pagesWideOut&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;logicalWidth&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;overlap&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;paperOffsetX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trueGridWidth&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;physicalWidth&lt;/span&gt;&lt;span class="p"&gt;)&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paperOffsetY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the preview and the PDF renderer the same definition of where the poster begins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep local files local
&lt;/h2&gt;

&lt;p&gt;For a local upload, the browser already gives us a &lt;code&gt;File&lt;/code&gt;, which is also a &lt;code&gt;Blob&lt;/code&gt;. It can be decoded without sending the source image to an application server.&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;function&lt;/span&gt; &lt;span class="nf"&gt;loadImageFromBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&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;objectUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onload&lt;/span&gt; &lt;span class="o"&gt;=&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;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;objectUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&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;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;objectUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to decode image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;objectUrl&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;p&gt;Remote image URLs are a separate case. They require a network fetch and may require a proxy when the origin does not allow cross-origin access. The UI and privacy language should make that distinction clear instead of treating local files and remote URLs as equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate the preview from sampled pixels
&lt;/h2&gt;

&lt;p&gt;Rendering the full-resolution source image for every preview update would be expensive. The halftone preview only needs one representative color for each grid cell.&lt;/p&gt;

&lt;p&gt;The implementation draws a scaled image to a Canvas and reads the pixel data. For manageable layouts, it samples a small 3×3 area per output cell and averages the values. For larger layouts, it falls back to one pixel per cell.&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;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sampleWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sampleHeight&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;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;willReadFrequently&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sampleWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sampleHeight&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;pixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getImageData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sampleWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sampleHeight&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final preview is represented as SVG. That makes circles, squares, line shapes, page boundaries, and labels easy to compose without repeatedly painting a large bitmap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Map luminance to halftone size
&lt;/h2&gt;

&lt;p&gt;A basic halftone model turns darker source pixels into larger marks and lighter source pixels into smaller marks.&lt;/p&gt;

&lt;p&gt;Relative luminance is calculated from the RGB channels:&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;luminance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2126&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;red&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.7152&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;green&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.0722&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;blue&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;brightness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;luminance&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then brightness is mapped into the user-selected minimum and maximum dot-size range:&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;minScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rasterSizeMin&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&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;maxScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rasterSizeMax&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&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;dotScale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxScale&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxScale&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;minScale&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;radius&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dotScale&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;maximumRadius&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same sampled color data can support several output modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;black marks on a light background,&lt;/li&gt;
&lt;li&gt;white marks on a dark background,&lt;/li&gt;
&lt;li&gt;a single custom mark color,&lt;/li&gt;
&lt;li&gt;marks colored from the source image,&lt;/li&gt;
&lt;li&gt;circles, squares, or horizontal lines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keeping the luminance-to-size function separate from the drawing code makes those combinations easier to manage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Render one PDF page window at a time
&lt;/h2&gt;

&lt;p&gt;The PDF is created in the browser with &lt;code&gt;pdf-lib&lt;/code&gt;. The document begins with an instruction page, followed by the printable poster pages.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StandardFonts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rgb&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf-lib&lt;/span&gt;&lt;span class="dl"&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;pdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;pagesHigh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;pagesWide&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pdf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addPage&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;paperWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;paperHeight&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="c1"&gt;// Render only the source window that belongs to this page.&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;p&gt;Each page represents a window into the full poster coordinate system:&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;windowLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;column&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;logicalWidth&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;windowTop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;logicalHeight&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;windowRight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;windowLeft&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;printableWidth&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;windowBottom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;windowTop&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;printableHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the plain-image mode, that window is converted back into source-image pixel coordinates. Only the intersecting image region is drawn to a temporary Canvas, encoded as JPEG, and embedded in the current PDF page.&lt;/p&gt;

&lt;p&gt;For the halftone modes, the renderer finds the grid cells that can intersect the current page and draws only those shapes. This avoids scanning every dot for every page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crop the geometry, not just the source image
&lt;/h2&gt;

&lt;p&gt;Large circles can extend beyond the center of their grid cell. Even when a cell center is inside the printable region, its geometry may spill into the page margin.&lt;/p&gt;

&lt;p&gt;A straightforward safeguard is to render the marks and then cover the four margin strips with white rectangles. This creates a strict printable-area mask.&lt;/p&gt;

&lt;p&gt;Crop marks and page labels are added after the poster content. Labels use spreadsheet-style columns and row numbers:&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="nx"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;B1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;C1&lt;/span&gt;
&lt;span class="nx"&gt;A2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;B2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;C2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The label generator must continue past column Z, so the implementation uses the same base-26 pattern commonly used for spreadsheet columns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add explicit browser resource limits
&lt;/h2&gt;

&lt;p&gt;Client-side processing shifts infrastructure cost away from a server, but it does not remove cost. Large Canvases, millions of sampled cells, and dozens of PDF pages can still freeze a browser tab or exhaust memory.&lt;/p&gt;

&lt;p&gt;The current safeguards reject layouts above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;24 pages or 450,000 sampled cells on mobile,&lt;/li&gt;
&lt;li&gt;80 pages or 1,600,000 sampled cells on desktop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those numbers are product limits rather than universal browser limits. The important design choice is to estimate work before starting the expensive preview or PDF loop, then return a useful message such as “reduce the page count” or “increase the grid size.”&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would separate earlier in a second implementation
&lt;/h2&gt;

&lt;p&gt;Three boundaries have been especially useful:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Layout math versus rendering.&lt;/strong&gt; The preview and PDF should consume the same layout object instead of calculating dimensions independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source loading versus privacy claims.&lt;/strong&gt; A local &lt;code&gt;Blob&lt;/code&gt; and a remote URL follow different data paths and should be described differently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output quality versus feasibility.&lt;/strong&gt; Sampling density, JPEG quality, page count, and grid size are related resource controls, not isolated settings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The general pattern applies beyond posters. Any browser tool that turns one visual asset into multiple physical pages needs a stable coordinate system, explicit clipping windows, deterministic rounding, and early resource checks.&lt;/p&gt;

&lt;p&gt;You can test the current implementation at &lt;a href="https://rasterbator.app/" rel="noopener noreferrer"&gt;Rasterbator.app&lt;/a&gt;. I am particularly interested in alternative approaches to preview sampling and large-document memory management.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: this article was drafted with AI assistance and then reviewed against the project implementation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built a Gemini Watermark Remover: From OpenCV to a Lightweight Client-Side Algorithm</title>
      <dc:creator>Gaven</dc:creator>
      <pubDate>Tue, 21 Apr 2026 12:42:17 +0000</pubDate>
      <link>https://dev.to/gaven/how-i-built-a-gemini-watermark-remover-from-opencv-to-a-lightweight-client-side-algorithm-1og6</link>
      <guid>https://dev.to/gaven/how-i-built-a-gemini-watermark-remover-from-opencv-to-a-lightweight-client-side-algorithm-1og6</guid>
      <description>&lt;p&gt;If you’ve been experimenting with Google Gemini Nano or downloading images generated by Gemini, you’ve likely encountered that persistent, colorful "Banana" watermark or the subtle "Gemini" branding.&lt;br&gt;
While it’s a sign of AI progress, for creators, developers, and designers, it’s often just friction. It ruins a clean UI screenshot, clutters a slide deck, and makes documentation look unprofessional.&lt;br&gt;
That’s why I built Gemini Watermark Remover—a dedicated tool designed to strip away these distractions using a high-performance, client-side approach.&lt;br&gt;
👉 Try it here: &lt;a href="https://gemini-watermarkremover.org/" rel="noopener noreferrer"&gt;https://gemini-watermarkremover.org/&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.amazonaws.com%2Fuploads%2Farticles%2Fokc9am86dy02xv50xdy4.jpg" 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.amazonaws.com%2Fuploads%2Farticles%2Fokc9am86dy02xv50xdy4.jpg" alt=" " width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
The Engineering Challenge: Why "Generic" Isn't Enough&lt;br&gt;
Most people think watermark removal is a solved problem. "Just use AI," they say. But when I started this project, I realized that generic AI inpainting is often overkill—or worse, it's destructive.&lt;br&gt;
Here was my technical journey to find the "Goldilocks" solution.&lt;br&gt;
Phase 1: The OpenCV Baseline (Fast but "Smudgy")&lt;br&gt;
My first attempt used standard OpenCV inpainting algorithms (like Telea or NS).&lt;br&gt;
The Pro: It was incredibly fast.&lt;br&gt;
The Con: It struggled with complex textures. Since the Gemini watermark often sits on top of rich, AI-generated gradients, OpenCV would leave a "smudge" that was sometimes more distracting than the watermark itself.&lt;br&gt;
Phase 2: The "Heavy AI" Trap (Great Quality, Zero Speed)&lt;br&gt;
Next, I moved to LaMa (Large Mask Inpainting).&lt;br&gt;
The quality was near-perfect, but the trade-offs were unacceptable for a web-based tool:&lt;br&gt;
Latency: Processing one image took 20-40 seconds on average hardware.&lt;br&gt;
Privacy Concerns: Running this requires heavy server-side GPU power, meaning users have to upload their private images to a cloud.&lt;br&gt;
Cost: High GPU costs mean I’d eventually have to charge users.&lt;br&gt;
Phase 3: The Breakthrough—Pattern-Specific Reconstruction&lt;br&gt;
The final version of Gemini Watermark Remover uses a specialized, lightweight client-side algorithm.&lt;br&gt;
Instead of trying to "understand" the whole world like a massive AI model, I optimized the code to specifically recognize the geometric patterns and color signatures of the Gemini/Banana watermarks.&lt;br&gt;
Key Technical Advantages:&lt;br&gt;
Browser-Native: It uses the client’s CPU/GPU via JavaScript and WebGL. No images ever leave your computer.&lt;br&gt;
Sub-Second Processing: Most images are cleaned in under 500ms.&lt;br&gt;
Pixel-Perfect Restoration: By targeting the specific alpha-channel signature of the watermark, the algorithm restores the background with minimal artifacts.&lt;br&gt;
Why Choose Gemini Watermark Remover?&lt;br&gt;
Unlike "freemium" extensions that clutter your browser or tools that force you to create an account, I built this to be as frictionless as possible.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Privacy First (No Uploads)
Because the algorithm runs entirely in your browser, your data is never sent to a server. Your private generations remain private.&lt;/li&gt;
&lt;li&gt;Zero Installation
While extensions are great, sometimes you just need a quick fix. Our Online Tool works on any device—mobile, tablet, or desktop—without needing to install extra software.&lt;/li&gt;
&lt;li&gt;Specifically Tuned for "Nano Banana"
We’ve spent hours fine-tuning the detection specifically for the Gemini Nano (Banana) debug icons that have been popping up for developers recently. We know exactly what those pink pixels look like and how to erase them.
How to use it
Go to Gemini-WatermarkRemover.org.
Upload or drag-and-drop your Gemini-generated image.
The algorithm automatically detects and removes the watermark.
Download your clean, high-resolution image instantly.
Final Thoughts
This project wasn't about building the biggest AI model; it was about solving a specific problem with the most efficient tool possible.
By moving away from heavy server-side AI and toward optimized client-side logic, we created a tool that is faster, safer, and completely free to use.
If you're tired of the "Banana" icons and Gemini stamps, give it a try:
👉 &lt;a href="https://gemini-watermarkremover.org/" rel="noopener noreferrer"&gt;https://gemini-watermarkremover.org/&lt;/a&gt;
I’d love to hear your feedback! What other AI-generated friction should we solve next?&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>gemini</category>
      <category>nanobanana</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why Every Team Needs a Random Spinner Tool for Better Workflow</title>
      <dc:creator>Gaven</dc:creator>
      <pubDate>Sun, 08 Feb 2026 04:01:14 +0000</pubDate>
      <link>https://dev.to/gaven/why-every-team-needs-a-random-spinner-tool-for-better-workflow-2n0d</link>
      <guid>https://dev.to/gaven/why-every-team-needs-a-random-spinner-tool-for-better-workflow-2n0d</guid>
      <description>&lt;h4&gt;
  
  
  How to Make Fair Random Decisions with One Click
&lt;/h4&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.amazonaws.com%2Fuploads%2Farticles%2F5b1k562i34sydwhz5fid.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.amazonaws.com%2Fuploads%2Farticles%2F5b1k562i34sydwhz5fid.png" alt=" " width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’ve all been there: staring at a group chat for 20 minutes trying to decide where to go for team lunch, or arguing over who should take the first shift on support duty. Decision fatigue is real, and it’s a productivity killer.&lt;/p&gt;

&lt;p&gt;When logic doesn’t give you a clear answer, the best way to stay fair and fast is to leave it to chance. That’s why I’m a huge fan of random decision tools—specifically, &lt;strong&gt;Spin the Wheel&lt;/strong&gt; tools.&lt;/p&gt;

&lt;p&gt;In this post, let’s look at how a simple "Joyful" spin can solve your daily dilemmas.&lt;/p&gt;

&lt;h3&gt;
  
  
  🎡 What is a Random Decision-Making Tool?
&lt;/h3&gt;

&lt;p&gt;At its core, a random decision tool is a digital arbiter. It uses a randomization algorithm (usually built on &lt;code&gt;Math.random()&lt;/code&gt;) to pick an outcome from a list of inputs. &lt;/p&gt;

&lt;p&gt;Instead of a boring text generator, a &lt;strong&gt;spinner wheel&lt;/strong&gt; adds a visual and psychological element: the "suspense" of the spin makes the result feel more official and easier for everyone to accept.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Why Choose JoySpin? (The Joy of Fairness)
&lt;/h3&gt;

&lt;p&gt;While there are many tools out there, a good spinner should focus on three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Zero Bias:&lt;/strong&gt; No "weighted" results or hidden logic. Just pure, clean randomization.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Instant Setup:&lt;/strong&gt; You shouldn't need an account to decide what pizza to order.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Visual Delight:&lt;/strong&gt; A smooth animation makes the process feel less like a chore and more like a game.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  🔍 Practical Use Cases for Developers &amp;amp; Teams
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;How to use the Spinner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Daily Standups&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spin to decide who gives their update first.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code Reviews&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Randomly assign a reviewer when everyone is busy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Giveaways&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pick a winner from a list of community members fairly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Icebreakers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Put "Fun Fact" topics on the wheel for team bonding.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Personal Life&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"What should I learn next: Rust or Go?" — Let the wheel decide!&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  ⚙️ The Logic Behind the Wheel
&lt;/h3&gt;

&lt;p&gt;For the developers here, a spinner tool is a classic exercise in CSS transforms and JavaScript. The key is calculating the &lt;code&gt;rotation&lt;/code&gt; degrees and mapping that angle back to the array index of your choices.&lt;/p&gt;

&lt;p&gt;A tool like &lt;a href="https://www.joyspin.xyz/" rel="noopener noreferrer"&gt;JoySpin&lt;/a&gt; takes care of all that math, providing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Smooth, physics-based rotation.&lt;/li&gt;
&lt;li&gt;  Easy-to-edit segments.&lt;/li&gt;
&lt;li&gt;  Mobile-responsive design so you can use it on the go.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🚀 Try It Now — Bring Some Joy to Your Decisions
&lt;/h3&gt;

&lt;p&gt;If you're stuck on a choice right now, don't overthink it. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.joyspin.xyz/" rel="noopener noreferrer"&gt;JoySpin.xyz&lt;/a&gt;&lt;/strong&gt; is a free, one-click tool designed to make random picking fun and completely fair. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features of JoySpin:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Simple Interface:&lt;/strong&gt; No clutter, just the wheel.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Fast &amp;amp; Lightweight:&lt;/strong&gt; Loads instantly on any device.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Fully Customizable:&lt;/strong&gt; Add as many options as you need.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Shareable results:&lt;/strong&gt; (Coming soon/Great for transparency).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🎯 Pro Tip: The "Reverse Psychology" of Spinning
&lt;/h3&gt;

&lt;p&gt;Here’s a trick: If the wheel lands on "Option A" and you feel a sudden pang of disappointment, it means you actually wanted "Option B" all along. The spinner helped you find your true preference!&lt;/p&gt;

&lt;h3&gt;
  
  
  🎉 Final Thoughts
&lt;/h3&gt;

&lt;p&gt;Fairness doesn't have to be boring. By using a visual spinner, you turn a potential conflict into a moment of engagement.&lt;/p&gt;

&lt;p&gt;Next time your team is stuck in a "you pick," "no, you pick" loop—head over to &lt;strong&gt;&lt;a href="https://www.joyspin.xyz/" rel="noopener noreferrer"&gt;JoySpin&lt;/a&gt;&lt;/strong&gt; and let the wheel handle the rest.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
