# Image Converter & Compressor - WebP, AVIF, strip EXIF/GPS (`quietworkslab/image-converter`) Actor

Convert and compress images to WebP, AVIF, JPEG or PNG, resize them, and remove EXIF, GPS location, XMP and IPTC metadata. Reads images from your key-value store or uploaded files, applies the EXIF rotation first, and reports which metadata each image had.

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

## Pricing

from $2.10 / 1,000 image processeds

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

## Image Converter & Compressor - WebP, AVIF, strip EXIF/GPS

Convert and compress your images to **WebP, AVIF, JPEG or PNG**, resize them, and **remove EXIF, GPS location, XMP, IPTC and comments**. The Actor reads images from **direct file links**, from a key-value store in your Apify account or from files you upload, and tells you for each image which kinds of metadata it had.

Typical uses:

- Make product photos, blog images and screenshots smaller for the web (WebP or AVIF, optional maximum width and height).
- Remove the GPS location, camera details and author data from photos before you publish or share them.
- Convert the images another Actor saved in its key-value store, right after it finishes.
- Normalize a batch of images: rotate them by their EXIF orientation, convert the colors to sRGB, limit their size.
- Convert a list of image links, for example your own product images, without uploading them first.

Only use images you have the right to use.

### Try it in one click

Click **Start** without choosing a store, uploading files or adding links. The Actor converts three built-in sample images that were drawn by a script (no `image-processed` events for the samples; Apify's Actor start event still applies):

- a landscape "photo" with **fake** GPS coordinates, an EXIF orientation tag, XMP, IPTC and a comment,
- a PNG screenshot with a text comment,
- an animated GIF with 8 frames.

You get three WebP files in the run's key-value store, one dataset record per image, and a summary: 570,182 bytes become 259,000 bytes (54.6% smaller), and the record of the photo shows `"hadGps": true` while the converted file has no metadata left.

### How it works

1. **Images in**: choose a **key-value store** (optionally only the keys that start with a prefix), **upload files** in the input form, and/or give **direct links** to image files (one GET per link, see [Image links](#image-links)). They are read in this order, one at a time. Records that are not images (JSON, text) are skipped. The format is detected from the file's content, not from its name.
2. **Rotate first**: the EXIF orientation is applied to the pixels before anything is removed, so portrait photos stay upright.
3. **Resize** (optional): shrink to a maximum width and/or height, keeping the aspect ratio. Images are never enlarged.
4. **Convert and compress** to the output format with the quality you choose.
5. **Remove metadata**: EXIF (including the GPS location), XMP, IPTC and comments are dropped. The color profile is converted to sRGB (default) or kept.
6. **Check**: every converted file is read again, and if any EXIF, GPS, XMP, IPTC or comment were still there, the file is not saved and the image is reported as failed.
7. **Save**: the converted image goes to the run's default key-value store, a record with the link goes to the dataset, and a summary goes to the `OUTPUT` record.

### Input

| Field | What it does |
|---|---|
| `keyValueStoreId` | Key-value store in your account that holds the images. Empty (with no uploads and no links) = built-in samples. |
| `keyPrefix` | Only convert records whose key starts with this text, for example `product-`. The value is saved encrypted, so Apify Console hides it after saving; `OUTPUT` shows the prefix the run used. |
| `uploadedFiles` | Files uploaded in the input form. This field takes only files stored in your Apify account; links to other websites go into `imageUrls`. See [Uploaded files](#uploaded-files). |
| `imageUrls` | Direct links to image files (not web pages), up to 1,000. Only use images you have the right to use. See [Image links](#image-links). |
| `outputFormat` | `webp` (default), `avif`, `jpeg`, `png`, or `original` (same format as the input). |
| `quality` | 1-100, default 80. For WebP, AVIF and JPEG; for PNG only with `pngReduceColors`. |
| `maxWidth`, `maxHeight` | Optional maximum size in pixels. Never enlarges. |
| `colorProfile` | `srgb` (default): convert colors to sRGB and write no profile. `keep`: keep the image's own ICC profile. An ICC profile can contain text, such as its description and copyright, and that text stays in the file with `keep`. |
| `keepMetadata` | Default `false` = remove EXIF, GPS, XMP, IPTC and comments. `true` = keep EXIF (with GPS), XMP and IPTC where the output format supports them. |
| `pngReduceColors` | For PNG output: reduce to at most 256 colors (lossy, much smaller for screenshots and graphics). Default `false`. |
| `maxImages` | Optional: stop after this many images, for example to test settings on 10 images first. |

Example input:

```json
{
    "keyValueStoreId": "YOUR_STORE_ID",
    "keyPrefix": "product-",
    "outputFormat": "webp",
    "quality": 80,
    "maxWidth": 1600,
    "colorProfile": "srgb",
    "keepMetadata": false
}
```

With links:

```json
{
    "imageUrls": [
        "https://example.com/photos/product-01.jpg",
        "https://example.com/photos/product-02.png"
    ],
    "outputFormat": "webp",
    "maxWidth": 1600
}
```

#### Image links

Only use images you have the right to use.

- The link must return the **file itself** (usually ending in `.jpg`, `.png`, `.webp`, `.gif`, `.avif` or `.tif`). A link to a page that shows an image is refused with `NOT_AN_IMAGE`, and the message asks for the direct link. The type is checked twice: the answer's `Content-Type` and the file's first bytes. A format that is not converted fails with `UNSUPPORTED_FORMAT`.
- Each link is read with **one GET request**, one link at a time, after the store and the uploads. Links inside a file are never followed and pages are never crawled. Up to 5 redirects are followed, and each target is checked again. A failed download is retried only after a server error (5xx) or a broken connection, at most twice. `429 Too Many Requests` is not retried. The same link given twice is read once.
- Besides `Host` and `Connection: close`, which HTTP needs, the only header sent is `User-Agent: image-converter (Apify Actor)`. The Actor does not log in, send cookies or imitate a browser: a link that needs a login fails with `AUTH_REQUIRED`, and so does a site that refuses automated requests with 401 or 403. Use a public link, or upload the file or put it in a key-value store instead.
- Links to private, local or reserved network addresses (for example `10.x.x.x`, `192.168.x.x`, `127.0.0.1`, `169.254.169.254`), also after a redirect, are refused with `BLOCKED_ADDRESS`.
- A link that carries a token in its query string (a signed link) works; the dataset and the log show links without their query string. The converted file is named after the last part of the link's path.
- A link that fails gives a failed record with the reason (and the HTTP status when the server answered), is not charged, and the run goes on. If links failed and no image could be converted, the run ends as failed with the reason of the first link, also when `maxImages` stopped it. Only the maximum cost, the timeout or an abort end such a run as `PARTIAL`.
- After **20 links in a row** have failed, or when failed links have used too much run time (see [Limits](#limits)), the Actor stops reading links: the rest of the list is not tried, and gets no records and no charges. A converted image starts the count of 20 again. The run ends `PARTIAL` if anything was converted and as failed otherwise, and the message says how many links were not tried. Check the list of links, then start a new run. Failures of store records and uploads do not count and are not stopped by this.
- A server that sends neither a `Content-Length` nor chunks ends the file by closing the connection, so a cut file can look complete. A GIF from such a server must reach the GIF end mark (what comes after it does not matter); otherwise it fails with `DOWNLOAD_FAILED` and is not charged. Other formats that are cut fail when they are decoded.
- A refused answer (an error, a web page, a file over the size limit) is closed at once, without reading the rest. See [Limits](#limits) for the time limits.

#### Uploaded files

Apify stores each upload in a new key-value store of your account. This Actor runs with limited permissions: it may always open a store you choose in **Key-value store**, but not necessarily the store of an upload. Accounts keep storages private by default (Settings > Login & Privacy > General resource access: Restricted), and then the first run cannot open the uploads. It fails with a message that names the store, for example:

> The run may not open 2 uploaded files: they are in your key-value store AbCdEf1234567890x, and Apify runs this Actor with limited permissions, so it may open only the store you choose. Choose AbCdEf1234567890x in "Key-value store" (pick it, or paste the ID) and start again; the files uploaded to it are then converted once. No image-processed events were charged for them.

Choose that store in **Key-value store** (keep the uploads in the form, or remove them) and start again. Every image in the chosen store is converted, and uploads that are in it are not converted a second time. Files uploaded at different times can be in different stores; a run reads one store.

### Output

Each dataset record is one image:

```json
{
    "status": "converted",
    "source": "key-value-store",
    "inputKey": "IMG_0001.jpg",
    "outputKey": "IMG_0001.webp",
    "outputUrl": "https://api.apify.com/v2/key-value-stores/RUN_STORE_ID/records/IMG_0001.webp",
    "inputFormat": "jpeg",
    "outputFormat": "webp",
    "inputBytes": 532066,
    "outputBytes": 249698,
    "savedPercent": 53.1,
    "inputWidth": 1200,
    "inputHeight": 1600,
    "outputWidth": 1200,
    "outputHeight": 1600,
    "frames": 1,
    "rotatedByExif": true,
    "hadExif": true,
    "hadGps": true,
    "hadXmp": true,
    "hadIptc": true,
    "hadIccProfile": true,
    "hadComments": true,
    "metadataRemoved": true,
    "chargedEvents": 1,
    "errorCode": null,
    "errorMessage": null
}
```

- `had...` fields say **whether** the original had that kind of metadata. The values themselves (for example the GPS coordinates) are never written to the dataset or the log.
- Widths and heights are as displayed, after the EXIF orientation. For animations they are the size of one frame, and `frames` is the number of frames.
- `source` is `sample`, `key-value-store`, `upload` or `url`. For a link, `inputKey` is the link without its query string, and `linkNumber` is its line in your `imageUrls` list, counting from 1, with blank lines and repeated links counted. Use it to tell apart links that differ only in their query string. Messages about the input use the same number. Records of other sources have no `linkNumber`.
- A failed image has `"status": "failed"`, an `errorCode` (`CORRUPT_IMAGE`, `UNSUPPORTED_FORMAT`, `NOT_AN_IMAGE`, `FILE_TOO_LARGE`, `TOO_MANY_PIXELS`, `TOO_MANY_FRAMES`, `READ_FAILED`, `SAVE_FAILED`, `METADATA_NOT_REMOVED`, `TIMEOUT`, `CONVERSION_FAILED`) and a message. Failed images are not charged, and the run goes on with the next image.
- Links can also fail with `AUTH_REQUIRED` (HTTP 401, 403 or 407), `NOT_FOUND` (404 or 410), `RATE_LIMITED` (429), `HTTP_ERROR` (another status), `TOO_MANY_REDIRECTS`, `BLOCKED_URL` (a redirect to another scheme or to an address with a login), `BLOCKED_ADDRESS`, `DNS_FAILED`, `DOWNLOAD_FAILED` (a broken connection or no answer, after the retries) and `SLOW_DOWNLOAD`.
- The converted files are in the run's default key-value store. The key is the original name (for a link, the last part of its path) with the new extension; characters that keys cannot contain become `-`, and a number is added if two images would get the same key (`photo.webp`, `photo-2.webp`).

The `OUTPUT` record of the run's key-value store holds the summary. `mode` says where the images came from (for example `key-value-store+url`), and `imageUrls` how many links the input had. The list of links is not stored there, but the reason of the first failed link can show that link without its query string (`resume.firstUrlError`, and `message` when every link failed).

```json
{
    "status": "COMPLETE",
    "mode": "sample",
    "message": "Converted 3 of 3 images (0 failed): 570,182 bytes -> 259,000 bytes (54.6% smaller). Location data (GPS) was found in 1 of them. ...",
    "counts": { "found": 3, "converted": 3, "failed": 0, "skippedNotImages": 0 },
    "bytes": { "input": 570182, "output": 259000, "savedPercent": 54.6 },
    "metadataFound": { "exif": 1, "gps": 1, "xmp": 1, "iptc": 1, "iccProfile": 1, "comments": 3 },
    "chargedEvents": 0
}
```

### Run it after another Actor

If another Actor saves images to its default key-value store (for example product images or screenshots), convert them automatically when it finishes:

1. Open that Actor (or its saved task) in Apify Console and go to its **Integrations** tab.
2. Add this Actor as an integration that runs when the run succeeds.
3. In the integration's input, set:

```json
{
    "keyValueStoreId": "{{resource.defaultKeyValueStoreId}}",
    "outputFormat": "webp",
    "maxWidth": 1600
}
```

`{{resource.defaultKeyValueStoreId}}` is replaced with the key-value store of the run that just finished. Its JSON records (such as `INPUT` and `OUTPUT`) are skipped automatically. Use `keyPrefix` if the images share a prefix.

Always set `keyValueStoreId` like this. The run information that Apify adds to the integration's input (`payload`) is not enough on its own: the Actor may only read a store chosen in `keyValueStoreId`. If `keyValueStoreId` is empty and no files or links are given, but `payload` names a store, the run stops at once with a message that tells you what to set (no `image-processed` events).

### Formats and special cases

- **Input**: JPEG, PNG, WebP, GIF, AVIF and TIFF.
- **Not supported**: HEIC/HEIF photos compressed with HEVC (the image library used here can read AVIF but not HEVC), SVG (vector), PDF, BMP, RAW camera files. They are reported as `UNSUPPORTED_FORMAT` and not charged. Export HEIC photos as JPEG first.
- **Animated GIF and WebP**: with WebP output (or `original`) all frames, delays and the loop count are kept. With JPEG, PNG or AVIF output, only the **first frame** is converted (these outputs are still images here). For animations, all frames together count for the size limits and the price.
- **Exception: an animation that is kept is not rotated.** The image library cannot rotate multi-frame images, so if an animated WebP carries an EXIF orientation tag, its frames stay as stored (the tag is removed with the other metadata) and `rotatedByExif` is `false`. Animated files with an orientation tag are rare; choose JPEG, PNG or AVIF output to get the rotated first frame.
- **Transparency**: kept in WebP, AVIF and PNG. JPEG has no transparency, so transparent areas become white.
- **CMYK JPEGs** are converted to sRGB.
- **Multi-page TIFF**: only the first page is converted.
- The original records are never changed or deleted.

### Pricing

This Actor uses pay-per-event pricing. The current prices are on the **Pricing** tab of this Actor.

| Event | Price | When it is charged |
|---|---|---|
| Actor start | $0.00005 per run, once more for each GB of memory beyond 1 GB | Once per run, set by Apify (it grows with the memory you give the run; the default is 1 GB). |
| Image processed (`image-processed`) | $0.003 per event (= $3 per 1,000 photos of up to 16 MP) | For each image that is converted and saved: **one event per started 16 megapixels** of the original image. **AVIF output and PNG with "Reduce PNG colors" count double** (one event per started 8 MP), because they take about twice as much computing time. |

Examples:

| Image | Output | Events |
|---|---|---|
| Product photo 1600 x 1200 (1.9 MP) | WebP, JPEG, PNG | 1 |
| Phone photo 4032 x 3024 (12.2 MP) | WebP, JPEG, PNG | 1 |
| Phone photo 4032 x 3024 (12.2 MP) | AVIF, or PNG with reduced colors | 2 |
| Camera photo 6000 x 4000 (24 MP) | WebP | 2 |
| 50 MP photo | WebP | 4 |
| Animated GIF, 100 frames of 480 x 270 (13 MP in total) | WebP | 1 |

- The size counted is the original's width x height, also when you shrink the image.
- Failed images and skipped records have no `image-processed` events, and neither do the built-in samples (the Actor start event, set by Apify, applies to every run). Each dataset record shows its `chargedEvents`, and the summary shows the total that was really charged.
- A link that fails is a failed image: no `image-processed` event. Downloading itself has no event, so a link costs the same as an image from your store. Links that are not tried, because too many links failed, have no record and no event.
- A run stopped by an input check (wrong store, a store given only in the integration's `payload`, a link in `uploadedFiles` that is not stored in Apify, a link in `imageUrls` that is not http or https or has a user name or password, too many links, no images found) ends before any image is charged. So does a run where links failed and no image could be converted: it ends as failed, with no `image-processed` events (or `PARTIAL`, if the maximum cost, the timeout or an abort stopped it first).
- **No double charges**: each image is charged right after its result and the progress are saved. If Apify moves the run to another server (or you resurrect a run that ran out of time or memory), it continues after the last saved image: images already handled are not charged again. In the worst case, the image that was being handled when the run stopped is not charged.
- **Abort and timeout**: the Actor stops 30 seconds before the run's timeout, and at once when you abort the run, and writes a `PARTIAL` summary. It does not start an image that would probably not finish in time. A download, or the reading or conversion of an image, that is already running is abandoned: that image is not saved and not charged. The run may still need a few seconds, for very large images up to the rest of that conversion, before it ends.
- If a run is stopped in a way the Actor cannot see (for example out of memory), the summary keeps the last progress (`"status": "RUNNING"` with the counts so far).
- **Maximum cost per run**: before each image the Actor checks that your limit still covers it. When it does not, the Actor stops cleanly: the images converted so far are saved and the summary says `"status": "PARTIAL"`. Images are processed in order (the store in key order, then the uploads, then the links), so the dataset shows where it stopped. A link is not read once the limit no longer covers an image. A new run starts from the beginning again: raise the limit, or narrow the images down with `keyPrefix` or fewer links.
- Memory: the default 1 GB is enough for most images (see Limits). More memory makes AVIF and very large images faster, but the Actor start event is charged once per GB of memory, so it also makes each run start cost more.

### Limits

- **File size**: up to 50 MB per file. A link whose `Content-Length` is bigger is refused before its body is read; without a `Content-Length`, reading stops at 50 MB.
- **Links**: up to 1,000 per run. One download may take at most 30 seconds (all attempts and redirects together), and less when the run's timeout is near. The server must answer within 8 seconds and must not pause for more than 8 seconds, and after the first 5 seconds the download must average at least 250 KB/s. Otherwise the image fails with `TIMEOUT`, `DOWNLOAD_FAILED` or `SLOW_DOWNLOAD` and is not charged. A file of the full 50 MB needs about 1.75 MB/s on average. For a file on a slow host, upload it or put it in a key-value store and choose that store. After 20 links in a row have failed, the rest of the list is not tried (see [Image links](#image-links)). The same happens when failed links have used too much run time: the seconds from the start of each failed download to its record, multiplied by the run's memory in GB, may reach 600, plus 40 for each charged event. These are limits the Actor enforces, not measured speeds: the time of a download depends on the host.
- **Animations**: a GIF or an animated WebP may have up to 10,000 frames, from any source. A file with more fails with `TOO_MANY_FRAMES` before it is opened and is not charged: the time to read an animation grows with the square of its frames. Shorten it, or export the frame you need as a still image.
- **Image size**: up to 100 megapixels, and less with less memory, because the whole image is held in memory while it is converted. Before each image the Actor checks what is left of the run's memory (the run's memory minus what the process already uses). Typical limits:

| Memory | to WebP | to JPEG | to PNG | to AVIF | from an AVIF file |
|---|---|---|---|---|---|
| 512 MB | 25 MP | 27 MP | 38 MP | 8 MP | 17 MP |
| 1 GB (default) | 63 MP | 69 MP | 95 MP | 20 MP | 42 MP |
| 2 GB | 100 MP | 100 MP | 100 MP | 44 MP | 93 MP |
| 4 GB | 100 MP | 100 MP | 100 MP | 92 MP | 100 MP (92 MP to AVIF) |
| 8 GB | 100 MP | 100 MP | 100 MP | 100 MP | 100 MP |

Decoding an AVIF file needs about 18 MB per megapixel whatever the output, so AVIF inputs have their own column. A bigger image fails with `TOO_MANY_PIXELS` and a message that tells you how much memory it needs (not charged).

- **Measured on Apify with 1 GB of memory** (the default; synthetic test photos):

| Test (1 GB run) | Time | Peak memory of the run |
|---|---|---|
| The 3 built-in samples (default input) | 3.6 s for the whole run | 73 MB |
| 10 photos of 12 MP (3.2 MB JPEG) to WebP | 105 s for the whole run (about 10 s each) | 228 MB |
| 5 photos of 55 MP (14.6 MB JPEG, needs an EXIF rotation) to WebP | 4-5 s each for the first two, then about 27.5 s each | 537 MB |
| 20 photos of 26 MP (6.9 MB JPEG) to WebP | 250 s for the whole run (about 2 s each for the first four, then about 14 s each) | 306 MB; the memory in use after each image stayed at 129-140 MB |
| One 18 MP photo to AVIF | about 8 s | 703 MB |
| One 38 MP AVIF file to PNG (the result is 78 MB) | 6.3 s | 639 MB |
| One 30 MP PNG with reduced colors | 19.7 s | 249 MB |
| One 70 MP photo to WebP | refused at once with `TOO_MANY_PIXELS` ("Start the run with 2048 MB"), not charged | 100 MB |

A 1 GB run gets a quarter of a CPU core. The first seconds of a run are about as fast as a full core, then the run slows down: for a long batch on 1 GB, expect about 7 times the times of our test computer below. The AVIF and PNG times above were measured at the start of a run, so a long batch of such images takes longer per image.

- **Measured on our test computer** (one processing thread):

| Test | Time | Peak memory |
|---|---|---|
| One 50 MP photo (12.6 MB JPEG, needs an EXIF rotation) to WebP | 3.5 s | 454 MB |
| ... to JPEG | 5.4 s | 537 MB |
| ... to PNG (lossless; the result is 120 MB) | 4.7 s | 349 MB |
| ... to AVIF | 13.3 s | 1,823 MB |
| ... to WebP, shrunk to 2000 px wide | 0.6 s | 113 MB |
| One 100 MP photo (25 MB JPEG) to WebP | 5.5 s | 844 MB |
| One 12 MP photo to WebP / to AVIF | 1.6 s / 3.6 s | 190 MB / 490 MB |
| One 50 MP AVIF file to WebP | 5.4 s | 943 MB |
| One 16 MP photo to PNG with reduced colors | 4.3 s | - |
| 200 photos of 2 MP to WebP, one after another | 50 s (0.25 s each) | 100 MB, flat |
| 200 photos of 12 MP to WebP, one after another | 303 s (1.5 s each) | 264 MB, flat |

The test photos are synthetic (gradients with noise), which compress less than most real photos, so real files usually get smaller than in these tests.

- Images are converted one at a time. WebP, JPEG and PNG encoding use one CPU core, so more than 4 GB of memory does not make them faster; it helps AVIF and very large images.
- Each image may take up to 300 seconds (less when the run's timeout is near); a longer conversion fails with `TIMEOUT` and is not charged. Reducing the colors of a PNG cannot be cut short this way, so a very large reduced-color PNG can run longer. For thousands of images, give the run a longer timeout (Run options) or split the work with `keyPrefix` or `maxImages`. A link is not started in the last 55 seconds before the run's timeout.
- Uploaded files must be stored in your Apify account, and the Actor reads them through the Apify API. Links to files on other websites go into `imageUrls`.
- The Actor only reads the links you give, the key-value store you choose and the files you upload (it runs with limited permissions). If your account keeps its storages private, choose the store your uploads went to (see [Uploaded files](#uploaded-files)).

### Privacy

- The Actor reads only the links you give (one GET each, plus redirects and retries as described above), the key-value store you choose and the files you upload. It does not send your images to any other service. The server behind a link sees a request from Apify's network with the User-Agent above. Only use images you have the right to use.
- The converted images are saved in this run's default key-value store in your account, like every Actor's results, and are kept or deleted according to your account's data retention. The original records are not changed.
- The dataset, the summary and the log contain file names, sizes and true/false flags, never the metadata values (no GPS coordinates, camera serial numbers or captions). They show links without their query string, because signed links can carry tokens there. The run's `INPUT` record, like every Actor's, keeps the links as you gave them.
- The summary keeps a hash of your list of links (`resume.urlsHash`), to continue the right list after a migration. It cannot be read back as such, but it has no salt, so predictable values (a URL pattern, sequential IDs) can be guessed and checked against it.
- Removing metadata removes the data stored in the file. Anything visible in the picture itself (a watermark, a street sign, a face) stays.
- With `colorProfile: keep`, the ICC color profile stays in the file, including any text it carries (its description and copyright, sometimes the name of the device or software that made it).
- With `keepMetadata: true`, EXIF (including the GPS location), XMP and IPTC stay in the converted files on purpose.

### Example tasks

Ready-made inputs for common jobs. Open one, then click **Try for free** to copy it to your account and change the input:

- [Convert JPEG and PNG images to WebP in bulk](https://apify.com/quietworkslab/image-converter/examples/convert-images-to-webp)
- [Compress images to AVIF for a faster website](https://apify.com/quietworkslab/image-converter/examples/compress-images-to-avif)
- [Remove EXIF and GPS location data from photos](https://apify.com/quietworkslab/image-converter/examples/remove-exif-and-gps-from-photos)
- [Resize and compress product photos for an online store](https://apify.com/quietworkslab/image-converter/examples/resize-product-photos-for-online-store)

### More from Quiet Works Lab

- [Video Frame Extractor](https://apify.com/quietworkslab/video-frame-extractor): extract thumbnails and frames from your videos, with a contact sheet per video.
- [Merge, Deduplicate & Join Datasets](https://apify.com/quietworkslab/merge-dedup-datasets): merge your datasets, remove duplicates by key and join fields from a lookup dataset.
- [Dataset Change Tracker](https://apify.com/quietworkslab/dataset-change-tracker): compare two runs of your scraper and get only the new, changed and removed items.
- [Japanese Text Normalizer & RAG Chunker](https://apify.com/quietworkslab/japanese-text-normalizer): clean Japanese text before an LLM or RAG and split it into sentence-aware chunks.

### Support

This Actor is developed and maintained with the help of AI (Claude Code). Issues are answered within a few days.

Please open an issue on the **Issues** tab with the run ID and, if you can, a sample image that shows the problem (remove anything private first).

# Changelog

This Actor's version history is a separate document: https://apify.com/quietworkslab/image-converter/changelog.md

# Actor input Schema

## `keyValueStoreId` (type: `string`):

The key-value store in your account that holds the images. Every record that is an image is converted; JSON and text records are skipped. Leave it empty (with no uploads and no links) to try the Actor on built-in sample images (no image-processed events for them). When this Actor runs after another Actor from its Integrations tab, set it to {{resource.defaultKeyValueStoreId}}. Uploaded files that are in this store are converted with it, once.

## `keyPrefix` (type: `string`):

Optional. Convert only the records of the key-value store whose key starts with this text, for example product- or 2026-10-. Empty = all records; the value is saved encrypted, so Apify Console hides it after saving, and OUTPUT shows the prefix the run used.

## `uploadedFiles` (type: `array`):

Optional. Upload image files here (JPEG, PNG, WebP, GIF, AVIF or TIFF, up to 50 MB each). This field takes only files stored in your Apify account; put links to images on other websites into "Image links". If your account keeps its storages private (the default), the run names the store your uploads are in: choose that store in "Key-value store" and start again.

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

Optional. Direct links to image files (JPEG, PNG, WebP, GIF, AVIF or TIFF), not web pages; use only images you have the right to use. A run takes up to 1,000 links, read one at a time after the store and the uploads; a link that fails is not charged, and the README lists the limits. Leave empty (with no store and no uploads) to try the Actor on built-in sample images.

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

Format of the converted images. WebP is small and works in all current browsers. AVIF is usually smaller still but slower to make (it counts double, see Pricing). Same as the original keeps the format and only compresses, resizes and removes metadata.

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

Compression quality from 1 (smallest file) to 100 (best quality) for WebP, AVIF and JPEG. 75 to 85 is a good balance. For PNG it is only used with "Reduce PNG colors".

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

Optional. Shrink images wider than this, keeping the aspect ratio, for example 1920. Images are never enlarged. Empty = keep the width.

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

Optional. Shrink images taller than this, keeping the aspect ratio, for example 1080. Images are never enlarged. Empty = keep the height.

## `colorProfile` (type: `string`):

Convert to sRGB turns the colors into standard sRGB and writes no profile, which is what browsers expect. Keep the original profile embeds the image's own ICC profile (for example Display P3 or Adobe RGB) for wide-gamut or print work; a profile can contain text such as its description and copyright, which then stays in the file.

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

Unchecked (default): all EXIF, GPS location, XMP, IPTC and comments are removed. Checked: EXIF (including the GPS location), XMP and IPTC are kept where the output format supports them, for example for a photo archive. The orientation is always applied to the pixels.

## `pngReduceColors` (type: `boolean`):

For PNG output: reduce the image to at most 256 colors (lossy, often 50-80% smaller for screenshots, logos and graphics; not for photos). Quality controls how much the colors may change. It takes about twice the computing time, so it counts double (see Pricing). Unchecked = lossless PNG.

## `maxImages` (type: `integer`):

Optional. Stop after this many images (converted or failed), for example to try the settings on 10 images first. Empty = no limit.

## Actor input object example

```json
{
  "outputFormat": "webp",
  "quality": 80,
  "colorProfile": "srgb",
  "keepMetadata": false,
  "pngReduceColors": false
}
```

# Actor output Schema

## `images` (type: `string`):

One record per image: status, link to the converted file, sizes before and after, which kinds of metadata it had.

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

List of the records in the run's key-value store (the converted images and OUTPUT).

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

Status, settings and counts of the run (record OUTPUT). It is written at the start and after every image, so it also exists when a run is stopped by a timeout or lack of memory (status RUNNING).

# 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("quietworkslab/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("quietworkslab/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 quietworkslab/image-converter --silent --output-dataset

```

## MCP server setup

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