# Image Converter & Compressor: WebP, AVIF, JPEG, PNG (`wrentools/bulk-image-converter`) Actor

Convert and compress images in bulk to WebP, AVIF, JPEG or PNG, with quality and resize settings. Keeps transparency and animation, turns photos upright, converts colour profiles to sRGB, strips EXIF and GPS. Damaged files are refused, not padded, and not charged.

- **URL**: https://apify.com/wrentools/bulk-image-converter.md
- **Developed by:** [WrenTools](https://apify.com/wrentools) (community)
- **Categories:** Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 image converteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Image Converter & Compressor: WebP, AVIF, JPEG, PNG

Convert a batch of images to **WebP, AVIF, JPEG or PNG** in one run, smaller and ready for the web. Give it direct
links to your images, or send them as base64. Each converted image is stored in the run's key-value store with a
download link, and the dataset tells you its size before and after, its dimensions and anything the conversion
changed.

### What you get

- **Four output formats.** WebP, AVIF, JPEG and PNG, from JPEG, PNG, WebP, AVIF, GIF, TIFF, BMP and other common
  formats.
- **Quality and size settings.** Set `quality` (1-100) or keep each format's default (WebP 80, JPEG 85, AVIF 60).
  Set `maxWidth` and/or `maxHeight` to shrink large images to fit, keeping their proportions. Small images are never
  enlarged.
- **Photos come out upright.** A phone photo stored sideways with an EXIF orientation tag is turned upright before
  the tag is removed, so it does not come out rotated.
- **Colours stay the same.** An image with a wide-gamut colour profile (Display P3, for example) is converted to sRGB,
  so it looks the same once the profile is gone.
- **Transparency is kept** in WebP, AVIF and PNG, including transparent palette PNGs. JPEG has no transparency, so
  transparent areas are filled with the `background` colour you choose (white by default), never black.
- **Animations stay animated.** An animated GIF, PNG, WebP or AVIF converted to WebP, AVIF or PNG keeps every frame and
  its timing, and plays once or loops as the original did. JPEG holds one frame, so you get the first, and the item
  says so.
- **EXIF, GPS and XMP are removed.** The camera, the location and other metadata are not written to the output.
- **16-bit images come out right**, scaled to 8 bits rather than clipped to a blank white or black picture.
- **Damaged files are refused, not padded.** An image that is cut short (a failed download, a half-copied file) is
  reported as `damaged-image` and not charged. It is never written out with invented rows or grey bars.
- **Every output is checked.** Each converted file is read back before it is stored: it must be the format you asked
  for, at the expected size, with every frame. A file that fails is reported and not charged.
- **An image too large for its format is refused with the reason.** WebP allows at most 16,383 pixels a side, so a
  long screenshot is refused with the formats that can take it, or you can set `maxHeight`.
- **One bad image does not sink the run.** A link that fails, a file that is not an image, or a damaged file is
  reported in its place, and the rest are converted.

Each of these is a test in the Actor's suite, run on generated images before every release.

### Respectful by design

- **Only the URLs you list are downloaded**, one request each. Pages are not crawled and links are not followed. The
  site's robots.txt is read first and obeyed, and so is the robots.txt of any site a link redirects to: a link that
  redirects somewhere its robots.txt refuses is not requested there and is reported as `robots-disallowed-redirect`.
  Downloads from one site are spaced (500 ms by default). No proxy, no browser, no login.
- Only public internet addresses are fetched. A URL or redirect into a private, loopback or link-local network is
  refused.
- No AI and no third-party service: the conversion runs inside the Actor with Pillow and its codecs.

Convert only images you have the right to copy.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `fileUrls` | array | - | Direct links to images. Duplicates are dropped. |
| `filesBase64` | array | - | Images as base64 strings, or `{"name": "photo.jpg", "data": "<base64>"}`. A `data:` URL prefix is accepted. |
| `outputFormat` | string | `webp` | `webp`, `avif`, `jpeg` or `png`. |
| `quality` | integer | per format | 1-100. WebP 80, JPEG 85, AVIF 60 when empty. Not used for PNG (lossless). |
| `maxWidth`, `maxHeight` | integer | - | Shrink to fit; never enlarge. |
| `background` | string | `#ffffff` | What transparency becomes in a JPEG. |
| `keepAnimation` | boolean | `true` | Off writes the first frame only. |
| `maxFileSizeMb` | integer | `50` | Up to 200. Larger images are skipped and not charged. |
| `requestDelayMs` | integer | `500` | Pause between downloads from one site. |

Give `fileUrls`, `filesBase64` or both.

### Output

One dataset item per image, in input order:

```json
{"source": "https://example.com/photos/beach.jpg", "url": "https://example.com/photos/beach.jpg",
 "finalUrl": "https://example.com/photos/beach.jpg", "status": "ok",
 "key": "0001-beach.webp", "fileName": "beach.webp",
 "downloadUrl": "https://api.apify.com/v2/key-value-stores/<store id>/records/0001-beach.webp",
 "inputFormat": "JPEG", "inputBytes": 2480133, "inputWidth": 3000, "inputHeight": 4000, "inputFrames": 1,
 "outputFormat": "WEBP", "outputBytes": 312904, "width": 1200, "height": 1600, "frames": 1, "savedPercent": 87.4,
 "notes": ["turned upright from its EXIF orientation", "resized from 3000x4000 to 1200x1600"], "error": null}
```

A refused image:

```json
{"source": "https://example.com/photos/broken.jpg", "status": "error", "key": null, "inputBytes": 1048576,
 "error": {"code": "damaged-image", "message": "the image is damaged or cut short (image file is truncated); not converted, not charged. ..."}}
```

Error codes: `damaged-image`, `not-an-image`, `too-large-for-format`, `too-many-pixels`, `output-check-failed`,
`file-too-large`, `fetch-failed`, `robots-disallowed`, `robots-disallowed-redirect`, `invalid-url`, `invalid-base64`,
`conversion-timeout` (the run's timeout came while it was converting).
A file whose whole reply takes longer than its time limit is a `fetch-failed` row whose message starts
`deadline-exceeded`, and the run goes on to the next. The limit is the largest file `maxFileSizeMb`
allows at 333 kB/s, between 60 s and 600 s (150 s at the default 50 MB). So a server that sends a file
very slowly cannot hold the run.

Key-value store record `SUMMARY`: images requested, converted and failed, bytes before and after, the saving, the
megabytes charged, and why the run stopped early if it did.

Files in a run's default storage are kept for as long as your Apify plan keeps unnamed storage (7 days on the free
plan). Copy anything you need to keep longer.

### Pricing

Pay per event, no start fee, and nothing for images that fail or are refused:

| Event | Price | Per 1,000 |
|---|---|---|
| `image-converted` (one image converted, checked and stored) | $0.0008 | $0.80 |
| `megabyte-processed` (each started megabyte of the larger of the image's input and output, at least one) | $0.0004 | $0.40 |

So a 300 KB web image costs $0.0012, a 1.5 MB image $0.0016, and a 5.7 MB 12-megapixel photo $0.0032. Converting a
photo to PNG usually makes it several times larger, and the larger size is what counts. When you set a maximum charge
for a run, the Actor stops before an image the remainder cannot pay for.

### Limits

- Images only, one URL each. It does not crawl a page for its images.
- **Pixels per image depend on the run's memory:** about 27 megapixels at the default 1,024 MB, 60 at 2,048 MB and 127
  at 4,096 MB (an animation counts every frame). A larger image is refused as `too-many-pixels` and not charged.
  These come from the peak memory we measured converting 12- and 50-megapixel photos to each format.
- Speed: converting one 12-megapixel photo on our own machine (one full CPU core) took about 0.2 s to JPEG, 1.5 s to
  AVIF, 1.8 s to PNG and 2.9 s to WebP. On Apify at the default 1,024 MB, a whole run converting one such photo took
  3.8 s to WebP and 2.9 s to AVIF, start-up included (measured 2026-11-02).
- Colour profiles are converted to sRGB; the output carries no profile. 16-bit images come out as 8-bit.
- A multi-page TIFF gives its first page.
- 200 MB per input file at most.
- A run stops early, before its timeout, so its results are saved: 60 s before it (a fifth of the time for a run
  shorter than 5 minutes) it starts no new image, and a download still running then is cut and reported as
  `fetch-failed` (`deadline-exceeded`), not charged. An image still converting 10 s before the timeout is cut (half
  the margin, for a run shorter than 5 minutes): it is reported as `conversion-timeout`, not stored and not charged.
  Every image converted by then is stored and charged, images not started are not in the dataset and not charged, and
  `SUMMARY.stoppedReason` says why the run stopped.

# Actor input Schema

## `fileUrls` (type: `array`):

Direct links to the images to convert, which you are allowed to copy (JPEG, PNG, WebP, AVIF, GIF, TIFF, BMP and other common formats). Each URL is downloaded once (duplicates are dropped); the site's robots.txt is respected. Pages are not crawled.

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

Images sent inline, for images that are not online: a list of base64 strings, or of objects {"name": "photo.jpg", "data": "<base64>"}. A data: URL prefix is accepted.

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

The format every image is converted to. WebP, AVIF and PNG keep transparency and animation; JPEG has neither, so transparency is flattened onto the background colour and an animation gives its first frame.

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

Lower gives smaller files. Empty uses the format's default: WebP 80, JPEG 85, AVIF 60. PNG is lossless, so this does not apply to it.

## `maxWidth` (type: `integer`):

Images wider than this are shrunk to fit, keeping their proportions. Smaller images are never enlarged. Empty keeps the width.

## `maxHeight` (type: `integer`):

Images taller than this are shrunk to fit, keeping their proportions. Smaller images are never enlarged. Empty keeps the height.

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

JPEG has no transparency, so transparent areas are filled with this colour (#rrggbb). Other formats keep the transparency.

## `keepAnimation` (type: `boolean`):

Animated GIF, PNG, WebP and AVIF images stay animated, with their frame times, when converted to WebP, PNG or AVIF. Off writes the first frame only.

## `maxFileSizeMb` (type: `integer`):

Larger images are skipped and not charged.

## `requestDelayMs` (type: `integer`):

Downloads from the same site are spaced by at least this long, to be gentle on small servers.

## Actor input object example

```json
{
  "fileUrls": [
    "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-photo-turned.jpg",
    "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-logo-transparent.png",
    "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-spinner.gif"
  ],
  "outputFormat": "webp",
  "background": "#ffffff",
  "keepAnimation": true,
  "maxFileSizeMb": 50,
  "requestDelayMs": 500
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `files` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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 = {
    "fileUrls": [
        "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-photo-turned.jpg",
        "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-logo-transparent.png",
        "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-spinner.gif"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("wrentools/bulk-image-converter").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 = { "fileUrls": [
        "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-photo-turned.jpg",
        "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-logo-transparent.png",
        "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-spinner.gif",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("wrentools/bulk-image-converter").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 '{
  "fileUrls": [
    "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-photo-turned.jpg",
    "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-logo-transparent.png",
    "https://api.apify.com/v2/key-value-stores/uM2zEXblvgf0s1cES/records/converter-spinner.gif"
  ]
}' |
apify call wrentools/bulk-image-converter --silent --output-dataset

```

## MCP server setup

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

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/MK7cGKtArPnrm8nmI/builds/OafSsgu0aeGvELGEP/openapi.json
