Skip to main content

Sprite Visible, Canvas Export Blocked? Test CORS

Updated 6 min readMarkdown
Sprite Visible, Canvas Export Blocked? Test CORS

A sprite can be visible in a browser while remaining unavailable for canvas export. Test three separate outcomes: the image loads, a fresh canvas can read its pixels, and the encoder returns a non-null Blob. Fix the first failed stage instead of repeatedly changing the PNG or renaming a failed download.

Key Takeaways

  • Image display does not prove permission to read or export pixels.
  • Configure the image request and the image server together.
  • Keep a same-origin control and test each input before composing the final sheet.

This walkthrough is for developers who control a browser-based sprite reviewer or exporter. Use a disposable page on your normal development origin and a small image you own. Never test against private images or try to disable the browser's security checks.

Locate the first failed boundary

Write down the page origin and the final image URL, including redirects. Then give each observation its own result column. An HTTP success in DevTools is useful network evidence; it is not an export result.

Check Passing observation A failure directs you toward
Image load Load completes with nonzero intrinsic dimensions URL, server response, decoding or request policy
Pixel readback Reading one pixel succeeds on a fresh canvas Origin-clean permission for the drawn inputs
Encoding Callback receives a non-null Blob Encoder failure, invalid canvas or export handling
Download Saved bytes have the intended format Filename, MIME and download code

The HTML canvas security model separates displaying foreign content from reading it back. Drawing an input without the required permission can contaminate a larger composition too. A clean background does not cancel a problematic foreground.

Make a fresh, permission-aware request

The following browser example uses anonymous CORS, sets the request mode before assigning the URL, and treats load and encoding errors separately. It is an illustrative implementation, not a report of testing your CDN or browser combination.

async function spritePng(url, maxPixels = 8_388_608) {
  if (!Number.isSafeInteger(maxPixels) || maxPixels < 1) {
    throw new Error('Invalid export pixel budget');
  }
  const image = new Image();
  image.crossOrigin = 'anonymous';
  await new Promise((resolve, reject) => {
    image.onload = resolve;
    image.onerror = () => reject(new Error('Image request or decode failed'));
    image.src = url;
  });
  if (!image.naturalWidth || !image.naturalHeight) {
    throw new Error('Image has no decoded dimensions');
  }
  if (image.naturalWidth > maxPixels / image.naturalHeight) {
    throw new Error('Image exceeds the configured export pixel budget');
  }
  const canvas = document.createElement('canvas');
  canvas.width = image.naturalWidth;
  canvas.height = image.naturalHeight;
  const context = canvas.getContext('2d');
  if (!context) throw new Error('Canvas 2D unavailable');
  context.drawImage(image, 0, 0);
  context.getImageData(0, 0, 1, 1); // May throw SecurityError.
  return await new Promise((resolve, reject) => {
    try {
      canvas.toBlob(blob => blob ? resolve(blob) :
        reject(new Error('PNG encoder returned null')), 'image/png');
    } catch (error) { reject(error); }
  });
}

The pixel cap is an example application policy, not a browser capacity claim. Set it from your own resource budget; the image may already consume decode memory before this canvas-allocation check. A production loader also needs cancellation and a request deadline. The server still has to approve the request. MDN's cross-origin canvas guide explains that the image's crossorigin setting works with the response's CORS headers. A client attribute alone cannot grant access. Anonymous mode also is not a substitute for designing an authenticated image-delivery flow.

The toBlob() reference documents both a possible null callback value and a SecurityError for a canvas that is not origin-clean. Preserve those distinctions in your error message. “Export failed” hides the information needed to repair the request.

Run a two-origin, four-case acceptance test

Prepare identical small artwork at three controlled URLs: same origin, another origin with CORS approval, and another origin without it. For the approved anonymous case, the remote server can return Access-Control-Allow-Origin matching the page origin, or * for a public, non-credentialed image. Keep authenticated image handling separate. Use a new image and canvas for every case.

Controlled case Image request Response CORS approval Expected load Expected fresh-canvas export
Same origin Ordinary image Not needed for this same-origin control Succeeds Succeeds
Other origin Ordinary image, no crossOrigin Absent Can succeed SecurityError after drawing
Other origin crossOrigin = 'anonymous' Absent Fails CORS approval Not attempted
Other origin crossOrigin = 'anonymous' Permits this page Succeeds Succeeds

The table assumes valid image bytes, accessible URLs and no separate content-security policy restriction. A network or decoding failure can prevent any case from reaching its intended permission check.

For the same-origin control, require a non-null PNG Blob and a decoded saved image of the original dimensions. For the approved remote URL, require the same result. With this anonymous-CORS loader, the unapproved remote URL should fail to load; record that as a request-policy failure. A separate plain image element might display that URL, but drawing that plain image to canvas does not establish readback permission.

Next, compose the intended atlas one input at a time. If every isolated input passes but the complete atlas fails, inspect additional overlays, fonts-as-raster inputs and copied canvases used only by the composition path. Keep the first failing input's URL in the diagnostic, with credentials and sensitive query parameters removed.

Repair the responsible layer

For an owned CDN, repair its image response policy and repeat the approved-remote test. For a third-party host that does not grant pixel access, request a permitted download or let the user select a local file they have permission to use. Do not route arbitrary private URLs through a new proxy.

Changing fetch() to no-cors does not produce readable image bytes; see the separate opaque-response and empty-Blob explanation. If readback works but the saved extension is wrong, follow the encoder MIME check. If the file is correct but its name cannot be read from a response header, inspect CORS header exposure.

The existing tainted-canvas reference explains the underlying rule. This matrix adds a reproducible comparison of request mode and response approval. Passing it proves the tested request and export path worked for those inputs. It does not prove every browser, redirected URL, signed-link lifetime or production policy is equivalent.

Evidence and editorial responsibility

Prepared by FrameSprite Engineering using primary browser documentation and an illustrative diagnostic procedure. This is AI-assisted editorial content from a team with a commercial interest in FrameSprite. No production CDN or cross-browser test is claimed. Editorial ownership and contact.

About the author

  • FrameSprite team publication: technical guides for image processing, sprite workflows and game development. Our AI-generated guides identify their sources, executed synthetic fixtures and testing limits. Original guides: https://www.framesprite.com/guides

No comments yet. Be the first to comment.