<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Graeme – IMG.LY Blog</title><description>Graeme has spent more than 20 years in IT, across enterprise vendor support, networking, database administration, software development and DevOps. He has been writing about that work for the last 10 years, and when not working or writing, he can be found in the ocean or playing bass in various heavy metal projects.</description><link>https://img.ly/blog/author/graeme/</link><language>en-us</language><image><url>https://img.ly/apple-touch-icon.png</url><title>Graeme – IMG.LY Blog</title><link>https://img.ly/blog/author/graeme/</link></image><atom:link href="https://img.ly/blog/author/graeme/rss.xml" rel="self" type="application/rss+xml"/><generator>Astro</generator><lastBuildDate>Fri, 02 Oct 2026 15:22:09 GMT</lastBuildDate><ttl>60</ttl><item><title>How to Edit AI-Generated Designs Without Regenerating Them</title><link>https://img.ly/blog/edit-ai-output-without-regenerating/</link><guid isPermaLink="true">https://img.ly/blog/edit-ai-output-without-regenerating/</guid><description>Create layered AI images as editable scenes where you can change text, swap logos and resize without another generation call.</description><pubDate>Mon, 28 Sep 2026 09:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;what-you-will-build-ai-generated-ads-as-editable-layers&quot;&gt;What You Will Build: AI-Generated Ads as Editable Layers&lt;/h2&gt;
&lt;p&gt;This guide will show you how to create layered images with a web app that turns a single prompt into an ad made of four separate layers. You’ll have a generated background, a product cutout, a real text headline, and a locked logo.&lt;/p&gt;
&lt;p&gt;In the app, you can retype the headline, drag the product, swap the logo for another approved version, and resize the ad to the ratio you’re after, like 1:1, 9:16 or 16:9. The surprise is that none of that needs another generation call. When the background isn’t quite right, the app regenerates only that layer and leaves the rest alone.&lt;/p&gt;
&lt;p&gt;Demo: &lt;a href=&quot;https://labs.imgly.dev/p/r24dbfde_editable-ai-ads&quot;&gt;labs.imgly.dev/p/r24dbfde_editable-ai-ads&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Repository: &lt;a href=&quot;https://github.com/imgly/editable-ai-ads&quot;&gt;github.com/imgly/editable-ai-ads&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This article is for anyone that builds their own marketing tools, or who are adding AI image generation to their apps and SaaS platforms. If you already have image generation in place but are tired of generating entire images when all you need is a few tweaks here and there then you’ve come to the right place.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Four regenerations of the same ad next to one ad edited as layers&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; sizes=&quot;(min-width: 2046px) 2046px, 100vw&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2046&quot; height=&quot;1090&quot; src=&quot;https://img.ly/_astro/regenerated-four-times-vs-edited-as-layers.BcgAFIAx_ZpG7CX.webp&quot; srcset=&quot;/_astro/regenerated-four-times-vs-edited-as-layers.BcgAFIAx_Z1uBd1x.webp 640w, /_astro/regenerated-four-times-vs-edited-as-layers.BcgAFIAx_2hNd6O.webp 750w, /_astro/regenerated-four-times-vs-edited-as-layers.BcgAFIAx_Z23PsEs.webp 828w, /_astro/regenerated-four-times-vs-edited-as-layers.BcgAFIAx_2400da.webp 1080w, /_astro/regenerated-four-times-vs-edited-as-layers.BcgAFIAx_2ibSns.webp 1280w, /_astro/regenerated-four-times-vs-edited-as-layers.BcgAFIAx_JMIgK.webp 1668w, /_astro/regenerated-four-times-vs-edited-as-layers.BcgAFIAx_ZpG7CX.webp 2046w&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;why-regenerate-with-a-better-prompt-is-the-wrong-answer-for-marketers&quot;&gt;Why “Regenerate With a Better Prompt” Is the Wrong Answer for Marketers&lt;/h2&gt;
&lt;p&gt;An image model generally gives you one flat picture with a single layer, which means that the headline, product, logo and backdrop are all pixels in the same bitmap. Making small changes with regular image generation tools means you roll the dice and hope that the next image makes the right changes without flipping the script and changing everything else.&lt;/p&gt;
&lt;p&gt;For this article, the same full-ad prompt went to Ideogram V3 four times. This is a useful way to explore designs in parallel, letting you focus on your initial idea until you find the perfect starting point. From here, what you really want is to be able to edit your idea and tweak it, not get a completely different picture. Using the demo app, you can move the headline or change a word while keeping everything else exactly as it was.&lt;/p&gt;
&lt;p&gt;The first round of generation produces layers, so the user can change specific elements of the design piece by piece, and the export keeps everything else exactly as it was except for the edit.&lt;/p&gt;
&lt;h2 id=&quot;editing-ai-output-regenerate-inpaint-or-generate-layers-into-a-scene&quot;&gt;Editing AI Output: Regenerate, Inpaint, or Generate Layers Into a Scene&lt;/h2&gt;
&lt;p&gt;The table below shows you all of the generation options for your next ad, and how it all fits together.&lt;/p&gt;

































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Property&lt;/th&gt;&lt;th&gt;Regenerate the whole image&lt;/th&gt;&lt;th&gt;Inpaint a region&lt;/th&gt;&lt;th&gt;Generate layers into a scene&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Cost per iteration (model calls)&lt;/td&gt;&lt;td&gt;1 per attempt&lt;/td&gt;&lt;td&gt;1 per attempt&lt;/td&gt;&lt;td&gt;0 for text, layout, logo and size changes; 1 to regenerate one layer&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Time per iteration&lt;/td&gt;&lt;td&gt;22 to 26 s per attempt in the test run&lt;/td&gt;&lt;td&gt;Not tested with a mask; one image-to-image edit of the flat ad took 21 s&lt;/td&gt;&lt;td&gt;Milliseconds for layout, logo and size changes; 26 to 38 s to regenerate one layer&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Brand consistency across iterations&lt;/td&gt;&lt;td&gt;Low: product, logo and layout changed between attempts&lt;/td&gt;&lt;td&gt;Medium: pixels outside the mask are kept, pixels inside are redrawn&lt;/td&gt;&lt;td&gt;High: layers you do not edit do not change&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Text stays real text&lt;/td&gt;&lt;td&gt;No, it is pixels&lt;/td&gt;&lt;td&gt;No, it is pixels&lt;/td&gt;&lt;td&gt;Yes, it is a text block&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;User can move and resize elements&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;td&gt;Yes, block by block&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;One-click resize to other formats&lt;/td&gt;&lt;td&gt;No, one generation per format&lt;/td&gt;&lt;td&gt;No, one generation per format&lt;/td&gt;&lt;td&gt;Yes, the layout runs again on the same blocks&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Logo stays exact&lt;/td&gt;&lt;td&gt;No, the model draws it&lt;/td&gt;&lt;td&gt;Only if the mask leaves it out&lt;/td&gt;&lt;td&gt;Yes, it is a file from the brand kit&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Works with any image model&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Only models that support inpainting&lt;/td&gt;&lt;td&gt;Any text-to-image model to generate; an image-to-image model to regenerate a layer&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;What you have to build&lt;/td&gt;&lt;td&gt;A prompt form&lt;/td&gt;&lt;td&gt;A prompt form and a masking tool&lt;/td&gt;&lt;td&gt;A generation step, a scene composer and an editor&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The first two approaches spend a model call on every change, which is perfect for prototyping your ideas until you find the design that you are looking for. They don’t keep the logo or the layout the same, so you will get variations until you’re happy with the general look and feel. The cool feature is that layers only use model calls where new pixels are needed, and every other change is an ordinary edit that only changes when you decide.&lt;/p&gt;
&lt;h2 id=&quot;step-1-generate-the-parts-not-the-picture&quot;&gt;Step 1: Generate the Parts, Not the Picture&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;generateParts&lt;/code&gt; in &lt;code&gt;src/generate.ts&lt;/code&gt; asks for four parts instead of one ad. Each of these parts comes from a different source, but the three that take time run in parallel. Instead of waiting for three separate generations, it only takes as long as the slowest render.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/generate.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; generateParts&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  client&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; GatewayClient&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  brief&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; AdBrief&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;AdParts&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  // The three sources are independent, so they run at the same time.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;span&gt;background&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;headline&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;product&lt;/span&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; Promise&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;all&lt;/span&gt;&lt;span&gt;([&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    generateBackground&lt;/span&gt;&lt;span&gt;(client, brief.backgroundPrompt),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    generateHeadline&lt;/span&gt;&lt;span&gt;(client, brief),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    cutOutProduct&lt;/span&gt;&lt;span&gt;(brief.productImage),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; { background, product, headline, logo: &lt;/span&gt;&lt;span&gt;BRAND&lt;/span&gt;&lt;span&gt;.logo };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The background is one text-to-image call through IMG.LY’s AI gateway, and the demo uses the Ideogram V3 model. The &lt;code&gt;buildInput&lt;/code&gt; helper in &lt;code&gt;src/gateway.ts&lt;/code&gt; reads the model’s input schema and passes only the fields that model accepts, so a different model ID doesn’t break the call.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/generate.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; generateBackground&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  client&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; GatewayClient&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  prompt&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ImagePart&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  // Ideogram V3 takes `format` (1:1, 4:3, 16:9, 3:4, 9:16) and a `style`.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; input&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; buildInput&lt;/span&gt;&lt;span&gt;(client, &lt;/span&gt;&lt;span&gt;MODELS&lt;/span&gt;&lt;span&gt;.text2image, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    prompt,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    format: &lt;/span&gt;&lt;span&gt;&apos;1:1&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    style: &lt;/span&gt;&lt;span&gt;&apos;REALISTIC&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; uri&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;generate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MODELS&lt;/span&gt;&lt;span&gt;.text2image, input, {});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; measureImage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; persistImage&lt;/span&gt;&lt;span&gt;(uri));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The headline comes from a separate text model, and as you would expect, arrives as a string. It only becomes pixels at export, which is why we can edit the headline and tweak it as its own property.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/generate.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; prompt&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  `Write one advertising headline for ${&lt;/span&gt;&lt;span&gt;brief&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;productName&lt;/span&gt;&lt;span&gt;}. `&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  `Audience: ${&lt;/span&gt;&lt;span&gt;brief&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;audience&lt;/span&gt;&lt;span&gt;}. `&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &apos;At most six words. No quotation marks. No punctuation at the end. &apos;&lt;/span&gt;&lt;span&gt; +&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &apos;Reply with the headline only.&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; input&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; { messages: [{ role: &lt;/span&gt;&lt;span&gt;&apos;user&apos;&lt;/span&gt;&lt;span&gt;, content: prompt }] };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let&lt;/span&gt;&lt;span&gt; text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; &apos;&apos;&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; chunk&lt;/span&gt;&lt;span&gt; of&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;generateStream&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MODELS&lt;/span&gt;&lt;span&gt;.text2text, input, {})) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  text &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; chunk;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The product doesn’t need a model call. &lt;code&gt;@imgly/background-removal&lt;/code&gt; cuts it out in the browser, and a short canvas step trims the transparent edges so the block is the size of the product instead of the original photo.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/generate.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; cutOutProduct&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Blob&lt;/span&gt;&lt;span&gt; |&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;ImagePart&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; cutout&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; removeBackground&lt;/span&gt;&lt;span&gt;(image);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; trimmed&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; cropToContent&lt;/span&gt;&lt;span&gt;(cutout);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; measureImage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;URL&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createObjectURL&lt;/span&gt;&lt;span&gt;(trimmed));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But what about the logo? Well, the logo isn’t generated at all. It comes from the brand kit in &lt;code&gt;src/brand.ts&lt;/code&gt;, so it’s the same file every time without any surprises.&lt;/p&gt;
&lt;p&gt;In the test run, the parts took 52 seconds and two model calls. The text model returned “Silence Your Commute With Northwind Headphones”. If you aren’t happy with that then you could shorten it in step 3.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The generated background, product cutout, headline and logo in the demo panel&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; sizes=&quot;(min-width: 2046px) 2046px, 100vw&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2046&quot; height=&quot;1279&quot; src=&quot;https://img.ly/_astro/generated-parts-in-demo-panel.TvKqtaxx_mUYKS.webp&quot; srcset=&quot;/_astro/generated-parts-in-demo-panel.TvKqtaxx_Z2onGDr.webp 640w, /_astro/generated-parts-in-demo-panel.TvKqtaxx_1lu5zO.webp 750w, /_astro/generated-parts-in-demo-panel.TvKqtaxx_Z2jNFrv.webp 828w, /_astro/generated-parts-in-demo-panel.TvKqtaxx_Z2aevCB.webp 1080w, /_astro/generated-parts-in-demo-panel.TvKqtaxx_Z23daJR.webp 1280w, /_astro/generated-parts-in-demo-panel.TvKqtaxx_Z1BrzKA.webp 1668w, /_astro/generated-parts-in-demo-panel.TvKqtaxx_mUYKS.webp 2046w&quot;&gt;&lt;/p&gt;
&lt;p&gt;To customize this even more, you can change the model IDs in &lt;code&gt;MODELS&lt;/code&gt; and load the brand kit from your customer’s settings instead.&lt;/p&gt;
&lt;h2 id=&quot;step-2-compose-the-parts-into-an-editable-scene&quot;&gt;Step 2: Compose the Parts Into an Editable Scene&lt;/h2&gt;
&lt;p&gt;Let’s recap. &lt;code&gt;composeScene&lt;/code&gt; in &lt;code&gt;src/scene.ts&lt;/code&gt; creates a page and turns each part into a block. The background and the product become graphic blocks with image fills, and the headline becomes a text block in the specified brand typeface and color. The logo is a third image block, and every block gets a name, so future steps can find it without tracking IDs.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/scene.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; background&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; addImageBlock&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  engine,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  page,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  parts.background,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  LAYER&lt;/span&gt;&lt;span&gt;.background&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;engine.block.&lt;/span&gt;&lt;span&gt;setContentFillMode&lt;/span&gt;&lt;span&gt;(background, &lt;/span&gt;&lt;span&gt;&apos;Cover&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; product&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; addImageBlock&lt;/span&gt;&lt;span&gt;(engine, page, parts.product, &lt;/span&gt;&lt;span&gt;LAYER&lt;/span&gt;&lt;span&gt;.product);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;engine.block.&lt;/span&gt;&lt;span&gt;setContentFillMode&lt;/span&gt;&lt;span&gt;(product, &lt;/span&gt;&lt;span&gt;&apos;Contain&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; headline&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; engine.block.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;text&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;engine.block.&lt;/span&gt;&lt;span&gt;setName&lt;/span&gt;&lt;span&gt;(headline, &lt;/span&gt;&lt;span&gt;LAYER&lt;/span&gt;&lt;span&gt;.headline);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;engine.block.&lt;/span&gt;&lt;span&gt;replaceText&lt;/span&gt;&lt;span&gt;(headline, parts.headline);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;engine.block.&lt;/span&gt;&lt;span&gt;setFont&lt;/span&gt;&lt;span&gt;(headline, &lt;/span&gt;&lt;span&gt;BRAND&lt;/span&gt;&lt;span&gt;.headline.fontUri, &lt;/span&gt;&lt;span&gt;BRAND&lt;/span&gt;&lt;span&gt;.headline.typeface);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;engine.block.&lt;/span&gt;&lt;span&gt;setTextColor&lt;/span&gt;&lt;span&gt;(headline, &lt;/span&gt;&lt;span&gt;BRAND&lt;/span&gt;&lt;span&gt;.headline.color);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;engine.block.&lt;/span&gt;&lt;span&gt;setWidthMode&lt;/span&gt;&lt;span&gt;(headline, &lt;/span&gt;&lt;span&gt;&apos;Absolute&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;engine.block.&lt;/span&gt;&lt;span&gt;setHeightMode&lt;/span&gt;&lt;span&gt;(headline, &lt;/span&gt;&lt;span&gt;&apos;Auto&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;engine.block.&lt;/span&gt;&lt;span&gt;appendChild&lt;/span&gt;&lt;span&gt;(page, headline);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; logo&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; addImageBlock&lt;/span&gt;&lt;span&gt;(engine, page, parts.logo, &lt;/span&gt;&lt;span&gt;LAYER&lt;/span&gt;&lt;span&gt;.logo);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;engine.block.&lt;/span&gt;&lt;span&gt;setContentFillMode&lt;/span&gt;&lt;span&gt;(logo, &lt;/span&gt;&lt;span&gt;&apos;Contain&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Image blocks carry the image’s width and height in their fill’s source set, so the engine can lay out the block before the image has loaded.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/scene.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;function&lt;/span&gt;&lt;span&gt; addImageBlock&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  engine&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CreativeEngine&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  page&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  image&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; ImagePart&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  name&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; number&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; block&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; engine.block.&lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;graphic&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  engine.block.&lt;/span&gt;&lt;span&gt;setShape&lt;/span&gt;&lt;span&gt;(block, engine.block.&lt;/span&gt;&lt;span&gt;createShape&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;rect&apos;&lt;/span&gt;&lt;span&gt;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; fill&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; engine.block.&lt;/span&gt;&lt;span&gt;createFill&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;&apos;image&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  engine.block.&lt;/span&gt;&lt;span&gt;setSourceSet&lt;/span&gt;&lt;span&gt;(fill, &lt;/span&gt;&lt;span&gt;&apos;fill/image/sourceSet&apos;&lt;/span&gt;&lt;span&gt;, [&lt;/span&gt;&lt;span&gt;toSource&lt;/span&gt;&lt;span&gt;(image)]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  engine.block.&lt;/span&gt;&lt;span&gt;setFill&lt;/span&gt;&lt;span&gt;(block, fill);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  engine.block.&lt;/span&gt;&lt;span&gt;setName&lt;/span&gt;&lt;span&gt;(block, name);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  engine.block.&lt;/span&gt;&lt;span&gt;appendChild&lt;/span&gt;&lt;span&gt;(page, block);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; block;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A source entry takes exactly three fields, &lt;code&gt;uri&lt;/code&gt;, &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt;; the engine rejects an entry with anything else. Brand kit logos also carry an ID and a label, so the &lt;code&gt;toSource&lt;/code&gt; helper in &lt;code&gt;src/scene.ts&lt;/code&gt; strips those out before they reach the engine.&lt;/p&gt;
&lt;p&gt;The composition step took under half a second and no model calls, which is pretty quick if you ask me.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The four parts composed into one editable scene&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; sizes=&quot;(min-width: 2046px) 2046px, 100vw&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2046&quot; height=&quot;1279&quot; src=&quot;https://img.ly/_astro/parts-composed-into-editable-scene.BktV-CG0_15Qufl.webp&quot; srcset=&quot;/_astro/parts-composed-into-editable-scene.BktV-CG0_AkBaY.webp 640w, /_astro/parts-composed-into-editable-scene.BktV-CG0_2bu5ow.webp 750w, /_astro/parts-composed-into-editable-scene.BktV-CG0_ZK1x0d.webp 828w, /_astro/parts-composed-into-editable-scene.BktV-CG0_UP9P5.webp 1080w, /_astro/parts-composed-into-editable-scene.BktV-CG0_1cdaJJ.webp 1280w, /_astro/parts-composed-into-editable-scene.BktV-CG0_2hge3u.webp 1668w, /_astro/parts-composed-into-editable-scene.BktV-CG0_15Qufl.webp 2046w&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;step-3-let-users-edit-text-position-and-images&quot;&gt;Step 3: Let Users Edit Text, Position and Images&lt;/h2&gt;
&lt;p&gt;CE.SDK controls what a user can change by using scopes. Each scope covers one action, like moving a block, replacing its image, or deleting it. &lt;code&gt;setEditable&lt;/code&gt; sets each scope to defer to the block, then turns it on or off for that block.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/scene.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; EDIT_SCOPES&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &apos;layer/move&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &apos;layer/resize&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &apos;layer/rotate&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &apos;layer/crop&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &apos;fill/change&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &apos;fill/changeType&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &apos;lifecycle/destroy&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  &apos;lifecycle/duplicate&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;] &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; const&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; setEditable&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  engine&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CreativeEngine&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  block&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; number&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  editable&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; boolean&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; void&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; scope&lt;/span&gt;&lt;span&gt; of&lt;/span&gt;&lt;span&gt; EDIT_SCOPES&lt;/span&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    engine.editor.&lt;/span&gt;&lt;span&gt;setGlobalScope&lt;/span&gt;&lt;span&gt;(scope, &lt;/span&gt;&lt;span&gt;&apos;Defer&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    engine.block.&lt;/span&gt;&lt;span&gt;setScopeEnabled&lt;/span&gt;&lt;span&gt;(block, scope, editable);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;composeScene&lt;/code&gt; makes the background, product and headline all editable, except for the logo. The demo calls it for every block, and once a scope defers to blocks, a block with no setting of its own is locked.&lt;/p&gt;
&lt;p&gt;In the editor, the user clicks the headline and types exactly what they want and the product can be dragged and resized. At this stage the logo can’t be moved, resized, replaced or deleted.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The shortened headline selected in the editor&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; sizes=&quot;(min-width: 2046px) 2046px, 100vw&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2046&quot; height=&quot;1279&quot; src=&quot;https://img.ly/_astro/shortened-headline-selected.BciR8pP4_2mWaQ3.webp&quot; srcset=&quot;/_astro/shortened-headline-selected.BciR8pP4_1IYONa.webp 640w, /_astro/shortened-headline-selected.BciR8pP4_vAsaw.webp 750w, /_astro/shortened-headline-selected.BciR8pP4_ZkXXMg.webp 828w, /_astro/shortened-headline-selected.BciR8pP4_Z1uxxfH.webp 1080w, /_astro/shortened-headline-selected.BciR8pP4_Z1D13GP.webp 1280w, /_astro/shortened-headline-selected.BciR8pP4_Z1el9E4.webp 1668w, /_astro/shortened-headline-selected.BciR8pP4_2mWaQ3.webp 2046w&quot;&gt;&lt;/p&gt;
&lt;p&gt;The brand kit has two approved logo versions, one dark and one white. The user can’t put a different image into the logo block, but the app can switch between the approved versions. In the example test run, the swap worked while the logo’s scopes were still off.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/scene.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; swapLogo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;engine&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CreativeEngine&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;variant&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; LogoVariant&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; void&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; logo&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; findLayer&lt;/span&gt;&lt;span&gt;(engine, &lt;/span&gt;&lt;span&gt;LAYER&lt;/span&gt;&lt;span&gt;.logo);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; fill&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; engine.block.&lt;/span&gt;&lt;span&gt;getFill&lt;/span&gt;&lt;span&gt;(logo);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  engine.block.&lt;/span&gt;&lt;span&gt;setSourceSet&lt;/span&gt;&lt;span&gt;(fill, &lt;/span&gt;&lt;span&gt;&apos;fill/image/sourceSet&apos;&lt;/span&gt;&lt;span&gt;, [&lt;/span&gt;&lt;span&gt;toSource&lt;/span&gt;&lt;span&gt;(variant)]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img alt=&quot;The logo switched to the white version&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; sizes=&quot;(min-width: 2046px) 2046px, 100vw&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2046&quot; height=&quot;1279&quot; src=&quot;https://img.ly/_astro/logo-swapped-to-white.Bbf79TJa_Z24eMPk.webp&quot; srcset=&quot;/_astro/logo-swapped-to-white.Bbf79TJa_Z5of8A.webp 640w, /_astro/logo-swapped-to-white.Bbf79TJa_ZRdLYA.webp 750w, /_astro/logo-swapped-to-white.Bbf79TJa_ZzS0WV.webp 828w, /_astro/logo-swapped-to-white.Bbf79TJa_ZmyjEC.webp 1080w, /_astro/logo-swapped-to-white.Bbf79TJa_Z139CkQ.webp 1280w, /_astro/logo-swapped-to-white.Bbf79TJa_Z1LNIC8.webp 1668w, /_astro/logo-swapped-to-white.Bbf79TJa_Z24eMPk.webp 2046w&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;step-4-regenerate-one-layer-only-with-image-to-image&quot;&gt;Step 4: Regenerate One Layer Only With Image-to-Image&lt;/h2&gt;
&lt;p&gt;If you aren’t happy with the backdrop, you can change it. &lt;code&gt;regenerateBackground&lt;/code&gt; in &lt;code&gt;src/regenerate-layer.ts&lt;/code&gt; exports the background block as a PNG, uploads it to the gateway, and sends it to an image-to-image model with a new prompt. The output from that call replaces the image in that block’s fill, and the block keeps its position, size and place in the stack. The other three blocks aren’t touched by this step, so they stay the same.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/regenerate-layer.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; background&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; findLayer&lt;/span&gt;&lt;span&gt;(engine, &lt;/span&gt;&lt;span&gt;LAYER&lt;/span&gt;&lt;span&gt;.background);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; current&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; engine.block.&lt;/span&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt;(background, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  mimeType: &lt;/span&gt;&lt;span&gt;&apos;image/png&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; upload&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;upload&lt;/span&gt;&lt;span&gt;(current, &lt;/span&gt;&lt;span&gt;&apos;image/png&apos;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; input&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; buildInput&lt;/span&gt;&lt;span&gt;(client, &lt;/span&gt;&lt;span&gt;MODELS&lt;/span&gt;&lt;span&gt;.image2image, {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  prompt,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  image_urls: [upload.asset_url],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  format: &lt;/span&gt;&lt;span&gt;&apos;auto&apos;&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; uri&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; client.&lt;/span&gt;&lt;span&gt;generate&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;MODELS&lt;/span&gt;&lt;span&gt;.image2image, input, {});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; image&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; measureImage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; persistImage&lt;/span&gt;&lt;span&gt;(uri));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; fill&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; engine.block.&lt;/span&gt;&lt;span&gt;getFill&lt;/span&gt;&lt;span&gt;(background);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;engine.block.&lt;/span&gt;&lt;span&gt;setSourceSet&lt;/span&gt;&lt;span&gt;(fill, &lt;/span&gt;&lt;span&gt;&apos;fill/image/sourceSet&apos;&lt;/span&gt;&lt;span&gt;, [image]);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The demo uses NanoBanana Pro Edit. In the test run, the prompt “Same backdrop, cooler blue tones, evening light” turned the amber studio blue in 38 seconds and only used one model call. The headline, product, and the logo stayed exactly as they were.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;Only the background changed&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; sizes=&quot;(min-width: 2046px) 2046px, 100vw&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2046&quot; height=&quot;1279&quot; src=&quot;https://img.ly/_astro/background-regenerated-only.DlXA9Tgk_18meIf.webp&quot; srcset=&quot;/_astro/background-regenerated-only.DlXA9Tgk_12ww55.webp 640w, /_astro/background-regenerated-only.DlXA9Tgk_ZaQPxy.webp 750w, /_astro/background-regenerated-only.DlXA9Tgk_Z12rhvl.webp 828w, /_astro/background-regenerated-only.DlXA9Tgk_2l3Eqq.webp 1080w, /_astro/background-regenerated-only.DlXA9Tgk_2cA8Yi.webp 1280w, /_astro/background-regenerated-only.DlXA9Tgk_Z2sV5LR.webp 1668w, /_astro/background-regenerated-only.DlXA9Tgk_18meIf.webp 2046w&quot;&gt;&lt;/p&gt;
&lt;p&gt;Sending only the background matters for the overall look and feel of your image. If the same model and prompt were run on one of the flat ads from the regeneration test, it would keep the layout, but possibly add unwanted effects like the headphones picking up the blue tint along with the wall. With layers, the product isn’t in the image that the model sees, so it can’t interfere with it.&lt;/p&gt;
&lt;h2 id=&quot;step-5-resize-the-same-design-to-11-916-and-169&quot;&gt;Step 5: Resize the Same Design to 1:1, 9:16 and 16:9&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;resizeTo&lt;/code&gt; in &lt;code&gt;src/resize.ts&lt;/code&gt; changes the page size and runs the layout again on the same blocks, and nothing is regenerated. The headline wraps to the new width because it’s text, and &lt;code&gt;layoutPage&lt;/code&gt; in &lt;code&gt;src/scene.ts&lt;/code&gt; stacks the headline above the product on tall pages and places them side by side on wide ones.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/resize.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; resizeTo&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  cesdk&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CreativeEditorSDK&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  format&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; AdFormat&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;void&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; engine&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; cesdk.engine;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; page&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; currentPage&lt;/span&gt;&lt;span&gt;(engine);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  engine.block.&lt;/span&gt;&lt;span&gt;setWidth&lt;/span&gt;&lt;span&gt;(page, format.width);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  engine.block.&lt;/span&gt;&lt;span&gt;setHeight&lt;/span&gt;&lt;span&gt;(page, format.height);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  layoutPage&lt;/span&gt;&lt;span&gt;(engine, page);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  void&lt;/span&gt;&lt;span&gt; engine.scene.&lt;/span&gt;&lt;span&gt;zoomToBlock&lt;/span&gt;&lt;span&gt;(page, { padding: &lt;/span&gt;&lt;span&gt;40&lt;/span&gt;&lt;span&gt;, animate: &lt;/span&gt;&lt;span&gt;false&lt;/span&gt;&lt;span&gt; });&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each resize took 2 milliseconds in the test run.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The same ad at 9:16&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; sizes=&quot;(min-width: 2046px) 2046px, 100vw&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2046&quot; height=&quot;1279&quot; src=&quot;https://img.ly/_astro/resized-9x16.Cs-o-_Js_Z1iDgqY.webp&quot; srcset=&quot;/_astro/resized-9x16.Cs-o-_Js_Z1Lt1cf.webp 640w, /_astro/resized-9x16.Cs-o-_Js_16NTaj.webp 750w, /_astro/resized-9x16.Cs-o-_Js_Z12nrEM.webp 828w, /_astro/resized-9x16.Cs-o-_Js_1UOvus.webp 1080w, /_astro/resized-9x16.Cs-o-_Js_Z26q9nQ.webp 1280w, /_astro/resized-9x16.Cs-o-_Js_Z1UXEJN.webp 1668w, /_astro/resized-9x16.Cs-o-_Js_Z1iDgqY.webp 2046w&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;The same ad at 16:9&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; sizes=&quot;(min-width: 2046px) 2046px, 100vw&quot; data-astro-image=&quot;constrained&quot; data-astro-image-pos=&quot;center&quot; width=&quot;2046&quot; height=&quot;1279&quot; src=&quot;https://img.ly/_astro/resized-16x9.DPfImeWe_2NY8b.webp&quot; srcset=&quot;/_astro/resized-16x9.DPfImeWe_1xj4Ys.webp 640w, /_astro/resized-16x9.DPfImeWe_ZDA8rU.webp 750w, /_astro/resized-16x9.DPfImeWe_2hoDvU.webp 828w, /_astro/resized-16x9.DPfImeWe_Z1MTmJj.webp 1080w, /_astro/resized-16x9.DPfImeWe_ZJWSNG.webp 1280w, /_astro/resized-16x9.DPfImeWe_ZzvpaD.webp 1668w, /_astro/resized-16x9.DPfImeWe_2NY8b.webp 2046w&quot;&gt;&lt;/p&gt;
&lt;p&gt;The layout rules in the demo are fractions of the page. For your own ads, replace &lt;code&gt;layoutPage&lt;/code&gt; with rules that match your templates.&lt;/p&gt;
&lt;h2 id=&quot;step-6-export-and-save-for-later-edits&quot;&gt;Step 6: Export and Save for Later Edits&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;src/export.ts&lt;/code&gt; renders the current page to PNG or the whole scene to PDF. The &lt;code&gt;exportAllFormats&lt;/code&gt; function resizes to each format, renders it, and returns the page to the size the user wants. &lt;code&gt;saveScene&lt;/code&gt; turns the scene into a string, and &lt;code&gt;saveArchive&lt;/code&gt; writes the same scene as a zip with the images inside it. Either one lets the user reopen the ad and edit the same blocks. Exporting all three formats took 3.1 seconds.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/export.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; exportAllFormats&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  cesdk&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CreativeEditorSDK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Record&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;AdFormatId&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Blob&lt;/span&gt;&lt;span&gt;&gt;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; before&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; AdFormat&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; currentFormat&lt;/span&gt;&lt;span&gt;(cesdk.engine) &lt;/span&gt;&lt;span&gt;??&lt;/span&gt;&lt;span&gt; FORMATS&lt;/span&gt;&lt;span&gt;.square;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; output&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; {} &lt;/span&gt;&lt;span&gt;as&lt;/span&gt;&lt;span&gt; Record&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;AdFormatId&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;Blob&lt;/span&gt;&lt;span&gt;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  for&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;const&lt;/span&gt;&lt;span&gt; format&lt;/span&gt;&lt;span&gt; of&lt;/span&gt;&lt;span&gt; Object.&lt;/span&gt;&lt;span&gt;values&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;FORMATS&lt;/span&gt;&lt;span&gt;)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    await&lt;/span&gt;&lt;span&gt; resizeTo&lt;/span&gt;&lt;span&gt;(cesdk, format);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    output[format.id] &lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; exportPng&lt;/span&gt;&lt;span&gt;(cesdk);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  await&lt;/span&gt;&lt;span&gt; resizeTo&lt;/span&gt;&lt;span&gt;(cesdk, before);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; output;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; saveScene&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cesdk&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CreativeEditorSDK&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; cesdk.engine.scene.&lt;/span&gt;&lt;span&gt;saveToString&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; saveArchive&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;cesdk&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; CreativeEditorSDK&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;Blob&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; cesdk.engine.scene.&lt;/span&gt;&lt;span&gt;saveToArchive&lt;/span&gt;&lt;span&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A saved scene points to where its images are instead of holding them itself, and that’s important for generated images. The gateway returns short-lived URLs, and in the test run they were redirected to signed storage links that expired after an hour. &lt;code&gt;persistImage&lt;/code&gt; downloads each generated image before it goes into a block.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// src/generate.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;export&lt;/span&gt;&lt;span&gt; async&lt;/span&gt;&lt;span&gt; function&lt;/span&gt;&lt;span&gt; persistImage&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; string&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; Promise&lt;/span&gt;&lt;span&gt;&amp;#x3C;&lt;/span&gt;&lt;span&gt;string&lt;/span&gt;&lt;span&gt;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  const&lt;/span&gt;&lt;span&gt; response&lt;/span&gt;&lt;span&gt; =&lt;/span&gt;&lt;span&gt; await&lt;/span&gt;&lt;span&gt; fetch&lt;/span&gt;&lt;span&gt;(uri);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span&gt;!&lt;/span&gt;&lt;span&gt;response.ok) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    throw&lt;/span&gt;&lt;span&gt; new&lt;/span&gt;&lt;span&gt; Error&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;`Could not download generated image (${&lt;/span&gt;&lt;span&gt;response&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;status&lt;/span&gt;&lt;span&gt;})`&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  return&lt;/span&gt;&lt;span&gt; URL&lt;/span&gt;&lt;span&gt;.&lt;/span&gt;&lt;span&gt;createObjectURL&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;await&lt;/span&gt;&lt;span&gt; response.&lt;/span&gt;&lt;span&gt;blob&lt;/span&gt;&lt;span&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The demo keeps the download as a local object URL, which lasts only as long as the browser tab stays open. The reason the save button writes an archive is that the pixels are inside the zip, so it still opens after the tab is gone. In your product, upload the file to your own storage at this point and put that URL in the block. A saved scene will be available whenever you need it, on any device, at a much more manageable size.&lt;/p&gt;
&lt;h2 id=&quot;how-to-verify-one-edit-vs-four-regenerations&quot;&gt;How to Verify: One Edit vs Four Regenerations&lt;/h2&gt;
&lt;p&gt;If you would like to test it out for yourself then you can run both paths and count the model calls. The ad regenerations in the test were run directly against IMG.LY’s AI gateway, with the same model that the demo uses for backgrounds. Everything else comes from the demo panel, which logs every step with its time and number of model calls.&lt;/p&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Change&lt;/th&gt;&lt;th&gt;Model calls&lt;/th&gt;&lt;th&gt;Time&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Regenerate the whole ad four times (Ideogram V3)&lt;/td&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;91.5 s in total&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Generate the four parts once&lt;/td&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;52 s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Shorten the headline&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;Typing time only&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Swap the logo&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;10 ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Regenerate the background layer (NanoBanana Pro Edit)&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;38.5 s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Resize to 9:16, 16:9 and 1:1&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;2 ms each&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Export all three formats&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;3.1 s&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;One layer is not faster than generating the whole ad. We find savings in the calls we don’t make, and four of the seven changes above cost nothing. The four regenerations also produced four different ads, and someone still had to pick one. In the layered run, each change touched one layer, and nothing else in the ad moved.&lt;/p&gt;
&lt;p&gt;To check your own build, generate once, then shorten the headline and resize. The log should show no model calls for either of those operations. Then you can regenerate the background and confirm that only that block changed.&lt;/p&gt;
&lt;p&gt;Some real-world examples of this style of image generation come from two IMG.LY customers that work in a space that relies on high-quality, efficient image generation. Omneky, an advertising creative platform, recorded a &lt;a href=&quot;https://img.ly/case-studies/omneky&quot;&gt;10x month-over-month increase&lt;/a&gt; in new sign-ups after embedding CE.SDK. Halio.ai builds content tools for financial advisors, who produce &lt;a href=&quot;https://img.ly/case-studies/halio&quot;&gt;30 days of branded content in 30 minutes&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;when-a-plain-text-to-image-api-is-enough&quot;&gt;When a Plain Text-to-Image API Is Enough&lt;/h2&gt;
&lt;p&gt;Sometimes a simple image will do the job and you don’t need an editor. A text-to-image API on its own is a totally viable option in these three situations.&lt;/p&gt;
&lt;p&gt;In these cases the image will never be edited, think of use cases like a blog header, a mood board or a concept sketch that gets generated once and used as it is. There usually won’t be another version to worry about, so regenerating costs one API call and you get what you need in a few simple steps.&lt;/p&gt;
&lt;p&gt;These kinds of images won’t contain text, logo or product, making the requirements even simpler. Decorative images like a landscape behind a page header don’t usually have anything in it that must stay exact. If the result is off, a quick regeneration will get you over the line.&lt;/p&gt;
&lt;p&gt;Another example is when a designer finishes some work in a design tool. If the output goes to someone else that will rebuild it in layers anyway, generating layers in your initial attempt will duplicate their work. Layers start to pay off when the person making the change is a non-designer working inside your product.&lt;/p&gt;
&lt;h2 id=&quot;faq-editing-ai-generated-images-and-designs&quot;&gt;FAQ: Editing AI-Generated Images and Designs&lt;/h2&gt;
&lt;h3 id=&quot;which-models-support-image-to-image-for-single-layer-regeneration&quot;&gt;Which models support image-to-image for single-layer regeneration?&lt;/h3&gt;
&lt;p&gt;IMG.LY’s &lt;a href=&quot;https://img.ly/docs/cesdk/js/user-interface/ai-integration/image-generation-0540d9/&quot;&gt;image generation docs&lt;/a&gt; list the image-to-image models the AI plugins support, and the list changes often, so be sure to check there for current information. The demo was tested with NanoBanana Pro Edit. Because &lt;code&gt;buildInput&lt;/code&gt; reads each model’s input schema, trying another model means changing one ID. Test it on your own images and see what your results are like.&lt;/p&gt;
&lt;h3 id=&quot;can-i-lock-the-logo&quot;&gt;Can I lock the logo?&lt;/h3&gt;
&lt;p&gt;Yes you can lock the logo. All you need to do is turn off the move, resize, rotate, crop, replace and delete scopes on the logo block, like &lt;code&gt;setEditable&lt;/code&gt; does in step 3. The user can still select the logo but they can’t change it. Your code can still switch it between approved versions if you like.&lt;/p&gt;
&lt;h3 id=&quot;what-happens-to-text-the-image-model-rendered-inside-the-picture&quot;&gt;What happens to text the image model rendered inside the picture?&lt;/h3&gt;
&lt;p&gt;It stays as pixels, and the layered approach can’t turn it back into text. Ask the image model for a background with no text in it, and generate the headline separately as a string.&lt;/p&gt;
&lt;h3 id=&quot;can-users-edit-after-export&quot;&gt;Can users edit after export?&lt;/h3&gt;
&lt;p&gt;No, they can’t edit the exported file because a PNG or PDF is flat (it combines all the layers into a flat image). But, users can edit the saved scene and then export it again if they need to. For that to work later, the images in the scene must come from a storage source that you have access to. Don’t rely on the gateway’s temporary URLs, as is explained in step 6.&lt;/p&gt;
&lt;h3 id=&quot;does-this-work-with-my-own-model-or-api&quot;&gt;Does this work with my own model or API?&lt;/h3&gt;
&lt;p&gt;Yes indeed. The demo calls models through IMG.LY’s AI gateway, but CE.SDK’s AI plugins also accept a &lt;a href=&quot;https://img.ly/docs/cesdk/js/user-interface/ai-integration/custom-provider-16e851/&quot;&gt;custom provider&lt;/a&gt; for your own model or service, and a proxy server that &lt;a href=&quot;https://img.ly/docs/cesdk/js/user-interface/ai-integration/proxy-server-61f901/&quot;&gt;keeps provider keys&lt;/a&gt; on your end. The scene code in steps 2 to 6 doesn’t care where an image came from, it just wants a valid image.&lt;/p&gt;
&lt;h3 id=&quot;how-do-i-keep-generated-backgrounds-on-brand&quot;&gt;How do I keep generated backgrounds on brand?&lt;/h3&gt;
&lt;p&gt;The best way is to use the prompt for what a model does well such as color, light and mood. AI models are non-deterministic, which means that even identical inputs can produce different results. Keep everything that must be exact away from the model. The headline typeface and color come from the brand kit, and the logo is a file. When a background doesn’t quite meet your expectations, regenerate that layer with a more detailed prompt, like in step 4, instead of starting the whole ad again.&lt;/p&gt;
&lt;h2 id=&quot;glossary-ai-image-editing-terms&quot;&gt;Glossary: AI Image Editing Terms&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Background removal.&lt;/strong&gt; This is when a subject is cut out of a photo and makes everything else transparent. The demo does it in the browser with no model call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Brand kit.&lt;/strong&gt; These are the approved logos, typefaces and colors that a customer supplies. In the demo it’s a set of constants that are defined in &lt;code&gt;src/brand.ts&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Image-to-image.&lt;/strong&gt; This is a model call that takes an existing image and a prompt and returns a changed image. The demo uses it to regenerate a single layer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inpainting.&lt;/strong&gt; Inpainting is used for regenerating a masked region of an image while keeping the pixels outside the mask.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Placeholder block.&lt;/strong&gt; A block in a template where the content is meant to be replaced, like an image slot that the user fills.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scene.&lt;/strong&gt; A CE.SDK document contains pages and the blocks in them. It can be saved as a string and loaded again.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scope.&lt;/strong&gt; A CE.SDK permission that allows or denies one kind of change to a block, like moving it or deleting it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Text block.&lt;/strong&gt; A block that holds editable text with a typeface, size and color, as opposed to a picture of text.&lt;/p&gt;
&lt;h2 id=&quot;next-steps&quot;&gt;Next Steps&lt;/h2&gt;
&lt;p&gt;Try the &lt;a href=&quot;https://labs.imgly.dev/p/r24dbfde_editable-ai-ads&quot;&gt;demo&lt;/a&gt; and paste your own IMG.LY keys into its Keys section, or clone the &lt;a href=&quot;https://github.com/imgly/editable-ai-ads&quot;&gt;repository&lt;/a&gt; and run it locally. Check out the &lt;a href=&quot;https://img.ly/docs/cesdk/js/user-interface/ai-integration-5aa356/&quot;&gt;AI integration docs&lt;/a&gt; that go over all the editor’s built-in AI tools, which use the same gateway. For a CE.SDK license, visit the &lt;a href=&quot;https://img.ly/pricing&quot;&gt;pricing page&lt;/a&gt; for a quote.&lt;/p&gt;
</content:encoded><dc:creator>Graeme</dc:creator><media:content url="/_astro/hero.DJbUUkkd.webp" medium="image"/><category>AI</category><category>CE.SDK</category><category>How-To</category><category>Creative Workflows</category></item></channel></rss>