# Bulk File Downloader and Website Image Extractor (`pennydata/website-image-extractor`) Actor

Bulk file downloader: PDFs, images, any file from a URL list, CSV or dataset, with retries, filename template and ZIP. $0.001 per file, failed files free. Also extracts images from pages.

- **URL**: https://apify.com/pennydata/website-image-extractor.md
- **Developed by:** [Penny Data](https://apify.com/pennydata) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Website Image Extractor: All Images from Any Page

**Two modes. Bulk file downloader: $0.001 per file downloaded. Image extractor: $0.002 per image row delivered (about $2 per 1,000 images). You are charged only for result rows that contain an image; pages that fail, are blocked by robots.txt or contain no images cost nothing; failed downloads are never charged.**

Give it public page URLs. It reads the HTML and returns one row per unique image: `<img src>` and `srcset` (largest candidate), `<picture><source>`, `og:image` / `twitter:image`, and optionally CSS `background-image: url()`. Plain HTTP, no browser, no proxies, no login.

### Bulk file downloader (mode `files`)

**$0.001 per file successfully downloaded.** Failed, timed-out, oversized and 4xx/5xx files are retried (timeouts, network errors, 429, 5xx; default 2 retries with backoff) and are never charged. Any file type: PDF, images, CSV, ZIP, video, audio.

Input example:

```json
{
  "mode": "files",
  "fileUrls": [{ "url": "https://example.com/a.pdf" }, { "url": "https://example.com/photo.jpg" }],
  "filenameTemplate": "{index}-{basename}",
  "createZip": true,
  "maxCostUsd": 5
}
```

- URL sources (combined, deduplicated): `fileUrls` (plain list), `csvUrl` (+ `csvColumn` header or 0-based index; a Google Sheet CSV export link works), `datasetId` (+ `datasetField`).
- `filenameTemplate`: `{index}`, `{basename}`, `{name}`, `{ext}`, `{host}`. Default `{index}-{basename}`.
- `createZip`: also writes all files to one `files.zip` record in the key-value store (up to 400 MB; larger sets stay available as individual records).
- `maxCostUsd`: hard guard, at most floor(maxCostUsd / 0.001) files are downloaded. Also `maxFiles`, `maxFileMb` (default 50), `retries`, `timeoutSecs`, `concurrency`.
- Output: one dataset row per URL (`status` ok/error, `error`, `fileName`, `kvsKey`, `kvsUrl`, `contentType`, `bytes`, `attempts`).
- Only public http(s) URLs are fetched; private and internal addresses are blocked. You are responsible for the right to download and store the files.

### Image extraction mode (default, mode `images`)

### Compared with per-URL pricing

| | This actor | onescales/bulk-image-downloader (as listed 2026-10) |
|---|---|---|
| Price | $0.002 per image found | $0.07 per URL processed |
| Page with 5 images | $0.010 | $0.07 |
| Page with 20 images | $0.040 | $0.07 |
| Page with 0 images | $0 | $0.07 |
| Break-even | about 35 images per page | |

Pages with more than ~35 images cost more here than at a flat $0.07 per URL; use `maxImages` or `minWidth` to control spend. Prices of other actors may change; check their listing.

### Input

- `startUrls`: page URLs (prefilled with a Wikipedia article). Aliases `inputUrls`, `urls`, `url` are merged in.
- `cssSelector`: only return images inside matching elements (e.g. `.product-gallery`). Each row also has a `filename`.
- `maxPagesPerStart` (default 1): also follow same-site links, up to N pages per start URL.
- `minWidth`, `minBytes`: drop small images (unknown width/size is kept).
- `includeCssBackgrounds` (default off), `sameDomainOnly` (default off), `fetchMetadata` (default on: HEAD per image for type and size).
- `downloadToKvs` (default off): store each image (max 10 MB) in the key-value store; failed downloads are not delivered or charged.
- `maxImages` (default 100), `respectRobotsTxt` (default on), `timeoutSecs`.

### Output (one row per image)

`pageUrl`, `imageUrl`, `alt`, `width`, `height`, `contentType`, `bytes`, `source` (`img`, `img-srcset`, `picture-source`, `og:image`, `twitter:image`, `css-background`), plus `kvsKey`/`kvsUrl` when downloading. Width/height are only what the page declares; the actor does not decode images.

### Limits

- Lazy-loaded images are found when their URL is in the HTML (`data-src`, `data-lazy-src`, `data-original`, `data-srcset`, `<picture>`, `<noscript>`, `data-bg`). Images added by JavaScript after page load are not seen (no rendering).
- A page with no images produces one row with `status: no_images` and a `reason` (JavaScript-rendered gallery, selector matched nothing, non-HTML). These rows are never charged.
- `dedupeAcrossPages` (default on) delivers and charges each image URL once per run. External stylesheets are not read.
- Only the pages you supply (and, if enabled, same-site links) are fetched. No search-engine scraping. Private and internal addresses are blocked. You are responsible for respecting the copyright and license of the images you download.
- Not affiliated with any website you scan.

### Next step: upscale the images

Each row of this Actor's output has an `imageUrl`. To make them larger, feed the `imageUrl` values into [Image Upscaler (OpenCV, fast and cheap)](https://apify.com/pennydata/image-upscaler-fast), which returns each image 2x, 3x or 4x larger. It is a separate Actor with its own pay-per-event price; you are charged for it only when you run it.

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });

// 1. Run this Actor, then read its dataset.
const run = await client.actor('pennydata/website-image-extractor').call({ /* your input */ });
const { items } = await client.dataset(run.defaultDatasetId).listItems();

// 2. Send the image URLs to the upscaler.
const imageUrls = items.map((i) => i.imageUrl).filter(Boolean).slice(0, 500);
const up = await client.actor('pennydata/image-upscaler-fast').call({ imageUrls, scale: '2', outputFormat: 'png' });
const out = await client.dataset(up.defaultDatasetId).listItems();
console.log(out.items.map((r) => r.outputUrl));
```

The upscaler accepts up to 500 URLs per run and images up to 2 megapixels and 25 MB; larger images come back as an error row and are not charged. It uses small OpenCV networks, not generative AI, so expect cleaner edges at a larger size rather than new detail. Use `minWidth` here to skip tiny icons before upscaling.

### Free bookmarklet: list images on any page

Need the image URLs of just one page you are looking at? Use this free bookmarklet instead of the Actor. It runs only in your browser, makes no network calls and sends nothing anywhere. Create a new browser bookmark and paste the line below as its URL, open any page, click the bookmark, then press Copy.

```
javascript:(function%20()%20{var%20seen%20=%20{},%20urls%20=%20[];function%20add(u)%20{if%20(!u)%20return;try%20{%20u%20=%20new%20URL(u,%20document.baseURI).href;%20}%20catch%20(e)%20{%20return;%20}if%20(/%5E(https?:|data:image)/.test(u)%20&&%20!seen[u])%20{%20seen[u]%20=%201;%20urls.push(u);%20}}function%20srcset(s)%20{%20(s%20||%20'').split(',').forEach(function%20(p)%20{%20add(p.trim().split(/%5Cs+/)[0]);%20});%20}function%20bg(v)%20{%20var%20re%20=%20/url%5C((['"]?)(.*?)%5C1%5C)/g,%20m;%20while%20((m%20=%20re.exec(v%20||%20'')))%20add(m[2]);%20}document.querySelectorAll('img').forEach(function%20(i)%20{%20add(i.currentSrc%20||%20i.src);%20add(i.getAttribute('data-src'));%20srcset(i.srcset);%20});document.querySelectorAll('source[srcset]').forEach(function%20(s)%20{%20srcset(s.srcset);%20});document.querySelectorAll('meta[property="og:image"],meta[name="twitter:image"]').forEach(function%20(m)%20{%20add(m.content);%20});document.querySelectorAll('link[rel~="icon"],link[rel="apple-touch-icon"]').forEach(function%20(l)%20{%20add(l.href);%20});document.querySelectorAll('*').forEach(function%20(e)%20{%20bg(getComputedStyle(e).backgroundImage);%20});var%20old%20=%20document.getElementById('wie-box');%20if%20(old)%20old.remove();var%20d%20=%20document.createElement('div');%20d.id%20=%20'wie-box';d.style.cssText%20=%20'position:fixed;z-index:2147483647;top:10px;right:10px;width:460px;max-width:95vw;background:%23fff;color:%23000;border:2px%20solid%20%23333;padding:10px;font:13px%20sans-serif;box-shadow:0%204px%2020px%20%230006';var%20h%20=%20document.createElement('div');%20h.textContent%20=%20urls.length%20+%20'%20image%20URLs%20found';%20h.style.fontWeight%20=%20'bold';var%20t%20=%20document.createElement('textarea');%20t.id%20=%20'wie-text';%20t.value%20=%20urls.join('%5Cn');%20t.style.cssText%20=%20'width:100%25;height:240px;margin:6px%200;box-sizing:border-box';var%20b%20=%20document.createElement('button');%20b.textContent%20=%20'Copy';%20b.onclick%20=%20function%20()%20{%20t.select();%20document.execCommand('copy');%20};var%20c%20=%20document.createElement('button');%20c.textContent%20=%20'Close';%20c.onclick%20=%20function%20()%20{%20d.remove();%20};var%20a%20=%20document.createElement('a');%20a.href%20=%20'https://apify.com/pennydata/website-image-extractor';%20a.target%20=%20'_blank';%20a.rel%20=%20'noopener';a.textContent%20=%20'Need%20many%20pages%20or%20bulk%20download?%20Website%20Image%20Extractor';%20a.style.cssText%20=%20'display:block;margin-top:8px;color:%2306c';d.append(h,%20t,%20b,%20c,%20a);%20document.body.appendChild(d);})();
```

It lists `img` src/srcset, `source` sets, lazy-load `data-src`, CSS backgrounds, og:image and icons, with duplicates removed. It only sees the page you are on. For many pages, whole sites, a CSS selector filter or downloading the files, run this Actor.

### AI disclosure

Built with AI assistance (Claude); maintained by the publisher.

### Related pennydata actors

Other Actors from the same publisher for website work. Each is a separate Actor with its own pay-per-event price; you are charged only for what you run.

- [Sitemap Extractor Plus](https://apify.com/pennydata/sitemap-extractor-plus): list a site's page URLs from its sitemaps, then feed them here as start URLs.
- [Screenshot & PDF (shot-pdf)](https://apify.com/pennydata/shot-pdf): capture full-page screenshots or PDFs of the same pages.
- [Website Tech Stack Detector](https://apify.com/pennydata/website-tech-stack-detector): detect the CMS, frameworks and analytics a site uses.

# Actor input Schema

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

images = extract image URLs from web pages ($0.002 per image). files = bulk file downloader: download every file in a URL list, CSV or dataset ($0.001 per file).

## `startUrls` (type: `array`):

Public web pages to read. Each page is fetched with plain HTTP (no browser, no JavaScript rendering).

## `cssSelector` (type: `string`):

Only return images inside elements matching this selector, e.g. .product-gallery or main. Empty = whole page (og:image/twitter:image meta tags are only included when empty).

## `inputUrls` (type: `array`):

Alias of startUrls; both are merged.

## `urls` (type: `array`):

Alias of startUrls; both are merged.

## `url` (type: `string`):

Alias of startUrls for a single page.

## `maxPagesPerStart` (type: `integer`):

1 = only the page you list. Higher values also follow same-site links, up to this many pages per start URL.

## `minWidth` (type: `integer`):

Skip images whose declared width (width attribute or srcset descriptor) is smaller. Images with unknown width are kept. 0 = off.

## `minBytes` (type: `integer`):

Skip images whose Content-Length (from a HEAD request) is smaller. Images with unknown size are kept. 0 = off.

## `includeCssBackgrounds` (type: `boolean`):

Also collect url() images from inline style attributes and <style> blocks (external stylesheets are not read).

## `dedupeAcrossPages` (type: `boolean`):

Deliver (and charge) each image URL once per run, even when it appears on several pages. Turn off to get one row per page and image.

## `sameDomainOnly` (type: `boolean`):

Keep only images hosted on the same site (or a subdomain) as the page; drops CDN and third-party images.

## `fetchMetadata` (type: `boolean`):

Send one HEAD request per image to fill contentType and bytes. Turn off for faster runs.

## `downloadToKvs` (type: `boolean`):

Also download each image (max 10 MB) into the run's key-value store and add kvsKey/kvsUrl to the row. Images that fail to download are not delivered or charged.

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

Stop after this many image rows. Caps your cost: 100 rows cost about $0.20.

## `respectRobotsTxt` (type: `boolean`):

Skip pages disallowed for all bots in robots.txt. Skipped pages cost nothing.

## `timeoutSecs` (type: `integer`):

Per-request timeout in seconds (5-60).

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

Files mode only. Direct links to files of any type (PDF, images, CSV, ZIP...). Plain URL list.

## `csvUrl` (type: `string`):

Files mode only. Public link to a CSV or Google Sheet CSV export containing file URLs.

## `csvColumn` (type: `string`):

Header name or 0-based column number holding the URLs. Empty = first column that contains URLs.

## `datasetId` (type: `string`):

Files mode only. ID or name of an Apify dataset whose items contain file URLs.

## `datasetField` (type: `string`):

Field holding the URL in each dataset item. Empty = first URL-like value in the item.

## `filenameTemplate` (type: `string`):

Name of each stored file. Placeholders: {index} (0001...), {basename} (name.ext from the URL), {name}, {ext}, {host}.

## `downloadZip` (type: `boolean`):

Images mode: download every image and put them all into one images.zip record in the key-value store. The link is in each row (zipUrl) and in the OUTPUT record. No extra charge.

## `maxZipMb` (type: `integer`):

Images mode with downloadZip: images beyond this total size are left out of the ZIP (still saved individually).

## `createZip` (type: `boolean`):

Also put all downloaded files into one files.zip record in the key-value store (up to 400 MB).

## `retries` (type: `integer`):

Extra attempts for timeouts, network errors, HTTP 429 and 5xx, with backoff.

## `maxFileMb` (type: `integer`):

Files larger than this are skipped and not charged.

## `maxFiles` (type: `integer`):

Stop after this many files.

## `maxCostUsd` (type: `number`):

Spending guard: at most floor(maxCostUsd / 0.001) files are downloaded. Empty = no extra cap.

## `concurrency` (type: `integer`):

Parallel downloads.

## Actor input object example

```json
{
  "mode": "images",
  "startUrls": [
    {
      "url": "https://en.wikipedia.org/wiki/Cat"
    },
    {
      "url": "https://books.toscrape.com/"
    }
  ],
  "maxPagesPerStart": 1,
  "minWidth": 0,
  "minBytes": 0,
  "includeCssBackgrounds": false,
  "dedupeAcrossPages": true,
  "sameDomainOnly": false,
  "fetchMetadata": true,
  "downloadToKvs": false,
  "maxImages": 10,
  "respectRobotsTxt": true,
  "timeoutSecs": 20,
  "filenameTemplate": "{index}-{basename}",
  "downloadZip": false,
  "maxZipMb": 100,
  "createZip": false,
  "retries": 2,
  "maxFileMb": 50,
  "maxFiles": 1000,
  "concurrency": 5
}
```

# Actor output Schema

## `results` (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 = {
    "startUrls": [
        {
            "url": "https://en.wikipedia.org/wiki/Cat"
        },
        {
            "url": "https://books.toscrape.com/"
        }
    ],
    "maxImages": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("pennydata/website-image-extractor").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 = {
    "startUrls": [
        { "url": "https://en.wikipedia.org/wiki/Cat" },
        { "url": "https://books.toscrape.com/" },
    ],
    "maxImages": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("pennydata/website-image-extractor").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 '{
  "startUrls": [
    {
      "url": "https://en.wikipedia.org/wiki/Cat"
    },
    {
      "url": "https://books.toscrape.com/"
    }
  ],
  "maxImages": 10
}' |
apify call pennydata/website-image-extractor --silent --output-dataset

```

## MCP server setup

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

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/c2yaNUJHPEXTTl8Aa/builds/lw0H3zLyLJABdYgRc/openapi.json
