Skip to content

Render images

This guide introduces how to render still images of a map. Start with a runtime from Create a map.

Create a map with MLN_MAP_MODE_STATICMapMode.staticMapMode.StaticMapModeStaticMAPLIBRE_NATIVE_FFI_MAP_MODE_STATICMapMode.STATICMapMode.STATICMapMode.STATICMapMode::StaticMapMode.staticMapMode.staticMapMode.static, at the image size you want in logical pixels. For a higher-resolution image, raise the scale factor: a scale factor of 2 doubles the image’s width and height in pixels. Then set its style, as in Create a map.

main.c
mln_map_options map_options = {
.size = sizeof(mln_map_options),
.initial_extent =
{.width = 800, .height = 600, .scale_factor = 2.0},
.map_mode = MLN_MAP_MODE_STATIC,
};
mln_map map = MLN_HANDLE_NULL;
mln_runtime_create_map(runtime, &map_options, &map, &done, NULL);
main.dart
final map = runtime.createMap(
const MapOptions(
initialExtent: LogicalExtent(
width: 800,
height: 600,
scaleFactor: 2,
),
mapMode: MapMode.static,
),
);
await map.ready;
Program.cs
var map = runtime.CreateMap(
new MapOptions
{
InitialExtent = new LogicalExtent(800, 600, 2),
MapMode = MapMode.Static,
}
);
await map.Ready;
main.go
m, created, err := runtime.CreateMap(maplibre.MapOptions{
InitialExtent: maplibre.LogicalExtent{
Width: 800, Height: 600, ScaleFactor: 2,
},
MapMode: maplibre.MapModeStatic,
})
if err != nil {
return err
}
if _, err := created.Await(ctx); err != nil {
return err
}
main.vala
var options = new MapOptions () {
initial_extent = { width: 800, height: 600, scale_factor: 2.0 },
map_mode = MapMode.STATIC,
};
var map = runtime.create_map (options);
yield map.ready_async ();
Main.java
MapHandle map =
runtime.createMap(
MapOptions.of(LogicalExtent.of(800, 600, 2.0))
.withMapMode(MapMode.STATIC));
map.getReady().join();
Main.kt
val map =
runtime.createMap(
MapOptions(
initialExtent =
LogicalExtent(
width = 800u,
height = 600u,
scaleFactor = 2.0,
),
mapMode = MapMode.STATIC,
),
)
map.ready.await()
main.py
map_handle = runtime.create_map(
mln.MapOptions(
initial_extent=mln.LogicalExtent(
width=800, height=600, scale_factor=2.0
),
map_mode=mln.MapMode.STATIC,
)
)
map_handle.ready.result()
main.rs
let map_options = MapOptions {
map_mode: MapMode::Static,
..MapOptions::new(LogicalExtent::new(800, 600, 2.0))
};
let (map, created) = runtime.create_map(&map_options)?;
created.await?;
main.swift
let map = try runtime.createMap(
options: MapOptions(
initialExtent: LogicalExtent(
width: 800,
height: 600,
scaleFactor: 2
),
mapMode: .static
)
)
try await map.ready
main.ts
const map = runtime.createMap({
initialExtent: { width: 800, height: 600, scaleFactor: 2 },
mapMode: MapMode.static,
});
await map.ready;
main.zig
const map_options = maplibre.MapOptions{
.initial_extent = .{
.width = 800,
.height = 600,
.scale_factor = 2.0,
},
.map_mode = .static,
};
const created = try runtime.createMap(map_options, null);
var map = created.map;
try created.ready.await(null);

Attach an offscreen render session to the map with mln_map_attach_offscreenMapHandle.attachOffscreenMapHandle.AttachOffscreenMapHandle.AttachOffscreenmaplibre_native_ffi_map_attach_offscreenMapHandle.attachOffscreenMapHandle.attachOffscreenMapHandle.attach_offscreenMapHandle::attach_offscreenMapHandle.attachOffscreenMapHandle.attachOffscreenMap.attachOffscreen, at the map’s size. It renders on a GPU device that the library creates, so your app doesn’t need a graphics context. Use the render session it returns.

To share one device among the sessions of many maps, or to pick a GPU, create an mln_offscreen_deviceOffscreenDeviceHandleOffscreenDeviceHandleOffscreenDeviceHandleMaplibreNativeFfiOffscreenDeviceOffscreenDeviceHandleOffscreenDeviceHandleOffscreenDeviceHandleOffscreenDeviceHandleOffscreenDeviceHandleOffscreenDeviceHandleOffscreenDevice and pass it when you attach.

main.c
mln_offscreen_descriptor offscreen = {
.size = sizeof(mln_offscreen_descriptor),
.logical_size = {.width = 800, .height = 600},
};
mln_render_session_attach_options attach_options = {
.size = sizeof(mln_render_session_attach_options),
// The library renders on its own thread.
.driver = MLN_RENDER_DRIVER_CORE_WORKER,
};
// With no device, the session creates its own.
mln_render_session session = MLN_HANDLE_NULL;
mln_map_attach_offscreen(
map, &offscreen, &attach_options, MLN_HANDLE_NULL, &session,
&done, NULL
);
main.dart
final session = map.attachOffscreen(
const OffscreenDescriptor(
logicalSize: LogicalSize(width: 800, height: 600),
),
// The library renders on its own thread.
const RenderSessionAttachOptions(
driver: RenderDriverKind.coreWorker,
),
);
await session.ready;
Program.cs
var session = map.AttachOffscreen(
new OffscreenDescriptor(new LogicalSize(800, 600)),
// The library renders on its own thread.
new RenderSessionAttachOptions
{
Driver = RenderDriverKind.CoreWorker,
}
);
await session.Ready;
main.go
session, attached, err := m.AttachOffscreen(
maplibre.OffscreenDescriptor{
LogicalSize: maplibre.LogicalSize{Width: 800, Height: 600},
},
// The library renders on its own thread.
maplibre.RenderSessionAttachOptions{
Driver: maplibre.RenderDriverKindCoreWorker,
},
// With no device, the session creates its own.
nil,
)
if err != nil {
return err
}
if _, err := attached.Await(ctx); err != nil {
return err
}
main.vala
var attach = new RenderSessionAttachOptions () {
// The library renders on its own thread.
driver = RenderDriverKind.CORE_WORKER,
};
var session = map.attach_offscreen (
{ logical_size: { width: 800, height: 600 } }, attach, null
);
yield session.ready_async ();
Main.java
RenderSessionHandle session =
map.attachOffscreen(
new OffscreenDescriptor(LogicalSize.of(800, 600)),
// The library renders on its own thread.
RenderSessionAttachOptions.of(
RenderDriverKind.CORE_WORKER),
null);
session.getReady().join();
Main.kt
val session =
map.attachOffscreen(
OffscreenDescriptor(LogicalSize(width = 800u, height = 600u)),
// The library renders on its own thread.
RenderSessionAttachOptions(
driver = RenderDriverKind.CORE_WORKER,
),
)
session.ready.await()
main.py
session = map_handle.attach_offscreen(
mln.OffscreenDescriptor(
logical_size=mln.LogicalSize(width=800, height=600)
),
# The library renders on its own thread.
mln.RenderSessionAttachOptions(
driver=mln.RenderDriverKind.CORE_WORKER
),
)
session.ready.result()
main.rs
let offscreen =
OffscreenDescriptor::new(LogicalSize::new(800, 600));
// The library renders on its own thread.
let attach_options =
RenderSessionAttachOptions::new(RenderDriverKind::CoreWorker);
// With no device, the session creates its own.
let (session, attached) =
map.attach_offscreen(&offscreen, &attach_options, None)?;
attached.await?;
main.swift
let session = try map.attachOffscreen(
descriptor: OffscreenDescriptor(
logicalSize: LogicalSize(width: 800, height: 600)
),
// The library renders on its own thread.
options: RenderSessionAttachOptions(driver: .coreWorker)
)
try await session.ready
main.ts
const session = map.attachOffscreen(
{ logicalSize: { width: 800, height: 600 } },
// The library renders on its own thread.
{ driver: RenderDriverKind.coreWorker },
);
await session.ready;
main.zig
const attached = try map.attachOffscreen(
.{ .logical_size = .{ .width = 800, .height = 600 } },
// The library renders on its own thread.
.{ .driver = .core_worker },
// With no device, the session creates its own.
null,
null,
);
var session = attached.session;
try attached.ready.await(null);

A static map renders only when you request an image. Jump the camera to the view you want, as in Move the camera, then request a still image with mln_map_request_still_imageMapHandle.requestStillImageMapHandle.RequestStillImageAsyncMapHandle.RequestStillImagemaplibre_native_ffi_map_request_still_image_asyncMapHandle.requestStillImageMapHandle.requestStillImageMapHandle.request_still_imageMapHandle::request_still_imageMapHandle.requestStillImageMapHandle.requestStillImageMap.requestStillImage.

To change the image size, resize the render session with mln_render_session_resizeRenderSessionHandle.resizeRenderSessionHandle.ResizeAsyncRenderSessionHandle.Resizemaplibre_native_ffi_render_session_resize_asyncRenderSessionHandle.resizeRenderSessionHandle.resizeRenderSessionHandle.resizeRenderSessionHandle::resizeRenderSessionHandle.resizeRenderSessionHandle.resizeRenderSession.resize, and it resizes the map too.

main.c
mln_camera_update update = {
.size = sizeof(mln_camera_update),
.mode = MLN_CAMERA_UPDATE_MODE_JUMP,
.camera = {
.size = sizeof(mln_camera_options),
.fields = MLN_CAMERA_OPTION_CENTER | MLN_CAMERA_OPTION_ZOOM,
.center = {.latitude = 48.8566, .longitude = 2.3522},
.zoom = 4,
},
};
mln_map_update_camera(map, &update, NULL, NULL);
mln_map_request_still_image(map, &done, NULL);
main.dart
map.discarding.updateCamera(
const CameraUpdate(
mode: CameraUpdateMode.jump,
camera: CameraOptions(
center: LatLng(latitude: 48.8566, longitude: 2.3522),
zoom: 4,
),
),
);
await map.requestStillImage();
Program.cs
map.Discarding.UpdateCamera(
new CameraUpdate
{
Mode = CameraUpdateMode.Jump,
Camera = new CameraOptions
{
Center = new LatLng(48.8566, 2.3522),
Zoom = 4,
},
}
);
await map.RequestStillImageAsync();
main.go
err = m.Discarding().UpdateCamera(maplibre.CameraUpdate{
Mode: maplibre.CameraUpdateModeJump,
Camera: maplibre.CameraOptions{
Center: &maplibre.LatLng{Latitude: 48.8566, Longitude: 2.3522},
Zoom: new(4.0),
},
})
if err != nil {
return err
}
rendered, err := m.RequestStillImage()
if err != nil {
return err
}
if _, err := rendered.Await(ctx); err != nil {
return err
}
main.vala
var camera = new CameraOptions () {
center = { latitude: 48.8566, longitude: 2.3522 },
zoom = 4,
};
var update = new CameraUpdate () {
mode = CameraUpdateMode.JUMP,
camera = camera,
};
map.update_camera_async.begin (update);
yield map.request_still_image_async ();
Main.java
map.getDiscarding()
.updateCamera(
CameraUpdate.of()
.withMode(CameraUpdateMode.JUMP)
.withCamera(
CameraOptions.of()
.withCenter(new LatLng(48.8566, 2.3522))
.withZoom(4.0)));
map.requestStillImage().join();
Main.kt
map.discarding.updateCamera(
CameraUpdate(
mode = CameraUpdateMode.JUMP,
camera =
CameraOptions(
center = LatLng(latitude = 48.8566, longitude = 2.3522),
zoom = 4.0,
),
),
)
map.requestStillImage().await()
main.py
map_handle.discarding.update_camera(
mln.CameraUpdate(
mode=mln.CameraUpdateMode.JUMP,
camera=mln.CameraOptions(
center=mln.LatLng(latitude=48.8566, longitude=2.3522),
zoom=4,
),
)
)
map_handle.request_still_image().result()
main.rs
map.discarding().update_camera(&CameraUpdate {
mode: CameraUpdateMode::Jump,
camera: CameraOptions {
center: Some(LatLng::new(48.8566, 2.3522)),
zoom: Some(4.0),
..Default::default()
},
..Default::default()
})?;
map.request_still_image()?.await?;
main.swift
try map.discarding.updateCamera(CameraUpdate(
mode: .jump,
camera: CameraOptions(
center: LatLng(latitude: 48.8566, longitude: 2.3522),
zoom: 4
)
))
try await map.requestStillImage()
main.ts
map.discarding.updateCamera({
mode: CameraUpdateMode.jump,
camera: {
center: { latitude: 48.8566, longitude: 2.3522 },
zoom: 4,
},
});
await map.requestStillImage();
main.zig
try map.discarding().updateCamera(.{
.mode = .jump,
.camera = .{
.center = .{ .latitude = 48.8566, .longitude = 2.3522 },
.zoom = 4,
},
}, null);
try (try map.requestStillImage(null)).await(null);

Read the image’s pixels with mln_render_session_read_textureRenderSessionHandle.readTextureRenderSessionHandle.ReadTextureAsyncRenderSessionHandle.ReadTexturemaplibre_native_ffi_render_session_read_texture_asyncRenderSessionHandle.readTextureRenderSessionHandle.readTextureRenderSessionHandle.read_textureRenderSessionHandle::read_textureRenderSessionHandle.readTextureRenderSessionHandle.readTextureRenderSession.readTexture.

To use the images on the GPU instead, see Render to a texture.

main.c
static void on_pixels(
void* user_data, const mln_completion_result* result
) {
if (result->status == MLN_STATUS_OK) {
const mln_texture_readback_result* readback = result->value;
// readback->data is valid only during this callback.
copy_pixels(readback->data, readback->info.stride);
}
on_done(user_data, result);
}
const mln_completion read = {
.size = sizeof(mln_completion), .callback = on_pixels
};
mln_render_session_read_texture(session, &read, NULL);
main.dart
final image = await session.readTexture();
copyPixels(image.data, image.info.stride);
Program.cs
var image = await session.ReadTextureAsync();
CopyPixels(image.Data, image.Info.Stride);
main.go
read, err := session.ReadTexture()
if err != nil {
return err
}
image, err := read.Await(ctx)
if err != nil {
return err
}
copyPixels(image.Data, image.Info.Stride)
main.vala
var image = yield session.read_texture_async ();
copy_pixels (image.get_data (), image.get_info ().stride);
Main.java
TextureReadbackResult image = session.readTexture().join();
copyPixels(image.getData(), image.getInfo().getStride());
Main.kt
val image = session.readTexture().await()
copyPixels(image.data, image.info.stride)
main.py
image = session.read_texture().result()
copy_pixels(image.data, image.info.stride)
main.rs
let image = session.read_texture()?.await?;
copy_pixels(&image.data, image.info.stride);
main.swift
let image = try await session.readTexture()
copyPixels(image.data, stride: image.info.stride)
main.ts
const image = await session.readTexture();
copyPixels(image.data, image.info.stride);
main.zig
var image = try (try session.readTexture(allocator, null)).await(null);
defer image.deinit();
copyPixels(image.value.data, image.value.info.stride);

For map tiles, such as on a tile server, create the map with MLN_MAP_MODE_TILEMapMode.tileMapMode.TileMapModeTileMAPLIBRE_NATIVE_FFI_MAP_MODE_TILEMapMode.TILEMapMode.TILEMapMode.TILEMapMode::TileMapMode.tileMapMode.tileMapMode.tile instead. Make it square, at the tile size that your map clients expect, such as 256 or 512. For @2x tiles, set the scale factor to 2 and keep the size.

To render tile z/x/y, compute its camera for the map’s size with mln_camera_for_tilecameraForTileMaplibreNative.CameraForTileCameraForTilemaplibre_native_ffi_camera_for_tilecameraForTilecameraForTilecamera_for_tilecamera_for_tilecameraForTilecameraForTilecameraForTile, and jump to it.

main.c
mln_map_options map_options = {
.size = sizeof(mln_map_options),
.initial_extent =
{.width = 512, .height = 512, .scale_factor = 1.0},
.map_mode = MLN_MAP_MODE_TILE,
};
mln_map map = MLN_HANDLE_NULL;
mln_runtime_create_map(runtime, &map_options, &map, &done, NULL);
mln_camera_update update = {
.size = sizeof(mln_camera_update),
.mode = MLN_CAMERA_UPDATE_MODE_JUMP,
.camera = {.size = sizeof(mln_camera_options)},
};
mln_camera_for_tile(
(mln_canonical_tile_id){.z = z, .x = x, .y = y},
(mln_logical_size){.width = 512, .height = 512}, &update.camera,
NULL
);
mln_map_update_camera(map, &update, NULL, NULL);
main.dart
final map = runtime.createMap(
const MapOptions(
initialExtent: LogicalExtent(
width: 512,
height: 512,
scaleFactor: 1,
),
mapMode: MapMode.tile,
),
);
await map.ready;
map.discarding.updateCamera(
CameraUpdate(
mode: CameraUpdateMode.jump,
camera: cameraForTile(
CanonicalTileId(z: z, x: x, y: y),
const LogicalSize(width: 512, height: 512),
),
),
);
Program.cs
var map = runtime.CreateMap(
new MapOptions
{
InitialExtent = new LogicalExtent(512, 512, 1),
MapMode = MapMode.Tile,
}
);
await map.Ready;
map.Discarding.UpdateCamera(
new CameraUpdate
{
Mode = CameraUpdateMode.Jump,
Camera = MaplibreNative.CameraForTile(
new CanonicalTileId(z, x, y),
new LogicalSize(512, 512)
),
}
);
main.go
m, created, err := runtime.CreateMap(maplibre.MapOptions{
InitialExtent: maplibre.LogicalExtent{
Width: 512, Height: 512, ScaleFactor: 1,
},
MapMode: maplibre.MapModeTile,
})
if err != nil {
return err
}
if _, err := created.Await(ctx); err != nil {
return err
}
camera, err := maplibre.CameraForTile(
maplibre.CanonicalTileID{Z: z, X: x, Y: y},
maplibre.LogicalSize{Width: 512, Height: 512},
)
if err != nil {
return err
}
err = m.Discarding().UpdateCamera(maplibre.CameraUpdate{
Mode: maplibre.CameraUpdateModeJump,
Camera: camera,
})
if err != nil {
return err
}
main.vala
var options = new MapOptions () {
initial_extent = { width: 512, height: 512, scale_factor: 1.0 },
map_mode = MapMode.TILE,
};
var map = runtime.create_map (options);
yield map.ready_async ();
var update = new CameraUpdate () {
mode = CameraUpdateMode.JUMP,
camera = Camera.for_tile ({ z, x, y }, { width: 512, height: 512 }),
};
map.update_camera_async.begin (update);
Main.java
MapHandle map =
runtime.createMap(
MapOptions.of(LogicalExtent.of(512, 512, 1.0))
.withMapMode(MapMode.TILE));
map.getReady().join();
map.getDiscarding()
.updateCamera(
CameraUpdate.of()
.withMode(CameraUpdateMode.JUMP)
.withCamera(
MaplibreNative.cameraForTile(
CanonicalTileId.of(z, x, y),
LogicalSize.of(512, 512))));
Main.kt
val map =
runtime.createMap(
MapOptions(
initialExtent =
LogicalExtent(
width = 512u,
height = 512u,
scaleFactor = 1.0,
),
mapMode = MapMode.TILE,
),
)
map.ready.await()
map.discarding.updateCamera(
CameraUpdate(
mode = CameraUpdateMode.JUMP,
camera =
cameraForTile(
CanonicalTileId(z = z, x = x, y = y),
LogicalSize(width = 512u, height = 512u),
),
),
)
main.py
map_handle = runtime.create_map(
mln.MapOptions(
initial_extent=mln.LogicalExtent(
width=512, height=512, scale_factor=1.0
),
map_mode=mln.MapMode.TILE,
)
)
map_handle.ready.result()
map_handle.discarding.update_camera(
mln.CameraUpdate(
mode=mln.CameraUpdateMode.JUMP,
camera=mln.camera_for_tile(
mln.CanonicalTileId(z=z, x=x, y=y),
mln.LogicalSize(width=512, height=512),
),
)
)
main.rs
let map_options = MapOptions {
map_mode: MapMode::Tile,
..MapOptions::new(LogicalExtent::new(512, 512, 1.0))
};
let (map, created) = runtime.create_map(&map_options)?;
created.await?;
map.discarding().update_camera(&CameraUpdate {
mode: CameraUpdateMode::Jump,
camera: camera_for_tile(
CanonicalTileId::new(z, x, y),
LogicalSize::new(512, 512),
)?,
..Default::default()
})?;
main.swift
let map = try runtime.createMap(
options: MapOptions(
initialExtent: LogicalExtent(
width: 512,
height: 512,
scaleFactor: 1
),
mapMode: .tile
)
)
try await map.ready
try map.discarding.updateCamera(CameraUpdate(
mode: .jump,
camera: cameraForTile(
CanonicalTileId(z: z, x: x, y: y),
logicalSize: LogicalSize(width: 512, height: 512)
)
))
main.ts
const map = runtime.createMap({
initialExtent: { width: 512, height: 512, scaleFactor: 1 },
mapMode: MapMode.tile,
});
await map.ready;
map.discarding.updateCamera({
mode: CameraUpdateMode.jump,
camera: cameraForTile({ z, x, y }, { width: 512, height: 512 }),
});
main.zig
const created = try runtime.createMap(.{
.initial_extent = .{
.width = 512,
.height = 512,
.scale_factor = 1.0,
},
.map_mode = .tile,
}, null);
var map = created.map;
try created.ready.await(null);
try map.discarding().updateCamera(.{
.mode = .jump,
.camera = try maplibre.cameraForTile(
.{ .z = z, .x = x, .y = y },
.{ .width = 512, .height = 512 },
null,
),
}, null);