# Bulk Image Converter & Compressor (WebP, AVIF, JPG, PNG) (`divinxxii/image-converter`) Actor

Convert and compress images in bulk to WebP, AVIF, JPEG or PNG: resize, strip EXIF/GPS metadata, get a per-image savings report and a download link for every file.

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

## Pricing

from $1.40 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Bulk Image Converter & Compressor (WebP, AVIF, JPG, PNG)

Convert and compress images in bulk. Give it a list of image URLs and/or files you uploaded to an Apify key-value store, pick **WebP, AVIF, JPEG or PNG**, set the quality and a maximum width/height, and get back a smaller file for every image plus a **savings report**: original and new format, size and dimensions, and the percentage of bytes saved.

- **Four output formats, including AVIF.** WebP and AVIF for the web, JPEG for everything else, lossless PNG when you need transparency. Inputs can be JPEG, PNG, WebP, GIF, BMP, TIFF or AVIF.
- **Metadata stripping, on by default.** Removes EXIF (camera model, timestamps, **GPS location**), XMP and comments. Embedded colour profiles are first baked into sRGB so the picture looks the same. Camera rotation is applied to the pixels before EXIF is dropped, so photos never end up sideways. Switch stripping off to keep EXIF, ICC profile and XMP.
- **Resize while you convert.** Maximum width and/or height, aspect ratio kept, never enlarged.
- **A report you can use.** One dataset row per image with sizes, dimensions, bytes saved % and a download link to the converted file.
- **No external services.** The images are processed inside the Actor with [Pillow](https://python-pillow.org/). Nothing is uploaded to any third-party API.
- **Polite fetching.** Honours `robots.txt`, refuses private/internal addresses, one request per second per host, 25 MB size cap, image content types only.

### Input

| Field | Description |
|---|---|
| `imageUrls` | Public `http(s)` URLs of images (up to 25 MB each). |
| `kvsKeys` | Keys of image files you have uploaded to a key-value store. Read directly, no download. |
| `sourceStore` | Name or ID of the store that holds those keys. Empty = this run's own default store. |
| `outputFormat` | `webp` (default), `avif`, `jpeg` or `png`. |
| `quality` | 1-100, default 80. Applies to WebP, AVIF and JPEG; PNG is always lossless. |
| `maxWidth` / `maxHeight` | Shrink images larger than this (px, `0` = no limit). Aspect ratio kept, never enlarged. |
| `stripMetadata` | Default **on**. Removes EXIF/GPS, XMP, comments and colour profiles. |
| `filenamePrefix` | Optional text put in front of each output file name, e.g. `web-`. |
| `outputStore` | Optional name or ID of a store for the converted files, instead of the run's default store. |

**Run it with no input** and it converts three small generated sample images (a photo carrying EXIF, a transparent gradient logo and a banner) so you can see the report, the savings and the file links in under a minute.

#### Example input

```json
{
    "imageUrls": [
        "https://www.python.org/static/img/python-logo.png",
        "https://example.com/images/product-photo.jpg"
    ],
    "outputFormat": "avif",
    "quality": 60,
    "maxWidth": 1600,
    "stripMetadata": true,
    "filenamePrefix": "web-"
}
```

To convert files you uploaded yourself, put them in a key-value store (Console → Storage → Key-value stores → create or open one → upload), then:

```json
{
    "sourceStore": "my-uploads",
    "kvsKeys": ["holiday-1.jpg", "holiday-2.jpg"],
    "outputFormat": "webp",
    "outputStore": "my-uploads"
}
```

(Converted files are named after the source, e.g. `holiday-1.webp`. An output file never overwrites a source key listed in `kvsKeys`; a name clash gets a `-2` suffix.)

#### Example output (one dataset row; the report from the no-input sample run, WebP at quality 80)

```json
{
    "row": 1,
    "source": "kvs:sample-photo.jpg",
    "sourceType": "kvs",
    "status": "converted",
    "error": null,
    "originalFormat": "jpeg",
    "originalBytes": 322865,
    "originalWidth": 1600,
    "originalHeight": 1067,
    "newFormat": "webp",
    "newBytes": 87808,
    "newWidth": 1600,
    "newHeight": 1067,
    "bytesSavedPercent": 72.8,
    "metadataStripped": true,
    "outputKey": "sample-photo.webp",
    "outputUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/sample-photo.webp",
    "duplicateOfRow": null,
    "warnings": []
}
```

#### Fields

| Field | Meaning |
|---|---|
| `row` | 1-based position in the input (URLs first, then store keys). |
| `source`, `sourceType` | The URL (`url`) or `kvs:<key>` (`kvs`). |
| `status` | `converted`, `unchanged`, `failed` or `duplicate` (see below). |
| `error` | Why a `failed` row failed. The message never contains the URL's query string. |
| `originalFormat`, `originalBytes`, `originalWidth`, `originalHeight` | The source. Dimensions are **as displayed**, i.e. after applying camera rotation. |
| `newFormat`, `newBytes`, `newWidth`, `newHeight` | The stored result. |
| `bytesSavedPercent` | `(original - new) / original`, one decimal. **Negative if the file got bigger** (for example a JPEG photo converted to PNG). |
| `metadataStripped` | `true` if the source had metadata and it was removed; `false` when there was nothing to remove or stripping is off. |
| `outputKey`, `outputUrl` | Key of the converted file in the output key-value store and its Apify link. |
| `duplicateOfRow` | On `duplicate` rows, the first row with the same source. |
| `warnings` | Notes about this image: flattened transparency, animated input reduced to its first frame, and so on. |
| `recoveredAfterRestart` | Only present (`true`) on a row delivered again after a restart; it was not charged a second time. |

**Statuses.** `converted`: new file saved (charged). `unchanged`: the target format equals the source format, nothing was resized, and re-encoding would not have been smaller, so your original bytes are saved under the new key as they are (free). `failed`: unreadable, not an image, too big, blocked by robots.txt, HTTP error, and so on (free). `duplicate`: the same URL or store key listed twice (free; the first one is converted).

The run also saves a `SUMMARY` record (counts and total savings) to the default key-value store, and shows the same totals as the final status message.

### Pricing (pay per event)

| Event | Price | When it is charged |
|---|---|---|
| Actor start | $0.005 | Once per run |
| Image converted | $0.002 | Per image converted and saved (failed images, duplicates and unchanged images are free) |

| Images | Approximate cost |
|---|---|
| 100 | $0.205 |
| 1,000 | $2.005 |
| 10,000 | $20.005 |

- **Actor start:** $0.005 per run (Apify's `apify-actor-start` event). Maximum run memory is capped at 1,024 MB so the start event is never multiplied.
- **Image converted:** $0.002 per image, charged **after** the converted file and its report row are saved.
- **Free:** failed images, duplicates, `unchanged` images, and rows redelivered after a restart.

The no-input sample run (three images) costs $0.011. If you set a maximum cost per run, the actor stops cleanly when it is reached and says so in the status message.

### What is stored, and where

- **Report rows** go to the run's default **dataset**.
- **Converted files** go to a **key-value store**: the run's default store, or the one named in `outputStore`. `outputUrl` is the Apify link to the file in that store. If the store requires it the link carries a signature, so anyone who has the link can download the file without your API token, and it stops working when the store is deleted. Do not share a report if the images are private.
- **Default (unnamed) storage expires** according to your Apify plan's data-retention period. Download the files in time, or set `outputStore` to a **named** store, which is kept until you delete it. Files already in the output store under the same key are overwritten.
- The run also keeps `PROGRESS` (counters only) and `SUMMARY` in its default key-value store.
- Your images are not sent to any service other than the Apify storage you chose. Source URLs appear in the dataset rows (they are your input); the Actor does not write URLs to its log.

### Fetching rules

For URL inputs the Actor:

- reads `robots.txt` per [RFC 9309](https://www.rfc-editor.org/rfc/rfc9309) with its own user agent (`ApifyActor-ImageConverter`); a disallowed URL is a `failed` row, and if `robots.txt` cannot be read (timeout, connection error, 5xx, 429) it **fails closed** and does not fetch the image. There is no option to turn this off;
- only fetches `http` and `https`, and refuses any host that resolves to a private, loopback, link-local or otherwise non-public address, **on every redirect hop** (up to 5);
- waits at least one second between requests to the same host (the `robots.txt` request counts), and fetches different hosts independently;
- Credentials in URLs (`user:pass@host`) are not sent: the actor never makes authenticated requests, so use a URL that needs no login.
- accepts only `image/*` content types (SVG is refused), stops reading as soon as the body passes **25 MB**, and gives up on one image after 90 seconds.

Speed is therefore about one image per second per host plus conversion time. 500 images from one site take about 10 minutes.

### Limitations

- **Supported inputs:** JPEG, PNG, WebP, GIF, BMP, TIFF, AVIF. Not supported: SVG, HEIC/HEIF, RAW, PDF, ICO.
- **Size limits:** 25 MB per source file; up to **40 megapixels** per image (a decompression-bomb guard; checked from the file header before any pixels are decoded); **AVIF output is limited to 24 megapixels** (it needs more memory), so set `maxWidth`/`maxHeight` for very large images. WebP is limited to 16,383 px per side and AVIF/JPEG have their own format limits; such images come back as `failed` with the reason.
- **Animated GIF/WebP and multi-page TIFF:** only the first frame is converted (a warning says so).
- **Transparency** is kept for WebP, AVIF and PNG, and flattened onto white for JPEG.
- **PNG output is lossless**; `quality` does not apply. Photos usually get larger as PNG.
- **Quality numbers are per encoder**, not comparable across formats. Judge on your own images.
- **Metadata:** with `stripMetadata` off, EXIF, ICC profile and XMP are carried over where the output format supports them. PNG text chunks, IPTC and other private blocks are not carried over. When stripping, an image whose embedded profile cannot be read keeps its stored colours (a warning says so).
- **Charged per conversion, not per byte saved.** Converting to a format that makes the file bigger is still a conversion you asked for and is charged.
- Memory: the Actor runs at 1 GB. Very large lossless PNG or slow WebP encodes of 30+ megapixel images can take 20 seconds or more each.
- Restarts: progress is kept in the key-value store (`PROGRESS`), so a restart or migration does not re-charge finished images. An image that was in flight at the interruption is converted again and delivered uncharged with `recoveredAfterRestart: true`; a rare duplicate row carries that flag.

### Licences of what runs in the Actor

- [Pillow](https://python-pillow.org/) (MIT-CMU / HPND licence), including its AVIF support, with the codec libraries bundled in the Linux wheel: libjpeg-turbo (IJG / BSD-3), libwebp (BSD-3), libavif (BSD-2), Little CMS (MIT), libtiff, OpenJPEG (BSD-2), zlib, HarfBuzz (MIT) and FreeType (used under the FreeType Licence, a BSD-style licence, not its GPL option). The Linux wheel the Actor installs (checked for Pillow 13.0.0) ships none of the GPL/LGPL components that appear in Pillow's build list for other platforms (libimagequant, FriBiDi).
- [httpx](https://www.python-httpx.org/) (BSD-3-Clause) and the Apify SDK for Python (Apache-2.0).
- [certifi](https://github.com/certifi/python-certifi) (MPL-2.0), the CA certificate bundle that httpx pulls in. It is used unmodified.
- No GPL/AGPL code, and no external API or paid service.

### Responsible use

You are responsible for having the right to copy and re-encode the images you submit. Respect the terms of the sites you fetch from; the Actor honours `robots.txt` but cannot know a site's licence terms.

### FAQ

**How do I convert images to WebP in bulk?**
Paste the image URLs into `imageUrls` (or list uploaded files in `kvsKeys`), leave `outputFormat` on `webp` and run. Every converted file is saved to the key-value store and its link is in the `outputUrl` column of the dataset.

**Can it convert to AVIF?**
Yes. Set `outputFormat` to `avif`. AVIF output is limited to 24 megapixels per image; use `maxWidth` / `maxHeight` for larger sources.

**Does it remove EXIF and GPS data?**
Yes, by default (`stripMetadata`). EXIF, XMP and comments are removed, colours are converted to sRGB first so the picture looks the same, and camera rotation is applied to the pixels. Turn the option off to keep EXIF, ICC profile and XMP.

**How do I compress images without changing the format?**
Set `outputFormat` to the source format (for example `jpeg` for JPEG photos), lower `quality` and optionally set `maxWidth`. If re-encoding cannot make a file smaller the original is kept and the row is marked `unchanged` (free).

**Are my images sent to another service?**
No. Conversion runs inside the Actor with Pillow. Images fetched from URLs are downloaded to the run; nothing is uploaded anywhere except the Apify storage you chose.

### Related Actors

- [Domain Health Checker](https://apify.com/divinxxii/domain-health-audit): SPF, DMARC, DKIM and SSL expiry checks for any domain.
- [PDF to Markdown for RAG](https://apify.com/divinxxii/doc-to-markdown): turn PDF, DOCX, PPTX and HTML into Markdown for LLMs.
- [Email Validator & List Cleaner](https://apify.com/divinxxii/email-list-cleaner): syntax, MX and disposable-domain checks for email lists.

# Actor input Schema

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

Public http(s) URLs of images (JPEG, PNG, WebP, GIF, BMP, TIFF or AVIF, up to 25 MB each). robots.txt is honoured and requests to one host are spaced 1 s apart. Leave empty (with no store keys) to run a small built-in sample.

## `kvsKeys` (type: `array`):

Keys of image files you uploaded to a key-value store (see 'Source key-value store'). Each key is read directly, no download involved.

## `sourceStore` (type: `string`):

Store that holds the files named in 'Key-value store keys'. Name or 17-character ID of one of your stores; empty means this run's own default store.

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

Format of the converted files. PNG is lossless, so 'Quality' does not apply to it.

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

Lossy quality for WebP, AVIF and JPEG. 75-85 is a good balance; ignored for PNG.

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

Shrink images wider than this, keeping the aspect ratio. 0 means no limit. Images are never enlarged.

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

Shrink images taller than this, keeping the aspect ratio. 0 means no limit. Images are never enlarged.

## `stripMetadata` (type: `boolean`):

Remove EXIF (camera, GPS location), XMP, comments and embedded colour profiles. Colours are first converted to sRGB so the picture looks the same. Turn off to keep EXIF, ICC profile and XMP.

## `filenamePrefix` (type: `string`):

Optional text put in front of every output file name, e.g. 'web-'. Output files are named after the source file with the new extension.

## `outputStore` (type: `string`):

Optional. Save the converted files to this store instead of the run's default store, e.g. a named store that is kept beyond the default retention period. Existing files with the same key are overwritten.

## Actor input object example

```json
{
  "outputFormat": "webp",
  "quality": 80,
  "maxWidth": 0,
  "maxHeight": 0,
  "stripMetadata": true
}
```

# Actor output Schema

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

No description

## `files` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("divinxxii/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 = {}

# Run the Actor and wait for it to finish
run = client.actor("divinxxii/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 '{}' |
apify call divinxxii/image-converter --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,divinxxii/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/4as8rbtP5uxANVaWz/builds/ifwJ2N9vdcsnbJZU4/openapi.json
