diff --git a/pytiled_parser/__init__.py b/pytiled_parser/__init__.py
index 46255ba..f37e2ae 100644
--- a/pytiled_parser/__init__.py
+++ b/pytiled_parser/__init__.py
@@ -12,8 +12,10 @@ PyTiled Parser is not tied to any particular graphics library or game engine.
# pylint: disable=too-few-public-methods
from .common_types import OrderedPair, Size
+from .exception import UnknownFormat
from .layer import ImageLayer, Layer, LayerGroup, ObjectLayer, TileLayer
+from .parser import parse_map
from .properties import Properties
-from .tiled_map import TiledMap, parse_map
+from .tiled_map import TiledMap
from .tileset import Tile, Tileset
from .version import __version__
diff --git a/pytiled_parser/exception.py b/pytiled_parser/exception.py
new file mode 100644
index 0000000..8d75354
--- /dev/null
+++ b/pytiled_parser/exception.py
@@ -0,0 +1,2 @@
+class UnknownFormat(Exception):
+ pass
diff --git a/pytiled_parser/layer.py b/pytiled_parser/layer.py
index a0237b2..a2cf80b 100644
--- a/pytiled_parser/layer.py
+++ b/pytiled_parser/layer.py
@@ -8,27 +8,14 @@ See:
# pylint: disable=too-few-public-methods
-import base64
-import gzip
-import importlib.util
-import zlib
from pathlib import Path
-from typing import Any, List, Optional, Union
-from typing import cast as type_cast
+from typing import List, Optional, Union
import attr
-from typing_extensions import TypedDict
-from . import properties as properties_
-from . import tiled_object
-from .common_types import Color, OrderedPair, Size
-from .util import parse_color
-
-zstd_spec = importlib.util.find_spec("zstd")
-if zstd_spec:
- import zstd # pylint: disable=import-outside-toplevel
-else:
- zstd = None # pylint: disable=invalid-name
+from pytiled_parser.common_types import Color, OrderedPair, Size
+from pytiled_parser.properties import Properties
+from pytiled_parser.tiled_object import TiledObject
@attr.s(auto_attribs=True, kw_only=True)
@@ -51,8 +38,8 @@ class Layer:
"""
name: str
- opacity: float
- visible: bool
+ opacity: float = 1
+ visible: bool = True
coordinates: OrderedPair = OrderedPair(0, 0)
parallax_factor: OrderedPair = OrderedPair(1, 1)
@@ -60,7 +47,7 @@ class Layer:
id: Optional[int] = None
size: Optional[Size] = None
- properties: Optional[properties_.Properties] = None
+ properties: Optional[Properties] = None
tint_color: Optional[Color] = None
@@ -127,7 +114,7 @@ class ObjectLayer(Layer):
for more info.
"""
- tiled_objects: List[tiled_object.TiledObject]
+ tiled_objects: List[TiledObject]
draw_order: Optional[str] = "topdown"
@@ -162,341 +149,3 @@ class LayerGroup(Layer):
"""
layers: Optional[List[Layer]]
-
-
-class RawChunk(TypedDict):
- """The keys and their types that appear in a Chunk JSON Object.
-
- See: https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk
- """
-
- data: Union[List[int], str]
- height: int
- width: int
- x: int
- y: int
-
-
-class RawLayer(TypedDict):
- """The keys and their types that appear in a Layer JSON Object.
-
- See: https://doc.mapeditor.org/en/stable/reference/json-map-format/#layer
- """
-
- chunks: List[RawChunk]
- compression: str
- data: Union[List[int], str]
- draworder: str
- encoding: str
- height: int
- id: int
- image: str
- layers: List[Any]
- name: str
- objects: List[tiled_object.RawTiledObject]
- offsetx: float
- offsety: float
- parallaxx: float
- parallaxy: float
- opacity: float
- properties: List[properties_.RawProperty]
- startx: int
- starty: int
- tintcolor: str
- transparentcolor: str
- type: str
- visible: bool
- width: int
- x: int
- y: int
-
-
-def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]:
- """Convert raw layer data into a nested lit based on the layer width
-
- Args:
- data: The data to convert
- layer_width: Width of the layer
-
- Returns:
- List[List[int]]: A nested list containing the converted data
- """
- tile_grid: List[List[int]] = [[]]
-
- column_count = 0
- row_count = 0
- for item in data:
- column_count += 1
- tile_grid[row_count].append(item)
- if not column_count % layer_width and column_count < len(data):
- row_count += 1
- tile_grid.append([])
-
- return tile_grid
-
-
-def _decode_tile_layer_data(
- data: str, compression: str, layer_width: int
-) -> List[List[int]]:
- """Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression.
-
- Args:
- data: The base64 encoded data
- compression: Either zlib, gzip, or empty. If empty no decompression is done.
-
- Returns:
- List[List[int]]: A nested list containing the decoded data
-
- Raises:
- ValueError: For an unsupported compression type.
- """
- unencoded_data = base64.b64decode(data)
- if compression == "zlib":
- unzipped_data = zlib.decompress(unencoded_data)
- elif compression == "gzip":
- unzipped_data = gzip.decompress(unencoded_data)
- elif compression == "zstd" and zstd is None:
- raise ValueError(
- "zstd compression support is not installed."
- "To install use 'pip install pytiled-parser[zstd]'"
- )
- elif compression == "zstd":
- unzipped_data = zstd.decompress(unencoded_data)
- else:
- unzipped_data = unencoded_data
-
- tile_grid: List[int] = []
-
- byte_count = 0
- int_count = 0
- int_value = 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.append(int_value)
- int_value = 0
-
- return _convert_raw_tile_layer_data(tile_grid, layer_width)
-
-
-def _cast_chunk(
- raw_chunk: RawChunk,
- encoding: Optional[str] = None,
- compression: Optional[str] = None,
-) -> Chunk:
- """Cast the raw_chunk to a Chunk.
-
- Args:
- raw_chunk: RawChunk to be casted to a Chunk
- encoding: Encoding type. ("base64" or None)
- compression: Either zlib, gzip, or empty. If empty no decompression is done.
-
- Returns:
- Chunk: The Chunk created from the raw_chunk
- """
- if encoding == "base64":
- assert isinstance(compression, str)
- assert isinstance(raw_chunk["data"], str)
- data = _decode_tile_layer_data(
- raw_chunk["data"], compression, raw_chunk["width"]
- )
- else:
- data = _convert_raw_tile_layer_data(
- raw_chunk["data"], raw_chunk["width"] # type: ignore
- )
-
- chunk = Chunk(
- coordinates=OrderedPair(raw_chunk["x"], raw_chunk["y"]),
- size=Size(raw_chunk["width"], raw_chunk["height"]),
- data=data,
- )
-
- return chunk
-
-
-def _get_common_attributes(raw_layer: RawLayer) -> Layer:
- """Create a Layer containing all the attributes common to all layers.
-
- This is to create the stub Layer object that can then be used to create the actual
- specific sub-classes of Layer.
-
- Args:
- raw_layer: Raw Tiled object get common attributes from
-
- Returns:
- Layer: The attributes in common of all layers
- """
- common_attributes = Layer(
- name=raw_layer["name"],
- opacity=raw_layer["opacity"],
- visible=raw_layer["visible"],
- )
-
- # if startx is present, starty is present
- if raw_layer.get("startx") is not None:
- common_attributes.coordinates = OrderedPair(
- raw_layer["startx"], raw_layer["starty"]
- )
-
- if raw_layer.get("id") is not None:
- common_attributes.id = raw_layer["id"]
-
- # if either width or height is present, they both are
- if raw_layer.get("width") is not None:
- common_attributes.size = Size(raw_layer["width"], raw_layer["height"])
-
- if raw_layer.get("offsetx") is not None:
- common_attributes.offset = OrderedPair(
- raw_layer["offsetx"], raw_layer["offsety"]
- )
-
- if raw_layer.get("properties") is not None:
- common_attributes.properties = properties_.cast(raw_layer["properties"])
-
- parallax = [1.0, 1.0]
-
- if raw_layer.get("parallaxx") is not None:
- parallax[0] = raw_layer["parallaxx"]
-
- if raw_layer.get("parallaxy") is not None:
- parallax[1] = raw_layer["parallaxy"]
-
- common_attributes.parallax_factor = OrderedPair(parallax[0], parallax[1])
-
- if raw_layer.get("tintcolor") is not None:
- common_attributes.tint_color = parse_color(raw_layer["tintcolor"])
-
- return common_attributes
-
-
-def _cast_tile_layer(raw_layer: RawLayer) -> TileLayer:
- """Cast the raw_layer to a TileLayer.
-
- Args:
- raw_layer: RawLayer to be casted to a TileLayer
-
- Returns:
- TileLayer: The TileLayer created from raw_layer
- """
- tile_layer = TileLayer(**_get_common_attributes(raw_layer).__dict__)
-
- if raw_layer.get("chunks") is not None:
- tile_layer.chunks = []
- for chunk in raw_layer["chunks"]:
- if raw_layer.get("encoding") is not None:
- tile_layer.chunks.append(
- _cast_chunk(chunk, raw_layer["encoding"], raw_layer["compression"])
- )
- else:
- tile_layer.chunks.append(_cast_chunk(chunk))
-
- if raw_layer.get("data") is not None:
- if raw_layer.get("encoding") is not None:
- tile_layer.data = _decode_tile_layer_data(
- data=type_cast(str, raw_layer["data"]),
- compression=raw_layer["compression"],
- layer_width=raw_layer["width"],
- )
- else:
- tile_layer.data = _convert_raw_tile_layer_data(
- raw_layer["data"], raw_layer["width"] # type: ignore
- )
-
- return tile_layer
-
-
-def _cast_object_layer(
- raw_layer: RawLayer,
- parent_dir: Optional[Path] = None,
-) -> ObjectLayer:
- """Cast the raw_layer to an ObjectLayer.
-
- Args:
- raw_layer: RawLayer to be casted to an ObjectLayer
- Returns:
- ObjectLayer: The ObjectLayer created from raw_layer
- """
-
- tiled_objects = []
- for tiled_object_ in raw_layer["objects"]:
- tiled_objects.append(tiled_object.cast(tiled_object_, parent_dir))
-
- return ObjectLayer(
- tiled_objects=tiled_objects,
- draw_order=raw_layer["draworder"],
- **_get_common_attributes(raw_layer).__dict__,
- )
-
-
-def _cast_image_layer(raw_layer: RawLayer) -> ImageLayer:
- """Cast the raw_layer to a ImageLayer.
-
- Args:
- raw_layer: RawLayer to be casted to a ImageLayer
-
- Returns:
- ImageLayer: The ImageLayer created from raw_layer
- """
- image_layer = ImageLayer(
- image=Path(raw_layer["image"]), **_get_common_attributes(raw_layer).__dict__
- )
-
- if raw_layer.get("transparentcolor") is not None:
- image_layer.transparent_color = parse_color(raw_layer["transparentcolor"])
-
- return image_layer
-
-
-def _cast_group_layer(
- raw_layer: RawLayer, parent_dir: Optional[Path] = None
-) -> LayerGroup:
- """Cast the raw_layer to a LayerGroup.
-
- Args:
- raw_layer: RawLayer to be casted to a LayerGroup
-
- Returns:
- LayerGroup: The LayerGroup created from raw_layer
- """
-
- layers = []
-
- for layer in raw_layer["layers"]:
- layers.append(cast(layer, parent_dir=parent_dir))
-
- return LayerGroup(layers=layers, **_get_common_attributes(raw_layer).__dict__)
-
-
-def cast(
- raw_layer: RawLayer,
- parent_dir: Optional[Path] = None,
-) -> Layer:
- """Cast a raw Tiled layer into a pytiled_parser type.
-
- This function will determine the type of layer and cast accordingly.
-
- Args:
- raw_layer: Raw layer to be cast.
- parent_dir: The parent directory that the map file is in.
-
- Returns:
- Layer: a properly typed Layer.
-
- Raises:
- RuntimeError: For an invalid layer type being provided
- """
- type_ = raw_layer["type"]
-
- if type_ == "objectgroup":
- return _cast_object_layer(raw_layer, parent_dir)
- elif type_ == "group":
- return _cast_group_layer(raw_layer, parent_dir)
- elif type_ == "imagelayer":
- return _cast_image_layer(raw_layer)
- elif type_ == "tilelayer":
- return _cast_tile_layer(raw_layer)
-
- raise RuntimeError(f"An invalid layer type of {type_} was supplied")
diff --git a/pytiled_parser/parser.py b/pytiled_parser/parser.py
new file mode 100644
index 0000000..9a0a389
--- /dev/null
+++ b/pytiled_parser/parser.py
@@ -0,0 +1,29 @@
+from pathlib import Path
+
+from pytiled_parser import UnknownFormat
+from pytiled_parser.parsers.json.tiled_map import parse as json_map_parse
+from pytiled_parser.parsers.tmx.tiled_map import parse as tmx_map_parse
+from pytiled_parser.tiled_map import TiledMap
+from pytiled_parser.util import check_format
+
+
+def parse_map(file: Path) -> TiledMap:
+ """Parse the raw Tiled map into a pytiled_parser type
+
+ Args:
+ file: Path to the map file
+
+ Returns:
+ Tiledmap: a properly typed TiledMap
+ """
+ parser = check_format(file)
+
+ # The type ignores are because mypy for some reaosn thinks those functions return Any
+ if parser == "tmx":
+ return tmx_map_parse(file) # type: ignore
+ elif parser == "json":
+ return json_map_parse(file) # type: ignore
+ else:
+ raise UnknownFormat(
+ "Unknown Map Format, please use either the TMX or JSON format."
+ )
diff --git a/pytiled_parser/parsers/json/layer.py b/pytiled_parser/parsers/json/layer.py
new file mode 100644
index 0000000..4953045
--- /dev/null
+++ b/pytiled_parser/parsers/json/layer.py
@@ -0,0 +1,364 @@
+"""Layer parsing for the JSON Map Format.
+"""
+import base64
+import gzip
+import importlib.util
+import zlib
+from pathlib import Path
+from typing import Any, List, Optional, Union, cast
+
+from typing_extensions import TypedDict
+
+from pytiled_parser.common_types import OrderedPair, Size
+from pytiled_parser.layer import (
+ Chunk,
+ ImageLayer,
+ Layer,
+ LayerGroup,
+ ObjectLayer,
+ TileLayer,
+)
+from pytiled_parser.parsers.json.properties import RawProperty
+from pytiled_parser.parsers.json.properties import parse as parse_properties
+from pytiled_parser.parsers.json.tiled_object import RawObject
+from pytiled_parser.parsers.json.tiled_object import parse as parse_object
+from pytiled_parser.util import parse_color
+
+zstd_spec = importlib.util.find_spec("zstd")
+if zstd_spec:
+ import zstd
+else:
+ zstd = None
+
+
+class RawChunk(TypedDict):
+ """The keys and their types that appear in a Tiled JSON Chunk Object.
+
+ Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#chunk
+ """
+
+ data: Union[List[int], str]
+ height: int
+ width: int
+ x: int
+ y: int
+
+
+class RawLayer(TypedDict):
+ """The keys and their types that appear in a Tiled JSON Layer Object.
+
+ Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#layer
+ """
+
+ chunks: List[RawChunk]
+ compression: str
+ data: Union[List[int], str]
+ draworder: str
+ encoding: str
+ height: int
+ id: int
+ image: str
+ layers: List[Any]
+ name: str
+ objects: List[RawObject]
+ offsetx: float
+ offsety: float
+ parallaxx: float
+ parallaxy: float
+ opacity: float
+ properties: List[RawProperty]
+ startx: int
+ starty: int
+ tintcolor: str
+ transparentcolor: str
+ type: str
+ visible: bool
+ width: int
+ x: int
+ y: int
+
+
+def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]:
+ """Convert raw layer data into a nested lit based on the layer width
+
+ Args:
+ data: The data to convert
+ layer_width: Width of the layer
+
+ Returns:
+ List[List[int]]: A nested list containing the converted data
+ """
+ tile_grid: List[List[int]] = [[]]
+
+ column_count = 0
+ row_count = 0
+ for item in data:
+ column_count += 1
+ tile_grid[row_count].append(item)
+ if not column_count % layer_width and column_count < len(data):
+ row_count += 1
+ tile_grid.append([])
+
+ return tile_grid
+
+
+def _decode_tile_layer_data(
+ data: str, compression: str, layer_width: int
+) -> List[List[int]]:
+ """Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression.
+
+ Args:
+ data: The base64 encoded data
+ compression: Either zlib, gzip, or empty. If empty no decompression is done.
+
+ Returns:
+ List[List[int]]: A nested list containing the decoded data
+
+ Raises:
+ ValueError: For an unsupported compression type.
+ """
+ unencoded_data = base64.b64decode(data)
+ if compression == "zlib":
+ unzipped_data = zlib.decompress(unencoded_data)
+ elif compression == "gzip":
+ unzipped_data = gzip.decompress(unencoded_data)
+ elif compression == "zstd" and zstd is None:
+ raise ValueError(
+ "zstd compression support is not installed."
+ "To install use 'pip install pytiled-parser[zstd]'"
+ )
+ elif compression == "zstd":
+ unzipped_data = zstd.decompress(unencoded_data)
+ else:
+ unzipped_data = unencoded_data
+
+ tile_grid: List[int] = []
+
+ byte_count = 0
+ int_count = 0
+ int_value = 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.append(int_value)
+ int_value = 0
+
+ return _convert_raw_tile_layer_data(tile_grid, layer_width)
+
+
+def _parse_chunk(
+ raw_chunk: RawChunk,
+ encoding: Optional[str] = None,
+ compression: Optional[str] = None,
+) -> Chunk:
+ """Parse the raw_chunk to a Chunk.
+
+ Args:
+ raw_chunk: RawChunk to be parsed to a Chunk
+ encoding: Encoding type. ("base64" or None)
+ compression: Either zlib, gzip, or empty. If empty no decompression is done.
+
+ Returns:
+ Chunk: The Chunk created from the raw_chunk
+ """
+ if encoding == "base64":
+ assert isinstance(compression, str)
+ assert isinstance(raw_chunk["data"], str)
+ data = _decode_tile_layer_data(
+ raw_chunk["data"], compression, raw_chunk["width"]
+ )
+ else:
+ data = _convert_raw_tile_layer_data(
+ raw_chunk["data"], raw_chunk["width"] # type: ignore
+ )
+
+ chunk = Chunk(
+ coordinates=OrderedPair(raw_chunk["x"], raw_chunk["y"]),
+ size=Size(raw_chunk["width"], raw_chunk["height"]),
+ data=data,
+ )
+
+ return chunk
+
+
+def _parse_common(raw_layer: RawLayer) -> Layer:
+ """Create a Layer containing all the attributes common to all layer types.
+
+ This is to create the stub Layer object that can then be used to create the actual
+ specific sub-classes of Layer.
+
+ Args:
+ raw_layer: Raw layer get common attributes from
+
+ Returns:
+ Layer: The attributes in common of all layer types
+ """
+ common = Layer(
+ name=raw_layer["name"],
+ opacity=raw_layer["opacity"],
+ visible=raw_layer["visible"],
+ )
+
+ # if startx is present, starty is present
+ if raw_layer.get("startx") is not None:
+ common.coordinates = OrderedPair(raw_layer["startx"], raw_layer["starty"])
+
+ if raw_layer.get("id") is not None:
+ common.id = raw_layer["id"]
+
+ # if either width or height is present, they both are
+ if raw_layer.get("width") is not None:
+ common.size = Size(raw_layer["width"], raw_layer["height"])
+
+ if raw_layer.get("offsetx") is not None:
+ common.offset = OrderedPair(raw_layer["offsetx"], raw_layer["offsety"])
+
+ if raw_layer.get("properties") is not None:
+ common.properties = parse_properties(raw_layer["properties"])
+
+ parallax = [1.0, 1.0]
+
+ if raw_layer.get("parallaxx") is not None:
+ parallax[0] = raw_layer["parallaxx"]
+
+ if raw_layer.get("parallaxy") is not None:
+ parallax[1] = raw_layer["parallaxy"]
+
+ common.parallax_factor = OrderedPair(parallax[0], parallax[1])
+
+ if raw_layer.get("tintcolor") is not None:
+ common.tint_color = parse_color(raw_layer["tintcolor"])
+
+ return common
+
+
+def _parse_tile_layer(raw_layer: RawLayer) -> TileLayer:
+ """Parse the raw_layer to a TileLayer.
+
+ Args:
+ raw_layer: RawLayer to be parsed to a TileLayer.
+
+ Returns:
+ TileLayer: The TileLayer created from raw_layer
+ """
+ tile_layer = TileLayer(**_parse_common(raw_layer).__dict__)
+
+ if raw_layer.get("chunks") is not None:
+ tile_layer.chunks = []
+ for chunk in raw_layer["chunks"]:
+ if raw_layer.get("encoding") is not None:
+ tile_layer.chunks.append(
+ _parse_chunk(chunk, raw_layer["encoding"], raw_layer["compression"])
+ )
+ else:
+ tile_layer.chunks.append(_parse_chunk(chunk))
+
+ if raw_layer.get("data") is not None:
+ if raw_layer.get("encoding") is not None:
+ tile_layer.data = _decode_tile_layer_data(
+ data=cast(str, raw_layer["data"]),
+ compression=raw_layer["compression"],
+ layer_width=raw_layer["width"],
+ )
+ else:
+ tile_layer.data = _convert_raw_tile_layer_data(
+ raw_layer["data"], raw_layer["width"] # type: ignore
+ )
+
+ return tile_layer
+
+
+def _parse_object_layer(
+ raw_layer: RawLayer,
+ parent_dir: Optional[Path] = None,
+) -> ObjectLayer:
+ """Parse the raw_layer to an ObjectLayer.
+
+ Args:
+ raw_layer: RawLayer to be parsed to an ObjectLayer.
+
+ Returns:
+ ObjectLayer: The ObjectLayer created from raw_layer
+ """
+ objects = []
+ for object_ in raw_layer["objects"]:
+ objects.append(parse_object(object_, parent_dir))
+
+ return ObjectLayer(
+ tiled_objects=objects,
+ draw_order=raw_layer["draworder"],
+ **_parse_common(raw_layer).__dict__,
+ )
+
+
+def _parse_image_layer(raw_layer: RawLayer) -> ImageLayer:
+ """Parse the raw_layer to an ImageLayer.
+
+ Args:
+ raw_layer: RawLayer to be parsed to an ImageLayer.
+
+ Returns:
+ ImageLayer: The ImageLayer created from raw_layer
+ """
+ image_layer = ImageLayer(
+ image=Path(raw_layer["image"]), **_parse_common(raw_layer).__dict__
+ )
+
+ if raw_layer.get("transparentcolor") is not None:
+ image_layer.transparent_color = parse_color(raw_layer["transparentcolor"])
+
+ return image_layer
+
+
+def _parse_group_layer(
+ raw_layer: RawLayer, parent_dir: Optional[Path] = None
+) -> LayerGroup:
+ """Parse the raw_layer to a LayerGroup.
+
+ Args:
+ raw_layer: RawLayer to be parsed to a LayerGroup.
+
+ Returns:
+ LayerGroup: The LayerGroup created from raw_layer
+ """
+ layers = []
+
+ for layer in raw_layer["layers"]:
+ layers.append(parse(layer, parent_dir=parent_dir))
+
+ return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__)
+
+
+def parse(
+ raw_layer: RawLayer,
+ parent_dir: Optional[Path] = None,
+) -> Layer:
+ """Parse a raw Layer into a pytiled_parser object.
+
+ This function will determine the type of layer and parse accordingly.
+
+ Args:
+ raw_layer: Raw layer to be parsed.
+ parent_dir: The parent directory that the map file is in.
+
+ Returns:
+ Layer: A parsed Layer.
+
+ Raises:
+ RuntimeError: For an invalid layer type being provided
+ """
+ type_ = raw_layer["type"]
+
+ if type_ == "objectgroup":
+ return _parse_object_layer(raw_layer, parent_dir)
+ elif type_ == "group":
+ return _parse_group_layer(raw_layer, parent_dir)
+ elif type_ == "imagelayer":
+ return _parse_image_layer(raw_layer)
+ elif type_ == "tilelayer":
+ return _parse_tile_layer(raw_layer)
+
+ raise RuntimeError(f"An invalid layer type of {type_} was supplied")
diff --git a/pytiled_parser/parsers/json/properties.py b/pytiled_parser/parsers/json/properties.py
new file mode 100644
index 0000000..4e9896f
--- /dev/null
+++ b/pytiled_parser/parsers/json/properties.py
@@ -0,0 +1,48 @@
+"""Property parsing for the JSON Map Format
+"""
+
+from pathlib import Path
+from typing import List, Union, cast
+
+from typing_extensions import TypedDict
+
+from pytiled_parser.properties import Properties, Property
+from pytiled_parser.util import parse_color
+
+RawValue = Union[float, str, bool]
+
+
+class RawProperty(TypedDict):
+ """The keys and their values that appear in a Tiled JSON Property Object.
+
+ Tiled Docs: https://doc.mapeditor.org/en/stable/reference/json-map-format/#property
+ """
+
+ name: str
+ type: str
+ value: RawValue
+
+
+def parse(raw_properties: List[RawProperty]) -> Properties:
+ """Parse a list of `RawProperty` objects into `Properties`.
+
+ Args:
+ raw_properties: The list of `RawProperty` objects to parse.
+
+ Returns:
+ Properties: The parsed `Property` objects.
+ """
+
+ final: Properties = {}
+ value: Property
+
+ for raw_property in raw_properties:
+ if raw_property["type"] == "file":
+ value = Path(cast(str, raw_property["value"]))
+ elif raw_property["type"] == "color":
+ value = parse_color(cast(str, raw_property["value"]))
+ else:
+ value = raw_property["value"]
+ final[raw_property["name"]] = value
+
+ return final
diff --git a/pytiled_parser/parsers/json/tiled_map.py b/pytiled_parser/parsers/json/tiled_map.py
new file mode 100644
index 0000000..82f6c04
--- /dev/null
+++ b/pytiled_parser/parsers/json/tiled_map.py
@@ -0,0 +1,170 @@
+import json
+import xml.etree.ElementTree as etree
+from pathlib import Path
+from typing import List, Union, cast
+
+from typing_extensions import TypedDict
+
+from pytiled_parser.common_types import Size
+from pytiled_parser.exception import UnknownFormat
+from pytiled_parser.parsers.json.layer import RawLayer
+from pytiled_parser.parsers.json.layer import parse as parse_layer
+from pytiled_parser.parsers.json.properties import RawProperty
+from pytiled_parser.parsers.json.properties import parse as parse_properties
+from pytiled_parser.parsers.json.tileset import RawTileSet
+from pytiled_parser.parsers.json.tileset import parse as parse_json_tileset
+from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx_tileset
+from pytiled_parser.tiled_map import TiledMap, TilesetDict
+from pytiled_parser.util import check_format, parse_color
+
+
+class RawTilesetMapping(TypedDict):
+
+ firstgid: int
+ source: str
+
+
+class RawTiledMap(TypedDict):
+ """The keys and their types that appear in a Tiled JSON Map Object.
+
+ Tiled Docs: https://doc.mapeditor.org/en/stable/reference/json-map-format/#map
+ """
+
+ backgroundcolor: str
+ compressionlevel: int
+ height: int
+ hexsidelength: int
+ infinite: bool
+ layers: List[RawLayer]
+ nextlayerid: int
+ nextobjectid: int
+ orientation: str
+ properties: List[RawProperty]
+ renderorder: str
+ staggeraxis: str
+ staggerindex: str
+ tiledversion: str
+ tileheight: int
+ tilesets: List[RawTilesetMapping]
+ tilewidth: int
+ type: str
+ version: Union[str, float]
+ width: int
+
+
+def parse(file: Path) -> TiledMap:
+ """Parse the raw Tiled map into a pytiled_parser type.
+
+ Args:
+ file: Path to the map file.
+
+ Returns:
+ TiledMap: A parsed TiledMap.
+ """
+ with open(file) as map_file:
+ raw_tiled_map = json.load(map_file)
+
+ parent_dir = file.parent
+
+ raw_tilesets: List[Union[RawTileSet, RawTilesetMapping]] = raw_tiled_map["tilesets"]
+ tilesets: TilesetDict = {}
+
+ for raw_tileset in raw_tilesets:
+ if raw_tileset.get("source") is not None:
+ # Is an external Tileset
+ tileset_path = Path(parent_dir / raw_tileset["source"])
+ parser = check_format(tileset_path)
+ with open(tileset_path) as raw_tileset_file:
+ if parser == "json":
+ tilesets[raw_tileset["firstgid"]] = parse_json_tileset(
+ json.load(raw_tileset_file),
+ raw_tileset["firstgid"],
+ external_path=tileset_path.parent,
+ )
+ elif parser == "tmx":
+ raw_tileset_external = etree.parse(raw_tileset_file).getroot()
+ tilesets[raw_tileset["firstgid"]] = parse_tmx_tileset(
+ raw_tileset_external,
+ raw_tileset["firstgid"],
+ external_path=tileset_path.parent,
+ )
+ else:
+ raise UnknownFormat(
+ "Unkown Tileset format, please use either the TSX or JSON format."
+ )
+
+ else:
+ # Is an embedded Tileset
+ raw_tileset = cast(RawTileSet, raw_tileset)
+ tilesets[raw_tileset["firstgid"]] = parse_json_tileset(
+ raw_tileset, raw_tileset["firstgid"]
+ )
+
+ if isinstance(raw_tiled_map["version"], float):
+ version = str(raw_tiled_map["version"])
+ else:
+ version = raw_tiled_map["version"]
+
+ # `map` is a built-in function
+ map_ = TiledMap(
+ map_file=file,
+ infinite=raw_tiled_map["infinite"],
+ layers=[parse_layer(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]],
+ map_size=Size(raw_tiled_map["width"], raw_tiled_map["height"]),
+ next_layer_id=raw_tiled_map["nextlayerid"],
+ next_object_id=raw_tiled_map["nextobjectid"],
+ orientation=raw_tiled_map["orientation"],
+ render_order=raw_tiled_map["renderorder"],
+ tiled_version=raw_tiled_map["tiledversion"],
+ tile_size=Size(raw_tiled_map["tilewidth"], raw_tiled_map["tileheight"]),
+ tilesets=tilesets,
+ version=version,
+ )
+
+ layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")]
+
+ for my_layer in layers:
+ for tiled_object in my_layer.tiled_objects: # type: ignore
+ if hasattr(tiled_object, "new_tileset"):
+ if tiled_object.new_tileset:
+ already_loaded = None
+ for val in map_.tilesets.values():
+ if val.name == tiled_object.new_tileset["name"]:
+ already_loaded = val
+ break
+
+ if not already_loaded:
+ highest_firstgid = max(map_.tilesets.keys())
+ last_tileset_count = map_.tilesets[highest_firstgid].tile_count
+ new_firstgid = highest_firstgid + last_tileset_count
+ map_.tilesets[new_firstgid] = parse_json_tileset(
+ tiled_object.new_tileset,
+ new_firstgid,
+ tiled_object.new_tileset_path,
+ )
+ tiled_object.gid = tiled_object.gid + (new_firstgid - 1)
+
+ else:
+ tiled_object.gid = tiled_object.gid + (
+ already_loaded.firstgid - 1
+ )
+
+ tiled_object.new_tileset = None
+ tiled_object.new_tileset_path = None
+
+ if raw_tiled_map.get("backgroundcolor") is not None:
+ map_.background_color = parse_color(raw_tiled_map["backgroundcolor"])
+
+ if raw_tiled_map.get("hexsidelength") is not None:
+ map_.hex_side_length = raw_tiled_map["hexsidelength"]
+
+ if raw_tiled_map.get("properties") is not None:
+ map_.properties = parse_properties(raw_tiled_map["properties"])
+
+ if raw_tiled_map.get("staggeraxis") is not None:
+ map_.stagger_axis = raw_tiled_map["staggeraxis"]
+
+ if raw_tiled_map.get("staggerindex") is not None:
+ map_.stagger_index = raw_tiled_map["staggerindex"]
+
+ return map_
diff --git a/pytiled_parser/parsers/json/tiled_object.py b/pytiled_parser/parsers/json/tiled_object.py
new file mode 100644
index 0000000..2acc4b3
--- /dev/null
+++ b/pytiled_parser/parsers/json/tiled_object.py
@@ -0,0 +1,321 @@
+"""Object parsing for the JSON Map Format.
+"""
+import json
+import xml.etree.ElementTree as etree
+from pathlib import Path
+from typing import Any, Callable, Dict, List, Optional
+
+from typing_extensions import TypedDict
+
+from pytiled_parser.common_types import OrderedPair, Size
+from pytiled_parser.parsers.json.properties import RawProperty
+from pytiled_parser.parsers.json.properties import parse as parse_properties
+from pytiled_parser.tiled_object import (
+ Ellipse,
+ Point,
+ Polygon,
+ Polyline,
+ Rectangle,
+ Text,
+ Tile,
+ TiledObject,
+)
+from pytiled_parser.util import load_object_template, parse_color
+
+
+class RawText(TypedDict):
+ """The keys and their types that appear in a Tiled JSON Text Object.
+
+ Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#text-example
+ """
+
+ text: str
+ color: str
+
+ fontfamily: str
+ pixelsize: float # this is `font_size` in Text
+
+ bold: bool
+ italic: bool
+ strikeout: bool
+ underline: bool
+ kerning: bool
+
+ halign: str
+ valign: str
+ wrap: bool
+
+
+class RawObject(TypedDict):
+ """The keys and their types that appear in a Tiled JSON Object.
+
+ Tiled Doc: https://doc.mapeditor.org/en/stable/reference/json-map-format/#object
+ """
+
+ id: int
+ gid: int
+ template: str
+ x: float
+ y: float
+ width: float
+ height: float
+ rotation: float
+ visible: bool
+ name: str
+ type: str
+ properties: List[RawProperty]
+ ellipse: bool
+ point: bool
+ polygon: List[Dict[str, float]]
+ polyline: List[Dict[str, float]]
+ text: RawText
+
+
+def _parse_common(raw_object: RawObject) -> TiledObject:
+ """Create an Object containing all the attributes common to all types of objects.
+
+ Args:
+ raw_object: Raw object to get common attributes from
+
+ Returns:
+ Object: The attributes in common of all types of objects
+ """
+
+ common = TiledObject(
+ id=raw_object["id"],
+ coordinates=OrderedPair(raw_object["x"], raw_object["y"]),
+ visible=raw_object["visible"],
+ size=Size(raw_object["width"], raw_object["height"]),
+ rotation=raw_object["rotation"],
+ name=raw_object["name"],
+ type=raw_object["type"],
+ )
+
+ if raw_object.get("properties") is not None:
+ common.properties = parse_properties(raw_object["properties"])
+
+ return common
+
+
+def _parse_ellipse(raw_object: RawObject) -> Ellipse:
+ """Parse the raw object into an Ellipse.
+
+ Args:
+ raw_object: Raw object to be parsed to an Ellipse
+
+ Returns:
+ Ellipse: The Ellipse object created from the raw object
+ """
+ return Ellipse(**_parse_common(raw_object).__dict__)
+
+
+def _parse_rectangle(raw_object: RawObject) -> Rectangle:
+ """Parse the raw object into a Rectangle.
+
+ Args:
+ raw_object: Raw object to be parsed to a Rectangle
+
+ Returns:
+ Rectangle: The Rectangle object created from the raw object
+ """
+ return Rectangle(**_parse_common(raw_object).__dict__)
+
+
+def _parse_point(raw_object: RawObject) -> Point:
+ """Parse the raw object into a Point.
+
+ Args:
+ raw_object: Raw object to be parsed to a Point
+
+ Returns:
+ Point: The Point object created from the raw object
+ """
+ return Point(**_parse_common(raw_object).__dict__)
+
+
+def _parse_polygon(raw_object: RawObject) -> Polygon:
+ """Parse the raw object into a Polygon.
+
+ Args:
+ raw_object: Raw object to be parsed to a Polygon
+
+ Returns:
+ Polygon: The Polygon object created from the raw object
+ """
+ polygon = []
+ for point in raw_object["polygon"]:
+ polygon.append(OrderedPair(point["x"], point["y"]))
+
+ return Polygon(points=polygon, **_parse_common(raw_object).__dict__)
+
+
+def _parse_polyline(raw_object: RawObject) -> Polyline:
+ """Parse the raw object into a Polyline.
+
+ Args:
+ raw_object: Raw object to be parsed to a Polyline
+
+ Returns:
+ Polyline: The Polyline object created from the raw object
+ """
+ polyline = []
+ for point in raw_object["polyline"]:
+ polyline.append(OrderedPair(point["x"], point["y"]))
+
+ return Polyline(points=polyline, **_parse_common(raw_object).__dict__)
+
+
+def _parse_tile(
+ raw_object: RawObject,
+ new_tileset: Optional[Dict[str, Any]] = None,
+ new_tileset_path: Optional[Path] = None,
+) -> Tile:
+ """Parse the raw object into a Tile.
+
+ Args:
+ raw_object: Raw object to be parsed to a Tile
+
+ Returns:
+ Tile: The Tile object created from the raw object
+ """
+ gid = raw_object["gid"]
+
+ return Tile(
+ gid=gid,
+ new_tileset=new_tileset,
+ new_tileset_path=new_tileset_path,
+ **_parse_common(raw_object).__dict__
+ )
+
+
+def _parse_text(raw_object: RawObject) -> Text:
+ """Parse the raw object into Text.
+
+ Args:
+ raw_object: Raw object to be parsed to a Text
+
+ Returns:
+ Text: The Text object created from the raw object
+ """
+ # required attributes
+ raw_text: RawText = raw_object["text"]
+ text = raw_text["text"]
+
+ # create base Text object
+ text_object = Text(text=text, **_parse_common(raw_object).__dict__)
+
+ # optional attributes
+ if raw_text.get("color") is not None:
+ text_object.color = parse_color(raw_text["color"])
+
+ if raw_text.get("fontfamily") is not None:
+ text_object.font_family = raw_text["fontfamily"]
+
+ if raw_text.get("pixelsize") is not None:
+ text_object.font_size = raw_text["pixelsize"]
+
+ if raw_text.get("bold") is not None:
+ text_object.bold = raw_text["bold"]
+
+ if raw_text.get("italic") is not None:
+ text_object.italic = raw_text["italic"]
+
+ if raw_text.get("kerning") is not None:
+ text_object.kerning = raw_text["kerning"]
+
+ if raw_text.get("strikeout") is not None:
+ text_object.strike_out = raw_text["strikeout"]
+
+ if raw_text.get("underline") is not None:
+ text_object.underline = raw_text["underline"]
+
+ if raw_text.get("halign") is not None:
+ text_object.horizontal_align = raw_text["halign"]
+
+ if raw_text.get("valign") is not None:
+ text_object.vertical_align = raw_text["valign"]
+
+ if raw_text.get("wrap") is not None:
+ text_object.wrap = raw_text["wrap"]
+
+ return text_object
+
+
+def _get_parser(raw_object: RawObject) -> Callable[[RawObject], TiledObject]:
+ """Get the parser function for a given raw object.
+
+ Only used internally by the JSON parser.
+
+ Args:
+ raw_object: Raw object that is analyzed to determine the parser function.
+
+ Returns:
+ Callable[[RawObject], Object]: The parser function.
+ """
+ if raw_object.get("ellipse"):
+ return _parse_ellipse
+
+ if raw_object.get("point"):
+ return _parse_point
+
+ if raw_object.get("gid"):
+ # Only tile objects have the `gid` key
+ return _parse_tile
+
+ if raw_object.get("polygon"):
+ return _parse_polygon
+
+ if raw_object.get("polyline"):
+ return _parse_polyline
+
+ if raw_object.get("text"):
+ return _parse_text
+
+ # If it's none of the above, rectangle is the only one left.
+ # Rectangle is the only object which has no special properties to signify that.
+ return _parse_rectangle
+
+
+def parse(
+ raw_object: RawObject,
+ parent_dir: Optional[Path] = None,
+) -> TiledObject:
+ """Parse the raw object into a pytiled_parser version
+
+ Args:
+ raw_object: Raw object that is to be cast.
+ parent_dir: The parent directory that the map file is in.
+
+ Returns:
+ Object: A parsed Object.
+
+ Raises:
+ RuntimeError: When a parameter that is conditionally required was not sent.
+ """
+ new_tileset = None
+ new_tileset_path = None
+
+ if raw_object.get("template"):
+ if not parent_dir:
+ raise RuntimeError(
+ "A parent directory must be specified when using object templates."
+ )
+ template_path = Path(parent_dir / raw_object["template"])
+ template, new_tileset, new_tileset_path = load_object_template(template_path)
+
+ if isinstance(template, dict):
+ loaded_template = template["object"]
+ for key in loaded_template:
+ if key != "id":
+ raw_object[key] = loaded_template[key] # type: ignore
+ elif isinstance(template, etree.Element):
+ # load the XML object into the JSON object
+ raise NotImplementedError(
+ "Loading TMX object templates inside a JSON map is currently not supported, "
+ "but will be in a future release."
+ )
+
+ if raw_object.get("gid"):
+ return _parse_tile(raw_object, new_tileset, new_tileset_path)
+
+ return _get_parser(raw_object)(raw_object)
diff --git a/pytiled_parser/parsers/json/tileset.py b/pytiled_parser/parsers/json/tileset.py
new file mode 100644
index 0000000..3206bac
--- /dev/null
+++ b/pytiled_parser/parsers/json/tileset.py
@@ -0,0 +1,272 @@
+from pathlib import Path
+from typing import List, Optional, Union
+
+from typing_extensions import TypedDict
+
+from pytiled_parser.common_types import OrderedPair
+from pytiled_parser.parsers.json.layer import RawLayer
+from pytiled_parser.parsers.json.layer import parse as parse_layer
+from pytiled_parser.parsers.json.properties import RawProperty
+from pytiled_parser.parsers.json.properties import parse as parse_properties
+from pytiled_parser.parsers.json.wang_set import RawWangSet
+from pytiled_parser.parsers.json.wang_set import parse as parse_wangset
+from pytiled_parser.tileset import Frame, Grid, Tile, Tileset, Transformations
+from pytiled_parser.util import parse_color
+
+
+class RawFrame(TypedDict):
+ """The keys and their types that appear in a Frame JSON Object."""
+
+ duration: int
+ tileid: int
+
+
+class RawTileOffset(TypedDict):
+ """The keys and their types that appear in a TileOffset JSON Object."""
+
+ x: int
+ y: int
+
+
+class RawTransformations(TypedDict):
+ """The keys and their types that appear in a Transformations JSON Object."""
+
+ hflip: bool
+ vflip: bool
+ rotate: bool
+ preferuntransformed: bool
+
+
+class RawTile(TypedDict):
+ """The keys and their types that appear in a Tile JSON Object."""
+
+ animation: List[RawFrame]
+ id: int
+ image: str
+ imageheight: int
+ imagewidth: int
+ opacity: float
+ properties: List[RawProperty]
+ objectgroup: RawLayer
+ type: str
+
+
+class RawGrid(TypedDict):
+ """The keys and their types that appear in a Grid JSON Object."""
+
+ height: int
+ width: int
+ orientation: str
+
+
+class RawTileSet(TypedDict):
+ """The keys and their types that appear in a TileSet JSON Object."""
+
+ backgroundcolor: str
+ columns: int
+ firstgid: int
+ grid: RawGrid
+ image: str
+ imageheight: int
+ imagewidth: int
+ margin: int
+ name: str
+ properties: List[RawProperty]
+ source: str
+ spacing: int
+ tilecount: int
+ tiledversion: str
+ tileheight: int
+ tileoffset: RawTileOffset
+ tiles: List[RawTile]
+ tilewidth: int
+ transparentcolor: str
+ transformations: RawTransformations
+ version: Union[str, float]
+ wangsets: List[RawWangSet]
+
+
+def _parse_frame(raw_frame: RawFrame) -> Frame:
+ """Parse the raw_frame to a Frame.
+
+ Args:
+ raw_frame: RawFrame to be parsed to a Frame
+
+ Returns:
+ Frame: The Frame created from the raw_frame
+ """
+
+ return Frame(duration=raw_frame["duration"], tile_id=raw_frame["tileid"])
+
+
+def _parse_tile_offset(raw_tile_offset: RawTileOffset) -> OrderedPair:
+ """Parse the raw_tile_offset to an OrderedPair.
+
+ Args:
+ raw_tile_offset: RawTileOffset to be parsed to an OrderedPair
+
+ Returns:
+ OrderedPair: The OrderedPair created from the raw_tile_offset
+ """
+
+ return OrderedPair(raw_tile_offset["x"], raw_tile_offset["y"])
+
+
+def _parse_transformations(raw_transformations: RawTransformations) -> Transformations:
+ """Parse the raw_transformations to a Transformations object.
+
+ Args:
+ raw_transformations: RawTransformations to be parsed to a Transformations
+
+ Returns:
+ Transformations: The Transformations created from the raw_transformations
+ """
+
+ return Transformations(
+ hflip=raw_transformations["hflip"],
+ vflip=raw_transformations["vflip"],
+ rotate=raw_transformations["rotate"],
+ prefer_untransformed=raw_transformations["preferuntransformed"],
+ )
+
+
+def _parse_grid(raw_grid: RawGrid) -> Grid:
+ """Parse the raw_grid to a Grid object.
+
+ Args:
+ raw_grid: RawGrid to be parsed to a Grid
+
+ Returns:
+ Grid: The Grid created from the raw_grid
+ """
+
+ return Grid(
+ orientation=raw_grid["orientation"],
+ width=raw_grid["width"],
+ height=raw_grid["height"],
+ )
+
+
+def _parse_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile:
+ """Parse the raw_tile to a Tile object.
+
+ Args:
+ raw_tile: RawTile to be parsed to a Tile
+
+ Returns:
+ Tile: The Tile created from the raw_tile
+ """
+
+ id_ = raw_tile["id"]
+ tile = Tile(id=id_)
+
+ if raw_tile.get("animation") is not None:
+ tile.animation = []
+ for frame in raw_tile["animation"]:
+ tile.animation.append(_parse_frame(frame))
+
+ if raw_tile.get("objectgroup") is not None:
+ tile.objects = parse_layer(raw_tile["objectgroup"])
+
+ if raw_tile.get("properties") is not None:
+ tile.properties = parse_properties(raw_tile["properties"])
+
+ if raw_tile.get("image") is not None:
+ if external_path:
+ tile.image = Path(external_path / raw_tile["image"]).absolute().resolve()
+ else:
+ tile.image = Path(raw_tile["image"])
+
+ if raw_tile.get("imagewidth") is not None:
+ tile.image_width = raw_tile["imagewidth"]
+
+ if raw_tile.get("imageheight") is not None:
+ tile.image_height = raw_tile["imageheight"]
+
+ if raw_tile.get("type") is not None:
+ tile.type = raw_tile["type"]
+
+ return tile
+
+
+def parse(
+ raw_tileset: RawTileSet,
+ firstgid: int,
+ external_path: Optional[Path] = None,
+) -> Tileset:
+ """Parse the raw tileset into a pytiled_parser type
+
+ Args:
+ raw_tileset: Raw Tileset to be parsed.
+ firstgid: GID corresponding the first tile in the set.
+ external_path: The path to the tileset if it is not an embedded one.
+
+ Returns:
+ TileSet: a properly typed TileSet.
+ """
+
+ tileset = Tileset(
+ name=raw_tileset["name"],
+ tile_count=raw_tileset["tilecount"],
+ tile_width=raw_tileset["tilewidth"],
+ tile_height=raw_tileset["tileheight"],
+ columns=raw_tileset["columns"],
+ spacing=raw_tileset["spacing"],
+ margin=raw_tileset["margin"],
+ firstgid=firstgid,
+ )
+
+ if raw_tileset.get("version") is not None:
+ if isinstance(raw_tileset["version"], float):
+ tileset.version = str(raw_tileset["version"])
+ else:
+ tileset.version = raw_tileset["version"]
+
+ if raw_tileset.get("tiledversion") is not None:
+ tileset.tiled_version = raw_tileset["tiledversion"]
+
+ if raw_tileset.get("image") is not None:
+ if external_path:
+ tileset.image = (
+ Path(external_path / raw_tileset["image"]).absolute().resolve()
+ )
+ else:
+ tileset.image = Path(raw_tileset["image"])
+
+ if raw_tileset.get("imagewidth") is not None:
+ tileset.image_width = raw_tileset["imagewidth"]
+
+ if raw_tileset.get("imageheight") is not None:
+ tileset.image_height = raw_tileset["imageheight"]
+
+ if raw_tileset.get("backgroundcolor") is not None:
+ tileset.background_color = parse_color(raw_tileset["backgroundcolor"])
+
+ if raw_tileset.get("tileoffset") is not None:
+ tileset.tile_offset = _parse_tile_offset(raw_tileset["tileoffset"])
+
+ if raw_tileset.get("transparentcolor") is not None:
+ tileset.transparent_color = parse_color(raw_tileset["transparentcolor"])
+
+ if raw_tileset.get("grid") is not None:
+ tileset.grid = _parse_grid(raw_tileset["grid"])
+
+ if raw_tileset.get("properties") is not None:
+ tileset.properties = parse_properties(raw_tileset["properties"])
+
+ if raw_tileset.get("tiles") is not None:
+ tiles = {}
+ for raw_tile in raw_tileset["tiles"]:
+ tiles[raw_tile["id"]] = _parse_tile(raw_tile, external_path=external_path)
+ tileset.tiles = tiles
+
+ if raw_tileset.get("wangsets") is not None:
+ wangsets = []
+ for raw_wangset in raw_tileset["wangsets"]:
+ wangsets.append(parse_wangset(raw_wangset))
+ tileset.wang_sets = wangsets
+
+ if raw_tileset.get("transformations") is not None:
+ tileset.transformations = _parse_transformations(raw_tileset["transformations"])
+
+ return tileset
diff --git a/pytiled_parser/parsers/json/wang_set.py b/pytiled_parser/parsers/json/wang_set.py
new file mode 100644
index 0000000..ea68905
--- /dev/null
+++ b/pytiled_parser/parsers/json/wang_set.py
@@ -0,0 +1,104 @@
+from typing import List
+
+from typing_extensions import TypedDict
+
+from pytiled_parser.parsers.json.properties import RawProperty
+from pytiled_parser.parsers.json.properties import parse as parse_properties
+from pytiled_parser.util import parse_color
+from pytiled_parser.wang_set import WangColor, WangSet, WangTile
+
+
+class RawWangTile(TypedDict):
+ """The keys and their types that appear in a Wang Tile JSON Object."""
+
+ tileid: int
+ # Tiled stores these IDs as a list represented like so:
+ # [top, top_right, right, bottom_right, bottom, bottom_left, left, top_left]
+ wangid: List[int]
+
+
+class RawWangColor(TypedDict):
+ """The keys and their types that appear in a Wang Color JSON Object."""
+
+ color: str
+ name: str
+ probability: float
+ tile: int
+ properties: List[RawProperty]
+
+
+class RawWangSet(TypedDict):
+ """The keys and their types that appear in a Wang Set JSON Object."""
+
+ colors: List[RawWangColor]
+ name: str
+ properties: List[RawProperty]
+ tile: int
+ type: str
+ wangtiles: List[RawWangTile]
+
+
+def _parse_wang_tile(raw_wang_tile: RawWangTile) -> WangTile:
+ """Parse the raw wang tile into a pytiled_parser type
+
+ Args:
+ raw_wang_tile: RawWangTile to be parsed.
+
+ Returns:
+ WangTile: A properly typed WangTile.
+ """
+ return WangTile(tile_id=raw_wang_tile["tileid"], wang_id=raw_wang_tile["wangid"])
+
+
+def _parse_wang_color(raw_wang_color: RawWangColor) -> WangColor:
+ """Parse the raw wang color into a pytiled_parser type
+
+ Args:
+ raw_wang_color: RawWangColor to be parsed.
+
+ Returns:
+ WangColor: A properly typed WangColor.
+ """
+ wang_color = WangColor(
+ name=raw_wang_color["name"],
+ color=parse_color(raw_wang_color["color"]),
+ tile=raw_wang_color["tile"],
+ probability=raw_wang_color["probability"],
+ )
+
+ if raw_wang_color.get("properties") is not None:
+ wang_color.properties = parse_properties(raw_wang_color["properties"])
+
+ return wang_color
+
+
+def parse(raw_wangset: RawWangSet) -> WangSet:
+ """Parse the raw wangset into a pytiled_parser type
+
+ Args:
+ raw_wangset: Raw Wangset to be parsed.
+
+ Returns:
+ WangSet: A properly typed WangSet.
+ """
+
+ colors = []
+ for raw_wang_color in raw_wangset["colors"]:
+ colors.append(_parse_wang_color(raw_wang_color))
+
+ tiles = {}
+ for raw_wang_tile in raw_wangset["wangtiles"]:
+ tiles[raw_wang_tile["tileid"]] = _parse_wang_tile(raw_wang_tile)
+
+ wangset = WangSet(
+ name=raw_wangset["name"],
+ tile=raw_wangset["tile"],
+ wang_type=raw_wangset["type"],
+ wang_colors=colors,
+ wang_tiles=tiles,
+ )
+
+ if raw_wangset.get("properties") is not None:
+ wangset.properties = parse_properties(raw_wangset["properties"])
+
+ return wangset
diff --git a/pytiled_parser/parsers/tmx/layer.py b/pytiled_parser/parsers/tmx/layer.py
new file mode 100644
index 0000000..4ba1fa1
--- /dev/null
+++ b/pytiled_parser/parsers/tmx/layer.py
@@ -0,0 +1,360 @@
+"""Layer parsing for the TMX Map Format.
+"""
+import base64
+import gzip
+import importlib.util
+import xml.etree.ElementTree as etree
+import zlib
+from pathlib import Path
+from typing import List, Optional
+
+from pytiled_parser.common_types import OrderedPair, Size
+from pytiled_parser.layer import (
+ Chunk,
+ ImageLayer,
+ Layer,
+ LayerGroup,
+ ObjectLayer,
+ TileLayer,
+)
+from pytiled_parser.parsers.tmx.properties import parse as parse_properties
+from pytiled_parser.parsers.tmx.tiled_object import parse as parse_object
+from pytiled_parser.util import parse_color
+
+zstd_spec = importlib.util.find_spec("zstd")
+if zstd_spec:
+ import zstd
+else:
+ zstd = None
+
+
+def _convert_raw_tile_layer_data(data: List[int], layer_width: int) -> List[List[int]]:
+ """Convert raw layer data into a nested lit based on the layer width
+
+ Args:
+ data: The data to convert
+ layer_width: Width of the layer
+
+ Returns:
+ List[List[int]]: A nested list containing the converted data
+ """
+ tile_grid: List[List[int]] = [[]]
+
+ column_count = 0
+ row_count = 0
+ for item in data:
+ column_count += 1
+ tile_grid[row_count].append(item)
+ if not column_count % layer_width and column_count < len(data):
+ row_count += 1
+ tile_grid.append([])
+
+ return tile_grid
+
+
+def _decode_tile_layer_data(
+ data: str, compression: str, layer_width: int
+) -> List[List[int]]:
+ """Decode Base64 Encoded tile data. Optionally supports gzip and zlib compression.
+
+ Args:
+ data: The base64 encoded data
+ compression: Either zlib, gzip, or empty. If empty no decompression is done.
+
+ Returns:
+ List[List[int]]: A nested list containing the decoded data
+
+ Raises:
+ ValueError: For an unsupported compression type.
+ """
+ unencoded_data = base64.b64decode(data)
+ if compression == "zlib":
+ unzipped_data = zlib.decompress(unencoded_data)
+ elif compression == "gzip":
+ unzipped_data = gzip.decompress(unencoded_data)
+ elif compression == "zstd" and zstd is None:
+ raise ValueError(
+ "zstd compression support is not installed."
+ "To install use 'pip install pytiled-parser[zstd]'"
+ )
+ elif compression == "zstd":
+ unzipped_data = zstd.decompress(unencoded_data)
+ else:
+ unzipped_data = unencoded_data
+
+ tile_grid: List[int] = []
+
+ byte_count = 0
+ int_count = 0
+ int_value = 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.append(int_value)
+ int_value = 0
+
+ return _convert_raw_tile_layer_data(tile_grid, layer_width)
+
+
+def _parse_chunk(
+ raw_chunk: etree.Element,
+ encoding: Optional[str] = None,
+ compression: Optional[str] = None,
+) -> Chunk:
+ """Parse the raw_chunk to a Chunk.
+
+ Args:
+ raw_chunk: XML Element to be parsed to a Chunk
+ encoding: Encoding type. ("base64" or None)
+ compression: Either zlib, gzip, or empty. If empty no decompression is done.
+
+ Returns:
+ Chunk: The Chunk created from the raw_chunk
+ """
+ if encoding == "base64":
+ assert isinstance(compression, str)
+ data = _decode_tile_layer_data(
+ raw_chunk.text, compression, int(raw_chunk.attrib["width"]) # type: ignore
+ )
+ else:
+ data = _convert_raw_tile_layer_data(
+ [int(v.strip()) for v in raw_chunk.text.split(",")], # type: ignore
+ int(raw_chunk.attrib["width"]),
+ )
+
+ return Chunk(
+ coordinates=OrderedPair(int(raw_chunk.attrib["x"]), int(raw_chunk.attrib["y"])),
+ size=Size(int(raw_chunk.attrib["width"]), int(raw_chunk.attrib["height"])),
+ data=data,
+ )
+
+
+def _parse_common(raw_layer: etree.Element) -> Layer:
+ """Create a Layer containing all the attributes common to all layer types.
+
+ This is to create the stub Layer object that can then be used to create the actual
+ specific sub-classes of Layer.
+
+ Args:
+ raw_layer: XML Element to get common attributes from
+
+ Returns:
+ Layer: The attributes in common of all layer types
+ """
+ if raw_layer.attrib.get("name") is None:
+ raw_layer.attrib["name"] = ""
+
+ common = Layer(
+ name=raw_layer.attrib["name"],
+ )
+
+ if raw_layer.attrib.get("opacity") is not None:
+ common.opacity = float(raw_layer.attrib["opacity"])
+
+ if raw_layer.attrib.get("visible") is not None:
+ common.visible = bool(int(raw_layer.attrib["visible"]))
+
+ if raw_layer.attrib.get("id") is not None:
+ common.id = int(raw_layer.attrib["id"])
+
+ if raw_layer.attrib.get("offsetx") is not None:
+ common.offset = OrderedPair(
+ float(raw_layer.attrib["offsetx"]), float(raw_layer.attrib["offsety"])
+ )
+
+ properties_element = raw_layer.find("./properties")
+ if properties_element is not None:
+ common.properties = parse_properties(properties_element)
+
+ parallax = [1.0, 1.0]
+
+ if raw_layer.attrib.get("parallaxx") is not None:
+ parallax[0] = float(raw_layer.attrib["parallaxx"])
+
+ if raw_layer.attrib.get("parallaxy") is not None:
+ parallax[1] = float(raw_layer.attrib["parallaxy"])
+
+ common.parallax_factor = OrderedPair(parallax[0], parallax[1])
+
+ if raw_layer.attrib.get("tintcolor") is not None:
+ common.tint_color = parse_color(raw_layer.attrib["tintcolor"])
+
+ return common
+
+
+def _parse_tile_layer(raw_layer: etree.Element) -> TileLayer:
+ """Parse the raw_layer to a TileLayer.
+
+ Args:
+ raw_layer: XML Element to be parsed to a TileLayer.
+
+ Returns:
+ TileLayer: The TileLayer created from raw_layer
+ """
+ common = _parse_common(raw_layer).__dict__
+ del common["size"]
+ tile_layer = TileLayer(
+ size=Size(int(raw_layer.attrib["width"]), int(raw_layer.attrib["height"])),
+ **common,
+ )
+
+ data_element = raw_layer.find("data")
+ if data_element is not None:
+ encoding = None
+ if data_element.attrib.get("encoding") is not None:
+ encoding = data_element.attrib["encoding"]
+
+ compression = ""
+ if data_element.attrib.get("compression") is not None:
+ compression = data_element.attrib["compression"]
+
+ raw_chunks = data_element.findall("chunk")
+ if not raw_chunks:
+ if encoding and encoding != "csv":
+ tile_layer.data = _decode_tile_layer_data(
+ data=data_element.text, # type: ignore
+ compression=compression,
+ layer_width=int(raw_layer.attrib["width"]),
+ )
+ else:
+ tile_layer.data = _convert_raw_tile_layer_data(
+ [int(v.strip()) for v in data_element.text.split(",")], # type: ignore
+ int(raw_layer.attrib["width"]),
+ )
+ else:
+ chunks = []
+ for raw_chunk in raw_chunks:
+ chunks.append(
+ _parse_chunk(
+ raw_chunk,
+ encoding,
+ compression,
+ )
+ )
+
+ if chunks:
+ tile_layer.chunks = chunks
+
+ return tile_layer
+
+
+def _parse_object_layer(
+ raw_layer: etree.Element, parent_dir: Optional[Path] = None
+) -> ObjectLayer:
+ """Parse the raw_layer to an ObjectLayer.
+
+ Args:
+ raw_layer: XML Element to be parsed to an ObjectLayer.
+
+ Returns:
+ ObjectLayer: The ObjectLayer created from raw_layer
+ """
+ objects = []
+ for object_ in raw_layer.findall("./object"):
+ objects.append(parse_object(object_, parent_dir))
+
+ object_layer = ObjectLayer(
+ tiled_objects=objects,
+ **_parse_common(raw_layer).__dict__,
+ )
+
+ if raw_layer.attrib.get("draworder") is not None:
+ object_layer.draw_order = raw_layer.attrib["draworder"]
+
+ return object_layer
+
+
+def _parse_image_layer(raw_layer: etree.Element) -> ImageLayer:
+ """Parse the raw_layer to an ImageLayer.
+
+ Args:
+ raw_layer: XML Element to be parsed to an ImageLayer.
+
+ Returns:
+ ImageLayer: The ImageLayer created from raw_layer
+ """
+ image_element = raw_layer.find("./image")
+ if image_element is not None:
+ source = Path(image_element.attrib["source"])
+
+ transparent_color = None
+ if image_element.attrib.get("trans") is not None:
+ transparent_color = parse_color(image_element.attrib["trans"])
+
+ image_layer = ImageLayer(
+ image=source,
+ transparent_color=transparent_color,
+ **_parse_common(raw_layer).__dict__,
+ )
+ print(image_layer.size)
+ return image_layer
+
+ raise RuntimeError("Tried to parse an image layer that doesn't have an image!")
+
+
+def _parse_group_layer(
+ raw_layer: etree.Element, parent_dir: Optional[Path] = None
+) -> LayerGroup:
+ """Parse the raw_layer to a LayerGroup.
+
+ Args:
+ raw_layer: XML Element to be parsed to a LayerGroup.
+
+ Returns:
+ LayerGroup: The LayerGroup created from raw_layer
+ """
+ layers: List[Layer] = []
+ for layer in raw_layer.findall("./layer"):
+ layers.append(_parse_tile_layer(layer))
+
+ for layer in raw_layer.findall("./objectgroup"):
+ layers.append(_parse_object_layer(layer, parent_dir))
+
+ for layer in raw_layer.findall("./imagelayer"):
+ layers.append(_parse_image_layer(layer))
+
+ for layer in raw_layer.findall("./group"):
+ layers.append(_parse_group_layer(layer, parent_dir))
+ # layers = []
+ # layers = [
+ # parse(child_layer, parent_dir=parent_dir)
+ # for child_layer in raw_layer.iter()
+ # if child_layer.tag in ["layer", "objectgroup", "imagelayer", "group"]
+ # ]
+
+ return LayerGroup(layers=layers, **_parse_common(raw_layer).__dict__)
+
+
+def parse(
+ raw_layer: etree.Element,
+ parent_dir: Optional[Path] = None,
+) -> Layer:
+ """Parse a raw Layer into a pytiled_parser object.
+
+ This function will determine the type of layer and parse accordingly.
+
+ Args:
+ raw_layer: Raw layer to be parsed.
+ parent_dir: The parent directory that the map file is in.
+
+ Returns:
+ Layer: A parsed Layer.
+
+ Raises:
+ RuntimeError: For an invalid layer type being provided
+ """
+ type_ = raw_layer.tag
+
+ if type_ == "objectgroup":
+ return _parse_object_layer(raw_layer, parent_dir)
+ elif type_ == "group":
+ return _parse_group_layer(raw_layer, parent_dir)
+ elif type_ == "imagelayer":
+ return _parse_image_layer(raw_layer)
+ elif type_ == "layer":
+ return _parse_tile_layer(raw_layer)
+
+ raise RuntimeError(f"An invalid layer type of {type_} was supplied")
diff --git a/pytiled_parser/parsers/tmx/properties.py b/pytiled_parser/parsers/tmx/properties.py
new file mode 100644
index 0000000..173463b
--- /dev/null
+++ b/pytiled_parser/parsers/tmx/properties.py
@@ -0,0 +1,33 @@
+import xml.etree.ElementTree as etree
+from pathlib import Path
+from typing import List, Union, cast
+
+from pytiled_parser.properties import Properties, Property
+from pytiled_parser.util import parse_color
+
+
+def parse(raw_properties: etree.Element) -> Properties:
+
+ final: Properties = {}
+ value: Property
+
+ for raw_property in raw_properties.findall("property"):
+
+ type_ = raw_property.attrib.get("type")
+ value_ = raw_property.attrib["value"]
+ if type_ == "file":
+ value = Path(value_)
+ elif type_ == "color":
+ value = parse_color(value_)
+ elif type_ == "int" or type_ == "float":
+ value = float(value_)
+ elif type_ == "bool":
+ if value_ == "true":
+ value = True
+ else:
+ value = False
+ else:
+ value = value_
+ final[raw_property.attrib["name"]] = value
+
+ return final
diff --git a/pytiled_parser/parsers/tmx/tiled_map.py b/pytiled_parser/parsers/tmx/tiled_map.py
new file mode 100644
index 0000000..f12c6e2
--- /dev/null
+++ b/pytiled_parser/parsers/tmx/tiled_map.py
@@ -0,0 +1,132 @@
+import json
+import xml.etree.ElementTree as etree
+from pathlib import Path
+
+from pytiled_parser.common_types import OrderedPair, Size
+from pytiled_parser.exception import UnknownFormat
+from pytiled_parser.parsers.json.tileset import parse as parse_json_tileset
+from pytiled_parser.parsers.tmx.layer import parse as parse_layer
+from pytiled_parser.parsers.tmx.properties import parse as parse_properties
+from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx_tileset
+from pytiled_parser.tiled_map import TiledMap, TilesetDict
+from pytiled_parser.util import check_format, parse_color
+
+
+def parse(file: Path) -> TiledMap:
+ """Parse the raw Tiled map into a pytiled_parser type.
+
+ Args:
+ file: Path to the map file.
+
+ Returns:
+ TiledMap: A parsed TiledMap.
+ """
+ with open(file) as map_file:
+ raw_map = etree.parse(map_file).getroot()
+
+ parent_dir = file.parent
+
+ raw_tilesets = raw_map.findall("./tileset")
+ tilesets: TilesetDict = {}
+
+ for raw_tileset in raw_tilesets:
+ if raw_tileset.attrib.get("source") is not None:
+ # Is an external Tileset
+ tileset_path = Path(parent_dir / raw_tileset.attrib["source"])
+ parser = check_format(tileset_path)
+ with open(tileset_path) as tileset_file:
+ if parser == "tmx":
+ raw_tileset_external = etree.parse(tileset_file).getroot()
+ tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset(
+ raw_tileset_external,
+ int(raw_tileset.attrib["firstgid"]),
+ external_path=tileset_path.parent,
+ )
+ elif parser == "json":
+ tilesets[int(raw_tileset.attrib["firstgid"])] = parse_json_tileset(
+ json.load(tileset_file),
+ int(raw_tileset.attrib["firstgid"]),
+ external_path=tileset_path.parent,
+ )
+ else:
+ raise UnknownFormat(
+ "Unkown Tileset format, please use either the TSX or JSON format."
+ )
+
+ else:
+ # Is an embedded Tileset
+ tilesets[int(raw_tileset.attrib["firstgid"])] = parse_tmx_tileset(
+ raw_tileset, int(raw_tileset.attrib["firstgid"])
+ )
+
+ layers = []
+ for element in raw_map.iter():
+ if element.tag in ["layer", "objectgroup", "imagelayer", "group"]:
+ layers.append(parse_layer(element, parent_dir))
+
+ map_ = TiledMap(
+ map_file=file,
+ infinite=bool(int(raw_map.attrib["infinite"])),
+ layers=layers,
+ map_size=Size(int(raw_map.attrib["width"]), int(raw_map.attrib["height"])),
+ next_layer_id=int(raw_map.attrib["nextlayerid"]),
+ next_object_id=int(raw_map.attrib["nextobjectid"]),
+ orientation=raw_map.attrib["orientation"],
+ render_order=raw_map.attrib["renderorder"],
+ tiled_version=raw_map.attrib["tiledversion"],
+ tile_size=Size(
+ int(raw_map.attrib["tilewidth"]), int(raw_map.attrib["tileheight"])
+ ),
+ tilesets=tilesets,
+ version=raw_map.attrib["version"],
+ )
+
+ layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")]
+
+ for my_layer in layers:
+ for tiled_object in my_layer.tiled_objects:
+ if hasattr(tiled_object, "new_tileset"):
+ if tiled_object.new_tileset is not None:
+ already_loaded = None
+ for val in map_.tilesets.values():
+ if val.name == tiled_object.new_tileset.attrib["name"]:
+ already_loaded = val
+ break
+
+ if not already_loaded:
+ print("here")
+ highest_firstgid = max(map_.tilesets.keys())
+ last_tileset_count = map_.tilesets[highest_firstgid].tile_count
+ new_firstgid = highest_firstgid + last_tileset_count
+ map_.tilesets[new_firstgid] = parse_tmx_tileset(
+ tiled_object.new_tileset,
+ new_firstgid,
+ tiled_object.new_tileset_path,
+ )
+ tiled_object.gid = tiled_object.gid + (new_firstgid - 1)
+
+ else:
+ tiled_object.gid = tiled_object.gid + (
+ already_loaded.firstgid - 1
+ )
+
+ tiled_object.new_tileset = None
+ tiled_object.new_tileset_path = None
+
+ if raw_map.attrib.get("backgroundcolor") is not None:
+ map_.background_color = parse_color(raw_map.attrib["backgroundcolor"])
+
+ if raw_map.attrib.get("hexsidelength") is not None:
+ map_.hex_side_length = int(raw_map.attrib["hexsidelength"])
+
+ properties_element = raw_map.find("./properties")
+ if properties_element:
+ map_.properties = parse_properties(properties_element)
+
+ if raw_map.attrib.get("staggeraxis") is not None:
+ map_.stagger_axis = raw_map.attrib["staggeraxis"]
+
+ if raw_map.attrib.get("staggerindex") is not None:
+ map_.stagger_index = raw_map.attrib["staggerindex"]
+
+ return map_
diff --git a/pytiled_parser/parsers/tmx/tiled_object.py b/pytiled_parser/parsers/tmx/tiled_object.py
new file mode 100644
index 0000000..ceb4e08
--- /dev/null
+++ b/pytiled_parser/parsers/tmx/tiled_object.py
@@ -0,0 +1,293 @@
+import json
+import xml.etree.ElementTree as etree
+from pathlib import Path
+from typing import Callable, Optional
+
+from pytiled_parser.common_types import OrderedPair, Size
+from pytiled_parser.parsers.tmx.properties import parse as parse_properties
+from pytiled_parser.tiled_object import (
+ Ellipse,
+ Point,
+ Polygon,
+ Polyline,
+ Rectangle,
+ Text,
+ Tile,
+ TiledObject,
+)
+from pytiled_parser.util import load_object_template, parse_color
+
+
+def _parse_common(raw_object: etree.Element) -> TiledObject:
+ """Create an Object containing all the attributes common to all types of objects.
+
+ Args:
+ raw_object: XML Element to get common attributes from
+
+ Returns:
+ Object: The attributes in common of all types of objects
+ """
+
+ common = TiledObject(
+ id=int(raw_object.attrib["id"]),
+ coordinates=OrderedPair(
+ float(raw_object.attrib["x"]), float(raw_object.attrib["y"])
+ ),
+ )
+
+ if raw_object.attrib.get("width") is not None:
+ common.size = Size(
+ float(raw_object.attrib["width"]), float(raw_object.attrib["height"])
+ )
+
+ if raw_object.attrib.get("visible") is not None:
+ common.visible = bool(int(raw_object.attrib["visible"]))
+
+ if raw_object.attrib.get("rotation") is not None:
+ common.rotation = float(raw_object.attrib["rotation"])
+
+ if raw_object.attrib.get("name") is not None:
+ common.name = raw_object.attrib["name"]
+
+ if raw_object.attrib.get("type") is not None:
+ common.type = raw_object.attrib["type"]
+
+ properties_element = raw_object.find("./properties")
+ if properties_element:
+ common.properties = parse_properties(properties_element)
+
+ return common
+
+
+def _parse_ellipse(raw_object: etree.Element) -> Ellipse:
+ """Parse the raw object into an Ellipse.
+
+ Args:
+ raw_object: XML Element to be parsed to an Ellipse
+
+ Returns:
+ Ellipse: The Ellipse object created from the raw object
+ """
+ return Ellipse(**_parse_common(raw_object).__dict__)
+
+
+def _parse_rectangle(raw_object: etree.Element) -> Rectangle:
+ """Parse the raw object into a Rectangle.
+
+ Args:
+ raw_object: XML Element to be parsed to a Rectangle
+
+ Returns:
+ Rectangle: The Rectangle object created from the raw object
+ """
+ return Rectangle(**_parse_common(raw_object).__dict__)
+
+
+def _parse_point(raw_object: etree.Element) -> Point:
+ """Parse the raw object into a Point.
+
+ Args:
+ raw_object: XML Element to be parsed to a Point
+
+ Returns:
+ Point: The Point object created from the raw object
+ """
+ return Point(**_parse_common(raw_object).__dict__)
+
+
+def _parse_polygon(raw_object: etree.Element) -> Polygon:
+ """Parse the raw object into a Polygon.
+
+ Args:
+ raw_object: XML Element to be parsed to a Polygon
+
+ Returns:
+ Polygon: The Polygon object created from the raw object
+ """
+ polygon = []
+ polygon_element = raw_object.find("./polygon")
+ if polygon_element is not None:
+ for raw_point in polygon_element.attrib["points"].split(" "):
+ point = raw_point.split(",")
+ polygon.append(OrderedPair(float(point[0]), float(point[1])))
+
+ return Polygon(points=polygon, **_parse_common(raw_object).__dict__)
+
+
+def _parse_polyline(raw_object: etree.Element) -> Polyline:
+ """Parse the raw object into a Polyline.
+
+ Args:
+ raw_object: Raw object to be parsed to a Polyline
+
+ Returns:
+ Polyline: The Polyline object created from the raw object
+ """
+ polyline = []
+ polyline_element = raw_object.find("./polyline")
+ if polyline_element is not None:
+ for raw_point in polyline_element.attrib["points"].split(" "):
+ point = raw_point.split(",")
+ polyline.append(OrderedPair(float(point[0]), float(point[1])))
+
+ return Polyline(points=polyline, **_parse_common(raw_object).__dict__)
+
+
+def _parse_tile(
+ raw_object: etree.Element,
+ new_tileset: Optional[etree.Element] = None,
+ new_tileset_path: Optional[Path] = None,
+) -> Tile:
+ """Parse the raw object into a Tile.
+
+ Args:
+ raw_object: XML Element to be parsed to a Tile
+
+ Returns:
+ Tile: The Tile object created from the raw object
+ """
+ return Tile(
+ gid=int(raw_object.attrib["gid"]),
+ new_tileset=new_tileset,
+ new_tileset_path=new_tileset_path,
+ **_parse_common(raw_object).__dict__
+ )
+
+
+def _parse_text(raw_object: etree.Element) -> Text:
+ """Parse the raw object into Text.
+
+ Args:
+ raw_object: XML Element to be parsed to a Text
+
+ Returns:
+ Text: The Text object created from the raw object
+ """
+ # required attributes
+ text_element = raw_object.find("./text")
+
+ if text_element is not None:
+ text = text_element.text
+
+ if not text:
+ text = ""
+ # create base Text object
+ text_object = Text(text=text, **_parse_common(raw_object).__dict__)
+
+ # optional attributes
+
+ if text_element.attrib.get("color") is not None:
+ text_object.color = parse_color(text_element.attrib["color"])
+
+ if text_element.attrib.get("fontfamily") is not None:
+ text_object.font_family = text_element.attrib["fontfamily"]
+
+ if text_element.attrib.get("pixelsize") is not None:
+ text_object.font_size = float(text_element.attrib["pixelsize"])
+
+ if text_element.attrib.get("bold") is not None:
+ text_object.bold = bool(int(text_element.attrib["bold"]))
+
+ if text_element.attrib.get("italic") is not None:
+ text_object.italic = bool(int(text_element.attrib["italic"]))
+
+ if text_element.attrib.get("kerning") is not None:
+ text_object.kerning = bool(int(text_element.attrib["kerning"]))
+
+ if text_element.attrib.get("strikeout") is not None:
+ text_object.strike_out = bool(int(text_element.attrib["strikeout"]))
+
+ if text_element.attrib.get("underline") is not None:
+ text_object.underline = bool(int(text_element.attrib["underline"]))
+
+ if text_element.attrib.get("halign") is not None:
+ text_object.horizontal_align = text_element.attrib["halign"]
+
+ if text_element.attrib.get("valign") is not None:
+ text_object.vertical_align = text_element.attrib["valign"]
+
+ if text_element.attrib.get("wrap") is not None:
+ text_object.wrap = bool(int(text_element.attrib["wrap"]))
+
+ return text_object
+
+
+def _get_parser(raw_object: etree.Element) -> Callable[[etree.Element], TiledObject]:
+ """Get the parser function for a given raw object.
+
+ Only used internally by the TMX parser.
+
+ Args:
+ raw_object: XML Element that is analyzed to determine the parser function.
+
+ Returns:
+ Callable[[Element], Object]: The parser function.
+ """
+ if raw_object.find("./ellipse") is not None:
+ return _parse_ellipse
+
+ if raw_object.find("./point") is not None:
+ return _parse_point
+
+ if raw_object.find("./polygon") is not None:
+ return _parse_polygon
+
+ if raw_object.find("./polyline") is not None:
+ return _parse_polyline
+
+ if raw_object.find("./text") is not None:
+ return _parse_text
+
+ # If it's none of the above, rectangle is the only one left.
+ # Rectangle is the only object which has no properties to signify that.
+ return _parse_rectangle
+
+
+def parse(raw_object: etree.Element, parent_dir: Optional[Path] = None) -> TiledObject:
+ """Parse the raw object into a pytiled_parser version
+
+ Args:
+ raw_object: XML Element that is to be parsed.
+ parent_dir: The parent directory that the map file is in.
+
+ Returns:
+ TiledObject: A parsed Object.
+
+ Raises:
+ RuntimeError: When a parameter that is conditionally required was not sent.
+ """
+ new_tileset = None
+ new_tileset_path = None
+
+ if raw_object.attrib.get("template"):
+ if not parent_dir:
+ raise RuntimeError(
+ "A parent directory must be specified when using object templates."
+ )
+ template_path = Path(parent_dir / raw_object.attrib["template"])
+ template, new_tileset, new_tileset_path = load_object_template(template_path)
+
+ if isinstance(template, etree.Element):
+ new_object = template.find("./object")
+ if new_object is not None:
+ if raw_object.attrib.get("id") is not None:
+ new_object.attrib["id"] = raw_object.attrib["id"]
+
+ if raw_object.attrib.get("x") is not None:
+ new_object.attrib["x"] = raw_object.attrib["x"]
+
+ if raw_object.attrib.get("y") is not None:
+ new_object.attrib["y"] = raw_object.attrib["y"]
+
+ raw_object = new_object
+ elif isinstance(template, dict):
+ # load the JSON object into the XML object
+ raise NotImplementedError(
+ "Loading JSON object templates inside a TMX map is currently not supported, "
+ "but will be in a future release."
+ )
+
+ if raw_object.attrib.get("gid"):
+ return _parse_tile(raw_object, new_tileset, new_tileset_path)
+
+ return _get_parser(raw_object)(raw_object)
diff --git a/pytiled_parser/parsers/tmx/tileset.py b/pytiled_parser/parsers/tmx/tileset.py
new file mode 100644
index 0000000..712d1cf
--- /dev/null
+++ b/pytiled_parser/parsers/tmx/tileset.py
@@ -0,0 +1,194 @@
+import xml.etree.ElementTree as etree
+from pathlib import Path
+from typing import Optional
+
+from pytiled_parser.common_types import OrderedPair
+from pytiled_parser.parsers.tmx.layer import parse as parse_layer
+from pytiled_parser.parsers.tmx.properties import parse as parse_properties
+from pytiled_parser.parsers.tmx.wang_set import parse as parse_wangset
+from pytiled_parser.tileset import Frame, Grid, Tile, Tileset, Transformations
+from pytiled_parser.util import parse_color
+
+
+def _parse_frame(raw_frame: etree.Element) -> Frame:
+ """Parse the raw_frame to a Frame object.
+
+ Args:
+ raw_frame: XML Element to be parsed to a Frame
+
+ Returns:
+ Frame: The Frame created from the raw_frame
+ """
+
+ return Frame(
+ duration=int(raw_frame.attrib["duration"]),
+ tile_id=int(raw_frame.attrib["tileid"]),
+ )
+
+
+def _parse_grid(raw_grid: etree.Element) -> Grid:
+ """Parse the raw_grid to a Grid object.
+
+ Args:
+ raw_grid: XML Element to be parsed to a Grid
+
+ Returns:
+ Grid: The Grid created from the raw_grid
+ """
+
+ return Grid(
+ orientation=raw_grid.attrib["orientation"],
+ width=int(raw_grid.attrib["width"]),
+ height=int(raw_grid.attrib["height"]),
+ )
+
+
+def _parse_transformations(raw_transformations: etree.Element) -> Transformations:
+ """Parse the raw_transformations to a Transformations object.
+
+ Args:
+ raw_transformations: XML Element to be parsed to a Transformations
+
+ Returns:
+ Transformations: The Transformations created from the raw_transformations
+ """
+
+ return Transformations(
+ hflip=bool(int(raw_transformations.attrib["hflip"])),
+ vflip=bool(int(raw_transformations.attrib["vflip"])),
+ rotate=bool(int(raw_transformations.attrib["rotate"])),
+ prefer_untransformed=bool(
+ int(raw_transformations.attrib["preferuntransformed"])
+ ),
+ )
+
+
+def _parse_tile(raw_tile: etree.Element, external_path: Optional[Path] = None) -> Tile:
+ """Parse the raw_tile to a Tile object.
+
+ Args:
+ raw_tile: XML Element to be parsed to a Tile
+
+ Returns:
+ Tile: The Tile created from the raw_tile
+ """
+
+ tile = Tile(id=int(raw_tile.attrib["id"]))
+
+ if raw_tile.attrib.get("type") is not None:
+ tile.type = raw_tile.attrib["type"]
+
+ animation_element = raw_tile.find("./animation")
+ if animation_element is not None:
+ tile.animation = []
+ for raw_frame in animation_element.findall("./frame"):
+ tile.animation.append(_parse_frame(raw_frame))
+
+ object_element = raw_tile.find("./objectgroup")
+ if object_element is not None:
+ tile.objects = parse_layer(object_element)
+
+ properties_element = raw_tile.find("./properties")
+ if properties_element is not None:
+ tile.properties = parse_properties(properties_element)
+
+ image_element = raw_tile.find("./image")
+ if image_element is not None:
+ if external_path:
+ tile.image = (
+ Path(external_path / image_element.attrib["source"])
+ .absolute()
+ .resolve()
+ )
+ else:
+ tile.image = Path(image_element.attrib["source"])
+
+ tile.image_width = int(image_element.attrib["width"])
+ tile.image_height = int(image_element.attrib["height"])
+
+ return tile
+
+
+def parse(
+ raw_tileset: etree.Element,
+ firstgid: int,
+ external_path: Optional[Path] = None,
+) -> Tileset:
+ tileset = Tileset(
+ name=raw_tileset.attrib["name"],
+ tile_count=int(raw_tileset.attrib["tilecount"]),
+ tile_width=int(raw_tileset.attrib["tilewidth"]),
+ tile_height=int(raw_tileset.attrib["tileheight"]),
+ columns=int(raw_tileset.attrib["columns"]),
+ firstgid=firstgid,
+ )
+
+ if raw_tileset.attrib.get("version") is not None:
+ tileset.version = raw_tileset.attrib["version"]
+
+ if raw_tileset.attrib.get("tiledversion") is not None:
+ tileset.tiled_version = raw_tileset.attrib["tiledversion"]
+
+ if raw_tileset.attrib.get("backgroundcolor") is not None:
+ tileset.background_color = parse_color(raw_tileset.attrib["backgroundcolor"])
+
+ if raw_tileset.attrib.get("spacing") is not None:
+ tileset.spacing = int(raw_tileset.attrib["spacing"])
+
+ if raw_tileset.attrib.get("margin") is not None:
+ tileset.margin = int(raw_tileset.attrib["margin"])
+
+ image_element = raw_tileset.find("image")
+ if image_element is not None:
+ if external_path:
+ tileset.image = (
+ Path(external_path / image_element.attrib["source"])
+ .absolute()
+ .resolve()
+ )
+ else:
+ tileset.image = Path(image_element.attrib["source"])
+
+ tileset.image_width = int(image_element.attrib["width"])
+ tileset.image_height = int(image_element.attrib["height"])
+
+ if image_element.attrib.get("trans") is not None:
+ my_string = image_element.attrib["trans"]
+ if my_string[0] != "#":
+ my_string = f"#{my_string}"
+ tileset.transparent_color = parse_color(my_string)
+
+ tileoffset_element = raw_tileset.find("./tileoffset")
+ if tileoffset_element is not None:
+ tileset.tile_offset = OrderedPair(
+ int(tileoffset_element.attrib["x"]), int(tileoffset_element.attrib["y"])
+ )
+
+ grid_element = raw_tileset.find("./grid")
+ if grid_element is not None:
+ tileset.grid = _parse_grid(grid_element)
+
+ properties_element = raw_tileset.find("./properties")
+ if properties_element is not None:
+ tileset.properties = parse_properties(properties_element)
+
+ tiles = {}
+ for tile_element in raw_tileset.findall("./tile"):
+ tiles[int(tile_element.attrib["id"])] = _parse_tile(
+ tile_element, external_path=external_path
+ )
+ if tiles:
+ tileset.tiles = tiles
+
+ wangsets_element = raw_tileset.find("./wangsets")
+ if wangsets_element is not None:
+ wangsets = []
+ for raw_wangset in wangsets_element.findall("./wangset"):
+ wangsets.append(parse_wangset(raw_wangset))
+ tileset.wang_sets = wangsets
+
+ transformations_element = raw_tileset.find("./transformations")
+ if transformations_element is not None:
+ tileset.transformations = _parse_transformations(transformations_element)
+
+ return tileset
diff --git a/pytiled_parser/parsers/tmx/wang_set.py b/pytiled_parser/parsers/tmx/wang_set.py
new file mode 100644
index 0000000..b167226
--- /dev/null
+++ b/pytiled_parser/parsers/tmx/wang_set.py
@@ -0,0 +1,74 @@
+import xml.etree.ElementTree as etree
+
+from pytiled_parser.parsers.tmx.properties import parse as parse_properties
+from pytiled_parser.util import parse_color
+from pytiled_parser.wang_set import WangColor, WangSet, WangTile
+
+
+def _parse_wang_tile(raw_wang_tile: etree.Element) -> WangTile:
+ """Parse the raw wang tile into a pytiled_parser type
+
+ Args:
+ raw_wang_tile: XML Element to be parsed.
+
+ Returns:
+ WangTile: A properly typed WangTile.
+ """
+ ids = [int(v.strip()) for v in raw_wang_tile.attrib["wangid"].split(",")]
+ return WangTile(tile_id=int(raw_wang_tile.attrib["tileid"]), wang_id=ids)
+
+
+def _parse_wang_color(raw_wang_color: etree.Element) -> WangColor:
+ """Parse the raw wang color into a pytiled_parser type
+
+ Args:
+ raw_wang_color: XML Element to be parsed.
+
+ Returns:
+ WangColor: A properly typed WangColor.
+ """
+ wang_color = WangColor(
+ name=raw_wang_color.attrib["name"],
+ color=parse_color(raw_wang_color.attrib["color"]),
+ tile=int(raw_wang_color.attrib["tile"]),
+ probability=float(raw_wang_color.attrib["probability"]),
+ )
+
+ properties = raw_wang_color.find("./properties")
+ if properties:
+ wang_color.properties = parse_properties(properties)
+
+ return wang_color
+
+
+def parse(raw_wangset: etree.Element) -> WangSet:
+ """Parse the raw wangset into a pytiled_parser type
+
+ Args:
+ raw_wangset: XML Element to be parsed.
+
+ Returns:
+ WangSet: A properly typed WangSet.
+ """
+
+ colors = []
+ for raw_wang_color in raw_wangset.findall("./wangcolor"):
+ colors.append(_parse_wang_color(raw_wang_color))
+
+ tiles = {}
+ for raw_wang_tile in raw_wangset.findall("./wangtile"):
+ tiles[int(raw_wang_tile.attrib["tileid"])] = _parse_wang_tile(raw_wang_tile)
+
+ wangset = WangSet(
+ name=raw_wangset.attrib["name"],
+ tile=int(raw_wangset.attrib["tile"]),
+ wang_type=raw_wangset.attrib["type"],
+ wang_colors=colors,
+ wang_tiles=tiles,
+ )
+
+ properties = raw_wangset.find("./properties")
+ if properties:
+ wangset.properties = parse_properties(properties)
+
+ return wangset
diff --git a/pytiled_parser/properties.py b/pytiled_parser/properties.py
index e9b87b5..f8bc0ac 100644
--- a/pytiled_parser/properties.py
+++ b/pytiled_parser/properties.py
@@ -1,55 +1,18 @@
"""Properties Module
-This module casts raw properties from Tiled maps into a dictionary of
-properly typed Properties.
+This module defines types for Property objects.
+For more about properties in Tiled maps see the below link:
+https://doc.mapeditor.org/en/stable/manual/custom-properties/
+
+The types defined in this module get added to other objects
+such as Layers, Maps, Objects, etc
"""
from pathlib import Path
-from typing import Dict, List, Union
-from typing import cast as type_cast
-
-from typing_extensions import TypedDict
+from typing import Dict, Union
from .common_types import Color
-from .util import parse_color
Property = Union[float, Path, str, bool, Color]
-
Properties = Dict[str, Property]
-
-
-RawValue = Union[float, str, bool]
-
-
-class RawProperty(TypedDict):
- """A dictionary of raw properties."""
-
- name: str
- type: str
- value: RawValue
-
-
-def cast(raw_properties: List[RawProperty]) -> Properties:
- """Cast a list of `RawProperty`s into `Properties`
-
- Args:
- raw_properties: The list of `RawProperty`s to cast.
-
- Returns:
- Properties: The casted `Properties`.
- """
-
- final: Properties = {}
- value: Property
-
- for property_ in raw_properties:
- if property_["type"] == "file":
- value = Path(type_cast(str, property_["value"]))
- elif property_["type"] == "color":
- value = parse_color(type_cast(str, property_["value"]))
- else:
- value = property_["value"]
- final[property_["name"]] = value
-
- return final
diff --git a/pytiled_parser/tiled_map.py b/pytiled_parser/tiled_map.py
index 1f56885..9c1c1ba 100644
--- a/pytiled_parser/tiled_map.py
+++ b/pytiled_parser/tiled_map.py
@@ -1,19 +1,12 @@
-# pylint: disable=too-few-public-methods
-
-import json
from pathlib import Path
-from typing import Dict, List, Optional, Union
-from typing import cast as typing_cast
+from typing import Dict, List, Optional
import attr
-from typing_extensions import TypedDict
-from . import layer, properties, tileset
-from .common_types import Color, Size
-from .layer import Layer, RawLayer
-from .properties import Properties, RawProperty
-from .tileset import RawTileSet, Tileset
-from .util import parse_color
+from pytiled_parser.common_types import Color, Size
+from pytiled_parser.layer import Layer
+from pytiled_parser.properties import Properties
+from pytiled_parser.tileset import Tileset
TilesetDict = Dict[int, Tileset]
@@ -68,146 +61,3 @@ class TiledMap:
hex_side_length: Optional[int] = None
stagger_axis: Optional[str] = None
stagger_index: Optional[str] = None
-
-
-class _RawTilesetMapping(TypedDict):
- """ The way that tilesets are stored in the Tiled JSON formatted map."""
-
- firstgid: int
- source: str
-
-
-class _RawTiledMap(TypedDict):
- """The keys and their types that appear in a Tiled JSON Map.
-
- Keys:
- compressionlevel: not documented - https://github.com/bjorn/tiled/issues/2815
- """
-
- backgroundcolor: str
- compressionlevel: int
- height: int
- hexsidelength: int
- infinite: bool
- layers: List[RawLayer]
- nextlayerid: int
- nextobjectid: int
- orientation: str
- properties: List[RawProperty]
- renderorder: str
- staggeraxis: str
- staggerindex: str
- tiledversion: str
- tileheight: int
- tilesets: List[_RawTilesetMapping]
- tilewidth: int
- type: str
- version: Union[str, float]
- width: int
-
-
-def parse_map(file: Path) -> TiledMap:
- """Parse the raw Tiled map into a pytiled_parser type
-
- Args:
- file: Path to the map's JSON file
-
- Returns:
- TileSet: a properly typed TileSet.
- """
-
- with open(file) as map_file:
- raw_tiled_map = json.load(map_file)
-
- parent_dir = file.parent
-
- raw_tilesets: List[Union[RawTileSet, _RawTilesetMapping]] = raw_tiled_map[
- "tilesets"
- ]
- tilesets: TilesetDict = {}
-
- for raw_tileset in raw_tilesets:
- if raw_tileset.get("source") is not None:
- # Is an external Tileset
- tileset_path = Path(parent_dir / raw_tileset["source"])
- with open(tileset_path) as raw_tileset_file:
- tilesets[raw_tileset["firstgid"]] = tileset.cast(
- json.load(raw_tileset_file),
- raw_tileset["firstgid"],
- external_path=tileset_path.parent,
- )
- else:
- # Is an embedded Tileset
- raw_tileset = typing_cast(RawTileSet, raw_tileset)
- tilesets[raw_tileset["firstgid"]] = tileset.cast(
- raw_tileset, raw_tileset["firstgid"]
- )
-
- if isinstance(raw_tiled_map["version"], float):
- version = str(raw_tiled_map["version"])
- else:
- version = raw_tiled_map["version"]
-
- # `map` is a built-in function
- map_ = TiledMap(
- map_file=file,
- infinite=raw_tiled_map["infinite"],
- layers=[layer.cast(layer_, parent_dir) for layer_ in raw_tiled_map["layers"]],
- map_size=Size(raw_tiled_map["width"], raw_tiled_map["height"]),
- next_layer_id=raw_tiled_map["nextlayerid"],
- next_object_id=raw_tiled_map["nextobjectid"],
- orientation=raw_tiled_map["orientation"],
- render_order=raw_tiled_map["renderorder"],
- tiled_version=raw_tiled_map["tiledversion"],
- tile_size=Size(raw_tiled_map["tilewidth"], raw_tiled_map["tileheight"]),
- tilesets=tilesets,
- version=version,
- )
-
- layers = [layer for layer in map_.layers if hasattr(layer, "tiled_objects")]
-
- for my_layer in layers:
- for tiled_object in my_layer.tiled_objects: # type: ignore
- if hasattr(tiled_object, "new_tileset"):
- if tiled_object.new_tileset:
- already_loaded = None
- for val in map_.tilesets.values():
- if val.name == tiled_object.new_tileset["name"]:
- already_loaded = val
- break
-
- if not already_loaded:
- highest_firstgid = max(map_.tilesets.keys())
- last_tileset_count = map_.tilesets[highest_firstgid].tile_count
- new_firstgid = highest_firstgid + last_tileset_count
- map_.tilesets[new_firstgid] = tileset.cast(
- tiled_object.new_tileset,
- new_firstgid,
- tiled_object.new_tileset_path,
- )
- tiled_object.gid = tiled_object.gid + (new_firstgid - 1)
-
- else:
- tiled_object.gid = tiled_object.gid + (
- already_loaded.firstgid - 1
- )
-
- tiled_object.new_tileset = None
- tiled_object.new_tileset_path = None
-
- if raw_tiled_map.get("backgroundcolor") is not None:
- map_.background_color = parse_color(raw_tiled_map["backgroundcolor"])
-
- if raw_tiled_map.get("hexsidelength") is not None:
- map_.hex_side_length = raw_tiled_map["hexsidelength"]
-
- if raw_tiled_map.get("properties") is not None:
- map_.properties = properties.cast(raw_tiled_map["properties"])
-
- if raw_tiled_map.get("staggeraxis") is not None:
- map_.stagger_axis = raw_tiled_map["staggeraxis"]
-
- if raw_tiled_map.get("staggerindex") is not None:
- map_.stagger_index = raw_tiled_map["staggerindex"]
-
- return map_
diff --git a/pytiled_parser/tiled_object.py b/pytiled_parser/tiled_object.py
index 3d167d0..a96d65a 100644
--- a/pytiled_parser/tiled_object.py
+++ b/pytiled_parser/tiled_object.py
@@ -1,14 +1,12 @@
# pylint: disable=too-few-public-methods
-import json
+import xml.etree.ElementTree as etree
from pathlib import Path
-from typing import Any, Callable, Dict, List, Optional, Union
+from typing import Any, Dict, List, Optional, Union
import attr
-from typing_extensions import TypedDict
from . import properties as properties_
from .common_types import Color, OrderedPair, Size
-from .util import parse_color
@attr.s(auto_attribs=True, kw_only=True)
@@ -37,10 +35,9 @@ class TiledObject:
coordinates: OrderedPair
size: Size = Size(0, 0)
rotation: float = 0
- visible: bool
-
- name: Optional[str] = None
- type: Optional[str] = None
+ visible: bool = True
+ name: str = ""
+ type: str = ""
properties: properties_.Properties = {}
@@ -148,302 +145,5 @@ class Tile(TiledObject):
"""
gid: int
- new_tileset: Optional[Dict[str, Any]] = None
+ new_tileset: Optional[Union[etree.Element, Dict[str, Any]]] = None
new_tileset_path: Optional[Path] = None
-
-
-class RawTextDict(TypedDict):
- """ The keys and their types that appear in a Text JSON Object."""
-
- text: str
- color: str
-
- fontfamily: str
- pixelsize: float # this is `font_size` in Text
-
- bold: bool
- italic: bool
- strikeout: bool
- underline: bool
- kerning: bool
-
- halign: str
- valign: str
- wrap: bool
-
-
-class RawTiledObject(TypedDict):
- """ The keys and their types that appear in a Tiled JSON Object."""
-
- id: int
- gid: int
- template: str
- x: float
- y: float
- width: float
- height: float
- rotation: float
- visible: bool
- name: str
- type: str
- properties: List[properties_.RawProperty]
- ellipse: bool
- point: bool
- polygon: List[Dict[str, float]]
- polyline: List[Dict[str, float]]
- text: Dict[str, Union[float, str]]
-
-
-RawTiledObjects = List[RawTiledObject]
-
-
-def _get_common_attributes(raw_tiled_object: RawTiledObject) -> TiledObject:
- """Create a TiledObject containing all the attributes common to all tiled objects
-
- Args:
- raw_tiled_object: Raw Tiled object get common attributes from
-
- Returns:
- TiledObject: The attributes in common of all Tiled Objects
- """
-
- common_attributes = TiledObject(
- id=raw_tiled_object["id"],
- coordinates=OrderedPair(raw_tiled_object["x"], raw_tiled_object["y"]),
- visible=raw_tiled_object["visible"],
- size=Size(raw_tiled_object["width"], raw_tiled_object["height"]),
- rotation=raw_tiled_object["rotation"],
- name=raw_tiled_object["name"],
- type=raw_tiled_object["type"],
- )
-
- if raw_tiled_object.get("properties") is not None:
- common_attributes.properties = properties_.cast(raw_tiled_object["properties"])
-
- return common_attributes
-
-
-def _cast_ellipse(raw_tiled_object: RawTiledObject) -> Ellipse:
- """Cast the raw_tiled_object to an Ellipse object.
-
- Args:
- raw_tiled_object: Raw Tiled object to be casted to an Ellipse
-
- Returns:
- Ellipse: The Ellipse object created from the raw_tiled_object
- """
- return Ellipse(**_get_common_attributes(raw_tiled_object).__dict__)
-
-
-def _cast_rectangle(raw_tiled_object: RawTiledObject) -> Rectangle:
- """Cast the raw_tiled_object to a Rectangle object.
-
- Args:
- raw_tiled_object: Raw Tiled object to be casted to a Rectangle
-
- Returns:
- Rectangle: The Rectangle object created from the raw_tiled_object
- """
- return Rectangle(**_get_common_attributes(raw_tiled_object).__dict__)
-
-
-def _cast_point(raw_tiled_object: RawTiledObject) -> Point:
- """Cast the raw_tiled_object to a Point object.
-
- Args:
- raw_tiled_object: Raw Tiled object to be casted to a Point
-
- Returns:
- Point: The Point object created from the raw_tiled_object
- """
- return Point(**_get_common_attributes(raw_tiled_object).__dict__)
-
-
-def _cast_tile(
- raw_tiled_object: RawTiledObject,
- new_tileset: Optional[Dict[str, Any]] = None,
- new_tileset_path: Optional[Path] = None,
-) -> Tile:
- """Cast the raw_tiled_object to a Tile object.
-
- Args:
- raw_tiled_object: Raw Tiled object to be casted to a Tile
-
- Returns:
- Tile: The Tile object created from the raw_tiled_object
- """
- gid = raw_tiled_object["gid"]
-
- return Tile(
- gid=gid,
- new_tileset=new_tileset,
- new_tileset_path=new_tileset_path,
- **_get_common_attributes(raw_tiled_object).__dict__
- )
-
-
-def _cast_polygon(raw_tiled_object: RawTiledObject) -> Polygon:
- """Cast the raw_tiled_object to a Polygon object.
-
- Args:
- raw_tiled_object: Raw Tiled object to be casted to a Polygon
-
- Returns:
- Polygon: The Polygon object created from the raw_tiled_object
- """
- polygon = []
- for point in raw_tiled_object["polygon"]:
- polygon.append(OrderedPair(point["x"], point["y"]))
-
- return Polygon(points=polygon, **_get_common_attributes(raw_tiled_object).__dict__)
-
-
-def _cast_polyline(raw_tiled_object: RawTiledObject) -> Polyline:
- """Cast the raw_tiled_object to a Polyline object.
-
- Args:
- raw_tiled_object: Raw Tiled Object to be casted to a Polyline
-
- Returns:
- Polyline: The Polyline object created from the raw_tiled_object
- """
- polyline = []
- for point in raw_tiled_object["polyline"]:
- polyline.append(OrderedPair(point["x"], point["y"]))
-
- return Polyline(
- points=polyline, **_get_common_attributes(raw_tiled_object).__dict__
- )
-
-
-def _cast_text(raw_tiled_object: RawTiledObject) -> Text:
- """Cast the raw_tiled_object to a Text object.
-
- Args:
- raw_tiled_object: Raw Tiled object to be casted to a Text object
-
- Returns:
- Text: The Text object created from the raw_tiled_object
- """
- # required attributes
- raw_text_dict: RawTextDict = raw_tiled_object["text"]
- text = raw_text_dict["text"]
-
- # create base Text object
- text_object = Text(text=text, **_get_common_attributes(raw_tiled_object).__dict__)
-
- # optional attributes
- if raw_text_dict.get("color") is not None:
- text_object.color = parse_color(raw_text_dict["color"])
-
- if raw_text_dict.get("fontfamily") is not None:
- text_object.font_family = raw_text_dict["fontfamily"]
-
- if raw_text_dict.get("pixelsize") is not None:
- text_object.font_size = raw_text_dict["pixelsize"]
-
- if raw_text_dict.get("bold") is not None:
- text_object.bold = raw_text_dict["bold"]
-
- if raw_text_dict.get("italic") is not None:
- text_object.italic = raw_text_dict["italic"]
-
- if raw_text_dict.get("kerning") is not None:
- text_object.kerning = raw_text_dict["kerning"]
-
- if raw_text_dict.get("strikeout") is not None:
- text_object.strike_out = raw_text_dict["strikeout"]
-
- if raw_text_dict.get("underline") is not None:
- text_object.underline = raw_text_dict["underline"]
-
- if raw_text_dict.get("halign") is not None:
- text_object.horizontal_align = raw_text_dict["halign"]
-
- if raw_text_dict.get("valign") is not None:
- text_object.vertical_align = raw_text_dict["valign"]
-
- if raw_text_dict.get("wrap") is not None:
- text_object.wrap = raw_text_dict["wrap"]
-
- return text_object
-
-
-def _get_caster(
- raw_tiled_object: RawTiledObject,
-) -> Callable[[RawTiledObject], TiledObject]:
- """Get the caster function for the raw tiled object.
-
- Args:
- raw_tiled_object: Raw Tiled object that is analysed to determine which caster
- to return.
-
- Returns:
- Callable[[RawTiledObject], TiledObject]: The caster function.
- """
- if raw_tiled_object.get("ellipse"):
- return _cast_ellipse
-
- if raw_tiled_object.get("point"):
- return _cast_point
-
- if raw_tiled_object.get("gid"):
- # Only Tile objects have the `gid` key (I think)
- return _cast_tile
-
- if raw_tiled_object.get("polygon"):
- return _cast_polygon
-
- if raw_tiled_object.get("polyline"):
- return _cast_polyline
-
- if raw_tiled_object.get("text"):
- return _cast_text
-
- return _cast_rectangle
-
-
-def cast(
- raw_tiled_object: RawTiledObject,
- parent_dir: Optional[Path] = None,
-) -> TiledObject:
- """Cast the raw tiled object into a pytiled_parser type
-
- Args:
- raw_tiled_object: Raw Tiled object that is to be cast.
- parent_dir: The parent directory that the map file is in.
-
- Returns:
- TiledObject: a properly typed Tiled object.
-
- Raises:
- RuntimeError: When a required parameter was not sent based on a condition.
- """
- new_tileset = None
- new_tileset_path = None
-
- if raw_tiled_object.get("template"):
- if not parent_dir:
- raise RuntimeError(
- "A parent directory must be specified when using object templates"
- )
- template_path = Path(parent_dir / raw_tiled_object["template"])
- with open(template_path) as raw_template_file:
- template = json.load(raw_template_file)
- if "tileset" in template:
- tileset_path = Path(
- template_path.parent / template["tileset"]["source"]
- )
- with open(tileset_path) as raw_tileset_file:
- new_tileset = json.load(raw_tileset_file)
- new_tileset_path = tileset_path.parent
-
- loaded_template = template["object"]
- for key in loaded_template:
- if key != "id":
- raw_tiled_object[key] = loaded_template[key] # type: ignore
-
- if raw_tiled_object.get("gid"):
- return _cast_tile(raw_tiled_object, new_tileset, new_tileset_path)
-
- return _get_caster(raw_tiled_object)(raw_tiled_object)
diff --git a/pytiled_parser/tileset.py b/pytiled_parser/tileset.py
index 25bd190..de48013 100644
--- a/pytiled_parser/tileset.py
+++ b/pytiled_parser/tileset.py
@@ -1,16 +1,13 @@
# pylint: disable=too-few-public-methods
from pathlib import Path
-from typing import Dict, List, NamedTuple, Optional, Union
+from typing import Dict, List, NamedTuple, Optional
import attr
-from typing_extensions import TypedDict
from . import layer
from . import properties as properties_
from .common_types import Color, OrderedPair
-from .util import parse_color
-from .wang_set import RawWangSet, WangSet
-from .wang_set import cast as cast_wangset
+from .wang_set import WangSet
class Grid(NamedTuple):
@@ -153,261 +150,3 @@ class Tileset:
properties: Optional[properties_.Properties] = None
tiles: Optional[Dict[int, Tile]] = None
wang_sets: Optional[List[WangSet]] = None
-
-
-class RawFrame(TypedDict):
- """ The keys and their types that appear in a Frame JSON Object."""
-
- duration: int
- tileid: int
-
-
-class RawTileOffset(TypedDict):
- """ The keys and their types that appear in a TileOffset JSON Object."""
-
- x: int
- y: int
-
-
-class RawTransformations(TypedDict):
- """ The keys and their types that appear in a Transformations JSON Object."""
-
- hflip: bool
- vflip: bool
- rotate: bool
- preferuntransformed: bool
-
-
-class RawTile(TypedDict):
- """ The keys and their types that appear in a Tile JSON Object."""
-
- animation: List[RawFrame]
- id: int
- image: str
- imageheight: int
- imagewidth: int
- opacity: float
- properties: List[properties_.RawProperty]
- objectgroup: layer.RawLayer
- type: str
-
-
-class RawGrid(TypedDict):
- """ The keys and their types that appear in a Grid JSON Object."""
-
- height: int
- width: int
- orientation: str
-
-
-class RawTileSet(TypedDict):
- """ The keys and their types that appear in a TileSet JSON Object."""
-
- backgroundcolor: str
- columns: int
- firstgid: int
- grid: RawGrid
- image: str
- imageheight: int
- imagewidth: int
- margin: int
- name: str
- properties: List[properties_.RawProperty]
- source: str
- spacing: int
- tilecount: int
- tiledversion: str
- tileheight: int
- tileoffset: RawTileOffset
- tiles: List[RawTile]
- tilewidth: int
- transparentcolor: str
- transformations: RawTransformations
- version: Union[str, float]
- wangsets: List[RawWangSet]
-
-
-def _cast_frame(raw_frame: RawFrame) -> Frame:
- """Cast the raw_frame to a Frame.
-
- Args:
- raw_frame: RawFrame to be casted to a Frame
-
- Returns:
- Frame: The Frame created from the raw_frame
- """
-
- return Frame(duration=raw_frame["duration"], tile_id=raw_frame["tileid"])
-
-
-def _cast_tile_offset(raw_tile_offset: RawTileOffset) -> OrderedPair:
- """Cast the raw_tile_offset to an OrderedPair.
-
- Args:
- raw_tile_offset: RawTileOffset to be casted to an OrderedPair
-
- Returns:
- OrderedPair: The OrderedPair created from the raw_tile_offset
- """
-
- return OrderedPair(raw_tile_offset["x"], raw_tile_offset["y"])
-
-
-def _cast_tile(raw_tile: RawTile, external_path: Optional[Path] = None) -> Tile:
- """Cast the raw_tile to a Tile object.
-
- Args:
- raw_tile: RawTile to be casted to a Tile
-
- Returns:
- Tile: The Tile created from the raw_tile
- """
-
- id_ = raw_tile["id"]
- tile = Tile(id=id_)
-
- if raw_tile.get("animation") is not None:
- tile.animation = []
- for frame in raw_tile["animation"]:
- tile.animation.append(_cast_frame(frame))
-
- if raw_tile.get("objectgroup") is not None:
- tile.objects = layer.cast(raw_tile["objectgroup"])
-
- if raw_tile.get("properties") is not None:
- tile.properties = properties_.cast(raw_tile["properties"])
-
- if raw_tile.get("image") is not None:
- if external_path:
- tile.image = Path(external_path / raw_tile["image"]).absolute().resolve()
- else:
- tile.image = Path(raw_tile["image"])
-
- if raw_tile.get("imagewidth") is not None:
- tile.image_width = raw_tile["imagewidth"]
-
- if raw_tile.get("imageheight") is not None:
- tile.image_height = raw_tile["imageheight"]
-
- if raw_tile.get("type") is not None:
- tile.type = raw_tile["type"]
-
- return tile
-
-
-def _cast_transformations(raw_transformations: RawTransformations) -> Transformations:
- """Cast the raw_transformations to a Transformations object.
-
- Args:
- raw_transformations: RawTransformations to be casted to a Transformations
-
- Returns:
- Transformations: The Transformations created from the raw_transformations
- """
-
- return Transformations(
- hflip=raw_transformations["hflip"],
- vflip=raw_transformations["vflip"],
- rotate=raw_transformations["rotate"],
- prefer_untransformed=raw_transformations["preferuntransformed"],
- )
-
-
-def _cast_grid(raw_grid: RawGrid) -> Grid:
- """Cast the raw_grid to a Grid object.
-
- Args:
- raw_grid: RawGrid to be casted to a Grid
-
- Returns:
- Grid: The Grid created from the raw_grid
- """
-
- return Grid(
- orientation=raw_grid["orientation"],
- width=raw_grid["width"],
- height=raw_grid["height"],
- )
-
-
-def cast(
- raw_tileset: RawTileSet,
- firstgid: int,
- external_path: Optional[Path] = None,
-) -> Tileset:
- """Cast the raw tileset into a pytiled_parser type
-
- Args:
- raw_tileset: Raw Tileset to be cast.
- firstgid: GID corresponding the first tile in the set.
- external_path: The path to the tileset if it is not an embedded one.
-
- Returns:
- TileSet: a properly typed TileSet.
- """
-
- tileset = Tileset(
- name=raw_tileset["name"],
- tile_count=raw_tileset["tilecount"],
- tile_width=raw_tileset["tilewidth"],
- tile_height=raw_tileset["tileheight"],
- columns=raw_tileset["columns"],
- spacing=raw_tileset["spacing"],
- margin=raw_tileset["margin"],
- firstgid=firstgid,
- )
-
- if raw_tileset.get("version") is not None:
- if isinstance(raw_tileset["version"], float):
- tileset.version = str(raw_tileset["version"])
- else:
- tileset.version = raw_tileset["version"]
-
- if raw_tileset.get("tiledversion") is not None:
- tileset.tiled_version = raw_tileset["tiledversion"]
-
- if raw_tileset.get("image") is not None:
- if external_path:
- tileset.image = (
- Path(external_path / raw_tileset["image"]).absolute().resolve()
- )
- else:
- tileset.image = Path(raw_tileset["image"])
-
- if raw_tileset.get("imagewidth") is not None:
- tileset.image_width = raw_tileset["imagewidth"]
-
- if raw_tileset.get("imageheight") is not None:
- tileset.image_height = raw_tileset["imageheight"]
-
- if raw_tileset.get("backgroundcolor") is not None:
- tileset.background_color = parse_color(raw_tileset["backgroundcolor"])
-
- if raw_tileset.get("tileoffset") is not None:
- tileset.tile_offset = _cast_tile_offset(raw_tileset["tileoffset"])
-
- if raw_tileset.get("transparentcolor") is not None:
- tileset.transparent_color = parse_color(raw_tileset["transparentcolor"])
-
- if raw_tileset.get("grid") is not None:
- tileset.grid = _cast_grid(raw_tileset["grid"])
-
- if raw_tileset.get("properties") is not None:
- tileset.properties = properties_.cast(raw_tileset["properties"])
-
- if raw_tileset.get("tiles") is not None:
- tiles = {}
- for raw_tile in raw_tileset["tiles"]:
- tiles[raw_tile["id"]] = _cast_tile(raw_tile, external_path=external_path)
- tileset.tiles = tiles
-
- if raw_tileset.get("wangsets") is not None:
- wangsets = []
- for raw_wangset in raw_tileset["wangsets"]:
- wangsets.append(cast_wangset(raw_wangset))
- tileset.wang_sets = wangsets
-
- if raw_tileset.get("transformations") is not None:
- tileset.transformations = _cast_transformations(raw_tileset["transformations"])
-
- return tileset
diff --git a/pytiled_parser/util.py b/pytiled_parser/util.py
index 75c7d1d..f8bb18b 100644
--- a/pytiled_parser/util.py
+++ b/pytiled_parser/util.py
@@ -1,4 +1,8 @@
"""Utility Functions for PyTiled"""
+import json
+import xml.etree.ElementTree as etree
+from pathlib import Path
+from typing import Any
from pytiled_parser.common_types import Color
@@ -27,3 +31,52 @@ def parse_color(color: str) -> Color:
)
raise ValueError("Improperly formatted color passed to parse_color")
+
+
+def check_format(file_path: Path) -> str:
+ with open(file_path) as file:
+ line = file.readline().rstrip().strip()
+ if line[0] == "<":
+ return "tmx"
+ else:
+ return "json"
+
+
+def load_object_template(file_path: Path) -> Any:
+ template_format = check_format(file_path)
+
+ new_tileset = None
+ new_tileset_path = None
+
+ if template_format == "tmx":
+ with open(file_path) as template_file:
+ template = etree.parse(template_file).getroot()
+
+ tileset_element = template.find("./tileset")
+ if tileset_element is not None:
+ tileset_path = Path(file_path.parent / tileset_element.attrib["source"])
+ new_tileset = load_object_tileset(tileset_path)
+ new_tileset_path = tileset_path.parent
+ elif template_format == "json":
+ with open(file_path) as template_file:
+ template = json.load(template_file)
+ if "tileset" in template:
+ tileset_path = Path(file_path.parent / template["tileset"]["source"]) # type: ignore
+ new_tileset = load_object_tileset(tileset_path)
+ new_tileset_path = tileset_path.parent
+
+ return (template, new_tileset, new_tileset_path)
+
+
+def load_object_tileset(file_path: Path) -> Any:
+ tileset_format = check_format(file_path)
+
+ new_tileset = None
+
+ with open(file_path) as tileset_file:
+ if tileset_format == "tmx":
+ new_tileset = etree.parse(tileset_file).getroot()
+ elif tileset_format == "json":
+ new_tileset = json.load(tileset_file)
+
+ return new_tileset
diff --git a/pytiled_parser/version.py b/pytiled_parser/version.py
index b7bdf02..b06d5b0 100644
--- a/pytiled_parser/version.py
+++ b/pytiled_parser/version.py
@@ -1,3 +1,3 @@
"""pytiled_parser version"""
-__version__ = "1.5.4"
+__version__ = "2.0.0-beta"
diff --git a/pytiled_parser/wang_set.py b/pytiled_parser/wang_set.py
index 011410f..9241742 100644
--- a/pytiled_parser/wang_set.py
+++ b/pytiled_parser/wang_set.py
@@ -1,11 +1,9 @@
from typing import Dict, List, Optional
import attr
-from typing_extensions import TypedDict
-from . import properties as properties_
-from .common_types import Color
-from .util import parse_color
+from pytiled_parser.common_types import Color
+from pytiled_parser.properties import Properties
@attr.s(auto_attribs=True)
@@ -22,7 +20,7 @@ class WangColor:
name: str
probability: float
tile: int
- properties: Optional[properties_.Properties] = None
+ properties: Optional[Properties] = None
@attr.s(auto_attribs=True)
@@ -33,100 +31,4 @@ class WangSet:
wang_type: str
wang_tiles: Dict[int, WangTile]
wang_colors: List[WangColor]
- properties: Optional[properties_.Properties] = None
-
-
-class RawWangTile(TypedDict):
- """ The keys and their types that appear in a Wang Tile JSON Object."""
-
- tileid: int
- # Tiled stores these IDs as a list represented like so:
- # [top, top_right, right, bottom_right, bottom, bottom_left, left, top_left]
- wangid: List[int]
-
-
-class RawWangColor(TypedDict):
- """ The keys and their types that appear in a Wang Color JSON Object."""
-
- color: str
- name: str
- probability: float
- tile: int
- properties: List[properties_.RawProperty]
-
-
-class RawWangSet(TypedDict):
- """ The keys and their types that appear in a Wang Set JSON Object."""
-
- colors: List[RawWangColor]
- name: str
- properties: List[properties_.RawProperty]
- tile: int
- type: str
- wangtiles: List[RawWangTile]
-
-
-def _cast_wang_tile(raw_wang_tile: RawWangTile) -> WangTile:
- """Cast the raw wang tile into a pytiled_parser type
-
- Args:
- raw_wang_tile: RawWangTile to be cast.
-
- Returns:
- WangTile: A properly typed WangTile.
- """
- return WangTile(tile_id=raw_wang_tile["tileid"], wang_id=raw_wang_tile["wangid"])
-
-
-def _cast_wang_color(raw_wang_color: RawWangColor) -> WangColor:
- """Cast the raw wang color into a pytiled_parser type
-
- Args:
- raw_wang_color: RawWangColor to be cast.
-
- Returns:
- WangColor: A properly typed WangColor.
- """
- wang_color = WangColor(
- name=raw_wang_color["name"],
- color=parse_color(raw_wang_color["color"]),
- tile=raw_wang_color["tile"],
- probability=raw_wang_color["probability"],
- )
-
- if raw_wang_color.get("properties") is not None:
- wang_color.properties = properties_.cast(raw_wang_color["properties"])
-
- return wang_color
-
-
-def cast(raw_wangset: RawWangSet) -> WangSet:
- """Cast the raw wangset into a pytiled_parser type
-
- Args:
- raw_wangset: Raw Wangset to be cast.
-
- Returns:
- WangSet: A properly typed WangSet.
- """
-
- colors = []
- for raw_wang_color in raw_wangset["colors"]:
- colors.append(_cast_wang_color(raw_wang_color))
-
- tiles = {}
- for raw_wang_tile in raw_wangset["wangtiles"]:
- tiles[raw_wang_tile["tileid"]] = _cast_wang_tile(raw_wang_tile)
-
- wangset = WangSet(
- name=raw_wangset["name"],
- tile=raw_wangset["tile"],
- wang_type=raw_wangset["type"],
- wang_colors=colors,
- wang_tiles=tiles,
- )
-
- if raw_wangset.get("properties") is not None:
- wangset.properties = properties_.cast(raw_wangset["properties"])
-
- return wangset
+ properties: Optional[Properties] = None
diff --git a/pytiled_parser/world.py b/pytiled_parser/world.py
index 5d35322..797ff39 100644
--- a/pytiled_parser/world.py
+++ b/pytiled_parser/world.py
@@ -8,8 +8,9 @@ from typing import List
import attr
from typing_extensions import TypedDict
-from .common_types import OrderedPair, Size
-from .tiled_map import TiledMap, parse_map
+from pytiled_parser.common_types import OrderedPair, Size
+from pytiled_parser.parser import parse_map
+from pytiled_parser.tiled_map import TiledMap
@attr.s(auto_attribs=True)
@@ -55,7 +56,7 @@ class RawWorld(TypedDict):
onlyShowAdjacentMaps: bool
-def _cast_world_map(raw_world_map: RawWorldMap, map_file: Path) -> WorldMap:
+def _parse_world_map(raw_world_map: RawWorldMap, map_file: Path) -> WorldMap:
"""Parse the RawWorldMap into a WorldMap.
Args:
@@ -94,7 +95,7 @@ def parse_world(file: Path) -> World:
if raw_world.get("maps"):
for raw_map in raw_world["maps"]:
map_path = Path(parent_dir / raw_map["fileName"])
- maps.append(_cast_world_map(raw_map, map_path))
+ maps.append(_parse_world_map(raw_map, map_path))
if raw_world.get("patterns"):
for raw_pattern in raw_world["patterns"]:
@@ -131,7 +132,7 @@ def parse_world(file: Path) -> World:
}
map_path = Path(parent_dir / map_file)
- maps.append(_cast_world_map(raw_world_map, map_path))
+ maps.append(_parse_world_map(raw_world_map, map_path))
world = World(maps=maps)
diff --git a/setup.cfg b/setup.cfg
index 86ce232..2ccb360 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -44,7 +44,7 @@ tests =
pytest
pytest-cov
black
- pylint
+ flake8
mypy
isort<5,>=4.2.5
@@ -104,3 +104,7 @@ strict_optional = True
[mypy-tests.*]
ignore_errors = True
+
+[flake8]
+max-line-length = 88
+exclude = .git,__pycache__,__init__.py,.mypy_cache,.pytest_cache
diff --git a/tests/test_cross_template/map.json b/tests/test_cross_template/map.json
new file mode 100644
index 0000000..451323f
--- /dev/null
+++ b/tests/test_cross_template/map.json
@@ -0,0 +1,73 @@
+{ "backgroundcolor":"#ff0004",
+ "compressionlevel":0,
+ "height":6,
+ "infinite":false,
+ "layers":[
+ {
+ "draworder":"topdown",
+ "id":2,
+ "name":"Object Layer 1",
+ "objects":[
+ {
+ "id":2,
+ "template":"template-rectangle.tx",
+ "x":98.4987608686521,
+ "y":46.2385012811358
+ }],
+ "opacity":1,
+ "type":"objectgroup",
+ "visible":true,
+ "x":0,
+ "y":0
+ }],
+ "nextlayerid":3,
+ "nextobjectid":8,
+ "orientation":"orthogonal",
+ "properties":[
+ {
+ "name":"bool property - true",
+ "type":"bool",
+ "value":true
+ },
+ {
+ "name":"color property",
+ "type":"color",
+ "value":"#ff49fcff"
+ },
+ {
+ "name":"file property",
+ "type":"file",
+ "value":"..\/..\/..\/..\/..\/..\/var\/log\/syslog"
+ },
+ {
+ "name":"float property",
+ "type":"float",
+ "value":1.23456789
+ },
+ {
+ "name":"int property",
+ "type":"int",
+ "value":13
+ },
+ {
+ "name":"string property",
+ "type":"string",
+ "value":"Hello, World!!"
+ }],
+ "renderorder":"right-down",
+ "tiledversion":"1.7.1",
+ "tileheight":32,
+ "tilesets":[
+ {
+ "firstgid":1,
+ "source":"tileset.json"
+ },
+ {
+ "firstgid":49,
+ "source":"tile_set_image_for_template.json"
+ }],
+ "tilewidth":32,
+ "type":"map",
+ "version":"1.6",
+ "width":8
+}
\ No newline at end of file
diff --git a/tests/test_cross_template/map.tmx b/tests/test_cross_template/map.tmx
new file mode 100644
index 0000000..b77d63b
--- /dev/null
+++ b/tests/test_cross_template/map.tmx
@@ -0,0 +1,16 @@
+
+
diff --git a/tests/test_cross_template/template-rectangle.json b/tests/test_cross_template/template-rectangle.json
new file mode 100644
index 0000000..fc39229
--- /dev/null
+++ b/tests/test_cross_template/template-rectangle.json
@@ -0,0 +1,12 @@
+{ "object":
+ {
+ "height":38.2811778048473,
+ "id":1,
+ "name":"",
+ "rotation":0,
+ "type":"",
+ "visible":true,
+ "width":63.6585878103079
+ },
+ "type":"template"
+}
\ No newline at end of file
diff --git a/tests/test_cross_template/template-rectangle.tx b/tests/test_cross_template/template-rectangle.tx
new file mode 100644
index 0000000..6daa364
--- /dev/null
+++ b/tests/test_cross_template/template-rectangle.tx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_cross_template/test_cross_template.py b/tests/test_cross_template/test_cross_template.py
new file mode 100644
index 0000000..a1f8d86
--- /dev/null
+++ b/tests/test_cross_template/test_cross_template.py
@@ -0,0 +1,16 @@
+import os
+from pathlib import Path
+
+import pytest
+
+from pytiled_parser import parse_map
+
+
+def test_cross_template_tmx_json():
+ with pytest.raises(NotImplementedError):
+ parse_map(Path(os.path.dirname(os.path.abspath(__file__))) / "map.tmx")
+
+
+def test_cross_template_json_tmx():
+ with pytest.raises(NotImplementedError):
+ parse_map(Path(os.path.dirname(os.path.abspath(__file__))) / "map.json")
diff --git a/tests/test_cross_template/tile_set_image_for_template.json b/tests/test_cross_template/tile_set_image_for_template.json
new file mode 100644
index 0000000..c0cbf4e
--- /dev/null
+++ b/tests/test_cross_template/tile_set_image_for_template.json
@@ -0,0 +1,14 @@
+{ "columns":1,
+ "image":"..\/..\/images\/tile_04.png",
+ "imageheight":32,
+ "imagewidth":32,
+ "margin":0,
+ "name":"tile_set_image_for_template",
+ "spacing":0,
+ "tilecount":1,
+ "tiledversion":"1.7.1",
+ "tileheight":32,
+ "tilewidth":32,
+ "type":"tileset",
+ "version":"1.6"
+}
\ No newline at end of file
diff --git a/tests/test_cross_template/tile_set_image_for_template.tsx b/tests/test_cross_template/tile_set_image_for_template.tsx
new file mode 100644
index 0000000..9c59779
--- /dev/null
+++ b/tests/test_cross_template/tile_set_image_for_template.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_cross_template/tileset.json b/tests/test_cross_template/tileset.json
new file mode 100644
index 0000000..3302884
--- /dev/null
+++ b/tests/test_cross_template/tileset.json
@@ -0,0 +1,14 @@
+{ "columns":8,
+ "image":"..\/test_data\/images\/tmw_desert_spacing.png",
+ "imageheight":199,
+ "imagewidth":265,
+ "margin":1,
+ "name":"tile_set_image",
+ "spacing":1,
+ "tilecount":48,
+ "tiledversion":"1.6.0",
+ "tileheight":32,
+ "tilewidth":32,
+ "type":"tileset",
+ "version":"1.6"
+}
\ No newline at end of file
diff --git a/tests/test_cross_template/tileset.tsx b/tests/test_cross_template/tileset.tsx
new file mode 100644
index 0000000..8aee17a
--- /dev/null
+++ b/tests/test_cross_template/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/layer_tests/all_layer_types/map.tmx b/tests/test_data/layer_tests/all_layer_types/map.tmx
new file mode 100644
index 0000000..c94d181
--- /dev/null
+++ b/tests/test_data/layer_tests/all_layer_types/map.tmx
@@ -0,0 +1,28 @@
+
+
diff --git a/tests/test_data/layer_tests/all_layer_types/tileset.tsx b/tests/test_data/layer_tests/all_layer_types/tileset.tsx
new file mode 100644
index 0000000..8aee17a
--- /dev/null
+++ b/tests/test_data/layer_tests/all_layer_types/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/layer_tests/b64/map.tmx b/tests/test_data/layer_tests/b64/map.tmx
new file mode 100644
index 0000000..060ebe1
--- /dev/null
+++ b/tests/test_data/layer_tests/b64/map.tmx
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/test_data/layer_tests/b64/tileset.tsx b/tests/test_data/layer_tests/b64/tileset.tsx
new file mode 100644
index 0000000..8aee17a
--- /dev/null
+++ b/tests/test_data/layer_tests/b64/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/layer_tests/b64_gzip/map.tmx b/tests/test_data/layer_tests/b64_gzip/map.tmx
new file mode 100644
index 0000000..21316ba
--- /dev/null
+++ b/tests/test_data/layer_tests/b64_gzip/map.tmx
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/test_data/layer_tests/b64_gzip/tileset.tsx b/tests/test_data/layer_tests/b64_gzip/tileset.tsx
new file mode 100644
index 0000000..8aee17a
--- /dev/null
+++ b/tests/test_data/layer_tests/b64_gzip/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/layer_tests/b64_zlib/map.tmx b/tests/test_data/layer_tests/b64_zlib/map.tmx
new file mode 100644
index 0000000..343bd2b
--- /dev/null
+++ b/tests/test_data/layer_tests/b64_zlib/map.tmx
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/test_data/layer_tests/b64_zlib/tileset.tsx b/tests/test_data/layer_tests/b64_zlib/tileset.tsx
new file mode 100644
index 0000000..8aee17a
--- /dev/null
+++ b/tests/test_data/layer_tests/b64_zlib/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/layer_tests/infinite_map/map.tmx b/tests/test_data/layer_tests/infinite_map/map.tmx
new file mode 100644
index 0000000..2a826bb
--- /dev/null
+++ b/tests/test_data/layer_tests/infinite_map/map.tmx
@@ -0,0 +1,35 @@
+
+
diff --git a/tests/test_data/layer_tests/infinite_map_b64/map.tmx b/tests/test_data/layer_tests/infinite_map_b64/map.tmx
new file mode 100644
index 0000000..d83d427
--- /dev/null
+++ b/tests/test_data/layer_tests/infinite_map_b64/map.tmx
@@ -0,0 +1,14 @@
+
+
diff --git a/tests/test_data/layer_tests/no_layers/map.tmx b/tests/test_data/layer_tests/no_layers/map.tmx
new file mode 100644
index 0000000..fb995a3
--- /dev/null
+++ b/tests/test_data/layer_tests/no_layers/map.tmx
@@ -0,0 +1,13 @@
+
+
diff --git a/tests/test_data/layer_tests/no_layers/tileset.tsx b/tests/test_data/layer_tests/no_layers/tileset.tsx
new file mode 100644
index 0000000..8aee17a
--- /dev/null
+++ b/tests/test_data/layer_tests/no_layers/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/map_tests/embedded_tileset/map.tmx b/tests/test_data/map_tests/embedded_tileset/map.tmx
new file mode 100644
index 0000000..c39ca67
--- /dev/null
+++ b/tests/test_data/map_tests/embedded_tileset/map.tmx
@@ -0,0 +1,6 @@
+
+
diff --git a/tests/test_data/map_tests/external_tileset_dif_dir/map.tmx b/tests/test_data/map_tests/external_tileset_dif_dir/map.tmx
new file mode 100644
index 0000000..9c19e15
--- /dev/null
+++ b/tests/test_data/map_tests/external_tileset_dif_dir/map.tmx
@@ -0,0 +1,17 @@
+
+
diff --git a/tests/test_data/map_tests/external_tileset_dif_dir/tileset/tileset.tsx b/tests/test_data/map_tests/external_tileset_dif_dir/tileset/tileset.tsx
new file mode 100644
index 0000000..192b15e
--- /dev/null
+++ b/tests/test_data/map_tests/external_tileset_dif_dir/tileset/tileset.tsx
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_data/map_tests/hexagonal/map.tmx b/tests/test_data/map_tests/hexagonal/map.tmx
new file mode 100644
index 0000000..482cf3c
--- /dev/null
+++ b/tests/test_data/map_tests/hexagonal/map.tmx
@@ -0,0 +1,18 @@
+
+
diff --git a/tests/test_data/map_tests/hexagonal/tileset.tsx b/tests/test_data/map_tests/hexagonal/tileset.tsx
new file mode 100644
index 0000000..cba4d04
--- /dev/null
+++ b/tests/test_data/map_tests/hexagonal/tileset.tsx
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tests/test_data/map_tests/no_background_color/map.tmx b/tests/test_data/map_tests/no_background_color/map.tmx
new file mode 100644
index 0000000..7e71558
--- /dev/null
+++ b/tests/test_data/map_tests/no_background_color/map.tmx
@@ -0,0 +1,4 @@
+
+
diff --git a/tests/test_data/map_tests/no_background_color/tileset.tsx b/tests/test_data/map_tests/no_background_color/tileset.tsx
new file mode 100644
index 0000000..8b1cf24
--- /dev/null
+++ b/tests/test_data/map_tests/no_background_color/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/map_tests/no_layers/map.tmx b/tests/test_data/map_tests/no_layers/map.tmx
new file mode 100644
index 0000000..1ef8b7a
--- /dev/null
+++ b/tests/test_data/map_tests/no_layers/map.tmx
@@ -0,0 +1,12 @@
+
+
diff --git a/tests/test_data/map_tests/no_layers/tileset.tsx b/tests/test_data/map_tests/no_layers/tileset.tsx
new file mode 100644
index 0000000..8b1cf24
--- /dev/null
+++ b/tests/test_data/map_tests/no_layers/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/map_tests/template/map.tmx b/tests/test_data/map_tests/template/map.tmx
new file mode 100644
index 0000000..24cc2f0
--- /dev/null
+++ b/tests/test_data/map_tests/template/map.tmx
@@ -0,0 +1,18 @@
+
+
diff --git a/tests/test_data/map_tests/template/template-rectangle.tx b/tests/test_data/map_tests/template/template-rectangle.tx
new file mode 100644
index 0000000..6daa364
--- /dev/null
+++ b/tests/test_data/map_tests/template/template-rectangle.tx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/map_tests/template/template-tile-image.tx b/tests/test_data/map_tests/template/template-tile-image.tx
new file mode 100644
index 0000000..989b725
--- /dev/null
+++ b/tests/test_data/map_tests/template/template-tile-image.tx
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tests/test_data/map_tests/template/template-tile-spritesheet.tx b/tests/test_data/map_tests/template/template-tile-spritesheet.tx
new file mode 100644
index 0000000..d958c77
--- /dev/null
+++ b/tests/test_data/map_tests/template/template-tile-spritesheet.tx
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tests/test_data/map_tests/template/tile_set_image_for_template.tsx b/tests/test_data/map_tests/template/tile_set_image_for_template.tsx
new file mode 100644
index 0000000..9c59779
--- /dev/null
+++ b/tests/test_data/map_tests/template/tile_set_image_for_template.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/map_tests/template/tile_set_single_image.tsx b/tests/test_data/map_tests/template/tile_set_single_image.tsx
new file mode 100644
index 0000000..c881c11
--- /dev/null
+++ b/tests/test_data/map_tests/template/tile_set_single_image.tsx
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/tests/test_data/map_tests/template/tileset.tsx b/tests/test_data/map_tests/template/tileset.tsx
new file mode 100644
index 0000000..8aee17a
--- /dev/null
+++ b/tests/test_data/map_tests/template/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/tilesets/image/tileset.tsx b/tests/test_data/tilesets/image/tileset.tsx
new file mode 100644
index 0000000..8aee17a
--- /dev/null
+++ b/tests/test_data/tilesets/image/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/tilesets/image_background_color/tileset.tsx b/tests/test_data/tilesets/image_background_color/tileset.tsx
new file mode 100644
index 0000000..25cae9b
--- /dev/null
+++ b/tests/test_data/tilesets/image_background_color/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/tilesets/image_grid/tileset.tsx b/tests/test_data/tilesets/image_grid/tileset.tsx
new file mode 100644
index 0000000..62ef87e
--- /dev/null
+++ b/tests/test_data/tilesets/image_grid/tileset.tsx
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/tests/test_data/tilesets/image_properties/tileset.tsx b/tests/test_data/tilesets/image_properties/tileset.tsx
new file mode 100644
index 0000000..42478ae
--- /dev/null
+++ b/tests/test_data/tilesets/image_properties/tileset.tsx
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_data/tilesets/image_tile_offset/tileset.tsx b/tests/test_data/tilesets/image_tile_offset/tileset.tsx
new file mode 100644
index 0000000..7c0c1bc
--- /dev/null
+++ b/tests/test_data/tilesets/image_tile_offset/tileset.tsx
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/tests/test_data/tilesets/image_transformations/tileset.tsx b/tests/test_data/tilesets/image_transformations/tileset.tsx
new file mode 100644
index 0000000..5b20f69
--- /dev/null
+++ b/tests/test_data/tilesets/image_transformations/tileset.tsx
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tests/test_data/tilesets/image_transparent_color/tileset.tsx b/tests/test_data/tilesets/image_transparent_color/tileset.tsx
new file mode 100644
index 0000000..5ab0346
--- /dev/null
+++ b/tests/test_data/tilesets/image_transparent_color/tileset.tsx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tests/test_data/tilesets/individual_images/tileset.tsx b/tests/test_data/tilesets/individual_images/tileset.tsx
new file mode 100644
index 0000000..877cf79
--- /dev/null
+++ b/tests/test_data/tilesets/individual_images/tileset.tsx
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_data/tilesets/terrain/tileset.tsx b/tests/test_data/tilesets/terrain/tileset.tsx
new file mode 100644
index 0000000..2a57297
--- /dev/null
+++ b/tests/test_data/tilesets/terrain/tileset.tsx
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/test_layer.py b/tests/test_layer.py
index 117761b..bce74b3 100644
--- a/tests/test_layer.py
+++ b/tests/test_layer.py
@@ -2,11 +2,14 @@
import importlib.util
import json
import os
+import xml.etree.ElementTree as etree
from pathlib import Path
import pytest
-from pytiled_parser import layer
+from pytiled_parser.common_types import OrderedPair, Size
+from pytiled_parser.parsers.json.layer import parse as parse_json
+from pytiled_parser.parsers.tmx.layer import parse as parse_tmx
TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
TEST_DATA = TESTS_DIR / "test_data"
@@ -25,8 +28,36 @@ ALL_LAYER_TESTS = [
]
+def fix_object(my_object):
+ my_object.coordinates = OrderedPair(
+ round(my_object.coordinates[0], 4),
+ round(my_object.coordinates[1], 4),
+ )
+ my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4))
+
+
+def fix_layer(layer):
+ layer.offset = OrderedPair(round(layer.offset[0], 3), round(layer.offset[1], 3))
+ layer.coordinates = OrderedPair(
+ round(layer.coordinates[0], 4), round(layer.coordinates[1], 4)
+ )
+ if layer.size:
+ layer.size = Size(round(layer.size[0], 4), round(layer.size[1], 4))
+ layer.parallax_factor = OrderedPair(
+ round(layer.parallax_factor[0], 4),
+ round(layer.parallax_factor[1], 4),
+ )
+ if hasattr(layer, "tiled_objects"):
+ for tiled_object in layer.tiled_objects:
+ fix_object(tiled_object)
+ if hasattr(layer, "layers"):
+ for child_layer in layer.layers:
+ fix_layer(child_layer)
+
+
+@pytest.mark.parametrize("parser_type", ["json", "tmx"])
@pytest.mark.parametrize("layer_test", ALL_LAYER_TESTS)
-def test_layer_integration(layer_test):
+def test_layer_integration(parser_type, layer_test):
# it's a PITA to import like this, don't do it
# https://stackoverflow.com/a/67692/1342874
spec = importlib.util.spec_from_file_location(
@@ -35,10 +66,33 @@ def test_layer_integration(layer_test):
expected = importlib.util.module_from_spec(spec)
spec.loader.exec_module(expected)
- raw_layers_path = layer_test / "map.json"
+ if parser_type == "json":
+ raw_layers_path = layer_test / "map.json"
+ with open(raw_layers_path) as raw_layers_file:
+ raw_layers = json.load(raw_layers_file)["layers"]
+ layers = [parse_json(raw_layer) for raw_layer in raw_layers]
+ elif parser_type == "tmx":
+ raw_layers_path = layer_test / "map.tmx"
+ with open(raw_layers_path) as raw_layers_file:
+ raw_layer = etree.parse(raw_layers_file).getroot()
+ layers = []
+ for layer in raw_layer.findall("./layer"):
+ layers.append(parse_tmx(layer))
- with open(raw_layers_path) as raw_layers_file:
- raw_layers = json.load(raw_layers_file)["layers"]
- layers = [layer.cast(raw_layer) for raw_layer in raw_layers]
+ for layer in raw_layer.findall("./objectgroup"):
+ layers.append(parse_tmx(layer))
+
+ for layer in raw_layer.findall("./group"):
+ layers.append(parse_tmx(layer))
+
+ for layer in raw_layer.findall("./imagelayer"):
+ layers.append(parse_tmx(layer))
+
+ for layer in layers:
+ fix_layer(layer)
+
+ for layer in expected.EXPECTED:
+ fix_layer(layer)
+ print(layer.size)
assert layers == expected.EXPECTED
diff --git a/tests/test_map.py b/tests/test_map.py
index 0019238..424e802 100644
--- a/tests/test_map.py
+++ b/tests/test_map.py
@@ -5,7 +5,8 @@ from pathlib import Path
import pytest
-from pytiled_parser import tiled_map
+from pytiled_parser import parse_map
+from pytiled_parser.common_types import OrderedPair, Size
TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
TEST_DATA = TESTS_DIR / "test_data"
@@ -21,17 +22,64 @@ ALL_MAP_TESTS = [
]
+def fix_object(my_object):
+ my_object.coordinates = OrderedPair(
+ round(my_object.coordinates[0], 3), round(my_object.coordinates[1], 3)
+ )
+ my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4))
+
+
+def fix_tileset(tileset):
+ tileset.version = None
+ tileset.tiled_version = None
+ if tileset.tiles:
+ for tile in tileset.tiles.values():
+ if tile.objects:
+ for my_object in tile.objects.tiled_objects:
+ fix_object(my_object)
+
+
+def fix_layer(layer):
+ for tiled_object in layer.tiled_objects:
+ fix_object(tiled_object)
+
+
+def fix_map(map):
+ map.version = None
+ map.tiled_version = None
+ for layer in [layer for layer in map.layers if hasattr(layer, "tiled_objects")]:
+ fix_layer(layer)
+
+ for tileset in map.tilesets.values():
+ fix_tileset(tileset)
+
+
+@pytest.mark.parametrize("parser_type", ["json", "tmx"])
@pytest.mark.parametrize("map_test", ALL_MAP_TESTS)
-def test_map_integration(map_test):
+def test_map_integration(parser_type, map_test):
# it's a PITA to import like this, don't do it
# https://stackoverflow.com/a/67692/1342874
spec = importlib.util.spec_from_file_location("expected", map_test / "expected.py")
expected = importlib.util.module_from_spec(spec)
spec.loader.exec_module(expected)
- raw_maps_path = map_test / "map.json"
+ if parser_type == "json":
+ raw_maps_path = map_test / "map.json"
+ elif parser_type == "tmx":
+ raw_maps_path = map_test / "map.tmx"
- casted_map = tiled_map.parse_map(raw_maps_path)
+ casted_map = parse_map(raw_maps_path)
+ # file detection when running from unit tests is broken
expected.EXPECTED.map_file = casted_map.map_file
+
+ # who even knows what/how/when the gods determine what the
+ # version values in maps/tileset files are, so we're just not
+ # gonna check them, because they don't make sense anyways.
+ #
+ # Yes the values could be set to None in the expected objects
+ # directly, but alas, this is just test code that's already stupid fast
+ # and I'm lazy because there's too many of them already existing.
+ fix_map(expected.EXPECTED)
+ fix_map(casted_map)
assert casted_map == expected.EXPECTED
diff --git a/tests/test_tiled_object.py b/tests/test_tiled_object_json.py
similarity index 96%
rename from tests/test_tiled_object.py
rename to tests/test_tiled_object_json.py
index 10e71c4..eacbc3e 100644
--- a/tests/test_tiled_object.py
+++ b/tests/test_tiled_object_json.py
@@ -5,7 +5,17 @@ from pathlib import Path
import pytest
-from pytiled_parser import common_types, tiled_object
+from pytiled_parser import common_types
+from pytiled_parser.parsers.json.tiled_object import parse
+from pytiled_parser.tiled_object import (
+ Ellipse,
+ Point,
+ Polygon,
+ Polyline,
+ Rectangle,
+ Text,
+ Tile,
+)
ELLIPSES = [
(
@@ -23,7 +33,7 @@ ELLIPSES = [
"y":81.1913152210981
}
""",
- tiled_object.Ellipse(
+ Ellipse(
id=6,
size=common_types.Size(57.4013868364215, 18.5517790155735),
name="name: ellipse",
@@ -48,7 +58,7 @@ ELLIPSES = [
"y":53.9092872570194
}
""",
- tiled_object.Ellipse(
+ Ellipse(
id=7,
size=common_types.Size(6.32943048766625, 31.4288962146186),
name="name: ellipse - invisible",
@@ -73,7 +83,7 @@ ELLIPSES = [
"y":120.040923041946
}
""",
- tiled_object.Ellipse(
+ Ellipse(
id=8,
size=common_types.Size(29.6828464249176, 24.2264408321018),
name="name: ellipse - rotated",
@@ -98,7 +108,7 @@ ELLIPSES = [
"y":127.679890871888
}
""",
- tiled_object.Ellipse(
+ Ellipse(
id=29,
name="name: ellipse - no width or height",
rotation=0,
@@ -124,7 +134,7 @@ RECTANGLES = [
"y":23.571672160964
}
""",
- tiled_object.Rectangle(
+ Rectangle(
id=1,
size=common_types.Size(45.3972945322269, 41.4686825053996),
name="name: rectangle",
@@ -148,7 +158,7 @@ RECTANGLES = [
"y":91.0128452881664
}
""",
- tiled_object.Rectangle(
+ Rectangle(
id=4,
size=common_types.Size(30.9923837671934, 32.7384335568944),
name="name: rectangle - invisible",
@@ -172,7 +182,7 @@ RECTANGLES = [
"y":23.3534159372513
}
""",
- tiled_object.Rectangle(
+ Rectangle(
id=5,
size=common_types.Size(10, 22),
name="name: rectangle - rotated",
@@ -196,7 +206,7 @@ RECTANGLES = [
"y":53.4727748095942
}
""",
- tiled_object.Rectangle(
+ Rectangle(
id=28,
size=common_types.Size(0, 0),
name="name: rectangle - no width or height",
@@ -251,7 +261,7 @@ RECTANGLES = [
"y":131.826759122428
}
""",
- tiled_object.Rectangle(
+ Rectangle(
id=30,
size=common_types.Size(21.170853700125, 13.7501420938956),
name="name: rectangle - properties",
@@ -287,7 +297,7 @@ POINTS = [
"y":82.9373650107991
}
""",
- tiled_object.Point(
+ Point(
id=2,
name="name: point",
rotation=0,
@@ -311,7 +321,7 @@ POINTS = [
"y":95.8144822098443
}
""",
- tiled_object.Point(
+ Point(
id=3,
name="name: point invisible",
rotation=0,
@@ -338,7 +348,7 @@ TILES = [
"y":48.3019211094691
}
""",
- tiled_object.Tile(
+ Tile(
id=13,
size=common_types.Size(32, 32),
name="name: tile",
@@ -364,7 +374,7 @@ TILES = [
"y":168.779356598841
}
""",
- tiled_object.Tile(
+ Tile(
id=14,
size=common_types.Size(32, 32),
name="name: tile - invisible",
@@ -390,7 +400,7 @@ TILES = [
"y":59.8695009662385
}
""",
- tiled_object.Tile(
+ Tile(
id=15,
size=common_types.Size(32, 32),
name="name: tile - horizontal flipped",
@@ -416,7 +426,7 @@ TILES = [
"y":60.742525861089
}
""",
- tiled_object.Tile(
+ Tile(
id=16,
size=common_types.Size(32, 32),
name="name: tile - vertical flipped",
@@ -442,7 +452,7 @@ TILES = [
"y":95.6635216551097
}
""",
- tiled_object.Tile(
+ Tile(
id=17,
size=common_types.Size(32, 32),
name="name: tile - both flipped",
@@ -468,7 +478,7 @@ TILES = [
"y":142.62
}
""",
- tiled_object.Tile(
+ Tile(
id=18,
size=common_types.Size(32, 32),
name="name: tile - rotated",
@@ -517,7 +527,7 @@ POLYGONS = [
"y":38.6313515971354
}
""",
- tiled_object.Polygon(
+ Polygon(
id=9,
name="name: polygon",
points=[
@@ -560,7 +570,7 @@ POLYGONS = [
"y":24.4446970558145
}
""",
- tiled_object.Polygon(
+ Polygon(
id=10,
name="name: polygon - invisible",
points=[
@@ -613,7 +623,7 @@ POLYGONS = [
"y":19.8613163578493
}
""",
- tiled_object.Polygon(
+ Polygon(
id=11,
name="name: polygon - rotated",
points=[
@@ -660,7 +670,7 @@ POLYLINES = [
"y":90.1398203933159
}
""",
- tiled_object.Polyline(
+ Polyline(
id=12,
name="name: polyline",
points=[
@@ -701,7 +711,7 @@ POLYLINES = [
"y":163.333333333333
}
""",
- tiled_object.Polyline(
+ Polyline(
id=31,
name="name: polyline - invisible",
points=[
@@ -742,7 +752,7 @@ POLYLINES = [
"y":128.666666666667
}
""",
- tiled_object.Polyline(
+ Polyline(
id=32,
name="name: polyline - rotated",
points=[
@@ -778,7 +788,7 @@ TEXTS = [
"y":93.2986813686484
}
""",
- tiled_object.Text(
+ Text(
id=19,
name="name: text",
text="Hello World",
@@ -809,7 +819,7 @@ TEXTS = [
"y":112.068716607935
}
""",
- tiled_object.Text(
+ Text(
id=20,
name="name: text - invisible",
text="Hello World",
@@ -840,7 +850,7 @@ TEXTS = [
"y":78.4572581561896
}
""",
- tiled_object.Text(
+ Text(
id=21,
name="name: text - rotated",
text="Hello World",
@@ -874,7 +884,7 @@ TEXTS = [
"y":101.592417869728
}
""",
- tiled_object.Text(
+ Text(
id=22,
name="name: text - different font",
text="Hello World",
@@ -907,7 +917,7 @@ TEXTS = [
"y":154.192167784472
}
""",
- tiled_object.Text(
+ Text(
id=23,
name="name: text - no word wrap",
text="Hello World",
@@ -939,7 +949,7 @@ TEXTS = [
"y":1.19455496191883
}
""",
- tiled_object.Text(
+ Text(
id=24,
name="name: text - right bottom align",
text="Hello World",
@@ -973,7 +983,7 @@ TEXTS = [
"y": 3.81362964647039
}
""",
- tiled_object.Text(
+ Text(
id=25,
name="text: center center align",
rotation=0,
@@ -1006,7 +1016,7 @@ TEXTS = [
"y": 60.7785040354666
}
""",
- tiled_object.Text(
+ Text(
id=26,
name="name: text - justified",
rotation=0,
@@ -1038,7 +1048,7 @@ TEXTS = [
"y": 130.620495623508
}
""",
- tiled_object.Text(
+ Text(
id=27,
name="name: text - red",
rotation=0,
@@ -1075,7 +1085,7 @@ TEXTS = [
"y":22
}
""",
- tiled_object.Text(
+ Text(
id=31,
name="name: text - font options",
rotation=0,
@@ -1100,7 +1110,7 @@ OBJECTS = ELLIPSES + RECTANGLES + POINTS + TILES + POLYGONS + POLYLINES + TEXTS
@pytest.mark.parametrize("raw_object_json,expected", OBJECTS)
def test_parse_layer(raw_object_json, expected):
raw_object = json.loads(raw_object_json)
- result = tiled_object.cast(raw_object)
+ result = parse(raw_object)
assert result == expected
@@ -1118,4 +1128,4 @@ def test_parse_no_parent_dir():
json_object = json.loads(raw_object)
with pytest.raises(RuntimeError):
- tiled_object.cast(json_object)
+ parse(json_object)
diff --git a/tests/test_tiled_object_tmx.py b/tests/test_tiled_object_tmx.py
new file mode 100644
index 0000000..d277759
--- /dev/null
+++ b/tests/test_tiled_object_tmx.py
@@ -0,0 +1,492 @@
+"""Tests for objects"""
+import xml.etree.ElementTree as etree
+from contextlib import ExitStack as does_not_raise
+from pathlib import Path
+
+import pytest
+
+from pytiled_parser import common_types
+from pytiled_parser.parsers.tmx.tiled_object import parse
+from pytiled_parser.tiled_object import (
+ Ellipse,
+ Point,
+ Polygon,
+ Polyline,
+ Rectangle,
+ Text,
+ Tile,
+)
+
+ELLIPSES = [
+ (
+ """
+
+ """,
+ Ellipse(
+ id=6,
+ size=common_types.Size(57.4014, 18.5518),
+ name="ellipse",
+ coordinates=common_types.OrderedPair(37.5401, 81.1913),
+ ),
+ ),
+ (
+ """
+
+ """,
+ Ellipse(
+ id=7,
+ size=common_types.Size(6.3294, 31.4289),
+ name="ellipse - invisible",
+ visible=False,
+ coordinates=common_types.OrderedPair(22.6986, 53.9093),
+ ),
+ ),
+ (
+ """
+
+ """,
+ Ellipse(
+ id=8,
+ size=common_types.Size(29.6828, 24.2264),
+ name="ellipse - rotated",
+ rotation=111,
+ coordinates=common_types.OrderedPair(35.7940, 120.0409),
+ ),
+ ),
+ (
+ """
+
+ """,
+ Ellipse(
+ id=29,
+ name="ellipse - no width or height",
+ coordinates=common_types.OrderedPair(72.4611, 127.6799),
+ ),
+ ),
+]
+
+RECTANGLES = [
+ (
+ """
+
+ """,
+ Rectangle(
+ id=1,
+ size=common_types.Size(45.3973, 41.4687),
+ coordinates=common_types.OrderedPair(27.7185, 23.5717),
+ name="rectangle",
+ ),
+ ),
+ (
+ """
+
+ """,
+ Rectangle(
+ id=4,
+ size=common_types.Size(30.9924, 32.7384),
+ coordinates=common_types.OrderedPair(163.9104, 91.0128),
+ name="rectangle - invisible",
+ visible=False,
+ ),
+ ),
+ (
+ """
+
+ """,
+ Rectangle(
+ id=5,
+ size=common_types.Size(10, 22),
+ coordinates=common_types.OrderedPair(183.3352, 23.3534),
+ name="rectangle - rotated",
+ rotation=10,
+ ),
+ ),
+ (
+ """
+
+ """,
+ Rectangle(
+ id=28,
+ coordinates=common_types.OrderedPair(131.1720, 53.4728),
+ name="rectangle - no width or height",
+ ),
+ ),
+ (
+ r"""
+
+ """,
+ Rectangle(
+ id=30,
+ size=common_types.Size(21.1709, 13.7501),
+ coordinates=common_types.OrderedPair(39.0679, 131.8268),
+ name="rectangle - properties",
+ properties={
+ "bool property": False,
+ "color property": common_types.Color(170, 0, 0, 255),
+ "file property": Path("../../../../../../dev/null"),
+ "float property": 42.1,
+ "int property": 8675309,
+ "string property": "pytiled_parser rulez!1!!",
+ },
+ ),
+ ),
+]
+
+POINTS = [
+ (
+ """
+
+ """,
+ Point(
+ id=2, coordinates=common_types.OrderedPair(159.9818, 82.9374), name="point"
+ ),
+ ),
+ (
+ """
+
+ """,
+ Point(
+ id=2,
+ coordinates=common_types.OrderedPair(159.9818, 82.9374),
+ name="point - invisible",
+ visible=False,
+ ),
+ ),
+]
+
+POLYGONS = [
+ (
+ """
+
+ """,
+ Polygon(
+ id=9,
+ coordinates=common_types.OrderedPair(89.4851, 38.6314),
+ name="polygon",
+ points=[
+ common_types.OrderedPair(0, 0),
+ common_types.OrderedPair(19.4248, 27.0638),
+ common_types.OrderedPair(19.6431, 3.0556),
+ common_types.OrderedPair(-2.6191, 15.9327),
+ common_types.OrderedPair(25.3177, 16.3692),
+ ],
+ ),
+ ),
+ (
+ """
+
+ """,
+ Polygon(
+ id=9,
+ coordinates=common_types.OrderedPair(89.4851, 38.6314),
+ name="polygon - invisible",
+ points=[
+ common_types.OrderedPair(0, 0),
+ common_types.OrderedPair(19.4248, 27.0638),
+ common_types.OrderedPair(19.6431, 3.0556),
+ common_types.OrderedPair(-2.6191, 15.9327),
+ common_types.OrderedPair(25.3177, 16.3692),
+ ],
+ visible=False,
+ ),
+ ),
+ (
+ """
+
+ """,
+ Polygon(
+ id=9,
+ coordinates=common_types.OrderedPair(89.4851, 38.6314),
+ name="polygon - rotated",
+ points=[
+ common_types.OrderedPair(0, 0),
+ common_types.OrderedPair(19.4248, 27.0638),
+ common_types.OrderedPair(19.6431, 3.0556),
+ common_types.OrderedPair(-2.6191, 15.9327),
+ common_types.OrderedPair(25.3177, 16.3692),
+ ],
+ rotation=123,
+ ),
+ ),
+]
+
+POLYLINES = [
+ (
+ """
+
+ """,
+ Polyline(
+ id=12,
+ coordinates=common_types.OrderedPair(124.1878, 90.1398),
+ name="polyline",
+ points=[
+ common_types.OrderedPair(0, 0),
+ common_types.OrderedPair(-13.3136, 41.0321),
+ common_types.OrderedPair(21.3891, 16.8057),
+ ],
+ ),
+ ),
+ (
+ """
+
+ """,
+ Polyline(
+ id=12,
+ coordinates=common_types.OrderedPair(124.1878, 90.1398),
+ name="polyline - invisible",
+ points=[
+ common_types.OrderedPair(0, 0),
+ common_types.OrderedPair(-13.3136, 41.0321),
+ common_types.OrderedPair(21.3891, 16.8057),
+ ],
+ visible=False,
+ ),
+ ),
+ (
+ """
+
+ """,
+ Polyline(
+ id=12,
+ coordinates=common_types.OrderedPair(124.1878, 90.1398),
+ name="polyline - rotated",
+ points=[
+ common_types.OrderedPair(0, 0),
+ common_types.OrderedPair(-13.3136, 41.0321),
+ common_types.OrderedPair(21.3891, 16.8057),
+ ],
+ rotation=110,
+ ),
+ ),
+]
+
+TEXTS = [
+ (
+ """
+
+ """,
+ Text(
+ id=19,
+ name="text",
+ text="Hello World",
+ size=common_types.Size(92.375, 19),
+ coordinates=common_types.OrderedPair(93.2987, 81.7106),
+ ),
+ ),
+ (
+ """
+
+ """,
+ Text(
+ id=19,
+ name="text - wrap",
+ text="Hello World",
+ wrap=True,
+ size=common_types.Size(92.375, 19),
+ coordinates=common_types.OrderedPair(93.2987, 81.7106),
+ ),
+ ),
+ (
+ """
+
+ """,
+ Text(
+ id=19,
+ name="text - rotated",
+ text="Hello World",
+ rotation=110,
+ size=common_types.Size(92.375, 19),
+ coordinates=common_types.OrderedPair(93.2987, 81.7106),
+ ),
+ ),
+ (
+ """
+
+ """,
+ Text(
+ id=19,
+ name="text - different font",
+ text="Hello World",
+ font_size=19,
+ font_family="DejaVu Sans",
+ rotation=110,
+ size=common_types.Size(92.375, 19),
+ coordinates=common_types.OrderedPair(93.2987, 81.7106),
+ ),
+ ),
+ (
+ """
+
+ """,
+ Text(
+ id=19,
+ name="text - right bottom align",
+ text="Hello World",
+ horizontal_align="right",
+ vertical_align="bottom",
+ size=common_types.Size(92.375, 19),
+ coordinates=common_types.OrderedPair(93.2987, 81.7106),
+ ),
+ ),
+ (
+ """
+
+ """,
+ Text(
+ id=19,
+ name="text - center center align",
+ text="Hello World",
+ horizontal_align="center",
+ vertical_align="center",
+ size=common_types.Size(92.375, 19),
+ coordinates=common_types.OrderedPair(93.2987, 81.7106),
+ ),
+ ),
+ (
+ """
+
+ """,
+ Text(
+ id=19,
+ name="text - justified",
+ text="Hello World",
+ horizontal_align="justify",
+ size=common_types.Size(92.375, 19),
+ coordinates=common_types.OrderedPair(93.2987, 81.7106),
+ ),
+ ),
+ (
+ """
+
+ """,
+ Text(
+ id=19,
+ name="text - colored",
+ text="Hello World",
+ color=common_types.Color(170, 0, 0, 255),
+ size=common_types.Size(92.375, 19),
+ coordinates=common_types.OrderedPair(93.2987, 81.7106),
+ ),
+ ),
+ (
+ """
+
+ """,
+ Text(
+ id=19,
+ name="text - font options",
+ text="Hello World",
+ size=common_types.Size(92.375, 19),
+ bold=True,
+ italic=True,
+ kerning=True,
+ strike_out=True,
+ underline=True,
+ wrap=True,
+ coordinates=common_types.OrderedPair(93.2987, 81.7106),
+ ),
+ ),
+]
+
+TILES = [
+ (
+ """
+
+ """,
+ Tile(
+ id=13,
+ size=common_types.Size(32, 32),
+ name="tile",
+ coordinates=common_types.OrderedPair(111.8981, 48.3019),
+ gid=79,
+ ),
+ ),
+ (
+ """
+
+ """,
+ Tile(
+ id=13,
+ size=common_types.Size(32, 32),
+ name="tile - invisible",
+ type="tile",
+ coordinates=common_types.OrderedPair(111.8981, 48.3019),
+ gid=79,
+ visible=False,
+ ),
+ ),
+ (
+ """
+
+ """,
+ Tile(
+ id=13,
+ size=common_types.Size(32, 32),
+ name="tile - rotated",
+ coordinates=common_types.OrderedPair(111.8981, 48.3019),
+ gid=79,
+ rotation=110,
+ ),
+ ),
+]
+
+OBJECTS = ELLIPSES + RECTANGLES + POINTS + POLYGONS + POLYLINES + TEXTS + TILES
+
+
+@pytest.mark.parametrize("raw_object_tmx,expected", OBJECTS)
+def test_parse_layer(raw_object_tmx, expected):
+ raw_object = etree.fromstring(raw_object_tmx)
+ result = parse(raw_object)
+
+ assert result == expected
diff --git a/tests/test_tileset.py b/tests/test_tileset.py
index d24e9d5..9f88f78 100644
--- a/tests/test_tileset.py
+++ b/tests/test_tileset.py
@@ -2,11 +2,14 @@
import importlib.util
import json
import os
+import xml.etree.ElementTree as etree
from pathlib import Path
import pytest
-from pytiled_parser import tileset
+from pytiled_parser.common_types import OrderedPair, Size
+from pytiled_parser.parsers.json.tileset import parse as parse_json
+from pytiled_parser.parsers.tmx.tileset import parse as parse_tmx
TESTS_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
TEST_DATA = TESTS_DIR / "test_data"
@@ -26,8 +29,26 @@ ALL_TILESET_DIRS = [
]
+def fix_object(my_object):
+ my_object.coordinates = OrderedPair(
+ round(my_object.coordinates[0], 4), round(my_object.coordinates[1], 4)
+ )
+ my_object.size = Size(round(my_object.size[0], 4), round(my_object.size[1], 4))
+
+
+def fix_tileset(tileset):
+ tileset.version = None
+ tileset.tiled_version = None
+ if tileset.tiles:
+ for tile in tileset.tiles.values():
+ if tile.objects:
+ for my_object in tile.objects.tiled_objects:
+ fix_object(my_object)
+
+
+@pytest.mark.parametrize("parser_type", ["json", "tmx"])
@pytest.mark.parametrize("tileset_dir", ALL_TILESET_DIRS)
-def test_tilesets_integration(tileset_dir):
+def test_tilesets_integration(parser_type, tileset_dir):
# it's a PITA to import like this, don't do it
# https://stackoverflow.com/a/67692/1342874
spec = importlib.util.spec_from_file_location(
@@ -36,9 +57,16 @@ def test_tilesets_integration(tileset_dir):
expected = importlib.util.module_from_spec(spec)
spec.loader.exec_module(expected)
- raw_tileset_path = tileset_dir / "tileset.json"
+ if parser_type == "json":
+ raw_tileset_path = tileset_dir / "tileset.json"
+ with open(raw_tileset_path) as raw_tileset:
+ tileset_ = parse_json(json.loads(raw_tileset.read()), 1)
+ elif parser_type == "tmx":
+ raw_tileset_path = tileset_dir / "tileset.tsx"
+ with open(raw_tileset_path) as raw_tileset:
+ tileset_ = parse_tmx(etree.parse(raw_tileset).getroot(), 1)
- with open(raw_tileset_path) as raw_tileset:
- tileset_ = tileset.cast(json.loads(raw_tileset.read()), 1)
+ fix_tileset(tileset_)
+ fix_tileset(expected.EXPECTED)
assert tileset_ == expected.EXPECTED