<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Gaven Builds]]></title><description><![CDATA[Gaven Builds]]></description><link>https://gaven-builds.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6a637f800dfb19e08698785c/41382e15-7358-4a9f-b75c-a0afa21db903.png</url><title>Gaven Builds</title><link>https://gaven-builds.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 25 Sep 2026 00:40:16 GMT</lastBuildDate><atom:link href="https://gaven-builds.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[AI Image Editing UX Is a State-Management Problem, Not Just a Prompt Box]]></title><description><![CDATA[AI Image Editing UX Is a State-Management Problem, Not Just a Prompt Box
A text box is an attractive interface for generative AI because it looks universal. Give the model an instruction, wait for a r]]></description><link>https://gaven-builds.hashnode.dev/ai-image-editing-ux-is-a-state-management-problem-not-just-a-prompt-box</link><guid isPermaLink="true">https://gaven-builds.hashnode.dev/ai-image-editing-ux-is-a-state-management-problem-not-just-a-prompt-box</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[AI]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Product Management]]></category><dc:creator><![CDATA[Gaven]]></dc:creator><pubDate>Tue, 11 Aug 2026 16:04:16 GMT</pubDate><content:encoded><![CDATA[<h1>AI Image Editing UX Is a State-Management Problem, Not Just a Prompt Box</h1>
<p>A text box is an attractive interface for generative AI because it looks universal. Give the model an instruction, wait for a result, and repeat. The problem is that an image editor is not actually stateless. The user is not asking the system to create an arbitrary next image. They are usually asking for a transition from <strong>this particular image</strong> to <strong>a slightly different image</strong> while preserving a set of important invariants.</p>
<p>That distinction sounds small, but it changes how I think about the product architecture of AI image editors.</p>
<p>When I look at workflows in <a href="https://cliplumi.com/">ClipLumi</a> and similar tools, the interesting engineering problem is not “How large should the prompt field be?” It is “How do we represent the user’s current visual state, intended change, preservation constraints, references, model capabilities, and previous result without making the interface feel like a control panel?”</p>
<p>Here are the design principles I would carry into any AI image editing product.</p>
<h2>1. Treat an edit as a state transition</h2>
<p>A generation request can often be represented as:</p>
<p><code>prompt -&gt; image</code></p>
<p>An edit request is closer to:</p>
<p><code>current image + requested delta + invariants + optional references -&gt; next image</code></p>
<p>The current image matters because it contains decisions the user has already accepted. Composition, identity, product geometry, camera angle, lighting, background, and small details may all have value.</p>
<p>If the UI only exposes a prompt box, it hides this structure. The model receives instructions, but the product does not help the user distinguish between “what should change” and “what must remain stable.”</p>
<p>That is why a useful editing interface should make the transition explicit even if the visual UI remains simple.</p>
<h2>A minimal edit-session data model</h2>
<p>If I were implementing the product state explicitly, I would start with something closer to this than a single <code>prompt</code> string:</p>
<pre><code class="language-ts">type Invariant = {
  key: string;
  description: string;
  severity: "hard" | "soft";
};

type EditSession = {
  sourceAssetId: string;
  currentAssetId: string;
  requestedDelta: string;
  invariants: Invariant[];
  references: Array&lt;{
    assetId: string;
    role: "style" | "color" | "composition" | "identity" | "material";
  }&gt;;
  modelId: string;
  options: {
    aspectRatio?: string;
    outputCount?: number;
    quality?: string;
  };
  status:
    | "ready"
    | "generating"
    | "reviewing"
    | "accepted"
    | "repairing"
    | "failed";
  parentVersionId?: string;
};
</code></pre>
<p>The important part is not the exact TypeScript. It is that the system can answer basic questions without reconstructing them from prose:</p>
<ul>
<li>Which asset is authoritative right now?</li>
<li>What is the requested delta?</li>
<li>Which constraints are hard failures versus preferences?</li>
<li>What role does each reference play?</li>
<li>Which version is this edit branching from?</li>
<li>Is the user exploring, reviewing, repairing, or done?</li>
</ul>
<p>Once those concepts exist as product state, prompt assembly becomes an adapter concern instead of the source of truth.</p>
<h2>Use a small state machine for the edit lifecycle</h2>
<p>The edit lifecycle is also easier to reason about if it has named states:</p>
<pre><code class="language-text">ready
  -&gt; generating
  -&gt; reviewing
      -&gt; accepted
      -&gt; repairing -&gt; generating
      -&gt; ready      (revert / branch)
  -&gt; failed
</code></pre>
<p>This prevents several UI bugs that otherwise look unrelated. For example, if the user can change the model while a request is generating, which model should a retry use? If the user reverts to an earlier asset, do the current preservation constraints still apply? If an output is accepted and then repaired, should the repair branch from the accepted result or from the original source?</p>
<p>A state machine forces those questions to have explicit answers.</p>
<h2>2. Model “change” and “preserve” separately</h2>
<p>Many failed edits are not failures to understand the requested change. They are failures to protect the rest of the image.</p>
<p>Consider a product-photo request:</p>
<blockquote>
<p>Replace the background with a clean studio setting.</p>
</blockquote>
<p>That describes the delta but not the invariants. A more complete product intent is:</p>
<ul>
<li>change: background and surrounding surface;</li>
<li>preserve: product silhouette, logo, label, perspective, scale, and shadow direction.</li>
</ul>
<p>Those are different data concepts even if they eventually become one model prompt.</p>
<p>This suggests an architectural separation between <strong>intent capture</strong> and <strong>model prompt assembly</strong>. The UI can collect or infer a small number of structured constraints, while the backend converts them into the model-specific representation required by the selected provider.</p>
<p>The user should not need to understand provider syntax to express invariants.</p>
<h2>3. References need roles, not just attachment slots</h2>
<p>An image reference is not self-explanatory.</p>
<p>A user might attach a reference because they want its color palette, clothing style, background material, lighting mood, composition, or object shape. If the product treats every reference as “make it like this,” the model gets more visual information but less semantic clarity.</p>
<p>A stronger design asks the product layer to understand the role of each reference.</p>
<p>Even if you do not expose a dropdown for every reference, the prompt pipeline can benefit from language such as:</p>
<blockquote>
<p>Use reference 1 only for the background color and surface texture. Preserve the subject and composition from the source image.</p>
</blockquote>
<p>This is especially useful when an editor supports multiple reference images. More files are not automatically more context. Unlabeled references can become competing instructions.</p>
<h2>4. Model capability is part of product state</h2>
<p>One of the easiest mistakes in a multi-model AI product is to make every control visible for every model.</p>
<p>That creates a UI contract the backend cannot always keep.</p>
<p>If model A supports a certain editing mode, model B supports reference images, and model C is optimized mainly for generation, the interface should derive available controls from a capability map rather than from a global form definition.</p>
<p>Conceptually:</p>
<pre><code class="language-text">model -&gt; capabilities -&gt; visible controls -&gt; validation -&gt; request adapter
</code></pre>
<p>The capability map becomes a useful source of truth for both frontend behavior and backend validation.</p>
<p>It also prevents a common observability problem: requests that look valid in the UI but are silently ignored or degraded by the selected model.</p>
<h2>5. Preserve a reversible history</h2>
<p>AI editing is probabilistic, so users need a way to compare states rather than merely replace them.</p>
<p>A good editing loop looks like:</p>
<p><code>source -&gt; edit A -&gt; edit B -&gt; revert to A -&gt; branch into edit C</code></p>
<p>You do not necessarily need a full Photoshop-style history tree, but the product should at least preserve enough context to let the user recover a previous useful state.</p>
<p>This is more important than it sounds. Without reversibility, every edit feels risky. Users compensate by downloading intermediate images, opening duplicate tabs, or repeatedly regenerating from memory.</p>
<p>Reversibility is not only a convenience feature. It increases the user’s willingness to make narrower, more experimental changes.</p>
<p>A practical implementation detail is to store edits as a graph of versions rather than a mutable “current image” pointer with no provenance:</p>
<pre><code class="language-ts">type ImageVersion = {
  id: string;
  parentId?: string;
  assetId: string;
  requestedDelta?: string;
  invariantKeys: string[];
  createdAt: string;
  decision?: "accepted" | "rejected" | "repair";
};
</code></pre>
<p>You do not need to expose the graph in the UI. But keeping the parent relationship makes revert, compare, branch, audit, and “repair from the last accepted version” much easier.</p>
<h2>6. Acceptance criteria can improve the UI without becoming a form</h2>
<p>Users often know how they will judge an edit, even if they do not call those checks “acceptance criteria.”</p>
<p>For a headshot:</p>
<ul>
<li>face still looks like the same person;</li>
<li>hairline is intact;</li>
<li>clothing changed correctly;</li>
<li>background looks natural.</li>
</ul>
<p>For a product image:</p>
<ul>
<li>label is readable;</li>
<li>shape is unchanged;</li>
<li>new background fits the brand;</li>
<li>shadow still makes physical sense.</li>
</ul>
<p>A product does not need four mandatory fields to capture this. It can surface lightweight reminders, example prompt structures, or contextual suggestions.</p>
<p>The important point is that the interface should help users evaluate continuity, not only generate novelty.</p>
<h2>7. Observability should record product decisions, not just provider errors</h2>
<p>When an edit goes wrong, “request failed” is rarely enough information for debugging.</p>
<p>For an AI image editor, useful operational records may include:</p>
<ul>
<li>selected model;</li>
<li>editing mode;</li>
<li>whether a source image was present;</li>
<li>number of references;</li>
<li>whether prompt enhancement was applied;</li>
<li>the final assembled prompt sent to the provider;</li>
<li>output dimensions or aspect ratio;</li>
<li>provider latency;</li>
<li>success/failure state;</li>
<li>which product-level options were ignored or transformed.</li>
</ul>
<p>This does not mean logging private user assets carelessly. It means recording the decisions made by your own product pipeline so failures are explainable.</p>
<p>If an “AI Enhance” layer rewrites a prompt before it reaches the image model, for example, the monitoring surface should make that transformation visible. Otherwise a developer may blame the image model for a regression introduced by the application layer.</p>
<p>I would also separate the <strong>event log</strong> from the visual assets themselves. A minimal event record can be small:</p>
<pre><code class="language-json">{
  "event": "edit_requested",
  "sessionId": "...",
  "sourceVersionId": "...",
  "modelId": "...",
  "referenceCount": 1,
  "invariantCount": 4,
  "promptEnhanced": false,
  "requestShapeVersion": "v3"
}
</code></pre>
<p>That is usually enough to debug product behavior without dumping user images or sensitive raw content into logs. Store only what you actually need, and treat private assets and full prompts according to your privacy model rather than assuming observability justifies indefinite retention.</p>
<h2>8. Prompt enhancement needs a stop condition</h2>
<p>Automatic prompt enhancement sounds universally helpful until it starts overriding clear user intent.</p>
<p>A short prompt such as “change the wall to blue” may benefit from contextual expansion. A long, carefully constrained instruction may not.</p>
<p>That suggests a rule-based or confidence-based enhancement policy rather than “always enhance.”</p>
<p>Possible signals include:</p>
<ul>
<li>prompt length;</li>
<li>presence of explicit preservation language;</li>
<li>number of referenced entities;</li>
<li>whether the request is generation or editing;</li>
<li>whether the selected model already performs its own prompt interpretation.</li>
</ul>
<p>The important architectural point is that prompt enhancement is a <strong>transformer in the request pipeline</strong>, not a magical quality switch. It should be observable, bypassable, and testable.</p>
<h2>9. Image editing benefits from a smaller default action</h2>
<p>Generative interfaces often encourage maximal prompts because novelty is the product. Editing interfaces benefit from the opposite default: make the smallest change that satisfies the intent.</p>
<p>That reduces unintended drift and makes failure easier to diagnose.</p>
<p>For example, an old-photo restoration request is inherently preservation-sensitive. A system that aggressively “improves” faces, clothing, and scenery may produce a prettier result while failing the actual task.</p>
<p>The better product question is not “Can the model make this more impressive?” It is “Can the model repair the requested defect without damaging trusted information?”</p>
<h2>10. The backend adapter should absorb provider differences</h2>
<p>A product with multiple image models should avoid leaking provider-specific request shapes into the rest of the application.</p>
<p>A useful boundary is:</p>
<pre><code class="language-text">Product Intent
  -&gt; normalized edit request
  -&gt; provider adapter
  -&gt; provider-specific payload
  -&gt; normalized result
</code></pre>
<p>The normalized request can include source image, references, edit text, aspect ratio, output size, and product-level flags. Each adapter decides how those concepts map to the provider.</p>
<p>This keeps the frontend stable while models change underneath it.</p>
<p>It also makes A/B testing and fallback logic less painful because the product is comparing normalized intents rather than unrelated API payloads.</p>
<h2>A useful repair concept: shrink the repair radius</h2>
<p>A production editor also needs a concept of <strong>repair radius</strong>: how much of the accepted state is allowed to reopen during the next pass.</p>
<p>Early in exploration, the radius can be wide. After the user accepts composition, subject, and lighting, the radius should shrink. If only a contact shadow is wrong, the next request should not implicitly reopen the face, product geometry, background, crop, and color grade.</p>
<p>You can model this explicitly or simply assemble stronger preservation language as confidence increases. Either way, the product should behave as if accepted state becomes progressively more expensive to disturb.</p>
<p>This gives the UI a useful default: the closer the user is to done, the more conservative the next action should be.</p>
<h2>Do not let an old request overwrite a newer edit</h2>
<p>Async image jobs add one more state problem: results do not necessarily finish in the order the user started them. Imagine edit A starts from version 12, then the user changes direction and starts edit B from version 13. If B finishes first and becomes the accepted canvas, a late completion from A must not silently replace it just because its provider response arrived last.</p>
<p>Bind each attempt to an application-level request ID and the source version it started from. When a result returns, compare that identity with the active version before moving the current pointer. A stale result can still be useful in history, but it should not overwrite <code>currentAssetId</code> without an explicit user action. Cancellation can save compute; the stale-response guard is what protects correctness.</p>
<p>Timeouts need the same discipline. A client timeout does not prove that the provider rejected the job. The provider may still be generating or may already have accepted a paid request. Treat that state as <code>status_unknown</code>, recover the existing provider task ID or idempotency key when the API supports one, and reconcile that attempt before creating another paid job.</p>
<p>A compact rule is:</p>
<ul>
<li>every attempt has a request ID and source version;</li>
<li>only a result that still matches the active intent may advance the current canvas automatically;</li>
<li>cancelled or stale completions may enter history but cannot seize the current pointer;</li>
<li>timeout means uncertain until reconciled, not confirmed failure;</li>
<li>retry the same logical attempt when possible instead of blindly paying for a duplicate.</li>
</ul>
<p>This turns race conditions and retries into explicit product state instead of timing accidents.</p>
<h2>The prompt box should be the surface, not the architecture</h2>
<p>Natural language is still the best interface for many AI image edits. I am not arguing for replacing it with dozens of controls.</p>
<p>The point is that the product behind the prompt box needs a stronger model of the task.</p>
<p>A reliable editor has to know what image is current, what change is requested, what must be preserved, what references mean, which model capabilities are available, what transformation happened to the prompt, and how the user can recover if the result is worse.</p>
<p>That is a state-management problem, a capability-management problem, and an observability problem before it is a text-box problem.</p>
<p>If you are building an AI image product, try mapping one real workflow as a state transition rather than a single prompt request. Then inspect a live editor such as <a href="https://cliplumi.com/">ClipLumi</a> and ask which parts of that state are explicit, inferred, hidden, or missing. Also ask what your version graph and event log would need in order to explain a bad second-pass edit six hours later.</p>
<p>That exercise usually reveals more useful product work than adding another adjective to the prompt template.</p>
<p><em>Disclosure: I work on ClipLumi. I am using it here as a concrete product example; the state model, version graph, and repair-radius ideas are general architecture patterns.</em></p>
]]></content:encoded></item><item><title><![CDATA[The PDF Export Was the Easy Part: Building a Browser-Only Tiled Poster Generator]]></title><description><![CDATA[A tiled-poster generator sounds like a small file-conversion project: load an image, divide it into rectangles, create one PDF page for each rectangle, and let the user print the result.
That descript]]></description><link>https://gaven-builds.hashnode.dev/the-pdf-export-was-the-easy-part-building-a-browser-only-tiled-poster-generator</link><guid isPermaLink="true">https://gaven-builds.hashnode.dev/the-pdf-export-was-the-easy-part-building-a-browser-only-tiled-poster-generator</guid><dc:creator><![CDATA[Gaven]]></dc:creator><pubDate>Fri, 24 Jul 2026 15:55:49 GMT</pubDate><content:encoded><![CDATA[<p>A tiled-poster generator sounds like a small file-conversion project: load an image, divide it into rectangles, create one PDF page for each rectangle, and let the user print the result.</p>
<p>That description is correct, but it hides the difficult part. The PDF file is only the final container. Before generating it, the application must make image pixels, physical paper, printable margins, page overlap, preview geometry, and browser memory limits agree with one another.</p>
<p>I ran into these problems while building <a href="https://rasterbator.app/">Rasterbator.app</a>, a browser-based tool that converts a local image into a multi-page poster PDF. This article explains the layout model behind the tool and several implementation decisions that mattered more than the export button.</p>
<h2>Start with physical units, not canvas pixels</h2>
<p>The browser preview is useful, but it should not be the source of truth. A user is ultimately printing sheets measured in millimetres or inches, while a PDF library expects points.</p>
<p>The conversion is:</p>
<pre><code class="language-js">const MM_TO_PTS = 72 / 25.4;
</code></pre>
<p>Every paper, margin, overlap, and final-poster calculation can then use PDF points. The canvas becomes a visual projection of that model rather than an independent layout system.</p>
<p>For a page with a margin on every side:</p>
<pre><code class="language-js">const printableWidth = paperWidth - 2 * margin;
const printableHeight = paperHeight - 2 * margin;
</code></pre>
<p>Overlap changes the distance between neighbouring pages. If two sheets share an overlap strip, the next tile does not start one complete printable width away.</p>
<pre><code class="language-js">const logicalWidth = printableWidth - overlap;
const logicalHeight = printableHeight - overlap;
</code></pre>
<p>For a poster that is <code>pagesWide × pagesHigh</code>, the physical dimensions become:</p>
<pre><code class="language-js">const posterWidth = pagesWide * logicalWidth + overlap;
const posterHeight = pagesHigh * logicalHeight + overlap;
</code></pre>
<p>The final <code>+ overlap</code> matters. There are <code>n - 1</code> shared transitions across <code>n</code> sheets, not <code>n</code> completely separate overlap deductions.</p>
<h2>Preserve image aspect ratio before rounding page counts</h2>
<p>Users may specify the page width, page height, or both. When only one dimension is fixed, calculate the poster's continuous physical size first and round the resulting page count afterward.</p>
<p>Suppose the user chooses a width of four pages. The continuous poster width is known. The image aspect ratio determines its physical height, and the logical page height determines the fractional page count.</p>
<pre><code class="language-js">const imageAspect = imageHeight / imageWidth;

const posterWidth = pagesWide * logicalWidth + overlap;
const posterHeight = posterWidth * imageAspect;
const pagesHighFloat = (posterHeight - overlap) / logicalHeight;
const pagesHigh = Math.ceil(pagesHighFloat);
</code></pre>
<p>Rounding too early changes the image ratio or silently stretches the result. After rounding, the page grid may be slightly larger than the intended physical poster. That extra area must be treated as an offset or crop region rather than as permission to distort the image.</p>
<p>In Rasterbator.app, the grid is centred horizontally while retaining the intended vertical alignment. The key is that the image dimensions and the sheet grid remain two related but distinct rectangles.</p>
<h2>A tile needs source and destination geometry</h2>
<p>For each output page, the application needs more than a row and column number. It needs to know:</p>
<ol>
<li>which part of the poster belongs on the sheet;</li>
<li>which part of the source image maps to that poster rectangle;</li>
<li>where the result sits inside the printable area;</li>
<li>whether overlap, crop marks, or page labels extend beyond the main image region.</li>
</ol>
<p>A useful mental model is:</p>
<pre><code class="language-text">source image pixels
        ↓ crop/fit mapping
poster-space physical coordinates
        ↓ page-window intersection
page-space PDF coordinates
</code></pre>
<p>Keeping these stages separate avoids a common bug: using preview-canvas coordinates as though they were physical output coordinates. Preview dimensions change with the viewport. The PDF must not.</p>
<h2>Halftone effects create another grid</h2>
<p>The tool optionally renders circles, squares, or lines rather than copying the source image directly. That introduces a second grid: the halftone sampling grid.</p>
<p>A nominal grid size rarely divides the final poster dimensions perfectly. If the application blindly steps by the requested size, the last row or column may be clipped or visibly narrower. One solution is to calculate the number of complete cells and derive a corrected cell size that fits the poster.</p>
<pre><code class="language-js">const columns = Math.floor(posterWidth / requestedCellSize);
const rows = Math.floor(posterHeight / requestedCellSize);

const exactCellWidth = posterWidth / Math.max(columns, 1);
const exactCellHeight = posterHeight / Math.max(rows, 1);
const cellSize = Math.min(exactCellWidth, exactCellHeight);
</code></pre>
<p>The remaining space can then be centred as an offset. This makes the halftone pattern look intentional instead of leaving a small accidental strip at an edge.</p>
<h2>Browser memory is part of the product design</h2>
<p>A compressed phone photo may be only a few megabytes on disk but much larger after decoding. A 6000 × 4000 RGBA image occupies roughly 96 MB before considering canvas buffers, cropped images, encoded JPEG or PNG data, PDF objects, previews, and temporary copies.</p>
<p>A browser-only workflow therefore needs product limits, not just exception handling.</p>
<p>The application should estimate work before export and communicate when a job is unreasonable. Desktop and mobile limits should differ because their memory ceilings and process-killing behaviour differ. It is better to explain that a 100-page job is too large on a phone than to let the tab disappear at 78%.</p>
<p>Other practical choices include:</p>
<ul>
<li>dynamically importing the PDF library only when needed;</li>
<li>processing pages sequentially rather than creating every canvas at once;</li>
<li>releasing temporary canvas references promptly;</li>
<li>avoiding multiple full-resolution image copies;</li>
<li>reporting page-by-page progress;</li>
<li>keeping the preview lower resolution than the final source data.</li>
</ul>
<h2>Printing requires trust, not only correctness</h2>
<p>Even mathematically correct output can feel unsafe to print. The user is spending paper, ink, tape, and time, so the interface should answer physical questions before export:</p>
<ul>
<li>What will the assembled width and height be?</li>
<li>Is the preview showing the full sheet or only the printable area?</li>
<li>Where will pages overlap?</li>
<li>Will the image be cropped, fitted, or stretched?</li>
<li>Are crop marks and page-position labels included?</li>
<li>Which printer setting preserves the intended scale?</li>
</ul>
<p>This is why Rasterbator.app exposes the page grid, margins, overlap, final dimensions, crop marks, and labels instead of hiding them behind a single “make poster” button.</p>
<h2>The larger lesson</h2>
<p>Small browser utilities often look simple because their complexity is compressed into a narrow workflow. The hardest engineering work may sit between familiar APIs: image decoding, canvas rendering, PDF generation, and printing are each manageable, but the coordinate contracts between them determine whether the result is usable.</p>
<p>For file-based tools, define one authoritative model, convert units explicitly, preserve continuous geometry before rounding, and treat resource limits as part of the interface. The export library should be the last step, not the architecture.</p>
<p>The live tool is available at <a href="https://rasterbator.app/">Rasterbator.app</a>. I am currently improving the first-run explanations, particularly around overlap and final physical size, and would value feedback from anyone who has built browser-based image or printing workflows.</p>
]]></content:encoded></item></channel></rss>