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..970c1be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -461,10 +461,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" @@ -800,6 +811,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" 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);