# JPEG Image Metadata Audit and EXIF Cleaner (`automation-lab/image-metadata-audit-cleaner`) Actor

Audit authorized JPEG batches and losslessly remove EXIF APP1 metadata. Export before/after segment inventories, scan-preservation hashes and cleaned JPEG references without pixel re-encoding.

- **URL**: https://apify.com/automation-lab/image-metadata-audit-cleaner.md
- **Developed by:** [Automation Lab](https://apify.com/automation-lab) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.96 / 1,000 item 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

## JPEG Image Metadata Audit and EXIF Cleaner

An image metadata remover for authorized JPEG batches: inspect segment inventories or remove EXIF APP1 segments without decoding or re-encoding pixels. Receive one audit row per attempted image, scan-preservation hashes and cleaned JPEG download references.

### Who is this for?

Publishing teams, photographers and developers who need a repeatable EXIF-cleaning step before distributing JPEG files. Use it for supplied files you own or are authorized to process, not for finding or collecting other people's images.

### Why use this Actor?

The cleaner works on compressed bytes, not a pixel reconstruction. Every non-EXIF segment and every scan byte remains unchanged. Before/after inventories make the operation auditable. Hashes provide a preservation check rather than a vague promise of an unchanged picture.

This is not a universal privacy scrubber. It does not detect or remove visible or invisible watermarks, AI labels, C2PA credentials, XMP, IPTC, comments or ICC profiles. It makes no undetectability or provenance-evasion claims.

### Getting started

1. Choose **clean** to remove EXIF or **audit** to inspect without producing a file.
2. Add an `images` array of source objects. Each object needs exactly one `url`, `base64` or `key`.
3. Set `maxItems`, the maximum number of entries attempted in input order.
4. Run the Actor and inspect the default dataset.
5. For successful clean rows, download the `cleanedUrl` or retrieve `cleanedKey` from that run's key-value store.

The Console prefill contains a small generated JPEG with a synthetic EXIF segment, so you can test the complete cleaning workflow without an external image server.

### Input parameters

| Input | Default | Behavior |
|---|---|---|
| images | required | 1–200 objects; one source per object |
| mode | clean | `clean` removes EXIF and stores files; `audit` only reports |
| maxItems | 20 | Global attempted-entry cap, 1–200; includes errors and duplicates |

Source objects:

- `url`: public HTTPS JPEG, port 443 only, no embedded credentials. Signed URL queries can be supplied but are not exported. Private/reserved addresses are rejected, DNS is pinned to a vetted resolution, and each redirect is checked.
- `base64`: canonical padded base64, optionally prefixed with `data:image/jpeg;base64,`. No whitespace or alternative media prefix.
- `key`: binary JPEG record in this run's default key-value store.
- `storeId`: optional accessible KV store ID/name, only with `key`. Use this for files already stored by another authorized run.

There is no source precedence: supplying multiple source fields is an error. Empty arrays, unknown fields, unsupported modes and zero/unlimited caps are rejected. Entries after the cap are neither fetched nor processed. Duplicate entries are processed and charged separately.

### Output

| Field | Meaning |
|---|---|
| index, sourceType | Input position and source route; no raw source value |
| status, error | `ok` or safe error detail |
| inputBytes, outputBytes | File sizes |
| removedExifSegments | Number removed; zero in audit mode |
| segmentsBefore, segmentsAfter | Ordered markers/entropy blocks with offsets, lengths and kinds |
| scanCount | Number of compressed scans |
| scanSha256Before, scanSha256After | SHA-256 over each entropy block prefixed by its four-byte big-endian length |
| scanPreserved | Matching scan hash and count; not a privacy guarantee |
| inputSha256, outputSha256 | SHA-256 of complete original/result files |
| cleanedKey, cleanedUrl | Run-local stored JPEG reference; null in audit mode |
| processedAt | Processing timestamp |

Error rows omit success-only fields and have no per-image charge. Inventories disclose segment existence and byte sizes, not raw tag values. Download access follows Apify storage permissions; never assume a private file is accessible to another user simply because a URL exists.

### Example output

For the generated prefill JPEG, cleaning removes one EXIF segment and preserves the scan:

```json
{
  "index": 0,
  "mode": "clean",
  "status": "ok",
  "sourceType": "base64",
  "removedExifSegments": 1,
  "scanCount": 1,
  "scanPreserved": true,
  "cleanedKey": "cleaned-0"
}
```

This is a subset of a real local result. Full rows also contain segment arrays and 64-character SHA-256 hashes.

### How much does it cost to clean JPEG EXIF metadata?

Pay-per-event pricing has one `start` event per validated run and one primary `item` event per successfully audited or cleaned JPEG. Both operations use the same price. Error rows are free of item charges; the start fee still applies when valid input yields only errors.

BRONZE pricing is **$0.001 per run plus $0.0016 per image**. Examples: 1 image $0.0026, 10 images $0.017, 100 images $0.161.

| Spend tier | Price per successful image |
|---|---|
| FREE | $0.00184 |
| BRONZE | $0.0016 |
| SILVER | $0.001248 |
| GOLD / PLATINUM / DIAMOND | $0.00096 |

The $0.001 start fee is the same at every tier. Spend tiers depend on qualifying aggregate monthly Apify Store spend, not on this Actor's batch volume. Your applicable tier and charge ceiling govern the invoice; stop at a charge limit rather than treating incomplete results as complete.

### Limits and failure behavior

- JPEG only: baseline, extended sequential and progressive Huffman frames. Not PNG, WebP, HEIC, arithmetic JPEG, TEM markers, JPEGs with zero-height frames or concatenated/trailing data.
- 8 MiB maximum per file. At most 200 entries; files are processed sequentially.
- HTTPS downloads have a 20-second deadline per request and at most three redirects. No retries, login, browser rendering or proxy fallback. Supply bytes/KV instead for authorized files behind access controls.
- Structural marker validation is not full entropy decoding. It cannot prove that an already-corrupt file renders correctly.
- EXIF orientation is removed too. Pixels are not rotated: viewers may display the cleaned file differently if they previously relied on its EXIF orientation. EXIF thumbnails are removed with the EXIF segment.
- XMP orientation, ICC profiles, comments and all other non-EXIF data remain. Do not use this as a guarantee that a file contains no identifying information.
- Individual loading/format errors generate rows and allow the rest of the batch to continue. If no JPEG succeeds, the run fails with error rows available. Storage or billing failures fail the run; outputs may be partial.

### Integrations

Use n8n or Make to supply approved publishing assets, read successful dataset rows and retrieve cleaned files into your publishing pipeline. Retain the scan hashes alongside your asset records as an audit trail. Filter `status=ok` before downstream publication and review non-EXIF metadata separately.

Run through the Apify API for CI publishing pipelines. No external image-processing API or AI provider is used at runtime, and no additional processing key is required.

### API usage

Save an input JSON file (for example the Console prefill), then invoke:

```bash
curl -X POST 'https://api.apify.com/v2/acts/automation-lab~image-metadata-audit-cleaner/runs' \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H 'Content-Type: application/json' --data-binary @input.json
```

JavaScript:

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/image-metadata-audit-cleaner').call(input);
const { items } = await client.dataset(run.defaultDatasetId).listItems({ limit: 20 });
```

Python:

```python
from apify_client import ApifyClient
import os
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('automation-lab/image-metadata-audit-cleaner').call(run_input=input_data)
items = client.dataset(run['defaultDatasetId']).list_items(limit=20).items
```

Retain the run ID and storage IDs. A start response is not the result dataset. Check terminal status and retrieve files from that same run; do not start another run merely to poll.

### MCP: Claude Code, Claude Desktop, Cursor and VS Code

Select the stable Actor-specific subset:

```bash
claude mcp add --transport http apify \
  "https://mcp.apify.com?tools=automation-lab/image-metadata-audit-cleaner"
```

Equivalent HTTP configuration for Claude Desktop, Cursor and VS Code clients that support remote MCP (adapt the outer key to the client's format):

```json
{
  "mcpServers": {
    "apify": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=automation-lab/image-metadata-audit-cleaner"
    }
  }
}
```

Authenticate using the client's supported Apify flow. Discover actual names/argument schemas with scoped `tools/list`; this selection includes run/dataset/KV retrieval and abort helpers too. Discovery is not permission to execute or abort.

Example prompt: “Audit these authorized JPEGs without modifying them; report whether EXIF segments are present.” Or: “Clean these approved JPEG assets; return download references and scan-preservation results, and flag errors.”

Start once with authorization. Keep the run ID. If nonterminal, use `get-actor-run` on that same ID with bounded 2/4/8-second backoff capped at 10 seconds and a 120-second total deadline. Configure client timeouts longer than the server wait plus transport margin. At timeout, recover the known run rather than restart; if its ID is unknown, report uncertainty. At deadline report pending and stop polling.

After success, retrieve untransformed JSON via `get-dataset-items`, with explicit limit/offset/fields. For example, process pages of 20 source rows host-side, admitting at most 100 rows or 64 KiB of serialized UTF-8 into model context. Enforce byte limits before model injection, including oversized rows. Report truncation and the source continuation offset. Full file exports stay outside model context. Clients without interception cannot promise a hard byte limit. No numeric token-saving claim is made.

### Legality, privacy and retention

Only process files you are authorized to use. Do not upload unlawful material or use cleaning to misrepresent provenance, ownership or authenticity. This is an independent utility, not an affiliation or certification.

Images can contain sensitive personal data in pixels and metadata. Input records remain in Apify run storage; clean output JPEGs and reports remain in the run's KV/dataset under your account's storage retention and deletion controls. The Actor does not automatically delete these records or set a fixed retention period. Delete the run's input, dataset and files when no longer needed; Apify backups/platform retention are governed by Apify, not this Actor. Intermediate buffers are memory-only and no cross-run cache is used. Source servers receive requests for supplied HTTPS URLs.

Failed operations send sanitized diagnostic input, exceptions and actor/build/run IDs to our private GlitchTip service for repair. Secret fields and URL queries are removed; the entire `images` field is redacted, including base64 and source keys. Reports are retained for 30 days. No supplied image bytes are sent to an AI provider. Icon artwork is generated separately from synthetic product descriptions, not customer data.

### FAQ and troubleshooting

**Does lossless mean all metadata is removed?** No. Only EXIF-signature APP1 segments are removed. Other metadata and identifying pixels remain.

**Why did orientation change?** Some viewers use an EXIF orientation tag. Removing it does not rotate pixels. Normalize orientation in a separate conversion step if needed.

**Why does my URL fail?** Use public direct HTTPS JPEG links without authentication or private addresses. Anti-bot pages, redirects beyond the cap and non-JPEG payloads are unsupported; supply base64 or an accessible KV record instead.

**Can I audit without creating a cleaned file?** Yes, use `mode: "audit"`. File references are null and byte hashes match.

**Why is the run successful with error rows?** At least one image succeeded. Check every row and never assume the whole batch succeeded from the run status alone.

### Related Actors

For re-encoding, resizing or format conversion instead of scan-preserving cleaning, see [Bulk Image Compressor](https://apify.com/automation-lab/bulk-image-compressor) and [Bulk Image Format Converter](https://apify.com/automation-lab/bulk-image-format-converter).

### Support

Use the Actor's Apify Issues tab for support. Include the run ID, operation and a safe reproducible example; do not post confidential image bytes, signed URLs or credentials publicly. Apify standard user terms apply; this README adds no custom end-user agreement.

# Changelog

This Actor's version history is a separate document: https://apify.com/automation-lab/image-metadata-audit-cleaner/changelog.md

# Actor input Schema

## `images` (type: `array`):

1–200 entries. Each needs exactly one url (public HTTPS port 443), base64 (padded JPEG base64, optional data:image/jpeg;base64, prefix), or key (binary JPEG record). Optional storeId applies only to key; without it uses this run’s default KV store. No deduplication; entries are processed in order. Each file is limited to 8 MiB.

## `mode` (type: `string`):

clean removes APP1 segments starting with Exif\0\0 and stores a JPEG. audit changes no bytes and produces no downloadable file. Both report inventories and scan hashes.

## `maxItems` (type: `integer`):

Global cap on attempted entries, in input order, including errors and duplicates. Entries beyond the cap are not fetched. Zero/unlimited is not supported.

## Actor input object example

```json
{
  "images": [
    {
      "base64": "/9j/4QAWRXhpZgAASUkqAAgAAAAAAAAAAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAgDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAT/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAABv/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AIgDcIf/2Q=="
    }
  ],
  "mode": "clean",
  "maxItems": 20
}
```

# Actor output Schema

## `dataset` (type: `string`):

Audit rows and safe per-image errors.

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

Stored JPEGs with EXIF removed; no files in audit mode

# 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 = {
    "images": [
        {
            "base64": "/9j/4QAWRXhpZgAASUkqAAgAAAAAAAAAAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAgDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAT/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAABv/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AIgDcIf/2Q=="
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/image-metadata-audit-cleaner").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 = { "images": [{ "base64": "/9j/4QAWRXhpZgAASUkqAAgAAAAAAAAAAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAgDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAT/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAABv/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AIgDcIf/2Q==" }] }

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/image-metadata-audit-cleaner").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 '{
  "images": [
    {
      "base64": "/9j/4QAWRXhpZgAASUkqAAgAAAAAAAAAAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAIAAgDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAT/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAABv/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AIgDcIf/2Q=="
    }
  ]
}' |
apify call automation-lab/image-metadata-audit-cleaner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automation-lab/image-metadata-audit-cleaner"
        }
    }
}
```

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/MdJ1ScDtNw8o8Fm7g/builds/91tkFS3uE1PBAWFuO/openapi.json
