Newer
Older
use xml::attribute::OwnedAttribute;
use xml::reader::XmlEvent;
use xml::EventReader;
use crate::error::TiledError;
use crate::image::Image;
use crate::tile::TileData;
use crate::{util::*, Gid, Tile};
/// A collection of tiles for usage in maps and template objects.
///
/// Also see the [TMX docs](https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tileset).
#[derive(Debug, PartialEq, Clone)]
pub struct Tileset {
/// The name of the tileset, set by the user.
/// The (maximum) width in pixels of the tiles in this tileset. Irrelevant for [image collection]
/// tilesets.
///
/// [image collection]: Self::image
/// The (maximum) height in pixels of the tiles in this tileset. Irrelevant for [image collection]
/// tilesets.
///
/// [image collection]: Self::image
/// The spacing in pixels between the tiles in this tileset (applies to the tileset image).
/// Irrelevant for image collection tilesets.
/// The margin around the tiles in this tileset (applies to the tileset image).
/// Irrelevant for image collection tilesets.
/// The number of tiles in this tileset. Note that tile IDs don't always have a connection with
/// the tile count, and as such there may be tiles with an ID bigger than the tile count.
/// The number of tile columns in the tileset. Editable for image collection tilesets, otherwise
/// calculated using [image](Self::image) width, [tile width](Self::tile_width),
/// [spacing](Self::spacing) and [margin](Self::margin).
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>,
/// All the tiles present in this tileset, indexed by their local IDs.
/// The custom properties of the tileset.
pub(crate) enum EmbeddedParseResultType {
ExternalReference { tileset_path: PathBuf },
Embedded { tileset: Tileset },
}
pub(crate) struct EmbeddedParseResult {
pub first_gid: Gid,
pub result_type: EmbeddedParseResultType,
}
/// Internal structure for holding mid-parse information.
struct TilesetProperties {
spacing: Option<u32>,
margin: Option<u32>,
columns: Option<u32>,
name: String,
tile_width: u32,
tile_height: u32,
/// The root all non-absolute paths contained within the tileset are relative to.
root_path: PathBuf,
/// Parses a tileset out of a reader hopefully containing the contents of a Tiled tileset.
/// Uses the `path` parameter as the root for any relative paths found in the tileset.
///
/// ## Example
/// ```
/// use std::fs::File;
/// use std::path::PathBuf;
/// use std::io::BufReader;
/// use tiled::Tileset;
///
/// let path = "assets/tilesheet.tsx";
/// let reader = BufReader::new(File::open(path).unwrap());
/// let tileset = Tileset::parse_reader(reader, path).unwrap();
///
/// assert_eq!(tileset.image.unwrap().source, PathBuf::from("assets/tilesheet.png"));
/// ```
pub fn parse_reader<R: Read>(reader: R, path: impl AsRef<Path>) -> Result<Self, TiledError> {
let mut tileset_parser = EventReader::new(reader);
loop {
match tileset_parser
.next()
.map_err(TiledError::XmlDecodingError)?
{
XmlEvent::StartElement {
name, attributes, ..
} if name.local_name == "tileset" => {
return Self::parse_external_tileset(
&mut tileset_parser.into_iter(),
&attributes,
}
XmlEvent::EndDocument => {
return Err(TiledError::PrematureEnd(
"Tileset Document ended before map was parsed".to_string(),
))
}
_ => {}
}
}
}
/// Gets the tile with the specified ID from the tileset.
pub fn get_tile(&self, id: u32) -> Option<Tile> {
self.tiles.get(&id).map(|data| Tile::new(self, data))
}
}
impl Tileset {
pub(crate) fn parse_xml_in_map(
parser: &mut impl Iterator<Item = XmlEventResult>,
attrs: Vec<OwnedAttribute>,
map_path: &Path,
) -> Result<EmbeddedParseResult, TiledError> {
Tileset::parse_xml_embedded(parser, &attrs, map_path).or_else(|err| {
if matches!(err, TiledError::MalformedAttributes(_)) {
Tileset::parse_xml_reference(&attrs, map_path)
} else {
Err(err)
}
})
}
fn parse_xml_embedded(
parser: &mut impl Iterator<Item = XmlEventResult>,
) -> Result<EmbeddedParseResult, TiledError> {
let ((spacing, margin, columns, name), (tilecount, first_gid, tile_width, tile_height)) = get_attrs!(
attrs,
optionals: [
("spacing", spacing, |v:String| v.parse().ok()),
("margin", margin, |v:String| v.parse().ok()),
("columns", columns, |v:String| v.parse().ok()),
("tilecount", tilecount, |v:String| v.parse().ok()),
("firstgid", first_gid, |v:String| v.parse().ok().map(|n| Gid(n))),
("tilewidth", width, |v:String| v.parse().ok()),
("tileheight", height, |v:String| v.parse().ok()),
],
TiledError::MalformedAttributes("tileset must have a firstgid, name tile width and height with correct types".to_string())
);
let root_path = map_path
.parent()
.ok_or(TiledError::PathIsNotFile)?
.to_owned();
Self::finish_parsing_xml(
parser,
TilesetProperties {
spacing,
margin,
columns,
tilecount,
tile_height,
tile_width,
},
.map(|tileset| EmbeddedParseResult {
first_gid,
result_type: EmbeddedParseResultType::Embedded { tileset },
})
) -> Result<EmbeddedParseResult, TiledError> {
("firstgid", first_gid, |v:String| v.parse().ok().map(|n| Gid(n))),
TiledError::MalformedAttributes("Tileset reference must have a firstgid and source with correct types".to_string())
let tileset_path = map_path
.parent()
.ok_or(TiledError::PathIsNotFile)?
Ok(EmbeddedParseResult {
first_gid,
result_type: EmbeddedParseResultType::ExternalReference { tileset_path },
})
fn parse_external_tileset(
parser: &mut impl Iterator<Item = XmlEventResult>,
let ((spacing, margin, columns, name), (tilecount, tile_width, tile_height)) = get_attrs!(
attrs,
optionals: [
("spacing", spacing, |v:String| v.parse().ok()),
("margin", margin, |v:String| v.parse().ok()),
("columns", columns, |v:String| v.parse().ok()),
("tilecount", tilecount, |v:String| v.parse().ok()),
("tilewidth", width, |v:String| v.parse().ok()),
("tileheight", height, |v:String| v.parse().ok()),
],
TiledError::MalformedAttributes("tileset must have a name, tile width and height with correct types".to_string())
let root_path = path.parent().ok_or(TiledError::PathIsNotFile)?.to_owned();
Self::finish_parsing_xml(
parser,
TilesetProperties {
spacing,
margin,
columns,
tilecount,
tile_height,
tile_width,
},
)
}
fn finish_parsing_xml(
parser: &mut impl Iterator<Item = XmlEventResult>,
) -> Result<Tileset, TiledError> {
let mut image = Option::None;
let mut tiles = HashMap::with_capacity(prop.tilecount as usize);
"image" => |attrs| {
image = Some(Image::new(parser, attrs, &prop.root_path)?);
Ok(())
},
"properties" => |_| {
properties = parse_properties(parser)?;
Ok(())
},
"tile" => |attrs| {
let (id, tile) = TileData::new(parser, attrs, &prop.root_path)?;
tiles.insert(id, tile);
Ok(())
},
});
// A tileset is considered an image collection tileset if there is no image attribute (because its tiles do).
let is_image_collection_tileset = image.is_none();
if !is_image_collection_tileset {
for tile_id in 0..prop.tilecount {
tiles.entry(tile_id).or_default();
}
}
let margin = prop.margin.unwrap_or(0);
let spacing = prop.spacing.unwrap_or(0);
let columns = prop
.columns
.map(Ok)
.unwrap_or_else(|| Self::calculate_columns(&image, prop.tile_width, margin, spacing))?;
name: prop.name,
tile_width: prop.tile_width,
tile_height: prop.tile_height,
spacing,
margin,
columns,
image,
fn calculate_columns(
image: &Option<Image>,
tile_width: u32,
margin: u32,
spacing: u32,
) -> Result<u32, TiledError> {
image
.as_ref()
.ok_or(TiledError::MalformedAttributes(
"No <image> nor columns attribute in <tileset>".to_string(),
))
.and_then(|image| Ok((image.width as u32 - margin + spacing) / (tile_width + spacing)))
}