Skip to content

Commit a6b4233

Browse files
authored
Add image merge intent with polaroid-stack + mosaic collage effects (#393)
Squash-merge of kvz/collage-intent: add `image merge` intent and sync `/image/merge` schema for the new polaroid-stack and mosaic effects shipped in transloadit/api2#7651.
1 parent 68e024a commit a6b4233

8 files changed

Lines changed: 296 additions & 7 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@transloadit/node': minor
3+
'@transloadit/mcp-server': patch
4+
'transloadit': minor
5+
---
6+
7+
Add an `image merge` intent command that wires up the `/image/merge` Robot's new `polaroid-stack`
8+
and `mosaic` collage effects alongside the classic spritesheet modes. Also syncs the updated
9+
`/image/merge` schema from alphalib.

packages/node/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ All intent commands also support the global CLI flags `--json`, `--log-level`, `
147147
| `image generate` | Generate images from text prompts | file, dir, URL, base64 | file |
148148
| `preview generate` | Generate a preview thumbnail | file, dir, URL, base64 | file |
149149
| `image remove-background` | Remove the background from images | file, dir, URL, base64 | file |
150+
| `image merge` | Merge several images into a single image | file, dir, URL, base64 | file |
150151
| `image optimize` | Optimize images without quality loss | file, dir, URL, base64 | file |
151152
| `image resize` | Convert, resize, or watermark images | file, dir, URL, base64 | file |
152153
| `document convert` | Convert documents into different formats | file, dir, URL, base64 | file |
@@ -354,6 +355,53 @@ npx transloadit image remove-background --input <path|dir|url|-> [options]
354355
transloadit image remove-background --input input.png --output output.png
355356
```
356357

358+
#### `image merge`
359+
360+
Merge several images into a single image
361+
362+
Runs `/image/merge` for the provided inputs and writes the result to `--output`.
363+
364+
**Usage**
365+
366+
```bash
367+
npx transloadit image merge --input <path|dir|url|-> [options]
368+
```
369+
370+
**Quick facts**
371+
372+
- Input: file, dir, URL, base64
373+
- Output: file
374+
- Execution: single assembly
375+
- Backend: `/image/merge`
376+
377+
**Shared flags**
378+
379+
- Uses the shared file input and output flags listed above.
380+
- Also supports the shared base processing flags listed above.
381+
382+
**Command options**
383+
384+
| Flag | Type | Required | Example | Description |
385+
| --- | --- | --- | --- | --- |
386+
| `--format` | `string` | no | `jpg` | The output format for the modified image. |
387+
| `--direction` | `string` | no | `horizontal` | Specifies the direction which the images are displayed. Only applies to the default spritesheet layout. Ignored when effect is set to polaroid-stack or mosaic, as those effects… |
388+
| `--effect` | `string` | no | `mosaic` | Applies a styled collage layout instead of a plain horizontal or vertical spritesheet. |
389+
| `--border` | `number` | no | `1` | An integer value which defines the gap between images on the spritesheet. |
390+
| `--background` | `string` | no | `transparent` | Either the hexadecimal code or name of the color used to fill the background (only shown with a border > 1). |
391+
| `--width` | `number` | no | `1` | The output canvas width in pixels. This is mainly used by styled effects such as polaroid-stack and mosaic. |
392+
| `--height` | `number` | no | `1` | The output canvas height in pixels. This is mainly used by styled effects such as polaroid-stack and mosaic. |
393+
| `--seed` | `number` | no | `1` | Optional deterministic seed used by styled effects such as polaroid-stack and mosaic. |
394+
| `--shuffle` | `boolean` | no | `true` | Whether styled effects such as polaroid-stack and mosaic may shuffle the input order before laying out the canvas. |
395+
| `--coverage` | `number` | no | `1` | Area-coverage multiplier for the polaroid-stack effect. Controls how large each polaroid is relative to the canvas and consequently how much of the canvas is covered by photos.… |
396+
| `--adaptive-filtering` | `boolean` | no | `true` | Controls the image compression for PNG images. Setting to true results in smaller file size, while increasing processing time. It is encouraged to keep this option disabled. |
397+
| `--quality` | `number` | no | `1` | Controls the image compression for JPG, PNG, and WebP images. Please also take a look at 🤖/image/optimize. |
398+
399+
**Examples**
400+
401+
```bash
402+
transloadit image merge --input input.png --output output.png
403+
```
404+
357405
#### `image optimize`
358406

359407
Optimize images without quality loss
@@ -1811,3 +1859,4 @@ See [CONTRIBUTING](./CONTRIBUTING.md).
18111859

18121860

18131861

1862+

packages/node/docs/intent-commands.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ All intent commands also support the global CLI flags `--json`, `--log-level`, `
1515
| `image generate` | Generate images from text prompts | file, dir, URL, base64 | file |
1616
| `preview generate` | Generate a preview thumbnail | file, dir, URL, base64 | file |
1717
| `image remove-background` | Remove the background from images | file, dir, URL, base64 | file |
18+
| `image merge` | Merge several images into a single image | file, dir, URL, base64 | file |
1819
| `image optimize` | Optimize images without quality loss | file, dir, URL, base64 | file |
1920
| `image resize` | Convert, resize, or watermark images | file, dir, URL, base64 | file |
2021
| `document convert` | Convert documents into different formats | file, dir, URL, base64 | file |
@@ -222,6 +223,53 @@ npx transloadit image remove-background --input <path|dir|url|-> [options]
222223
transloadit image remove-background --input input.png --output output.png
223224
```
224225

226+
## `image merge`
227+
228+
Merge several images into a single image
229+
230+
Runs `/image/merge` for the provided inputs and writes the result to `--output`.
231+
232+
**Usage**
233+
234+
```bash
235+
npx transloadit image merge --input <path|dir|url|-> [options]
236+
```
237+
238+
**Quick facts**
239+
240+
- Input: file, dir, URL, base64
241+
- Output: file
242+
- Execution: single assembly
243+
- Backend: `/image/merge`
244+
245+
**Shared flags**
246+
247+
- Uses the shared file input and output flags listed above.
248+
- Also supports the shared base processing flags listed above.
249+
250+
**Command options**
251+
252+
| Flag | Type | Required | Example | Description |
253+
| --- | --- | --- | --- | --- |
254+
| `--format` | `string` | no | `jpg` | The output format for the modified image. |
255+
| `--direction` | `string` | no | `horizontal` | Specifies the direction which the images are displayed. Only applies to the default spritesheet layout. Ignored when effect is set to polaroid-stack or mosaic, as those effects… |
256+
| `--effect` | `string` | no | `mosaic` | Applies a styled collage layout instead of a plain horizontal or vertical spritesheet. |
257+
| `--border` | `number` | no | `1` | An integer value which defines the gap between images on the spritesheet. |
258+
| `--background` | `string` | no | `transparent` | Either the hexadecimal code or name of the color used to fill the background (only shown with a border > 1). |
259+
| `--width` | `number` | no | `1` | The output canvas width in pixels. This is mainly used by styled effects such as polaroid-stack and mosaic. |
260+
| `--height` | `number` | no | `1` | The output canvas height in pixels. This is mainly used by styled effects such as polaroid-stack and mosaic. |
261+
| `--seed` | `number` | no | `1` | Optional deterministic seed used by styled effects such as polaroid-stack and mosaic. |
262+
| `--shuffle` | `boolean` | no | `true` | Whether styled effects such as polaroid-stack and mosaic may shuffle the input order before laying out the canvas. |
263+
| `--coverage` | `number` | no | `1` | Area-coverage multiplier for the polaroid-stack effect. Controls how large each polaroid is relative to the canvas and consequently how much of the canvas is covered by photos.… |
264+
| `--adaptive-filtering` | `boolean` | no | `true` | Controls the image compression for PNG images. Setting to true results in smaller file size, while increasing processing time. It is encouraged to keep this option disabled. |
265+
| `--quality` | `number` | no | `1` | Controls the image compression for JPG, PNG, and WebP images. Please also take a look at 🤖/image/optimize. |
266+
267+
**Examples**
268+
269+
```bash
270+
transloadit image merge --input input.png --output output.png
271+
```
272+
225273
## `image optimize`
226274

227275
Optimize images without quality loss

packages/node/src/alphalib/types/robots/image-merge.ts

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { z } from 'zod'
22

33
import type { RobotMetaInput } from './_instructions-primitives.ts'
44
import {
5-
color_without_alpha,
6-
imageQualitySchema,
5+
color_without_alpha_with_named,
76
interpolateRobot,
87
robotBase,
98
robotUse,
@@ -59,13 +58,25 @@ It's recommended to use this Robot with
5958
similar size before merging them.
6059
`),
6160
format: z
62-
.enum(['jpg', 'png'])
61+
.enum(['jpg', 'png', 'webp'])
6362
.default('png')
6463
.describe('The output format for the modified image.'),
6564
direction: z
6665
.enum(['horizontal', 'vertical'])
6766
.default('horizontal')
68-
.describe('Specifies the direction which the images are displayed.'),
67+
.describe(`
68+
Specifies the direction which the images are displayed.
69+
70+
Only applies to the default spritesheet layout. Ignored when \`effect\` is set to \`polaroid-stack\` or \`mosaic\`, as those effects use their own layout algorithms.
71+
`),
72+
effect: z
73+
.enum(['mosaic', 'polaroid-stack'])
74+
.optional()
75+
.describe(`
76+
Applies a styled collage layout instead of a plain horizontal or vertical spritesheet.
77+
78+
Currently supports \`polaroid-stack\`, which renders the inputs as overlapping instant photos on a canvas, and \`mosaic\`, which builds a justified tiled collage.
79+
`),
6980
// TODO: default is not between 1 and 10
7081
border: z
7182
.number()
@@ -75,21 +86,78 @@ similar size before merging them.
7586
An integer value which defines the gap between images on the spritesheet.
7687
7788
A value of \`10\` would cause the images to have the largest gap between them, while a value of \`1\` would place the images side-by-side.
89+
90+
When \`effect\` is \`polaroid-stack\`, this value is instead used as canvas padding so the outermost photos keep that many pixels of distance from the edge.
91+
92+
When \`effect\` is \`mosaic\`, this value is used both as the outer canvas padding and as the gutter width between neighbouring tiles.
7893
`),
79-
background: color_without_alpha.default('#FFFFFF').describe(`
94+
background: color_without_alpha_with_named.default('#fff').describe(`
8095
Either the hexadecimal code or [name](https://www.imagemagick.org/script/color.php#color_names) of the color used to fill the background (only shown with a border > 1).
8196
82-
By default, the background of transparent images is changed to white.
97+
By default, the background of transparent images is changed to white. Set to \`none\` or \`transparent\` for a transparent canvas — requires \`format\` \`png\` or \`webp\` to preserve alpha.
8398
8499
For details about how to preserve transparency across all image types, see [this demo](/demos/image-manipulation/properly-preserve-transparency-across-all-image-types/).
100+
`),
101+
width: z
102+
.number()
103+
.int()
104+
.positive()
105+
.optional()
106+
.describe(`
107+
The output canvas width in pixels.
108+
109+
This is mainly used by styled effects such as \`polaroid-stack\` and \`mosaic\`.
110+
`),
111+
height: z
112+
.number()
113+
.int()
114+
.positive()
115+
.optional()
116+
.describe(`
117+
The output canvas height in pixels.
118+
119+
This is mainly used by styled effects such as \`polaroid-stack\` and \`mosaic\`.
120+
`),
121+
seed: z
122+
.number()
123+
.int()
124+
.optional()
125+
.describe(`
126+
Optional deterministic seed used by styled effects such as \`polaroid-stack\` and \`mosaic\`.
127+
`),
128+
shuffle: z
129+
.boolean()
130+
.default(false)
131+
.describe(`
132+
Whether styled effects such as \`polaroid-stack\` and \`mosaic\` may shuffle the input order before laying out the canvas.
133+
`),
134+
coverage: z
135+
.number()
136+
.min(0.5)
137+
.max(3)
138+
.optional()
139+
.describe(`
140+
Area-coverage multiplier for the \`polaroid-stack\` effect. Controls how large each polaroid is relative to the canvas and consequently how much of the canvas is covered by photos.
141+
142+
The default of \`1.5\` leaves a subtle beige border along some edges. Use \`2.0\`–\`2.5\` for edge-to-edge coverage (photos overlap more). Values below \`1.0\` produce smaller, more widely spaced polaroids.
143+
144+
Has no effect on the \`mosaic\` style or on plain spritesheets.
85145
`),
86146
adaptive_filtering: z
87147
.boolean()
88148
.default(false)
89149
.describe(`
90150
Controls the image compression for PNG images. Setting to \`true\` results in smaller file size, while increasing processing time. It is encouraged to keep this option disabled.
91151
`),
92-
quality: imageQualitySchema,
152+
quality: z
153+
.number()
154+
.int()
155+
.min(1)
156+
.max(100)
157+
.default(100)
158+
.describe(`
159+
Controls the image compression for JPG, PNG, and WebP images. Please also take a look at [🤖/image/optimize](/docs/robots/image-optimize/).
160+
`),
93161
})
94162
.strict()
95163

packages/node/src/cli/intentCommandSpecs.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ import {
3737
robotImageBgremoveInstructionsSchema,
3838
meta as robotImageBgremoveMeta,
3939
} from '../alphalib/types/robots/image-bgremove.ts'
40+
import {
41+
robotImageMergeInstructionsSchema,
42+
meta as robotImageMergeMeta,
43+
} from '../alphalib/types/robots/image-merge.ts'
4044
import {
4145
robotImageOptimizeInstructionsSchema,
4246
meta as robotImageOptimizeMeta,
@@ -183,6 +187,13 @@ export const intentCatalog = [
183187
meta: robotImageBgremoveMeta,
184188
schema: robotImageBgremoveInstructionsSchema,
185189
}),
190+
defineRobotIntent({
191+
kind: 'robot',
192+
robot: '/image/merge',
193+
defaultSingleAssembly: true,
194+
meta: robotImageMergeMeta,
195+
schema: robotImageMergeInstructionsSchema,
196+
}),
186197
defineRobotIntent({
187198
kind: 'robot',
188199
robot: '/image/optimize',

packages/node/test/support/intentSmokeCases.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ const intentSmokeOverrides: Record<string, Omit<IntentSmokeCase, 'key' | 'paths'
5858
outputPath: 'image-remove-background.png',
5959
verifier: 'png',
6060
},
61+
'/image/merge': {
62+
args: [
63+
'--input',
64+
'@fixture/input.jpg',
65+
'--input',
66+
'@fixture/input.jpg',
67+
'--effect',
68+
'polaroid-stack',
69+
],
70+
outputPath: 'image-merge.png',
71+
verifier: 'png',
72+
},
6173
'image-generate:image/generate': {
6274
args: ['--prompt', 'A small red bicycle on a cream background, studio lighting'],
6375
outputPath: 'image-generate.png',

packages/node/test/unit/cli/intents.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,6 +1529,49 @@ describe('intent commands', () => {
15291529
)
15301530
})
15311531

1532+
it('maps image merge to a bundled single assembly with collage effect', async () => {
1533+
const { createSpy } = await runIntentCommand([
1534+
'image',
1535+
'merge',
1536+
'--input',
1537+
'photo-a.jpg',
1538+
'--input',
1539+
'photo-b.jpg',
1540+
'--effect',
1541+
'polaroid-stack',
1542+
'--width',
1543+
'1920',
1544+
'--height',
1545+
'1200',
1546+
'--output',
1547+
'collage.png',
1548+
])
1549+
1550+
expect(process.exitCode).toBeUndefined()
1551+
expect(createSpy).toHaveBeenCalledWith(
1552+
expect.any(OutputCtl),
1553+
expect.anything(),
1554+
expect.objectContaining({
1555+
inputs: ['photo-a.jpg', 'photo-b.jpg'],
1556+
output: 'collage.png',
1557+
singleAssembly: true,
1558+
stepsData: {
1559+
[getIntentStepName(['image', 'merge'])]: expect.objectContaining({
1560+
robot: '/image/merge',
1561+
result: true,
1562+
effect: 'polaroid-stack',
1563+
width: 1920,
1564+
height: 1200,
1565+
use: {
1566+
steps: [':original'],
1567+
bundle_steps: true,
1568+
},
1569+
}),
1570+
},
1571+
}),
1572+
)
1573+
})
1574+
15321575
it('omits nullable defaults like file compress password when not provided', async () => {
15331576
const { createSpy } = await runIntentCommand([
15341577
'file',

0 commit comments

Comments
 (0)