Merge pull request #62 from neocturne/webp

core, viewer: add support for WebP output
This commit is contained in:
Matthias Schiffer 2025-01-11 01:50:36 +01:00 committed by GitHub
commit ec309dc15f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 232 additions and 20 deletions

View file

@ -2,6 +2,10 @@
## [Unreleased] - ReleaseDate ## [Unreleased] - ReleaseDate
### Added
- Added support for rendering tiles WebP format using the `--image-format` option
## [2.3.1] - 2025-01-06 ## [2.3.1] - 2025-01-06
### Fixed ### Fixed

177
Cargo.lock generated
View file

@ -125,9 +125,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.6.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
@ -313,6 +313,16 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "erased-serde"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
dependencies = [
"serde",
"typeid",
]
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.10" version = "0.3.10"
@ -461,10 +471,21 @@ checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
"image-webp",
"num-traits", "num-traits",
"png", "png",
] ]
[[package]]
name = "image-webp"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f"
dependencies = [
"byteorder-lite",
"quick-error",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.7.0" version = "2.7.0"
@ -546,9 +567,12 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.22" version = "0.4.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d"
dependencies = [
"value-bag",
]
[[package]] [[package]]
name = "lru" name = "lru"
@ -800,6 +824,12 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.38" version = "1.0.38"
@ -850,7 +880,7 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.7.0",
] ]
[[package]] [[package]]
@ -900,7 +930,7 @@ version = "0.38.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.7.0",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@ -948,6 +978,15 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "serde_fmt"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.135" version = "1.0.135"
@ -1009,10 +1048,88 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "sval"
version = "2.0.95" version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" checksum = "f6dc0f9830c49db20e73273ffae9b5240f63c42e515af1da1fceefb69fceafd8"
[[package]]
name = "sval_buffer"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "429922f7ad43c0ef8fd7309e14d750e38899e32eb7e8da656ea169dd28ee212f"
dependencies = [
"sval",
"sval_ref",
]
[[package]]
name = "sval_dynamic"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f16ff5d839396c11a30019b659b0976348f3803db0626f736764c473b50ff4"
dependencies = [
"sval",
]
[[package]]
name = "sval_fmt"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c01c27a80b6151b0557f9ccbe89c11db571dc5f68113690c1e028d7e974bae94"
dependencies = [
"itoa",
"ryu",
"sval",
]
[[package]]
name = "sval_json"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0deef63c70da622b2a8069d8600cf4b05396459e665862e7bdb290fd6cf3f155"
dependencies = [
"itoa",
"ryu",
"sval",
]
[[package]]
name = "sval_nested"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a39ce5976ae1feb814c35d290cf7cf8cd4f045782fe1548d6bc32e21f6156e9f"
dependencies = [
"sval",
"sval_buffer",
"sval_ref",
]
[[package]]
name = "sval_ref"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb7c6ee3751795a728bc9316a092023529ffea1783499afbc5c66f5fabebb1fa"
dependencies = [
"sval",
]
[[package]]
name = "sval_serde"
version = "2.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a5572d0321b68109a343634e3a5d576bf131b82180c6c442dee06349dfc652a"
dependencies = [
"serde",
"sval",
"sval_nested",
]
[[package]]
name = "syn"
version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1107,6 +1224,12 @@ dependencies = [
"tracing-log", "tracing-log",
] ]
[[package]]
name = "typeid"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.14"
@ -1125,6 +1248,42 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "value-bag"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
dependencies = [
"value-bag-serde1",
"value-bag-sval2",
]
[[package]]
name = "value-bag-serde1"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b"
dependencies = [
"erased-serde",
"serde",
"serde_fmt",
]
[[package]]
name = "value-bag-sval2"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a"
dependencies = [
"sval",
"sval_buffer",
"sval_dynamic",
"sval_fmt",
"sval_json",
"sval_ref",
"sval_serde",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View file

@ -44,7 +44,7 @@ enum-map = "2.7.3"
fastnbt = "2.3.2" fastnbt = "2.3.2"
futures-util = "0.3.28" futures-util = "0.3.28"
git-version = "0.3.5" git-version = "0.3.5"
image = { version = "0.25.1", default-features = false, features = ["png"] } image = { version = "0.25.1", default-features = false, features = ["png", "webp"] }
indexmap = { version = "2.0.0", features = ["serde"] } indexmap = { version = "2.0.0", features = ["serde"] }
lru = "0.12.0" lru = "0.12.0"
minedmap-nbt = { version = "0.1.1", path = "crates/nbt", default-features = false } minedmap-nbt = { version = "0.1.1", path = "crates/nbt", default-features = false }

View file

@ -55,9 +55,18 @@ a proper webserver like [nginx](https://nginx.org/) or upload the viewer togethe
the generated map files to public webspace to make the map available to others. the generated map files to public webspace to make the map available to others.
If you are uploading the directory to a remote webserver, you do not need to upload the If you are uploading the directory to a remote webserver, you do not need to upload the
`<viewer>/data/processed` directory, as that is only used locally to allow processing `<viewer>/data/processed` directory, as it is only used locally to allow processing
updates more quickly. updates more quickly.
### Image formats
MinedMap renders map tiles as PNG by default. Pass `--image-format webp` to select
WebP instead. For typical Minecraft worlds, using WebP reduces file sizes by 10-15%
without increasing processing time.
MinedMap always uses lossless compression for tile images, regardless of the
image format.
### Signs ### Signs
![Sign screenshot](https://raw.githubusercontent.com/neocturne/MinedMap/e5d9c813ba3118d04dc7e52e3dc6f48808a69120/docs/images/signs.png) ![Sign screenshot](https://raw.githubusercontent.com/neocturne/MinedMap/e5d9c813ba3118d04dc7e52e3dc6f48808a69120/docs/images/signs.png)

View file

@ -7,6 +7,7 @@ use std::{
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::ValueEnum;
use indexmap::IndexSet; use indexmap::IndexSet;
use regex::{Regex, RegexSet}; use regex::{Regex, RegexSet};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -150,6 +151,8 @@ pub struct Config {
pub viewer_info_path: PathBuf, pub viewer_info_path: PathBuf,
/// Path of viewer entities file /// Path of viewer entities file
pub viewer_entities_path: PathBuf, pub viewer_entities_path: PathBuf,
/// Format of generated map tiles
pub image_format: ImageFormat,
/// Sign text filter patterns /// Sign text filter patterns
pub sign_patterns: RegexSet, pub sign_patterns: RegexSet,
/// Sign text transformation pattern /// Sign text transformation pattern
@ -189,6 +192,7 @@ impl Config {
entities_path_final, entities_path_final,
viewer_info_path, viewer_info_path,
viewer_entities_path, viewer_entities_path,
image_format: args.image_format,
sign_patterns, sign_patterns,
sign_transforms, sign_transforms,
}) })
@ -264,14 +268,39 @@ impl Config {
[&self.output_dir, Path::new(&dir)].iter().collect() [&self.output_dir, Path::new(&dir)].iter().collect()
} }
/// Returns the file extension for the configured image format
pub fn tile_extension(&self) -> &'static str {
match self.image_format {
ImageFormat::Png => "png",
ImageFormat::Webp => "webp",
}
}
/// Returns the configurured image format for the image library
pub fn tile_image_format(&self) -> image::ImageFormat {
match self.image_format {
ImageFormat::Png => image::ImageFormat::Png,
ImageFormat::Webp => image::ImageFormat::WebP,
}
}
/// Constructs the path of an output tile image /// Constructs the path of an output tile image
pub fn tile_path(&self, kind: TileKind, level: usize, coords: TileCoords) -> PathBuf { pub fn tile_path(&self, kind: TileKind, level: usize, coords: TileCoords) -> PathBuf {
let filename = coord_filename(coords, "png"); let filename = coord_filename(coords, self.tile_extension());
let dir = self.tile_dir(kind, level); let dir = self.tile_dir(kind, level);
[Path::new(&dir), Path::new(&filename)].iter().collect() [Path::new(&dir), Path::new(&filename)].iter().collect()
} }
} }
/// Format of generated map tiles
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum ImageFormat {
/// Generate PNG images
#[default]
Png,
/// Generate WebP images
Webp,
}
/// Copies a chunk image into a region tile /// Copies a chunk image into a region tile
pub fn overlay_chunk<I, J>(image: &mut I, chunk: &J, coords: ChunkCoords) pub fn overlay_chunk<I, J>(image: &mut I, chunk: &J, coords: ChunkCoords)
where where

View file

@ -61,6 +61,8 @@ struct Metadata<'t> {
spawn: Spawn, spawn: Spawn,
/// Enabled MinedMap features /// Enabled MinedMap features
features: Features, features: Features,
/// Format of generated map tiles
tile_extension: &'static str,
} }
/// Viewer entity JSON data structure /// Viewer entity JSON data structure
@ -205,6 +207,7 @@ impl<'a> MetadataWriter<'a> {
mipmaps: Vec::new(), mipmaps: Vec::new(),
spawn: Self::spawn(&level_dat), spawn: Self::spawn(&level_dat),
features, features,
tile_extension: self.config.tile_extension(),
}; };
for tile_map in self.tiles.iter() { for tile_map in self.tiles.iter() {

View file

@ -16,7 +16,7 @@ use anyhow::{Context, Result};
use clap::Parser; use clap::Parser;
use git_version::git_version; use git_version::git_version;
use common::Config; use common::{Config, ImageFormat};
use metadata_writer::MetadataWriter; use metadata_writer::MetadataWriter;
use region_processor::RegionProcessor; use region_processor::RegionProcessor;
use tile_mipmapper::TileMipmapper; use tile_mipmapper::TileMipmapper;
@ -47,6 +47,9 @@ pub struct Args {
/// Enable verbose messages /// Enable verbose messages
#[arg(short, long)] #[arg(short, long)]
pub verbose: bool, pub verbose: bool,
/// Format of generated map tiles
#[arg(long, value_enum, default_value_t)]
pub image_format: ImageFormat,
/// Prefix for text of signs to show on the map /// Prefix for text of signs to show on the map
#[arg(long)] #[arg(long)]
pub sign_prefix: Vec<String>, pub sign_prefix: Vec<String>,

View file

@ -79,6 +79,8 @@ struct SingleRegionProcessor<'a> {
lightmap: image::GrayAlphaImage, lightmap: image::GrayAlphaImage,
/// Processed entity intermediate data /// Processed entity intermediate data
entities: ProcessedEntities, entities: ProcessedEntities,
/// Format of generated map tiles
image_format: image::ImageFormat,
/// True if any unknown block or biome types were encountered during processing /// True if any unknown block or biome types were encountered during processing
has_unknown: bool, has_unknown: bool,
} }
@ -127,6 +129,7 @@ impl<'a> SingleRegionProcessor<'a> {
processed_region, processed_region,
lightmap, lightmap,
entities, entities,
image_format: processor.config.tile_image_format(),
has_unknown: false, has_unknown: false,
}) })
} }
@ -179,7 +182,7 @@ impl<'a> SingleRegionProcessor<'a> {
self.input_timestamp, self.input_timestamp,
|file| { |file| {
self.lightmap self.lightmap
.write_to(file, image::ImageFormat::Png) .write_to(file, self.image_format)
.context("Failed to save image") .context("Failed to save image")
}, },
) )

View file

@ -144,7 +144,7 @@ where
} }
image image
.write_to(file, image::ImageFormat::Png) .write_to(file, self.config.tile_image_format())
.context("Failed to save image") .context("Failed to save image")
} }
} }

View file

@ -304,7 +304,7 @@ impl<'a> TileRenderer<'a> {
processed_timestamp, processed_timestamp,
|file| { |file| {
image image
.write_to(file, image::ImageFormat::Png) .write_to(file, self.config.tile_image_format())
.context("Failed to save image") .context("Failed to save image")
}, },
)?; )?;

View file

@ -73,7 +73,7 @@ function signIcon(material, kind) {
} }
const MinedMapLayer = L.TileLayer.extend({ const MinedMapLayer = L.TileLayer.extend({
initialize: function (mipmaps, layer) { initialize: function (mipmaps, layer, tile_extension) {
L.TileLayer.prototype.initialize.call(this, '', { L.TileLayer.prototype.initialize.call(this, '', {
detectRetina: true, detectRetina: true,
tileSize: 512, tileSize: 512,
@ -88,6 +88,7 @@ const MinedMapLayer = L.TileLayer.extend({
this.mipmaps = mipmaps; this.mipmaps = mipmaps;
this.layer = layer; this.layer = layer;
this.ext = tile_extension;
}, },
createTile: function (coords, done) { createTile: function (coords, done) {
@ -112,7 +113,7 @@ const MinedMapLayer = L.TileLayer.extend({
return L.Util.emptyImageUrl; return L.Util.emptyImageUrl;
return 'data/'+this.layer+'/'+z+'/r.'+coords.x+'.'+coords.y+'.png'; return `data/${this.layer}/${z}/r.${coords.x}.${coords.y}.${this.ext}`;
}, },
}); });
@ -332,6 +333,7 @@ window.createMap = function () {
const res = await response.json(); const res = await response.json();
const {mipmaps, spawn} = res; const {mipmaps, spawn} = res;
const features = res.features || {}; const features = res.features || {};
const tile_extension = res.tile_extension || 'png';
const updateParams = function () { const updateParams = function () {
const args = parseHash(); const args = parseHash();
@ -369,10 +371,10 @@ window.createMap = function () {
const overlayMaps = {}; const overlayMaps = {};
const mapLayer = new MinedMapLayer(mipmaps, 'map'); const mapLayer = new MinedMapLayer(mipmaps, 'map', tile_extension);
mapLayer.addTo(map); mapLayer.addTo(map);
const lightLayer = new MinedMapLayer(mipmaps, 'light'); const lightLayer = new MinedMapLayer(mipmaps, 'light', tile_extension);
overlayMaps['Illumination'] = lightLayer; overlayMaps['Illumination'] = lightLayer;
if (params.light) if (params.light)
map.addLayer(lightLayer); map.addLayer(lightLayer);