diff --git a/adafruit_pycamera/__init__.py b/adafruit_pycamera/__init__.py
index 6b3309d..d019cf5 100644
--- a/adafruit_pycamera/__init__.py
+++ b/adafruit_pycamera/__init__.py
@@ -5,7 +5,7 @@
 """Library for the Adafruit PyCamera with OV5640 autofocus module"""
 
 # pylint: disable=too-many-lines
-
+import gc
 import os
 import struct
 import time
@@ -149,7 +149,7 @@ class PyCameraBase:  # pylint: disable=too-many-instance-attributes,too-many-pub
         espcamera.FrameSize.QVGA,  # 320x240
         # espcamera.FrameSize.CIF, # 400x296
         # espcamera.FrameSize.HVGA, # 480x320
-        espcamera.FrameSize.VGA,  #  640x480
+        espcamera.FrameSize.VGA,  # 640x480
         espcamera.FrameSize.SVGA,  # 800x600
         espcamera.FrameSize.XGA,  # 1024x768
         espcamera.FrameSize.HD,  # 1280x720
@@ -232,6 +232,12 @@ def __init__(self) -> None:  # pylint: disable=too-many-statements
         self.display = None
         self.pixels = None
         self.sdcard = None
+        self._last_saved_image_filename = None
+        self.decoder = None
+        self._overlay = None
+        self.overlay_transparency_color = None
+        self.overlay_bmp = None
+        self.combined_bmp = None
         self.splash = displayio.Group()
 
         # Reset display and I/O expander
@@ -827,6 +833,7 @@ def open_next_image(self, extension="jpg"):
                 os.stat(filename)
             except OSError:
                 break
+        self._last_saved_image_filename = filename
         print("Writing to", filename)
         return open(filename, "wb")
 
@@ -857,6 +864,89 @@ def capture_jpeg(self):
         else:
             print("# frame capture failed")
 
+    @property
+    def overlay(self) -> str:
+        """
+        The overlay file to be used. A filepath string that points
+        to a .bmp file that has 24bit RGB888 Colorspace.
+        The overlay image will be shown in the camera preview,
+        and combined to create a modified version of the
+        final photo.
+        """
+        return self._overlay
+
+    @overlay.setter
+    def overlay(self, new_overlay_file: str) -> None:
+        # pylint: disable=import-outside-toplevel
+        from displayio import ColorConverter, Colorspace
+        import ulab.numpy as np
+        import adafruit_imageload
+
+        if self.overlay_bmp is not None:
+            self.overlay_bmp.deinit()
+        self._overlay = new_overlay_file
+        cc888 = ColorConverter(input_colorspace=Colorspace.RGB888)
+        self.overlay_bmp, _ = adafruit_imageload.load(new_overlay_file, palette=cc888)
+
+        arr = np.frombuffer(self.overlay_bmp, dtype=np.uint16)
+        arr.byteswap(inplace=True)
+
+        del arr
+
+    def _init_jpeg_decoder(self):
+        # pylint: disable=import-outside-toplevel
+        from jpegio import JpegDecoder
+
+        """
+        Initialize the JpegDecoder if it hasn't been already.
+        Only needed if overlay is used.
+        """
+        if self.decoder is None:
+            self.decoder = JpegDecoder()
+
+    def blit_overlay_into_last_capture(self):
+        """
+        Create a modified version of the last photo taken that pastes
+        the overlay image on top of the photo and saves the new version
+        in a separate but similarly named .bmp file on the SDCard.
+        """
+        if self.overlay_bmp is None:
+            raise ValueError(
+                "Must set overlay before calling blit_overlay_into_last_capture"
+            )
+        # pylint: disable=import-outside-toplevel
+        from adafruit_bitmapsaver import save_pixels
+        from displayio import Bitmap, ColorConverter, Colorspace
+
+        self._init_jpeg_decoder()
+
+        width, height = self.decoder.open(self._last_saved_image_filename)
+        photo_bitmap = Bitmap(width, height, 65535)
+
+        self.decoder.decode(photo_bitmap, scale=0, x=0, y=0)
+
+        bitmaptools.blit(
+            photo_bitmap,
+            self.overlay_bmp,
+            0,
+            0,
+            skip_source_index=self.overlay_transparency_color,
+            skip_dest_index=None,
+        )
+
+        cc565_swapped = ColorConverter(input_colorspace=Colorspace.RGB565_SWAPPED)
+        save_pixels(
+            self._last_saved_image_filename.replace(".jpg", "_modified.bmp"),
+            photo_bitmap,
+            cc565_swapped,
+        )
+
+        # RAM cleanup
+        photo_bitmap.deinit()
+        del photo_bitmap
+        del cc565_swapped
+        gc.collect()
+
     def continuous_capture_start(self):
         """Switch the camera to continuous-capture mode"""
         pass  # pylint: disable=unnecessary-pass
@@ -901,6 +991,22 @@ def blit(self, bitmap, x_offset=0, y_offset=32):
         The default preview capture is 240x176, leaving 32 pixel rows at the top and bottom
         for status information.
         """
+        # pylint: disable=import-outside-toplevel
+        from displayio import Bitmap
+
+        if self.overlay_bmp is not None:
+            if self.combined_bmp is None:
+                self.combined_bmp = Bitmap(bitmap.width, bitmap.height, 65535)
+
+            bitmaptools.blit(self.combined_bmp, bitmap, 0, 0)
+
+            bitmaptools.rotozoom(
+                self.combined_bmp,
+                self.overlay_bmp,
+                scale=0.75,
+                skip_index=self.overlay_transparency_color,
+            )
+            bitmap = self.combined_bmp
 
         self._display_bus.send(
             42, struct.pack(">hh", 80 + x_offset, 80 + x_offset + bitmap.width - 1)
diff --git a/docs/conf.py b/docs/conf.py
index 3b6c67e..cbf1452 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -41,6 +41,7 @@
     "digitalio",
     "espcamera",
     "fourwire",
+    "jpegio",
     "micropython",
     "neopixel",
     "sdcardio",
diff --git a/docs/mock/displayio.py b/docs/mock/displayio.py
index b4fd1c5..f18251d 100644
--- a/docs/mock/displayio.py
+++ b/docs/mock/displayio.py
@@ -9,3 +9,24 @@ def __init__(self, i):
 
     def __setitem__(self, idx, value):
         self._data[idx] = value
+
+
+class ColorConverter:
+    def __init__(self, colorspace):
+        self._colorspace = colorspace
+
+    def convert(self, color_value) -> int:
+        pass
+
+
+class Bitmap:
+    def __init__(self, width, height, color_count):
+        pass
+
+
+class Colorspace:
+    pass
+
+
+class Display:
+    pass
diff --git a/examples/overlay/code_select.py b/examples/overlay/code_select.py
new file mode 100644
index 0000000..480675e
--- /dev/null
+++ b/examples/overlay/code_select.py
@@ -0,0 +1,91 @@
+# SPDX-FileCopyrightText: Copyright (c) 2023 john park for Adafruit Industries
+# SPDX-FileCopyrightText: Copyright (c) 2024 Tim Cocks for Adafruit Industries
+#
+# SPDX-License-Identifier: MIT
+""" simple point-and-shoot camera example, with overly selecting using select button.
+
+Place all overlay files inside /sd/overlays/ directory.
+"""
+import os
+import time
+import traceback
+import adafruit_pycamera  # pylint: disable=import-error
+
+
+pycam = adafruit_pycamera.PyCamera()
+pycam.mode = 0  # only mode 0 (JPEG) will work in this example
+
+# User settings - try changing these:
+pycam.resolution = 1  # 0-12 preset resolutions:
+#                      0: 240x240, 1: 320x240, 2: 640x480
+
+pycam.led_level = 1  # 0-4 preset brightness levels
+pycam.led_color = 0  # 0-7  preset colors: 0: white, 1: green, 2: yellow, 3: red,
+#                                          4: pink, 5: blue, 6: teal, 7: rainbow
+pycam.effect = 0  # 0-7 preset FX: 0: normal, 1: invert, 2: b&w, 3: red,
+#                                  4: green, 5: blue, 6: sepia, 7: solarize
+
+print("Overlay example camera ready.")
+pycam.tone(800, 0.1)
+pycam.tone(1200, 0.05)
+
+overlay_files = os.listdir("/sd/overlays/")
+cur_overlay_idx = 0
+
+pycam.overlay = f"/sd/overlays/{overlay_files[cur_overlay_idx]}"
+pycam.overlay_transparency_color = 0xE007
+
+overlay_files = os.listdir("/sd/overlays/")
+cur_overlay_idx = 0
+
+while True:
+    pycam.blit(pycam.continuous_capture())
+    pycam.keys_debounce()
+    # print(dir(pycam.select))
+    if pycam.select.fell:
+        cur_overlay_idx += 1
+        if cur_overlay_idx >= len(overlay_files):
+            cur_overlay_idx = 0
+        print(f"changing overlay to {overlay_files[cur_overlay_idx]}")
+        pycam.overlay = f"/sd/overlays/{overlay_files[cur_overlay_idx]}"
+
+    if pycam.shutter.short_count:
+        print("Shutter released")
+        pycam.tone(1200, 0.05)
+        pycam.tone(1600, 0.05)
+        try:
+            pycam.display_message("snap", color=0x00DD00)
+            pycam.capture_jpeg()
+            pycam.display_message("overlay", color=0x00DD00)
+            pycam.blit_overlay_into_last_capture()
+            pycam.live_preview_mode()
+        except TypeError as exception:
+            traceback.print_exception(exception)
+            pycam.display_message("Failed", color=0xFF0000)
+            time.sleep(0.5)
+            pycam.live_preview_mode()
+        except RuntimeError as exception:
+            pycam.display_message("Error\nNo SD Card", color=0xFF0000)
+            time.sleep(0.5)
+
+    if pycam.card_detect.fell:
+        print("SD card removed")
+        pycam.unmount_sd_card()
+        pycam.display.refresh()
+
+    if pycam.card_detect.rose:
+        print("SD card inserted")
+        pycam.display_message("Mounting\nSD Card", color=0xFFFFFF)
+        for _ in range(3):
+            try:
+                print("Mounting card")
+                pycam.mount_sd_card()
+                print("Success!")
+                break
+            except OSError as exception:
+                print("Retrying!", exception)
+                time.sleep(0.5)
+        else:
+            pycam.display_message("SD Card\nFailed!", color=0xFF0000)
+            time.sleep(0.5)
+        pycam.display.refresh()
diff --git a/examples/overlay/code_simple.py b/examples/overlay/code_simple.py
new file mode 100644
index 0000000..ffcea6a
--- /dev/null
+++ b/examples/overlay/code_simple.py
@@ -0,0 +1,74 @@
+# SPDX-FileCopyrightText: Copyright (c) 2023 john park for Adafruit Industries
+# SPDX-FileCopyrightText: Copyright (c) 2024 Tim Cocks for Adafruit Industries
+#
+# SPDX-License-Identifier: MIT
+""" simple point-and-shoot camera example, with an overlay frame image. """
+
+import time
+import traceback
+import adafruit_pycamera  # pylint: disable=import-error
+
+pycam = adafruit_pycamera.PyCamera()
+pycam.mode = 0  # only mode 0 (JPEG) will work in this example
+
+# User settings - try changing these:
+pycam.resolution = 1  # 0-12 preset resolutions:
+#                      0: 240x240, 1: 320x240, 2: 640x480
+
+pycam.led_level = 1  # 0-4 preset brightness levels
+pycam.led_color = 0  # 0-7  preset colors: 0: white, 1: green, 2: yellow, 3: red,
+#                                          4: pink, 5: blue, 6: teal, 7: rainbow
+pycam.effect = 0  # 0-7 preset FX: 0: normal, 1: invert, 2: b&w, 3: red,
+#                                  4: green, 5: blue, 6: sepia, 7: solarize
+
+print("Overlay example camera ready.")
+pycam.tone(800, 0.1)
+pycam.tone(1200, 0.05)
+
+pycam.overlay = "/heart_frame_rgb888.bmp"
+pycam.overlay_transparency_color = 0xE007
+
+while True:
+    pycam.blit(pycam.continuous_capture())
+    pycam.keys_debounce()
+
+    if pycam.shutter.short_count:
+        print("Shutter released")
+        pycam.tone(1200, 0.05)
+        pycam.tone(1600, 0.05)
+        try:
+            pycam.display_message("snap", color=0x00DD00)
+            pycam.capture_jpeg()
+            pycam.display_message("overlay", color=0x00DD00)
+            pycam.blit_overlay_into_last_capture()
+            pycam.live_preview_mode()
+        except TypeError as exception:
+            traceback.print_exception(exception)
+            pycam.display_message("Failed", color=0xFF0000)
+            time.sleep(0.5)
+            pycam.live_preview_mode()
+        except RuntimeError as exception:
+            pycam.display_message("Error\nNo SD Card", color=0xFF0000)
+            time.sleep(0.5)
+
+    if pycam.card_detect.fell:
+        print("SD card removed")
+        pycam.unmount_sd_card()
+        pycam.display.refresh()
+
+    if pycam.card_detect.rose:
+        print("SD card inserted")
+        pycam.display_message("Mounting\nSD Card", color=0xFFFFFF)
+        for _ in range(3):
+            try:
+                print("Mounting card")
+                pycam.mount_sd_card()
+                print("Success!")
+                break
+            except OSError as exception:
+                print("Retrying!", exception)
+                time.sleep(0.5)
+        else:
+            pycam.display_message("SD Card\nFailed!", color=0xFF0000)
+            time.sleep(0.5)
+        pycam.display.refresh()
diff --git a/examples/overlay/heart_frame_rgb888.bmp b/examples/overlay/heart_frame_rgb888.bmp
new file mode 100644
index 0000000..eab3d0a
Binary files /dev/null and b/examples/overlay/heart_frame_rgb888.bmp differ
diff --git a/examples/overlay/heart_frame_rgb888.bmp.license b/examples/overlay/heart_frame_rgb888.bmp.license
new file mode 100644
index 0000000..831eb5c
--- /dev/null
+++ b/examples/overlay/heart_frame_rgb888.bmp.license
@@ -0,0 +1,2 @@
+# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
+# SPDX-License-Identifier: MIT
diff --git a/examples/overlay/pencil_frame_rgb888.bmp b/examples/overlay/pencil_frame_rgb888.bmp
new file mode 100644
index 0000000..e92fc9a
Binary files /dev/null and b/examples/overlay/pencil_frame_rgb888.bmp differ
diff --git a/examples/overlay/pencil_frame_rgb888.bmp.license b/examples/overlay/pencil_frame_rgb888.bmp.license
new file mode 100644
index 0000000..831eb5c
--- /dev/null
+++ b/examples/overlay/pencil_frame_rgb888.bmp.license
@@ -0,0 +1,2 @@
+# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
+# SPDX-License-Identifier: MIT
diff --git a/optional_requirements.txt b/optional_requirements.txt
index d4e27c4..42b579d 100644
--- a/optional_requirements.txt
+++ b/optional_requirements.txt
@@ -1,3 +1,6 @@
 # SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries
 #
 # SPDX-License-Identifier: Unlicense
+
+adafruit-circuitpython-bitmapsaver
+adafruit-circuitpython-imageload