# Image Tools (`marcel-rbro/image-tools`) Actor

Resize, crop, rotate, convert, compress and watermark an image in one call. Reads JPEG, PNG, GIF, WebP, TIFF, BMP, HEIC and AVIF. Built to be safe to hand to an AI agent: typed, allowlisted operations instead of an ImageMagick command line, and a clear refusal when the file is not really an image.

- **URL**: https://apify.com/marcel-rbro/image-tools.md
- **Developed by:** [Marcel Rebro](https://apify.com/marcel-rbro) (community)
- **Categories:** Agents, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 40.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

Resize, crop, rotate, convert, compress and watermark images — one image or a batch of up to 100 — through one API call. Built to be usable as a tool by an AI agent: typed operations, no shell, and a clear refusal when the input is not an image.

- 🧰 **One call does the lot** — crop, resize, rotate, flip, grayscale, blur, watermark, re-encode
- 📦 **Batch up to 100 images** per run, returned as a ZIP, with one bad file not sinking the rest
- 🖼️ **Reads 8 formats** — JPEG, PNG, GIF, WebP, TIFF, BMP, HEIC and AVIF
- 📐 **Keeps the source format** unless you ask otherwise, so a resize stays a PNG
- 📍 **Strips GPS and EXIF on request**, while keeping the colour profile
- 🤖 **Safe to hand to an agent** — allowlisted operations, never an ImageMagick argument string
- 🔌 **Callable from the API, a schedule, Zapier, Make, n8n or an MCP client**

### How to use it

1. Upload one or more images, or paste direct URLs into **Images**.
2. Set the operations you want. Everything is optional.
3. Run it. Results land in the **Storage** tab and in the run's dataset records.

Leave **Output format** empty and the source format is kept — that is what you want for a pure resize or crop.

### Batch processing

Upload up to 100 files, or pass a list of URLs in `files`. The same operations are applied to every one.

- **One failure does not sink the run.** An unreadable file, a 404 or a video in the list is recorded as a dataset row with `ok: false` and the reason, then skipped. The run fails only if *nothing* converted.
- **Colliding names get a suffix, not a prefix.** `photo.png` and `photo.jpg` both want to become `photo.webp`, so the second becomes `photo-2.webp`. Single-file runs keep their plain filename.
- **Images are processed in sequence and their scratch files deleted as they go**, so peak memory and disk stay at roughly one source plus one output regardless of batch size.
- **Downloads have a 60-second timeout.** In a single run a dead host wastes that run; in a batch it would stall everything queued behind it.

#### Run limits

A batch is charged by the second, so an unguarded run turns a few hundred uploads into a timed-out run you still pay for. Four limits are checked as the run proceeds, each failing with the number that hit it:

| Limit | Value |
|---|---|
| Images per run | 100 |
| Total input bytes | 512 MB |
| Pixels in any one image | 128 MP |
| Pixels across the run | 4,000 MP |

The pixel ceilings are set so the case this Actor advertises — a folder of 100 phone photos at 12 MP — fits comfortably. They exist to stop 100 × 48 MP from silently becoming a timeout.

#### What `run-sync` gives back

`OUTPUT` holds exactly one record, and `run-sync` returns exactly one body:

| Run | `OUTPUT` |
|---|---|
| One image | The image itself, so `curl --output photo.webp` just works |
| A batch | A ZIP of every result, also stored as `processed-images.zip` |
| A batch over 192 MB of output | A JSON manifest of keys and URLs, since the archive is built in memory |

Every image is also its own key-value store record in all three cases, so nothing is only available inside the archive.

### Operation order

Operations are not independent, so they run in a fixed order rather than the order you happen to list them:

```
auto-orient → rotate → flip/flop → crop → resize → grayscale → blur → watermark → flatten → strip metadata → format + quality
```

Orientation is settled first so that every later geometry is measured against an upright image; cropping picks the region before resizing scales it; and the target format's own concerns come last.

### Input

| Field | Description |
|---|---|
| `files` | The images. A multi-file upload, or a list of URLs. Up to 100. |
| `outputFormat` | `jpeg`, `png`, `webp`, `avif`, `tiff`, `gif`. Empty keeps the source format. |
| `quality` | 1–100, default 90. Used by JPEG, WebP and AVIF. |
| `width`, `height`, `resizeFit` | Resize. `inside` (default) keeps the aspect ratio and never enlarges; `force` hits the exact size; `grow` allows upscaling. |
| `cropWidth`, `cropHeight`, `cropGravity` | Crop a region of that size anchored at the gravity. Both dimensions are needed. |
| `rotate`, `flipVertical`, `flipHorizontal` | Transform. Rotation is clockwise degrees. |
| `grayscale`, `blur` | Filters. Blur 3–8 is a normal soft blur. |
| `background` | Used when rotating off-axis and when flattening transparency into JPEG. |
| `watermarkText`, `watermarkGravity`, `watermarkColour`, `watermarkSize` | A short caption drawn onto the image. |
| `keepMetadata` | `true` by default. `false` strips EXIF, XMP and IPTC. |
| `filesBase64`, `fileBase64`, `filename` | Images as base64, for callers holding bytes rather than URLs. |

The 0.1 field names (`imageUrl`, `imageUrls`, `imageBase64`) still work as hidden aliases.

#### Cropping without knowing the source size

`cropGravity` anchors the crop region, so "a 400×400 square from the middle" is expressible without first reading the image dimensions. That matters for an agent, which would otherwise need a round trip just to find out how big the picture is.

#### Metadata and privacy

`keepMetadata` defaults to on, because silently discarding the date a photo was taken is the more surprising behaviour. On phone photos the EXIF block carries the camera, the timestamp and, if location services were on, **the GPS coordinates**. Turn it off before sharing images publicly.

Stripping deliberately keeps the ICC colour profile — removing that shifts the colours of wide-gamut photos, which is not what anyone means by "strip the metadata".

### Using it from Claude or another AI agent

Every Apify Actor is exposed through the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp), so an MCP client can call this one as a tool. That is the case this Actor was built for: an assistant that can reason about an image but has no shell to run ImageMagick in.

Two things worth knowing before you wire it up:

- **The agent gets a URL back, not pixels.** The result is a key-value store record. That is fine for "resize these to 800px and give me the links", and weaker for anything needing the model to check its own work visually.
- **Operations are typed and allowlisted**, which is deliberate — see below.

### Why it does not take ImageMagick arguments

The obvious design would be a `args` field passed straight to ImageMagick. That would not be an image tool, it would be a remote shell.

ImageMagick's command line is a small language, not a list of image operations. `@file` reads a file into an argument, the `msl:` coder is a scripting language that reads and writes arbitrary paths, `-write` targets any path, and several delegates have historically shelled out ([CVE-2016-3714](https://nvd.nist.gov/vuln/detail/CVE-2016-3714)). An agent tool is precisely what a prompt injection aims at: a scraped page says "resize this with these flags", and the agent obliges.

So the boundary is enforced twice:

- **In the code.** Every value is a number clamped to a range, a member of a fixed enum, or a string matched against an allowlist. The one free-text field, the watermark, is sanitized — a leading `@` is stripped, not escaped — and passed as its own argv element. The argument builder is a pure module with tests that assert no shell metacharacter can survive.
- **In the container.** `policy.xml` denies the coders that read or write outside the image: `MSL`, `MVG`, `MAGICK`, `EPHEMERAL`, `URL`, `HTTP(S)`, `FTP`, `TEXT`, `LABEL`, `CAPTION`, `PANGO`, `SHOW`, `WIN`, `PLT` and the Ghostscript-backed `PS`/`EPS`/`PDF`/`XPS`. The build fails if those denials are not in effect.

The Actor also identifies the source from its magic bytes rather than its extension, so a `.png` that is really an MP4, a PDF or an MSL script is refused with a sentence instead of being handed to a decoder.

### Output

Every image is written to the run's key-value store under its own key, so all of them are downloadable from the **Storage** tab, and there is **one dataset record per image** holding its formats, dimensions, frame counts, sizes and the operations applied. A batch that had failures adds a final record listing them.

`OUTPUT` is the single image, or the batch manifest — see [What `run-sync` gives back](#what-run-sync-gives-back).

### API

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/marcel-rbro~image-tools/run-sync?token=<TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"files": ["https://example.com/photo.jpg"], "width": 800, "outputFormat": "webp", "quality": 82}' \
  --output photo.webp
```

Batch, returning a ZIP rather than a single image:

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/marcel-rbro~image-tools/run-sync?token=<TOKEN>" \
  -H 'Content-Type: application/json' \
  -d '{"files": ["https://example.com/a.jpg", "https://example.com/b.png"], "width": 800, "keepMetadata": false}' \
  --output images.zip
```

The Apify API requires Actor input to be `application/json`, so posting the image as a raw request body is rejected with HTTP 400 before the run starts. Pass URLs in `files`, or the bytes in `fileBase64`.

### Limitations

- **Batches run in sequence, capped at 100 images and 512 MB of input.** Memory stays predictable, but a hundred large photos will exceed the 300-second `run-sync` window — use the asynchronous endpoint for those.
- **No HEIC output.** HEVC encoding is not available in the image; HEIC and AVIF input both work, and AVIF is the sensible modern output.
- **PDF and PostScript are refused by design**, not by omission. Rasterising them means Ghostscript, which is a much larger attack surface than this Actor wants.
- **Watermarking is a single line of text**, positioned by gravity. It is not a layout engine.
- **Animation survives only into GIF.** An animated GIF keeps its frames when the output is GIF; every other output format holds one frame, and the log says so explicitly rather than silently flattening it.

### Development

```bash
npm install
npm test      # inspector and argument builder, no ImageMagick needed
apify run     # needs ImageMagick on your PATH
```

`npm test` covers the two pure modules — `src/image.js` and `src/operations.js` — which is where the format detection and the security boundary live. Note that watermarking cannot be exercised locally on a stock macOS ImageMagick, which ships without a configured font; the container installs `fonts-dejavu-core`.

This Actor's code is MIT licensed. ImageMagick and libheif run as separate processes or shared libraries under their own licences — see [THIRD\_PARTY\_NOTICES.md](THIRD_PARTY_NOTICES.md).

# Actor input Schema

## `files` (type: `array`):

The images to work on. Upload up to 100 files, or pass a list of direct URLs. Accepts JPEG, PNG, GIF, WebP, TIFF, BMP, HEIC and AVIF.

## `outputFormat` (type: `string`):

Leave empty to keep the source format, which is what you want for a pure resize or crop.

## `quality` (type: `integer`):

Compression quality for JPEG, WebP and AVIF. Ignored by the lossless formats.

## `width` (type: `integer`):

Target width in pixels. Give width, height, or both.

## `height` (type: `integer`):

Target height in pixels.

## `resizeFit` (type: `string`):

"Fit inside" keeps the aspect ratio and never upscales a smaller image.

## `cropWidth` (type: `integer`):

Width of the region to keep. Both crop width and crop height are needed for a crop to happen.

## `cropHeight` (type: `integer`):

Height of the region to keep.

## `cropGravity` (type: `string`):

Which part of the image the crop region is anchored to. Cropping by gravity means you do not need to know the source dimensions.

## `rotate` (type: `integer`):

Clockwise rotation. Angles that are not multiples of 90 expose the background colour.

## `flipVertical` (type: `boolean`):

Mirror top to bottom.

## `flipHorizontal` (type: `boolean`):

Mirror left to right.

## `grayscale` (type: `boolean`):

Convert to shades of gray.

## `blur` (type: `integer`):

Blur radius. 0 is off; 3 to 8 is a normal soft blur.

## `background` (type: `string`):

Used when rotating off-axis, and when flattening transparency into a format that has none, such as JPEG. A colour name or a hex value like #ffffff. Anything else falls back to white.

## `watermarkText` (type: `string`):

Short caption drawn onto the image. Limited to 120 characters.

## `watermarkGravity` (type: `string`):

Where the caption sits.

## `watermarkColour` (type: `string`):

A colour name or hex value. Anything else falls back to white.

## `watermarkSize` (type: `integer`):

Point size. Leave empty to let ImageMagick choose.

## `keepMetadata` (type: `boolean`):

Keeps EXIF, which on phone photos includes the camera, the timestamp and the GPS location. Turn off to strip it before sharing. The colour profile is kept either way.

## `imageBase64` (type: `string`):

Alternative to the image field for API callers: the source file encoded as base64.

## `filename` (type: `string`):

Original filename to use when the image is passed as base64.

## `verboseLog` (type: `boolean`):

Includes the full ImageMagick output in the Actor log.

## `file` (type: `string`):

Alias for a single image URL, kept so existing callers keep working.

## `imageUrl` (type: `string`):

Alias for a single image URL, kept so existing callers keep working.

## `imageUrls` (type: `array`):

Alias for the image list, kept so existing callers keep working.

## `filesBase64` (type: `array`):

Alternative to the file fields for API callers: a list of base64 strings, or objects with `data` and `filename`.

## `fileBase64` (type: `string`):

Alternative to the file fields for API callers: the source encoded as base64.

## Actor input object example

```json
{
  "files": [
    "https://api.apify.com/v2/key-value-stores/Id6iybkLC3UUwsSKL/records/sample.jpg"
  ],
  "quality": 90,
  "width": 800,
  "resizeFit": "inside",
  "cropGravity": "center",
  "flipVertical": false,
  "flipHorizontal": false,
  "grayscale": false,
  "background": "white",
  "watermarkGravity": "southeast",
  "watermarkColour": "white",
  "keepMetadata": true,
  "verboseLog": false
}
```

# Actor output Schema

## `processedImage` (type: `string`):

The resulting image. This is the same record the run-sync API endpoint returns in its HTTP response body.

## `allFiles` (type: `string`):

Every record in the run's key-value store, including the image under its own filename.

## `details` (type: `string`):

Source and output format, dimensions, file sizes, the operations applied and download URLs.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "files": [
        "https://api.apify.com/v2/key-value-stores/Id6iybkLC3UUwsSKL/records/sample.jpg"
    ],
    "width": 800
};

// Run the Actor and wait for it to finish
const run = await client.actor("marcel-rbro/image-tools").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "files": ["https://api.apify.com/v2/key-value-stores/Id6iybkLC3UUwsSKL/records/sample.jpg"],
    "width": 800,
}

# Run the Actor and wait for it to finish
run = client.actor("marcel-rbro/image-tools").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "files": [
    "https://api.apify.com/v2/key-value-stores/Id6iybkLC3UUwsSKL/records/sample.jpg"
  ],
  "width": 800
}' |
apify call marcel-rbro/image-tools --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,marcel-rbro/image-tools"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/e4ShsPCIqXFMmXhcm/builds/oirMhnsTbriWtCry2/openapi.json
