Inline images in formatted labels
Shared planar point symbol layers support ['image', name] sections inside format in text-field. Registered RGBA images and text share wrapping, anchoring, rotation, collision and queries. The capability is commonFormattedTextImages.
import { map } from 'sqsmap';
const rgba = (width, height, color) => ({
width, height,
data: Array.from({ length: width * height }, () => color).flat()
});
map.addImage('badge', rgba(16, 8, [0, 180, 90, 255]), { pixelRatio: 2 });
map.addSource('labels', { type: 'geojson', data: {
type: 'Feature', id: 1, properties: { badge: 'badge' },
geometry: { type: 'Point', coordinates: [116.4, 39.9] }
} });
map.addLayer({ id: 'formatted', type: 'symbol', source: 'labels',
layout: {
'text-size': 24,
'text-field': ['let', 'badge', ['image', ['get', 'badge']],
['format', 'Beijing ', { 'text-color': 'red' },
['coalesce', ['var', 'badge'], 'No image'], { 'vertical-align': 'center' }]]
}
});
await map.sqs.flush();
Image dimensions are their pixel dimensions divided by pixelRatio: a 16×8 image with pixelRatio=2 occupies 8×4 CSS pixels, independently of root text-size. An image section ignores font-scale, text-font and text-color; supplied expressions must still satisfy type and budget checks. vertical-align supports bottom (default), center and top using the row's text em height and actual image height. Images are indivisible wrapping units, and the entire label has one collision/query box.
Images retain their RGBA colors and alpha, multiplied by root text-opacity. Transparent text-color does not hide images. text-opacity: 0 or text-size: 0 hides the entire label. Image-only labels work without adding text glyphs to the glyph atlas.
image checks the current atlas synchronously. It returns a typed image when available and actual null when missing, so coalesce can select another image or plain text. case, match and lexical/lazy let/var preserve image identity. Empty strings and empty formatted values remain nonnull. Image variables cannot be passed to numeric, boolean or ordinary string operations. A top-level text-field: ['image', ...] is rejected; place it inside format. Empty image names are also rejected.
Names encountered while evaluating visible labels use existing styleimagemissing notifications. Subscribe with map.on('styleimagemissing', event => ...); the name is event.id. See missing style images for deduplication, generations and stopping. Adding, updating or removing an image re-evaluates the label on refresh, including null fallback and actual bitmap updates. Image writes validate the label candidate before committing; failures preserve previous images, labels and queries.
This capability covers registered RGBA sections. A selected SDF image fails with CAPABILITY_UNAVAILABLE; unreferenced SDF images remain valid for existing icons. Each label has at most 32 sections and 256 text characters/4096 bytes. Names are limited to 256 bytes, each image side to 4096 CSS pixels; existing image-atlas and display budgets still apply. Line placement, 3D combinations, complex shaping, font files and the full image type system in patterns remain outside this scope. Ordinary icon image values and fallbacks are available.
Semantics follow the MapLibre format/image specification, implemented independently in this project. Full formatted-value or platform compatibility is not claimed.
Copy portable-format-image.js, or use the complete browser page. The same script checks 15 lifecycle/atomic-failure behaviors and actual red text, an 8×4 green image and a 12×12 blue image. Font-scale/text-color options do not alter the blue image size or original color.