Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If an html2canvas image is unreadable after a PHP upload, first determine where the bytes become invalid. Export the canvas locally, then use a binary-safe transport: preferably canvas.toBlob() in multipart FormData. If you must send a data URL, remove everything through the first comma, strictly base64-decode the remaining payload, validate the detected MIME type, and write the bytes without text processing. A blank or clipped local export, a tainted canvas, an oversized canvas, or a request-size limit must be fixed before changing PHP.

Find the failing stage before changing code

  1. Export in the browser. Open the generated data URL in a new tab or save the Blob locally. If it is blank, clipped, or throws a security error, the upload endpoint is not yet involved.
  2. Inspect the export bytes. A PNG begins with the PNG signature; a JPEG begins with its JPEG marker. Compare the local file’s size and hash with the file PHP writes.
  3. Inspect the request. Confirm the browser actually sends the expected multipart field or JSON property and that the server and reverse proxy accept its size.
  4. Inspect PHP’s output. Validate MIME and dimensions before moving a temporary upload or storing decoded bytes.

html2canvas reconstructs a page from the DOM; it does not capture the browser’s native composited screenshot. Unsupported CSS, fonts that have not loaded, animations, and resource timing can therefore produce a different image even when the upload is perfect.

Use multipart Blob upload when possible

toDataURL() creates a complete image in a large in-memory JavaScript string. MDN recommends toBlob() instead for this reason. Multipart upload also avoids putting a long base64 value inside JSON and makes PHP’s MIME and upload-error checks straightforward.

Method Payload and memory PHP handling Typical failure clues
Blob + FormData Binary bytes; no base64 expansion or giant data-URL string $_FILES, upload error, MIME check, then move the validated temporary file Missing field, upload-size limit, or nonzero upload error
JSON data URL Base64 text is larger than the binary and is held as a string Validate the prefix, take the text after the first comma, strict-decode it, then write bytes Prefix decoded as data, altered whitespace, truncation, or invalid base64

Browser code for a reliable multipart request

const canvas = await html2canvas(element, {
  useCORS: true,
  windowWidth: element.scrollWidth,
  windowHeight: element.scrollHeight
});

const blob = await new Promise((resolve, reject) => {
  canvas.toBlob(file => file ? resolve(file) : reject(new Error('toBlob failed')), 'image/png');
});

const form = new FormData();
form.append('image', blob, 'capture.png');

const response = await fetch('/upload.php', {
  method: 'POST',
  body: form
});
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);

Do not set Content-Type yourself for this request. The browser adds the multipart boundary. Setting a plain multipart content type without that boundary can make PHP report that no file was uploaded.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

PHP receiver for multipart files

<?php
$upload = $_FILES['image'] ?? null;
if (!$upload || $upload['error'] !== UPLOAD_ERR_OK) {
    http_response_code(400);
    exit('upload failed');
}

if (!is_uploaded_file($upload['tmp_name'])) {
    http_response_code(400);
    exit('invalid upload');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($upload['tmp_name']);
$allowed = [
    'image/png' => 'capture.png',
    'image/jpeg' => 'capture.jpg',
    'image/webp' => 'capture.webp'
];
if (!isset($allowed[$mime])) {
    http_response_code(415);
    exit('unsupported image type');
}

$destination = __DIR__ . '/uploads/' . $allowed[$mime];
if (!move_uploaded_file($upload['tmp_name'], $destination)) {
    http_response_code(500);
    exit('could not store image');
}
echo 'ok';

Create the destination directory with permissions appropriate to your deployment, and generate a unique server-side filename in production rather than trusting a client filename. Check dimensions and an application-specific maximum size before permanent storage.

If you send a data URL, decode only its payload

A valid value looks like data:image/png;base64,..... The prefix is metadata, not base64 image data. Split at the first comma, validate the media type, and use strict decoding.

Browser request

const dataUrl = canvas.toDataURL('image/png');
const response = await fetch('/upload.php', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({image: dataUrl})
});
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);

PHP data-URL receiver

<?php
$input = json_decode(file_get_contents('php://input'), true);
$dataUrl = is_array($input) ? ($input['image'] ?? '') : '';

if (!preg_match('#^data:image/(png|jpeg|webp);base64,#i', $dataUrl)) {
    http_response_code(400);
    exit('invalid image data URL');
}

$comma = strpos($dataUrl, ',');
$payload = substr($dataUrl, $comma + 1);
// Remove whitespace only when your transport inserted it.
$payload = preg_replace('/s+/', '', $payload);
$bytes = base64_decode($payload, true);
if ($bytes === false) {
    http_response_code(400);
    exit('invalid base64');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->buffer($bytes);
if (!in_array($mime, ['image/png', 'image/jpeg', 'image/webp'], true)) {
    http_response_code(415);
    exit('decoded bytes are not an allowed image');
}

if (file_put_contents(__DIR__ . '/uploads/capture.' . ($mime === 'image/png' ? 'png' : ($mime === 'image/jpeg' ? 'jpg' : 'webp')), $bytes, LOCK_EX) === false) {
    http_response_code(500);
    exit('write failed');
}
echo 'ok';

Do not HTML-escape the bytes, URL-decode them twice, trim binary data, concatenate diagnostics into the file, or pass the literal data:image/png;base64, prefix to base64_decode().

Fix cross-origin images before export

html2canvas cannot bypass browser content-policy restrictions. A cross-origin image can taint the canvas, after which toDataURL() and toBlob() may fail with a security error. Set useCORS: true only when the image server returns an Access-Control-Allow-Origin value matching your page (or an appropriate permitted origin) and serves the image with CORS enabled.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Set crossorigin="anonymous" on images before they load when required by your setup:

<img crossorigin="anonymous" src="https://assets.example.test/photo.jpg" alt="">

If you cannot change the source server, fetch or serve the asset through a same-origin proxy that adds the correct CORS response. A proxy must validate allowed hosts and content types; otherwise it can become a server-side request-forgery risk. A data URI embedded in your page is already same-origin from the canvas’s perspective, but very large embedded resources increase memory use.

Make the rendered page deterministic

  • Wait for images and fonts before calling html2canvas. Decode images and await document.fonts.ready where your browser support requires it.
  • Disable transitions, blinking cursors, and video updates during capture. Freeze the state you intend to save.
  • Capture the correct element and set windowWidth: element.scrollWidth and windowHeight: element.scrollHeight for a full scrollable region.
  • Reduce scale or capture smaller sections when output is blank, partially rendered, or causes memory pressure. Browser canvas dimensions are platform-dependent; figures such as roughly 32,767 pixels are guidance, not a universal limit.

A blank image produced locally is a rendering or canvas-limit problem, not corrupted PHP storage.

Localize corruption with hashes

Save the browser Blob before uploading and calculate a cryptographic hash (for example, SHA-256) of those bytes. Calculate the hash of the PHP file after writing it. Equal lengths and hashes prove that transport and storage preserved the image. Different values indicate request encoding, truncation, decoding, or writing. This test is more useful than opening an image viewer, which may hide a small trailing or leading-byte defect.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Common symptoms and fixes

PHP says no file was uploaded

For FormData, verify the field name is exactly image, do not manually set the multipart content type, and inspect $_FILES['image']['error']. Check post_max_size, upload_max_filesize, and any web-server or reverse-proxy request limit; their correct values depend on your deployment.

The data URL decodes but the image viewer rejects it

Log the prefix and decoded byte length, not the entire image. Ensure you removed text before the first comma, used strict base64 decoding, and did not JSON- or URL-decode the payload twice. Detect MIME from bytes rather than trusting a client-supplied extension.

The browser throws a security error

Find every external image, background image, SVG, and font used by the captured element. Enable CORS on each source or route it through a controlled same-origin proxy. useCORS cannot grant permission that the server did not send.

The file is valid but blank or clipped

Open the local export before upload. Wait for resources, set the element’s scroll dimensions as the html2canvas window dimensions, lower scale, and split very large captures. Also check that the element is visible and has nonzero dimensions at capture time.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Only some images disappear

Those resources are commonly cross-origin, still loading, blocked by authentication, or supplied as unsupported CSS. Test each URL in the browser’s network panel and ensure the final response includes the needed CORS header.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Operational safeguards

  • Set a maximum pixel area and byte size before accepting an upload to limit memory and storage abuse.
  • Use random server-side names, store uploads outside the executable web root when possible, and serve them with an image content type.
  • Do not trust extensions, MIME strings, or dimensions supplied by the browser; inspect the temporary file or decoded bytes.
  • Return short machine-readable errors and log request IDs, detected MIME, byte length, and PHP upload error codes without logging the image itself.
  • For repeated captures, reuse a page and upload connection where practical, but keep each request bounded so one oversized canvas cannot exhaust worker memory.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF; it accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—work with Claude, Cursor, and other MCP clients.

For the full option list and parameter names, see the ScreenshotNeo documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());

Every plan includes all features, including full-page lazy-image loading, CSS-selector element capture, device presets, custom JavaScript and CSS, waits, request blocking, headers and cookies, geolocation, PDFs, caching, signed links, webhooks, bulk capture of 100 URLs per call, and a usage API. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

FAQ

Can I repair a PNG after PHP has altered it?

Usually not reliably. Re-export the canvas and preserve the original binary bytes; repairing a truncated or text-contaminated file can conceal the actual transport defect.

Should I choose JPEG to reduce upload size?

Only when lossy compression is acceptable. PNG is generally safer for text, sharp edges, and transparency; choose the format that matches the image’s visual and alpha requirements.

Why does a cache hit matter when diagnosing an API response?

A cache hit can return a previously captured result rather than exercising the current page. Check the response verdict and billing headers when distinguishing a fresh capture from a cached one.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.