Image Toolkit — convert, resize, strip metadata, inspect
Pricing
from $2.00 / 1,000 image processeds
Image Toolkit — convert, resize, strip metadata, inspect
Batch image jobs without a design tool: convert between JPEG, PNG, WebP and AVIF, resize to a target width without upscaling, strip EXIF/GPS/XMP for privacy, or read image metadata. One dataset item per image, files in the key-value store, per-image pricing.
Pricing
from $2.00 / 1,000 image processeds
Rating
0.0
(0)
Developer
praestat media
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 hours ago
Last modified
Categories
Share
The routine image jobs of a data pipeline, without Photoshop, ImageMagick or an external service. Everything runs inside the actor container: convert between JPEG, PNG, WebP and AVIF, resize to a target width, strip EXIF/GPS/XMP for privacy, or just read the metadata. Results come back as structured dataset items plus downloadable files in the key-value store.
Operations
operation | What it does | Output |
|---|---|---|
convert | Change the file format (jpg, png, webp, avif); transparency is flattened onto background for JPEG | new image file |
resize | Scale to targetWidth, keep the aspect ratio, optional maxHeight ceiling; never upscales | new image file |
strip_metadata | Rebuild the image without EXIF, GPS, XMP, ICC or DPI — lossless PNG by default, JPEG with jpegQuality | new image file |
info | Format, mode, dimensions, EXIF/GPS/profile flags, DPI, frame count, megapixels, aspect ratio, print size at 300 dpi — and for geotagged photos the coordinates plus ready-made links to OpenStreetMap, Google Maps and Bing Maps — no image data, no file written | metadata only |
Supported inputs: whatever the Pillow build reads — JPEG, PNG, WebP, AVIF, GIF (including animated), TIFF, BMP, ICO, PPM. Unreadable files produce a failed item with a clear message instead of aborting the run.
Input example
{"operation": "convert","imageUrls": ["https://example.com/photo.png"],"targetFormat": "jpg","jpegQuality": 82,"background": "#ffffff"}
{"operation": "resize","imageUrls": ["https://example.com/hero.jpg", "https://example.com/thumb.jpg"],"targetWidth": 1600,"maxHeight": 2000}
{"operation": "strip_metadata","imageUrls": ["https://example.com/upload/selfie.jpg"],"stripFormat": "png"}
{"operation": "info","imageUrls": ["https://example.com/photo.jpg"]}
Output
One dataset item per image. Every image gets its own item, plus a file in the key-value store.
{"operation": "convert","source": "photo.png","status": "ok","sourceFormat": "PNG","sourceMode": "RGBA","format": "JPEG","width": 800,"height": 600,"mode": "RGB","bytesBefore": 51234,"bytesAfter": 18420,"savedPercent": 64.05,"file": "out/photo-converted.jpg","filename": "photo-converted.jpg","key": "file-photo-converted.jpg","note": null,"error": null}
Why one item per image instead of one summary item
The alternative — a single item with a list of all files — was considered and rejected:
- Batches fail partially: one truncated download, one 404, one CMYK oddity. With one item per
image,
statusanderrorare first-class fields;GET /items?status=failedor Apify's dataset filter shows exactly what to retry. With a summary item the caller has to parse a nested list to discover the same thing. - Dataset views, sorting and CSV export work per row. A nested file list breaks all three.
- Apify's dataset UI, webhooks and
apify callconsumers expect a flat item per processed unit; this is the shape that plays well withpushData-style pipelines. - Cost: 10,000 images produce 10,000 small items. That is well inside the platform limits and is the normal shape for actors that process lists.
The trade-off is more rows for very large jobs. Each row stays small (a few hundred bytes, no image
data), and key plus file always point at the actual file.
Generated files are also stored in the default key-value store under the prefix file-, so a
follow-up actor or your own script can pick them up directly. Sub-paths are flattened with _
(file-out_photo-converted.jpg), which is why the store schema can collect them as one collection.
Downloaded inputs live under _src/ and are deliberately not stored.
Pricing (pay per event)
| Event | When | Price |
|---|---|---|
apify-actor-start | once per run and per GB of memory (2 GB default) | $0.005 |
image-processed | per image whose item has status: "ok" — convert, resize, strip_metadata and info alike | $0.001 |
A 1,000-image conversion therefore costs about $1.01 including the start event.
Failed images are not charged: they appear in the dataset, but the counter only adds status: "ok".
byte-saved is deliberately not an event. The saving depends entirely on the input material
(a 12 MP photo re-encoded at quality 82 shrinks by 90 %, a 5 KB icon by 2 %), so billing on it would
be unpredictable for you and unverifiable for us. Storage and compute are already covered by
image-processed.
Limits and honest notes
- No upscaling, ever. If an image is narrower than
targetWidth, the original file is copied unchanged (byte-identical) and the item carriesresized: falseplus a note. If you need a minimum size, filter onresizedafterwards. - Animated images: GIF and animated WebP are processed as their first frame. The item says
animated: trueand how many frames the source had (frames). There is no frame/animation output. - Transparency and JPEG: transparent areas are flattened onto
background(default#ffffff). Left to itself Pillow would write black, which is almost never what you want for a product shot. - CMYK: CMYK JPEGs are converted to RGB. There is no colour management — no ICC transform to
sRGB — so a print proof can look slightly different after conversion.
convertkeeps an embedded ICC profile, but it does not apply it. - 16-bit and float images: reduced to 8 bit per channel (
65535 → 255); the item flagsbitDepthReduced: true. PNG output is 8 bit, not 16 bit. - EXIF orientation:
resizeandstrip_metadatabake the rotation into the pixels, so the result stands upright even after the EXIF tag is gone.convertleaves the pixels as stored and keeps the orientation tag, so viewers still rotate it — but a naive consumer that ignores EXIF will see it sideways. - AVIF needs a Pillow build with libavif (Pillow ≥ 11.2; the official wheels include it). If the build lacks it, the run stops with a clear message instead of writing unusable files. AVIF encoding is noticeably slower than JPEG.
strip_metadatais a privacy tool, not a forensic one. It removes EXIF (including GPS and camera data), XMP, ICC and DPI by rebuilding the pixel buffer and saving without metadata. It does not scrub additional images hidden in MPO/MPF containers, steganography or proprietary chunks; PNG output (the default) cannot carry EXIF at all.infodecodes the file to validate it (Pillow'sload()), even though it only reports headers. A truncated file is therefore reported as failed rather than described as healthy.- No colour management, no HDR/wide-gamut handling, no 32-bit output. Resizing uses Lanczos.
- Source access: public URLs or local paths. No authenticated fetches, cookies or signed S3 requests. On Apify, local paths refer to the container filesystem (e.g. a file you fetched in an earlier step) — in practice use URLs.
- Duplicate file names from different sources get
-2,-3suffixes in the output folder; nothing is silently overwritten. - Very large images (100 MP+) need more memory than the default 2 GB run; raise the memory setting.
The
apify-actor-startprice scales with it.
Reporting a problem
Please use the Issues tab on the Actor page instead of email. The report then sits with the right Actor, together with the input and the run it came from — usually exactly what is missing to reproduce it. Issues are read and answered there.
What happens to your data
Input files and results stay in your run's storage on Apify. Apify keeps the ten most recent runs of an Actor indefinitely; older runs and their default storages are deleted under the platform's retention rules. I do not export or archive your files anywhere else, and the Actor never sends them to a third-party service.
Local development
python -m venv .venv && .venv/bin/pip install -r requirements.txt.venv/bin/python -m pytest tests -q.venv/bin/python samples/make_samples.pyecho '{"operation":"resize","imageUrls":["samples/transparent-rgba.png"],"targetWidth":400}' > /tmp/in.json.venv/bin/python -m src --input /tmp/in.json --outdir out --push
The CLI path is the same code as the actor run, so what passes locally behaves identically on Apify. The tests need no network and no Apify token; they generate their own test images.