diff --git a/docs/Changelog.md b/docs/Changelog.md
index 76975796..2afb4e30 100644
--- a/docs/Changelog.md
+++ b/docs/Changelog.md
@@ -2,13 +2,16 @@
 
 ## New since 0.15.0
 
+- Android: scoped storage map / theme example [#804](https://github.com/mapsforge/vtm/pull/804)
+- Render theme from zip archive [#804](https://github.com/mapsforge/vtm/pull/804)
+- Render themes: custom resource providers [#804](https://github.com/mapsforge/vtm/pull/804)
 - Nautical unit adapter with feet [#803](https://github.com/mapsforge/vtm/pull/803)
 - Many other minor improvements and bug fixes
 - [Solved issues](https://github.com/mapsforge/vtm/issues?q=is%3Aclosed+milestone%3A0.16.0)
 
 ## Version 0.15.0 (2021-01-01)
 
-- Android: scoped storage example [#785](https://github.com/mapsforge/vtm/pull/785)
+- Android: scoped storage map example [#785](https://github.com/mapsforge/vtm/pull/785)
 - Mapsforge: map stream support [#784](https://github.com/mapsforge/vtm/pull/784)
 - Render theme from Android content providers [#783](https://github.com/mapsforge/vtm/pull/783)
 - Render theme xml pull parser [#786](https://github.com/mapsforge/vtm/pull/786)
diff --git a/vtm-android-example/res/menu/theme_menu.xml b/vtm-android-example/res/menu/theme_menu.xml
index 7a93959d..c32ba88d 100644
--- a/vtm-android-example/res/menu/theme_menu.xml
+++ b/vtm-android-example/res/menu/theme_menu.xml
@@ -20,6 +20,9 @@
         <item
             android:id="@+id/theme_newtron"
             android:title="@string/theme_newtron" />
+        <item
+            android:id="@+id/theme_external_archive"
+            android:title="@string/theme_external_archive" />
         <item
             android:id="@+id/theme_external"
             android:title="@string/theme_external" />
diff --git a/vtm-android-example/res/values/strings.xml b/vtm-android-example/res/values/strings.xml
index d4757468..fe1ea541 100644
--- a/vtm-android-example/res/values/strings.xml
+++ b/vtm-android-example/res/values/strings.xml
@@ -6,7 +6,8 @@
     <string name="theme_osmagray">Osmagray</string>
     <string name="theme_tubes">Tubes</string>
     <string name="theme_newtron">NewTron</string>
-    <string name="theme_external">External theme</string>
+    <string name="theme_external">External theme (Android 5)</string>
+    <string name="theme_external_archive">External theme archive</string>
     <string name="styler_mode_line">Line</string>
     <string name="styler_mode_area">Area</string>
     <string name="styler_mode_outline">Outline</string>
@@ -15,6 +16,7 @@
     <string name="style_2">Hide nature</string>
     <string name="menu_gridlayer">Grid</string>
     <string name="dialog_reverse_geocoding_title">Reverse Geocoding</string>
+    <string name="dialog_theme_title">Select a theme</string>
     <string name="add">Add</string>
     <string name="cancel">Cancel</string>
     <string name="error">Error</string>
diff --git a/vtm-android-example/src/org/oscim/android/test/MapsforgeActivity.java b/vtm-android-example/src/org/oscim/android/test/MapsforgeActivity.java
index 059706a3..f0adacbf 100644
--- a/vtm-android-example/src/org/oscim/android/test/MapsforgeActivity.java
+++ b/vtm-android-example/src/org/oscim/android/test/MapsforgeActivity.java
@@ -1,8 +1,9 @@
 /*
  * Copyright 2014 Hannes Janetzek
- * Copyright 2016-2020 devemux86
+ * Copyright 2016-2021 devemux86
  * Copyright 2017 Longri
  * Copyright 2018 Gustl22
+ * Copyright 2021 eddiemuc
  *
  * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
  *
@@ -20,13 +21,17 @@
 package org.oscim.android.test;
 
 import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.provider.DocumentsContract;
 import android.view.Menu;
 import android.view.MenuItem;
 import org.oscim.android.theme.ContentRenderTheme;
+import org.oscim.android.theme.ContentResolverResourceProvider;
 import org.oscim.backend.CanvasAdapter;
 import org.oscim.core.MapElement;
 import org.oscim.core.MapPosition;
@@ -42,9 +47,7 @@ import org.oscim.renderer.BitmapRenderer;
 import org.oscim.renderer.GLViewport;
 import org.oscim.renderer.bucket.RenderBuckets;
 import org.oscim.scalebar.*;
-import org.oscim.theme.IRenderTheme;
-import org.oscim.theme.ThemeFile;
-import org.oscim.theme.VtmThemes;
+import org.oscim.theme.*;
 import org.oscim.theme.styles.AreaStyle;
 import org.oscim.theme.styles.RenderStyle;
 import org.oscim.tiling.source.mapfile.MapFileTileSource;
@@ -52,15 +55,20 @@ import org.oscim.tiling.source.mapfile.MapInfo;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.BufferedInputStream;
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.util.List;
+import java.util.zip.ZipInputStream;
 
 public class MapsforgeActivity extends MapActivity {
 
     private static final Logger log = LoggerFactory.getLogger(MapsforgeActivity.class);
 
     static final int SELECT_MAP_FILE = 0;
-    static final int SELECT_THEME_FILE = 1;
+    private static final int SELECT_THEME_ARCHIVE = 1;
+    private static final int SELECT_THEME_DIR = 2;
+    static final int SELECT_THEME_FILE = 3;
 
     private static final Tag ISSEA_TAG = new Tag("natural", "issea");
     private static final Tag NOSEA_TAG = new Tag("natural", "nosea");
@@ -71,6 +79,7 @@ public class MapsforgeActivity extends MapActivity {
     private final boolean mS3db;
     IRenderTheme mTheme;
     VectorTileLayer mTileLayer;
+    private Uri mThemeDirUri;
 
     public MapsforgeActivity() {
         this(false);
@@ -142,11 +151,19 @@ public class MapsforgeActivity extends MapActivity {
                 item.setChecked(true);
                 return true;
 
-            case R.id.theme_external:
+            case R.id.theme_external_archive:
                 Intent intent = new Intent(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_GET_CONTENT);
                 intent.addCategory(Intent.CATEGORY_OPENABLE);
                 intent.setType("*/*");
-                startActivityForResult(intent, SELECT_THEME_FILE);
+                startActivityForResult(intent, SELECT_THEME_ARCHIVE);
+                return true;
+
+            case R.id.theme_external:
+                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
+                    return false;
+                intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
+                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                startActivityForResult(intent, SELECT_THEME_DIR);
                 return true;
 
             case R.id.gridlayer:
@@ -176,75 +193,100 @@ public class MapsforgeActivity extends MapActivity {
                 return;
             }
 
-            MapFileTileSource tileSource = new MapFileTileSource();
-            //tileSource.setPreferredLanguage("en");
+            try {
+                Uri uri = data.getData();
+
+                MapFileTileSource tileSource = new MapFileTileSource();
+                //tileSource.setPreferredLanguage("en");
+                FileInputStream fis = (FileInputStream) getContentResolver().openInputStream(uri);
+                tileSource.setMapFileInputStream(fis);
+
+                mTileLayer = mMap.setBaseMap(tileSource);
+                loadTheme(null);
+
+                if (mS3db)
+                    mMap.layers().add(new S3DBLayer(mMap, mTileLayer));
+                else
+                    mMap.layers().add(new BuildingLayer(mMap, mTileLayer));
+                mMap.layers().add(new LabelLayer(mMap, mTileLayer));
+
+                DefaultMapScaleBar mapScaleBar = new DefaultMapScaleBar(mMap);
+                mapScaleBar.setScaleBarMode(DefaultMapScaleBar.ScaleBarMode.BOTH);
+                mapScaleBar.setDistanceUnitAdapter(MetricUnitAdapter.INSTANCE);
+                mapScaleBar.setSecondaryDistanceUnitAdapter(ImperialUnitAdapter.INSTANCE);
+                mapScaleBar.setScaleBarPosition(MapScaleBar.ScaleBarPosition.BOTTOM_LEFT);
+
+                MapScaleBarLayer mapScaleBarLayer = new MapScaleBarLayer(mMap, mapScaleBar);
+                BitmapRenderer renderer = mapScaleBarLayer.getRenderer();
+                renderer.setPosition(GLViewport.Position.BOTTOM_LEFT);
+                renderer.setOffset(5 * CanvasAdapter.getScale(), 0);
+                mMap.layers().add(mapScaleBarLayer);
+
+                MapInfo info = tileSource.getMapInfo();
+                if (!info.boundingBox.contains(mMap.getMapPosition().getGeoPoint())) {
+                    MapPosition pos = new MapPosition();
+                    pos.setByBoundingBox(info.boundingBox, Tile.SIZE * 4, Tile.SIZE * 4);
+                    mMap.setMapPosition(pos);
+                    mPrefs.clear();
+                }
+            } catch (Exception e) {
+                log.error(e.getMessage());
+                finish();
+            }
+        } else if (requestCode == SELECT_THEME_ARCHIVE) {
+            if (resultCode != Activity.RESULT_OK || data == null)
+                return;
 
             try {
                 Uri uri = data.getData();
-                FileInputStream fis = (FileInputStream) getContentResolver().openInputStream(uri);
-                tileSource.setMapFileInputStream(fis);
+
+                final ZipXmlThemeResourceProvider resourceProvider = new ZipXmlThemeResourceProvider(new ZipInputStream(new BufferedInputStream(getContentResolver().openInputStream(uri))));
+                final List<String> xmlThemes = resourceProvider.getXmlThemes();
+                if (xmlThemes.isEmpty())
+                    return;
+
+                AlertDialog.Builder builder = new AlertDialog.Builder(this);
+                builder.setTitle(R.string.dialog_theme_title);
+                builder.setSingleChoiceItems(xmlThemes.toArray(new String[0]), -1, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int which) {
+                        dialog.dismiss();
+                        ThemeFile theme = new ZipRenderTheme(xmlThemes.get(which), resourceProvider);
+                        if (mTheme != null)
+                            mTheme.dispose();
+                        mTheme = mMap.setTheme(theme);
+                        mapsforgeTheme(mTheme);
+                        mMenu.findItem(R.id.theme_external_archive).setChecked(true);
+                    }
+                });
+                builder.show();
             } catch (IOException e) {
-                log.error(e.getMessage());
-                finish();
+                e.printStackTrace();
+            }
+        } else if (requestCode == SELECT_THEME_DIR) {
+            if (resultCode != Activity.RESULT_OK || data == null)
                 return;
-            }
 
-            mTileLayer = mMap.setBaseMap(tileSource);
-            loadTheme(null);
+            mThemeDirUri = data.getData();
 
-            if (mS3db)
-                mMap.layers().add(new S3DBLayer(mMap, mTileLayer));
-            else
-                mMap.layers().add(new BuildingLayer(mMap, mTileLayer));
-            mMap.layers().add(new LabelLayer(mMap, mTileLayer));
-
-            DefaultMapScaleBar mapScaleBar = new DefaultMapScaleBar(mMap);
-            mapScaleBar.setScaleBarMode(DefaultMapScaleBar.ScaleBarMode.BOTH);
-            mapScaleBar.setDistanceUnitAdapter(MetricUnitAdapter.INSTANCE);
-            mapScaleBar.setSecondaryDistanceUnitAdapter(ImperialUnitAdapter.INSTANCE);
-            mapScaleBar.setScaleBarPosition(MapScaleBar.ScaleBarPosition.BOTTOM_LEFT);
-
-            MapScaleBarLayer mapScaleBarLayer = new MapScaleBarLayer(mMap, mapScaleBar);
-            BitmapRenderer renderer = mapScaleBarLayer.getRenderer();
-            renderer.setPosition(GLViewport.Position.BOTTOM_LEFT);
-            renderer.setOffset(5 * CanvasAdapter.getScale(), 0);
-            mMap.layers().add(mapScaleBarLayer);
-
-            MapInfo info = tileSource.getMapInfo();
-            if (!info.boundingBox.contains(mMap.getMapPosition().getGeoPoint())) {
-                MapPosition pos = new MapPosition();
-                pos.setByBoundingBox(info.boundingBox, Tile.SIZE * 4, Tile.SIZE * 4);
-                mMap.setMapPosition(pos);
-                mPrefs.clear();
-            }
+            // Now we have the directory for resources, but we need to let the user also select a theme file
+            Intent intent = new Intent(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_GET_CONTENT);
+            intent.addCategory(Intent.CATEGORY_OPENABLE);
+            intent.setType("*/*");
+            intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, mThemeDirUri);
+            startActivityForResult(intent, SELECT_THEME_FILE);
         } else if (requestCode == SELECT_THEME_FILE) {
             if (resultCode != Activity.RESULT_OK || data == null)
                 return;
 
             Uri uri = data.getData();
-            ThemeFile theme = new ContentRenderTheme(getContentResolver(), "", uri);
-
-            // Use tessellation with sea and land for Mapsforge themes
-            if (theme.isMapsforgeTheme()) {
-                mTileLayer.addHook(new VectorTileLayer.TileLoaderThemeHook() {
-                    @Override
-                    public boolean process(MapTile tile, RenderBuckets buckets, MapElement element, RenderStyle style, int level) {
-                        if (element.tags.contains(ISSEA_TAG) || element.tags.contains(SEA_TAG) || element.tags.contains(NOSEA_TAG)) {
-                            if (style instanceof AreaStyle)
-                                ((AreaStyle) style).mesh = true;
-                        }
-                        return false;
-                    }
-
-                    @Override
-                    public void complete(MapTile tile, boolean success) {
-                    }
-                });
-            }
+            ThemeFile theme = new ContentRenderTheme(getContentResolver(), uri);
+            theme.setResourceProvider(new ContentResolverResourceProvider(getContentResolver(), mThemeDirUri));
 
             if (mTheme != null)
                 mTheme.dispose();
             mTheme = mMap.setTheme(theme);
+            mapsforgeTheme(mTheme);
             mMenu.findItem(R.id.theme_external).setChecked(true);
         }
     }
@@ -254,4 +296,25 @@ public class MapsforgeActivity extends MapActivity {
             mTheme.dispose();
         mTheme = mMap.setTheme(VtmThemes.DEFAULT);
     }
+
+    private void mapsforgeTheme(IRenderTheme theme) {
+        if (!theme.isMapsforgeTheme())
+            return;
+
+        // Use tessellation with sea and land for Mapsforge themes
+        mTileLayer.addHook(new VectorTileLayer.TileLoaderThemeHook() {
+            @Override
+            public boolean process(MapTile tile, RenderBuckets buckets, MapElement element, RenderStyle style, int level) {
+                if (element.tags.contains(ISSEA_TAG) || element.tags.contains(SEA_TAG) || element.tags.contains(NOSEA_TAG)) {
+                    if (style instanceof AreaStyle)
+                        ((AreaStyle) style).mesh = true;
+                }
+                return false;
+            }
+
+            @Override
+            public void complete(MapTile tile, boolean success) {
+            }
+        });
+    }
 }
diff --git a/vtm-android-example/src/org/oscim/android/test/Samples.java b/vtm-android-example/src/org/oscim/android/test/Samples.java
index 83c153cf..23ae7c7c 100644
--- a/vtm-android-example/src/org/oscim/android/test/Samples.java
+++ b/vtm-android-example/src/org/oscim/android/test/Samples.java
@@ -86,8 +86,8 @@ public class Samples extends Activity {
         LinearLayout linearLayout = findViewById(R.id.samples);
         linearLayout.addView(createButton(GettingStarted.class));
         linearLayout.addView(createLabel(null));
-        linearLayout.addView(createButton(SimpleMapActivity.class));
         linearLayout.addView(createButton(MapsforgeActivity.class));
+        linearLayout.addView(createButton(SimpleMapActivity.class));
         linearLayout.addView(createButton(MBTilesMvtActivity.class));
         linearLayout.addView(createButton(MapilionMvtActivity.class));
         /*linearLayout.addView(createButton(MapzenMvtActivity.class));
diff --git a/vtm-android/src/org/oscim/android/canvas/AndroidGraphics.java b/vtm-android/src/org/oscim/android/canvas/AndroidGraphics.java
index b608d592..81964b08 100644
--- a/vtm-android/src/org/oscim/android/canvas/AndroidGraphics.java
+++ b/vtm-android/src/org/oscim/android/canvas/AndroidGraphics.java
@@ -30,6 +30,7 @@ import org.oscim.backend.canvas.Canvas;
 import org.oscim.backend.canvas.Paint;
 import org.oscim.layers.marker.MarkerSymbol;
 import org.oscim.layers.marker.MarkerSymbol.HotspotPlace;
+import org.oscim.theme.XmlThemeResourceProvider;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -69,8 +70,8 @@ public final class AndroidGraphics extends CanvasAdapter {
     }
 
     @Override
-    public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, int width, int height, int percent) throws IOException {
-        return createBitmap(relativePathPrefix, src, width, height, percent);
+    public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException {
+        return createBitmap(relativePathPrefix, src, resourceProvider, width, height, percent);
     }
 
     @Override
diff --git a/vtm-android/src/org/oscim/android/theme/AssetsRenderTheme.java b/vtm-android/src/org/oscim/android/theme/AssetsRenderTheme.java
index ff969d49..9be12478 100644
--- a/vtm-android/src/org/oscim/android/theme/AssetsRenderTheme.java
+++ b/vtm-android/src/org/oscim/android/theme/AssetsRenderTheme.java
@@ -2,6 +2,7 @@
  * Copyright 2010, 2011, 2012 mapsforge.org
  * Copyright 2016-2021 devemux86
  * Copyright 2017 Andrey Novikov
+ * Copyright 2021 eddiemuc
  *
  * This program is free software: you can redistribute it and/or modify it under the
  * terms of the GNU Lesser General Public License as published by the Free Software
@@ -21,6 +22,7 @@ import android.text.TextUtils;
 import org.oscim.theme.IRenderTheme.ThemeException;
 import org.oscim.theme.ThemeFile;
 import org.oscim.theme.XmlRenderThemeMenuCallback;
+import org.oscim.theme.XmlThemeResourceProvider;
 import org.oscim.utils.Utils;
 
 import java.io.IOException;
@@ -38,6 +40,7 @@ public class AssetsRenderTheme implements ThemeFile {
     private boolean mMapsforgeTheme;
     private XmlRenderThemeMenuCallback mMenuCallback;
     private final String mRelativePathPrefix;
+    private XmlThemeResourceProvider mResourceProvider;
 
     /**
      * @param assetManager       the Android asset manager.
@@ -99,6 +102,11 @@ public class AssetsRenderTheme implements ThemeFile {
         }
     }
 
+    @Override
+    public XmlThemeResourceProvider getResourceProvider() {
+        return mResourceProvider;
+    }
+
     @Override
     public boolean isMapsforgeTheme() {
         return mMapsforgeTheme;
@@ -113,4 +121,9 @@ public class AssetsRenderTheme implements ThemeFile {
     public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) {
         mMenuCallback = menuCallback;
     }
+
+    @Override
+    public void setResourceProvider(XmlThemeResourceProvider resourceProvider) {
+        mResourceProvider = resourceProvider;
+    }
 }
diff --git a/vtm-android/src/org/oscim/android/theme/ContentRenderTheme.java b/vtm-android/src/org/oscim/android/theme/ContentRenderTheme.java
index d90e06a5..bf11eece 100644
--- a/vtm-android/src/org/oscim/android/theme/ContentRenderTheme.java
+++ b/vtm-android/src/org/oscim/android/theme/ContentRenderTheme.java
@@ -1,5 +1,6 @@
 /*
  * Copyright 2020-2021 devemux86
+ * Copyright 2021 eddiemuc
  *
  * This program is free software: you can redistribute it and/or modify it under the
  * terms of the GNU Lesser General Public License as published by the Free Software
@@ -19,7 +20,7 @@ import android.net.Uri;
 import org.oscim.theme.IRenderTheme.ThemeException;
 import org.oscim.theme.ThemeFile;
 import org.oscim.theme.XmlRenderThemeMenuCallback;
-import org.oscim.utils.Utils;
+import org.oscim.theme.XmlThemeResourceProvider;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -34,29 +35,26 @@ public class ContentRenderTheme implements ThemeFile {
     private final ContentResolver mContentResolver;
     private boolean mMapsforgeTheme;
     private XmlRenderThemeMenuCallback mMenuCallback;
-    private final String mRelativePathPrefix;
+    private XmlThemeResourceProvider mResourceProvider;
     private final Uri mUri;
 
     /**
-     * @param contentResolver    the Android content resolver.
-     * @param relativePathPrefix the prefix for all relative resource paths.
-     * @param uri                the XML render theme URI.
+     * @param contentResolver the Android content resolver.
+     * @param uri             the XML render theme URI.
      * @throws ThemeException if an error occurs while reading the render theme XML.
      */
-    public ContentRenderTheme(ContentResolver contentResolver, String relativePathPrefix, Uri uri) throws ThemeException {
-        this(contentResolver, relativePathPrefix, uri, null);
+    public ContentRenderTheme(ContentResolver contentResolver, Uri uri) throws ThemeException {
+        this(contentResolver, uri, null);
     }
 
     /**
-     * @param contentResolver    the Android content resolver.
-     * @param relativePathPrefix the prefix for all relative resource paths.
-     * @param uri                the XML render theme URI.
-     * @param menuCallback       the interface callback to create a settings menu on the fly.
+     * @param contentResolver the Android content resolver.
+     * @param uri             the XML render theme URI.
+     * @param menuCallback    the interface callback to create a settings menu on the fly.
      * @throws ThemeException if an error occurs while reading the render theme XML.
      */
-    public ContentRenderTheme(ContentResolver contentResolver, String relativePathPrefix, Uri uri, XmlRenderThemeMenuCallback menuCallback) throws ThemeException {
+    public ContentRenderTheme(ContentResolver contentResolver, Uri uri, XmlRenderThemeMenuCallback menuCallback) throws ThemeException {
         mContentResolver = contentResolver;
-        mRelativePathPrefix = relativePathPrefix;
         mUri = uri;
         mMenuCallback = menuCallback;
     }
@@ -72,9 +70,6 @@ public class ContentRenderTheme implements ThemeFile {
         if (getRenderThemeAsStream() != other.getRenderThemeAsStream()) {
             return false;
         }
-        if (!Utils.equals(mRelativePathPrefix, other.mRelativePathPrefix)) {
-            return false;
-        }
         return true;
     }
 
@@ -85,7 +80,7 @@ public class ContentRenderTheme implements ThemeFile {
 
     @Override
     public String getRelativePathPrefix() {
-        return mRelativePathPrefix;
+        return "";
     }
 
     @Override
@@ -97,6 +92,11 @@ public class ContentRenderTheme implements ThemeFile {
         }
     }
 
+    @Override
+    public XmlThemeResourceProvider getResourceProvider() {
+        return mResourceProvider;
+    }
+
     @Override
     public boolean isMapsforgeTheme() {
         return mMapsforgeTheme;
@@ -111,4 +111,9 @@ public class ContentRenderTheme implements ThemeFile {
     public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) {
         mMenuCallback = menuCallback;
     }
+
+    @Override
+    public void setResourceProvider(XmlThemeResourceProvider resourceProvider) {
+        mResourceProvider = resourceProvider;
+    }
 }
diff --git a/vtm-android/src/org/oscim/android/theme/ContentResolverResourceProvider.java b/vtm-android/src/org/oscim/android/theme/ContentResolverResourceProvider.java
new file mode 100644
index 00000000..d0bc3b20
--- /dev/null
+++ b/vtm-android/src/org/oscim/android/theme/ContentResolverResourceProvider.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2021 eddiemuc
+ *
+ * This program is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.oscim.android.theme;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.DocumentsContract;
+import org.oscim.backend.CanvasAdapter;
+import org.oscim.theme.XmlThemeResourceProvider;
+import org.oscim.utils.IOUtils;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.*;
+
+/**
+ * An xml theme resource provider resolving resources using Android scoped storage (document framework).
+ * <p>
+ * Implementation note: these methods do not use DocumentFile internally,
+ * but query directly for document info due to vastly better performance.
+ * Also for better performance, this implementation caches resource uris.
+ * <p>
+ * Note: this implementation requires minimum Android 5.0 (API 21)
+ */
+public class ContentResolverResourceProvider implements XmlThemeResourceProvider {
+
+    private final ContentResolver contentResolver;
+    private final Uri relativeRootUri;
+
+    private final Map<String, Uri> resourceUriCache = new HashMap<>();
+
+    private static class DocumentInfo {
+        private final String name;
+        private final Uri uri;
+        private final boolean isDirectory;
+
+        private DocumentInfo(String name, Uri uri, boolean isDirectory) {
+            this.name = name;
+            this.uri = uri;
+            this.isDirectory = isDirectory;
+        }
+    }
+
+    public ContentResolverResourceProvider(ContentResolver contentResolver, Uri treeUri) {
+        this.contentResolver = contentResolver;
+        this.relativeRootUri = treeUri;
+
+        refreshCache();
+    }
+
+    /**
+     * Build uri cache for one dir level (recursive function).
+     */
+    private void buildCacheLevel(String prefix, Uri dirUri) {
+        List<DocumentInfo> docs = queryDir(dirUri);
+        for (DocumentInfo doc : docs) {
+            if (doc.isDirectory) {
+                buildCacheLevel(prefix + doc.name + "/", doc.uri);
+            } else {
+                resourceUriCache.put(prefix + doc.name, doc.uri);
+            }
+        }
+    }
+
+    @Override
+    public InputStream createInputStream(String relativePath, String source) throws FileNotFoundException {
+        Uri docUri = resourceUriCache.get(source);
+        if (docUri != null) {
+            return contentResolver.openInputStream(docUri);
+        }
+        return null;
+    }
+
+    /**
+     * Query the content of a directory using scoped storage.
+     *
+     * @return a list of arrays with info [0: name (String), 1: uri (Uri), 2: isDir (boolean)]
+     */
+    private List<DocumentInfo> queryDir(Uri dirUri) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            return Collections.emptyList();
+        }
+        if (dirUri == null) {
+            return Collections.emptyList();
+        }
+
+        List<DocumentInfo> result = new ArrayList<>();
+        Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(dirUri, DocumentsContract.getDocumentId(dirUri));
+
+        String[] columns = new String[]{
+                DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+                DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+                DocumentsContract.Document.COLUMN_MIME_TYPE
+        };
+
+        Cursor c = null;
+        try {
+            c = contentResolver.query(childrenUri, columns, null, null, null);
+
+            while (c.moveToNext()) {
+                String documentId = c.getString(0);
+                String name = c.getString(1);
+                String mimeType = c.getString(2);
+
+                Uri uri = DocumentsContract.buildDocumentUriUsingTree(dirUri, documentId);
+                boolean isDir = DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType);
+                result.add(new DocumentInfo(name, uri, isDir));
+            }
+
+            return result;
+        } finally {
+            IOUtils.closeQuietly(c);
+        }
+    }
+
+    /**
+     * Refresh the uri cache by recreating it.
+     */
+    private void refreshCache() {
+        resourceUriCache.clear();
+
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            return;
+        }
+        if (relativeRootUri == null) {
+            return;
+        }
+
+        Uri dirUri = DocumentsContract.buildDocumentUriUsingTree(relativeRootUri, DocumentsContract.getTreeDocumentId(relativeRootUri));
+        buildCacheLevel(CanvasAdapter.PREFIX_FILE, dirUri);
+    }
+}
diff --git a/vtm-desktop/src/org/oscim/awt/AwtGraphics.java b/vtm-desktop/src/org/oscim/awt/AwtGraphics.java
index 54aa6da5..e25a4d1f 100644
--- a/vtm-desktop/src/org/oscim/awt/AwtGraphics.java
+++ b/vtm-desktop/src/org/oscim/awt/AwtGraphics.java
@@ -23,11 +23,9 @@ import org.oscim.backend.Platform;
 import org.oscim.backend.canvas.Bitmap;
 import org.oscim.backend.canvas.Canvas;
 import org.oscim.backend.canvas.Paint;
+import org.oscim.theme.XmlThemeResourceProvider;
 
-import java.awt.Font;
-import java.awt.FontMetrics;
-import java.awt.Graphics2D;
-import java.awt.RenderingHints;
+import java.awt.*;
 import java.awt.image.BufferedImage;
 import java.io.IOException;
 import java.io.InputStream;
@@ -119,7 +117,7 @@ public class AwtGraphics extends CanvasAdapter {
     }
 
     @Override
-    public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, int width, int height, int percent) throws IOException {
-        return createBitmap(relativePathPrefix, src, width, height, percent);
+    public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException {
+        return createBitmap(relativePathPrefix, src, resourceProvider, width, height, percent);
     }
 }
diff --git a/vtm-ios-example/src/org/oscim/ios/test/IOSPathLayerTest.java b/vtm-ios-example/src/org/oscim/ios/test/IOSPathLayerTest.java
index bae128f2..cfd7f4a5 100644
--- a/vtm-ios-example/src/org/oscim/ios/test/IOSPathLayerTest.java
+++ b/vtm-ios-example/src/org/oscim/ios/test/IOSPathLayerTest.java
@@ -19,7 +19,6 @@
 package org.oscim.ios.test;
 
 import com.badlogic.gdx.graphics.glutils.GLVersion;
-
 import org.oscim.backend.GLAdapter;
 import org.oscim.backend.canvas.Color;
 import org.oscim.core.GeoPoint;
@@ -74,7 +73,7 @@ public class IOSPathLayerTest extends GdxMap {
 
         mMap.setMapPosition(0, 0, 1 << 2);
 
-        tex = Utils.loadTexture("", "patterns/pike.png", 0, 0, 100);
+        tex = Utils.loadTexture("", "patterns/pike.png", null, 0, 0, 100);
 //        tex = new TextureItem(CanvasAdapter.getBitmapAsset("", "patterns/pike.png"));
         tex.mipmap = true;
 
diff --git a/vtm-ios/src/org/oscim/ios/backend/IosGraphics.java b/vtm-ios/src/org/oscim/ios/backend/IosGraphics.java
index 253fe649..39a57492 100644
--- a/vtm-ios/src/org/oscim/ios/backend/IosGraphics.java
+++ b/vtm-ios/src/org/oscim/ios/backend/IosGraphics.java
@@ -21,6 +21,7 @@ import org.oscim.backend.Platform;
 import org.oscim.backend.canvas.Bitmap;
 import org.oscim.backend.canvas.Canvas;
 import org.oscim.backend.canvas.Paint;
+import org.oscim.theme.XmlThemeResourceProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -70,7 +71,7 @@ public class IosGraphics extends CanvasAdapter {
     }
 
     @Override
-    protected Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, int width, int height, int percent) throws IOException {
-        return createBitmap(relativePathPrefix, src, width, height, percent);
+    protected Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException {
+        return createBitmap(relativePathPrefix, src, resourceProvider, width, height, percent);
     }
 }
diff --git a/vtm-tests/build.gradle b/vtm-tests/build.gradle
index cee19e71..a2bc7ddd 100644
--- a/vtm-tests/build.gradle
+++ b/vtm-tests/build.gradle
@@ -13,4 +13,5 @@ dependencies {
 sourceSets {
     main.java.srcDirs = ['src']
     test.java.srcDirs = ['test']
+    test.resources.srcDirs = ['resources']
 }
diff --git a/vtm-tests/resources/xmlthemetest.zip b/vtm-tests/resources/xmlthemetest.zip
new file mode 100644
index 00000000..e3921de9
Binary files /dev/null and b/vtm-tests/resources/xmlthemetest.zip differ
diff --git a/vtm-tests/test/org/oscim/theme/ZipXmlThemeResourceProviderTest.java b/vtm-tests/test/org/oscim/theme/ZipXmlThemeResourceProviderTest.java
new file mode 100644
index 00000000..b1563781
--- /dev/null
+++ b/vtm-tests/test/org/oscim/theme/ZipXmlThemeResourceProviderTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2021 eddiemuc
+ * Copyright 2021 devemux86
+ *
+ * This program is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.oscim.theme;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.oscim.utils.IOUtils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.zip.ZipInputStream;
+
+public class ZipXmlThemeResourceProviderTest {
+
+    @Test
+    public void openZip() throws IOException {
+        ZipInputStream zis = new ZipInputStream(new BufferedInputStream(ZipXmlThemeResourceProviderTest.class.getResourceAsStream("/xmlthemetest.zip")));
+        Assert.assertNotNull(zis);
+
+        ZipXmlThemeResourceProvider zts = new ZipXmlThemeResourceProvider(zis);
+
+        // All files contained
+        Assert.assertNotNull(zts.createInputStream(null, "file:one.xml"));
+        Assert.assertNotNull(zts.createInputStream(null, "file:two.xml"));
+        Assert.assertNotNull(zts.createInputStream(null, "file:res/three.xml"));
+        Assert.assertNotNull(zts.createInputStream(null, "file:res/blue_star_1.svg"));
+        Assert.assertNotNull(zts.createInputStream(null, "file:res/test.txt"));
+        Assert.assertNotNull(zts.createInputStream(null, "file:res/sub/four.xml"));
+        Assert.assertNotNull(zts.createInputStream(null, "file:res/sub/blue_star_sub_1.svg"));
+        Assert.assertNotNull(zts.createInputStream(null, "file:res/sub/blue_star_sub_2.svg"));
+
+        //Relative Reference ok
+        Assert.assertNotNull(zts.createInputStream("", "file:res/sub/blue_star_sub_2.svg"));
+        Assert.assertNotNull(zts.createInputStream("res", "file:sub/blue_star_sub_2.svg"));
+        Assert.assertNotNull(zts.createInputStream("/", "file:res/sub/blue_star_sub_2.svg"));
+        Assert.assertNotNull(zts.createInputStream("/res", "file:sub/blue_star_sub_2.svg"));
+        Assert.assertNotNull(zts.createInputStream("res/", "file:/sub/blue_star_sub_2.svg"));
+
+        // Can get same files using various other formats
+        Assert.assertNotNull(zts.createInputStream(null, "res/sub/blue_star_sub_2.svg"));
+        Assert.assertNotNull(zts.createInputStream(null, "/res/sub/blue_star_sub_2.svg"));
+        Assert.assertNotNull(zts.createInputStream(null, "file:/res/sub/blue_star_sub_2.svg"));
+
+        // Dirs NOT contained!
+        Assert.assertNull(zts.createInputStream(null, "file:res/"));
+
+        Assert.assertEquals(8, zts.getCount());
+
+        List<String> xmlThemes = zts.getXmlThemes();
+        Assert.assertEquals(4, xmlThemes.size());
+        Assert.assertTrue(xmlThemes.contains("one.xml"));
+        Assert.assertTrue(xmlThemes.contains("two.xml"));
+        Assert.assertTrue(xmlThemes.contains("res/three.xml"));
+        Assert.assertTrue(xmlThemes.contains("res/sub/four.xml"));
+
+        BufferedReader reader = null;
+        try {
+            reader = new BufferedReader(new InputStreamReader(zts.createInputStream(null, "file:res/test.txt")));
+            String line = reader.readLine();
+            Assert.assertEquals(line, "This is a test");
+        } finally {
+            IOUtils.closeQuietly(reader);
+        }
+    }
+
+    @Test
+    public void openEmpty() throws IOException {
+        Assert.assertTrue(new ZipXmlThemeResourceProvider(null).getXmlThemes().isEmpty());
+    }
+}
diff --git a/vtm-themes/src/org/oscim/theme/VtmThemes.java b/vtm-themes/src/org/oscim/theme/VtmThemes.java
index c5ce9f45..ccca4449 100644
--- a/vtm-themes/src/org/oscim/theme/VtmThemes.java
+++ b/vtm-themes/src/org/oscim/theme/VtmThemes.java
@@ -4,6 +4,7 @@
  * Copyright 2016-2021 devemux86
  * Copyright 2017 nebular
  * Copyright 2017 Andrey Novikov
+ * Copyright 2021 eddiemuc
  *
  * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
  *
@@ -59,6 +60,11 @@ public enum VtmThemes implements ThemeFile {
         return AssetAdapter.readFileAsStream(mPath);
     }
 
+    @Override
+    public XmlThemeResourceProvider getResourceProvider() {
+        return null;
+    }
+
     @Override
     public boolean isMapsforgeTheme() {
         return false;
@@ -71,4 +77,8 @@ public enum VtmThemes implements ThemeFile {
     @Override
     public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) {
     }
+
+    @Override
+    public void setResourceProvider(XmlThemeResourceProvider resourceProvider) {
+    }
 }
diff --git a/vtm-web/src/org/oscim/gdx/client/GwtGdxGraphics.java b/vtm-web/src/org/oscim/gdx/client/GwtGdxGraphics.java
index 270b3419..917f2fa9 100644
--- a/vtm-web/src/org/oscim/gdx/client/GwtGdxGraphics.java
+++ b/vtm-web/src/org/oscim/gdx/client/GwtGdxGraphics.java
@@ -21,11 +21,11 @@ package org.oscim.gdx.client;
 import com.google.gwt.canvas.client.Canvas;
 import com.google.gwt.canvas.dom.client.Context2d;
 import com.google.gwt.canvas.dom.client.TextMetrics;
-
 import org.oscim.backend.CanvasAdapter;
 import org.oscim.backend.Platform;
 import org.oscim.backend.canvas.Bitmap;
 import org.oscim.backend.canvas.Paint;
+import org.oscim.theme.XmlThemeResourceProvider;
 
 import java.io.File;
 import java.io.InputStream;
@@ -68,7 +68,7 @@ public class GwtGdxGraphics extends CanvasAdapter {
     }
 
     @Override
-    public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, int width, int height, int percent) {
+    public Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) {
         String pathName = (relativePathPrefix == null || relativePathPrefix.length() == 0 ? "" : relativePathPrefix + File.separatorChar) + src;
         return new GwtBitmap(pathName);
     }
diff --git a/vtm/src/org/oscim/backend/CanvasAdapter.java b/vtm/src/org/oscim/backend/CanvasAdapter.java
index 04a0c273..0eae14c9 100644
--- a/vtm/src/org/oscim/backend/CanvasAdapter.java
+++ b/vtm/src/org/oscim/backend/CanvasAdapter.java
@@ -1,7 +1,8 @@
 /*
  * Copyright 2013 Hannes Janetzek
- * Copyright 2016-2020 devemux86
+ * Copyright 2016-2021 devemux86
  * Copyright 2017 Longri
+ * Copyright 2021 eddiemuc
  *
  * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
  *
@@ -21,6 +22,7 @@ package org.oscim.backend;
 import org.oscim.backend.canvas.Bitmap;
 import org.oscim.backend.canvas.Canvas;
 import org.oscim.backend.canvas.Paint;
+import org.oscim.theme.XmlThemeResourceProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -37,7 +39,7 @@ public abstract class CanvasAdapter {
     private static final Logger log = LoggerFactory.getLogger(CanvasAdapter.class);
 
     private static final String PREFIX_ASSETS = "assets:";
-    private static final String PREFIX_FILE = "file:";
+    public static final String PREFIX_FILE = "file:";
 
     /**
      * The instance provided by backend
@@ -156,34 +158,45 @@ public abstract class CanvasAdapter {
      * @param src                the resource
      * @return the bitmap
      */
-    protected abstract Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, int width, int height, int percent) throws IOException;
+    protected abstract Bitmap loadBitmapAssetImpl(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException;
 
     public static Bitmap getBitmapAsset(String relativePathPrefix, String src) throws IOException {
-        return getBitmapAsset(relativePathPrefix, src, 0, 0, 100);
+        return getBitmapAsset(relativePathPrefix, src, null, 0, 0, 100);
     }
 
-    public static Bitmap getBitmapAsset(String relativePathPrefix, String src, int width, int height, int percent) throws IOException {
-        return g.loadBitmapAssetImpl(relativePathPrefix, src, width, height, percent);
+    public static Bitmap getBitmapAsset(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException {
+        return g.loadBitmapAssetImpl(relativePathPrefix, src, resourceProvider, width, height, percent);
     }
 
-    protected static Bitmap createBitmap(String relativePathPrefix, String src, int width, int height, int percent) throws IOException {
+    protected static Bitmap createBitmap(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) throws IOException {
         if (src == null || src.length() == 0) {
             // no image source defined
             return null;
         }
 
-        InputStream inputStream;
-        if (src.startsWith(PREFIX_ASSETS)) {
-            src = src.substring(PREFIX_ASSETS.length());
-            inputStream = inputStreamFromAssets(relativePathPrefix, src);
-        } else if (src.startsWith(PREFIX_FILE)) {
-            src = src.substring(PREFIX_FILE.length());
-            inputStream = inputStreamFromFile(relativePathPrefix, src);
-        } else {
-            inputStream = inputStreamFromFile(relativePathPrefix, src);
+        InputStream inputStream = null;
+        if (resourceProvider != null) {
+            try {
+                inputStream = resourceProvider.createInputStream(relativePathPrefix, src);
+            } catch (IOException ioe) {
+                log.debug("Exception trying to access resource: " + src + " using custom provider: " + ioe);
+                // Ignore and try to resolve input stream using the standard process
+            }
+        }
 
-            if (inputStream == null)
+        if (inputStream == null) {
+            if (src.startsWith(PREFIX_ASSETS)) {
+                src = src.substring(PREFIX_ASSETS.length());
                 inputStream = inputStreamFromAssets(relativePathPrefix, src);
+            } else if (src.startsWith(PREFIX_FILE)) {
+                src = src.substring(PREFIX_FILE.length());
+                inputStream = inputStreamFromFile(relativePathPrefix, src);
+            } else {
+                inputStream = inputStreamFromFile(relativePathPrefix, src);
+
+                if (inputStream == null)
+                    inputStream = inputStreamFromAssets(relativePathPrefix, src);
+            }
         }
 
         // Fallback to internal resources
diff --git a/vtm/src/org/oscim/theme/ExternalRenderTheme.java b/vtm/src/org/oscim/theme/ExternalRenderTheme.java
index 58e741b3..e1efca28 100644
--- a/vtm/src/org/oscim/theme/ExternalRenderTheme.java
+++ b/vtm/src/org/oscim/theme/ExternalRenderTheme.java
@@ -3,6 +3,7 @@
  * Copyright 2013 Hannes Janetzek
  * Copyright 2016-2021 devemux86
  * Copyright 2017 Andrey Novikov
+ * Copyright 2021 eddiemuc
  *
  * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
  *
@@ -37,6 +38,7 @@ public class ExternalRenderTheme implements ThemeFile {
     private boolean mMapsforgeTheme;
     private XmlRenderThemeMenuCallback mMenuCallback;
     private final String mPath;
+    private XmlThemeResourceProvider mResourceProvider;
 
     /**
      * @param fileName the path to the XML render theme file.
@@ -109,6 +111,11 @@ public class ExternalRenderTheme implements ThemeFile {
         return is;
     }
 
+    @Override
+    public XmlThemeResourceProvider getResourceProvider() {
+        return mResourceProvider;
+    }
+
     @Override
     public boolean isMapsforgeTheme() {
         return mMapsforgeTheme;
@@ -123,4 +130,9 @@ public class ExternalRenderTheme implements ThemeFile {
     public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) {
         mMenuCallback = menuCallback;
     }
+
+    @Override
+    public void setResourceProvider(XmlThemeResourceProvider resourceProvider) {
+        mResourceProvider = resourceProvider;
+    }
 }
diff --git a/vtm/src/org/oscim/theme/StreamRenderTheme.java b/vtm/src/org/oscim/theme/StreamRenderTheme.java
index 00ad4522..17a88a20 100644
--- a/vtm/src/org/oscim/theme/StreamRenderTheme.java
+++ b/vtm/src/org/oscim/theme/StreamRenderTheme.java
@@ -1,6 +1,7 @@
 /*
  * Copyright 2016-2021 devemux86
  * Copyright 2017 Andrey Novikov
+ * Copyright 2021 eddiemuc
  *
  * This program is free software: you can redistribute it and/or modify it under the
  * terms of the GNU Lesser General Public License as published by the Free Software
@@ -31,12 +32,14 @@ public class StreamRenderTheme implements ThemeFile {
     private boolean mMapsforgeTheme;
     private XmlRenderThemeMenuCallback mMenuCallback;
     private final String mRelativePathPrefix;
+    private XmlThemeResourceProvider mResourceProvider;
 
     /**
      * @param relativePathPrefix the prefix for all relative resource paths.
      * @param inputStream        an input stream containing valid render theme XML data.
+     * @throws ThemeException if an error occurs while reading the render theme XML.
      */
-    public StreamRenderTheme(String relativePathPrefix, InputStream inputStream) {
+    public StreamRenderTheme(String relativePathPrefix, InputStream inputStream) throws ThemeException {
         this(relativePathPrefix, inputStream, null);
     }
 
@@ -44,8 +47,9 @@ public class StreamRenderTheme implements ThemeFile {
      * @param relativePathPrefix the prefix for all relative resource paths.
      * @param inputStream        an input stream containing valid render theme XML data.
      * @param menuCallback       the interface callback to create a settings menu on the fly.
+     * @throws ThemeException if an error occurs while reading the render theme XML.
      */
-    public StreamRenderTheme(String relativePathPrefix, InputStream inputStream, XmlRenderThemeMenuCallback menuCallback) {
+    public StreamRenderTheme(String relativePathPrefix, InputStream inputStream, XmlRenderThemeMenuCallback menuCallback) throws ThemeException {
         mRelativePathPrefix = relativePathPrefix;
         mInputStream = inputStream;
         mMenuCallback = menuCallback;
@@ -83,6 +87,11 @@ public class StreamRenderTheme implements ThemeFile {
         return mInputStream;
     }
 
+    @Override
+    public XmlThemeResourceProvider getResourceProvider() {
+        return mResourceProvider;
+    }
+
     @Override
     public boolean isMapsforgeTheme() {
         return mMapsforgeTheme;
@@ -97,4 +106,9 @@ public class StreamRenderTheme implements ThemeFile {
     public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) {
         mMenuCallback = menuCallback;
     }
+
+    @Override
+    public void setResourceProvider(XmlThemeResourceProvider resourceProvider) {
+        mResourceProvider = resourceProvider;
+    }
 }
diff --git a/vtm/src/org/oscim/theme/ThemeFile.java b/vtm/src/org/oscim/theme/ThemeFile.java
index 077fc1d5..dc47b2de 100644
--- a/vtm/src/org/oscim/theme/ThemeFile.java
+++ b/vtm/src/org/oscim/theme/ThemeFile.java
@@ -3,6 +3,7 @@
  * Copyright 2013 Hannes Janetzek
  * Copyright 2016-2021 devemux86
  * Copyright 2017 Andrey Novikov
+ * Copyright 2021 eddiemuc
  *
  * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
  *
@@ -44,6 +45,11 @@ public interface ThemeFile extends Serializable {
      */
     InputStream getRenderThemeAsStream() throws ThemeException;
 
+    /**
+     * @return a custom provider to retrieve resources internally referenced by "src" attribute (e.g. images, icons).
+     */
+    XmlThemeResourceProvider getResourceProvider();
+
     /**
      * Tells ThemeLoader if theme file is in Mapsforge format
      *
@@ -60,4 +66,9 @@ public interface ThemeFile extends Serializable {
      * @param menuCallback the interface callback to create a settings menu on the fly.
      */
     void setMenuCallback(XmlRenderThemeMenuCallback menuCallback);
+
+    /**
+     * @param resourceProvider a custom provider to retrieve resources internally referenced by "src" attribute (e.g. images, icons).
+     */
+    void setResourceProvider(XmlThemeResourceProvider resourceProvider);
 }
diff --git a/vtm/src/org/oscim/theme/XmlThemeBuilder.java b/vtm/src/org/oscim/theme/XmlThemeBuilder.java
index 9e2743b2..ee6712c3 100644
--- a/vtm/src/org/oscim/theme/XmlThemeBuilder.java
+++ b/vtm/src/org/oscim/theme/XmlThemeBuilder.java
@@ -8,6 +8,7 @@
  * Copyright 2018-2019 Gustl22
  * Copyright 2018 Izumi Kawashima
  * Copyright 2019 Murray Hughes
+ * Copyright 2021 eddiemuc
  *
  * This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
  *
@@ -664,7 +665,7 @@ public class XmlThemeBuilder {
         } else {
             if (src != null) {
                 float symbolScale = Parameters.SYMBOL_SCALING == Parameters.SymbolScaling.ALL ? CanvasAdapter.symbolScale : 1;
-                b.texture = Utils.loadTexture(mTheme.getRelativePathPrefix(), src, b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * symbolScale));
+                b.texture = Utils.loadTexture(mTheme.getRelativePathPrefix(), src, mTheme.getResourceProvider(), b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * symbolScale));
             }
 
             if (b.texture != null && hasSymbol) {
@@ -777,7 +778,7 @@ public class XmlThemeBuilder {
         }
 
         if (src != null)
-            b.texture = Utils.loadTexture(mTheme.getRelativePathPrefix(), src, b.symbolWidth, b.symbolHeight, b.symbolPercent);
+            b.texture = Utils.loadTexture(mTheme.getRelativePathPrefix(), src, mTheme.getResourceProvider(), b.symbolWidth, b.symbolHeight, b.symbolPercent);
 
         return b.build();
     }
@@ -1103,7 +1104,7 @@ public class XmlThemeBuilder {
             String lowValue = symbol.toLowerCase(Locale.ENGLISH);
             if (lowValue.endsWith(".png") || lowValue.endsWith(".svg")) {
                 try {
-                    b.bitmap = CanvasAdapter.getBitmapAsset(mTheme.getRelativePathPrefix(), symbol, b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * CanvasAdapter.symbolScale));
+                    b.bitmap = CanvasAdapter.getBitmapAsset(mTheme.getRelativePathPrefix(), symbol, mTheme.getResourceProvider(), b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * CanvasAdapter.symbolScale));
                 } catch (Exception e) {
                     log.error("{}: {}", symbol, e.getMessage());
                 }
@@ -1257,7 +1258,7 @@ public class XmlThemeBuilder {
                             symbolScale = CanvasAdapter.symbolScale;
                         break;
                 }
-                Bitmap bitmap = CanvasAdapter.getBitmapAsset(mTheme.getRelativePathPrefix(), b.src, b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * symbolScale));
+                Bitmap bitmap = CanvasAdapter.getBitmapAsset(mTheme.getRelativePathPrefix(), b.src, mTheme.getResourceProvider(), b.symbolWidth, b.symbolHeight, (int) (b.symbolPercent * symbolScale));
                 if (bitmap != null)
                     return buildSymbol(b, b.src, bitmap);
             } catch (Exception e) {
diff --git a/vtm/src/org/oscim/theme/XmlThemeResourceProvider.java b/vtm/src/org/oscim/theme/XmlThemeResourceProvider.java
new file mode 100644
index 00000000..7596e772
--- /dev/null
+++ b/vtm/src/org/oscim/theme/XmlThemeResourceProvider.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2021 eddiemuc
+ *
+ * This program is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.oscim.theme;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Interface for a provider of resources referenced inside XML themes.
+ */
+public interface XmlThemeResourceProvider {
+
+    /**
+     * @param relativePath a relative path to use as a base for search in the resource provuider
+     * @param source       a source string parsed out of an XML render theme "src" attribute.
+     * @return an InputStream to read the resource data from.
+     * @throws IOException if the resource cannot be found or an access error occurred.
+     */
+    InputStream createInputStream(String relativePath, String source) throws IOException;
+}
diff --git a/vtm/src/org/oscim/theme/ZipRenderTheme.java b/vtm/src/org/oscim/theme/ZipRenderTheme.java
new file mode 100644
index 00000000..ed582e64
--- /dev/null
+++ b/vtm/src/org/oscim/theme/ZipRenderTheme.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2021 devemux86
+ * Copyright 2021 eddiemuc
+ *
+ * This program is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.oscim.theme;
+
+import org.oscim.theme.IRenderTheme.ThemeException;
+import org.oscim.utils.Utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A ZipRenderTheme allows for customizing the rendering style of the map
+ * via an XML from an archive.
+ */
+public class ZipRenderTheme implements ThemeFile {
+    private static final long serialVersionUID = 1L;
+
+    private boolean mMapsforgeTheme;
+    private XmlRenderThemeMenuCallback mMenuCallback;
+    private final String mRelativePathPrefix;
+    private XmlThemeResourceProvider mResourceProvider;
+    protected final String mXmlTheme;
+
+    /**
+     * @param xmlTheme         the XML theme path in the archive.
+     * @param resourceProvider the custom provider to retrieve resources internally referenced by "src" attribute (e.g. images, icons).
+     * @throws ThemeException if an error occurs while reading the render theme XML.
+     */
+    public ZipRenderTheme(String xmlTheme, XmlThemeResourceProvider resourceProvider) throws ThemeException {
+        this(xmlTheme, resourceProvider, null);
+    }
+
+    /**
+     * @param xmlTheme         the XML theme path in the archive.
+     * @param resourceProvider the custom provider to retrieve resources internally referenced by "src" attribute (e.g. images, icons).
+     * @param menuCallback     the interface callback to create a settings menu on the fly.
+     * @throws ThemeException if an error occurs while reading the render theme XML.
+     */
+    public ZipRenderTheme(String xmlTheme, XmlThemeResourceProvider resourceProvider, XmlRenderThemeMenuCallback menuCallback) throws ThemeException {
+        mXmlTheme = xmlTheme;
+        mResourceProvider = resourceProvider;
+        mMenuCallback = menuCallback;
+
+        mRelativePathPrefix = xmlTheme.substring(0, xmlTheme.lastIndexOf("/") + 1);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        } else if (!(obj instanceof ZipRenderTheme)) {
+            return false;
+        }
+        ZipRenderTheme other = (ZipRenderTheme) obj;
+        if (getRenderThemeAsStream() != other.getRenderThemeAsStream()) {
+            return false;
+        }
+        if (!Utils.equals(mRelativePathPrefix, other.mRelativePathPrefix)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public XmlRenderThemeMenuCallback getMenuCallback() {
+        return mMenuCallback;
+    }
+
+    @Override
+    public String getRelativePathPrefix() {
+        return mRelativePathPrefix;
+    }
+
+    @Override
+    public InputStream getRenderThemeAsStream() throws ThemeException {
+        try {
+            return mResourceProvider.createInputStream(mRelativePathPrefix, mXmlTheme.substring(mXmlTheme.lastIndexOf("/") + 1));
+        } catch (IOException e) {
+            throw new ThemeException(e.getMessage());
+        }
+    }
+
+    @Override
+    public XmlThemeResourceProvider getResourceProvider() {
+        return mResourceProvider;
+    }
+
+    @Override
+    public boolean isMapsforgeTheme() {
+        return mMapsforgeTheme;
+    }
+
+    @Override
+    public void setMapsforgeTheme(boolean mapsforgeTheme) {
+        mMapsforgeTheme = mapsforgeTheme;
+    }
+
+    @Override
+    public void setMenuCallback(XmlRenderThemeMenuCallback menuCallback) {
+        mMenuCallback = menuCallback;
+    }
+
+    @Override
+    public void setResourceProvider(XmlThemeResourceProvider resourceProvider) {
+        mResourceProvider = resourceProvider;
+    }
+}
diff --git a/vtm/src/org/oscim/theme/ZipXmlThemeResourceProvider.java b/vtm/src/org/oscim/theme/ZipXmlThemeResourceProvider.java
new file mode 100644
index 00000000..94dddbda
--- /dev/null
+++ b/vtm/src/org/oscim/theme/ZipXmlThemeResourceProvider.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2021 eddiemuc
+ * Copyright 2021 devemux86
+ *
+ * This program is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with
+ * this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.oscim.theme;
+
+import org.oscim.backend.CanvasAdapter;
+import org.oscim.utils.IOUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * Resource provider reading resource files out of a zip input stream.
+ * <p>
+ * Resources are cached.
+ */
+public class ZipXmlThemeResourceProvider implements XmlThemeResourceProvider {
+
+    private final Map<String, byte[]> files = new HashMap<>();
+    private final List<String> xmlThemes = new ArrayList<>();
+
+    /**
+     * @param zipInputStream zip stream to read resources from
+     * @throws IOException if a problem occurs reading the stream
+     */
+    public ZipXmlThemeResourceProvider(ZipInputStream zipInputStream) throws IOException {
+        this(zipInputStream, Integer.MAX_VALUE);
+    }
+
+    /**
+     * @param zipInputStream         zip stream to read resources from
+     * @param maxResourceSizeToCache only resources in the zip stream with a maximum size of this parameter (in bytes) are cached and provided
+     * @throws IOException if a problem occurs reading the stream
+     */
+    public ZipXmlThemeResourceProvider(ZipInputStream zipInputStream, int maxResourceSizeToCache) throws IOException {
+        if (zipInputStream == null) {
+            return;
+        }
+
+        try {
+            ZipEntry zipEntry;
+            while ((zipEntry = zipInputStream.getNextEntry()) != null) {
+                if (zipEntry.isDirectory() || zipEntry.getSize() > maxResourceSizeToCache) {
+                    continue;
+                }
+                byte[] entry = streamToBytes(zipInputStream, (int) zipEntry.getSize());
+                String fileName = zipEntry.getName();
+                if (fileName.startsWith("/")) {
+                    fileName = fileName.substring(1);
+                }
+                files.put(fileName, entry);
+                if (fileName.toLowerCase(Locale.ROOT).endsWith(".xml")) {
+                    xmlThemes.add(fileName);
+                }
+            }
+        } finally {
+            IOUtils.closeQuietly(zipInputStream);
+        }
+    }
+
+    @Override
+    public InputStream createInputStream(String relativePath, String source) {
+        String sourceKey = source;
+        if (sourceKey.startsWith(CanvasAdapter.PREFIX_FILE)) {
+            sourceKey = sourceKey.substring(CanvasAdapter.PREFIX_FILE.length());
+        }
+        if (sourceKey.startsWith("/")) {
+            sourceKey = sourceKey.substring(1);
+        }
+        if (relativePath != null) {
+            if (relativePath.startsWith("/")) {
+                relativePath = relativePath.substring(1);
+            }
+            if (relativePath.endsWith("/")) {
+                relativePath = relativePath.substring(0, relativePath.length() - 1);
+            }
+            sourceKey = relativePath.isEmpty() ? sourceKey : relativePath + "/" + sourceKey;
+        }
+        if (files.containsKey(sourceKey)) {
+            return new ByteArrayInputStream(files.get(sourceKey));
+        }
+        return null;
+    }
+
+    /**
+     * @return the number of files in the archive.
+     */
+    public int getCount() {
+        return files.size();
+    }
+
+    /**
+     * @return the XML theme paths in the archive.
+     */
+    public List<String> getXmlThemes() {
+        return xmlThemes;
+    }
+
+    private static byte[] streamToBytes(InputStream in, int size) throws IOException {
+        byte[] bytes = new byte[size];
+        int count, offset = 0;
+        while ((count = in.read(bytes, offset, size)) > 0) {
+            size -= count;
+            offset += count;
+        }
+        return bytes;
+    }
+}
diff --git a/vtm/src/org/oscim/utils/Utils.java b/vtm/src/org/oscim/utils/Utils.java
index 62c6991e..b8344591 100644
--- a/vtm/src/org/oscim/utils/Utils.java
+++ b/vtm/src/org/oscim/utils/Utils.java
@@ -19,6 +19,7 @@ import org.oscim.backend.CanvasAdapter;
 import org.oscim.backend.canvas.Bitmap;
 import org.oscim.backend.canvas.Canvas;
 import org.oscim.renderer.bucket.TextureItem;
+import org.oscim.theme.XmlThemeResourceProvider;
 import org.oscim.utils.math.MathUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -37,12 +38,12 @@ public final class Utils {
     /**
      * Load a texture from a specified location and optional dimensions.
      */
-    public static TextureItem loadTexture(String relativePathPrefix, String src, int width, int height, int percent) {
+    public static TextureItem loadTexture(String relativePathPrefix, String src, XmlThemeResourceProvider resourceProvider, int width, int height, int percent) {
         if (src == null || src.length() == 0)
             return null;
 
         try {
-            Bitmap bitmap = CanvasAdapter.getBitmapAsset(relativePathPrefix, src, width, height, percent);
+            Bitmap bitmap = CanvasAdapter.getBitmapAsset(relativePathPrefix, src, resourceProvider, width, height, percent);
             if (bitmap != null) {
                 log.debug("loading {}", src);
                 return new TextureItem(potBitmap(bitmap), true);