Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sdf sprites via additional apis #1492

Merged
merged 18 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion docs/src/sources-sprites.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## Sprite Sources

Given a directory with SVG images, Martin will generate a sprite -- a JSON index and a PNG image, for both low and highresolution displays. The SVG filenames without extension will be used as the sprites' image IDs (remember that one sprite and thus `sprite_id` contains multiple images).
Given a directory with SVG images, Martin will generate a sprite -- a JSON index and a PNG image, for both low and highresolution displays.
The SVG filenames without extension will be used as the sprites' image IDs (remember that one sprite and thus `sprite_id` contains multiple images).
The images are searched recursively in the given directory, so subdirectory names will be used as prefixes for the image IDs.
For example `icons/bicycle.svg` will be available as `icons/bicycle` sprite image.

Expand Down Expand Up @@ -40,6 +41,19 @@ the PNG, there is a high DPI version available at `/sprite/<sprite_id>@2x.json`.
}
```

##### Coloring at runtime via Signed Distance Fields (SDFs)
CommanderStorm marked this conversation as resolved.
Show resolved Hide resolved

If you want to set the color of a sprite at runtime, you will need use the [Signed Distance Fields (SDFs)](https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf)-endpoints.
For example, maplibre does support the image being modified via the [`icon-color`](https://maplibre.org/maplibre-style-spec/layers/#icon-color) and [`icon-halo-color`](https://maplibre.org/maplibre-style-spec/layers/#icon-halo-color) properties if using SDFs.

SDFs have the significant **downside of only allowing one color**.
If you want multiple colors, you will need to layer icons on top of each other.

The following APIs are available:

- `/sdf_sprite/<sprite_id>.json` for getting a sprite index as SDF and
- `/sdf_sprite/<sprite_id>.png` for getting sprite PNGs as SDF

#### Combining Multiple Sprites

Multiple `sprite_id` values can be combined into one sprite with the same pattern as for tile
Expand Down
25 changes: 13 additions & 12 deletions docs/src/using.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@

Martin data is available via the HTTP `GET` endpoints:

| URL | Description |
|-----------------------------------------|------------------------------------------------|
| `/` | Web UI |
| `/catalog` | [List of all sources](#catalog) |
| `/{sourceID}` | [Source TileJSON](#source-tilejson) |
| `/{sourceID}/{z}/{x}/{y}` | Map Tiles |
| `/{source1},…,{sourceN}` | [Composite Source TileJSON](#source-tilejson) |
| `/{source1},…,{sourceN}/{z}/{x}/{y}` | [Composite Source Tiles](sources-composite.md) |
| `/sprite/{spriteID}[@2x].{json,png}` | [Sprite sources](sources-sprites.md) |
| `/font/{font}/{start}-{end}` | [Font source](sources-fonts.md) |
| `/font/{font1},…,{fontN}/{start}-{end}` | [Composite Font source](sources-fonts.md) |
| `/health` | Martin server health check: returns 200 `OK` |
| URL | Description |
|------------------------------------------|------------------------------------------------|
| `/` | Web UI |
| `/catalog` | [List of all sources](#catalog) |
| `/{sourceID}` | [Source TileJSON](#source-tilejson) |
| `/{sourceID}/{z}/{x}/{y}` | Map Tiles |
| `/{source1},…,{sourceN}` | [Composite Source TileJSON](#source-tilejson) |
| `/{source1},…,{sourceN}/{z}/{x}/{y}` | [Composite Source Tiles](sources-composite.md) |
| `/sprite/{spriteID}[@2x].{json,png}` | [Sprite sources](sources-sprites.md) |
| `/sdf_sprite/{spriteID}[@2x].{json,png}` | [SDF Sprite sources](sources-sprites.md) |
| `/font/{font}/{start}-{end}` | [Font source](sources-fonts.md) |
| `/font/{font1},…,{fontN}/{start}-{end}` | [Composite Font source](sources-fonts.md) |
| `/health` | Martin server health check: returns 200 `OK` |

### Duplicate Source ID

Expand Down
44 changes: 31 additions & 13 deletions martin/src/sprites/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ impl SpriteSources {

/// Given a list of IDs in a format "id1,id2,id3", return a spritesheet with them all.
/// `ids` may optionally end with "@2x" to request a high-DPI spritesheet.
pub async fn get_sprites(&self, ids: &str) -> SpriteResult<Spritesheet> {
pub async fn get_sprites(&self, ids: &str, as_sdf: bool) -> SpriteResult<Spritesheet> {
let (ids, dpi) = if let Some(ids) = ids.strip_suffix("@2x") {
(ids, 2)
} else {
Expand All @@ -162,7 +162,7 @@ impl SpriteSources {
})
.collect::<SpriteResult<Vec<_>>>()?;

get_spritesheet(sprite_ids.into_iter(), dpi).await
get_spritesheet(sprite_ids.into_iter(), dpi, as_sdf).await
}
}

Expand All @@ -175,6 +175,7 @@ async fn parse_sprite(
name: String,
path: PathBuf,
pixel_ratio: u8,
as_sdf: bool,
) -> SpriteResult<(String, Sprite)> {
let on_err = |e| SpriteError::IoError(e, path.clone());

Expand All @@ -186,14 +187,20 @@ async fn parse_sprite(
let tree = Tree::from_data(&buffer, &Options::default())
.map_err(|e| SpriteParsingError(e, path.clone()))?;

let sprite = Sprite::new(tree, pixel_ratio).ok_or_else(|| SpriteInstError(path.clone()))?;
let sprite = if as_sdf {
Sprite::new_sdf(tree, pixel_ratio)
} else {
Sprite::new(tree, pixel_ratio)
};
let sprite = sprite.ok_or_else(|| SpriteInstError(path.clone()))?;

Ok((name, sprite))
}

pub async fn get_spritesheet(
sources: impl Iterator<Item = &SpriteSource>,
pixel_ratio: u8,
as_sdf: bool,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Attention: There are 2-3 semver-breaking changes here.

Without reworking the API completely, I don't see how this can be avoided.

=> @nyurik would a major version bump be okay?

Copy link
Member

Choose a reason for hiding this comment

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

we are totally ok to keep bumping major for martin - no biggie

) -> SpriteResult<Spritesheet> {
// Asynchronously load all SVG files from the given sources
let mut futures = Vec::new();
Expand All @@ -203,11 +210,14 @@ pub async fn get_spritesheet(
for path in paths {
let name = sprite_name(&path, &source.path)
.map_err(|e| SpriteProcessingError(e, source.path.clone()))?;
futures.push(parse_sprite(name, path, pixel_ratio));
futures.push(parse_sprite(name, path, pixel_ratio, as_sdf));
}
}
let sprites = try_join_all(futures).await?;
let mut builder = SpritesheetBuilder::new();
if as_sdf {
builder.make_sdf();
}
builder.sprites(sprites.into_iter().collect());

// TODO: decide if this is needed and/or configurable
Expand All @@ -234,24 +244,32 @@ mod tests {
let sprites = SpriteSources::resolve(&mut cfg).unwrap().0;
assert_eq!(sprites.len(), 2);

test_src(sprites.values(), 1, "all_1").await;
test_src(sprites.values(), 2, "all_2").await;
//.sdf => generate sdf from png, add sdf == true
//- => does not generate sdf, omits sdf == true
for extension in ["_sdf", ""] {
test_src(sprites.values(), 1, "all_1", extension).await;
test_src(sprites.values(), 2, "all_2", extension).await;

test_src(sprites.get("src1").into_iter(), 1, "src1_1").await;
test_src(sprites.get("src1").into_iter(), 2, "src1_2").await;
test_src(sprites.get("src1").into_iter(), 1, "src1_1", extension).await;
test_src(sprites.get("src1").into_iter(), 2, "src1_2", extension).await;

test_src(sprites.get("src2").into_iter(), 1, "src2_1").await;
test_src(sprites.get("src2").into_iter(), 2, "src2_2").await;
test_src(sprites.get("src2").into_iter(), 1, "src2_1", extension).await;
test_src(sprites.get("src2").into_iter(), 2, "src2_2", extension).await;
}
}

async fn test_src(
sources: impl Iterator<Item = &SpriteSource>,
pixel_ratio: u8,
filename: &str,
extension: &str,
) {
let path = PathBuf::from(format!("../tests/fixtures/sprites/expected/{filename}"));

let sprites = get_spritesheet(sources, pixel_ratio).await.unwrap();
let path = PathBuf::from(format!(
"../tests/fixtures/sprites/expected/{filename}{extension}"
));
let sprites = get_spritesheet(sources, pixel_ratio, extension == "_sdf")
.await
.unwrap();
let mut json = serde_json::to_string_pretty(sprites.get_index()).unwrap();
json.push('\n');
let png = sprites.encode_png().unwrap();
Expand Down
4 changes: 3 additions & 1 deletion martin/src/srv/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ pub fn router(cfg: &mut web::ServiceConfig, #[allow(unused_variables)] usr_cfg:
.service(get_tile);

#[cfg(feature = "sprites")]
cfg.service(crate::srv::sprites::get_sprite_json)
cfg.service(crate::srv::sprites::get_sprite_sdf_json)
.service(crate::srv::sprites::get_sprite_json)
.service(crate::srv::sprites::get_sprite_sdf_png)
.service(crate::srv::sprites::get_sprite_png);

#[cfg(feature = "fonts")]
Expand Down
37 changes: 33 additions & 4 deletions martin/src/srv/sprites.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,18 @@ async fn get_sprite_png(
path: Path<SourceIDsRequest>,
sprites: Data<SpriteSources>,
) -> ActixResult<HttpResponse> {
let sheet = get_sprite(&path, &sprites).await?;
let sheet = get_sprite(&path, &sprites, false).await?;
Ok(HttpResponse::Ok()
.content_type(ContentType::png())
.body(sheet.encode_png().map_err(map_internal_error)?))
}

#[route("/sdf_sprite/{source_ids}.png", method = "GET", method = "HEAD")]
async fn get_sprite_sdf_png(
path: Path<SourceIDsRequest>,
sprites: Data<SpriteSources>,
) -> ActixResult<HttpResponse> {
let sheet = get_sprite(&path, &sprites, true).await?;
Ok(HttpResponse::Ok()
.content_type(ContentType::png())
.body(sheet.encode_png().map_err(map_internal_error)?))
Expand All @@ -31,13 +42,31 @@ async fn get_sprite_json(
path: Path<SourceIDsRequest>,
sprites: Data<SpriteSources>,
) -> ActixResult<HttpResponse> {
let sheet = get_sprite(&path, &sprites).await?;
let sheet = get_sprite(&path, &sprites, false).await?;
Ok(HttpResponse::Ok().json(sheet.get_index()))
}

#[route(
"/sdf_sprite/{source_ids}.json",
method = "GET",
method = "HEAD",
wrap = "middleware::Compress::default()"
)]
async fn get_sprite_sdf_json(
path: Path<SourceIDsRequest>,
sprites: Data<SpriteSources>,
) -> ActixResult<HttpResponse> {
let sheet = get_sprite(&path, &sprites, true).await?;
Ok(HttpResponse::Ok().json(sheet.get_index()))
}

async fn get_sprite(path: &SourceIDsRequest, sprites: &SpriteSources) -> ActixResult<Spritesheet> {
async fn get_sprite(
path: &SourceIDsRequest,
sprites: &SpriteSources,
as_sdf: bool,
) -> ActixResult<Spritesheet> {
sprites
.get_sprites(&path.source_ids)
.get_sprites(&path.source_ids, as_sdf)
.await
.map_err(|e| match e {
SpriteError::SpriteNotFound(_) => ErrorNotFound(e.to_string()),
Expand Down
34 changes: 34 additions & 0 deletions tests/expected/configured/sdf_spr_cmp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"another_bicycle": {
"height": 21,
"pixelRatio": 1,
"width": 21,
"x": 26,
"y": 22,
"sdf": true
},
"bear": {
"height": 22,
"pixelRatio": 1,
"width": 22,
"x": 26,
"y": 0,
"sdf": true
},
"bicycle": {
"height": 21,
"pixelRatio": 1,
"width": 21,
"x": 0,
"y": 26,
"sdf": true
},
"sub/circle": {
"height": 26,
"pixelRatio": 1,
"width": 26,
"x": 0,
"y": 0,
"sdf": true
}
}
Binary file added tests/expected/configured/sdf_spr_cmp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/expected/configured/sdf_spr_cmp.png.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tests/output/configured/sdf_spr_cmp.png: PNG image data, 48 x 47, 8-bit gray+alpha, non-interlaced
34 changes: 34 additions & 0 deletions tests/expected/configured/sdf_spr_cmp_2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"another_bicycle": {
"height": 36,
"pixelRatio": 2,
"width": 36,
"x": 84,
"y": 0,
"sdf": true
},
"bear": {
"height": 38,
"pixelRatio": 2,
"width": 38,
"x": 46,
"y": 0,
"sdf": true
},
"bicycle": {
"height": 36,
"pixelRatio": 2,
"width": 36,
"x": 84,
"y": 36,
"sdf": true
},
"sub/circle": {
"height": 46,
"pixelRatio": 2,
"width": 46,
"x": 0,
"y": 0,
"sdf": true
}
}
Binary file added tests/expected/configured/sdf_spr_cmp_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/expected/configured/sdf_spr_cmp_2.png.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tests/output/configured/sdf_spr_cmp_2.png: PNG image data, 120 x 72, 8-bit gray+alpha, non-interlaced
10 changes: 10 additions & 0 deletions tests/expected/configured/sdf_spr_mysrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"bicycle": {
"height": 36,
"pixelRatio": 2,
"width": 36,
"x": 0,
"y": 0,
"sdf": true
}
}
Binary file added tests/expected/configured/sdf_spr_mysrc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/expected/configured/sdf_spr_mysrc.png.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tests/output/configured/sdf_spr_mysrc.png: PNG image data, 36 x 36, 8-bit gray+alpha, non-interlaced
26 changes: 26 additions & 0 deletions tests/expected/configured/sdf_spr_src1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"another_bicycle": {
"height": 21,
"pixelRatio": 1,
"width": 21,
"x": 26,
"y": 22,
"sdf": true
},
"bear": {
"height": 22,
"pixelRatio": 1,
"width": 22,
"x": 26,
"y": 0,
"sdf": true
},
"sub/circle": {
"height": 26,
"pixelRatio": 1,
"width": 26,
"x": 0,
"y": 0,
"sdf": true
}
}
Binary file added tests/expected/configured/sdf_spr_src1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/expected/configured/sdf_spr_src1.png.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tests/output/configured/sdf_spr_src1.png: PNG image data, 48 x 43, 8-bit gray+alpha, non-interlaced
26 changes: 26 additions & 0 deletions tests/expected/configured/sdf_spr_src1_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"another_bicycle": {
"height": 36,
"pixelRatio": 2,
"width": 36,
"x": 84,
"y": 0,
"sdf": true
},
"bear": {
"height": 38,
"pixelRatio": 2,
"width": 38,
"x": 46,
"y": 0,
"sdf": true
},
"sub/circle": {
"height": 46,
"pixelRatio": 2,
"width": 46,
"x": 0,
"y": 0,
"sdf": true
}
}
Binary file added tests/expected/configured/sdf_spr_src1_.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tests/expected/configured/sdf_spr_src1_.png.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tests/output/configured/sdf_spr_src1_.png: PNG image data, 120 x 46, 8-bit gray+alpha, non-interlaced
Loading
Loading