diff --git a/.flutter b/.flutter
index 9b2d32b6..4d7946a6 160000
--- a/.flutter
+++ b/.flutter
@@ -1 +1 @@
-Subproject commit 9b2d32b605630f28625709ebd9d78ab3016b2bf6
+Subproject commit 4d7946a68d26794349189cf21b3f68cc6fe61dcb
diff --git a/assets/langs/en.yaml b/assets/langs/en.yaml
index 9e6ce4c3..371abf58 100644
--- a/assets/langs/en.yaml
+++ b/assets/langs/en.yaml
@@ -49,6 +49,12 @@ settings:
           validator:
             empty: Tags cannot be empty.
             same: Tag cannot be identical to a “DoNotCaption-Tag”.
+      detailsView:
+        header: Detail View
+        maxZoom: Maximal zoom level
+        rotateGestures:
+          title: Rotate Image with gestures
+          subtitle: Rotate by moving two fingers in a circle
       theming:
         title: Image Theming
         subtitle: Configure how images are themed
diff --git a/lib/screens/settings_display_images.dart b/lib/screens/settings_display_images.dart
index f0d0f022..0935b6ee 100644
--- a/lib/screens/settings_display_images.dart
+++ b/lib/screens/settings_display_images.dart
@@ -14,12 +14,17 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
+import 'dart:math';
+
 import 'package:flutter/material.dart';
 
 import 'package:easy_localization/easy_localization.dart';
 
 import 'package:gitjournal/screens/settings_display_images_caption.dart';
 import 'package:gitjournal/screens/settings_display_images_theming.dart';
+import 'package:gitjournal/screens/settings_screen.dart';
+import 'package:gitjournal/settings.dart';
+import 'package:provider/provider.dart';
 
 class SettingsDisplayImagesScreen extends StatefulWidget {
   @override
@@ -29,11 +34,11 @@ class SettingsDisplayImagesScreen extends StatefulWidget {
 
 class SettingsDisplayImagesScreenState
     extends State<SettingsDisplayImagesScreen> {
-  final doNotThemeTagsKey = GlobalKey<FormFieldState<String>>();
-  final doThemeTagsKey = GlobalKey<FormFieldState<String>>();
-
   @override
   Widget build(BuildContext context) {
+    final settings = Provider.of<Settings>(context);
+    final theme = Theme.of(context);
+
     final body = ListView(children: <Widget>[
       ListTile(
         title: Text(tr("settings.display.images.theming.title")),
@@ -59,6 +64,49 @@ class SettingsDisplayImagesScreenState
           Navigator.of(context).push(route);
         },
       ),
+      SettingsHeader(tr('settings.display.images.detailsView.header')),
+      ListTile(
+          title: Text(tr("settings.display.images.detailsView.maxZoom")),
+          subtitle: Row(children: [
+            Expanded(
+                child: Slider.adaptive(
+              value: min(settings.maxImageZoom, 30),
+              onChanged: (v) {
+                setState(() {
+                  settings.maxImageZoom = v == 30 ? double.infinity : v;
+                  settings.save();
+                });
+              },
+              max: 30,
+              min: 1,
+              activeColor: theme.accentColor,
+              inactiveColor: theme.disabledColor,
+            )),
+            Container(
+                width: 40,
+                child: settings.maxImageZoom == double.infinity
+                    ? Icon(
+                        Icons.all_inclusive,
+                        color: theme.accentColor,
+                      )
+                    : Text(
+                        NumberFormat("##.0").format(settings.maxImageZoom),
+                        style: theme.textTheme.subtitle2
+                            .copyWith(color: theme.accentColor),
+                        textAlign: TextAlign.center,
+                      ))
+          ])),
+      SwitchListTile(
+        title: Text(
+            tr('settings.display.images.detailsView.rotateGestures.title')),
+        subtitle: Text(
+            tr('settings.display.images.detailsView.rotateGestures.subtitle')),
+        value: settings.rotateImageGestures,
+        onChanged: (bool newVal) {
+          settings.rotateImageGestures = newVal;
+          settings.save();
+        },
+      ),
     ]);
 
     return Scaffold(
diff --git a/lib/settings.dart b/lib/settings.dart
index 158e04ec..6ad0f770 100644
--- a/lib/settings.dart
+++ b/lib/settings.dart
@@ -70,6 +70,10 @@ class Settings extends ChangeNotifier {
   SettingsHomeScreen homeScreen = SettingsHomeScreen.Default;
   SettingsTheme theme = SettingsTheme.Default;
 
+  // Display - Image
+  bool rotateImageGestures = false;
+  double maxImageZoom = 10;
+
   // Display - Image - Theming
   bool themeRasterGraphics = false;
   SettingsImageTextType themeOverrideTagLocation =
@@ -171,6 +175,11 @@ class Settings extends ChangeNotifier {
         SettingsHomeScreen.fromInternalString(_getString(pref, "homeScreen"));
     theme = SettingsTheme.fromInternalString(_getString(pref, "theme"));
 
+    // Display - Image
+    rotateImageGestures =
+        _getBool(pref, "rotateImageGestures") ?? rotateImageGestures;
+    maxImageZoom = _getDouble(pref, "maxImageZoom") ?? maxImageZoom;
+
     // Display - Image - Theming
     themeRasterGraphics =
         _getBool(pref, "themeRasterGraphics") ?? themeRasterGraphics;
@@ -244,6 +253,10 @@ class Settings extends ChangeNotifier {
     return pref.getInt(id + '_' + key);
   }
 
+  double _getDouble(SharedPreferences pref, String key) {
+    return pref.getDouble(id + '_' + key);
+  }
+
   Future<void> save() async {
     var pref = await SharedPreferences.getInstance();
     var defaultSet = Settings(id);
@@ -312,6 +325,12 @@ class Settings extends ChangeNotifier {
     await _setString(pref, "theme", theme.toInternalString(),
         defaultSet.theme.toInternalString());
 
+    // Display - Image
+    await _setBool(pref, "rotateImageGestures", rotateImageGestures,
+        defaultSet.rotateImageGestures);
+    await _setDouble(
+        pref, "maxImageZoom", maxImageZoom, defaultSet.maxImageZoom);
+
     // Display - Image - Theme
     await _setBool(pref, "themeRasterGraphics", themeRasterGraphics,
         defaultSet.themeRasterGraphics);
@@ -427,6 +446,20 @@ class Settings extends ChangeNotifier {
     }
   }
 
+  Future<void> _setDouble(
+    SharedPreferences pref,
+    String key,
+    double value,
+    double defaultValue,
+  ) async {
+    key = id + '_' + key;
+    if (value == defaultValue) {
+      await pref.remove(key);
+    } else {
+      await pref.setDouble(key, value);
+    }
+  }
+
   Future<void> _setStringSet(
     SharedPreferences pref,
     String key,
@@ -471,6 +504,9 @@ class Settings extends ChangeNotifier {
       'markdownLastUsedView': markdownLastUsedView.toInternalString(),
       'homeScreen': homeScreen.toInternalString(),
       'theme': theme.toInternalString(),
+      // Display - Image
+      'rotateImageGestures': rotateImageGestures.toString(),
+      'maxImageZoom': maxImageZoom.toString(),
       // Display - Image - Theming
       'themeRasterGraphics': themeRasterGraphics.toString(),
       'themeOverrideTagLocation': themeOverrideTagLocation.toInternalString(),
diff --git a/lib/widgets/image_renderer.dart b/lib/widgets/image_renderer.dart
deleted file mode 100644
index af2855db..00000000
--- a/lib/widgets/image_renderer.dart
+++ /dev/null
@@ -1,480 +0,0 @@
-/*
-Copyright 2020-2021 Roland Fredenhagen <important@van-fredenhagen.de>
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import 'dart:io';
-import 'dart:ui';
-
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart';
-import 'package:flutter/widgets.dart';
-
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter_cache_manager/flutter_cache_manager.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:flutter_svg/svg.dart';
-import 'package:provider/provider.dart';
-
-import 'package:gitjournal/settings.dart';
-import 'package:gitjournal/utils/hero_dialog.dart';
-import 'package:gitjournal/utils/logger.dart';
-
-class ThemableImage extends StatelessWidget {
-  final double width;
-  final double height;
-  final String altText;
-  final String tooltip;
-  final Future<dynamic> data;
-
-  ThemableImage._(this.data, this.width, this.height, altText, tooltip)
-      : altText = altText ?? "",
-        tooltip = tooltip ?? "";
-
-  factory ThemableImage(Uri uri, String imageDirectory,
-      {double width, double height, String altText, String titel}) {
-    final file = ((uri.isScheme("http") || uri.isScheme("https"))
-        ? DefaultCacheManager().getSingleFile(uri.toString())
-        : Future.sync(
-            () => File.fromUri(Uri.parse(imageDirectory + uri.toString()))));
-
-    final data = file.then(
-        (value) => value.path.endsWith(".svg") ? value.readAsString() : file);
-
-    return ThemableImage._(data, width, height, altText, titel);
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    final settings = Provider.of<Settings>(context);
-    final theme = Theme.of(context);
-    final dark = theme.brightness == Brightness.dark;
-
-    ThemeOverride override = ThemeOverride.None;
-    if (settings.themeOverrideTagLocation == SettingsImageTextType.Alt ||
-        settings.themeOverrideTagLocation == SettingsImageTextType.AltTool) {
-      if (hasTag(altText, settings.doThemeTags)) {
-        override = ThemeOverride.Do;
-      } else if (hasTag(altText, settings.doNotThemeTags)) {
-        override = ThemeOverride.No;
-      }
-    }
-    if (settings.themeOverrideTagLocation == SettingsImageTextType.Tooltip ||
-        settings.themeOverrideTagLocation == SettingsImageTextType.AltTool) {
-      if (hasTag(tooltip, settings.doThemeTags)) {
-        override = ThemeOverride.Do;
-      } else if (hasTag(tooltip, settings.doNotThemeTags)) {
-        override = ThemeOverride.No;
-      }
-    }
-
-    return LayoutBuilder(
-      builder: (context, constraints) {
-        final small =
-            constraints.maxWidth < MediaQuery.of(context).size.width - 40;
-        final image = FutureBuilder(
-            future: data,
-            builder: (context, snapshot) {
-              if (snapshot.hasError) {
-                String errorMessage = snapshot.error.toString();
-                Log.e(errorMessage);
-                if (snapshot.error is HttpExceptionWithStatus) {
-                  final httpError = snapshot.error as HttpExceptionWithStatus;
-                  errorMessage = tr("widgets.imageRenderer.httpError",
-                      namedArgs: {
-                        "status": httpError.statusCode.toString(),
-                        "url": httpError.uri.toString()
-                      });
-                }
-                return SizedBox(
-                  width: width,
-                  height: height,
-                  child: Padding(
-                    padding: const EdgeInsets.all(16),
-                    child: Center(
-                      child: Column(
-                          crossAxisAlignment: CrossAxisAlignment.center,
-                          children: [
-                            Icon(
-                              Icons.error,
-                              color: theme.errorColor,
-                              size: 36,
-                            ),
-                            Text(
-                              errorMessage,
-                              style: theme.textTheme.bodyText1
-                                  .copyWith(color: theme.errorColor),
-                              textAlign: TextAlign.center,
-                              maxLines: 2,
-                              overflow: TextOverflow.ellipsis,
-                            )
-                          ]),
-                    ),
-                  ),
-                );
-              }
-
-              if (snapshot.hasData) {
-                Widget im;
-                if (snapshot.data is String) {
-                  im = _handleSvg(
-                      snapshot.data, width, height, context, override);
-                } else {
-                  im = Image.file(snapshot.data, width: width, height: height);
-                  if ((settings.themeRasterGraphics ||
-                          override == ThemeOverride.Do) &&
-                      override != ThemeOverride.No &&
-                      dark) {
-                    im = themeFilter(im, theme.canvasColor);
-                  }
-                }
-
-                if (small) {
-                  return GestureDetector(
-                    child: Hero(tag: im, child: im),
-                    onTap: () {
-                      Navigator.push(
-                          context,
-                          HeroDialogRoute(
-                            builder: (context) => GestureDetector(
-                              child: Hero(tag: im, child: im),
-                              onTap: () => Navigator.pop(context),
-                            ),
-                          ));
-                    },
-                  );
-                }
-
-                return im;
-              }
-
-              return SizedBox(
-                  width: width,
-                  height: height,
-                  child: const Padding(
-                      padding: EdgeInsets.all(16),
-                      child: Center(child: CircularProgressIndicator())));
-            });
-
-        if (small || !settings.overlayCaption) {
-          final caption = _imageCaption(context, altText, tooltip, false);
-          return Column(children: [image, if (caption != null) caption]);
-        }
-        final caption = _imageCaption(context, altText, tooltip, true);
-        return Stack(
-            alignment: AlignmentDirectional.bottomEnd,
-            children: [image, if (caption != null) caption]);
-      },
-    );
-  }
-}
-
-String _cleanCaption(BuildContext context, String caption) {
-  final settings = Provider.of<Settings>(context);
-  final tags = [
-    ...settings.doThemeTags,
-    ...settings.doNotThemeTags,
-    ...settings.doCaptionTags,
-    ...settings.doNotCaptionTags
-  ];
-  return caption
-      .replaceAll(
-          RegExp(
-              r"\s*(?<=\s|\b)(" +
-                  tags.map(RegExp.escape).join("|") +
-                  r")(?=\s|\b)\s*",
-              caseSensitive: false),
-          " ")
-      .trim();
-}
-
-Widget _imageCaption(
-    BuildContext context, String altText, String tooltip, bool overlay) {
-  final theme = Theme.of(context);
-  final settings = Provider.of<Settings>(context);
-  final dark = theme.brightness == Brightness.dark;
-
-  bool altTextCaption =
-      settings.useAsCaption == SettingsImageTextType.AltTool ||
-          settings.useAsCaption == SettingsImageTextType.AltTool;
-  if (hasTag(altText, settings.doCaptionTags)) {
-    altTextCaption = true;
-  } else if (hasTag(altText, settings.doNotCaptionTags)) {
-    altTextCaption = false;
-  }
-
-  bool tooltipCaption =
-      settings.useAsCaption == SettingsImageTextType.Tooltip ||
-          settings.useAsCaption == SettingsImageTextType.AltTool;
-  if (hasTag(tooltip, settings.doCaptionTags)) {
-    tooltipCaption = true;
-  } else if (hasTag(tooltip, settings.doNotCaptionTags)) {
-    tooltipCaption = false;
-  }
-
-  altText = altTextCaption ? _cleanCaption(context, altText) : "";
-  tooltip = tooltipCaption ? _cleanCaption(context, tooltip) : "";
-  String text = "";
-  if (altText.isNotEmpty && tooltip.isNotEmpty) {
-    text = tr("widgets.imageRenderer.caption",
-        namedArgs: settings.tooltipFirst
-            ? {"first": tooltip, "second": altText}
-            : {"first": altText, "second": tooltip});
-  } else {
-    text = altText + tooltip;
-  }
-
-  if (text.isNotEmpty) {
-    if (!overlay) {
-      return Text(text, style: theme.textTheme.caption);
-    }
-
-    return LayoutBuilder(builder: (context, constraints) {
-      final maxWidth = constraints.constrainWidth(200);
-      const padding = 6.0, margin = 4.0, borderRadius = 5.0;
-      final blur = settings.blurBehindCaption ? 2.0 : 0.0;
-
-      final overflown = (TextPainter(
-              text: TextSpan(text: text),
-              maxLines: 2,
-              textScaleFactor: MediaQuery.of(context).textScaleFactor,
-              textDirection: Directionality.of(context))
-            ..layout(maxWidth: maxWidth - 2 * (padding + margin)))
-          .didExceedMaxLines;
-
-      final bColor = settings.transparentCaption
-          ? (dark ? Colors.black : Colors.white).withAlpha(150)
-          : theme.canvasColor;
-      final box = ClipRRect(
-        borderRadius: BorderRadius.circular(borderRadius),
-        child: BackdropFilter(
-          filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
-          child: Container(
-            color: bColor,
-            child: Padding(
-                padding: const EdgeInsets.all(padding),
-                child: Text(
-                  text,
-                  maxLines: 2,
-                  overflow: TextOverflow.ellipsis,
-                  style: theme.textTheme.bodyText2,
-                )),
-          ),
-        ),
-      );
-      if (!overflown) {
-        return ConstrainedBox(
-            constraints: BoxConstraints(maxWidth: maxWidth),
-            child: Container(margin: const EdgeInsets.all(margin), child: box));
-      }
-
-      final caption = Hero(tag: "caption", child: box);
-
-      return ConstrainedBox(
-        constraints: BoxConstraints(maxWidth: maxWidth),
-        child: Padding(
-          padding: const EdgeInsets.all(margin),
-          child: GestureDetector(
-            onTap: () {
-              Navigator.push(context, HeroDialogRoute(builder: (context) {
-                return Dialog(
-                    backgroundColor: Colors.transparent,
-                    child: GestureDetector(
-                      onTap: () {
-                        Navigator.pop(context);
-                      },
-                      child: caption,
-                    ));
-              }));
-            },
-            child: caption,
-          ),
-        ),
-      );
-    });
-  }
-
-  return null;
-}
-
-Widget _handleSvg(final String string, double width, final double height,
-    final BuildContext context, final ThemeOverride override) {
-  final settings = Provider.of<Settings>(context);
-  final theme = Theme.of(context);
-  width ??= MediaQuery.of(context).size.width;
-  final dark = theme.brightness == Brightness.dark;
-  if (settings.themeVectorGraphics == SettingsThemeVectorGraphics.Off &&
-          override != ThemeOverride.Do ||
-      override == ThemeOverride.No) {
-    return SvgPicture.string(string, width: width, height: height);
-  }
-  if (settings.themeVectorGraphics == SettingsThemeVectorGraphics.Filter &&
-      dark) {
-    return themeFilter(SvgPicture.string(string, width: width, height: height),
-        theme.canvasColor);
-  }
-  final transformColor = dark
-      ? themeDark(theme.canvasColor, settings.vectorGraphicsAdjustColors)
-      : settings.matchCanvasColor
-          ? whiteToCanvas(theme.canvasColor)
-          : noTheme;
-  return SvgPicture(
-    StringPicture((data, colorFilter, key) async {
-      DrawableRoot svgRoot = await svg.fromSvgString(data, key);
-      if (settings.themeSvgWithBackground ||
-          override == ThemeOverride.Do ||
-          !hasBackground(svgRoot, svgRoot.viewport.viewBox.width,
-              svgRoot.viewport.viewBox.height)) {
-        svgRoot = themeDrawable(svgRoot, transformColor);
-      }
-      final Picture pic = svgRoot.toPicture(
-        clipToViewBox: false,
-        colorFilter: colorFilter,
-      );
-      return PictureInfo(
-        picture: pic,
-        viewport: svgRoot.viewport.viewBoxRect,
-        size: svgRoot.viewport.size,
-      );
-    },
-        string +
-            '<?theme darkMode="$dark" ' +
-            'override="$override" ' +
-            'opaqueBackground="${settings.themeSvgWithBackground}" ' +
-            'whiteToCanvas="${settings.matchCanvasColor}" ' +
-            'adjustColors="${settings.vectorGraphicsAdjustColors.toInternalString()}"?>'),
-    width: width,
-    height: height,
-  );
-}
-
-Color Function(Color color) whiteToCanvas(Color canvasColor) =>
-    (Color color) => color.value == 0xffffffff ? canvasColor : color;
-
-Color noTheme(Color color) => color;
-
-Color Function(Color color) themeDark(
-        Color canvasColor, SettingsVectorGraphicsAdjustColors adjustColors) =>
-    (Color color) {
-      final hslColor = HSLColor.fromColor(color);
-      final backGroundLightness = HSLColor.fromColor(canvasColor).lightness;
-      if (adjustColors == SettingsVectorGraphicsAdjustColors.BnW) {
-        if (hslColor.lightness > 0.95 && hslColor.saturation < 0.02) {
-          return HSLColor.fromAHSL(hslColor.alpha, 0, 0, backGroundLightness)
-              .toColor();
-        }
-        if (hslColor.lightness < 0.02) {
-          return HSLColor.fromAHSL(hslColor.alpha, 0, 0, 1).toColor();
-        }
-        return color;
-      }
-      if (adjustColors == SettingsVectorGraphicsAdjustColors.Grays) {
-        if (hslColor.saturation < 0.02 || hslColor.lightness < 0.02) {
-          return HSLColor.fromAHSL(hslColor.alpha, 0, 0,
-                  1 - (hslColor.lightness * (1 - backGroundLightness)))
-              .toColor();
-        }
-        return color;
-      }
-
-      return HSLColor.fromAHSL(
-              hslColor.alpha,
-              hslColor.hue,
-              hslColor.saturation,
-              1 - (hslColor.lightness * (1 - backGroundLightness)))
-          .toColor();
-    };
-
-bool hasBackground(Drawable draw, double width, double height) {
-  if (draw is DrawableShape) {
-    final drawShape = draw;
-    return drawShape.style.fill != null &&
-        drawShape.style.fill.color.alpha > 0.99 &&
-        [
-          Offset(0 + width * 0.01, 0 + height * 0.01),
-          Offset(width - width * 0.01, 0 + height * 0.01),
-          Offset(width - width * 0.01, height - height * 0.01),
-          Offset(0 + width * 0.01, height - height * 0.01),
-        ].every(drawShape.path.contains);
-  }
-  if (draw is DrawableParent) {
-    final drawParent = draw;
-    return drawParent.children
-        .any((element) => hasBackground(element, width, height));
-  }
-  return false;
-}
-
-bool hasTag(String text, Set<String> tags) {
-  return tags
-      .map((e) => RegExp(r"(?<=\s|\b)" + RegExp.escape(e) + r"(?=\s|\b)",
-          caseSensitive: false))
-      .any((e) => e.hasMatch(text));
-}
-
-Drawable themeDrawable(
-    Drawable draw, Color Function(Color color) transformColor) {
-  if (draw is DrawableStyleable && !(draw is DrawableGroup)) {
-    final DrawableStyleable drawStylable = draw;
-    draw = drawStylable.mergeStyle(DrawableStyle(
-        stroke: drawStylable.style.stroke != null &&
-                drawStylable.style.stroke.color != null
-            ? DrawablePaint.merge(
-                DrawablePaint(drawStylable.style.stroke.style,
-                    color: transformColor(drawStylable.style.stroke.color)),
-                drawStylable.style.stroke)
-            : null,
-        fill: drawStylable.style.fill != null &&
-                drawStylable.style.fill.color != null
-            ? DrawablePaint.merge(
-                DrawablePaint(drawStylable.style.fill.style,
-                    color: transformColor(drawStylable.style.fill.color)),
-                drawStylable.style.fill)
-            : null));
-  }
-  if (draw is DrawableParent) {
-    final DrawableParent drawParent = draw;
-    final children = drawParent.children
-        .map((e) => themeDrawable(e, transformColor))
-        .toList(growable: false);
-    if (draw is DrawableRoot) {
-      final DrawableRoot drawRoot = draw;
-      draw = DrawableRoot(drawRoot.id, drawRoot.viewport, children,
-          drawRoot.definitions, drawRoot.style,
-          transform: drawRoot.transform);
-    } else if (draw is DrawableGroup) {
-      final DrawableGroup drawGroup = draw;
-      draw = DrawableGroup(drawGroup.id, children, drawGroup.style,
-          transform: drawGroup.transform);
-    }
-  }
-  return draw;
-}
-
-Widget themeFilter(Widget widget, Color background) {
-  final lightness = (1 - HSLColor.fromColor(background).lightness) * 255;
-  // TODO Switch to HSL Filter, when availible (https://github.com/flutter/flutter/issues/76729)
-  final stack = ColorFiltered(
-      colorFilter: ColorFilter.matrix(<double>[
-        -(lightness / 255), 0, 0, 0, 255, // R
-        0, -(lightness / 255), 0, 0, 255, // G
-        0, 0, -(lightness / 255), 0, 255, // B
-        0, 0, 0, 1, 0 // A
-      ]),
-      child: widget);
-  return stack;
-}
-
-enum ThemeOverride { None, Do, No }
diff --git a/lib/widgets/images/image_caption.dart b/lib/widgets/images/image_caption.dart
new file mode 100644
index 00000000..edddfc4a
--- /dev/null
+++ b/lib/widgets/images/image_caption.dart
@@ -0,0 +1,178 @@
+/*
+Copyright 2020-2021 Roland Fredenhagen <important@van-fredenhagen.de>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import 'dart:ui';
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:gitjournal/settings.dart';
+import 'package:gitjournal/utils/hero_dialog.dart';
+import 'package:provider/provider.dart';
+
+class ImageCaption extends StatelessWidget {
+  final String altText;
+  final String tooltip;
+  final bool overlay;
+  ImageCaption(this.altText, this.tooltip, this.overlay);
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    final settings = Provider.of<Settings>(context);
+
+    final text = captionText(context, altText, tooltip);
+
+    if (!overlay) {
+      return Text(text, style: theme.textTheme.caption);
+    }
+
+    return LayoutBuilder(builder: (context, constraints) {
+      final maxWidth = constraints.constrainWidth(200);
+      const padding = 6.0, margin = 4.0, borderRadius = 5.0;
+      final blur = settings.blurBehindCaption ? 2.0 : 0.0;
+
+      final overflown = (TextPainter(
+              text: TextSpan(text: text),
+              maxLines: 2,
+              textScaleFactor: MediaQuery.of(context).textScaleFactor,
+              textDirection: Directionality.of(context))
+            ..layout(maxWidth: maxWidth - 2 * (padding + margin)))
+          .didExceedMaxLines;
+
+      final box = ClipRRect(
+        borderRadius: BorderRadius.circular(borderRadius),
+        child: BackdropFilter(
+          filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
+          child: Container(
+            color: _overlayBackgroundColor(context),
+            child: Padding(
+                padding: const EdgeInsets.all(padding),
+                child: Text(
+                  text,
+                  maxLines: 2,
+                  overflow: TextOverflow.ellipsis,
+                  style: theme.textTheme.bodyText2,
+                )),
+          ),
+        ),
+      );
+      if (!overflown) {
+        return ConstrainedBox(
+            constraints: BoxConstraints(maxWidth: maxWidth),
+            child: Container(margin: const EdgeInsets.all(margin), child: box));
+      }
+
+      final caption = Hero(tag: "caption", child: box);
+
+      return ConstrainedBox(
+        constraints: BoxConstraints(maxWidth: maxWidth),
+        child: Padding(
+          padding: const EdgeInsets.all(margin),
+          child: GestureDetector(
+            onTap: () {
+              Navigator.push(context, HeroDialogRoute(builder: (context) {
+                return Dialog(
+                    backgroundColor: Colors.transparent,
+                    child: GestureDetector(
+                      onTap: () {
+                        Navigator.pop(context);
+                      },
+                      child: caption,
+                    ));
+              }));
+            },
+            child: caption,
+          ),
+        ),
+      );
+    });
+  }
+}
+
+bool hasTag(String text, Set<String> tags) {
+  return tags
+      .map((e) => RegExp(r"(?<=^|\s|\b)" + RegExp.escape(e) + r"(?=$|\s|\b)",
+          caseSensitive: false))
+      .any((e) => e.hasMatch(text));
+}
+
+bool shouldCaption(BuildContext context, String altText, String tooltip) {
+  return captionText(context, altText, tooltip).isNotEmpty;
+}
+
+String captionText(BuildContext context, String altText, String tooltip) {
+  final settings = Provider.of<Settings>(context);
+
+  bool altTextCaption =
+      settings.useAsCaption == SettingsImageTextType.AltTool ||
+          settings.useAsCaption == SettingsImageTextType.AltTool;
+  if (hasTag(altText, settings.doCaptionTags)) {
+    altTextCaption = true;
+  } else if (hasTag(altText, settings.doNotCaptionTags)) {
+    altTextCaption = false;
+  }
+
+  bool tooltipCaption =
+      settings.useAsCaption == SettingsImageTextType.Tooltip ||
+          settings.useAsCaption == SettingsImageTextType.AltTool;
+  if (hasTag(tooltip, settings.doCaptionTags)) {
+    tooltipCaption = true;
+  } else if (hasTag(tooltip, settings.doNotCaptionTags)) {
+    tooltipCaption = false;
+  }
+
+  String _altText = altTextCaption ? _cleanCaption(context, altText) : "";
+  String _tooltip = tooltipCaption ? _cleanCaption(context, tooltip) : "";
+  String text = "";
+  if (_altText.isNotEmpty && _tooltip.isNotEmpty) {
+    text = tr("widgets.imageRenderer.caption",
+        namedArgs: settings.tooltipFirst
+            ? {"first": _tooltip, "second": _altText}
+            : {"first": _altText, "second": _tooltip});
+  } else {
+    text = _altText + _tooltip;
+  }
+
+  return text;
+}
+
+String _cleanCaption(BuildContext context, String caption) {
+  final settings = Provider.of<Settings>(context);
+  final tags = [
+    ...settings.doThemeTags,
+    ...settings.doNotThemeTags,
+    ...settings.doCaptionTags,
+    ...settings.doNotCaptionTags
+  ];
+  return caption
+      .replaceAll(
+          RegExp(
+              r"\s*(?<=^|\s|\b)(" +
+                  tags.map(RegExp.escape).join("|") +
+                  r")(?=$|\s|\b)\s*",
+              caseSensitive: false),
+          " ")
+      .trim();
+}
+
+Color _overlayBackgroundColor(context) {
+  final settings = Provider.of<Settings>(context);
+  final theme = Theme.of(context);
+  return settings.transparentCaption
+      ? (theme.brightness == Brightness.dark ? Colors.black : Colors.white)
+          .withAlpha(100)
+      : theme.canvasColor;
+}
diff --git a/lib/widgets/images/image_details.dart b/lib/widgets/images/image_details.dart
new file mode 100644
index 00000000..94e43d8f
--- /dev/null
+++ b/lib/widgets/images/image_details.dart
@@ -0,0 +1,106 @@
+/*
+Copyright 2020-2021 Roland Fredenhagen <important@van-fredenhagen.de>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import 'package:flutter/material.dart';
+import 'package:gitjournal/settings.dart';
+import 'package:gitjournal/widgets/images/markdown_image.dart';
+import 'package:gitjournal/widgets/images/themable_image.dart';
+import 'package:photo_view/photo_view.dart';
+import 'package:provider/provider.dart';
+
+class ImageDetails extends StatefulWidget {
+  final ThemableImage image;
+  final String caption;
+  ImageDetails(this.image, this.caption);
+
+  @override
+  _ImageDetailsState createState() => _ImageDetailsState();
+}
+
+class _ImageDetailsState extends State<ImageDetails> {
+  int rotation = 0;
+  bool showUI = true;
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    final settings = Provider.of<Settings>(context);
+    final bg =
+        theme.brightness == Brightness.dark ? Colors.black : Colors.white;
+    final overlayColor = getOverlayBackgroundColor(context,
+        light: Colors.white, dark: Colors.black);
+    return Stack(
+      children: [
+        PhotoView.customChild(
+          backgroundDecoration: BoxDecoration(color: bg),
+          child: RotatedBox(
+              quarterTurns: rotation,
+              child: ThemableImage.from(widget.image, bg: bg)),
+          minScale: 1.0,
+          maxScale: settings.maxImageZoom,
+          heroAttributes: PhotoViewHeroAttributes(tag: widget.image),
+          onTapUp: (context, details, controllerValue) =>
+              setState(() => showUI = !showUI),
+          enableRotation: settings.rotateImageGestures,
+        ),
+        if (showUI)
+          Positioned(
+              top: MediaQuery.of(context).padding.top,
+              left: 0,
+              right: 0,
+              height: 60,
+              child: Material(
+                  color: overlayColor,
+                  child: Padding(
+                      padding: const EdgeInsetsDirectional.fromSTEB(8, 0, 0, 0),
+                      child: Row(
+                        children: [
+                          IconButton(
+                              splashRadius: 20,
+                              icon: const Icon(Icons.arrow_back),
+                              onPressed: () => Navigator.pop(context)),
+                          const Spacer(),
+                          IconButton(
+                              splashRadius: 20,
+                              icon: const Icon(Icons.rotate_90_degrees_ccw),
+                              onPressed: () => setState(() => rotation--))
+                        ],
+                      )))),
+        // TODO use a DraggableScrollableSheet, when they can be dynamically
+        // height restricted https://github.com/flutter/flutter/issues/41599
+        if (showUI && widget.caption.isNotEmpty)
+          Positioned(
+              bottom: 0,
+              left: 0,
+              right: 0,
+              child: Hero(
+                  tag: "caption",
+                  child: Container(
+                    child: ConstrainedBox(
+                      constraints: const BoxConstraints(maxHeight: 200),
+                      child: SingleChildScrollView(
+                          child: Text(
+                        widget.caption,
+                        style: theme.primaryTextTheme.bodyText1,
+                      )),
+                    ),
+                    color: overlayColor,
+                    padding:
+                        const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+                  )))
+      ],
+    );
+  }
+}
diff --git a/lib/widgets/images/markdown_image.dart b/lib/widgets/images/markdown_image.dart
new file mode 100644
index 00000000..45a1bdc0
--- /dev/null
+++ b/lib/widgets/images/markdown_image.dart
@@ -0,0 +1,227 @@
+/*
+Copyright 2020-2021 Roland Fredenhagen <important@van-fredenhagen.de>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import 'dart:io';
+import 'dart:ui';
+
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_cache_manager/flutter_cache_manager.dart';
+import 'package:gitjournal/widgets/images/image_caption.dart';
+import 'package:gitjournal/widgets/images/image_details.dart';
+import 'package:gitjournal/widgets/images/themable_image.dart';
+import 'package:provider/provider.dart';
+
+import 'package:gitjournal/utils/logger.dart';
+import 'package:gitjournal/settings.dart';
+
+class MarkdownImage extends StatelessWidget {
+  final double width;
+  final double height;
+  final String altText;
+  final String tooltip;
+  final Future<dynamic> data;
+
+  MarkdownImage._(
+      this.data, this.width, this.height, String altText, String tooltip)
+      : altText = altText ?? "",
+        tooltip = tooltip ?? "";
+
+  factory MarkdownImage(Uri uri, String imageDirectory,
+      {double width, double height, String altText, String titel}) {
+    final file = ((uri.isScheme("http") || uri.isScheme("https"))
+        ? DefaultCacheManager().getSingleFile(uri.toString())
+        : Future.sync(
+            () => File.fromUri(Uri.parse(imageDirectory + uri.toString()))));
+
+    final data = file.then(
+        (value) => value.path.endsWith(".svg") ? value.readAsString() : file);
+
+    return MarkdownImage._(data, width, height, altText, titel);
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final settings = Provider.of<Settings>(context);
+    final theme = Theme.of(context);
+    final dark = theme.brightness == Brightness.dark;
+
+    // Test for override tags in AltText/Tooltip
+    ThemeOverride override = ThemeOverride.None;
+    if (settings.themeOverrideTagLocation == SettingsImageTextType.Alt ||
+        settings.themeOverrideTagLocation == SettingsImageTextType.AltTool) {
+      if (hasTag(altText, settings.doThemeTags)) {
+        override = ThemeOverride.Do;
+      } else if (hasTag(altText, settings.doNotThemeTags)) {
+        override = ThemeOverride.No;
+      }
+    }
+    if (settings.themeOverrideTagLocation == SettingsImageTextType.Tooltip ||
+        settings.themeOverrideTagLocation == SettingsImageTextType.AltTool) {
+      if (hasTag(tooltip, settings.doThemeTags)) {
+        override = ThemeOverride.Do;
+      } else if (hasTag(tooltip, settings.doNotThemeTags)) {
+        override = ThemeOverride.No;
+      }
+    }
+
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final small =
+            constraints.maxWidth < MediaQuery.of(context).size.width - 40;
+        final image = FutureBuilder(
+            future: data,
+            builder: (context, snapshot) {
+              if (snapshot.hasError) {
+                String errorMessage = snapshot.error.toString();
+                Log.e(errorMessage);
+                if (snapshot.error is HttpExceptionWithStatus) {
+                  final httpError = snapshot.error as HttpExceptionWithStatus;
+                  errorMessage = tr("widgets.imageRenderer.httpError",
+                      namedArgs: {
+                        "status": httpError.statusCode.toString(),
+                        "url": httpError.uri.toString()
+                      });
+                }
+                return SizedBox(
+                  width: width,
+                  height: height,
+                  child: Padding(
+                    padding: const EdgeInsets.all(16),
+                    child: Center(
+                      child: Column(
+                          crossAxisAlignment: CrossAxisAlignment.center,
+                          children: [
+                            Icon(
+                              Icons.error,
+                              color: theme.errorColor,
+                              size: 36,
+                            ),
+                            Text(
+                              errorMessage,
+                              style: theme.textTheme.bodyText1
+                                  .copyWith(color: theme.errorColor),
+                              textAlign: TextAlign.center,
+                              maxLines: 2,
+                              overflow: TextOverflow.ellipsis,
+                            )
+                          ]),
+                    ),
+                  ),
+                );
+              }
+
+              if (snapshot.hasData) {
+                Widget im;
+                if (snapshot.data is String) {
+                  im = ThemableImage.svg(
+                    snapshot.data,
+                    width: width ?? MediaQuery.of(context).size.width,
+                    height: height,
+                    themingMethod: override == ThemeOverride.No ||
+                            settings.themeVectorGraphics ==
+                                    SettingsThemeVectorGraphics.Off &&
+                                override != ThemeOverride.Do
+                        ? ThemingMethod.none
+                        : dark
+                            ? settings.themeVectorGraphics ==
+                                    SettingsThemeVectorGraphics.Filter
+                                ? ThemingMethod.filter
+                                : ThemingMethod.invertBrightness
+                            : settings.matchCanvasColor
+                                ? ThemingMethod.wToBg
+                                : ThemingMethod.none,
+                    themingCondition: settings.themeSvgWithBackground ||
+                            override == ThemeOverride.Do
+                        ? ThemingCondition.none
+                        : ThemingCondition.noBackground,
+                    colorCondition: settings.vectorGraphicsAdjustColors ==
+                            SettingsVectorGraphicsAdjustColors.All
+                        ? ColorCondition.all
+                        : settings.vectorGraphicsAdjustColors ==
+                                SettingsVectorGraphicsAdjustColors.BnW
+                            ? ColorCondition.bw
+                            : ColorCondition.gray,
+                    bg: settings.matchCanvasColor
+                        ? theme.canvasColor
+                        : Colors.black,
+                  );
+                } else {
+                  im = ThemableImage.image(
+                    snapshot.data,
+                    width: width,
+                    height: height,
+                    doTheme: (settings.themeRasterGraphics ||
+                            override == ThemeOverride.Do) &&
+                        override != ThemeOverride.No &&
+                        dark,
+                    bg: theme.canvasColor,
+                  );
+                }
+
+                return GestureDetector(
+                  child: Hero(tag: im, child: im),
+                  onTap: () {
+                    Navigator.push(
+                        context,
+                        MaterialPageRoute(
+                            builder: (context) => ImageDetails(
+                                im, captionText(context, altText, tooltip))));
+                  },
+                );
+              }
+
+              return SizedBox(
+                  width: width,
+                  height: height,
+                  child: const Padding(
+                      padding: EdgeInsets.all(16),
+                      child: Center(child: CircularProgressIndicator())));
+            });
+
+        if (shouldCaption(context, altText, tooltip)) {
+          if (small || !settings.overlayCaption) {
+            return Column(
+                children: [image, ImageCaption(altText, tooltip, false)]);
+          } else {
+            return Stack(
+                alignment: AlignmentDirectional.bottomEnd,
+                children: [image, ImageCaption(altText, tooltip, true)]);
+          }
+        }
+        return image;
+      },
+    );
+  }
+}
+
+Color getOverlayBackgroundColor(BuildContext context,
+    {Color light, Color dark}) {
+  final settings = Provider.of<Settings>(context);
+  final theme = Theme.of(context);
+  return theme.brightness == Brightness.dark
+      ? settings.transparentCaption
+          ? Colors.black.withAlpha(100)
+          : dark ?? theme.canvasColor
+      : settings.transparentCaption
+          ? Colors.white.withAlpha(100)
+          : light ?? theme.canvasColor;
+}
+
+enum ThemeOverride { None, Do, No }
diff --git a/lib/widgets/images/themable_image.dart b/lib/widgets/images/themable_image.dart
new file mode 100644
index 00000000..e8b484b7
--- /dev/null
+++ b/lib/widgets/images/themable_image.dart
@@ -0,0 +1,248 @@
+/*
+Copyright 2020-2021 Roland Fredenhagen <important@van-fredenhagen.de>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import 'dart:io';
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:flutter_svg/svg.dart';
+
+class ThemableImage extends StatelessWidget {
+  final double width;
+  final double height;
+  final File file;
+  final String string;
+  final ThemingMethod themingMethod;
+  final ThemingCondition themingCondition;
+  final ColorCondition colorCondition;
+  final Color bg;
+
+  ThemableImage.image(
+    this.file, {
+    this.width,
+    this.height,
+    doTheme = false,
+    this.bg = Colors.white,
+  })  : string = "",
+        themingMethod = doTheme ? ThemingMethod.filter : ThemingMethod.none,
+        themingCondition = ThemingCondition.none,
+        colorCondition = ColorCondition.all;
+
+  ThemableImage.svg(
+    this.string, {
+    this.width,
+    this.height,
+    this.themingMethod = ThemingMethod.none,
+    this.bg = Colors.white,
+    this.themingCondition = ThemingCondition.none,
+    this.colorCondition = ColorCondition.all,
+  }) : file = null;
+
+  ThemableImage.from(
+    ThemableImage ti, {
+    double width,
+    double height,
+    ThemingMethod themingMethod,
+    ThemingCondition themingCondition,
+    ColorCondition colorCondition,
+    Color bg,
+  })  : file = ti.file,
+        string = ti.string,
+        width = width ?? ti.width,
+        height = height ?? ti.height,
+        themingMethod = themingMethod ?? ti.themingMethod,
+        themingCondition = themingCondition ?? ti.themingCondition,
+        colorCondition = colorCondition ?? ti.colorCondition,
+        bg = bg ?? ti.bg;
+
+  @override
+  Widget build(BuildContext context) {
+    Widget image;
+    if (file != null) {
+      image = Image.file(file, width: width, height: height);
+    } else if (string.isNotEmpty) {
+      image = SvgPicture(
+          StringPicture(
+              _transformSVG,
+              string +
+                  '<?theme '
+                      'themingMethod="$themingMethod" '
+                      'themingCondition="$themingCondition" '
+                      'colorCondition="$colorCondition" '
+                      'backgroundColor="$bg" '
+                      '?>'),
+          width: width,
+          height: height);
+    } else {
+      throw Exception("Tried to render an image without File or SVG string");
+    }
+    return themingMethod == ThemingMethod.filter
+        ? _themeFilter(image, bg)
+        : image;
+  }
+
+  Future<PictureInfo> _transformSVG(data, colorFilter, key) async {
+    DrawableRoot svgRoot = await svg.fromSvgString(data, key);
+    if (themingCondition != ThemingCondition.noBackground ||
+        !_hasBackground(svgRoot, svgRoot.viewport.viewBox.width,
+            svgRoot.viewport.viewBox.height) ||
+        themingMethod == ThemingMethod.wToBg) {
+      svgRoot = _themeDrawable(svgRoot, (Color color) {
+        switch (themingMethod) {
+          case ThemingMethod.wToBg:
+            return color == Colors.white ? bg : color;
+
+          case ThemingMethod.none:
+          case ThemingMethod.filter:
+            return color;
+
+          case ThemingMethod.invertBrightness:
+            final hslColor = HSLColor.fromColor(color);
+            final backGroundLightness = HSLColor.fromColor(bg).lightness;
+            switch (colorCondition) {
+              case ColorCondition.all:
+                return HSLColor.fromAHSL(
+                        hslColor.alpha,
+                        hslColor.hue,
+                        hslColor.saturation,
+                        1 - (hslColor.lightness * (1 - backGroundLightness)))
+                    .toColor();
+
+              case ColorCondition.bw:
+                if (hslColor.lightness > 0.95 && hslColor.saturation < 0.02) {
+                  return HSLColor.fromAHSL(
+                          hslColor.alpha, 0, 0, backGroundLightness)
+                      .toColor();
+                }
+                return color;
+
+              case ColorCondition.gray:
+                if (hslColor.saturation < 0.02 || hslColor.lightness < 0.02) {
+                  return HSLColor.fromAHSL(hslColor.alpha, 0, 0,
+                          1 - (hslColor.lightness * (1 - backGroundLightness)))
+                      .toColor();
+                }
+                return color;
+            }
+        }
+        return color;
+      });
+    }
+
+    final Picture pic = svgRoot.toPicture(
+      clipToViewBox: false,
+      colorFilter: colorFilter,
+    );
+
+    return PictureInfo(
+      picture: pic,
+      viewport: svgRoot.viewport.viewBoxRect,
+      size: svgRoot.viewport.size,
+    );
+  }
+}
+
+enum ThemingMethod { none, filter, wToBg, invertBrightness }
+
+enum ThemingCondition { none, noBackground }
+
+enum ColorCondition { all, bw, gray }
+
+/// Tests if the [Drawable] [draw] has a non transparent background
+///
+/// [width] and [height] specify the area for a drawable to fill to be a valid background.
+/// Use [minAlpha] and [maxBorder] to accept imperfect Backgrounds.
+/// [maxBorder] is the fraction of the [width]/[height] that does not need to be filled.
+/// Use [maxDepth] to controll how deep the Tree is traversed to find a background,
+/// `maxDepth = -1` will traverse the whole Tree.
+bool _hasBackground(Drawable draw, double width, double height,
+    {minAlpha = 0.99, maxBorder = 0.01, maxDepth = 10}) {
+  if (maxDepth == 0) {
+    if (draw is DrawableShape) {
+      final drawShape = draw;
+      return drawShape.style.fill != null &&
+          drawShape.style.fill.color.alpha > minAlpha &&
+          [
+            Offset(width * maxBorder, height * maxBorder),
+            Offset(width - width * maxBorder, height * maxBorder),
+            Offset(width - width * maxBorder, height - height * maxBorder),
+            Offset(width * maxBorder, height - height * maxBorder),
+          ].every(drawShape.path.contains);
+    }
+    // TODO Allow for two shapes to be the background together
+    if (draw is DrawableParent) {
+      final drawParent = draw;
+      return drawParent.children.any((element) => _hasBackground(
+          element, width, height,
+          minAlpha: minAlpha, maxBorder: maxBorder, maxDepth: maxDepth - 1));
+    }
+  }
+  return false;
+}
+
+Drawable _themeDrawable(
+    Drawable draw, Color Function(Color color) transformColor) {
+  if (draw is DrawableStyleable && !(draw is DrawableGroup)) {
+    final DrawableStyleable drawStylable = draw;
+    draw = drawStylable.mergeStyle(DrawableStyle(
+        stroke: drawStylable.style.stroke != null &&
+                drawStylable.style.stroke.color != null
+            ? DrawablePaint.merge(
+                DrawablePaint(drawStylable.style.stroke.style,
+                    color: transformColor(drawStylable.style.stroke.color)),
+                drawStylable.style.stroke)
+            : null,
+        fill: drawStylable.style.fill != null &&
+                drawStylable.style.fill.color != null
+            ? DrawablePaint.merge(
+                DrawablePaint(drawStylable.style.fill.style,
+                    color: transformColor(drawStylable.style.fill.color)),
+                drawStylable.style.fill)
+            : null));
+  }
+  if (draw is DrawableParent) {
+    final DrawableParent drawParent = draw;
+    final children = drawParent.children
+        .map((e) => _themeDrawable(e, transformColor))
+        .toList(growable: false);
+    if (draw is DrawableRoot) {
+      final DrawableRoot drawRoot = draw;
+      draw = DrawableRoot(drawRoot.id, drawRoot.viewport, children,
+          drawRoot.definitions, drawRoot.style,
+          transform: drawRoot.transform);
+    } else if (draw is DrawableGroup) {
+      final DrawableGroup drawGroup = draw;
+      draw = DrawableGroup(drawGroup.id, children, drawGroup.style,
+          transform: drawGroup.transform);
+    }
+  }
+  return draw;
+}
+
+Widget _themeFilter(Widget widget, Color background) {
+  final lightness = (1 - HSLColor.fromColor(background).lightness) * 255;
+  // TODO Switch to HSL Filter, when available (https://github.com/flutter/flutter/issues/76729)
+  final stack = ColorFiltered(
+      colorFilter: ColorFilter.matrix(<double>[
+        -(lightness / 255), 0, 0, 0, 255, // R
+        0, -(lightness / 255), 0, 0, 255, // G
+        0, 0, -(lightness / 255), 0, 255, // B
+        0, 0, 0, 1, 0 // A
+      ]),
+      child: widget);
+  return stack;
+}
diff --git a/lib/widgets/markdown_renderer.dart b/lib/widgets/markdown_renderer.dart
index bbb0a610..a36ee4e3 100644
--- a/lib/widgets/markdown_renderer.dart
+++ b/lib/widgets/markdown_renderer.dart
@@ -31,7 +31,7 @@ import 'package:gitjournal/folder_views/common.dart';
 import 'package:gitjournal/utils.dart';
 import 'package:gitjournal/utils/link_resolver.dart';
 import 'package:gitjournal/utils/logger.dart';
-import 'package:gitjournal/widgets/image_renderer.dart';
+import 'package:gitjournal/widgets/images/markdown_image.dart';
 
 class MarkdownRenderer extends StatelessWidget {
   final Note note;
@@ -117,7 +117,7 @@ class MarkdownRenderer extends StatelessWidget {
           );
         }
       },
-      imageBuilder: (url, title, alt) => ThemableImage(
+      imageBuilder: (url, title, alt) => MarkdownImage(
           url, note.parent.folderPath + p.separator,
           titel: title, altText: alt),
       extensionSet: markdownExtensions(),
diff --git a/pubspec.lock b/pubspec.lock
index 771636b7..f1b9c8f0 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -771,6 +771,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "4.0.2"
+  photo_view:
+    dependency: "direct main"
+    description:
+      name: photo_view
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.10.3"
   platform:
     dependency: transitive
     description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 0f17fb44..37534d7c 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -59,6 +59,7 @@ dependencies:
   package_info: ">=0.4.1 <2.0.0"
   path: ^1.6.2
   permission_handler: ^6.1.1
+  photo_view: ^0.10.3
   provider: ^4.3.2+2
   quick_actions: ^0.4.0+10
   receive_sharing_intent: ^1.4.0+2