diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e38ca3671bdd959f4955d5d4d62453991fd83e3..7af9eb34a6d8dbe15a3f1419b1f8260a8b1622c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,12 +24,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Map::get_tileset_by_gid` -> `Map::tileset_by_gid` - Tile now has `image` instead of `images`. ([Issue comment](https://github.com/mapeditor/rs-tiled/issues/103#issuecomment-940773123)) - Tileset now has `image` instead of `images`. +- `Image::source` is now a `PathBuf` instead of a `String`. - Functions that took in `&Path` now take `impl AsRef<Path>`. - Bumped `zstd` to `0.9`. - Fix markdown formatting in the `CONTRIBUTORS` file. ### Added -- `Map::source` for obtaining where the map actually came from. +- `Tileset::source` for obtaining where the tileset actually came from. - `Tileset::columns`. - `layers::Layer::id`. - Support for 'object'-type properties. diff --git a/src/error.rs b/src/error.rs index e3f243c1bf4fab2d73423338d85399337b7017fb..55c4d032fe695152bc40b9017fdb9a8db4e4b1d4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,6 +18,11 @@ pub enum TiledError { Base64DecodingError(base64::DecodeError), XmlDecodingError(xml::reader::Error), PrematureEnd(String), + /// Tried to parse external data of an object without a file location, + /// e.g. by using Map::parse_reader. + SourceRequired { + object_to_parse: String, + }, Other(String), } @@ -29,6 +34,11 @@ impl fmt::Display for TiledError { TiledError::Base64DecodingError(ref e) => write!(fmt, "{}", e), TiledError::XmlDecodingError(ref e) => write!(fmt, "{}", e), TiledError::PrematureEnd(ref e) => write!(fmt, "{}", e), + TiledError::SourceRequired { + ref object_to_parse, + } => { + write!(fmt, "Tried to parse external {} without a file location, e.g. by using Map::parse_reader.", object_to_parse) + } TiledError::Other(ref s) => write!(fmt, "{}", s), } } @@ -43,6 +53,7 @@ impl std::error::Error for TiledError { TiledError::Base64DecodingError(ref e) => Some(e as &dyn std::error::Error), TiledError::XmlDecodingError(ref e) => Some(e as &dyn std::error::Error), TiledError::PrematureEnd(_) => None, + TiledError::SourceRequired { .. } => None, TiledError::Other(_) => None, } } diff --git a/src/image.rs b/src/image.rs index 21166f4b0837a6ffc39ef15cc1401b986787a771..a7cf8eff2874bc13208101e6e996c93ab313e614 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,4 +1,7 @@ -use std::io::Read; +use std::{ + io::Read, + path::{Path, PathBuf}, +}; use xml::{attribute::OwnedAttribute, EventReader}; @@ -6,8 +9,15 @@ use crate::{error::TiledError, properties::Color, util::*}; #[derive(Debug, PartialEq, Eq, Clone)] pub struct Image { - /// The filepath of the image - pub source: String, + /// The filepath of the image. + /// + /// ## Note + /// The crate does not currently support embedded images (Even though Tiled + /// does not allow creating maps with embedded image data, the TMX format does; [source]) + /// + /// [source]: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#image + // TODO: Embedded images + pub source: PathBuf, pub width: i32, pub height: i32, pub transparent_colour: Option<Color>, @@ -17,6 +27,7 @@ impl Image { pub(crate) fn new<R: Read>( parser: &mut EventReader<R>, attrs: Vec<OwnedAttribute>, + path_relative_to: impl AsRef<Path>, ) -> Result<Image, TiledError> { let (c, (s, w, h)) = get_attrs!( attrs, @@ -28,12 +39,12 @@ impl Image { ("width", width, |v:String| v.parse().ok()), ("height", height, |v:String| v.parse().ok()), ], - TiledError::MalformedAttributes("image must have a source, width and height with correct types".to_string()) + TiledError::MalformedAttributes("Image must have a source, width and height with correct types".to_string()) ); parse_tag!(parser, "image", { "" => |_| Ok(()) }); Ok(Image { - source: s, + source: path_relative_to.as_ref().join(s), width: w, height: h, transparent_colour: c, diff --git a/src/layers.rs b/src/layers.rs index 19bb2ce266f4a97f2f57f9a093eba13eabb8bbbd..cd494087105883d65f6592ab77fdc4b3e6c51c3b 100644 --- a/src/layers.rs +++ b/src/layers.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, io::Read}; +use std::{collections::HashMap, io::Read, path::Path}; use xml::{attribute::OwnedAttribute, EventReader}; @@ -134,6 +134,7 @@ impl ImageLayer { parser: &mut EventReader<R>, attrs: Vec<OwnedAttribute>, layer_index: u32, + path_relative_to: Option<&Path>, ) -> Result<ImageLayer, TiledError> { let ((o, v, ox, oy), n) = get_attrs!( attrs, @@ -151,7 +152,7 @@ impl ImageLayer { let mut image: Option<Image> = None; parse_tag!(parser, "imagelayer", { "image" => |attrs| { - image = Some(Image::new(parser, attrs)?); + image = Some(Image::new(parser, attrs, path_relative_to.ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_string()})?)?); Ok(()) }, "properties" => |_| { diff --git a/src/map.rs b/src/map.rs index ec9d3c9c5714e668c5c5778b59ff877888bb09b4..f717d8b445c6eb6832bcbe92b3ca877b5ec1b060 100644 --- a/src/map.rs +++ b/src/map.rs @@ -43,17 +43,17 @@ pub struct Map { /// The background color of this map, if any. pub background_color: Option<Color>, pub infinite: bool, - /// Where this map was loaded from. - /// If fully embedded (loaded with path = `None`), this will return `None`. - pub source: Option<PathBuf>, } impl Map { /// Parse a buffer hopefully containing the contents of a Tiled file and try to - /// parse it. This augments `parse` with a file location: some engines + /// parse it. This augments `parse_file` with a custom reader: some engines /// (e.g. Amethyst) simply hand over a byte stream (and file location) for parsing, /// in which case this function may be required. - /// The path may be skipped if the map is fully embedded (Doesn't refer to external files). + /// + /// The path is used for external dependencies such as tilesets or images, and may be skipped + /// if the map is fully embedded (Doesn't refer to external files). If a map *does* refer to + /// external files and a path is not given, the function will return [TiledError::SourceRequired]. pub fn parse_reader<R: Read>(reader: R, path: Option<&Path>) -> Result<Self, TiledError> { let mut parser = EventReader::new(reader); loop { @@ -75,9 +75,8 @@ impl Map { } } - /// Parse a file hopefully containing a Tiled map and try to parse it. If the - /// file has an external tileset, the tileset file will be loaded using a path - /// relative to the map file's path. + /// Parse a file hopefully containing a Tiled map and try to parse it. All external + /// files will be loaded relative to the path given. pub fn parse_file(path: impl AsRef<Path>) -> Result<Self, TiledError> { let file = File::open(path.as_ref()) .map_err(|_| TiledError::Other(format!("Map file not found: {:?}", path.as_ref())))?; @@ -106,6 +105,8 @@ impl Map { TiledError::MalformedAttributes("map must have a version, width and height with correct types".to_string()) ); + let source_path = map_path.and_then(|p| p.parent()); + let mut tilesets = Vec::new(); let mut layers = Vec::new(); let mut image_layers = Vec::new(); @@ -114,7 +115,7 @@ impl Map { let mut layer_index = 0; parse_tag!(parser, "map", { "tileset" => |attrs| { - tilesets.push(Tileset::parse_xml(parser, attrs, map_path)?); + tilesets.push(Tileset::parse_xml(parser, attrs, source_path)?); Ok(()) }, "layer" => |attrs| { @@ -123,7 +124,7 @@ impl Map { Ok(()) }, "imagelayer" => |attrs| { - image_layers.push(ImageLayer::new(parser, attrs, layer_index)?); + image_layers.push(ImageLayer::new(parser, attrs, layer_index, source_path)?); layer_index += 1; Ok(()) }, @@ -151,7 +152,6 @@ impl Map { properties, background_color: c, infinite: infinite.unwrap_or(false), - source: map_path.and_then(|p| Some(p.to_owned())), }) } diff --git a/src/tile.rs b/src/tile.rs index 22e5620039ddce3fb1c857701b84417547c6df62..a90d25d00b7ae61b1b6deaaf7725b362e9823c8d 100644 --- a/src/tile.rs +++ b/src/tile.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, io::Read}; +use std::{collections::HashMap, io::Read, path::Path}; use xml::{attribute::OwnedAttribute, EventReader}; @@ -26,6 +26,7 @@ impl Tile { pub(crate) fn new<R: Read>( parser: &mut EventReader<R>, attrs: Vec<OwnedAttribute>, + path_relative_to: Option<&Path>, ) -> Result<Tile, TiledError> { let ((tile_type, probability), id) = get_attrs!( attrs, @@ -45,7 +46,7 @@ impl Tile { let mut animation = None; parse_tag!(parser, "tile", { "image" => |attrs| { - image = Some(Image::new(parser, attrs)?); + image = Some(Image::new(parser, attrs, path_relative_to.ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_owned()})?)?); Ok(()) }, "properties" => |_| { diff --git a/src/tileset.rs b/src/tileset.rs index 56ff1e69e1af4da225748c43c23a0638d27f8a0e..1c49c3a18df3ef22f322d15a7318e2a2b8dd297e 100644 --- a/src/tileset.rs +++ b/src/tileset.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::fs::File; use std::io::Read; -use std::path::Path; +use std::path::{Path, PathBuf}; use xml::attribute::OwnedAttribute; use xml::reader::XmlEvent; @@ -16,7 +16,7 @@ use crate::util::*; /// A tileset, usually the tilesheet image. #[derive(Debug, PartialEq, Clone)] pub struct Tileset { - /// The GID of the first tile stored + /// The GID of the first tile stored. pub first_gid: u32, pub name: String, pub tile_width: u32, @@ -25,15 +25,21 @@ pub struct Tileset { pub margin: u32, pub tilecount: Option<u32>, pub columns: u32, + /// A tileset can either: /// * have a single spritesheet `image` in `tileset` ("regular" tileset); /// * have zero images in `tileset` and one `image` per `tile` ("image collection" tileset). /// + /// -------- /// - Source: [tiled issue #2117](https://github.com/mapeditor/tiled/issues/2117) /// - Source: [`columns` documentation](https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tileset) pub image: Option<Image>, pub tiles: Vec<Tile>, pub properties: Properties, + + /// Where this tileset was loaded from. + /// If fully embedded (loaded with path = `None`), this will return `None`. + pub source: Option<PathBuf>, } impl Tileset { @@ -43,21 +49,40 @@ impl Tileset { /// map. You must pass in `first_gid`. If you do not need to use gids for anything, /// passing in 1 will work fine. pub fn parse<R: Read>(reader: R, first_gid: u32) -> Result<Self, TiledError> { - Tileset::new_external(reader, first_gid) + Tileset::new_external(reader, first_gid, None) + } + + /// Parse a buffer hopefully containing the contents of a Tiled tileset. + /// + /// External tilesets do not have a firstgid attribute. That lives in the + /// map. You must pass in `first_gid`. If you do not need to use gids for anything, + /// passing in 1 will work fine. + pub fn parse_with_path<R: Read>( + reader: R, + first_gid: u32, + path: impl AsRef<Path>, + ) -> Result<Self, TiledError> { + Tileset::new_external(reader, first_gid, Some(path.as_ref())) } pub(crate) fn parse_xml<R: Read>( parser: &mut EventReader<R>, attrs: Vec<OwnedAttribute>, - map_path: Option<&Path>, + path_relative_to: Option<&Path>, ) -> Result<Tileset, TiledError> { - Tileset::parse_xml_embedded(parser, &attrs) - .or_else(|_| Tileset::parse_xml_reference(&attrs, map_path)) + Tileset::parse_xml_embedded(parser, &attrs, path_relative_to).or_else(|err| { + if matches!(err, TiledError::MalformedAttributes(_)) { + Tileset::parse_xml_reference(&attrs, path_relative_to) + } else { + Err(err) + } + }) } fn parse_xml_embedded<R: Read>( parser: &mut EventReader<R>, attrs: &Vec<OwnedAttribute>, + path_relative_to: Option<&Path>, ) -> Result<Tileset, TiledError> { let ((spacing, margin, tilecount, columns), (first_gid, name, width, height)) = get_attrs!( attrs, @@ -81,7 +106,7 @@ impl Tileset { let mut properties = HashMap::new(); parse_tag!(parser, "tileset", { "image" => |attrs| { - image = Some(Image::new(parser, attrs)?); + image = Some(Image::new(parser, attrs, path_relative_to.ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_string()})?)?); Ok(()) }, "properties" => |_| { @@ -89,7 +114,7 @@ impl Tileset { Ok(()) }, "tile" => |attrs| { - tiles.push(Tile::new(parser, attrs)?); + tiles.push(Tile::new(parser, attrs, path_relative_to)?); Ok(()) }, }); @@ -118,12 +143,13 @@ impl Tileset { image, tiles, properties, + source: None, }) } fn parse_xml_reference( attrs: &Vec<OwnedAttribute>, - map_path: Option<&Path>, + path_relative_to: Option<&Path>, ) -> Result<Tileset, TiledError> { let ((), (first_gid, source)) = get_attrs!( attrs, @@ -132,20 +158,28 @@ impl Tileset { ("firstgid", first_gid, |v:String| v.parse().ok()), ("source", name, |v| Some(v)), ], - TiledError::MalformedAttributes("tileset must have a firstgid, name tile width and height with correct types".to_string()) + TiledError::MalformedAttributes("Tileset reference must have a firstgid and source with correct types".to_string()) ); - let tileset_path = map_path.ok_or(TiledError::Other("Maps with external tilesets must know their file location. See parse_with_path(Path).".to_string()))?.with_file_name(source); + let tileset_path = path_relative_to + .ok_or(TiledError::SourceRequired { + object_to_parse: "Tileset".to_string(), + })? + .join(source); let file = File::open(&tileset_path).map_err(|_| { TiledError::Other(format!( "External tileset file not found: {:?}", tileset_path )) })?; - Tileset::new_external(file, first_gid) + Tileset::new_external(file, first_gid, Some(&tileset_path)) } - pub(crate) fn new_external<R: Read>(file: R, first_gid: u32) -> Result<Tileset, TiledError> { + pub(crate) fn new_external<R: Read>( + file: R, + first_gid: u32, + path: Option<&Path>, + ) -> Result<Self, TiledError> { let mut tileset_parser = EventReader::new(file); loop { match tileset_parser @@ -156,10 +190,11 @@ impl Tileset { name, attributes, .. } => { if name.local_name == "tileset" { - return Tileset::parse_external_tileset( + return Self::parse_external_tileset( first_gid, &mut tileset_parser, &attributes, + path, ); } } @@ -177,6 +212,7 @@ impl Tileset { first_gid: u32, parser: &mut EventReader<R>, attrs: &Vec<OwnedAttribute>, + path: Option<&Path>, ) -> Result<Tileset, TiledError> { let ((spacing, margin, tilecount, columns), (name, width, height)) = get_attrs!( attrs, @@ -194,12 +230,14 @@ impl Tileset { TiledError::MalformedAttributes("tileset must have a firstgid, name tile width and height with correct types".to_string()) ); + let source_path = path.and_then(|p| p.parent()); + let mut image = Option::None; let mut tiles = Vec::new(); let mut properties = HashMap::new(); parse_tag!(parser, "tileset", { "image" => |attrs| { - image = Some(Image::new(parser, attrs)?); + image = Some(Image::new(parser, attrs, source_path.ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_string()})?)?); Ok(()) }, "properties" => |_| { @@ -207,7 +245,7 @@ impl Tileset { Ok(()) }, "tile" => |attrs| { - tiles.push(Tile::new(parser, attrs)?); + tiles.push(Tile::new(parser, attrs, path)?); Ok(()) }, }); @@ -236,6 +274,7 @@ impl Tileset { image, tiles: tiles, properties, + source: path.and_then(|x| Some(x.to_owned())), }) } } diff --git a/tests/lib.rs b/tests/lib.rs index ec19f17f88a669b0f2aad0c30ff12c6c423d90c2..eb4aa6d7f5cb3381fbd695898d32e90b6013f4c0 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -1,5 +1,5 @@ -use std::fs::File; use std::path::Path; +use std::{fs::File, path::PathBuf}; use tiled::{ error::TiledError, layers::LayerData, map::Map, properties::PropertyValue, tileset::Tileset, }; @@ -11,11 +11,11 @@ fn parse_map_without_source(p: impl AsRef<Path>) -> Result<Map, TiledError> { #[test] fn test_gzip_and_zlib_encoded_and_raw_are_the_same() { - let z = parse_map_without_source("assets/tiled_base64_zlib.tmx").unwrap(); - let g = parse_map_without_source("assets/tiled_base64_gzip.tmx").unwrap(); - let r = parse_map_without_source("assets/tiled_base64.tmx").unwrap(); - let zstd = parse_map_without_source("assets/tiled_base64_zstandard.tmx").unwrap(); - let c = parse_map_without_source("assets/tiled_csv.tmx").unwrap(); + let z = Map::parse_file("assets/tiled_base64_zlib.tmx").unwrap(); + let g = Map::parse_file("assets/tiled_base64_gzip.tmx").unwrap(); + let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); + let zstd = Map::parse_file("assets/tiled_base64_zstandard.tmx").unwrap(); + let c = Map::parse_file("assets/tiled_csv.tmx").unwrap(); assert_eq!(z, g); assert_eq!(z, r); assert_eq!(z, c); @@ -37,17 +37,30 @@ fn test_gzip_and_zlib_encoded_and_raw_are_the_same() { #[test] fn test_external_tileset() { - let r = parse_map_without_source("assets/tiled_base64.tmx").unwrap(); + let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); let mut e = Map::parse_file("assets/tiled_base64_external.tmx").unwrap(); - e.source = None; - // Compare everything BUT source + e.tilesets[0].source = None; assert_eq!(r, e); } +#[test] +fn test_sources() { + let e = Map::parse_file("assets/tiled_base64_external.tmx").unwrap(); + assert_eq!( + e.tilesets[0].source, + Some(PathBuf::from("assets/tilesheet.tsx")) + ); + assert_eq!( + e.tilesets[0].image.as_ref().unwrap().source, + PathBuf::from("assets/tilesheet.png") + ); +} + #[test] fn test_just_tileset() { - let r = parse_map_without_source("assets/tiled_base64.tmx").unwrap(); - let t = Tileset::parse(File::open(Path::new("assets/tilesheet.tsx")).unwrap(), 1).unwrap(); + let r = Map::parse_file("assets/tiled_base64_external.tmx").unwrap(); + let path = "assets/tilesheet.tsx"; + let t = Tileset::parse_with_path(File::open(path).unwrap(), 1, path).unwrap(); assert_eq!(r.tilesets[0], t); } @@ -70,7 +83,7 @@ fn test_infinite_tileset() { #[test] fn test_image_layers() { - let r = parse_map_without_source("assets/tiled_image_layers.tmx").unwrap(); + let r = Map::parse_file("assets/tiled_image_layers.tmx").unwrap(); assert_eq!(r.image_layers.len(), 2); { let first = &r.image_layers[0]; @@ -88,7 +101,7 @@ fn test_image_layers() { .image .as_ref() .expect(&format!("{}'s image shouldn't be None", second.name)); - assert_eq!(image.source, "tilesheet.png"); + assert_eq!(image.source, PathBuf::from("assets/tilesheet.png")); assert_eq!(image.width, 448); assert_eq!(image.height, 192); } @@ -96,7 +109,7 @@ fn test_image_layers() { #[test] fn test_tile_property() { - let r = parse_map_without_source("assets/tiled_base64.tmx").unwrap(); + let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) = r.tilesets[0].tiles[0].properties.get("a tile property") { @@ -109,7 +122,7 @@ fn test_tile_property() { #[test] fn test_layer_property() { - let r = parse_map_without_source(&Path::new("assets/tiled_base64.tmx")).unwrap(); + let r = Map::parse_file(&Path::new("assets/tiled_base64.tmx")).unwrap(); let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) = r.layers[0].properties.get("prop3") { v.clone() @@ -121,7 +134,7 @@ fn test_layer_property() { #[test] fn test_object_group_property() { - let r = parse_map_without_source("assets/tiled_object_groups.tmx").unwrap(); + let r = Map::parse_file("assets/tiled_object_groups.tmx").unwrap(); let prop_value: bool = if let Some(&PropertyValue::BoolValue(ref v)) = r.object_groups[0] .properties .get("an object group property") @@ -134,7 +147,7 @@ fn test_object_group_property() { } #[test] fn test_tileset_property() { - let r = parse_map_without_source("assets/tiled_base64.tmx").unwrap(); + let r = Map::parse_file("assets/tiled_base64.tmx").unwrap(); let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) = r.tilesets[0].properties.get("tileset property") {