@orangecatai/adgen-canvas 0.0.32 → 0.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/dev/{chunk-VL47Q2RH.js → chunk-DB6EMYXB.js} +2 -2
  2. package/dist/dev/{chunk-OWDDSUUQ.js → chunk-JBHZ6CNN.js} +3 -3
  3. package/dist/dev/data/{image-5Y3GORNT.js → image-IIQSXNFT.js} +3 -3
  4. package/dist/dev/index.css +716 -703
  5. package/dist/dev/index.css.map +3 -3
  6. package/dist/dev/index.js +9700 -8140
  7. package/dist/dev/index.js.map +4 -4
  8. package/dist/dev/subset-shared.chunk.js +1 -1
  9. package/dist/dev/subset-worker.chunk.js +1 -1
  10. package/dist/prod/{chunk-452N23ZE.js → chunk-KFCCNTO2.js} +1 -1
  11. package/dist/prod/{chunk-AQSA3HNR.js → chunk-T2MY3VHD.js} +2 -2
  12. package/dist/prod/data/image-3RDLE3VN.js +1 -0
  13. package/dist/prod/index.css +1 -1
  14. package/dist/prod/index.js +167 -105
  15. package/dist/prod/subset-shared.chunk.js +1 -1
  16. package/dist/prod/subset-worker.chunk.js +1 -1
  17. package/dist/types/excalidraw/components/ImageGeneratorPanel.d.ts +15 -0
  18. package/dist/types/excalidraw/components/ai-chat/imagePayload.d.ts +53 -0
  19. package/dist/types/excalidraw/components/auto-resize/AutoResizePanel.d.ts +3 -4
  20. package/dist/types/excalidraw/components/auto-resize/autoResizeEngine.d.ts +17 -14
  21. package/dist/types/excalidraw/components/auto-resize/backgroundExtend.d.ts +105 -0
  22. package/dist/types/excalidraw/components/auto-resize/frameSource.d.ts +16 -0
  23. package/dist/types/excalidraw/components/frameToolbarVariant.d.ts +34 -1
  24. package/dist/types/excalidraw/types.d.ts +23 -4
  25. package/dist/types/excalidraw/utils/brandContextUtils.d.ts +11 -6
  26. package/dist/types/excalidraw/utils/imageApi.d.ts +52 -6
  27. package/dist/types/excalidraw/utils/imageCompression.d.ts +46 -0
  28. package/dist/types/excalidraw/utils/referenceBudget.d.ts +138 -0
  29. package/package.json +1 -1
  30. package/dist/prod/data/image-5D6ZUEOG.js +0 -1
  31. /package/dist/dev/{chunk-VL47Q2RH.js.map → chunk-DB6EMYXB.js.map} +0 -0
  32. /package/dist/dev/{chunk-OWDDSUUQ.js.map → chunk-JBHZ6CNN.js.map} +0 -0
  33. /package/dist/dev/data/{image-5Y3GORNT.js.map → image-IIQSXNFT.js.map} +0 -0
@@ -1 +1 @@
1
- import{a,b,c,d}from"./chunk-Z5NKEFVG.js";import"./chunk-452N23ZE.js";import"./chunk-SRAX5OIU.js";export{a as Commands,b as subsetToBase64,c as subsetToBinary,d as toBase64};
1
+ import{a,b,c,d}from"./chunk-Z5NKEFVG.js";import"./chunk-KFCCNTO2.js";import"./chunk-SRAX5OIU.js";export{a as Commands,b as subsetToBase64,c as subsetToBinary,d as toBase64};
@@ -1 +1 @@
1
- import{a as r,c as t}from"./chunk-Z5NKEFVG.js";import"./chunk-452N23ZE.js";import"./chunk-SRAX5OIU.js";var s=import.meta.url?new URL(import.meta.url):void 0;typeof window>"u"&&typeof self<"u"&&(self.onmessage=async e=>{switch(e.data.command){case r.Subset:let a=await t(e.data.arrayBuffer,e.data.codePoints);self.postMessage(a,{transfer:[a]});break}});export{s as WorkerUrl};
1
+ import{a as r,c as t}from"./chunk-Z5NKEFVG.js";import"./chunk-KFCCNTO2.js";import"./chunk-SRAX5OIU.js";var s=import.meta.url?new URL(import.meta.url):void 0;typeof window>"u"&&typeof self<"u"&&(self.onmessage=async e=>{switch(e.data.command){case r.Subset:let a=await t(e.data.arrayBuffer,e.data.codePoints);self.postMessage(a,{transfer:[a]});break}});export{s as WorkerUrl};
@@ -87,6 +87,21 @@ export declare function getDimensions(model: ModelName, ratio: string, resolutio
87
87
  width: number;
88
88
  height: number;
89
89
  };
90
+ /**
91
+ * Turn the panel's picked ratio + resolution into the request fields the model
92
+ * actually accepts.
93
+ *
94
+ * gpt-image-* is the awkward one: its `aspect_ratio` enum does NOT accept 4:5
95
+ * or 5:4 at the 2K/4K tiers, and `supportsImageSize: false` meant the picked
96
+ * resolution was dropped on the floor — so "4:5 at 2K" silently asked for a
97
+ * ratio the API rejects and gave no size at all. The panel already knows the
98
+ * exact pixel dimensions for every ratio/resolution pair, so hand those over
99
+ * as an authoritative `size` and skip the enum entirely.
100
+ */
101
+ export declare function resolveRequestSize(model: ModelName, ratio: string, resolution: Resolution): {
102
+ aspectRatio: string | null;
103
+ imageSize: string | null;
104
+ };
90
105
  export declare const ImageGeneratorPanel: ({ element, app, onBeforeImageGen, onAfterImageGen, }: {
91
106
  element: NonDeletedExcalidrawElement & ExcalidrawFrameLikeElement;
92
107
  app: AppClassProperties;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Payload-size normalisation for images sent to the chat API.
3
+ *
4
+ * Every `image_url` content block in a chat request travels as a base64 data
5
+ * URL embedded in JSON. Base64 inflates bytes by 4/3, and the host proxy runs
6
+ * on Vercel, whose platform-level request cap is 4.5 MB — enforced at the edge
7
+ * *before* the route handler runs, so it surfaces as an opaque 413 that never
8
+ * reaches any of our error handling.
9
+ *
10
+ * Rather than budget-and-drop (which loses the user's images, the one thing
11
+ * they actually asked the model to look at), every image is downscaled to a
12
+ * size where the whole request comfortably fits. A 1280px-long-edge JPEG at
13
+ * q0.78 lands around 100-150KB, so a request carrying the full complement of
14
+ * user uploads + brand references + frame screenshot sits near 1MB — roughly
15
+ * a quarter of the cap, with room for prompt text and tool schemas.
16
+ *
17
+ * This runs at payload-assembly time, not upload time, which is deliberate:
18
+ * conversation history persisted before this existed still holds raw
19
+ * multi-megabyte data URLs, and those are re-sent on every subsequent turn.
20
+ * Normalising here fixes existing conversations, not just new uploads.
21
+ */
22
+ import { dataUrlWireBytes, isRasterImageDataUrl, shrinkImageDataUrl, shrinkImageDataUrls } from "../../utils/imageCompression";
23
+ export { dataUrlWireBytes, isRasterImageDataUrl, shrinkImageDataUrl, shrinkImageDataUrls, };
24
+ /**
25
+ * Normalise every image in an outgoing request so the body cannot exceed the
26
+ * platform request cap.
27
+ *
28
+ * Three escalating passes, so the common case costs nothing and the worst case
29
+ * still succeeds rather than 413ing:
30
+ *
31
+ * 1. Downscale every image to {@link MAX_EDGE_PX}. Almost always sufficient —
32
+ * a full request of 9 images lands near 1MB.
33
+ * 2. If still over (many images, or images that resisted re-encoding),
34
+ * re-encode at successively lower quality.
35
+ * 3. Only if *still* over, drop the oldest images — newest are the ones the
36
+ * user is actually asking about. Images on the final message are never
37
+ * dropped.
38
+ *
39
+ * Mutates the parts in place and returns the message array for convenience.
40
+ */
41
+ export declare function normalizeRequestImages<T extends {
42
+ role: string;
43
+ }>(messages: T[]): Promise<T[]>;
44
+ /**
45
+ * Build a user-facing message from a failed chat-proxy response.
46
+ *
47
+ * The previous handling assumed every non-OK status came from OpenRouter and
48
+ * carried an OpenRouter-shaped JSON error. Platform-level rejections (notably
49
+ * Vercel's 413 at the edge) return plain text, so `response.json()` threw and
50
+ * the raw fallback `OpenRouter error 413` reached the user — naming the wrong
51
+ * system and offering no action.
52
+ */
53
+ export declare function describeChatError(response: Response): Promise<string>;
@@ -1,7 +1,7 @@
1
1
  import "./AutoResizePanel.scss";
2
2
  import type { ExcalidrawFrameLikeElement, NonDeletedExcalidrawElement } from "@orangecatai/element/types";
3
3
  import type { AppClassProperties } from "../../types";
4
- export declare const AutoResizePanel: ({ element, app, onClose, onBeforeAutoResize, onAfterAutoResize, onRunningChange, autoResizeUrl, customResizeUrl, tier1CropUrl, chatModel, agentImageModel, }: {
4
+ export declare const AutoResizePanel: ({ element, app, onClose, onBeforeAutoResize, onAfterAutoResize, onRunningChange, autoResizeUrl, chatModel, reviewerModel, outpaintModel, }: {
5
5
  element: NonDeletedExcalidrawElement & ExcalidrawFrameLikeElement;
6
6
  app: AppClassProperties;
7
7
  onClose: () => void;
@@ -12,8 +12,7 @@ export declare const AutoResizePanel: ({ element, app, onClose, onBeforeAutoResi
12
12
  onAfterAutoResize?: () => void;
13
13
  onRunningChange?: (running: boolean) => void;
14
14
  autoResizeUrl: string;
15
- customResizeUrl?: string;
16
- tier1CropUrl?: string;
17
15
  chatModel: string;
18
- agentImageModel?: string;
16
+ reviewerModel?: string;
17
+ outpaintModel?: string;
19
18
  }) => import("react/jsx-runtime").JSX.Element;
@@ -152,6 +152,19 @@ export declare function enforceMinProductBox({ x, y, width, height, targetW, tar
152
152
  width: number;
153
153
  height: number;
154
154
  };
155
+ /**
156
+ * Checks generated HTML against the strict contract in `buildAutoResizePrompt`
157
+ * BEFORE handing it to the parser.
158
+ *
159
+ * The parser is deliberately narrow — absolutely-positioned divs, no flex, no
160
+ * grid, no transforms — because that is what maps cleanly onto Excalidraw
161
+ * elements. Models drift off strict DOM contracts, and a violation shows up as
162
+ * silently mangled geometry rather than an error. Catching it here buys one
163
+ * targeted retry instead of a wrong layout.
164
+ *
165
+ * Returns the list of violations; empty means the HTML conforms.
166
+ */
167
+ export declare function validateHtmlContract(html: string, expectedChildCount: number): string[];
155
168
  export declare function buildAutoResizeImagePrompt(srcW: number, srcH: number, tgtW: number, tgtH: number, aspectRatio: string, brandImages?: {
156
169
  logo?: string;
157
170
  fontPreview?: string;
@@ -171,10 +184,11 @@ export declare function runAutoResize(opts: {
171
184
  targetDimensions: TargetDimension[];
172
185
  app: AppClassProperties;
173
186
  autoResizeUrl: string;
174
- customResizeUrl?: string;
175
- tier1CropUrl?: string;
176
187
  chatModel?: string;
177
- agentImageModel?: string;
188
+ /** Vision model for the layout critique loop. Falls back to `chatModel`. */
189
+ reviewerModel?: string;
190
+ /** Image model used ONLY to outpaint photographic backgrounds. */
191
+ outpaintModel?: string;
178
192
  customFontMap?: Record<string, number>;
179
193
  onBeforeAutoResize?: () => Promise<{
180
194
  allowed: boolean;
@@ -185,16 +199,5 @@ export declare function runAutoResize(opts: {
185
199
  signal?: AbortSignal;
186
200
  /** Pass the result of preCreateAllFrames() to skip redundant frame creation */
187
201
  preCreatedFrameInfos?: PreCreatedFrameInfo[];
188
- /**
189
- * Brand assets handed to the image model so the recomposed ad keeps the brand
190
- * logo, typeface, and (for font-aware models) the real font file. Sourced from
191
- * `app.state.brandContext` by the panel.
192
- */
193
- brandImages?: {
194
- logo?: string;
195
- fontPreview?: string;
196
- fontUrl?: string;
197
- name?: string;
198
- };
199
202
  }): Promise<DimensionResult[]>;
200
203
  export {};
@@ -0,0 +1,105 @@
1
+ export type BgKind = "solid" | "texture" | "panels" | "gradient" | "photo";
2
+ export type PixelBuffer = {
3
+ data: Uint8ClampedArray;
4
+ width: number;
5
+ height: number;
6
+ };
7
+ export type Rect = {
8
+ x: number;
9
+ y: number;
10
+ w: number;
11
+ h: number;
12
+ };
13
+ /** Where a panel boundary sits, as a fraction (0-1) along `axis`. */
14
+ export type BgSplit = {
15
+ axis: "x" | "y";
16
+ at: number;
17
+ };
18
+ export type BgClassification = {
19
+ kind: BgKind;
20
+ /** Present only for `kind: "panels"`. */
21
+ split?: BgSplit;
22
+ /** Present for `kind: "solid"` — the plate's average colour as hex. */
23
+ solidColor?: string;
24
+ /** Per-region verdicts; length 2 when a split was found, else 1. */
25
+ regions: RegionStats[];
26
+ };
27
+ export type RegionStats = {
28
+ rect: Rect;
29
+ /** Mean luminance stdev within patches — how "busy" the region is. */
30
+ textureEnergy: number;
31
+ /** Spread of patch MEANS — how much the region changes across itself. */
32
+ meanSpread: number;
33
+ /** True when patch means march in one direction along x or y. */
34
+ monotonic: boolean;
35
+ meanColor: {
36
+ r: number;
37
+ g: number;
38
+ b: number;
39
+ };
40
+ verdict: "solid" | "texture" | "gradient" | "photo";
41
+ };
42
+ export declare function toHex({ r, g, b, }: {
43
+ r: number;
44
+ g: number;
45
+ b: number;
46
+ }): string;
47
+ /**
48
+ * Finds the single strongest panel boundary, if any. Deliberately detects at
49
+ * most ONE split — that covers the dominant real-world ad layout (a two-panel
50
+ * composition) without overfitting to noise in busier plates.
51
+ */
52
+ export declare function detectSplit(buf: PixelBuffer): BgSplit | null;
53
+ /** Patch-grid statistics for one region, and the verdict they imply. */
54
+ export declare function analyzeRegion(buf: PixelBuffer, rect: Rect): RegionStats;
55
+ /** Full classification: split detection, then per-region analysis. */
56
+ export declare function classifyBackground(buf: PixelBuffer): BgClassification;
57
+ export declare function loadImageElement(dataUrl: string): Promise<HTMLImageElement>;
58
+ /**
59
+ * Reads an image into a pixel buffer, downscaled to at most `maxSide` on its
60
+ * long edge. Classification only needs coarse statistics, and a 4K plate would
61
+ * otherwise cost tens of megabytes of ImageData for no extra accuracy.
62
+ */
63
+ export declare function imageToPixelBuffer(dataUrl: string, maxSide?: number): Promise<PixelBuffer>;
64
+ export type BackgroundSource = {
65
+ type: "solid";
66
+ color: string;
67
+ elementId?: string;
68
+ } | {
69
+ type: "image";
70
+ dataUrl: string;
71
+ elementId?: string;
72
+ } | {
73
+ type: "none";
74
+ };
75
+ export type BackgroundResult = {
76
+ kind: "none";
77
+ } | {
78
+ kind: "solid";
79
+ color: string;
80
+ } | {
81
+ kind: "image";
82
+ dataUrl: string;
83
+ strategy: BgKind | "scaled";
84
+ };
85
+ export type BuildBackgroundOpts = {
86
+ /**
87
+ * Called ONLY for photographic plates that cannot be extended locally.
88
+ * Supplied by the caller so this module stays free of network concerns and
89
+ * fully unit-testable. Returning null falls back to edge-extension.
90
+ */
91
+ outpaint?: (args: {
92
+ dataUrl: string;
93
+ targetWidth: number;
94
+ targetHeight: number;
95
+ }) => Promise<string | null>;
96
+ /** When false, skip analysis and just cover-scale (small AR delta). */
97
+ needsExtension: boolean;
98
+ onProgress?: (phase: string) => void;
99
+ };
100
+ /**
101
+ * Produces a background sized EXACTLY to the target frame. The caller inserts
102
+ * the result 1:1 — no cover-crop afterwards, unlike the old image-gen path
103
+ * where the model returned approximate dimensions.
104
+ */
105
+ export declare function buildTargetBackground(bg: BackgroundSource, tgtW: number, tgtH: number, opts: BuildBackgroundOpts): Promise<BackgroundResult>;
@@ -0,0 +1,16 @@
1
+ import type { ExcalidrawElement, ExcalidrawFrameLikeElement } from "@orangecatai/element/types";
2
+ /** Live (non-deleted) children of a frame. */
3
+ export declare function getLiveFrameChildren(elements: readonly ExcalidrawElement[], frame: ExcalidrawFrameLikeElement): readonly ExcalidrawElement[];
4
+ /**
5
+ * True when this frame holds separable layers Auto Resize can re-arrange.
6
+ *
7
+ * Any ONE of these is sufficient:
8
+ * - more than one live child → there is something to arrange
9
+ * - a child tagged `decomposeType` → came from "Make editable"
10
+ * - a text child → real editable copy, so the frame is not a flat render
11
+ *
12
+ * False for the flat case: zero or one child (typically one full-bleed image).
13
+ */
14
+ export declare function isStructuredFrame(elements: readonly ExcalidrawElement[], frame: ExcalidrawFrameLikeElement): boolean;
15
+ /** Human-readable reason shown when Auto Resize is unavailable. */
16
+ export declare const AUTO_RESIZE_FLAT_FRAME_REASON = "Make this ad editable first \u2014 Auto Resize needs separate layers to re-lay-out.";
@@ -1,4 +1,4 @@
1
- import type { ElementsMapOrArray, ExcalidrawElement, ExcalidrawFrameLikeElement, ExcalidrawVideoElement } from "@orangecatai/element/types";
1
+ import type { ElementsMapOrArray, ExcalidrawElement, ExcalidrawFrameLikeElement, ExcalidrawImageElement, ExcalidrawVideoElement } from "@orangecatai/element/types";
2
2
  export type FrameToolbarVariant = "frame" | "video";
3
3
  export type FrameToolbarVariantResult = {
4
4
  variant: FrameToolbarVariant;
@@ -34,3 +34,36 @@ export declare const getFrameToolbarVariant: (frame: ExcalidrawFrameLikeElement,
34
34
  * toolbar, so the child never surfaces an action the frame does not have.
35
35
  */
36
36
  export declare const getResizeTargetFrame: (element: ExcalidrawElement, elements: readonly ExcalidrawElement[]) => ExcalidrawFrameLikeElement | null;
37
+ /**
38
+ * Resolves the image a *frame's* toolbar should act on.
39
+ *
40
+ * A generated ad covers its frame edge to edge, so whether a click lands on the
41
+ * frame or on the artwork is arbitrary from the user's side — but only the
42
+ * artwork carries the image actions (Quick Edit, and the host's Make editable).
43
+ * This lets a selected frame offer those same actions, targeting its artwork,
44
+ * so they no longer come and go with where the user happened to click. It is
45
+ * the mirror of `getResizeTargetFrame`, which lets a selected image reach the
46
+ * frame's Resize.
47
+ *
48
+ * When a frame holds several images (artwork plus a logo overlay, say), the
49
+ * largest one is the artwork.
50
+ */
51
+ export declare const getFrameChildImage: (frame: ExcalidrawFrameLikeElement, elements: ElementsMapOrArray) => ExcalidrawImageElement | null;
52
+ export type AutoResizeAvailability = {
53
+ /** False when the frame is a flat render with nothing to re-arrange. */
54
+ enabled: boolean;
55
+ /** Tooltip text — the reason when disabled, the action when enabled. */
56
+ title: string;
57
+ };
58
+ /**
59
+ * Whether Auto Resize can act on this frame, and what to say if it cannot.
60
+ *
61
+ * Auto Resize re-lays-out an ad by repositioning its individual layers, so it
62
+ * needs a frame that HAS layers. A freshly generated ad is one flat image; the
63
+ * button stays visible but disabled, pointing at "Make editable", rather than
64
+ * silently vanishing.
65
+ *
66
+ * Shared by both entry points (a selected frame, and a selected image resolved
67
+ * to its parent frame) so the rule cannot drift between them.
68
+ */
69
+ export declare const getAutoResizeAvailability: (frame: ExcalidrawFrameLikeElement | null, elements: readonly ExcalidrawElement[]) => AutoResizeAvailability;
@@ -767,12 +767,31 @@ export interface ExcalidrawProps {
767
767
  agentImageModel?: string;
768
768
  /**
769
769
  * OpenRouter model tag used by the auto-resize engine for HTML layout
770
- * generation. This is the LLM call that figures out how to reflow the ad
771
- * content into the target dimensions before Gemini regenerates the background.
772
- * @default "google/gemini-3.1-flash-lite-preview"
773
- * @example "openai/gpt-4o"
770
+ * generation — the LLM call that decides how to reflow the ad's layers into
771
+ * the target dimensions. Must be a chat model.
772
+ * @default "x-ai/grok-4.6"
774
773
  */
775
774
  chatModel?: string;
775
+ /**
776
+ * Vision model for the auto-resize layout critique loop. Falls back to
777
+ * `chatModel` when omitted, so pass a cheap fast vision model here — the
778
+ * reviewer only compares a screenshot against the brief, it does not carry
779
+ * an agentic conversation.
780
+ * @example "openai/gpt-5.6-luna"
781
+ */
782
+ reviewerModel?: string;
783
+ /**
784
+ * Image model used ONLY to outpaint a photographic background when the
785
+ * aspect-ratio change is too large to cover-crop. Solid, textured and
786
+ * two-panel backgrounds are extended locally on a canvas and never reach a
787
+ * model at all.
788
+ *
789
+ * Must support extreme aspect ratios (4:1, 1:4, 8:1, 1:8) and be callable
790
+ * from `chat/completions`. `openai/gpt-image-2` satisfies NEITHER — it is
791
+ * generation-only on OpenRouter and its ratio enum stops at 3:1 / 1:3.
792
+ * @default "google/gemini-3.1-flash-image-preview"
793
+ */
794
+ outpaintModel?: string;
776
795
  /**
777
796
  * Show the Library sidebar trigger button. Defaults to `false`.
778
797
  * Set to `true` to show the library icon in the top-right area.
@@ -19,12 +19,17 @@ export declare function buildBrandContextMessage(ctx: BrandContext): string;
19
19
  */
20
20
  export declare function buildImageGenSafePrompt(prompt: string, brandContext: BrandContext, opts?: {
21
21
  /**
22
- * True when the user attached their own reference images. The brand logo
23
- * and font specimens are then not attached at all (see imageApi), so the
24
- * mandates that point at them — "embed the logo VERBATIM", "render ALL text
25
- * in this exact typeface" — must go too: they would order the model to
26
- * reproduce images it can no longer see, and they compete with the
27
- * reference the user actually chose.
22
+ * True when the user attached their own reference images (or, on quick
23
+ * edit, when there is a source image).
24
+ *
25
+ * This no longer suppresses the logo and font mandates. Brand assets are
26
+ * always attached now — the budget in utils/referenceBudget sheds them only
27
+ * under real payload pressure, not because the user supplied something —
28
+ * so the mandates still point at images the model can see.
29
+ *
30
+ * What it still does is demote brand *direction*: the "this ad is for brand
31
+ * X, everything must reflect it" line, which genuinely does fight a
32
+ * user-chosen reference for subject and style.
28
33
  */
29
34
  hasUserReferences?: boolean;
30
35
  }): string;
@@ -1,9 +1,25 @@
1
+ import { MAX_USER_REFERENCE_IMAGES } from "./referenceBudget";
2
+ export { MAX_USER_REFERENCE_IMAGES };
3
+ /** True for OpenAI's Images-API models, which are routed and shaped differently. */
4
+ export declare const isOpenAIImageModel: (modelId: string) => boolean;
5
+ /** Nearest preset ratio to `frameW / frameH`. */
6
+ export declare function nearestPresetAspectRatio(frameW: number, frameH: number): string;
1
7
  /**
2
- * Hard cap on user-supplied reference images per generation. Providers differ on
3
- * how many references they accept; six covers every realistic ad brief and keeps
4
- * the request well inside the limits of the models we route to.
8
+ * Picks the output size for a frame.
9
+ *
10
+ * For gpt-image-* this returns an exact `"1088x1360"` size matching the frame's
11
+ * real aspect ratio, so a 1080×1350 (4:5) frame no longer comes back as a 3:4
12
+ * image that has to be letterboxed to 1013×1350. It travels in the request's
13
+ * `size` field, NOT `resolution` — see the Images API body below. `aspectRatio`
14
+ * is null in that case: an exact size is authoritative and OpenRouter 400s if a
15
+ * ratio is sent alongside it (4:5 is not a valid preset anyway).
16
+ *
17
+ * Every other model keeps the preset-snapping behaviour it has always had.
5
18
  */
6
- export declare const MAX_USER_REFERENCE_IMAGES = 6;
19
+ export declare function resolveImageOutputSize(modelId: string, frameW: number, frameH: number): {
20
+ aspectRatio: string | null;
21
+ imageSize: string | null;
22
+ };
7
23
  /**
8
24
  * Describes each attached reference image by its real 1-based position, and
9
25
  * states the precedence rule between them.
@@ -20,8 +36,18 @@ export declare const MAX_USER_REFERENCE_IMAGES = 6;
20
36
  * Returns "" when there is nothing worth numbering, so prompts that carry no
21
37
  * attachments are left byte-identical to before.
22
38
  */
23
- export declare function buildReferenceManifest(userReferenceCount: number, brandLabels: string[]): string;
24
- export declare function callOpenRouterImageAPI(prompt: string, modelId: string, aspectRatio: string, imageSize: string | null, textOutput: boolean, imageGenUrl: string, referenceImageDataUrl?: string | string[], signal?: AbortSignal, brandImages?: {
39
+ export declare function buildReferenceManifest(userReferenceCount: number, brandLabels: string[],
40
+ /** The brand logo survived the budget and is attached. */
41
+ logoIsAttached?: boolean,
42
+ /** A source image (quick edit / resize) is attached below the user's refs. */
43
+ hasSourceImage?: boolean): string;
44
+ export declare function callOpenRouterImageAPI(prompt: string, modelId: string,
45
+ /**
46
+ * Preset ratio label ("4:5" is NOT one). Pass null when `imageSize` carries an
47
+ * exact `WIDTHxHEIGHT` resolution — that is authoritative, and sending a
48
+ * conflicting (or unsupported) ratio alongside it only muddies the request.
49
+ */
50
+ aspectRatio: string | null, imageSize: string | null, textOutput: boolean, imageGenUrl: string, referenceImageDataUrl?: string | string[], signal?: AbortSignal, brandImages?: {
25
51
  logo?: string;
26
52
  fontPreview?: string;
27
53
  /** Optional second font preview (brand body typeface) shown as a reference. */
@@ -56,4 +82,24 @@ export declare function callOpenRouterImageAPI(prompt: string, modelId: string,
56
82
  * manifest, so those callers are unaffected by this option.
57
83
  */
58
84
  referenceMode?: "user-priority" | "legacy-positional";
85
+ /**
86
+ * The image being edited or resized. Ranks directly below the user's own
87
+ * references and above the logo — on those surfaces the source IS the
88
+ * subject, so it must never be shed, and the logo becomes droppable.
89
+ */
90
+ sourceImageDataUrl?: string;
91
+ /**
92
+ * Whether the logo is part of the protected core. True on generate flows
93
+ * (the floor is 6 user references + the logo); false on quick edit and
94
+ * resize, where the source image outranks it.
95
+ */
96
+ protectLogo?: boolean;
97
+ /**
98
+ * How many brand-bank assets / ad references the host proxy may append.
99
+ * The SDK cannot see those images, so it reserves
100
+ * {@link HOST_ASSET_BYTE_RESERVATION} of budget per slot and tells the host
101
+ * how many survived; the host compresses each one to fit its reservation.
102
+ */
103
+ hostBrandAssetSlots?: number;
104
+ hostAdReferenceSlots?: number;
59
105
  }): Promise<string>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared image-compression primitives.
3
+ *
4
+ * Extracted from `components/ai-chat/imagePayload` so the image-generation
5
+ * surfaces (canvas chat, generator panel, quick edit, auto-resize) can use the
6
+ * same downscaling the chat request path has always used. Previously only chat
7
+ * shrank its images, which is why an image-gen request could still carry a raw
8
+ * 1 MB PNG straight at the host proxy's 4.5 MB platform cap.
9
+ *
10
+ * Everything here is best-effort and never throws: on any failure the original
11
+ * data URL is returned unchanged, and the caller's budget pass is the backstop.
12
+ */
13
+ /** Long-edge cap in CSS pixels. Well above what vision models resolve. */
14
+ export declare const MAX_EDGE_PX = 1280;
15
+ /** JPEG quality for re-encoded images. */
16
+ export declare const JPEG_QUALITY = 0.78;
17
+ /**
18
+ * Images already smaller than this are passed through untouched — re-encoding
19
+ * a 40KB thumbnail costs a canvas round-trip and can make it *larger*.
20
+ */
21
+ export declare const PASSTHROUGH_BYTES: number;
22
+ /** Estimated wire size of a data URL: the base64 payload is what gets sent. */
23
+ export declare function dataUrlWireBytes(dataUrl: string): number;
24
+ /**
25
+ * Whether a data URL is a raster format safe to send as an `image_url` block.
26
+ * SVG is commonly rejected outright by vision models, and canvas re-encoding
27
+ * of an SVG is unreliable across browsers, so it is neither shrunk nor sent.
28
+ */
29
+ export declare function isRasterImageDataUrl(dataUrl: string): boolean;
30
+ /**
31
+ * Downscale + re-encode a raster data URL so it is safe to embed in a request.
32
+ *
33
+ * Never throws: on any failure (decode error, no canvas context, non-raster
34
+ * input) the original data URL is returned unchanged.
35
+ */
36
+ export declare function shrinkImageDataUrl(dataUrl: string): Promise<string>;
37
+ /** Shrink a batch in parallel, preserving order. */
38
+ export declare function shrinkImageDataUrls(dataUrls: string[]): Promise<string[]>;
39
+ /**
40
+ * Force a re-encode at a specific long-edge cap, ignoring the passthrough rule.
41
+ * Used to escalate compression when a request is still over budget after the
42
+ * standard downscale — always preferable to dropping a reference outright.
43
+ */
44
+ export declare function reencodeAtEdge(dataUrl: string, edge: number, quality?: number): Promise<string>;
45
+ /** Escalation ladder used when the standard downscale is not enough. */
46
+ export declare const ESCALATION_EDGES: readonly [960, 720, 512];
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Ranked slot allocation for the reference images attached to an image
3
+ * generation request.
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * The previous rule was a single boolean: if the user attached ANY reference,
8
+ * the brand logo, both font specimens, and every host-injected brand asset were
9
+ * deleted. Unconditionally — no size check, no count check. That made
10
+ * "replace their logo with ours" unsatisfiable, because the logo the prompt
11
+ * asked for was never attached.
12
+ *
13
+ * The real constraint was never "brand assets compete with the user" — it is
14
+ * the host proxy's platform request cap (Vercel rejects bodies over 4.5 MB at
15
+ * the edge with an opaque 413). Competition is solved by ORDER and by the
16
+ * prompt's precedence rules, not by deletion.
17
+ *
18
+ * ## The contract
19
+ *
20
+ * Priority, highest first:
21
+ *
22
+ * 1. `user` — references the user attached themselves (max 6)
23
+ * 2. `source` — the image being edited or resized (quick edit, resize)
24
+ * 3. `logo` — the brand logo
25
+ * 4. `brandAsset` — product / lifestyle photos from the brand bank
26
+ * 5. `adReference` — existing ad creatives and template thumbnails
27
+ *
28
+ * Everything is sent when it fits. Truncation happens ONLY when the count or
29
+ * byte budget is exceeded, and always from the bottom up — never touching
30
+ * `user`, and never touching `logo` unless the caller marks it droppable
31
+ * (quick edit and resize, where the source image matters more).
32
+ *
33
+ * Brand assets and ad references degrade gracefully: surplus copies go first,
34
+ * so one of each survives as long as possible, and only then is a whole tier
35
+ * given up — ad references before brand assets, per the priority order.
36
+ *
37
+ * The floor is therefore 6 user references + the logo.
38
+ *
39
+ * ## Compression first, always
40
+ *
41
+ * Dropping a reference is the last resort, not the first. `prepareReferences`
42
+ * downscales everything, then escalates compression on the largest offenders,
43
+ * and only starts shedding slots if the payload is *still* over budget.
44
+ */
45
+ /** Reference tiers in descending priority. Index === priority rank. */
46
+ export declare const REFERENCE_TIERS: readonly ["user", "source", "logo", "brandAsset", "adReference"];
47
+ export type ReferenceTier = typeof REFERENCE_TIERS[number];
48
+ export interface ReferenceSlot {
49
+ tier: ReferenceTier;
50
+ /**
51
+ * The image itself. Empty string marks a *placeholder* — a slot the host
52
+ * proxy will fill from the brand database, which the SDK can only reserve
53
+ * room for because it never sees the bytes. `bytes` carries the reservation.
54
+ */
55
+ dataUrl: string;
56
+ /** Human label used in the reference manifest. */
57
+ label?: string;
58
+ /** Wire-byte estimate. Derived from `dataUrl` when omitted. */
59
+ bytes?: number;
60
+ }
61
+ export interface DroppedSlot {
62
+ tier: ReferenceTier;
63
+ label?: string;
64
+ reason: "count" | "bytes";
65
+ }
66
+ /**
67
+ * Hard cap on user-supplied reference images per generation. Six covers every
68
+ * realistic ad brief and keeps the request inside the limits of the models we
69
+ * route to.
70
+ */
71
+ export declare const MAX_USER_REFERENCE_IMAGES = 6;
72
+ /**
73
+ * Ceiling on total attached references. 6 user + 1 source + 1 logo + 2 brand
74
+ * assets + 2 ad references. Comfortably under the ~20 images-per-request cap
75
+ * observed on OpenRouter.
76
+ */
77
+ export declare const MAX_REFERENCE_IMAGES = 12;
78
+ /**
79
+ * Wire-byte budget for the attached images alone.
80
+ *
81
+ * Vercel rejects at 4.5 MB. This leaves ~1.3 MB of headroom for the prompt,
82
+ * the brand context block, and the JSON envelope — all of which the caller can
83
+ * further reserve against via `reservedBytes`.
84
+ */
85
+ export declare const REFERENCE_WIRE_BUDGET_BYTES: number;
86
+ /**
87
+ * What the SDK assumes a host-injected brand asset costs on the wire, and the
88
+ * ceiling the host compresses each one down to before injecting it. Keeping
89
+ * both sides on the same number is what makes the SDK's reservation honest.
90
+ */
91
+ export declare const HOST_ASSET_BYTE_RESERVATION: number;
92
+ /** Sort into priority order, stable within a tier. */
93
+ export declare function orderByPriority(slots: ReferenceSlot[]): ReferenceSlot[];
94
+ /**
95
+ * The order in which slots may be shed, first to go listed first.
96
+ *
97
+ * `user` and `source` never appear — they are the request. `logo` appears only
98
+ * when `protectLogo` is false, and always last, so it outlives every brand
99
+ * asset.
100
+ */
101
+ export declare function shedOrder(slots: ReferenceSlot[], protectLogo: boolean): ReferenceSlot[];
102
+ export interface AllocateOptions {
103
+ /** Count ceiling. Defaults to {@link MAX_REFERENCE_IMAGES}. */
104
+ maxImages?: number;
105
+ /** Byte ceiling for images. Defaults to {@link REFERENCE_WIRE_BUDGET_BYTES}. */
106
+ maxBytes?: number;
107
+ /** Non-image payload (prompt, schemas) to charge against `maxBytes`. */
108
+ reservedBytes?: number;
109
+ /**
110
+ * False on surfaces where the source image outranks the logo — quick edit and
111
+ * resize. On generate flows the logo is part of the protected core.
112
+ */
113
+ protectLogo?: boolean;
114
+ }
115
+ export interface Allocation {
116
+ kept: ReferenceSlot[];
117
+ dropped: DroppedSlot[];
118
+ bytes: number;
119
+ /**
120
+ * True when the protected core alone still exceeds the budget. The caller
121
+ * should send anyway — a 413 with the user's own images is a better failure
122
+ * than silently generating without them.
123
+ */
124
+ overBudget: boolean;
125
+ }
126
+ /**
127
+ * Pure allocation: order, cap the user tier, then shed bottom-up until the
128
+ * count and byte budgets are met. No compression — see `prepareReferences`.
129
+ */
130
+ export declare function allocateReferences(slots: ReferenceSlot[], options?: AllocateOptions): Allocation;
131
+ /**
132
+ * Compress everything, escalate on the largest offenders, and only then shed
133
+ * slots. This is the entry point every image-generation surface should use.
134
+ *
135
+ * Placeholder slots (`dataUrl === ""`) are carried through untouched — the host
136
+ * compresses those on its side against {@link HOST_ASSET_BYTE_RESERVATION}.
137
+ */
138
+ export declare function prepareReferences(slots: ReferenceSlot[], options?: AllocateOptions): Promise<Allocation>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangecatai/adgen-canvas",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "type": "module",
5
5
  "types": "./dist/types/excalidraw/index.d.ts",
6
6
  "main": "./dist/prod/index.js",