diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b5dd97..dbd71f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] - ReleaseDate +### Added + +- Added support for rendering tiles WebP format using the `--image-format` option + ## [2.3.1] - 2025-01-06 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 87d4135..e902fc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,9 +125,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" [[package]] name = "bytemuck" @@ -313,6 +313,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "errno" version = "0.3.10" @@ -461,10 +471,21 @@ checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", + "image-webp", "num-traits", "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]] name = "indexmap" version = "2.7.0" @@ -546,9 +567,12 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "3d6ea2a48c204030ee31a7d7fc72c93294c92fe87ecb1789881c9543516e1a0d" +dependencies = [ + "value-bag", +] [[package]] name = "lru" @@ -800,6 +824,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.38" @@ -850,7 +880,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", ] [[package]] @@ -900,7 +930,7 @@ version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "errno", "libc", "linux-raw-sys", @@ -948,6 +978,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_fmt" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +dependencies = [ + "serde", +] + [[package]] name = "serde_json" version = "1.0.135" @@ -1009,10 +1048,88 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "syn" -version = "2.0.95" +name = "sval" +version = "2.13.2" 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 = [ "proc-macro2", "quote", @@ -1107,6 +1224,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + [[package]] name = "unicode-ident" version = "1.0.14" @@ -1125,6 +1248,42 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 6a6a300..c4bfdcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ enum-map = "2.7.3" fastnbt = "2.3.2" futures-util = "0.3.28" 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"] } lru = "0.12.0" minedmap-nbt = { version = "0.1.1", path = "crates/nbt", default-features = false } diff --git a/README.md b/README.md index 4c684f9..1ea4856 100644 --- a/README.md +++ b/README.md @@ -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. If you are uploading the directory to a remote webserver, you do not need to upload the -`/data/processed` directory, as that is only used locally to allow processing +`/data/processed` directory, as it is only used locally to allow processing 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 ![Sign screenshot](https://raw.githubusercontent.com/neocturne/MinedMap/e5d9c813ba3118d04dc7e52e3dc6f48808a69120/docs/images/signs.png) diff --git a/src/core/common.rs b/src/core/common.rs index be6d28a..b933dcd 100644 --- a/src/core/common.rs +++ b/src/core/common.rs @@ -7,6 +7,7 @@ use std::{ }; use anyhow::{Context, Result}; +use clap::ValueEnum; use indexmap::IndexSet; use regex::{Regex, RegexSet}; use serde::{Deserialize, Serialize}; @@ -150,6 +151,8 @@ pub struct Config { pub viewer_info_path: PathBuf, /// Path of viewer entities file pub viewer_entities_path: PathBuf, + /// Format of generated map tiles + pub image_format: ImageFormat, /// Sign text filter patterns pub sign_patterns: RegexSet, /// Sign text transformation pattern @@ -189,6 +192,7 @@ impl Config { entities_path_final, viewer_info_path, viewer_entities_path, + image_format: args.image_format, sign_patterns, sign_transforms, }) @@ -264,14 +268,39 @@ impl Config { [&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 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); [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 pub fn overlay_chunk(image: &mut I, chunk: &J, coords: ChunkCoords) where diff --git a/src/core/metadata_writer.rs b/src/core/metadata_writer.rs index 0ea1f65..92d8566 100644 --- a/src/core/metadata_writer.rs +++ b/src/core/metadata_writer.rs @@ -61,6 +61,8 @@ struct Metadata<'t> { spawn: Spawn, /// Enabled MinedMap features features: Features, + /// Format of generated map tiles + tile_extension: &'static str, } /// Viewer entity JSON data structure @@ -205,6 +207,7 @@ impl<'a> MetadataWriter<'a> { mipmaps: Vec::new(), spawn: Self::spawn(&level_dat), features, + tile_extension: self.config.tile_extension(), }; for tile_map in self.tiles.iter() { diff --git a/src/core/mod.rs b/src/core/mod.rs index f552ffa..5832379 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -16,7 +16,7 @@ use anyhow::{Context, Result}; use clap::Parser; use git_version::git_version; -use common::Config; +use common::{Config, ImageFormat}; use metadata_writer::MetadataWriter; use region_processor::RegionProcessor; use tile_mipmapper::TileMipmapper; @@ -47,6 +47,9 @@ pub struct Args { /// Enable verbose messages #[arg(short, long)] 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 #[arg(long)] pub sign_prefix: Vec, diff --git a/src/core/region_processor.rs b/src/core/region_processor.rs index ce2d060..e448f5e 100644 --- a/src/core/region_processor.rs +++ b/src/core/region_processor.rs @@ -79,6 +79,8 @@ struct SingleRegionProcessor<'a> { lightmap: image::GrayAlphaImage, /// Processed entity intermediate data entities: ProcessedEntities, + /// Format of generated map tiles + image_format: image::ImageFormat, /// True if any unknown block or biome types were encountered during processing has_unknown: bool, } @@ -127,6 +129,7 @@ impl<'a> SingleRegionProcessor<'a> { processed_region, lightmap, entities, + image_format: processor.config.tile_image_format(), has_unknown: false, }) } @@ -179,7 +182,7 @@ impl<'a> SingleRegionProcessor<'a> { self.input_timestamp, |file| { self.lightmap - .write_to(file, image::ImageFormat::Png) + .write_to(file, self.image_format) .context("Failed to save image") }, ) diff --git a/src/core/tile_mipmapper.rs b/src/core/tile_mipmapper.rs index d7e54a9..2eda0e9 100644 --- a/src/core/tile_mipmapper.rs +++ b/src/core/tile_mipmapper.rs @@ -144,7 +144,7 @@ where } image - .write_to(file, image::ImageFormat::Png) + .write_to(file, self.config.tile_image_format()) .context("Failed to save image") } } diff --git a/src/core/tile_renderer.rs b/src/core/tile_renderer.rs index 09ad8a1..a972b78 100644 --- a/src/core/tile_renderer.rs +++ b/src/core/tile_renderer.rs @@ -304,7 +304,7 @@ impl<'a> TileRenderer<'a> { processed_timestamp, |file| { image - .write_to(file, image::ImageFormat::Png) + .write_to(file, self.config.tile_image_format()) .context("Failed to save image") }, )?; diff --git a/viewer/MinedMap.js b/viewer/MinedMap.js index cfcccf1..61188b1 100644 --- a/viewer/MinedMap.js +++ b/viewer/MinedMap.js @@ -73,7 +73,7 @@ function signIcon(material, kind) { } const MinedMapLayer = L.TileLayer.extend({ - initialize: function (mipmaps, layer) { + initialize: function (mipmaps, layer, tile_extension) { L.TileLayer.prototype.initialize.call(this, '', { detectRetina: true, tileSize: 512, @@ -88,6 +88,7 @@ const MinedMapLayer = L.TileLayer.extend({ this.mipmaps = mipmaps; this.layer = layer; + this.ext = tile_extension; }, createTile: function (coords, done) { @@ -112,7 +113,7 @@ const MinedMapLayer = L.TileLayer.extend({ 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 {mipmaps, spawn} = res; const features = res.features || {}; + const tile_extension = res.tile_extension || 'png'; const updateParams = function () { const args = parseHash(); @@ -369,10 +371,10 @@ window.createMap = function () { const overlayMaps = {}; - const mapLayer = new MinedMapLayer(mipmaps, 'map'); + const mapLayer = new MinedMapLayer(mipmaps, 'map', tile_extension); mapLayer.addTo(map); - const lightLayer = new MinedMapLayer(mipmaps, 'light'); + const lightLayer = new MinedMapLayer(mipmaps, 'light', tile_extension); overlayMaps['Illumination'] = lightLayer; if (params.light) map.addLayer(lightLayer);