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