diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..16b486a88261c6699751c5c44fc6283019d8faa3
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,14 @@
+# Set the default behavior, in case people don't have core.autocrlf set.
+* text=auto
+
+# Explicitly declare text files we want to always be normalized and converted
+# to native line endings on checkout.
+*.rs text
+
+# Normalize example TMX & TSX file line endings for consistent tests among all platforms.
+*.tmx text eol=crlf
+*.tsx text eol=crlf
+
+# Denote all files that are truly binary and should not be modified.
+*.png binary
+*.jpg binary
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3b9a6498a2e8a271527f30647b11b77fcb13f6c..893e365d6bf441460b0723b6d485cbb5ebcd5437 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,22 +6,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Changed
+- Reorganized crate:
+    - `parse_file`, `parse` -> `Map::parse_file` with optional path.
+    - `parse_with_path` -> `Map::parse_reader`
+    - `parse_tileset` -> `Tileset::parse`
+    - `Frame` has been moved to the `animation` module.
+    - `ParseTileError` & `TiledError` have been moved into the `error` module.
+    - `Image` has been moved into the `image` module.
+    - `LayerTile`, `Layer`, `LayerData`, `ImageLayer` & `Chunk` have been moved into the `layers` module.
+    - `Map` & `Orientation` have been moved into the `map` module.
+    - `ObjectGroup`, `ObjectShape` & `Object` have been moved into the `objects` module.
+    - `Colour`, `PropertyValue` & `Properties` have been moved into the `properties` module.
+    - All mentions of `Colour` have been changed to `Color` for consistency with the Tiled dataformat.
+    - `Tile` has been moved into the `tile` module.
+    - `Tileset` has been moved into the `tileset` module.
+    - `Map::get_tileset_by_gid` -> `Map::tileset_by_gid`
+- Tile now has `image` instead of `images`. ([Issue comment](https://github.com/mapeditor/rs-tiled/issues/103#issuecomment-940773123))
+- Tileset now has `image` instead of `images`.
+- `Image::source` is now a `PathBuf` instead of a `String`.
+- Functions that took in `&Path` now take `impl AsRef<Path>`.
+- Refactored internals.
+- Fixed library warnings.
+- Bumped `zstd` to `0.9`.
+- Fixed markdown formatting in the `CONTRIBUTORS` file.
 
 ### Added
-
+- `Tileset::source` for obtaining where the tileset actually came from.
+- `Tileset::columns`.
+- `layers::Layer::id`.
 - Support for 'object'-type properties.
+- Documentation for map members.
+- Tests for `tiled_base64_zstandard.tmx`.
+- `.gitattributes` for line ending consistency.
+- Support for multiline string properties.
+- MIT license file.
 
-## [0.9.2] - 2020-Apr-25
 
-### Added
+## [0.9.5]
+TODO
 
-- Properties to Tilesets.
-- Test verifying `tileset.properties`
+## [0.9.4]
+TODO
 
-### Changed
+## [0.9.3]
+TODO
 
--
-
-### Removed
-
--
+## [0.9.2] - 2020-Apr-25
+### Added
+- Properties to Tilesets.
+- Test verifying `tileset.properties`
\ No newline at end of file
diff --git a/README.md b/README.md
index f92ec5fa6010bc2286657c3e9223734bb520cedb..d5c3bbaa1f6d65df128333405752d6d30c0085d4 100644
--- a/README.md
+++ b/README.md
@@ -18,22 +18,12 @@ to the dependencies section of your Cargo.toml.
 ### Example
 
 ```rust
-extern crate serialize;
-extern crate tiled;
-
-use std::fs::File;
-use std::io::BufReader;
-use std::path::Path;
-
-use tiled::parse;
+use tiled::map::Map;
 
 fn main() {
-    let file = File::open(&Path::new("assets/tiled_base64_zlib.tmx")).unwrap();
-    println!("Opened file");
-    let reader = BufReader::new(file);
-    let map = parse(reader).unwrap();
+    let map = Map::parse_file("assets/tiled_base64_zlib.tmx").unwrap();
     println!("{:?}", map);
-    println!("{:?}", map.get_tileset_by_gid(22));
+    println!("{:?}", map.tileset_by_gid(22));
 }
 ```
 
diff --git a/examples/main.rs b/examples/main.rs
index c4ae445d7c21f05a1d29504181b649ad76678660..afdcb6d209980f26edeec11c2e21b7537d822815 100644
--- a/examples/main.rs
+++ b/examples/main.rs
@@ -1,11 +1,7 @@
-use std::fs::File;
-use std::path::Path;
-use tiled::parse;
+use tiled::map::Map;
 
 fn main() {
-    let file = File::open(&Path::new("assets/tiled_base64_zlib.tmx")).unwrap();
-    println!("Opened file");
-    let map = parse(file).unwrap();
+    let map = Map::parse_file("assets/tiled_base64_zlib.tmx").unwrap();
     println!("{:?}", map);
-    println!("{:?}", map.get_tileset_by_gid(22));
+    println!("{:?}", map.tileset_by_gid(22));
 }
diff --git a/src/error.rs b/src/error.rs
index 6a502c21c01edaa186ad2a9cca205ee7c963e74f..55c4d032fe695152bc40b9017fdb9a8db4e4b1d4 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -2,7 +2,7 @@ use std::fmt;
 
 #[derive(Debug, Copy, Clone)]
 pub enum ParseTileError {
-    ColourError,
+    ColorError,
     OrientationError,
 }
 
@@ -18,6 +18,11 @@ pub enum TiledError {
     Base64DecodingError(base64::DecodeError),
     XmlDecodingError(xml::reader::Error),
     PrematureEnd(String),
+    /// Tried to parse external data of an object without a file location,
+    /// e.g. by using Map::parse_reader.
+    SourceRequired {
+        object_to_parse: String,
+    },
     Other(String),
 }
 
@@ -29,6 +34,11 @@ impl fmt::Display for TiledError {
             TiledError::Base64DecodingError(ref e) => write!(fmt, "{}", e),
             TiledError::XmlDecodingError(ref e) => write!(fmt, "{}", e),
             TiledError::PrematureEnd(ref e) => write!(fmt, "{}", e),
+            TiledError::SourceRequired {
+                ref object_to_parse,
+            } => {
+                write!(fmt, "Tried to parse external {} without a file location, e.g. by using Map::parse_reader.", object_to_parse)
+            }
             TiledError::Other(ref s) => write!(fmt, "{}", s),
         }
     }
@@ -43,6 +53,7 @@ impl std::error::Error for TiledError {
             TiledError::Base64DecodingError(ref e) => Some(e as &dyn std::error::Error),
             TiledError::XmlDecodingError(ref e) => Some(e as &dyn std::error::Error),
             TiledError::PrematureEnd(_) => None,
+            TiledError::SourceRequired { .. } => None,
             TiledError::Other(_) => None,
         }
     }
diff --git a/src/image.rs b/src/image.rs
index 351cc5bf61ebf59f25543a9ed454780bb4fecc0b..a7cf8eff2874bc13208101e6e996c93ab313e614 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -1,22 +1,33 @@
-use std::io::Read;
+use std::{
+    io::Read,
+    path::{Path, PathBuf},
+};
 
 use xml::{attribute::OwnedAttribute, EventReader};
 
-use crate::{error::TiledError, properties::Colour, util::*};
+use crate::{error::TiledError, properties::Color, util::*};
 
 #[derive(Debug, PartialEq, Eq, Clone)]
 pub struct Image {
-    /// The filepath of the image
-    pub source: String,
+    /// The filepath of the image.
+    ///
+    /// ## Note
+    /// The crate does not currently support embedded images (Even though Tiled
+    /// does not allow creating maps with embedded image data, the TMX format does; [source])
+    ///
+    /// [source]: https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#image
+    // TODO: Embedded images
+    pub source: PathBuf,
     pub width: i32,
     pub height: i32,
-    pub transparent_colour: Option<Colour>,
+    pub transparent_colour: Option<Color>,
 }
 
 impl Image {
     pub(crate) fn new<R: Read>(
         parser: &mut EventReader<R>,
         attrs: Vec<OwnedAttribute>,
+        path_relative_to: impl AsRef<Path>,
     ) -> Result<Image, TiledError> {
         let (c, (s, w, h)) = get_attrs!(
             attrs,
@@ -28,12 +39,12 @@ impl Image {
                 ("width", width, |v:String| v.parse().ok()),
                 ("height", height, |v:String| v.parse().ok()),
             ],
-            TiledError::MalformedAttributes("image must have a source, width and height with correct types".to_string())
+            TiledError::MalformedAttributes("Image must have a source, width and height with correct types".to_string())
         );
 
         parse_tag!(parser, "image", { "" => |_| Ok(()) });
         Ok(Image {
-            source: s,
+            source: path_relative_to.as_ref().join(s),
             width: w,
             height: h,
             transparent_colour: c,
diff --git a/src/layers.rs b/src/layers.rs
index 4d65d759e24e1c5f6a36da6278f6e0354edb5238..883426a03d4d2c47176f0d420c3982a022f1422b 100644
--- a/src/layers.rs
+++ b/src/layers.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashMap, io::Read};
+use std::{collections::HashMap, io::Read, path::Path};
 
 use xml::{attribute::OwnedAttribute, EventReader};
 
@@ -86,7 +86,7 @@ impl Layer {
         parse_tag!(parser, "layer", {
             "data" => |attrs| {
                 if infinite {
-                    tiles = parse_infinite_data(parser, attrs, width)?;
+                    tiles = parse_infinite_data(parser, attrs)?;
                 } else {
                     tiles = parse_data(parser, attrs, width)?;
                 }
@@ -137,6 +137,7 @@ impl ImageLayer {
         parser: &mut EventReader<R>,
         attrs: Vec<OwnedAttribute>,
         layer_index: u32,
+        path_relative_to: Option<&Path>,
     ) -> Result<ImageLayer, TiledError> {
         let ((o, v, ox, oy, n, id), ()) = get_attrs!(
             attrs,
@@ -156,7 +157,7 @@ impl ImageLayer {
         let mut image: Option<Image> = None;
         parse_tag!(parser, "imagelayer", {
             "image" => |attrs| {
-                image = Some(Image::new(parser, attrs)?);
+                image = Some(Image::new(parser, attrs, path_relative_to.ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_string()})?)?);
                 Ok(())
             },
             "properties" => |_| {
diff --git a/src/lib.rs b/src/lib.rs
index 06805e70b8daa4ce9e7102a1f65b57bc57d54e91..2033ef1188ba07f1a3501cfb2d3f1f5213cb5548 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -8,55 +8,3 @@ pub mod properties;
 pub mod tile;
 pub mod tileset;
 mod util;
-
-use base64;
-
-use error::*;
-use image::*;
-use layers::*;
-use map::*;
-use std::collections::HashMap;
-use std::fmt;
-use std::fs::File;
-use std::io::Read;
-use std::path::{Path, PathBuf};
-use std::str::FromStr;
-use tile::*;
-use tileset::*;
-use util::*;
-use xml::attribute::OwnedAttribute;
-use xml::reader::XmlEvent;
-use xml::reader::{Error as XmlError, EventReader};
-
-// TODO move these
-/// Parse a buffer hopefully containing the contents of a Tiled file and try to
-/// parse it. This augments `parse` with a file location: some engines
-/// (e.g. Amethyst) simply hand over a byte stream (and file location) for parsing,
-/// in which case this function may be required.
-pub fn parse_with_path<R: Read>(reader: R, path: &Path) -> Result<Map, TiledError> {
-    parse_impl(reader, Some(path))
-}
-
-/// Parse a file hopefully containing a Tiled map and try to parse it.  If the
-/// file has an external tileset, the tileset file will be loaded using a path
-/// relative to the map file's path.
-pub fn parse_file(path: &Path) -> Result<Map, TiledError> {
-    let file = File::open(path)
-        .map_err(|_| TiledError::Other(format!("Map file not found: {:?}", path)))?;
-    parse_impl(file, Some(path))
-}
-
-/// Parse a buffer hopefully containing the contents of a Tiled file and try to
-/// parse it.
-pub fn parse<R: Read>(reader: R) -> Result<Map, TiledError> {
-    parse_impl(reader, None)
-}
-
-/// Parse a buffer hopefully containing the contents of a Tiled tileset.
-///
-/// External tilesets do not have a firstgid attribute.  That lives in the
-/// map. You must pass in `first_gid`.  If you do not need to use gids for anything,
-/// passing in 1 will work fine.
-pub fn parse_tileset<R: Read>(reader: R, first_gid: u32) -> Result<Tileset, TiledError> {
-    Tileset::new_external(reader, first_gid)
-}
diff --git a/src/map.rs b/src/map.rs
index f9271fa2dd1234210ab2dcb27662a1b17190750f..e22ff65f977b797ed717b5095d287249e3ff5110 100644
--- a/src/map.rs
+++ b/src/map.rs
@@ -1,38 +1,82 @@
-use std::{collections::HashMap, fmt, io::Read, path::Path, str::FromStr};
+use std::{collections::HashMap, fmt, fs::File, io::Read, path::Path, str::FromStr};
 
-use xml::{attribute::OwnedAttribute, EventReader};
+use xml::{attribute::OwnedAttribute, reader::XmlEvent, EventReader};
 
 use crate::{
     error::{ParseTileError, TiledError},
     layers::{ImageLayer, Layer},
     objects::ObjectGroup,
-    properties::{parse_properties, Colour, Properties},
+    properties::{parse_properties, Color, Properties},
     tileset::Tileset,
-    util::*,
+    util::{get_attrs, parse_tag},
 };
 
-/// All Tiled files will be parsed into this. Holds all the layers and tilesets
+/// All Tiled map files will be parsed into this. Holds all the layers and tilesets.
 #[derive(Debug, PartialEq, Clone)]
 pub struct Map {
+    /// The TMX format version this map was saved to.
     pub version: String,
     pub orientation: Orientation,
-    /// Width of the map, in tiles
+    /// Width of the map, in tiles.
     pub width: u32,
-    /// Height of the map, in tiles
+    /// Height of the map, in tiles.
     pub height: u32,
+    /// Tile width, in pixels.
     pub tile_width: u32,
+    /// Tile height, in pixels.
     pub tile_height: u32,
+    /// The tilesets present in this map.
     pub tilesets: Vec<Tileset>,
+    /// The tile layers present in this map.
     pub layers: Vec<Layer>,
     pub image_layers: Vec<ImageLayer>,
     pub object_groups: Vec<ObjectGroup>,
+    /// The custom properties of this map.
     pub properties: Properties,
-    pub background_colour: Option<Colour>,
+    /// The background color of this map, if any.
+    pub background_color: Option<Color>,
     pub infinite: bool,
 }
 
 impl Map {
-    pub(crate) fn new<R: Read>(
+    /// Parse a buffer hopefully containing the contents of a Tiled file and try to
+    /// parse it. This augments `parse_file` with a custom reader: some engines
+    /// (e.g. Amethyst) simply hand over a byte stream (and file location) for parsing,
+    /// in which case this function may be required.
+    ///
+    /// The path is used for external dependencies such as tilesets or images, and may be skipped
+    /// if the map is fully embedded (Doesn't refer to external files). If a map *does* refer to
+    /// external files and a path is not given, the function will return [TiledError::SourceRequired].
+    pub fn parse_reader<R: Read>(reader: R, path: Option<&Path>) -> Result<Self, TiledError> {
+        let mut parser = EventReader::new(reader);
+        loop {
+            match parser.next().map_err(TiledError::XmlDecodingError)? {
+                XmlEvent::StartElement {
+                    name, attributes, ..
+                } => {
+                    if name.local_name == "map" {
+                        return Self::parse_xml(&mut parser, attributes, path);
+                    }
+                }
+                XmlEvent::EndDocument => {
+                    return Err(TiledError::PrematureEnd(
+                        "Document ended before map was parsed".to_string(),
+                    ))
+                }
+                _ => {}
+            }
+        }
+    }
+
+    /// Parse a file hopefully containing a Tiled map and try to parse it.  All external
+    /// files will be loaded relative to the path given.
+    pub fn parse_file(path: impl AsRef<Path>) -> Result<Self, TiledError> {
+        let file = File::open(path.as_ref())
+            .map_err(|_| TiledError::Other(format!("Map file not found: {:?}", path.as_ref())))?;
+        Self::parse_reader(file, Some(path.as_ref()))
+    }
+
+    fn parse_xml<R: Read>(
         parser: &mut EventReader<R>,
         attrs: Vec<OwnedAttribute>,
         map_path: Option<&Path>,
@@ -54,6 +98,8 @@ impl Map {
             TiledError::MalformedAttributes("map must have a version, width and height with correct types".to_string())
         );
 
+        let source_path = map_path.and_then(|p| p.parent());
+
         let mut tilesets = Vec::new();
         let mut layers = Vec::new();
         let mut image_layers = Vec::new();
@@ -62,7 +108,7 @@ impl Map {
         let mut layer_index = 0;
         parse_tag!(parser, "map", {
             "tileset" => |attrs| {
-                tilesets.push(Tileset::new(parser, attrs, map_path)?);
+                tilesets.push(Tileset::parse_xml(parser, attrs, source_path)?);
                 Ok(())
             },
             "layer" => |attrs| {
@@ -71,7 +117,7 @@ impl Map {
                 Ok(())
             },
             "imagelayer" => |attrs| {
-                image_layers.push(ImageLayer::new(parser, attrs, layer_index)?);
+                image_layers.push(ImageLayer::new(parser, attrs, layer_index, source_path)?);
                 layer_index += 1;
                 Ok(())
             },
@@ -97,13 +143,13 @@ impl Map {
             image_layers,
             object_groups,
             properties,
-            background_colour: c,
+            background_color: c,
             infinite: infinite.unwrap_or(false),
         })
     }
 
     /// This function will return the correct Tileset given a GID.
-    pub fn get_tileset_by_gid(&self, gid: u32) -> Option<&Tileset> {
+    pub fn tileset_by_gid(&self, gid: u32) -> Option<&Tileset> {
         let mut maximum_gid: i32 = -1;
         let mut maximum_ts = None;
         for tileset in self.tilesets.iter() {
@@ -116,6 +162,7 @@ impl Map {
     }
 }
 
+/// Represents the way tiles are laid out in a map.
 #[derive(Debug, PartialEq, Eq, Copy, Clone)]
 pub enum Orientation {
     Orthogonal,
diff --git a/src/objects.rs b/src/objects.rs
index 3b66e1752633b4dd8965b94b5e81e360e11bcbca..9f4a9938c6d9e93eed74aab3d802cd39009380c1 100644
--- a/src/objects.rs
+++ b/src/objects.rs
@@ -4,7 +4,7 @@ use xml::{attribute::OwnedAttribute, EventReader};
 
 use crate::{
     error::TiledError,
-    properties::{parse_properties, Colour, Properties},
+    properties::{parse_properties, Color, Properties},
     util::{get_attrs, parse_tag},
 };
 
@@ -14,7 +14,7 @@ pub struct ObjectGroup {
     pub opacity: f32,
     pub visible: bool,
     pub objects: Vec<Object>,
-    pub colour: Option<Colour>,
+    pub colour: Option<Color>,
     /**
      * Layer index is not preset for tile collision boxes
      */
diff --git a/src/properties.rs b/src/properties.rs
index 68afa62bfee52abea5f3552dc2deba26ed34453d..7e8c0bd995e5675785f61216a2f3b1bd739ab481 100644
--- a/src/properties.rs
+++ b/src/properties.rs
@@ -8,31 +8,31 @@ use crate::{
 };
 
 #[derive(Debug, PartialEq, Eq, Copy, Clone)]
-pub struct Colour {
+pub struct Color {
     pub red: u8,
     pub green: u8,
     pub blue: u8,
 }
 
-impl FromStr for Colour {
+impl FromStr for Color {
     type Err = ParseTileError;
 
-    fn from_str(s: &str) -> Result<Colour, ParseTileError> {
+    fn from_str(s: &str) -> Result<Color, ParseTileError> {
         let s = if s.starts_with("#") { &s[1..] } else { s };
         if s.len() != 6 {
-            return Err(ParseTileError::ColourError);
+            return Err(ParseTileError::ColorError);
         }
         let r = u8::from_str_radix(&s[0..2], 16);
         let g = u8::from_str_radix(&s[2..4], 16);
         let b = u8::from_str_radix(&s[4..6], 16);
         if r.is_ok() && g.is_ok() && b.is_ok() {
-            return Ok(Colour {
+            return Ok(Color {
                 red: r.unwrap(),
                 green: g.unwrap(),
                 blue: b.unwrap(),
             });
         }
-        Err(ParseTileError::ColourError)
+        Err(ParseTileError::ColorError)
     }
 }
 
diff --git a/src/tile.rs b/src/tile.rs
index 80d6f979ea1c89f462b013c3f5ef7c4b20369aae..4d14d2f3551ef0108c17399c76d7a1e418cfa82f 100644
--- a/src/tile.rs
+++ b/src/tile.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashMap, io::Read};
+use std::{collections::HashMap, io::Read, path::Path};
 
 use xml::{attribute::OwnedAttribute, EventReader};
 
@@ -14,7 +14,7 @@ use crate::{
 #[derive(Debug, PartialEq, Clone)]
 pub struct Tile {
     pub id: u32,
-    pub images: Vec<Image>,
+    pub image: Option<Image>,
     pub properties: Properties,
     pub objectgroup: Option<ObjectGroup>,
     pub animation: Option<Vec<Frame>>,
@@ -26,6 +26,7 @@ impl Tile {
     pub(crate) fn new<R: Read>(
         parser: &mut EventReader<R>,
         attrs: Vec<OwnedAttribute>,
+        path_relative_to: Option<&Path>,
     ) -> Result<Tile, TiledError> {
         let ((tile_type, probability), id) = get_attrs!(
             attrs,
@@ -39,13 +40,13 @@ impl Tile {
             TiledError::MalformedAttributes("tile must have an id with the correct type".to_string())
         );
 
-        let mut images = Vec::new();
+        let mut image = Option::None;
         let mut properties = HashMap::new();
         let mut objectgroup = None;
         let mut animation = None;
         parse_tag!(parser, "tile", {
             "image" => |attrs| {
-                images.push(Image::new(parser, attrs)?);
+                image = Some(Image::new(parser, attrs, path_relative_to.ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_owned()})?)?);
                 Ok(())
             },
             "properties" => |_| {
@@ -63,7 +64,7 @@ impl Tile {
         });
         Ok(Tile {
             id,
-            images,
+            image,
             properties,
             objectgroup,
             animation,
diff --git a/src/tileset.rs b/src/tileset.rs
index fe45353523aea6176296d6718ccd682e0688b647..2b120ecd9b53f4c45085cb9d75c29a0ac7571f45 100644
--- a/src/tileset.rs
+++ b/src/tileset.rs
@@ -1,11 +1,22 @@
+use std::collections::HashMap;
+use std::fs::File;
+use std::io::Read;
+use std::path::{Path, PathBuf};
+
+use xml::attribute::OwnedAttribute;
+use xml::reader::XmlEvent;
+use xml::EventReader;
+
+use crate::error::TiledError;
+use crate::image::Image;
 use crate::properties::{parse_properties, Properties};
+use crate::tile::Tile;
 use crate::util::*;
-use crate::*; // FIXME
 
 /// A tileset, usually the tilesheet image.
 #[derive(Debug, PartialEq, Clone)]
 pub struct Tileset {
-    /// The GID of the first tile stored
+    /// The GID of the first tile stored.
     pub first_gid: u32,
     pub name: String,
     pub tile_width: u32,
@@ -13,32 +24,120 @@ pub struct Tileset {
     pub spacing: u32,
     pub margin: u32,
     pub tilecount: Option<u32>,
-    /// The Tiled spec says that a tileset can have mutliple images so a `Vec`
-    /// is used. Usually you will only use one.
-    pub images: Vec<Image>,
+    pub columns: u32,
+
+    /// A tileset can either:
+    /// * have a single spritesheet `image` in `tileset` ("regular" tileset);
+    /// * have zero images in `tileset` and one `image` per `tile` ("image collection" tileset).
+    ///
+    /// --------
+    /// - Source: [tiled issue #2117](https://github.com/mapeditor/tiled/issues/2117)
+    /// - Source: [`columns` documentation](https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tileset)
+    pub image: Option<Image>,
     pub tiles: Vec<Tile>,
     pub properties: Properties,
+
+    /// Where this tileset was loaded from.
+    /// If fully embedded (loaded with path = `None`), this will return `None`.
+    pub source: Option<PathBuf>,
+}
+
+/// Internal structure for holding mid-parse information.
+struct TilesetProperties {
+    spacing: Option<u32>,
+    margin: Option<u32>,
+    tilecount: Option<u32>,
+    columns: Option<u32>,
+    first_gid: u32,
+    name: String,
+    tile_width: u32,
+    tile_height: u32,
+    path_relative_to: Option<PathBuf>,
+    source: Option<PathBuf>,
 }
 
 impl Tileset {
-    pub(crate) fn new<R: Read>(
+    /// Parse a buffer hopefully containing the contents of a Tiled tileset.
+    ///
+    /// External tilesets do not have a firstgid attribute.  That lives in the
+    /// map. You must pass in `first_gid`.  If you do not need to use gids for anything,
+    /// passing in 1 will work fine.
+    pub fn parse<R: Read>(reader: R, first_gid: u32) -> Result<Self, TiledError> {
+        Tileset::new_external(reader, first_gid, None)
+    }
+
+    /// Parse a buffer hopefully containing the contents of a Tiled tileset.
+    ///
+    /// External tilesets do not have a firstgid attribute.  That lives in the
+    /// map. You must pass in `first_gid`.  If you do not need to use gids for anything,
+    /// passing in 1 will work fine.
+    pub fn parse_with_path<R: Read>(
+        reader: R,
+        first_gid: u32,
+        path: impl AsRef<Path>,
+    ) -> Result<Self, TiledError> {
+        Tileset::new_external(reader, first_gid, Some(path.as_ref()))
+    }
+
+    pub(crate) fn parse_xml<R: Read>(
         parser: &mut EventReader<R>,
         attrs: Vec<OwnedAttribute>,
-        map_path: Option<&Path>,
+        path_relative_to: Option<&Path>,
     ) -> Result<Tileset, TiledError> {
-        Tileset::new_internal(parser, &attrs).or_else(|_| Tileset::new_reference(&attrs, map_path))
+        Tileset::parse_xml_embedded(parser, &attrs, path_relative_to).or_else(|err| {
+            if matches!(err, TiledError::MalformedAttributes(_)) {
+                Tileset::parse_xml_reference(&attrs, path_relative_to)
+            } else {
+                Err(err)
+            }
+        })
     }
 
-    fn new_internal<R: Read>(
+    pub(crate) fn new_external<R: Read>(
+        file: R,
+        first_gid: u32,
+        path: Option<&Path>,
+    ) -> Result<Self, TiledError> {
+        let mut tileset_parser = EventReader::new(file);
+        loop {
+            match tileset_parser
+                .next()
+                .map_err(TiledError::XmlDecodingError)?
+            {
+                XmlEvent::StartElement {
+                    name, attributes, ..
+                } => {
+                    if name.local_name == "tileset" {
+                        return Self::parse_external_tileset(
+                            first_gid,
+                            &mut tileset_parser,
+                            &attributes,
+                            path,
+                        );
+                    }
+                }
+                XmlEvent::EndDocument => {
+                    return Err(TiledError::PrematureEnd(
+                        "Tileset Document ended before map was parsed".to_string(),
+                    ))
+                }
+                _ => {}
+            }
+        }
+    }
+
+    fn parse_xml_embedded<R: Read>(
         parser: &mut EventReader<R>,
         attrs: &Vec<OwnedAttribute>,
+        path_relative_to: Option<&Path>,
     ) -> Result<Tileset, TiledError> {
-        let ((spacing, margin, tilecount), (first_gid, name, width, height)) = get_attrs!(
+        let ((spacing, margin, tilecount, columns), (first_gid, name, tile_width, tile_height)) = get_attrs!(
            attrs,
            optionals: [
                 ("spacing", spacing, |v:String| v.parse().ok()),
                 ("margin", margin, |v:String| v.parse().ok()),
                 ("tilecount", tilecount, |v:String| v.parse().ok()),
+                ("columns", columns, |v:String| v.parse().ok()),
             ],
            required: [
                 ("firstgid", first_gid, |v:String| v.parse().ok()),
@@ -49,41 +148,26 @@ impl Tileset {
             TiledError::MalformedAttributes("tileset must have a firstgid, name tile width and height with correct types".to_string())
         );
 
-        let mut images = Vec::new();
-        let mut tiles = Vec::new();
-        let mut properties = HashMap::new();
-        parse_tag!(parser, "tileset", {
-            "image" => |attrs| {
-                images.push(Image::new(parser, attrs)?);
-                Ok(())
-            },
-            "properties" => |_| {
-                properties = parse_properties(parser)?;
-                Ok(())
-            },
-            "tile" => |attrs| {
-                tiles.push(Tile::new(parser, attrs)?);
-                Ok(())
+        Self::finish_parsing_xml(
+            parser,
+            TilesetProperties {
+                spacing,
+                margin,
+                name,
+                path_relative_to: path_relative_to.map(Path::to_owned),
+                columns,
+                tilecount,
+                tile_height,
+                tile_width,
+                first_gid,
+                source: None,
             },
-        });
-
-        Ok(Tileset {
-            tile_width: width,
-            tile_height: height,
-            spacing: spacing.unwrap_or(0),
-            margin: margin.unwrap_or(0),
-            first_gid,
-            name,
-            tilecount,
-            images,
-            tiles,
-            properties,
-        })
+        )
     }
 
-    fn new_reference(
+    fn parse_xml_reference(
         attrs: &Vec<OwnedAttribute>,
-        map_path: Option<&Path>,
+        path_relative_to: Option<&Path>,
     ) -> Result<Tileset, TiledError> {
         let ((), (first_gid, source)) = get_attrs!(
             attrs,
@@ -92,58 +176,36 @@ impl Tileset {
                 ("firstgid", first_gid, |v:String| v.parse().ok()),
                 ("source", name, |v| Some(v)),
             ],
-            TiledError::MalformedAttributes("tileset must have a firstgid, name tile width and height with correct types".to_string())
+            TiledError::MalformedAttributes("Tileset reference must have a firstgid and source with correct types".to_string())
         );
 
-        let tileset_path = map_path.ok_or(TiledError::Other("Maps with external tilesets must know their file location.  See parse_with_path(Path).".to_string()))?.with_file_name(source);
+        let tileset_path = path_relative_to
+            .ok_or(TiledError::SourceRequired {
+                object_to_parse: "Tileset".to_string(),
+            })?
+            .join(source);
         let file = File::open(&tileset_path).map_err(|_| {
             TiledError::Other(format!(
                 "External tileset file not found: {:?}",
                 tileset_path
             ))
         })?;
-        Tileset::new_external(file, first_gid)
-    }
-
-    pub(crate) fn new_external<R: Read>(file: R, first_gid: u32) -> Result<Tileset, TiledError> {
-        let mut tileset_parser = EventReader::new(file);
-        loop {
-            match tileset_parser
-                .next()
-                .map_err(TiledError::XmlDecodingError)?
-            {
-                XmlEvent::StartElement {
-                    name, attributes, ..
-                } => {
-                    if name.local_name == "tileset" {
-                        return Tileset::parse_external_tileset(
-                            first_gid,
-                            &mut tileset_parser,
-                            &attributes,
-                        );
-                    }
-                }
-                XmlEvent::EndDocument => {
-                    return Err(TiledError::PrematureEnd(
-                        "Tileset Document ended before map was parsed".to_string(),
-                    ))
-                }
-                _ => {}
-            }
-        }
+        Tileset::new_external(file, first_gid, Some(&tileset_path))
     }
 
     fn parse_external_tileset<R: Read>(
         first_gid: u32,
         parser: &mut EventReader<R>,
         attrs: &Vec<OwnedAttribute>,
+        path: Option<&Path>,
     ) -> Result<Tileset, TiledError> {
-        let ((spacing, margin, tilecount), (name, width, height)) = get_attrs!(
+        let ((spacing, margin, tilecount, columns), (name, tile_width, tile_height)) = get_attrs!(
             attrs,
             optionals: [
                 ("spacing", spacing, |v:String| v.parse().ok()),
                 ("margin", margin, |v:String| v.parse().ok()),
                 ("tilecount", tilecount, |v:String| v.parse().ok()),
+                ("columns", columns, |v:String| v.parse().ok()),
             ],
             required: [
                 ("name", name, |v| Some(v)),
@@ -153,35 +215,81 @@ impl Tileset {
             TiledError::MalformedAttributes("tileset must have a firstgid, name tile width and height with correct types".to_string())
         );
 
-        let mut images = Vec::new();
+        let source_path = path.and_then(|p| p.parent().map(Path::to_owned));
+
+        Self::finish_parsing_xml(
+            parser,
+            TilesetProperties {
+                spacing,
+                margin,
+                name,
+                path_relative_to: source_path,
+                columns,
+                tilecount,
+                tile_height,
+                tile_width,
+                first_gid,
+                source: path.map(Path::to_owned),
+            },
+        )
+    }
+
+    fn finish_parsing_xml<R: Read>(
+        parser: &mut EventReader<R>,
+        prop: TilesetProperties,
+    ) -> Result<Self, TiledError> {
+        let mut image = Option::None;
         let mut tiles = Vec::new();
         let mut properties = HashMap::new();
         parse_tag!(parser, "tileset", {
             "image" => |attrs| {
-                images.push(Image::new(parser, attrs)?);
-                Ok(())
-            },
-            "tile" => |attrs| {
-                tiles.push(Tile::new(parser, attrs)?);
+                image = Some(Image::new(parser, attrs, prop.path_relative_to.as_ref().ok_or(TiledError::SourceRequired{object_to_parse: "Image".to_string()})?)?);
                 Ok(())
             },
             "properties" => |_| {
                 properties = parse_properties(parser)?;
                 Ok(())
             },
+            "tile" => |attrs| {
+                tiles.push(Tile::new(parser, attrs, prop.path_relative_to.as_ref().and_then(|p| Some(p.as_path())))?);
+                Ok(())
+            },
         });
 
+        let (margin, spacing) = (prop.margin.unwrap_or(0), prop.spacing.unwrap_or(0));
+
+        let columns = prop
+            .columns
+            .map(Ok)
+            .unwrap_or_else(|| Self::calculate_columns(&image, prop.tile_width, margin, spacing))?;
+
         Ok(Tileset {
-            first_gid: first_gid,
-            name: name,
-            tile_width: width,
-            tile_height: height,
-            spacing: spacing.unwrap_or(0),
-            margin: margin.unwrap_or(0),
-            tilecount: tilecount,
-            images: images,
-            tiles: tiles,
+            first_gid: prop.first_gid,
+            name: prop.name,
+            tile_width: prop.tile_width,
+            tile_height: prop.tile_height,
+            spacing,
+            margin,
+            columns,
+            tilecount: prop.tilecount,
+            image,
+            tiles,
             properties,
+            source: prop.source,
         })
     }
+
+    fn calculate_columns(
+        image: &Option<Image>,
+        tile_width: u32,
+        margin: u32,
+        spacing: u32,
+    ) -> Result<u32, TiledError> {
+        image
+            .as_ref()
+            .ok_or(TiledError::MalformedAttributes(
+                "No <image> nor columns attribute in <tileset>".to_string(),
+            ))
+            .and_then(|image| Ok((image.width as u32 - margin + spacing) / (tile_width + spacing)))
+    }
 }
diff --git a/src/util.rs b/src/util.rs
index 61dcfd1c24bf24cdbebaba82fe17da8e93d048b4..50d28e5ebf87d4282ad27ef3b52fb54c677331c2 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -26,9 +26,7 @@ macro_rules! get_attrs {
 }
 
 /// Goes through the children of the tag and will call the correct function for
-/// that child. Closes the tag
-///
-/// Not quite as bad.
+/// that child. Closes the tag.
 macro_rules! parse_tag {
     ($parser:expr, $close_tag:expr, {$($open_tag:expr => $open_method:expr),* $(,)*}) => {
         loop {
@@ -56,9 +54,7 @@ macro_rules! parse_tag {
 
 use std::{
     collections::HashMap,
-    fs::File,
     io::{BufReader, Read},
-    path::Path,
 };
 
 pub(crate) use get_attrs;
@@ -69,8 +65,6 @@ use crate::{
     animation::Frame,
     error::TiledError,
     layers::{Chunk, LayerData, LayerTile},
-    map::Map,
-    tileset::Tileset,
 };
 
 pub(crate) fn parse_animation<R: Read>(
@@ -89,7 +83,6 @@ pub(crate) fn parse_animation<R: Read>(
 pub(crate) fn parse_infinite_data<R: Read>(
     parser: &mut EventReader<R>,
     attrs: Vec<OwnedAttribute>,
-    width: u32,
 ) -> Result<LayerData, TiledError> {
     let ((e, c), ()) = get_attrs!(
         attrs,
@@ -278,24 +271,3 @@ pub(crate) fn convert_to_tile(all: &Vec<u8>, width: u32) -> Vec<Vec<LayerTile>>
     }
     data
 }
-
-pub(crate) fn parse_impl<R: Read>(reader: R, map_path: Option<&Path>) -> Result<Map, TiledError> {
-    let mut parser = EventReader::new(reader);
-    loop {
-        match parser.next().map_err(TiledError::XmlDecodingError)? {
-            XmlEvent::StartElement {
-                name, attributes, ..
-            } => {
-                if name.local_name == "map" {
-                    return Map::new(&mut parser, attributes, map_path);
-                }
-            }
-            XmlEvent::EndDocument => {
-                return Err(TiledError::PrematureEnd(
-                    "Document ended before map was parsed".to_string(),
-                ))
-            }
-            _ => {}
-        }
-    }
-}
\ No newline at end of file
diff --git a/tests/lib.rs b/tests/lib.rs
index c46a54e904eb5e9d15592fc458b3242ff0f66791..eb4aa6d7f5cb3381fbd695898d32e90b6013f4c0 100644
--- a/tests/lib.rs
+++ b/tests/lib.rs
@@ -1,26 +1,21 @@
-use std::fs::File;
 use std::path::Path;
+use std::{fs::File, path::PathBuf};
 use tiled::{
-    error::TiledError, layers::LayerData, map::Map, parse, parse_file, parse_tileset,
-    properties::PropertyValue,
+    error::TiledError, layers::LayerData, map::Map, properties::PropertyValue, tileset::Tileset,
 };
 
-fn read_from_file(p: &Path) -> Result<Map, TiledError> {
+fn parse_map_without_source(p: impl AsRef<Path>) -> Result<Map, TiledError> {
     let file = File::open(p).unwrap();
-    return parse(file);
-}
-
-fn read_from_file_with_path(p: &Path) -> Result<Map, TiledError> {
-    return parse_file(p);
+    return Map::parse_reader(file, None);
 }
 
 #[test]
 fn test_gzip_and_zlib_encoded_and_raw_are_the_same() {
-    let z = read_from_file(&Path::new("assets/tiled_base64_zlib.tmx")).unwrap();
-    let g = read_from_file(&Path::new("assets/tiled_base64_gzip.tmx")).unwrap();
-    let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap();
-    let zstd = read_from_file(&Path::new("assets/tiled_base64_zstandard.tmx")).unwrap();
-    let c = read_from_file(&Path::new("assets/tiled_csv.tmx")).unwrap();
+    let z = Map::parse_file("assets/tiled_base64_zlib.tmx").unwrap();
+    let g = Map::parse_file("assets/tiled_base64_gzip.tmx").unwrap();
+    let r = Map::parse_file("assets/tiled_base64.tmx").unwrap();
+    let zstd = Map::parse_file("assets/tiled_base64_zstandard.tmx").unwrap();
+    let c = Map::parse_file("assets/tiled_csv.tmx").unwrap();
     assert_eq!(z, g);
     assert_eq!(z, r);
     assert_eq!(z, c);
@@ -42,21 +37,36 @@ fn test_gzip_and_zlib_encoded_and_raw_are_the_same() {
 
 #[test]
 fn test_external_tileset() {
-    let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap();
-    let e = read_from_file_with_path(&Path::new("assets/tiled_base64_external.tmx")).unwrap();
+    let r = Map::parse_file("assets/tiled_base64.tmx").unwrap();
+    let mut e = Map::parse_file("assets/tiled_base64_external.tmx").unwrap();
+    e.tilesets[0].source = None;
     assert_eq!(r, e);
 }
 
+#[test]
+fn test_sources() {
+    let e = Map::parse_file("assets/tiled_base64_external.tmx").unwrap();
+    assert_eq!(
+        e.tilesets[0].source,
+        Some(PathBuf::from("assets/tilesheet.tsx"))
+    );
+    assert_eq!(
+        e.tilesets[0].image.as_ref().unwrap().source,
+        PathBuf::from("assets/tilesheet.png")
+    );
+}
+
 #[test]
 fn test_just_tileset() {
-    let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap();
-    let t = parse_tileset(File::open(Path::new("assets/tilesheet.tsx")).unwrap(), 1).unwrap();
+    let r = Map::parse_file("assets/tiled_base64_external.tmx").unwrap();
+    let path = "assets/tilesheet.tsx";
+    let t = Tileset::parse_with_path(File::open(path).unwrap(), 1, path).unwrap();
     assert_eq!(r.tilesets[0], t);
 }
 
 #[test]
 fn test_infinite_tileset() {
-    let r = read_from_file_with_path(&Path::new("assets/tiled_base64_zlib_infinite.tmx")).unwrap();
+    let r = Map::parse_file("assets/tiled_base64_zlib_infinite.tmx").unwrap();
 
     if let LayerData::Infinite(chunks) = &r.layers[0].tiles {
         assert_eq!(chunks.len(), 4);
@@ -73,7 +83,7 @@ fn test_infinite_tileset() {
 
 #[test]
 fn test_image_layers() {
-    let r = read_from_file(&Path::new("assets/tiled_image_layers.tmx")).unwrap();
+    let r = Map::parse_file("assets/tiled_image_layers.tmx").unwrap();
     assert_eq!(r.image_layers.len(), 2);
     {
         let first = &r.image_layers[0];
@@ -91,7 +101,7 @@ fn test_image_layers() {
             .image
             .as_ref()
             .expect(&format!("{}'s image shouldn't be None", second.name));
-        assert_eq!(image.source, "tilesheet.png");
+        assert_eq!(image.source, PathBuf::from("assets/tilesheet.png"));
         assert_eq!(image.width, 448);
         assert_eq!(image.height, 192);
     }
@@ -99,7 +109,7 @@ fn test_image_layers() {
 
 #[test]
 fn test_tile_property() {
-    let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap();
+    let r = Map::parse_file("assets/tiled_base64.tmx").unwrap();
     let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) =
         r.tilesets[0].tiles[0].properties.get("a tile property")
     {
@@ -112,20 +122,19 @@ fn test_tile_property() {
 
 #[test]
 fn test_layer_property() {
-    let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap();
-    let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) =
-        r.layers[0].properties.get("prop3")
-    {
-        v.clone()
-    } else {
-        String::new()
-    };
+    let r = Map::parse_file(&Path::new("assets/tiled_base64.tmx")).unwrap();
+    let prop_value: String =
+        if let Some(&PropertyValue::StringValue(ref v)) = r.layers[0].properties.get("prop3") {
+            v.clone()
+        } else {
+            String::new()
+        };
     assert_eq!("Line 1\r\nLine 2\r\nLine 3,\r\n  etc\r\n   ", prop_value);
 }
 
 #[test]
 fn test_object_group_property() {
-    let r = read_from_file(&Path::new("assets/tiled_object_groups.tmx")).unwrap();
+    let r = Map::parse_file("assets/tiled_object_groups.tmx").unwrap();
     let prop_value: bool = if let Some(&PropertyValue::BoolValue(ref v)) = r.object_groups[0]
         .properties
         .get("an object group property")
@@ -138,7 +147,7 @@ fn test_object_group_property() {
 }
 #[test]
 fn test_tileset_property() {
-    let r = read_from_file(&Path::new("assets/tiled_base64.tmx")).unwrap();
+    let r = Map::parse_file("assets/tiled_base64.tmx").unwrap();
     let prop_value: String = if let Some(&PropertyValue::StringValue(ref v)) =
         r.tilesets[0].properties.get("tileset property")
     {
@@ -151,7 +160,7 @@ fn test_tileset_property() {
 
 #[test]
 fn test_flipped_gid() {
-    let r = read_from_file_with_path(&Path::new("assets/tiled_flipped.tmx")).unwrap();
+    let r = Map::parse_file("assets/tiled_flipped.tmx").unwrap();
 
     if let LayerData::Finite(tiles) = &r.layers[0].tiles {
         let t1 = tiles[0][0];
@@ -180,7 +189,7 @@ fn test_flipped_gid() {
 
 #[test]
 fn test_ldk_export() {
-    let r = read_from_file_with_path(&Path::new("assets/ldk_tiled_export.tmx")).unwrap();
+    let r = Map::parse_file("assets/ldk_tiled_export.tmx").unwrap();
     if let LayerData::Finite(tiles) = &r.layers[0].tiles {
         assert_eq!(tiles.len(), 8);
         assert_eq!(tiles[0].len(), 8);
@@ -193,9 +202,10 @@ fn test_ldk_export() {
 
 #[test]
 fn test_object_property() {
-    let r = read_from_file(&Path::new("assets/tiled_object_property.tmx")).unwrap();
-    let prop_value = if let Some(PropertyValue::ObjectValue(v)) =
-        r.object_groups[0].objects[0].properties.get("object property")
+    let r = parse_map_without_source(&Path::new("assets/tiled_object_property.tmx")).unwrap();
+    let prop_value = if let Some(PropertyValue::ObjectValue(v)) = r.object_groups[0].objects[0]
+        .properties
+        .get("object property")
     {
         *v
     } else {