Logo composite
LLMs — including Gemini Pro — degrade complex logos. They smear gradients, drop text, and rearrange elements. The Assets app sidesteps this entirely with a generate-then-composite pipeline that every serious brand-imagery system uses today.
How it works
- The library has a
canonicalLogoUrl (set via set-canonical-logo --libraryId --assetId). The asset's role is logo_reference.
- Logo compositing is an associated-template option. A Template carries
includeLogo (stored in its settings and surfaced as a first-class field). Global templates cannot enable it because a canonical logo belongs to a brand kit. When a generation resolves to an associated template with includeLogo: true, generate-image composites the logo. A generate call's own includeLogo arg, when passed, overrides the template for that run; when omitted, the template's value wins.
- Template skeletons generalize logo compositing. An associated template can carry
settings.skeletonSpec: background first, generated subject second, foreground layers last. If the skeleton already has a foreground layer with source: "canonicalLogo", includeLogo is treated as a no-op for that run so the logo is not stamped twice. If the skeleton does not include the logo and includeLogo is true, the existing canonical-logo layer is appended at the default upper-right position during compositing.
- When logo compositing is on, the prompt envelope adds:
Leave a clean uncluttered area in the upper-right for the real brand logo; do not draw or approximate the logo yourself.
- Gemini returns an image with empty space in that corner.
compositeLogo() from server/lib/image-processing.ts (Sharp) loads the canonical logo PNG / SVG, resizes it to ~16% of the image width with reasonable inset, and composites it onto the generated image. Skeleton runs use applyPresetSkeleton() at the same seam.
- Output: the image with the actual logo, pixel-perfect, vector-quality if the source is SVG.
Set includeLogo via create-template / update-template after associating
the template with a brand kit. The *-generation-preset actions are deprecated
aliases retained for existing threads.
generate-image / generate-image-batch stamp the library's pixel-perfect
canonical logo onto the result when the resolved template has it on, and it is a
no-op if the library has no canonical logo. The model never draws the logo
itself.
Template skeletons
Template skeletons live in settings.skeletonSpec on associated templates and
are edited from /templates/:templateId. They composite a
generated subject onto an uploaded brand background image and optional
foreground layers.
settings.skeletonSpec is the prototype storage shape:
{
background: { type: "asset"; assetId: string };
mask?: { type: "asset"; assetId: string };
contentMode: "fill" | "cutout";
contentRegion?: { x: number; y: number; w: number; h: number };
dropShadow?: boolean;
foreground?: Array<{
source: "canonicalLogo" | { assetId: string };
x: number;
y: number;
w: number;
}>;
}
- The background is an uploaded or selected brand-library image asset. The compositor cover-fits it to the preset canvas and throws if the asset pixels are unavailable.
fill drops an opaque generation into the content region, so it works through the normal managed provider path.
- When the resolved skeleton model is
gpt-image-2, the action sends a managed edit/inpaint request instead of generate-then-composite: the uploaded plate is the edit_target, an optional same-size mask asset becomes the editable-area mask, and maskFromPlateAlpha() is only the fallback when no manual mask is set. Transparent mask pixels are editable, opaque pixels are sent as preserved regions, and the returned image is final. The manual mask or plate fallback must have transparent pixels.
- Other
cutout skeletons ask for an isolated transparent subject, force gpt-image-1, attach the background plate as a background_reference/composition reference, request background: "transparent" through the managed Builder image provider, and only fall back to OpenAI BYOK when the managed provider fails. They clamp only the provider subject ratio to 1:1, 2:3, or 3:2. The final skeleton canvas still uses the preset's requested aspect ratio.
- The prompt envelope for cutout mode asks for an isolated subject on an empty transparent background. The gpt-image-2 inpaint branch instead asks the model to render the requested foreground content inside the mask's transparent/open region while preserving logos, text, framing, and other opaque plate content.
- Background and foreground
assetId values must belong to the selected brand kit and must be images.
When to use it
- The user turned on "Composite canonical logo" on an associated template (the template then stamps the logo on every image made with it).
- The agent infers the user wants the logo for a one-off (e.g. "make a hero with our brand logo") — pass
includeLogo: true on that single generate call to override the template.
- The image will appear in a customer-facing context where logo accuracy matters.
When NOT to use it
- Logo on a product (a t-shirt mockup, a billboard scene, a coffee cup).
Compositing onto a flat corner is fine; compositing onto a curved or perspective surface needs mask-based inpainting. Use a gpt-image-2 skeleton plate with transparent editable regions when that exact layout is required.
- Multi-logo scenes (a partner-logo wall, a footer sponsor row). Same reason: use a prepared plate plus mask-inpaint skeleton, or ask the user to mock it up in design.
Setting a canonical logo
upload reference image (role: logo_reference, category: logo) →
set-canonical-logo --libraryId=<id> --assetId=<asset-id>
set-canonical-logo flips the asset's role to logo_reference AND its status to reference. This means the reference selector won't pick up generated logo candidates as canonical — only intentionally pinned uploads.
Sharp composite parameters (current defaults)
In image-processing.ts:compositeLogo():
- Logo width:
max(120, round(imageWidth * 0.16)) — ~16% of the image, but never smaller than 120 px.
- Inset:
max(24, round(min(width, height) * 0.035)) — ~3.5% of the smaller dimension, but never less than 24 px.
- Position: upper-right (
top: inset, left: width - logoWidth - inset).
- Output format: PNG (preserves transparency).
If you change these, also update the corresponding language in the prompt envelope ("upper-right") so the LLM's clean area aligns with where Sharp will composite.
Why not in-image text?
The same logic applies to body and headline text. Image models still smear small letters and rearrange long strings. The Assets app's prompt envelope explicitly says:
Do not render headlines, body text, UI labels, or prompt wording inside the image unless the user explicitly asks for exact visible text.
Overlay text in HTML/CSS in the calling app (slides, design, mail) — it's more reliable, more accessible, and the user can edit it without re-running the generation.
Failure modes & detection
- Gemini ignores the placeholder ask and renders something in the corner. The composite still works, but the hand-drawn-looking element underneath will peek out behind a transparent logo. Fix: re-roll, or ask the user to crop.
- The canonical logo's transparency is lost on a non-PNG source. Fix: re-upload as PNG; SVG works too via Sharp's rasterization.
- The user swaps a logo mid-generation. The action reads
canonicalLogoAssetId at generate time, so racing here is rare; but the variant slot will reflect whichever logo was current when the call landed.
1---2name: logo-composite3description: How the generate-then-composite pipeline puts a pixel-perfect canonical logo onto a generated image without letting the LLM regenerate the logo.4---56# Logo composite78LLMs — including Gemini Pro — degrade complex logos. They smear gradients, drop text, and rearrange elements. The Assets app sidesteps this entirely with a **generate-then-composite** pipeline that every serious brand-imagery system uses today.910## How it works11121. The library has a `canonicalLogoUrl` (set via `set-canonical-logo --libraryId --assetId`). The asset's role is `logo_reference`.132. **Logo compositing is an associated-template option.** A Template carries `includeLogo` (stored in its `settings` and surfaced as a first-class field). Global templates cannot enable it because a canonical logo belongs to a brand kit. When a generation resolves to an associated template with `includeLogo: true`, `generate-image` composites the logo. A generate call's own `includeLogo` arg, when passed, overrides the template for that run; when omitted, the template's value wins.143. **Template skeletons generalize logo compositing.** An associated template can carry `settings.skeletonSpec`: background first, generated subject second, foreground layers last. If the skeleton already has a `foreground` layer with `source: "canonicalLogo"`, `includeLogo` is treated as a no-op for that run so the logo is not stamped twice. If the skeleton does not include the logo and `includeLogo` is true, the existing canonical-logo layer is appended at the default upper-right position during compositing.154. When logo compositing is on, the prompt envelope adds:16 > Leave a clean uncluttered area in the upper-right for the real brand logo; do not draw or approximate the logo yourself.175. Gemini returns an image with empty space in that corner.186. `compositeLogo()` from `server/lib/image-processing.ts` (Sharp) loads the canonical logo PNG / SVG, resizes it to ~16% of the image width with reasonable inset, and composites it onto the generated image. Skeleton runs use `applyPresetSkeleton()` at the same seam.197. Output: the image with the actual logo, pixel-perfect, vector-quality if the source is SVG.2021Set `includeLogo` via `create-template` / `update-template` after associating22the template with a brand kit. The `*-generation-preset` actions are deprecated23aliases retained for existing threads.24`generate-image` / `generate-image-batch` stamp the library's pixel-perfect25canonical logo onto the result when the resolved template has it on, and it is a26no-op if the library has no canonical logo. The model never draws the logo27itself.2829## Template skeletons3031Template skeletons live in `settings.skeletonSpec` on associated templates and32are edited from `/templates/:templateId`. They composite a33generated subject onto an uploaded brand background image and optional34foreground layers.3536`settings.skeletonSpec` is the prototype storage shape:3738```ts39{40 background: { type: "asset"; assetId: string };41 mask?: { type: "asset"; assetId: string };42 contentMode: "fill" | "cutout";43 contentRegion?: { x: number; y: number; w: number; h: number };44 dropShadow?: boolean;45 foreground?: Array<{46 source: "canonicalLogo" | { assetId: string };47 x: number;48 y: number;49 w: number;50 }>;51}52```5354- The background is an uploaded or selected brand-library image asset. The compositor cover-fits it to the preset canvas and throws if the asset pixels are unavailable.55- `fill` drops an opaque generation into the content region, so it works through the normal managed provider path.56- When the resolved skeleton model is `gpt-image-2`, the action sends a managed edit/inpaint request instead of generate-then-composite: the uploaded plate is the `edit_target`, an optional same-size `mask` asset becomes the editable-area mask, and `maskFromPlateAlpha()` is only the fallback when no manual mask is set. Transparent mask pixels are editable, opaque pixels are sent as preserved regions, and the returned image is final. The manual mask or plate fallback must have transparent pixels.57- Other `cutout` skeletons ask for an isolated transparent subject, force `gpt-image-1`, attach the background plate as a `background_reference`/composition reference, request `background: "transparent"` through the managed Builder image provider, and only fall back to OpenAI BYOK when the managed provider fails. They clamp only the provider subject ratio to `1:1`, `2:3`, or `3:2`. The final skeleton canvas still uses the preset's requested aspect ratio.58- The prompt envelope for cutout mode asks for an isolated subject on an empty transparent background. The gpt-image-2 inpaint branch instead asks the model to render the requested foreground content inside the mask's transparent/open region while preserving logos, text, framing, and other opaque plate content.59- Background and foreground `assetId` values must belong to the selected brand kit and must be images.6061## When to use it6263- The user turned on "Composite canonical logo" on an associated template (the template then stamps the logo on every image made with it).64- The agent infers the user wants the logo for a one-off (e.g. "make a hero with our brand logo") — pass `includeLogo: true` on that single generate call to override the template.65- The image will appear in a customer-facing context where logo accuracy matters.6667## When NOT to use it6869- **Logo on a product** (a t-shirt mockup, a billboard scene, a coffee cup).70 Compositing onto a flat corner is fine; compositing onto a curved or perspective surface needs mask-based inpainting. Use a gpt-image-2 skeleton plate with transparent editable regions when that exact layout is required.71- **Multi-logo scenes** (a partner-logo wall, a footer sponsor row). Same reason: use a prepared plate plus mask-inpaint skeleton, or ask the user to mock it up in design.7273## Setting a canonical logo7475```76upload reference image (role: logo_reference, category: logo) →77set-canonical-logo --libraryId=<id> --assetId=<asset-id>78```7980`set-canonical-logo` flips the asset's role to `logo_reference` AND its status to `reference`. This means the reference selector won't pick up generated logo candidates as canonical — only intentionally pinned uploads.8182## Sharp composite parameters (current defaults)8384In `image-processing.ts:compositeLogo()`:8586- Logo width: `max(120, round(imageWidth * 0.16))` — ~16% of the image, but never smaller than 120 px.87- Inset: `max(24, round(min(width, height) * 0.035))` — ~3.5% of the smaller dimension, but never less than 24 px.88- Position: upper-right (`top: inset`, `left: width - logoWidth - inset`).89- Output format: PNG (preserves transparency).9091If you change these, also update the corresponding language in the prompt envelope ("upper-right") so the LLM's clean area aligns with where Sharp will composite.9293## Why not in-image text?9495The same logic applies to body and headline text. Image models still smear small letters and rearrange long strings. The Assets app's prompt envelope explicitly says:9697> Do not render headlines, body text, UI labels, or prompt wording inside the image unless the user explicitly asks for exact visible text.9899Overlay text in HTML/CSS in the calling app (slides, design, mail) — it's more reliable, more accessible, and the user can edit it without re-running the generation.100101## Failure modes & detection102103- Gemini ignores the placeholder ask and renders something in the corner. The composite still works, but the hand-drawn-looking element underneath will peek out behind a transparent logo. Fix: re-roll, or ask the user to crop.104- The canonical logo's transparency is lost on a non-PNG source. Fix: re-upload as PNG; SVG works too via Sharp's rasterization.105- The user swaps a logo mid-generation. The action reads `canonicalLogoAssetId` at generate time, so racing here is rare; but the variant slot will reflect whichever logo was current when the call landed.