Load external images
Load external images (10.2b)
await map.loadImage(url) downloads and decodes PNG, JPEG or WebP into {data:{width,height,data:Uint8Array}}. Pass result.data directly to map.addImage. Loading does not register an image, change a style or submit a map write. Pixels belong to the caller; editing them does not change a registered image. The name and {data} response follow MapLibre conventions; SqsMap returns portable RGBA8 pixels instead of DOM image objects.
interface MapImageLoadResult {
readonly data: {readonly width:number; readonly height:number; readonly data:Uint8Array}
}
map.loadImage(url:string):Promise<MapImageLoadResult>
Use an absolute HTTP(S) or hierarchical custom URL with a host, without credentials, at most4096 UTF-8 bytes. HTTP downloads require CORS; custom handlers are host-owned. Format detection uses actual bytes. SVG/GIF are unsupported; EXIF orientation, color-space conversions and animation frames are not implicitly applied. RGBA8 uses straight, unpremultiplied alpha.
Each encoded image is limited to4 MiB,512 pixels per edge and1 MiB decoded pixels; at most8 loads may be active. Browser/native limits are per map; desktop background downloads also have a process-wide8-job limit. Unclaimed completed native results still occupy a slot. Caller-owned pixels retained after claiming do not count as in-flight results. Decoder internals have a separate16 MiB allocation limit.
Errors include INVALID_ARGUMENT (URL), IO_ERROR (HTTP/empty/corrupt response), CAPABILITY_UNAVAILABLE (format), RESOURCE_LIMIT (budget) and CANCELLED (cancellation). Stopping a script or destroying a map cancels its owned download; late responses cannot revive released requests. The standalone browser SDK also provides view.loadImage(url,{signal}) and transformRequest(url,ResourceType.Image); function request hooks require the main-thread host. Common scripts take only a URL, without a DOM AbortSignal.
// @gis-api 0.5-session
// @permissions ["project.read", "map.write"]
import {map} from "sqsmap";
const result=await map.loadImage("https://www.sqsmap.com/sdk/sqsmap-core/load-image-fixtures/icon.png");
map.addImage("marker",result.data);
await map.sqs.flush();
export default {width:result.data.width,height:result.data.height};
The public URL requires deployment of matching attachments; local acceptance uses a local server. The identical four-entry acceptance script explicitly uses a local test URL. The complete HTML/CSS with download/cancel/remove/destroy handlers and attribution events uses adjacent fixtures; copy the complete SDK distribution directory to run it.
The native library contains no HTTP client. sqsm_map_image_json method loadImage([url]) returns a requestId. Poll sqsm_map_resource_requests_json, download resourceType=Image and use the existing resource_response entry; deliver transport failures with error/errorCode. loadImageResult([requestId]) returns pending or owned data; only successful output allocation consumes a result. Failed output preserves the request. Release an error result with cancelLoadImage([requestId]). Capability SQSM_MAP_LOAD_IMAGE=4194304; ABI1 and the export count are unchanged. Retained native scripts wait for this queue; the synchronous one-shot native script entry explicitly rejects external image loading with CAPABILITY_UNAVAILABLE. Desktop ScriptService owns background transport, so its ordinary one-shot scripts can await loading.
Desktop, browser main/Worker and Linux retained native QuickJS are connected. Mobile application scripts, Windows/macOS execution, hardware backends and formal releases retain separate acceptance. Dynamic/stretchable images and complete fonts remain subsequent work.
<!doctype html>
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SqsMap external image</title>
<style>body{margin:0;font:14px system-ui}#map{height:65vh;min-height:240px}.tools{display:flex;gap:8px;padding:12px;flex-wrap:wrap}input{min-width:260px;flex:1}#status,#attribution{padding:8px 12px;white-space:pre-wrap}</style>
<div id="map"></div><div class="tools"><input id="url" aria-label="Image URL"><button id="load" disabled>Load PNG/JPEG/WebP</button><button id="cancel" disabled>Cancel download</button><button id="remove" disabled>Remove image</button><button id="destroy" disabled>Destroy map</button></div><div id="status" role="status">Starting…</div><div id="attribution"></div>
<script type="module">
import {MapView} from './dist/index.js';
const status=document.querySelector('#status'),url=document.querySelector('#url'),load=document.querySelector('#load'),cancel=document.querySelector('#cancel'),remove=document.querySelector('#remove'),destroy=document.querySelector('#destroy');
url.value=new URL('./load-image-fixtures/icon.png',import.meta.url).href;
const view=await MapView.create(document.querySelector('#map'));let alive=true,pending;
const report=error=>{if(alive)status.textContent=(error.code?error.code+': ':'')+String(error.message??error);};
const offAttribution=view.on('sqs.attributionschange',event=>{document.querySelector('#attribution').textContent=event.attributions.text;});
const offImages=view.on('sqs.imageschange',event=>{if(alive)status.textContent='Image '+event.method+': '+event.id;});
view.jumpTo({center:[0,0],zoom:0});view.setStyle(new URL('./missing-image-style.json',import.meta.url).href);
try {
const deadline=Date.now()+15000;while(!(['ready','degraded'].includes(view.basemapStatus.state)&&view.basemapStatus.readyTiles>0)){if(Date.now()>deadline)throw Error('Style did not become ready');await new Promise(r=>setTimeout(r,30));}
document.querySelector('#attribution').textContent=(await view.getAttributions()).text;load.disabled=remove.disabled=destroy.disabled=false;status.textContent='Ready to load an external image';
}catch(error){report(error);destroy.disabled=false;}
load.onclick=async()=>{
if(!alive||pending)return;const controller=new AbortController();pending=controller;load.disabled=true;cancel.disabled=false;status.textContent='Downloading…';
try {const image=await view.loadImage(url.value,{signal:controller.signal});if(!alive||controller.signal.aborted)return;
if(await view.hasImage('marker'))view.removeImage('marker');view.addImage('marker',image.data);await view.flush({render:true});if(alive)status.textContent='Loaded '+image.data.width+' × '+image.data.height;
}catch(error){report(error);}finally{if(pending===controller)pending=undefined;if(alive){load.disabled=false;cancel.disabled=true;}}
};
cancel.onclick=()=>pending?.abort();
remove.onclick=async()=>{try{if(await view.hasImage('marker')){view.removeImage('marker');await view.flush({render:true});}if(alive)status.textContent='Image removed';}catch(error){report(error);}};
destroy.onclick=()=>{alive=false;pending?.abort();offAttribution();offImages();view.destroy();for(const button of [load,cancel,remove,destroy])button.disabled=true;url.disabled=true;status.textContent='Map destroyed';};
window.addEventListener('pagehide',()=>{if(alive)destroy.onclick();},{once:true});
</script></html>
Cancelling a delivered pending native download emits an Image descriptor with cancel:true. The host must abort its owned I/O for that generation. Unclaimed cancellation notices retain one slot until successfully delivered, bounding the notification queue.
See custom protocol image loading for URI support and host requirements.