diff --git a/pytiled_parser/objects.py b/pytiled_parser/objects.py
new file mode 100644
index 0000000..0e7667e
--- /dev/null
+++ b/pytiled_parser/objects.py
@@ -0,0 +1,569 @@
+"""pytiled_parser objects for Tiled maps.
+"""
+
+# pylint: disable=too-few-public-methods
+
+from pathlib import Path
+from typing import Dict, List, NamedTuple, Optional, Union
+
+import attr
+
+# See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#data
+TileLayerGrid = List[List[int]]
+
+
+class Color(NamedTuple):
+ """Color object.
+
+ Attributes:
+ red: Red, between 1 and 255.
+ green: Green, between 1 and 255.
+ blue: Blue, between 1 and 255.
+ alpha: Alpha, between 1 and 255.
+ """
+
+ red: int
+ green: int
+ blue: int
+ alpha: int
+
+
+class OrderedPair(NamedTuple):
+ """OrderedPair NamedTuple.
+
+ Attributes:
+ x: X coordinate.
+ y: Y coordinate.
+ """
+
+ x: Union[int, float]
+ y: Union[int, float]
+
+
+class Property(NamedTuple):
+ """Property NamedTuple.
+
+ Attributes:
+ name: Name of property
+ value: Value of property
+ """
+
+ name: str
+ value: str
+
+
+class Size(NamedTuple):
+ """Size NamedTuple.
+
+ Attributes:
+ width: The width of the object.
+ size: The height of the object.
+ """
+
+ width: Union[int, float]
+ height: Union[int, float]
+
+
+@attr.s(auto_attribs=True)
+class Template:
+ """FIXME TODO"""
+
+
+@attr.s(auto_attribs=True)
+class Chunk:
+ """Chunk object for infinite maps.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#chunk
+
+ Attributes:
+ location: Location of chunk in tiles.
+ width: The width of the chunk in tiles.
+ height: The height of the chunk in tiles.
+ layer_data: The global tile IDs in chunky according to row.
+ """
+
+ location: OrderedPair
+ width: int
+ height: int
+ chunk_data: TileLayerGrid
+
+
+@attr.s(auto_attribs=True)
+class Image:
+ """Image object.
+
+ pytiled_parser does not support embedded data in image elements at this time,
+ even though the TMX format technically does.
+
+ Attributes:
+ source: The reference to the tileset image file. Note that this is a relative
+ path compared to FIXME
+ trans: Defines a specific color that is treated as transparent.
+ width: The image width in pixels (optional, used for tile index correction when
+ the image changes).
+ height: The image height in pixels (optional).
+ """
+
+ source: str
+ size: Optional[Size] = None
+ trans: Optional[str] = None
+ width: Optional[int] = None
+ height: Optional[int] = None
+
+
+Properties = Dict[str, Union[int, float, Color, Path, str]]
+
+
+class Grid(NamedTuple):
+ """Contains info for isometric maps.
+
+ This element is only used in case of isometric orientation, and determines how tile
+ overlays for terrain and collision information are rendered.
+
+ Args:
+ orientation: Orientation of the grid for the tiles in this tileset (orthogonal
+ or isometric).
+ width: Width of a grid cell.
+ height: Height of a grid cell.
+ """
+
+ orientation: str
+ width: int
+ height: int
+
+
+class Terrain(NamedTuple):
+ """Terrain object.
+
+ Args:
+ name: The name of the terrain type.
+ tile: The local tile-id of the tile that represents the terrain visually.
+ """
+
+ name: str
+ tile: int
+
+
+class Frame(NamedTuple):
+ """Animation Frame object.
+
+ This is only used as a part of an animation for Tile objects.
+
+ Args:
+ tile_id: The local ID of a tile within the parent tile set object.
+ duration: How long in milliseconds this frame should be displayed before
+ advancing to the next frame.
+ """
+
+ tile_id: int
+ duration: int
+
+
+@attr.s(auto_attribs=True)
+class TileTerrain:
+ """Defines each corner of a tile by Terrain index in
+ 'TileSet.terrain_types'.
+
+ Defaults to 'None'. 'None' means that corner has no terrain.
+
+ Attributes:
+ top_left: Top left terrain type.
+ top_right: Top right terrain type.
+ bottom_left: Bottom left terrain type.
+ bottom_right: Bottom right terrain type.
+ """
+
+ top_left: Optional[int] = None
+ top_right: Optional[int] = None
+ bottom_left: Optional[int] = None
+ bottom_right: Optional[int] = None
+
+
+@attr.s(auto_attribs=True, kw_only=True)
+class Layer:
+ # FIXME:this docstring appears to be innacurate
+ """Class that all layers inherit from.
+
+ Args:
+ id: Unique ID of the layer. Each layer that added to a map gets a unique id.
+ Even if a layer is deleted, no layer ever gets the same ID.
+ name: The name of the layer object.
+ tiled_objects: List of tiled_objects in the layer.
+ offset: Rendering offset of the layer object in pixels.
+ opacity: Decimal value between 0 and 1 to determine opacity. 1 is completely
+ opaque, 0 is completely transparent.
+ properties: Properties for the layer.
+ color: The color used to display the objects in this group.
+ draworder: Whether the objects are drawn according to the order of the object
+ elements in the object group element ('manual'), or sorted by their
+ y-coordinate ('topdown'). Defaults to 'topdown'.
+ See:
+ https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order
+ for more info.
+
+ """
+
+ id_: int
+ name: str
+
+ offset: Optional[OrderedPair]
+ opacity: Optional[float]
+ properties: Optional[Properties]
+
+
+LayerData = Union[TileLayerGrid, List[Chunk]]
+"""The tile data for one layer.
+
+Either a 2 dimensional array of integers representing the global tile IDs
+ for a TileLayerGrid, or a list of chunks for an infinite map layer.
+"""
+
+
+@attr.s(auto_attribs=True, kw_only=True)
+class TileLayer(Layer):
+ """Tile map layer containing tiles.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#layer
+
+ Args:
+ size: The width of the layer in tiles. The same as the map width unless map is
+ infitite.
+ data: Either an 2 dimensional array of integers representing the global tile
+ IDs for the map layer, or a list of chunks for an infinite map.
+ """
+
+ size: Size
+ layer_data: LayerData
+
+
+@attr.s(auto_attribs=True, kw_only=True)
+class TiledObject:
+ """TiledObject object.
+
+ See:
+ https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#object
+
+ Args:
+ id: Unique ID of the object. Each object that is placed on a map gets a unique
+ id. Even if an object was deleted, no object gets the same ID.
+ gid: Global tiled object ID.
+ location: The location of the object in pixels.
+ size: The width of the object in pixels (default: (0, 0)).
+ rotation: The rotation of the object in degrees clockwise (default: 0).
+ opacity: The opacity of the object. (default: 255)
+ name: The name of the object.
+ type: The type of the object.
+ properties: The properties of the TiledObject.
+ template: A reference to a Template object FIXME
+ """
+
+ id_: int
+ gid: Optional[int] = None
+
+ location: OrderedPair
+ size: Optional[Size] = None
+ rotation: Optional[float] = None
+ opacity: Optional[float] = None
+
+ name: Optional[str] = None
+ type: Optional[str] = None
+
+ properties: Optional[Properties] = None
+ template: Optional[Template] = None
+
+
+@attr.s()
+class RectangleObject(TiledObject):
+ """Rectangle shape defined by a point, width, and height.
+
+ See: https://doc.mapeditor.org/en/stable/manual/objects/#insert-rectangle
+ (objects in tiled are rectangles by default, so there is no specific
+ documentation on the tmx-map-format page for it.)
+ """
+
+
+@attr.s()
+class ElipseObject(TiledObject):
+ """Elipse shape defined by a point, width, and height.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#ellipse
+ """
+
+
+@attr.s()
+class PointObject(TiledObject):
+ """Point defined by a point (x,y).
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#point
+ """
+
+
+@attr.s(auto_attribs=True, kw_only=True)
+class TileImageObject(TiledObject):
+ """Polygon shape defined by a set of connections between points.
+
+ See: https://doc.mapeditor.org/en/stable/manual/objects/#insert-tile
+
+ Attributes:
+ gid: Refference to a global tile id.
+ """
+
+ gid: int
+
+
+@attr.s(auto_attribs=True, kw_only=True)
+class PolygonObject(TiledObject):
+ """Polygon shape defined by a set of connections between points.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polygon
+
+ Attributes:
+ points: FIXME
+ """
+
+ points: List[OrderedPair]
+
+
+@attr.s(auto_attribs=True, kw_only=True)
+class PolylineObject(TiledObject):
+ """Polyline defined by a set of connections between points.
+
+ See:
+ https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#polyline
+
+ Attributes:
+ points: List of coordinates relative to the location of the object.
+ """
+
+ points: List[OrderedPair]
+
+
+@attr.s(auto_attribs=True, kw_only=True)
+class TextObject(TiledObject):
+ """Text object with associated settings.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#text
+ and https://doc.mapeditor.org/en/stable/manual/objects/#insert-text
+
+ Attributes:
+ font_family: The font family used (default: "sans-serif")
+ font_size: The size of the font in pixels. (default: 16)
+ wrap: Whether word wrapping is enabled. (default: False)
+ color: Color of the text. (default: #000000)
+ bold: Whether the font is bold. (default: False)
+ italic: Whether the font is italic. (default: False)
+ underline: Whether the text is underlined. (default: False)
+ strike_out: Whether the text is striked-out. (default: False)
+ kerning: Whether kerning should be used while rendering the text. (default:
+ False)
+ horizontal_align: Horizontal alignment of the text (default: "left")
+ vertical_align: Vertical alignment of the text (defalt: "top")
+ """
+
+ text: str
+ font_family: str = "sans-serif"
+ font_size: int = 16
+ wrap: bool = False
+ color: str = "#000000"
+ bold: bool = False
+ italic: bool = False
+ underline: bool = False
+ strike_out: bool = False
+ kerning: bool = False
+ horizontal_align: str = "left"
+ vertical_align: str = "top"
+
+
+@attr.s(auto_attribs=True, kw_only=True)
+class ObjectLayer(Layer):
+ """
+ TiledObject Group Object.
+
+ The object group is in fact a map layer, and is hence called "object layer" in
+ Tiled.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup
+
+ Args:
+ tiled_objects: List of tiled_objects in the layer.
+ offset: Rendering offset of the layer object in pixels.
+ color: The color used to display the objects in this group. FIXME: editor only?
+ draworder: Whether the objects are drawn according to the order of the object
+ elements in the object group element ('manual'), or sorted by their
+ y-coordinate ('topdown'). Defaults to 'topdown'. See:
+ https://doc.mapeditor.org/en/stable/manual/objects/#changing-stacking-order
+ for more info.
+
+ """
+
+ tiled_objects: List[TiledObject]
+
+ color: Optional[str] = None
+ draw_order: Optional[str] = "topdown"
+
+
+@attr.s(auto_attribs=True, kw_only=True)
+class LayerGroup(Layer):
+ """Layer Group.
+
+ A LayerGroup can be thought of as a layer that contains layers
+ (potentially including other LayerGroups).
+
+ Offset and opacity recursively affect child layers.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#group
+
+ Attributes:
+ Layers: Layers in group.
+ """
+
+ layers: Optional[List[Union["LayerGroup", Layer, ObjectLayer]]]
+
+
+@attr.s(auto_attribs=True, kw_only=True)
+class ImageLayer(Layer):
+ """Image Layer.
+
+ An image layer displays a single image.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#imagelayer
+
+ Attributes:
+ image: the image to display for this layer.
+ """
+
+ image: Image
+
+
+@attr.s(auto_attribs=True)
+class TileSet:
+ """Object for storing a TSX with all associated collision data.
+
+ Args:
+ name: The name of this tileset.
+ max_tile_size: The maximum size of a tile in this tile set in pixels.
+ spacing: The spacing in pixels between the tiles in this tileset (applies to
+ the tileset image).
+ margin: The margin around the tiles in this tileset (applies to the tileset
+ image).
+ tile_count: The number of tiles in this tileset.
+ columns: The number of tile columns in the tileset. For image collection
+ tilesets it is editable and is used when displaying the tileset.
+ grid: Only used in case of isometric orientation, and determines how tile
+ overlays for terrain and collision information are rendered.
+ tileoffset: Used to specify an offset in pixels when drawing a tile from the
+ tileset. When not present, no offset is applied.
+ image: Used for spritesheet tile sets.
+ terrain_types: List of of terrain types which can be referenced from the
+ terrain attribute of the tile object. Ordered according to the terrain
+ element's appearance in the TSX file.
+ tiles: Dict of Tile objects by Tile.id.
+ tsx_file: Path of the file containing the tileset, None if loaded internally
+ from a map
+ parent_dir: Path of the parent directory of the file containing the tileset,
+ None if loaded internally from a map
+ """
+
+ name: str
+ max_tile_size: Size
+
+ spacing: Optional[int] = None
+ margin: Optional[int] = None
+ tile_count: Optional[int] = None
+ columns: Optional[int] = None
+ tile_offset: Optional[OrderedPair] = None
+ grid: Optional[Grid] = None
+ properties: Optional[Properties] = None
+ image: Optional[Image] = None
+ terrain_types: Optional[List[Terrain]] = None
+ tiles: Optional[Dict[int, "Tile"]] = None
+ tsx_file: Path = None
+ parent_dir: Path = None
+
+
+TileSetDict = Dict[int, TileSet]
+
+
+@attr.s(auto_attribs=True, kw_only=True)
+class Tile:
+ """Individual tile object.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tile
+
+ Args:
+ id: The local tile ID within its tileset.
+ type: The type of the tile. Refers to an object type and is used by tile
+ objects.
+ terrain: Defines the terrain type of each corner of the tile.
+ animation: Each tile can have exactly one animation associated with it.
+ """
+
+ id_: int
+ type_: Optional[str] = None
+ terrain: Optional[TileTerrain] = None
+ animation: Optional[List[Frame]] = None
+ objectgroup: Optional[List[TiledObject]] = None
+ image: Optional[Image] = None
+ properties: Optional[List[Property]] = None
+ tileset: Optional[TileSet] = None
+ flipped_horizontally: bool = False
+ flipped_diagonally: bool = False
+ flipped_vertically: bool = False
+
+
+@attr.s(auto_attribs=True)
+class TileMap:
+ """Object for storing a TMX with all associated layers and properties.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map
+
+ Attributes:
+ parent_dir: The directory the TMX file is in. Used for finding relative paths
+ to TSX files and other assets.
+ version: The TMX format version.
+ tiledversion: The Tiled version used to save the file. May be a date (for
+ snapshot builds).
+ orientation: Map orientation. Tiled supports "orthogonal", "isometric",
+ "staggered" and "hexagonal"
+ renderorder: The order in which tiles on tile layers are rendered. Valid values
+ are right-down, right-up, left-down and left-up. In all cases, the map is
+ drawn row-by-row. (only supported for orthogonal maps at the moment)
+ map_size: The map width in tiles.
+ tile_size: The width of a tile.
+ infinite: If the map is infinite or not.
+ hexsidelength: Only for hexagonal maps. Determines the width or height
+ (depending on the staggered axis) of the tile's edge, in pixels.
+ stagger_axis: For staggered and hexagonal maps, determines which axis ("x" or
+ "y") is staggered.
+ staggerindex: For staggered and hexagonal maps, determines whether the "even"
+ or "odd" indexes along the staggered axis are shifted.
+ backgroundcolor: The background color of the map.
+ nextlayerid: Stores the next available ID for new layers.
+ nextobjectid: Stores the next available ID for new objects.
+ tile_sets: Dict of tile sets used in this map. Key is the first GID for the
+ tile set. The value is a TileSet object.
+ layers: List of layer objects by draw order.
+ """
+
+ parent_dir: Path
+ tmx_file: Union[str, Path]
+
+ version: str
+ tiled_version: str
+ orientation: str
+ render_order: str
+ map_size: Size
+ tile_size: Size
+ infinite: bool
+ next_layer_id: Optional[int]
+ next_object_id: int
+
+ tile_sets: TileSetDict
+ layers: List[Layer]
+
+ hex_side_length: Optional[int] = None
+ stagger_axis: Optional[int] = None
+ stagger_index: Optional[int] = None
+ background_color: Optional[Color] = None
+
+ properties: Optional[Properties] = None
diff --git a/pytiled_parser/typing_helpers.py b/pytiled_parser/typing_helpers.py
new file mode 100644
index 0000000..cc13d40
--- /dev/null
+++ b/pytiled_parser/typing_helpers.py
@@ -0,0 +1,6 @@
+def is_float(string: str):
+ try:
+ float(string)
+ return True
+ except (ValueError, TypeError):
+ return False
diff --git a/pytiled_parser/utilities.py b/pytiled_parser/utilities.py
new file mode 100644
index 0000000..7d5e09c
--- /dev/null
+++ b/pytiled_parser/utilities.py
@@ -0,0 +1,69 @@
+"""Helper unitilies for pytiled_parser."""
+
+from typing import List, Optional
+
+import pytiled_parser.objects as objects
+
+
+def parse_color(color: str) -> objects.Color:
+ """Convert Tiled color format into Arcade's.
+
+ Args:
+ color (str): Tiled formatted color string.
+
+ Returns:
+ :Color: Color object in the format that Arcade understands.
+ """
+ # the actual part we care about is always an even number
+ if len(color) % 2:
+ # strip initial '#' character
+ color = color[1:]
+
+ if len(color) == 6:
+ # full opacity if no alpha specified
+ alpha = 0xFF
+ red = int(color[0:2], 16)
+ green = int(color[2:4], 16)
+ blue = int(color[4:6], 16)
+ else:
+ alpha = int(color[0:2], 16)
+ red = int(color[2:4], 16)
+ green = int(color[4:6], 16)
+ blue = int(color[6:8], 16)
+
+ return objects.Color(red, green, blue, alpha)
+
+
+def _get_tile_set_key(gid: int, tile_set_keys: List[int]) -> int:
+ """Gets tile set key given a tile GID.
+
+ Args:
+ gid (int): Global ID of the tile.
+ tile_set_keys (List[int]): List of tile set keys.
+
+ Returns:
+ int: The key of the tile set that contains the tile for the GID.
+ """
+
+ # credit to __m4ch1n3__ on ##learnpython for this idea
+ return max([key for key in tile_set_keys if key <= gid])
+
+
+def get_tile_by_gid(gid: int, tile_sets: objects.TileSetDict) -> Optional[objects.Tile]:
+ """Gets correct Tile for a given global ID.
+
+ Args:
+ tile_sets (objects.TileSetDict): TileSetDict from TileMap.
+ gid (int): Global tile ID of the tile to be returned.
+
+ Returns:
+ objects.Tile: The Tile object reffered to by the global tile ID.
+ None: If there is no objects.Tile object in the tile_set.tiles dict for the associated gid.
+ """
+ tile_set_key = _get_tile_set_key(gid, list(tile_sets.keys()))
+ tile_set = tile_sets[tile_set_key]
+
+ if tile_set.tiles is not None:
+ return tile_set.tiles.get(gid - tile_set_key)
+
+ return None
diff --git a/pytiled_parser/xml_parser.py b/pytiled_parser/xml_parser.py
new file mode 100644
index 0000000..d03fed7
--- /dev/null
+++ b/pytiled_parser/xml_parser.py
@@ -0,0 +1,940 @@
+"""Handle XML parsing for TMX files"""
+
+import base64
+import functools
+import gzip
+import re
+import xml.etree.ElementTree as etree
+import zlib
+from pathlib import Path
+from typing import Callable, Dict, List, Optional, Tuple, Union
+
+import pytiled_parser.objects as objects
+import pytiled_parser.typing_helpers as TH
+from pytiled_parser.utilities import parse_color
+
+
+def _decode_base64_data(
+ data_text: str, layer_width: int, compression: Optional[str] = None
+) -> objects.TileLayerGrid:
+ """Decode base64 data.
+
+ Args:
+ data_text: Data to be decoded.
+ layer_width: Width of each layer in tiles.
+ compression: The type of compression for the data.
+
+ Raises:
+ ValueError: If compression type is unsupported.
+
+ Returns:
+ objects.TileLayerGrid: Tile grid.
+ """
+ tile_grid: objects.TileLayerGrid = [[]]
+
+ unencoded_data = base64.b64decode(data_text)
+ if compression == "zlib":
+ unzipped_data = zlib.decompress(unencoded_data)
+ elif compression == "gzip":
+ unzipped_data = gzip.decompress(unencoded_data)
+ elif compression is None:
+ unzipped_data = unencoded_data
+ else:
+ raise ValueError(f"Unsupported compression type: '{compression}'.")
+
+ # Turn bytes into 4-byte integers
+ byte_count = 0
+ int_count = 0
+ int_value = 0
+ row_count = 0
+ for byte in unzipped_data:
+ int_value += byte << (byte_count * 8)
+ byte_count += 1
+ if not byte_count % 4:
+ byte_count = 0
+ int_count += 1
+ tile_grid[row_count].append(int_value)
+ int_value = 0
+ if not int_count % layer_width:
+ row_count += 1
+ tile_grid.append([])
+
+ tile_grid.pop()
+ return tile_grid
+
+
+def _decode_csv_data(data_text: str) -> objects.TileLayerGrid:
+ """Decodes csv encoded layer data.
+
+ Args:
+ data_text: Data to be decoded.
+
+ Returns:
+ objects.TileLayerGrid: Tile grid.
+ """
+ tile_grid = []
+ lines: List[str] = data_text.split("\n")
+ # remove erroneous empty lists due to a newline being on both ends of text
+ lines = lines[1:-1]
+ for line in lines:
+ line_list = line.split(",")
+ # FIXME: what is this for?
+ while "" in line_list:
+ line_list.remove("")
+ line_list_int = [int(item) for item in line_list]
+ tile_grid.append(line_list_int)
+
+ return tile_grid
+
+
+# I'm not sure if this is the best way to do this
+TileLayerDecoder = Union[
+ Callable[[str], objects.TileLayerGrid],
+ Callable[[str, int, Optional[str]], objects.TileLayerGrid],
+]
+
+
+def _decode_tile_layer_data(
+ element: etree.Element,
+ layer_width: int,
+ encoding: str,
+ compression: Optional[str] = None,
+) -> objects.TileLayerGrid:
+ """Decodes tile layer data or chunk data.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tmx-data
+
+ Args:
+ element: Element to have text decoded.
+ layer_width: Number of tiles per column in this layer. Used for determining
+ when to cut off a row when decoding base64 encoding layers.
+ encoding: Encoding format of the layer data.
+ compression: Compression format of the layer data.
+
+ Raises:
+ ValueError: Encoding type is not supported.
+
+ Returns:
+ objects.TileLayerGrid: Tile grid.
+ """
+
+ data_text: str = element.text # type: ignore
+
+ if encoding == "csv":
+ return _decode_csv_data(data_text)
+ if encoding == "base64":
+ return _decode_base64_data(data_text, layer_width, compression)
+
+ raise ValueError(f"{encoding} is not a supported encoding")
+
+
+def _parse_data(element: etree.Element, layer_width: int) -> objects.LayerData:
+ """Parses layer data.
+
+ Will parse CSV, base64, gzip-base64, or zlip-base64 encoded data.
+
+ Args:
+ element: Data element to parse.
+ layer_width: Layer width. Used for base64 decoding.
+
+ Returns:
+ LayerData: Data object containing layer data or chunks of data.
+ """
+ encoding = element.attrib["encoding"]
+
+ compression = None
+ try:
+ compression = element.attrib["compression"]
+ except KeyError:
+ pass
+
+ chunk_elements = element.findall("./chunk")
+ if chunk_elements:
+ chunks: List[objects.Chunk] = []
+ for chunk_element in chunk_elements:
+ x = int(chunk_element.attrib["x"])
+ y = int(chunk_element.attrib["y"])
+ location = objects.OrderedPair(x, y)
+ width = int(chunk_element.attrib["width"])
+ height = int(chunk_element.attrib["height"])
+ layer_data = _decode_tile_layer_data(
+ chunk_element, layer_width, encoding, compression
+ )
+ chunks.append(objects.Chunk(location, width, height, layer_data))
+ return chunks
+
+ return _decode_tile_layer_data(element, layer_width, encoding, compression)
+
+
+def _parse_layer(
+ layer_element: etree.Element,
+) -> Tuple[
+ int,
+ str,
+ Optional[objects.OrderedPair],
+ Optional[float],
+ Optional[objects.Properties],
+]:
+ """Parses all of the attributes for a Layer object.
+
+ Args:
+ layer_element: The layer element to be parsed.
+
+ Returns:
+ FIXME
+ """
+ id_ = int(layer_element.attrib["id"])
+
+ name = layer_element.attrib["name"]
+
+ offset: Optional[objects.OrderedPair]
+ offset_x = layer_element.attrib.get("offsetx")
+ offset_y = layer_element.attrib.get("offsety")
+ if offset_x and offset_y:
+ assert TH.is_float(offset_x)
+ assert TH.is_float(offset_y)
+ offset = objects.OrderedPair(float(offset_x), float(offset_y))
+ else:
+ offset = None
+
+ opacity: Optional[float]
+ opacity_attrib = layer_element.attrib.get("opacity")
+ if opacity_attrib:
+ opacity = float(opacity_attrib)
+ else:
+ opacity = None
+
+ properties: Optional[objects.Properties]
+ properties_element = layer_element.find("./properties")
+ if properties_element is not None:
+ properties = _parse_properties_element(properties_element)
+ else:
+ properties = None
+
+ return id_, name, offset, opacity, properties
+
+
+def _parse_tile_layer(element: etree.Element,) -> objects.TileLayer:
+ """Parses tile layer element.
+
+ Args:
+ element: The layer element to be parsed.
+
+ Raises:
+ ValueError: Element has no child data element.
+
+ Returns:
+ TileLayer: The tile layer object.
+ """
+ id_, name, offset, opacity, properties = _parse_layer(element)
+
+ width = int(element.attrib["width"])
+ height = int(element.attrib["height"])
+ size = objects.Size(width, height)
+
+ data_element = element.find("./data")
+ assert data_element is not None
+ layer_data: objects.LayerData = _parse_data(data_element, width)
+
+ return objects.TileLayer(
+ id_=id_,
+ name=name,
+ offset=offset,
+ opacity=opacity,
+ properties=properties,
+ size=size,
+ layer_data=layer_data,
+ )
+
+
+def _parse_tiled_objects(
+ object_elements: List[etree.Element],
+) -> List[objects.TiledObject]:
+ """Parses objects found in a 'objectgroup' element.
+
+ Args:
+ object_elements: List of object elements to be parsed.
+
+ Returns:
+ list: List of parsed tiled objects.
+ """
+ tiled_objects: List[objects.TiledObject] = []
+
+ for object_element in object_elements:
+ my_object = _parse_object(object_element)
+ if my_object is not None:
+ tiled_objects.append(my_object)
+
+ return tiled_objects
+
+
+def _parse_object(obj):
+ my_id = obj.attrib["id"]
+ my_x = float(obj.attrib["x"])
+ my_y = float(obj.attrib["y"])
+ my_location = objects.OrderedPair(x=my_x, y=my_y)
+ if "width" in obj.attrib:
+ my_width = float(obj.attrib["width"])
+ else:
+ my_width = None
+ if "height" in obj.attrib:
+ my_height = float(obj.attrib["height"])
+ else:
+ my_height = None
+ my_name = obj.attrib["name"]
+
+ properties: Optional[objects.Properties]
+ properties_element = obj.find("./properties")
+ if properties_element is not None:
+ properties = _parse_properties_element(properties_element)
+ else:
+ properties = None
+
+ # This is where it would be nice if we could assume a walrus
+ # operator was part of our Python distribution.
+
+ my_object = None
+
+ polygon = obj.findall("./polygon")
+
+ if polygon and len(polygon) > 0:
+ points = _parse_points(polygon[0].attrib["points"])
+ my_object = objects.PolygonObject(
+ id_=my_id,
+ name=my_name,
+ location=my_location,
+ size=(my_width, my_height),
+ points=points,
+ properties=properties,
+ )
+
+ if my_object is None:
+ polyline = obj.findall("./polyline")
+
+ if polyline and len(polyline) > 0:
+ points = _parse_points(polyline[0].attrib["points"])
+ my_object = objects.PolylineObject(
+ id_=my_id,
+ name=my_name,
+ location=my_location,
+ size=(my_width, my_height),
+ points=points,
+ properties=properties,
+ )
+
+ if my_object is None:
+ ellipse = obj.findall("./ellipse")
+
+ if ellipse and len(ellipse):
+ my_object = objects.ElipseObject(
+ id_=my_id,
+ name=my_name,
+ location=my_location,
+ size=(my_width, my_height),
+ properties=properties,
+ )
+
+ if my_object is None:
+ if "template" in obj.attrib:
+ print(
+ "Warning, this .tmx file is using an unsupported"
+ "'template' attribute. Ignoring."
+ )
+
+ if my_object is None:
+ point = obj.findall("./point")
+ if point:
+ my_object = objects.PointObject(
+ id_=my_id,
+ name=my_name,
+ location=my_location,
+ properties=properties,
+ )
+
+ if my_object is None:
+ my_object = objects.RectangleObject(
+ id_=my_id,
+ name=my_name,
+ location=my_location,
+ size=(my_width, my_height),
+ properties=properties,
+ )
+
+ return my_object
+
+
+
+def _parse_object_layer(element: etree.Element,) -> objects.ObjectLayer:
+ """Parse the objectgroup element given.
+
+ See: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#objectgroup
+
+ Args:
+ element: Element to be parsed.
+
+ Returns:
+ ObjectLayer: The object layer object.
+ """
+ id_, name, offset, opacity, properties = _parse_layer(element)
+
+ color = None
+ try:
+ color = element.attrib["color"]
+ except KeyError:
+ pass
+
+ draw_order = None
+ try:
+ draw_order = element.attrib["draworder"]
+ except KeyError:
+ pass
+
+ tiled_objects = _parse_tiled_objects(element.findall("./object"))
+
+ return objects.ObjectLayer(
+ id_=id_,
+ name=name,
+ offset=offset,
+ opacity=opacity,
+ properties=properties,
+ color=color,
+ draw_order=draw_order,
+ tiled_objects=tiled_objects,
+ )
+
+
+def _parse_image_layer(element: etree.Element,) -> objects.ImageLayer:
+ """Parse the imagelayer element given.
+
+ Args:
+ element: Element to be parsed.
+
+ Returns:
+ ImageLayer: The image layer object.
+ """
+ id_, name, offset, opacity, properties = _parse_layer(element)
+
+ image = _parse_image_element(element.find("./image"))
+
+ return objects.ImageLayer(
+ id_=id_,
+ name=name,
+ offset=offset,
+ opacity=opacity,
+ properties=properties,
+ image=image,
+ )
+
+
+def _parse_layer_group(element: etree.Element,) -> objects.LayerGroup:
+ """Parse the objectgroup element given.
+
+ Args:
+ element: Element to be parsed.
+
+ Returns:
+ LayerGroup: The layer group object.
+ """
+ id_, name, offset, opacity, properties = _parse_layer(element)
+
+ layers = _get_layers(element)
+
+ return objects.LayerGroup(
+ id_=id_,
+ name=name,
+ offset=offset,
+ opacity=opacity,
+ properties=properties,
+ layers=layers,
+ )
+
+
+def _get_layer_parser(
+ layer_tag: str,
+) -> Optional[Callable[[etree.Element], objects.Layer]]:
+ """Gets a the parser for the layer type specified.
+
+ Layer tags are 'layer' for a tile layer, 'objectgroup' for an object layer, and
+ 'group' for a layer group. If anything else is passed, returns None.
+
+ Args:
+ layer_tag: Specifies the layer type to be parsed based on the element tag.
+
+ Returns:
+ Callable: the function to be used to parse the layer.
+ None: The element is not a map layer.
+ """
+ if layer_tag == "layer":
+ return _parse_tile_layer
+ if layer_tag == "objectgroup":
+ return _parse_object_layer
+ if layer_tag == "group":
+ return _parse_layer_group
+ if layer_tag == "imagelayer":
+ return _parse_image_layer
+ return None
+
+
+def _get_layers(map_element: etree.Element) -> List[objects.Layer]:
+ """Parse layer type element given.
+
+ Retains draw order based on the returned lists index FIXME: confirm
+
+ Args:
+ map_element: The element containing the layer.
+
+ Returns:
+ List[Layer]: A list of the layers, ordered by draw order. FIXME: confirm
+ """
+ layers: List[objects.Layer] = []
+ for element in map_element.findall("./"):
+ layer_parser = _get_layer_parser(element.tag)
+ if layer_parser:
+ layers.append(layer_parser(element))
+
+ return layers
+
+
+@functools.lru_cache()
+def _parse_external_tile_set(
+ parent_dir: Path, tile_set_element: etree.Element
+) -> objects.TileSet:
+ """Parses an external tile set.
+
+ Caches the results to speed up subsequent maps with identical tilesets.
+
+ Args:
+ parent_dir: Directory that TMX is in.
+ tile_set_element: Tile set element.
+
+ Returns:
+ objects.Tileset: The tileset being parsed.
+ """
+ source = Path(tile_set_element.attrib["source"])
+ resolved_path = parent_dir / source
+ tile_set_tree = etree.parse(str(parent_dir / Path(source))).getroot()
+
+ parsed_tile_set = _parse_tile_set(tile_set_tree)
+
+ parsed_tile_set.tsx_file = resolved_path
+ parsed_tile_set.parent_dir = resolved_path.parent
+
+ return parsed_tile_set
+
+
+def _parse_points(point_string: str) -> List[objects.OrderedPair]:
+ str_pairs = point_string.split(" ")
+
+ points = []
+ for str_pair in str_pairs:
+ xys = str_pair.split(",")
+ x = float(xys[0])
+ y = float(xys[1])
+ points.append(objects.OrderedPair(x, y))
+
+ return points
+
+
+def _parse_tiles(tile_element_list: List[etree.Element]) -> Dict[int, objects.Tile]:
+ """Parse a list of tile elements.
+
+ Args:
+ tile_element_list: List of tile elements.
+
+ Returns:
+ Dict[int, objects.Tile]: Dictionary containing Tile objects by their ID.
+ """
+ tiles: Dict[int, objects.Tile] = {}
+ for tile_element in tile_element_list:
+ # id is not optional
+ id_ = int(tile_element.attrib["id"])
+
+ # optional attributes
+ _type = None
+ try:
+ _type = tile_element.attrib["type"]
+ except KeyError:
+ pass
+
+ terrain = None
+ try:
+ tile_terrain_attrib = tile_element.attrib["terrain"]
+ except KeyError:
+ pass
+ else:
+ # below is an attempt to explain how terrains are handled.
+ # 'terrain' attribute is a comma seperated list of 4 values,
+ # each is either an integer or blank
+
+ # convert to list of values
+ terrain_list_attrib = re.split(",", tile_terrain_attrib)
+ # terrain_list is list of indexes of Tileset.terrain_types
+ terrain_list: List[Optional[int]] = []
+ # each index in terrain_list_attrib refers to a corner
+ for corner in terrain_list_attrib:
+ if not corner:
+ terrain_list.append(None)
+ else:
+ terrain_list.append(int(corner))
+ terrain = objects.TileTerrain(*terrain_list)
+
+ # tile element optional sub-elements
+ properties: Optional[List[objects.Property]] = None
+ tile_properties_element = tile_element.find("./properties")
+ if tile_properties_element:
+ properties = []
+ property_list = tile_properties_element.findall("./property")
+ for property_ in property_list:
+ name = property_.attrib["name"]
+ value = property_.attrib["value"]
+ obj = objects.Property(name, value)
+ properties.append(obj)
+
+ # tile element optional sub-elements
+ animation: Optional[List[objects.Frame]] = None
+ tile_animation_element = tile_element.find("./animation")
+ if tile_animation_element:
+ animation = []
+ frames = tile_animation_element.findall("./frame")
+ for frame in frames:
+ # tileid refers to the Tile.id of the animation frame
+ animated_id = int(frame.attrib["tileid"])
+ # duration is in MS. Should perhaps be converted to seconds.
+ # FIXME: make decision
+ duration = int(frame.attrib["duration"])
+ animation.append(objects.Frame(animated_id, duration))
+
+ # tile element optional sub-elements
+ objectgroup: Optional[List[objects.TiledObject]] = None
+ objectgroup_element = tile_element.find("./objectgroup")
+ if objectgroup_element:
+ objectgroup = []
+ object_list = objectgroup_element.findall("./object")
+ for obj in object_list:
+ my_object = _parse_object(obj)
+ if my_object is not None:
+ objectgroup.append(my_object)
+
+ # if this is None, then the Tile is part of a spritesheet
+ image = None
+ image_element = tile_element.find("./image")
+ if image_element is not None:
+ image = _parse_image_element(image_element)
+
+ # print(f"Adding '{id_}', {image}, {objectgroup}")
+
+ tiles[id_] = objects.Tile(
+ id_=id_,
+ type_=_type,
+ terrain=terrain,
+ animation=animation,
+ image=image,
+ properties=properties,
+ tileset=None,
+ objectgroup=objectgroup,
+ )
+
+ return tiles
+
+
+def _parse_image_element(image_element: etree.Element) -> objects.Image:
+ """Parse image element given.
+
+ Args:
+ image_element (etree.Element): Image element to be parsed.
+
+ Returns:
+ objects.Image: FIXME what is this?
+ """
+ # FIXME doc
+ image = objects.Image(image_element.attrib["source"])
+
+ width_attrib = image_element.attrib.get("width")
+ height_attrib = image_element.attrib.get("height")
+
+ if width_attrib and height_attrib:
+ image.size = objects.Size(int(width_attrib), int(height_attrib))
+
+ try:
+ image.trans = image_element.attrib["trans"]
+ except KeyError:
+ pass
+
+ return image
+
+
+def _parse_properties_element(properties_element: etree.Element) -> objects.Properties:
+ # FIXME: wtf is this pseudo 'attributes' section?
+ """Adds Tiled property to Properties dict.
+
+ Each property element has a number of attributes:
+ name: Name of property.
+ property_type: Type of property. Can be string, int, float, bool, color or
+ file. Defaults to string.
+ value: The value of the property.
+
+ Args:
+ properties_element: Element to be parsed.
+
+ Returns:
+ objects.Properties: Dict of the property values by property name.
+
+
+ """
+ properties: objects.Properties = {}
+ for property_element in properties_element.findall("./property"):
+ name = property_element.attrib["name"]
+ try:
+ property_type = property_element.attrib["type"]
+ except KeyError:
+ # strings do not have an attribute in property elements
+ property_type = "string"
+ value = property_element.attrib["value"]
+
+ property_types = ["string", "int", "float", "bool", "color", "file"]
+ assert property_type in property_types, f"Invalid type for property {name}"
+
+ if property_type == "int":
+ properties[name] = int(value)
+ elif property_type == "float":
+ properties[name] = float(value)
+ elif property_type == "color":
+ properties[name] = value
+ elif property_type == "file":
+ properties[name] = Path(value)
+ elif property_type == "bool":
+ if value == "true":
+ properties[name] = True
+ else:
+ properties[name] = False
+ else:
+ properties[name] = value
+
+ return properties
+
+
+def _parse_tile_set(tile_set_element: etree.Element) -> objects.TileSet:
+ """Parses a tile set that is embedded into a TMX.
+
+ Args:
+ tile_set_element: Element to be parsed.
+
+ Returns:
+ objects.TileSet: Tile Set from element.
+ """
+ # get all basic attributes
+ name = tile_set_element.attrib["name"]
+ max_tile_width = int(tile_set_element.attrib["tilewidth"])
+ max_tile_height = int(tile_set_element.attrib["tileheight"])
+ max_tile_size = objects.Size(max_tile_width, max_tile_height)
+
+ spacing = None
+ try:
+ spacing = int(tile_set_element.attrib["spacing"])
+ except KeyError:
+ pass
+
+ margin = None
+ try:
+ margin = int(tile_set_element.attrib["margin"])
+ except KeyError:
+ pass
+
+ tile_count = None
+ try:
+ tile_count = int(tile_set_element.attrib["tilecount"])
+ except KeyError:
+ pass
+
+ columns = None
+ try:
+ columns = int(tile_set_element.attrib["columns"])
+ except KeyError:
+ pass
+
+ tile_offset = None
+ tileoffset_element = tile_set_element.find("./tileoffset")
+ if tileoffset_element is not None:
+ tile_offset_x = int(tileoffset_element.attrib["x"])
+ tile_offset_y = int(tileoffset_element.attrib["y"])
+ tile_offset = objects.OrderedPair(tile_offset_x, tile_offset_y)
+
+ grid = None
+ grid_element = tile_set_element.find("./grid")
+ if grid_element is not None:
+ grid_orientation = grid_element.attrib["orientation"]
+ grid_width = int(grid_element.attrib["width"])
+ grid_height = int(grid_element.attrib["height"])
+ grid = objects.Grid(grid_orientation, grid_width, grid_height)
+
+ properties = None
+ properties_element = tile_set_element.find("./properties")
+ if properties_element is not None:
+ properties = _parse_properties_element(properties_element)
+
+ terrain_types: Optional[List[objects.Terrain]] = None
+ terrain_types_element = tile_set_element.find("./terraintypes")
+ if terrain_types_element is not None:
+ terrain_types = []
+ for terrain in terrain_types_element.findall("./terrain"):
+ name = terrain.attrib["name"]
+ terrain_tile = int(terrain.attrib["tile"])
+ terrain_types.append(objects.Terrain(name, terrain_tile))
+
+ image = None
+ image_element = tile_set_element.find("./image")
+ if image_element is not None:
+ image = _parse_image_element(image_element)
+
+ tile_element_list = tile_set_element.findall("./tile")
+ tiles = _parse_tiles(tile_element_list)
+
+ tileset = objects.TileSet(
+ name,
+ max_tile_size,
+ spacing,
+ margin,
+ tile_count,
+ columns,
+ tile_offset,
+ grid,
+ properties,
+ image,
+ terrain_types,
+ tiles,
+ )
+
+ # Go back and create a circular link so tiles know what tileset they are
+ # part of. Needed for animation.
+ for id_, tile in tiles.items():
+ tile.tileset = tileset
+
+ return tileset
+
+
+def _get_tile_sets(map_element: etree.Element, parent_dir: Path) -> objects.TileSetDict:
+ """Get tile sets.
+
+ Args:
+ map_element: Element to be parsed.
+ parent_dir: Directory that TMX is in.
+
+ Returns:
+ objects.TileSetDict: Dict of tile sets in the TMX by first_gid
+ """
+ # parse all tilesets
+ tile_sets: objects.TileSetDict = {}
+ tile_set_element_list = map_element.findall("./tileset")
+ for tile_set_element in tile_set_element_list:
+ # tiled docs are ambiguous about the 'firstgid' attribute
+ # current understanding is for the purposes of mapping the layer
+ # data to the tile set data, add the 'firstgid' value to each
+ # tile 'id'; this means that the 'firstgid' is specific to each,
+ # tile set as they pertain to the map, not tile set specific as
+ # the tiled docs can make it seem
+ # 'firstgid' the key for each TileMap
+ first_gid = int(tile_set_element.attrib["firstgid"])
+ try:
+ # check if is an external TSX
+ source = tile_set_element.attrib["source"]
+ except KeyError:
+ # the tile set is embedded
+ name = tile_set_element.attrib["name"]
+ tile_sets[first_gid] = _parse_tile_set(tile_set_element)
+ else:
+ # tile set is external
+ tile_sets[first_gid] = _parse_external_tile_set(
+ parent_dir, tile_set_element
+ )
+
+ return tile_sets
+
+
+def parse_tile_map(tmx_file: Union[str, Path]) -> objects.TileMap:
+ """Parse tile map.
+
+ Args:
+ tmx_file: TMX file to be parsed.
+
+ Returns:
+ objects.TileMap: TileMap object generated from the TMX file provided.
+ """
+ # setting up XML parsing
+ map_tree = etree.parse(str(tmx_file))
+ map_element = map_tree.getroot()
+
+ # positional arguments for TileMap
+ parent_dir = Path(tmx_file).parent
+
+ version = map_element.attrib["version"]
+ tiled_version = map_element.attrib["tiledversion"]
+ orientation = map_element.attrib["orientation"]
+ render_order = map_element.attrib["renderorder"]
+
+ map_width = int(map_element.attrib["width"])
+ map_height = int(map_element.attrib["height"])
+ map_size = objects.Size(map_width, map_height)
+
+ tile_width = int(map_element.attrib["tilewidth"])
+ tile_height = int(map_element.attrib["tileheight"])
+ tile_size = objects.Size(tile_width, tile_height)
+
+ infinite_attribute = map_element.attrib["infinite"]
+ infinite = bool(infinite_attribute == "1")
+
+ if "nextlayerid" in map_element.attrib:
+ next_layer_id = int(map_element.attrib["nextlayerid"])
+ else:
+ next_layer_id = None
+
+ if "nextobjectid" in map_element.attrib:
+ next_object_id = int(map_element.attrib["nextobjectid"])
+ else:
+ next_object_id = None
+
+ tile_sets = _get_tile_sets(map_element, parent_dir)
+
+ layers = _get_layers(map_element)
+
+ tile_map = objects.TileMap(
+ parent_dir,
+ tmx_file,
+ version,
+ tiled_version,
+ orientation,
+ render_order,
+ map_size,
+ tile_size,
+ infinite,
+ next_layer_id,
+ next_object_id,
+ tile_sets,
+ layers,
+ )
+
+ try:
+ tile_map.hex_side_length = int(map_element.attrib["hexsidelength"])
+ except KeyError:
+ pass
+
+ try:
+ tile_map.stagger_axis = map_element.attrib["staggeraxis"]
+ except KeyError:
+ pass
+
+ try:
+ tile_map.stagger_index = map_element.attrib["staggerindex"]
+ except KeyError:
+ pass
+
+ try:
+ color = parse_color(map_element.attrib["backgroundcolor"])
+ tile_map.background_color = (color.red, color.green, color.blue)
+ except KeyError:
+ pass
+
+ properties_element = map_tree.find("./properties")
+ if properties_element is not None:
+ tile_map.properties = _parse_properties_element(properties_element)
+
+ return tile_map
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..3aeeee8
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,3 @@
+import pytest
+
+pytest.main(["--tb=native", "-s", "tests"])
diff --git a/tests/test_data/test_map_image_tile_set.tmx b/tests/test_data/test_map_image_tile_set.tmx
new file mode 100644
index 0000000..63e60f8
--- /dev/null
+++ b/tests/test_data/test_map_image_tile_set.tmx
@@ -0,0 +1,91 @@
+
+
diff --git a/tests/test_data/test_map_infinite.tmx b/tests/test_data/test_map_infinite.tmx
new file mode 100644
index 0000000..d117bcf
--- /dev/null
+++ b/tests/test_data/test_map_infinite.tmx
@@ -0,0 +1,390 @@
+
+
diff --git a/tests/test_data/test_map_simple.tmx b/tests/test_data/test_map_simple.tmx
new file mode 100644
index 0000000..f5e2a0d
--- /dev/null
+++ b/tests/test_data/test_map_simple.tmx
@@ -0,0 +1,23 @@
+
+
diff --git a/tests/test_data/test_map_simple_meme.tmx b/tests/test_data/test_map_simple_meme.tmx
new file mode 100644
index 0000000..bc47bdc
--- /dev/null
+++ b/tests/test_data/test_map_simple_meme.tmx
@@ -0,0 +1,18 @@
+
+
diff --git a/tests/test_data/test_map_simple_objects.tmx b/tests/test_data/test_map_simple_objects.tmx
new file mode 100644
index 0000000..26adfbb
--- /dev/null
+++ b/tests/test_data/test_map_simple_objects.tmx
@@ -0,0 +1,14 @@
+
+
diff --git a/tests/test_data/test_map_simple_offset.tmx b/tests/test_data/test_map_simple_offset.tmx
new file mode 100644
index 0000000..400a639
--- /dev/null
+++ b/tests/test_data/test_map_simple_offset.tmx
@@ -0,0 +1,22 @@
+
+
diff --git a/tests/test_data/tile_set_image.tsx b/tests/test_data/tile_set_image.tsx
new file mode 100644
index 0000000..2f6ef20
--- /dev/null
+++ b/tests/test_data/tile_set_image.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/tile_set_image_objects.tsx b/tests/test_data/tile_set_image_objects.tsx
new file mode 100644
index 0000000..6d65375
--- /dev/null
+++ b/tests/test_data/tile_set_image_objects.tsx
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_infinite_maps.py b/tests/test_infinite_maps.py
new file mode 100644
index 0000000..33f5d1b
--- /dev/null
+++ b/tests/test_infinite_maps.py
@@ -0,0 +1,82 @@
+"""Tests for infinite maps"""
+import os
+import xml.etree.ElementTree as etree
+from contextlib import ExitStack as does_not_raise
+from pathlib import Path
+
+import pytest
+
+import pytiled_parser
+from pytiled_parser import objects, utilities, xml_parser
+
+TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
+TEST_DATA = TESTS_DIR / "test_data"
+
+
+def test_map_infinite():
+ """
+ TMX with a very simple spritesheet tile set and some properties.
+ """
+
+ test_map = pytiled_parser.parse_tile_map(TEST_DATA / "test_map_infinite.tmx")
+
+ print(test_map.layers)
+
+ # map
+ assert test_map.version == "1.2"
+ assert test_map.tiled_version == "1.2.3"
+ assert test_map.orientation == "orthogonal"
+ assert test_map.render_order == "right-down"
+ assert test_map.map_size == (8, 6)
+ assert test_map.tile_size == (32, 32)
+ assert test_map.infinite
+ assert test_map.next_layer_id == 3
+ assert test_map.next_object_id == 1
+
+ # optional, not for orthogonal maps
+ assert test_map.hex_side_length == None
+ assert test_map.stagger_axis == None
+ assert test_map.stagger_index == None
+ assert test_map.background_color == None
+
+ # tileset
+ assert test_map.tile_sets[1].name == "tile_set_image"
+ assert test_map.tile_sets[1].max_tile_size == (32, 32)
+ assert test_map.tile_sets[1].spacing == 1
+ assert test_map.tile_sets[1].margin == 1
+ assert test_map.tile_sets[1].tile_count == 48
+ assert test_map.tile_sets[1].columns == 8
+ assert test_map.tile_sets[1].tile_offset == None
+ assert test_map.tile_sets[1].grid == None
+ assert test_map.tile_sets[1].properties == None
+
+ # unsure how to get paths to compare propperly
+ assert str(test_map.tile_sets[1].image.source) == ("images/tmw_desert_spacing.png")
+ assert test_map.tile_sets[1].image.trans == None
+ assert test_map.tile_sets[1].image.size == (265, 199)
+
+ assert test_map.tile_sets[1].terrain_types == None
+ assert test_map.tile_sets[1].tiles == {}
+
+ # layers
+ assert test_map.layers[0].id_ == 1
+ assert test_map.layers[0].name == "Tile Layer 1"
+ assert test_map.layers[0].offset == None
+ assert test_map.layers[0].opacity == None
+ assert test_map.layers[0].properties == None
+ assert test_map.layers[0].size == (8, 6)
+
+ # fmt: off
+ assert test_map.layers[0].layer_data == [objects.Chunk(location=objects.OrderedPair(x=-32, y=-32), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=-32), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=0, y=-32), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=16, y=-32), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-32, y=-16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=-16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 1, 2, 3], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 9, 10, 11], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 17, 18, 19]]), objects.Chunk(location=objects.OrderedPair(x=0, y=-16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [4, 5, 6, 7, 8, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [12, 13, 14, 15, 16, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [20, 21, 22, 23, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=16, y=-16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-32, y=0), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=0), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 25, 26, 27], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 33, 34, 35], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 41, 42, 43], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=0, y=0), width=16, height=16, chunk_data=[[28, 29, 30, 31, 32, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [36, 37, 38, 39, 40, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [44, 45, 46, 47, 48, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=16, y=0), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-32, y=16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=0, y=16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]]), objects.Chunk(location=objects.OrderedPair(x=16, y=16), width=16, height=16, chunk_data=[[30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]])]
+ # fmt: on
+
+ assert test_map.layers[1].id_ == 2
+ assert test_map.layers[1].name == "Tile Layer 2"
+ assert test_map.layers[1].offset == None
+ assert test_map.layers[1].opacity == None
+ assert test_map.layers[1].properties == None
+ assert test_map.layers[1].size == (8, 6)
+
+ # fmt: off
+ assert test_map.layers[1].layer_data == [objects.Chunk(location=objects.OrderedPair(x=-32, y=-16), width=16, height=16, chunk_data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 20, 21], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 29], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), objects.Chunk(location=objects.OrderedPair(x=16, y=-16), width=16, height=16, chunk_data=[[0, 0, 20, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=0), width=16, height=16, chunk_data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [20, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), objects.Chunk(location=objects.OrderedPair(x=16, y=0), width=16, height=16, chunk_data=[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [20, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), objects.Chunk(location=objects.OrderedPair(x=-16, y=16), width=16, height=16, chunk_data=[[28, 29, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])]
+ # fmt: on
diff --git a/tests/test_parser.py b/tests/test_parser.py
new file mode 100644
index 0000000..dcad131
--- /dev/null
+++ b/tests/test_parser.py
@@ -0,0 +1,223 @@
+"""Unit tests for pytiled_parser"""
+
+import xml.etree.ElementTree as etree
+from contextlib import ExitStack as does_not_raise
+
+import pytest
+
+from pytiled_parser import objects, utilities, xml_parser
+
+LAYER_DATA = [
+ (
+ '' + "",
+ (int(1), "Tile Layer 1", None, None, None),
+ ),
+ (
+ ''
+ + "",
+ (int(2), "Tile Layer 2", None, float(0.5), None),
+ ),
+ (
+ ''
+ + ""
+ + ""
+ + "",
+ (int(5), "Tile Layer 4", objects.OrderedPair(49, -50), None, "properties",),
+ ),
+]
+
+
+@pytest.mark.parametrize("xml,expected", LAYER_DATA)
+def test_parse_layer(xml, expected, monkeypatch):
+ def mockreturn(*args):
+ return "properties"
+
+ monkeypatch.setattr(xml_parser, "_parse_properties_element", mockreturn)
+
+ result = xml_parser._parse_layer(etree.fromstring(xml))
+
+ assert result == expected
+
+
+@pytest.mark.parametrize(
+ "test_input,expected",
+ [
+ ("#001122", (0x00, 0x11, 0x22, 0xFF)),
+ ("001122", (0x00, 0x11, 0x22, 0xFF)),
+ ("#FF001122", (0x00, 0x11, 0x22, 0xFF)),
+ ("FF001122", (0x00, 0x11, 0x22, 0xFF)),
+ ("FF001122", (0x00, 0x11, 0x22, 0xFF)),
+ ],
+)
+def test_color_parsing(test_input, expected):
+ assert utilities.parse_color(test_input) == expected
+
+
+layer_data = [
+ (
+ etree.fromstring(
+ "\n1,2,3,4,5,6,7,8,\n"
+ "9,10,11,12,13,14,15,16,\n"
+ "17,18,19,20,21,22,23,24,\n"
+ "25,26,27,28,29,30,31,32,\n"
+ "33,34,35,36,37,38,39,40,\n"
+ "41,42,43,44,45,46,47,48\n"
+ ),
+ 8,
+ "csv",
+ None,
+ [
+ [1, 2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15, 16],
+ [17, 18, 19, 20, 21, 22, 23, 24],
+ [25, 26, 27, 28, 29, 30, 31, 32],
+ [33, 34, 35, 36, 37, 38, 39, 40],
+ [41, 42, 43, 44, 45, 46, 47, 48],
+ ],
+ does_not_raise(),
+ ),
+ (
+ etree.fromstring("\n0,0,0,0,0\n"),
+ 5,
+ "csv",
+ None,
+ [[0, 0, 0, 0, 0]],
+ does_not_raise(),
+ ),
+ (
+ etree.fromstring(
+ "AQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAJAAAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAEAAAABEAAAASAAAAEwAAABQAAAAVAAAAFgAAABcAAAAYAAAAGQAAABoAAAAbAAAAHAAAAB0AAAAeAAAAHwAAACAAAAAhAAAAIgAAACMAAAAkAAAAJQAAACYAAAAnAAAAKAAAACkAAAAqAAAAKwAAACwAAAAtAAAALgAAAC8AAAAwAAAA"
+ ),
+ 8,
+ "base64",
+ None,
+ [
+ [1, 2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15, 16],
+ [17, 18, 19, 20, 21, 22, 23, 24],
+ [25, 26, 27, 28, 29, 30, 31, 32],
+ [33, 34, 35, 36, 37, 38, 39, 40],
+ [41, 42, 43, 44, 45, 46, 47, 48],
+ ],
+ does_not_raise(),
+ ),
+ (
+ etree.fromstring(
+ "eJwNwwUSgkAAAMAzEQOwUCzExPb/r2N3ZlshhLYdu/bsGzkwdujIsRMTUzOnzpy7cGnuyrWFG7fu3Huw9GjlybMXr968W/vw6cu3H7/+/NsAMw8EmQ=="
+ ),
+ 8,
+ "base64",
+ "zlib",
+ [
+ [1, 2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15, 16],
+ [17, 18, 19, 20, 21, 22, 23, 24],
+ [25, 26, 27, 28, 29, 30, 31, 32],
+ [33, 34, 35, 36, 37, 38, 39, 40],
+ [41, 42, 43, 44, 45, 46, 47, 48],
+ ],
+ does_not_raise(),
+ ),
+ (
+ etree.fromstring(
+ "H4sIAAAAAAAAAw3DBRKCQAAAwDMRA7BQLMTE9v+vY3dmWyGEth279uwbOTB26MixExNTM6fOnLtwae7KtYUbt+7ce7D0aOXJsxev3rxb+/Dpy7cfv/782wAcvDirwAAAAA=="
+ ),
+ 8,
+ "base64",
+ "gzip",
+ [
+ [1, 2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15, 16],
+ [17, 18, 19, 20, 21, 22, 23, 24],
+ [25, 26, 27, 28, 29, 30, 31, 32],
+ [33, 34, 35, 36, 37, 38, 39, 40],
+ [41, 42, 43, 44, 45, 46, 47, 48],
+ ],
+ does_not_raise(),
+ ),
+ (
+ etree.fromstring("SGVsbG8gV29ybGQh"),
+ 8,
+ "base64",
+ "lzma",
+ [
+ [1, 2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15, 16],
+ [17, 18, 19, 20, 21, 22, 23, 24],
+ [25, 26, 27, 28, 29, 30, 31, 32],
+ [33, 34, 35, 36, 37, 38, 39, 40],
+ [41, 42, 43, 44, 45, 46, 47, 48],
+ ],
+ pytest.raises(ValueError),
+ ),
+ (
+ etree.fromstring(
+ "/ .---- --..-- ..--- --..-- ...-- --..-- ....- --..-- ..... --..-- -.... --..-- --... --..-- ---.. --..-- / ----. --..-- .---- ----- --..-- .---- .---- --..-- .---- ..--- --..-- .---- ...-- --..-- .---- ....- --..-- .---- ..... --..-- .---- -.... --..-- / .---- --... --..-- .---- ---.. --..-- .---- ----. --..-- ..--- ----- --..-- ..--- .---- --..-- ..--- ..--- --..-- ..--- ...-- --..-- ..--- ....- --..-- / ..--- ..... --..-- ..--- -.... --..-- ..--- --... --..-- ..--- ---.. --..-- ..--- ----. --..-- ...-- ----- --..-- ...-- .---- --..-- ...-- ..--- --..-- / ...-- ...-- --..-- ...-- ....- --..-- ...-- ..... --..-- ...-- -.... --..-- ...-- --... --..-- ...-- ---.. --..-- ...-- ----. --..-- ....- ----- --..-- / ....- .---- --..-- ....- ..--- --..-- ....- ...-- --..-- ....- ....- --..-- ....- ..... --..-- ....- -.... --..-- ....- --... --..-- ....- ---.."
+ ),
+ 8,
+ "morse",
+ None,
+ [
+ [1, 2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15, 16],
+ [17, 18, 19, 20, 21, 22, 23, 24],
+ [25, 26, 27, 28, 29, 30, 31, 32],
+ [33, 34, 35, 36, 37, 38, 39, 40],
+ [41, 42, 43, 44, 45, 46, 47, 48],
+ ],
+ pytest.raises(ValueError),
+ ),
+]
+
+
+@pytest.mark.parametrize(
+ "layer_data,width,encoding,compression,expected,raises", layer_data
+)
+def test_decode_layer_data(layer_data, width, encoding, compression, expected, raises):
+ with raises:
+ assert (
+ xml_parser._decode_tile_layer_data(layer_data, width, encoding, compression)
+ == expected
+ )
+
+
+# FIXME: use hypothesis for this
+def create_tile_set(qty_of_tiles):
+ """ Create tile set of specific size.
+ """
+ tile_set = objects.TileSet(None, None)
+
+ if qty_of_tiles == 0:
+ return tile_set
+
+ tiles = {}
+
+ for tile_id in range(qty_of_tiles):
+ tiles[tile_id] = objects.Tile(id_=tile_id)
+
+ tile_set.tiles = tiles
+
+ return tile_set
+
+
+tile_by_gid = [
+ (1, {1: create_tile_set(0)}, None),
+ (1, {1: create_tile_set(1)}, objects.Tile(id_=0)),
+ (1, {1: create_tile_set(2)}, objects.Tile(id_=0)),
+ (2, {1: create_tile_set(1)}, None),
+ (10, {1: create_tile_set(10)}, objects.Tile(id_=9)),
+ (1, {1: create_tile_set(1), 2: create_tile_set(1)}, objects.Tile(id_=0)),
+ (2, {1: create_tile_set(1), 2: create_tile_set(1)}, objects.Tile(id_=0)),
+ (3, {1: create_tile_set(1), 2: create_tile_set(1)}, None),
+ (15, {1: create_tile_set(5), 6: create_tile_set(10)}, objects.Tile(id_=9)),
+ (
+ 20,
+ {1: create_tile_set(5), 6: create_tile_set(10), 16: create_tile_set(10),},
+ objects.Tile(id_=4),
+ ),
+]
+
+
+@pytest.mark.parametrize("gid,tile_sets,expected", tile_by_gid)
+def test_get_tile_by_gid(gid, tile_sets, expected):
+ assert utilities.get_tile_by_gid(gid, tile_sets) == expected
diff --git a/tests/test_pytiled_parser_integration.py b/tests/test_pytiled_parser_integration.py
new file mode 100644
index 0000000..8022cde
--- /dev/null
+++ b/tests/test_pytiled_parser_integration.py
@@ -0,0 +1,98 @@
+import os
+from pathlib import Path
+
+import pytest
+
+import pytiled_parser
+
+TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
+TEST_DATA = TESTS_DIR / "test_data"
+
+
+def test_map_simple():
+ """
+ TMX with a very simple spritesheet tile set and some properties.
+ """
+
+ test_map = pytiled_parser.parse_tile_map(TEST_DATA / "test_map_simple.tmx")
+
+ # map
+ # unsure how to get paths to compare propperly
+ assert test_map.parent_dir == TEST_DATA
+ assert test_map.version == "1.2"
+ assert test_map.tiled_version == "1.2.3"
+ assert test_map.orientation == "orthogonal"
+ assert test_map.render_order == "right-down"
+ assert test_map.map_size == (8, 6)
+ assert test_map.tile_size == (32, 32)
+ assert test_map.infinite == False
+ assert test_map.next_layer_id == 2
+ assert test_map.next_object_id == 1
+
+ # optional, not for orthogonal maps
+ assert test_map.hex_side_length == None
+ assert test_map.stagger_axis == None
+ assert test_map.stagger_index == None
+ assert test_map.background_color == None
+
+ assert test_map.properties == {
+ "bool property - false": False,
+ "bool property - true": True,
+ "color property": "#ff49fcff",
+ "file property": Path("/var/log/syslog"),
+ "float property": 1.23456789,
+ "int property": 13,
+ "string property": "Hello, World!!",
+ }
+
+ # tileset
+ assert test_map.tile_sets[1].name == "tile_set_image"
+ assert test_map.tile_sets[1].max_tile_size == (32, 32)
+ assert test_map.tile_sets[1].spacing == 1
+ assert test_map.tile_sets[1].margin == 1
+ assert test_map.tile_sets[1].tile_count == 48
+ assert test_map.tile_sets[1].columns == 8
+ assert test_map.tile_sets[1].tile_offset == None
+ assert test_map.tile_sets[1].grid == None
+ assert test_map.tile_sets[1].properties == None
+
+ # unsure how to get paths to compare propperly
+ assert str(test_map.tile_sets[1].image.source) == ("images/tmw_desert_spacing.png")
+ assert test_map.tile_sets[1].image.trans == None
+ assert test_map.tile_sets[1].image.size == (265, 199)
+
+ assert test_map.tile_sets[1].terrain_types == None
+ assert test_map.tile_sets[1].tiles == {}
+
+ # layers
+ assert test_map.layers[0].layer_data == [
+ [1, 2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15, 16],
+ [17, 18, 19, 20, 21, 22, 23, 24],
+ [25, 26, 27, 28, 29, 30, 31, 32],
+ [33, 34, 35, 36, 37, 38, 39, 40],
+ [41, 42, 43, 44, 45, 46, 47, 48],
+ ]
+ assert test_map.layers[0].id_ == 1
+ assert test_map.layers[0].name == "Tile Layer 1"
+ assert test_map.layers[0].offset == None
+ assert test_map.layers[0].opacity == None
+ assert test_map.layers[0].properties == None
+ assert test_map.layers[0].size == (8, 6)
+
+
+@pytest.mark.parametrize(
+ "test_input,expected",
+ [
+ ("#001122", (0x00, 0x11, 0x22, 0xFF)),
+ ("001122", (0x00, 0x11, 0x22, 0xFF)),
+ ("#FF001122", (0x00, 0x11, 0x22, 0xFF)),
+ ("FF001122", (0x00, 0x11, 0x22, 0xFF)),
+ ("FF001122", (0x00, 0x11, 0x22, 0xFF)),
+ ],
+)
+def test_color_parsing(test_input, expected):
+ """
+ Tiled has a few different types of color representations.
+ """
+ assert pytiled_parser.utilities.parse_color(test_input) == expected
diff --git a/tests/test_test_map_simple_offset.py b/tests/test_test_map_simple_offset.py
new file mode 100644
index 0000000..e3ba1b9
--- /dev/null
+++ b/tests/test_test_map_simple_offset.py
@@ -0,0 +1,81 @@
+"""test test_map_simple_offset.tmx"""
+import os
+from pathlib import Path
+
+import pytiled_parser
+
+TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
+TEST_DATA = TESTS_DIR / "test_data"
+
+
+def test_map_simple():
+ """
+ TMX with a very simple spritesheet tile set and some properties.
+ """
+
+ test_map = pytiled_parser.parse_tile_map(TEST_DATA / "test_map_simple_offset.tmx")
+
+ # map
+ # unsure how to get paths to compare propperly
+ assert test_map.parent_dir == TEST_DATA
+ assert test_map.version == "1.2"
+ assert test_map.tiled_version == "1.3.1"
+ assert test_map.orientation == "orthogonal"
+ assert test_map.render_order == "right-down"
+ assert test_map.map_size == (8, 6)
+ assert test_map.tile_size == (32, 32)
+ assert test_map.infinite == False
+ assert test_map.next_layer_id == 2
+ assert test_map.next_object_id == 1
+
+ # optional, not for orthogonal maps
+ assert test_map.hex_side_length == None
+ assert test_map.stagger_axis == None
+ assert test_map.stagger_index == None
+ assert test_map.background_color == None
+
+ assert test_map.properties == {
+ "bool property - false": False,
+ "bool property - true": True,
+ "color property": "#ff49fcff",
+ "float property": 1.23456789,
+ "int property": 13,
+ "string property": "Hello, World!!",
+ }
+
+ # tileset
+ assert test_map.tile_sets[1].name == "tile_set_image"
+ assert test_map.tile_sets[1].max_tile_size == (32, 32)
+ assert test_map.tile_sets[1].spacing == 1
+ assert test_map.tile_sets[1].margin == 1
+ assert test_map.tile_sets[1].tile_count == 48
+ assert test_map.tile_sets[1].columns == 8
+ assert test_map.tile_sets[1].tile_offset == None
+ assert test_map.tile_sets[1].grid == None
+ assert test_map.tile_sets[1].properties == None
+
+ # unsure how to get paths to compare propperly
+ assert str(test_map.tile_sets[1].image.source) == ("images/tmw_desert_spacing.png")
+ assert test_map.tile_sets[1].image.trans == None
+ assert test_map.tile_sets[1].image.size == (265, 199)
+
+ assert test_map.tile_sets[1].terrain_types == None
+ assert test_map.tile_sets[1].tiles == {}
+
+ # layers
+ assert test_map.layers[0].layer_data == [
+ [1, 2, 3, 4, 5, 6, 7, 8],
+ [9, 10, 11, 12, 13, 14, 15, 16],
+ [17, 18, 19, 20, 21, 22, 23, 24],
+ [25, 26, 27, 28, 29, 30, 31, 32],
+ [33, 34, 35, 36, 37, 38, 39, 40],
+ [41, 42, 43, 44, 45, 46, 47, 48],
+ ]
+ assert test_map.layers[0].id_ == 1
+ assert test_map.layers[0].name == "Tile Layer 1"
+ assert test_map.layers[0].offset == pytiled_parser.objects.OrderedPair(
+ x=16.0, y=-16.42
+ )
+ assert test_map.layers[0].opacity == None
+ assert test_map.layers[0].properties == None
+ assert test_map.layers[0].size == (8, 6)
diff --git a/tests/test_typing_helpers.py b/tests/test_typing_helpers.py
new file mode 100644
index 0000000..e9b9183
--- /dev/null
+++ b/tests/test_typing_helpers.py
@@ -0,0 +1,19 @@
+"""Tests for typing helpers"""
+
+import pytest
+
+from pytiled_parser import typing_helpers as TH
+
+TEST_IS_FLOAT_PARAMS = [
+ (1, True),
+ ("1", True),
+ (1.1, True),
+ ("1.1", True),
+ ("one", False),
+ (None, False),
+]
+
+
+@pytest.mark.parametrize("string,expected", TEST_IS_FLOAT_PARAMS)
+def test_is_float(string, expected):
+ assert TH.is_float(string) == expected