Skip to content

feat: support updating an ImageSource with a decoded image#7944

Open
mondsichtung wants to merge 9 commits into
maplibre:mainfrom
mondsichtung:feat/image-source-image-option
Open

feat: support updating an ImageSource with a decoded image#7944
mondsichtung wants to merge 9 commits into
maplibre:mainfrom
mondsichtung:feat/image-source-image-option

Conversation

@mondsichtung

@mondsichtung mondsichtung commented Jul 14, 2026

Copy link
Copy Markdown

Summary

ImageSource.updateImage() can now take an already-decoded image directly, as an alternative to a url. When you pass image, no network request is made and the image is used as-is.

const bitmap = await createImageBitmap(myCanvas);
const source = map.getSource('overlay') as ImageSource;

// before: only a URL was accepted, forcing a round-trip through a blob/data URL
source.updateImage({ url: URL.createObjectURL(await myCanvas.convertToBlob()) });

// now: hand the decoded image straight to the source
source.updateImage({ image: bitmap });

Motivation

Callers that already hold a decoded image (an ImageBitmap from OffscreenCanvas/a worker, an HTMLCanvasElement they render into, procedurally generated or GPU read-back pixels as ImageData, etc.) currently have to serialize it to a blob or data URL just so ImageSource can fetch and decode it again. That round-trip is wasteful and, in practice, has pushed apps to reach into ImageSource's private fields (image, texture, _request) to inject the bitmap themselves. This makes that a supported, first-class operation.

It also mirrors an API that already exists for sprite images: Map.updateImage(id, image) accepts HTMLImageElement | ImageData | ImageBitmap. This brings the same convenience, and the same set of decoded image types, to the geo-positioned ImageSource.

Changes

  • UpdateImageOptions gains an image?: HTMLImageElement | HTMLCanvasElement | ImageBitmap | ImageData field; url is now optional. If both are given, image wins.
  • updateImage() aborts any in-flight request, sets the image directly, updates coordinates if provided, resets the texture so it re-uploads on the next prepare(), and fires the usual data/metadata event, reusing the exact texture lifecycle of the existing URL path.
  • New exported type ImageSourceImage; the public ImageSource.image field type is widened to it (a superset, backward compatible).

Public API changes

  • UpdateImageOptions.url is now optional.
  • UpdateImageOptions.image added (HTMLImageElement | HTMLCanvasElement | ImageBitmap | ImageData).
  • New exported type ImageSourceImage.
  • ImageSource.image field type widened.

All are additive / widening, so existing updateImage({ url }) callers are unaffected. Documented via TSDoc (rendered by typedoc), including an updated ImageSource example.

Tests

Added to src/source/image_source.test.ts (updateImage with a decoded image): sets the image without a network request, resets the texture, updates coordinates alongside the image, fires the metadata data event, cancels a pending request, accepts an ImageData instance, prefers image over url, and no-ops on empty options. Full unit file: 27/27 passing; eslint and typecheck clean.

Launch Checklist

  • Confirm your changes do not include backports from Mapbox projects (unless with compliant license) - if you are not sure about this, please ask!
  • Briefly describe the changes in this PR.
  • Link to related issues.
  • Include before/after visuals or gifs if this PR includes visual changes.
  • Write tests for all new functionality.
  • Document any changes to public APIs.
  • Post benchmark scores.
  • Add an entry to CHANGELOG.md under the ## main section.
  • Confirm you have read our AI policy here.

Generated-By: Claude Opus 4.8 xhigh

ImageSource.updateImage() now accepts an already-decoded image
(HTMLImageElement, HTMLCanvasElement or ImageBitmap) via a new `image`
option, as an alternative to `url`. When provided, the image is used
directly without a network request. `url` is now optional and `image`
takes precedence when both are given.
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.26%. Comparing base (4435459) to head (c41d999).
⚠️ Report is 24 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7944      +/-   ##
==========================================
+ Coverage   93.25%   93.26%   +0.01%     
==========================================
  Files         290      290              
  Lines       24650    24655       +5     
  Branches     6471     6472       +1     
==========================================
+ Hits        22987    22995       +8     
+ Misses       1663     1660       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mondsichtung
mondsichtung force-pushed the feat/image-source-image-option branch from 97bcb85 to 9e58afe Compare July 14, 2026 18:20
Comment thread src/source/image_source.ts Outdated
Comment thread src/source/image_source.test.ts Outdated
Comment thread src/source/image_source.test.ts Outdated
Express UpdateImageOptions as a discriminated union of `{url}` or `{image}`
instead of two optional fields, so the "provide exactly one" contract is
enforced at the type level. Passing both or neither is now a compile error;
the runtime guard is kept for untyped callers.
Move the duplicated source/map setup in the "updateImage with a decoded
image" tests into a beforeEach. Stop relying on sleep(0): onAdd starts the
initial load synchronously, so request cancellation is asserted on the
AbortController directly instead of waiting for the network request to be
dispatched.

@CommanderStorm CommanderStorm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change LGTM from my side, I have a few minor nits left.

Comment thread src/source/image_source.ts Outdated
Comment thread src/source/image_source.ts Outdated
Comment thread src/source/image_source.test.ts Outdated
Comment thread src/source/image_source.test.ts Outdated
mondsichtung and others added 5 commits July 14, 2026 21:31
Co-authored-by: Frank Elsinga <frank@elsinga.de>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
Give StubMap an injectable transformRequest and assert on that mock, so the
ImageSource tests exercise the public request-transform path instead of
reaching into the internal _requestManager.
The simplified UpdateImageOptions union (url | image, without never-guards)
no longer permits reading url/image off the raw union, which broke the
typecheck. Narrow with `'image' in options` and drop the now-impossible
"neither provided" guard.
* @param options - The options object.
*/
updateImage(options: UpdateImageOptions): this {
if (!options.url) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test still needs to be executed below the "if image" part, right?

return new ImageSource('id', options, {} as any, options.eventedParent);
}

class StubMap extends Evented {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this use the util's createmap method and avoid creating another type of stub here?

expect(source.serialize().coordinates).toEqual([[0, 0], [-1, 0], [-1, -1], [0, -1]]);
});

test('fires a metadata data event', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test can be collapsed to a different test.

@HarelM

HarelM commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Thanks! Looks good overall.
I've added some minor comments.
Can be merged otherwise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants