use bevy_rpack::{AtlasFrame, SerializableRect}; use image::DynamicImage; use serde::{Deserialize, Serialize}; use serde_json::Value; #[cfg(all(feature = "cli", not(target_arch = "wasm32")))] use std::io::Write; use std::{ ffi::OsStr, fmt::Display, path::{Path, PathBuf}, }; use texture_packer::{importer::ImageImporter, TexturePacker, TexturePackerConfig}; use thiserror::Error; #[derive(Clone)] pub struct Spritesheet { pub image_data: DynamicImage, pub atlas_asset: bevy_rpack::AtlasAsset, pub atlas_asset_json: Value, } #[derive(Clone)] pub struct ImageFile { pub id: String, pub image: DynamicImage, } impl ImageFile { pub fn at_path

(path: &Path, id: P) -> Option where P: AsRef, { let mut id = id.as_ref().to_owned().replace("\\", "/"); if let Some((before, _)) = id.split_once('.') { id = before.to_string(); } if let Ok(image) = ImageImporter::import_from_file(path) { Some(ImageFile { image, id }) } else { None } } } pub fn get_common_prefix(paths: &[S]) -> String where S: AsRef + Sized, { if paths.is_empty() { return String::new(); } let path = Path::new(paths[0].as_ref()) .file_name() .unwrap_or_default() .to_str() .unwrap_or_default(); let mut prefix = String::from(paths[0].as_ref().to_string_lossy()) .strip_suffix(&path) .unwrap_or_default() .to_owned(); for s in paths.iter().skip(1) { let s = s.as_ref().to_string_lossy(); while !(s.starts_with(&prefix) || prefix.is_empty()) { prefix.pop(); } } prefix } #[derive(Clone, Debug, Default, Copy, Serialize, Deserialize)] #[cfg_attr( all(feature = "cli", not(target_arch = "wasm32")), derive(clap::ValueEnum) )] pub enum SaveImageFormat { #[default] Png, Dds, Basis, } impl Display for SaveImageFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SaveImageFormat::Png => f.write_str(".png"), SaveImageFormat::Dds => f.write_str(".dds"), SaveImageFormat::Basis => f.write_str(".basis"), } } } /// Errors that can occur while building a `Spritesheet`. #[non_exhaustive] #[derive(Debug, Error)] pub enum SpritesheetError { #[error("Cannot pack image: {0}")] CannotPackImage(String), #[error("Failed to export tilemap image")] FailedToExportImage, #[error("could not parse asset: {0}")] ParsingError(#[from] serde_json::Error), #[error("Failed to pack image into tilemap, tilemap to small")] FailedToPackImage, } impl Spritesheet { pub fn build

( config: TexturePackerConfig, images: &[ImageFile], filename: P, ) -> Result where P: AsRef, { let mut packer = TexturePacker::new_skyline(config); for image in images.iter() { if !packer.can_pack(&image.image) { return Err(SpritesheetError::CannotPackImage(image.id.clone())); } if let Err(_err) = packer.pack_own(&image.id, image.image.clone()) { return Err(SpritesheetError::FailedToPackImage); } } let Ok(image_data) = texture_packer::exporter::ImageExporter::export(&packer, None) else { return Err(SpritesheetError::FailedToExportImage); }; let mut atlas_asset = bevy_rpack::AtlasAsset { size: [image_data.width(), image_data.height()], filename: filename.as_ref().to_owned(), frames: packer .get_frames() .values() .map(|v| -> AtlasFrame { AtlasFrame { key: v.key.clone(), frame: SerializableRect { x: v.frame.x, y: v.frame.y, w: v.frame.w, h: v.frame.h, }, } }) .collect(), }; atlas_asset.frames.sort_by(|a, b| a.key.cmp(&b.key)); let atlas_asset_json = serde_json::to_value(&atlas_asset)?; Ok(Spritesheet { image_data, atlas_asset, atlas_asset_json, }) } #[cfg(all(feature = "dds", not(target_arch = "wasm32")))] pub fn save_as_dds(&self, output_path: R) where R: AsRef, { let rgba_image = self.image_data.to_rgba8(); let dds = image_dds::dds_from_image( &rgba_image, image_dds::ImageFormat::Rgba8Unorm, image_dds::Quality::Fast, image_dds::Mipmaps::GeneratedAutomatic, ) .unwrap(); let mut writer = std::io::BufWriter::new(std::fs::File::create(output_path.as_ref()).unwrap()); dds.write(&mut writer).unwrap(); } #[cfg(all(feature = "basis", not(target_arch = "wasm32")))] pub fn save_as_basis(&self, output_path: R) -> anyhow::Result<()> where R: AsRef, { use basis_universal::{BasisTextureFormat, Compressor, Transcoder}; use image::{EncodableLayout, GenericImageView}; let rgba_image = self.image_data.to_rgba8(); let channel_count = 4; let (pixel_width, pixel_height) = self.image_data.dimensions(); let mut compressor_params = basis_universal::CompressorParams::new(); compressor_params.set_generate_mipmaps(true); compressor_params.set_basis_format(BasisTextureFormat::ETC1S); compressor_params.set_etc1s_quality_level(basis_universal::ETC1S_QUALITY_MAX); compressor_params.set_print_status_to_stdout(false); let mut compressor_image = compressor_params.source_image_mut(0); compressor_image.init( rgba_image.as_bytes(), pixel_width, pixel_height, channel_count, ); // // Create the compressor and compress // let mut compressor = Compressor::default(); let compression_time = unsafe { compressor.init(&compressor_params); let t0 = std::time::Instant::now(); compressor.process().expect("Failed to compress the image."); let t1 = std::time::Instant::now(); t1 - t0 }; // You could write it to disk like this let basis_file = compressor.basis_file(); let transcoder = Transcoder::new(); let mip_level_count = transcoder.image_level_count(basis_file, 0); println!( "Compressed {} mip levels to {} total bytes in {} ms", mip_level_count, compressor.basis_file_size(), compression_time.as_secs_f64() * 1000.0 ); std::fs::write(output_path.as_ref(), basis_file)?; Ok(()) } } #[derive(Clone, Serialize, Deserialize, Default)] pub struct TilemapGenerationConfig { pub asset_patterns: Vec, pub output_path: String, /// Image format, png by default #[serde(skip_serializing_if = "Option::is_none", default)] pub format: Option, /// Size of the tilemap texture. Default value is `2048`. #[serde(skip_serializing_if = "Option::is_none", default)] pub size: Option, /// Size of the padding between frames in pixel. Default value is `2` #[serde(skip_serializing_if = "Option::is_none", default)] pub texture_padding: Option, /// Size of the padding on the outer edge of the packed image in pixel. Default value is `0`. #[serde(skip_serializing_if = "Option::is_none", default)] pub border_padding: Option, #[serde(skip)] pub working_dir: Option, } #[cfg(all(feature = "cli", not(target_arch = "wasm32")))] impl TilemapGenerationConfig { pub fn read_from_file

(path: P) -> anyhow::Result where P: AsRef, { let config_file = std::fs::read_to_string(path.as_ref())?; let mut config: TilemapGenerationConfig = serde_json::from_str(&config_file)?; config.working_dir = Path::new(path.as_ref()).parent().map(|p| p.to_path_buf()); Ok(config) } pub fn generate(&self) -> anyhow::Result<()> { let dir = match &self.working_dir { None => std::env::current_dir().expect("msg"), Some(p) => { if p.to_string_lossy().len() == 0 { std::env::current_dir().expect("msg") } else { p.clone() } } }; let working_dir = match std::path::absolute(dir) { Ok(p) => p, Err(e) => panic!("DUPA {:?}", e), }; let mut file_paths: Vec = self .asset_patterns .iter() .flat_map(|pattern| { let p = format!("{}/{}", working_dir.to_string_lossy(), pattern); println!("{}", p); glob::glob(&p).expect("Wrong pattern for assets").flatten() }) .filter(|e| e.is_file()) .collect(); file_paths.sort(); let prefix = get_common_prefix(&file_paths); let images: Vec = file_paths .iter() .flat_map(|f| { let id = f .to_str() .unwrap_or_default() .strip_prefix(&prefix) .unwrap_or_default(); ImageFile::at_path(f, id) }) .collect(); let atlas_image_path = working_dir.join(format!( "{}{}", self.output_path, self.format.unwrap_or_default() )); let atlas_filename = Path::new(&atlas_image_path) .file_name() .expect("D") .to_string_lossy() .to_string(); let atlas_config_path = working_dir.join(format!("{}.rpack.json", self.output_path)); let spritesheet = Spritesheet::build( texture_packer::TexturePackerConfig { max_width: self.size.unwrap_or(2048), max_height: self.size.unwrap_or(2048), allow_rotation: false, force_max_dimensions: true, border_padding: self.border_padding.unwrap_or(0), texture_padding: self.texture_padding.unwrap_or(2), texture_extrusion: 0, trim: false, texture_outlines: false, }, &images, &atlas_filename, )?; if Path::new(&atlas_config_path).exists() { std::fs::remove_file(&atlas_config_path).expect("Could not remove the old file"); } if Path::new(&atlas_image_path).exists() { std::fs::remove_file(&atlas_image_path).expect("Could not remove the old file"); } match self.format.unwrap_or_default() { SaveImageFormat::Dds => { #[cfg(feature = "dds")] spritesheet.save_as_dds(&atlas_image_path); #[cfg(not(feature = "dds"))] panic!("Program is compiled without support for dds. Compile it yourself with feature `dds` enabled."); } SaveImageFormat::Png => { spritesheet .image_data .save_with_format(&atlas_image_path, image::ImageFormat::Png)?; } SaveImageFormat::Basis => { #[cfg(feature = "basis")] spritesheet.save_as_basis(&atlas_image_path)?; #[cfg(not(feature = "basis"))] panic!("Program is compiled without support for basis. Compile it yourself with feature `basis` enabled."); } } let json = serde_json::to_string_pretty(&spritesheet.atlas_asset_json)?; let mut file = std::fs::File::create(&atlas_config_path)?; file.write_all(json.as_bytes())?; Ok(()) } }