#[expect(
clippy::unnecessary_box_returns,
reason = "Diplomat requires `Box<T>` returns for opaque constructors"
)]
#[expect(
clippy::use_self,
reason = "Diplomat expands macros using the concrete type name rather than `Self`."
)]
#[diplomat::bridge]
mod ffi {
use std::fmt::Write as _;
use mlt_core::encoder::{EncoderConfig, WireVersion};
use mlt_core::mvt::{mvt_to_tile_layers, tile_layers_to_mvt};
use mlt_core::{Decoder, Parser};
#[derive(Debug, PartialEq, Eq)]
pub enum ConvertErrorKind {
InvalidInput,
EncodingFailed,
}
#[diplomat::opaque]
#[diplomat::attr(auto, error)]
pub struct ConvertError {
kind: ConvertErrorKind,
message: String,
}
impl ConvertError {
#[diplomat::attr(auto, getter = "kind")]
pub fn kind(&self) -> ConvertErrorKind {
self.kind
}
#[diplomat::attr(auto, getter = "message")]
pub fn message(&self, write: &mut DiplomatWrite) {
let _ = write.write_str(&self.message);
}
#[cfg(test)]
pub(crate) fn message_text(&self) -> &str {
&self.message
}
}
impl ConvertError {
fn new(kind: ConvertErrorKind, source: &impl std::fmt::Display) -> Box<Self> {
Box::new(Self {
kind,
message: source.to_string(),
})
}
}
#[diplomat::opaque]
pub struct MltBuffer(Vec<u8>);
impl MltBuffer {
#[diplomat::attr(auto, getter = "bytes")]
#[expect(
clippy::needless_lifetimes,
reason = "diplomat requires explicit lifetimes"
)]
pub fn as_bytes<'a>(&'a self) -> &'a [u8] {
&self.0
}
#[diplomat::attr(auto, getter = "len")]
pub fn len(&self) -> usize {
self.0.len()
}
}
#[expect(
dead_code,
reason = "Diplomat constructs the variants on the foreign side"
)]
pub enum MltWireVersion {
V01,
V02,
}
#[diplomat::opaque_mut]
pub struct MltEncoderOptions(EncoderConfig);
impl MltEncoderOptions {
#[diplomat::attr(auto, constructor)]
pub fn new() -> Box<MltEncoderOptions> {
Box::new(MltEncoderOptions(EncoderConfig::default()))
}
pub fn set_tessellate(&mut self, enabled: bool) {
self.0 = self.0.with_tessellation(enabled);
}
pub fn set_attempt_spatial_morton_sort(&mut self, enabled: bool) {
self.0 = self.0.with_spatial_morton_sort(enabled);
}
pub fn set_attempt_spatial_hilbert_sort(&mut self, enabled: bool) {
self.0 = self.0.with_spatial_hilbert_sort(enabled);
}
pub fn set_attempt_id_sort(&mut self, enabled: bool) {
self.0 = self.0.with_id_sort(enabled);
}
pub fn set_allow_fsst(&mut self, enabled: bool) {
self.0 = self.0.with_fsst(enabled);
}
pub fn set_allow_fastpfor(&mut self, enabled: bool) {
self.0 = self.0.with_fastpfor(enabled);
}
pub fn set_allow_shared_dict(&mut self, enabled: bool) {
self.0 = self.0.with_shared_dict(enabled);
}
pub fn set_wire_version(&mut self, version: MltWireVersion) {
let version = match version {
MltWireVersion::V01 => WireVersion::V01,
MltWireVersion::V02 => WireVersion::V02,
};
self.0 = self.0.with_wire_version(version);
}
pub fn set_allow_triangles_only(&mut self, enabled: bool) {
self.0 = self.0.with_triangles_only(enabled);
}
pub fn set_allow_delta2(&mut self, enabled: bool) {
self.0 = self.0.with_delta2(enabled);
}
pub fn set_allow_float_dict(&mut self, enabled: bool) {
self.0 = self.0.with_float_dict(enabled);
}
pub fn set_allow_float_alp(&mut self, enabled: bool) {
self.0 = self.0.with_float_alp(enabled);
}
pub fn set_allow_packed_dict_codes(&mut self, enabled: bool) {
self.0 = self.0.with_packed_dict_codes(enabled);
}
pub fn set_allow_rans_vertices(&mut self, enabled: bool) {
self.0 = self.0.with_rans_vertices(enabled);
}
pub fn set_allow_row_shapes(&mut self, enabled: bool) {
self.0 = self.0.with_row_shapes(enabled);
}
}
#[diplomat::opaque]
pub struct MltConverter;
impl MltConverter {
pub fn mlt_to_mvt(mlt: &[u8]) -> Result<Box<MltBuffer>, Box<ConvertError>> {
let layers = Parser::default()
.parse_layers(mlt)
.map_err(|e| ConvertError::new(ConvertErrorKind::InvalidInput, &e))?;
let mut dec = Decoder::default();
let mut tiles = Vec::new();
for layer in layers {
let tile = layer
.into_tile(&mut dec)
.map_err(|e| ConvertError::new(ConvertErrorKind::InvalidInput, &e))?;
tiles.extend(tile);
}
let out = tile_layers_to_mvt(tiles)
.map_err(|e| ConvertError::new(ConvertErrorKind::InvalidInput, &e))?;
Ok(Box::new(MltBuffer(out)))
}
pub fn mvt_to_mlt(
mvt: &[u8],
options: &MltEncoderOptions,
) -> Result<Box<MltBuffer>, Box<ConvertError>> {
let mut out = Vec::new();
let layers = mvt_to_tile_layers(mvt)
.map_err(|e| ConvertError::new(ConvertErrorKind::EncodingFailed, &e))?;
for tile in layers {
let encoded_tile = tile
.encode(options.0)
.map_err(|e| ConvertError::new(ConvertErrorKind::EncodingFailed, &e))?;
out.extend_from_slice(&encoded_tile);
}
Ok(Box::new(MltBuffer(out)))
}
}
}
#[cfg(test)]
mod tests {
use insta::assert_debug_snapshot;
use mlt_core::mvt::mvt_to_tile_layers;
use super::ffi::{ConvertErrorKind, MltConverter, MltEncoderOptions, MltWireVersion};
const POINT: &[u8] = include_bytes!("../../../test/fixtures/simple/point-boolean.mvt");
const POLYGON: &[u8] = include_bytes!("../../../test/fixtures/simple/polygon-boolean.mvt");
const MULTIPOLYGON: &[u8] =
include_bytes!("../../../test/fixtures/simple/multipolygon-boolean.mvt");
#[test]
fn v1_encoding_of_a_point_layer_decodes_to_the_same_layers() {
let options = MltEncoderOptions::new();
let mlt = MltConverter::mvt_to_mlt(POINT, &options).ok().unwrap();
let decoded = MltConverter::mlt_to_mvt(mlt.as_bytes()).ok().unwrap();
assert_eq!(
mvt_to_tile_layers(decoded.as_bytes()).unwrap(),
mvt_to_tile_layers(POINT).unwrap()
);
}
#[test]
fn v1_encoding_of_a_multipolygon_layer_decodes_to_the_same_layers() {
let options = MltEncoderOptions::new();
let mlt = MltConverter::mvt_to_mlt(MULTIPOLYGON, &options)
.ok()
.unwrap();
let decoded = MltConverter::mlt_to_mvt(mlt.as_bytes()).ok().unwrap();
assert_eq!(
mvt_to_tile_layers(decoded.as_bytes()).unwrap(),
mvt_to_tile_layers(MULTIPOLYGON).unwrap()
);
}
#[test]
fn v2_encoding_of_a_multipolygon_layer_decodes_to_the_same_layers() {
let mut options = MltEncoderOptions::new();
options.set_wire_version(MltWireVersion::V02);
let mlt = MltConverter::mvt_to_mlt(MULTIPOLYGON, &options)
.ok()
.unwrap();
let decoded = MltConverter::mlt_to_mvt(mlt.as_bytes()).ok().unwrap();
assert_eq!(
mvt_to_tile_layers(decoded.as_bytes()).unwrap(),
mvt_to_tile_layers(MULTIPOLYGON).unwrap()
);
}
#[test]
fn v2_tessellated_encoding_without_outlines_decodes_a_polygon_as_triangles() {
let mut options = MltEncoderOptions::new();
options.set_wire_version(MltWireVersion::V02);
options.set_tessellate(true);
options.set_allow_triangles_only(true);
let mlt = MltConverter::mvt_to_mlt(POLYGON, &options).ok().unwrap();
let decoded = MltConverter::mlt_to_mvt(mlt.as_bytes()).ok().unwrap();
assert_debug_snapshot!(mvt_to_tile_layers(decoded.as_bytes()).unwrap(), @r#"
[
TileLayer {
name: "layer",
extent: Extent(
4096,
),
property_names: [
"key",
],
property_kinds: [
Bool,
],
m_value_names: [],
m_value_kinds: [],
nested_names: [],
nested_kinds: [],
z_step: None,
features: [
TileFeature {
id: Some(
1,
),
geometry: POLYGON((8 12,20 34,3 6,8 12)),
properties: [
Bool(
Some(
true,
),
),
],
m_values: [],
nested: [],
z: [],
},
],
},
]
"#);
}
#[test]
fn decoding_garbage_as_mlt_reports_invalid_input_with_the_parser_message() {
let error = MltConverter::mlt_to_mvt(&[0xff]).err().unwrap();
assert_eq!(error.kind(), ConvertErrorKind::InvalidInput);
assert_debug_snapshot!(error.message_text(), @r#""buffer underflow: needed 2 bytes, but only 1 remain""#);
}
#[test]
fn encoding_garbage_as_mvt_reports_encoding_failed_with_the_parser_message() {
let error = MltConverter::mvt_to_mlt(&[0xff], &MltEncoderOptions::new())
.err()
.unwrap();
assert_eq!(error.kind(), ConvertErrorKind::EncodingFailed);
assert_debug_snapshot!(error.message_text(), @r#""MVT error: protobuf decode error: unexpected end of buffer""#);
}
}