Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/image-merge-collage-intent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@transloadit/node': minor
'@transloadit/mcp-server': patch
'transloadit': minor
---

Add an `image merge` intent command that wires up the `/image/merge` Robot's new `polaroid-stack`
and `mosaic` collage effects alongside the classic spritesheet modes. Also syncs the updated
`/image/merge` schema from alphalib.
49 changes: 49 additions & 0 deletions packages/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ All intent commands also support the global CLI flags `--json`, `--log-level`, `
| `image generate` | Generate images from text prompts | file, dir, URL, base64 | file |
| `preview generate` | Generate a preview thumbnail | file, dir, URL, base64 | file |
| `image remove-background` | Remove the background from images | file, dir, URL, base64 | file |
| `image merge` | Merge several images into a single image | file, dir, URL, base64 | file |
| `image optimize` | Optimize images without quality loss | file, dir, URL, base64 | file |
| `image resize` | Convert, resize, or watermark images | file, dir, URL, base64 | file |
| `document convert` | Convert documents into different formats | file, dir, URL, base64 | file |
Expand Down Expand Up @@ -354,6 +355,53 @@ npx transloadit image remove-background --input <path|dir|url|-> [options]
transloadit image remove-background --input input.png --output output.png
```

#### `image merge`

Merge several images into a single image

Runs `/image/merge` for the provided inputs and writes the result to `--output`.

**Usage**

```bash
npx transloadit image merge --input <path|dir|url|-> [options]
```

**Quick facts**

- Input: file, dir, URL, base64
- Output: file
- Execution: single assembly
- Backend: `/image/merge`

**Shared flags**

- Uses the shared file input and output flags listed above.
- Also supports the shared base processing flags listed above.

**Command options**

| Flag | Type | Required | Example | Description |
| --- | --- | --- | --- | --- |
| `--format` | `string` | no | `jpg` | The output format for the modified image. |
| `--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… |
| `--effect` | `string` | no | `mosaic` | Applies a styled collage layout instead of a plain horizontal or vertical spritesheet. |
| `--border` | `number` | no | `1` | An integer value which defines the gap between images on the spritesheet. |
| `--background` | `string` | no | `transparent` | Either the hexadecimal code or name of the color used to fill the background (only shown with a border > 1). |
| `--width` | `number` | no | `1` | The output canvas width in pixels. This is mainly used by styled effects such as polaroid-stack and mosaic. |
| `--height` | `number` | no | `1` | The output canvas height in pixels. This is mainly used by styled effects such as polaroid-stack and mosaic. |
| `--seed` | `number` | no | `1` | Optional deterministic seed used by styled effects such as polaroid-stack and mosaic. |
| `--shuffle` | `boolean` | no | `true` | Whether styled effects such as polaroid-stack and mosaic may shuffle the input order before laying out the canvas. |
| `--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.… |
| `--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. |
| `--quality` | `number` | no | `1` | Controls the image compression for JPG, PNG, and WebP images. Please also take a look at 🤖/image/optimize. |

**Examples**

```bash
transloadit image merge --input input.png --output output.png
```

#### `image optimize`

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




48 changes: 48 additions & 0 deletions packages/node/docs/intent-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ All intent commands also support the global CLI flags `--json`, `--log-level`, `
| `image generate` | Generate images from text prompts | file, dir, URL, base64 | file |
| `preview generate` | Generate a preview thumbnail | file, dir, URL, base64 | file |
| `image remove-background` | Remove the background from images | file, dir, URL, base64 | file |
| `image merge` | Merge several images into a single image | file, dir, URL, base64 | file |
| `image optimize` | Optimize images without quality loss | file, dir, URL, base64 | file |
| `image resize` | Convert, resize, or watermark images | file, dir, URL, base64 | file |
| `document convert` | Convert documents into different formats | file, dir, URL, base64 | file |
Expand Down Expand Up @@ -222,6 +223,53 @@ npx transloadit image remove-background --input <path|dir|url|-> [options]
transloadit image remove-background --input input.png --output output.png
```

## `image merge`

Merge several images into a single image

Runs `/image/merge` for the provided inputs and writes the result to `--output`.

**Usage**

```bash
npx transloadit image merge --input <path|dir|url|-> [options]
```

**Quick facts**

- Input: file, dir, URL, base64
- Output: file
- Execution: single assembly
- Backend: `/image/merge`

**Shared flags**

- Uses the shared file input and output flags listed above.
- Also supports the shared base processing flags listed above.

**Command options**

| Flag | Type | Required | Example | Description |
| --- | --- | --- | --- | --- |
| `--format` | `string` | no | `jpg` | The output format for the modified image. |
| `--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… |
| `--effect` | `string` | no | `mosaic` | Applies a styled collage layout instead of a plain horizontal or vertical spritesheet. |
| `--border` | `number` | no | `1` | An integer value which defines the gap between images on the spritesheet. |
| `--background` | `string` | no | `transparent` | Either the hexadecimal code or name of the color used to fill the background (only shown with a border > 1). |
| `--width` | `number` | no | `1` | The output canvas width in pixels. This is mainly used by styled effects such as polaroid-stack and mosaic. |
| `--height` | `number` | no | `1` | The output canvas height in pixels. This is mainly used by styled effects such as polaroid-stack and mosaic. |
| `--seed` | `number` | no | `1` | Optional deterministic seed used by styled effects such as polaroid-stack and mosaic. |
| `--shuffle` | `boolean` | no | `true` | Whether styled effects such as polaroid-stack and mosaic may shuffle the input order before laying out the canvas. |
| `--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.… |
| `--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. |
| `--quality` | `number` | no | `1` | Controls the image compression for JPG, PNG, and WebP images. Please also take a look at 🤖/image/optimize. |

**Examples**

```bash
transloadit image merge --input input.png --output output.png
```

## `image optimize`

Optimize images without quality loss
Expand Down
82 changes: 75 additions & 7 deletions packages/node/src/alphalib/types/robots/image-merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { z } from 'zod'

import type { RobotMetaInput } from './_instructions-primitives.ts'
import {
color_without_alpha,
imageQualitySchema,
color_without_alpha_with_named,
interpolateRobot,
robotBase,
robotUse,
Expand Down Expand Up @@ -59,13 +58,25 @@ It's recommended to use this Robot with
similar size before merging them.
`),
format: z
.enum(['jpg', 'png'])
.enum(['jpg', 'png', 'webp'])
.default('png')
.describe('The output format for the modified image.'),
direction: z
.enum(['horizontal', 'vertical'])
.default('horizontal')
.describe('Specifies the direction which the images are displayed.'),
.describe(`
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 use their own layout algorithms.
`),
effect: z
.enum(['mosaic', 'polaroid-stack'])
.optional()
.describe(`
Applies a styled collage layout instead of a plain horizontal or vertical spritesheet.

Currently supports \`polaroid-stack\`, which renders the inputs as overlapping instant photos on a canvas, and \`mosaic\`, which builds a justified tiled collage.
`),
// TODO: default is not between 1 and 10
border: z
.number()
Expand All @@ -75,21 +86,78 @@ similar size before merging them.
An integer value which defines the gap between images on the spritesheet.

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.

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.

When \`effect\` is \`mosaic\`, this value is used both as the outer canvas padding and as the gutter width between neighbouring tiles.
`),
background: color_without_alpha.default('#FFFFFF').describe(`
background: color_without_alpha_with_named.default('#fff').describe(`
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).

By default, the background of transparent images is changed to white.
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.

For details about how to preserve transparency across all image types, see [this demo](/demos/image-manipulation/properly-preserve-transparency-across-all-image-types/).
`),
width: z
.number()
.int()
.positive()
.optional()
.describe(`
The output canvas width in pixels.

This is mainly used by styled effects such as \`polaroid-stack\` and \`mosaic\`.
`),
height: z
.number()
.int()
.positive()
.optional()
.describe(`
The output canvas height in pixels.

This is mainly used by styled effects such as \`polaroid-stack\` and \`mosaic\`.
`),
seed: z
.number()
.int()
.optional()
.describe(`
Optional deterministic seed used by styled effects such as \`polaroid-stack\` and \`mosaic\`.
`),
shuffle: z
.boolean()
.default(false)
.describe(`
Whether styled effects such as \`polaroid-stack\` and \`mosaic\` may shuffle the input order before laying out the canvas.
`),
coverage: z
.number()
.min(0.5)
.max(3)
.optional()
.describe(`
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.

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.

Has no effect on the \`mosaic\` style or on plain spritesheets.
`),
adaptive_filtering: z
.boolean()
.default(false)
.describe(`
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.
`),
quality: imageQualitySchema,
quality: z
.number()
.int()
.min(1)
.max(100)
.default(100)
.describe(`
Controls the image compression for JPG, PNG, and WebP images. Please also take a look at [🤖/image/optimize](/docs/robots/image-optimize/).
`),
})
.strict()

Expand Down
11 changes: 11 additions & 0 deletions packages/node/src/cli/intentCommandSpecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ import {
robotImageBgremoveInstructionsSchema,
meta as robotImageBgremoveMeta,
} from '../alphalib/types/robots/image-bgremove.ts'
import {
robotImageMergeInstructionsSchema,
meta as robotImageMergeMeta,
} from '../alphalib/types/robots/image-merge.ts'
import {
robotImageOptimizeInstructionsSchema,
meta as robotImageOptimizeMeta,
Expand Down Expand Up @@ -183,6 +187,13 @@ export const intentCatalog = [
meta: robotImageBgremoveMeta,
schema: robotImageBgremoveInstructionsSchema,
}),
defineRobotIntent({
kind: 'robot',
robot: '/image/merge',
defaultSingleAssembly: true,
meta: robotImageMergeMeta,
schema: robotImageMergeInstructionsSchema,
}),
defineRobotIntent({
kind: 'robot',
robot: '/image/optimize',
Expand Down
12 changes: 12 additions & 0 deletions packages/node/test/support/intentSmokeCases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ const intentSmokeOverrides: Record<string, Omit<IntentSmokeCase, 'key' | 'paths'
outputPath: 'image-remove-background.png',
verifier: 'png',
},
'/image/merge': {
args: [
'--input',
'@fixture/input.jpg',
'--input',
'@fixture/input.jpg',
'--effect',
'polaroid-stack',
],
outputPath: 'image-merge.png',
verifier: 'png',
},
'image-generate:image/generate': {
args: ['--prompt', 'A small red bicycle on a cream background, studio lighting'],
outputPath: 'image-generate.png',
Expand Down
43 changes: 43 additions & 0 deletions packages/node/test/unit/cli/intents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,49 @@ describe('intent commands', () => {
)
})

it('maps image merge to a bundled single assembly with collage effect', async () => {
const { createSpy } = await runIntentCommand([
'image',
'merge',
'--input',
'photo-a.jpg',
'--input',
'photo-b.jpg',
'--effect',
'polaroid-stack',
'--width',
'1920',
'--height',
'1200',
'--output',
'collage.png',
])

expect(process.exitCode).toBeUndefined()
expect(createSpy).toHaveBeenCalledWith(
expect.any(OutputCtl),
expect.anything(),
expect.objectContaining({
inputs: ['photo-a.jpg', 'photo-b.jpg'],
output: 'collage.png',
singleAssembly: true,
stepsData: {
[getIntentStepName(['image', 'merge'])]: expect.objectContaining({
robot: '/image/merge',
result: true,
effect: 'polaroid-stack',
width: 1920,
height: 1200,
use: {
steps: [':original'],
bundle_steps: true,
},
}),
},
}),
)
})

it('omits nullable defaults like file compress password when not provided', async () => {
const { createSpy } = await runIntentCommand([
'file',
Expand Down
Loading