Skip to content

Instantly share code, notes, and snippets.

@dhondta
Created July 3, 2026 23:43
Show Gist options
  • Select an option

  • Save dhondta/7a7339eb5f5de61c6bd1365ace6e1581 to your computer and use it in GitHub Desktop.

Select an option

Save dhondta/7a7339eb5f5de61c6bd1365ace6e1581 to your computer and use it in GitHub Desktop.

StegoEMD

This Tinyscript-based tool allows to apply steganography based on EMD (Exploiting Modification Direction) in order to retrieve hidden data from an image.

$ pip install tinyscript
$ tsm install stegoemd

This tool is especially useful in the use cases hereafter.

Extract hidden data from an image using EMD stegano

$ stegopvd extract image.png --length 1024
12:34:56 [INFO] Hidden data:
[...]

Hide data into an image using EMD stegano

$ stegopvd hide image.png secret.txt
12:34:56 [INFO] Image saved to 'image-hidden.png'
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
from PIL import Image
from tinyscript import *
__author__ = "Alexandre D'Hondt"
__version__ = "1.0"
__copyright__ = ("A. D'Hondt", 2026)
__license__ = "gpl-3.0"
__examples__ = ["extract image.png -l 892"]
__doc__ = """
*StegoEMD* allows to apply steganography based on EMD (Exploiting Modification Direction) in order to retrieve hidden
data from an image.
"""
BANNER_FONT = "standard"
BANNER_STYLE = {'fgcolor': "lolcat"}
SCRIPTNAME_FORMAT = "none"
class EMD:
""" Original EMD algorithm (Zhang & Wang, 2006) """
def __init__(self, image, mode="RGB", channel=None, **kw):
self.__image = image
self.__channel = channel
self.__mode = mode
self.__obj = Image.open(image).convert(mode)
logger.debug("Image size: {}x{}".format(*self.__obj.size))
try:
self.__channel_idx = str(self.__mode).index(self.__channel)
except:
self.__channel_idx = -1
def __pixv(self, pixel):
return pixel[self.__channel_idx] if isinstance(pixel, (list, tuple)) else pixel
def extract(self, length=1024, write=False, **kw):
digits, p, i = [], list(self.__obj.getdata()), -2
for k in range(3*length):
i += 2
g1, g2 = self.__pixv(p[i]), self.__pixv(p[i+1])
digits.append((g1 + 2 * g2) % 5)
out = bytearray()
for i in range(0, len(digits) // 3 * 3, 3):
out.append(digits[i] * 25 + digits[i+1] * 5 + digits[i+2])
data = out.decode("latin1", errors="replace")
logger.info(f"Hidden data:\n{data}")
if write:
with open(self.secret, 'ab') as f:
f.write(b(data))
logger.warning(f"Secret saved to '{self.secret}'")
def hide(self, data, **kw):
if os.path.exists(data):
with open(data, 'rb') as f:
data = f.read()
digits = []
for byte in b(data):
digits.extend([byte // 25, (byte // 5) % 5, byte % 5])
p = list(self.__obj.getdata())
if len(digits) > (capacity := len(p) // 2):
logger.error(f"Message too large ({len(digits)} digits, capacity={capacity})")
return
for pair_index, target_digit in enumerate(digits):
p1 = 2 * pair_index
p2 = p1 + 1
g1, g2 = self.__pixv(p[p1]), self.__pixv(p[p2])
current = (g1 + 2 * g2) % 5
# Already encodes the desired digit
if current == target_digit:
continue
delta = (target_digit - current) % 5
# Zhang & Wang modification table
if delta == 1:
if g1 < 255:
g1 += 1
else:
g1 -= 1
elif delta == 2:
if g2 < 255:
g2 += 1
else:
g2 -= 1
elif delta == 3:
if g2 > 0:
g2 -= 1
else:
g2 += 1
elif delta == 4:
if g1 > 0:
g1 -= 1
else:
g1 += 1
if isinstance(p[p1], (list, tuple)):
pix1, pix2 = list(p[p1]), list(p[p2])
pix1[self.__channel_idx], pix2[self.__channel_idx] = g1, g2
p[p1], p[p2] = tuple(pix1), tuple(pix2)
else:
p[p1], p[p2] = g1, g2
img = Image.new(self.__mode, self.__obj.size)
img.putdata(p)
img.save(self.image)
logger.warning(f"Image saved to '{self.image}'")
return self
@property
def basename(self):
return os.path.splitext(os.path.basename(self.__image))[0]
@property
def image(self):
return f"{self.basename}-hidden.png"
@property
def secret(self):
return f"{self.basename}-secret.txt"
if __name__ == "__main__":
subparsers = parser.add_subparsers(help="commands", dest="command")
extract = subparsers.add_parser('extract', help="extract hidden data from the target image")
hide = subparsers.add_parser('hide', help="hide data in the target image")
extract.add_argument("image", type=ts.file_exists, help="image path")
extract.add_argument("-c", "--channel", help="channel to be considered (if relevant)",
note="e.g. use 'A' when using mode 'RGBA' or 'B' if using 'RGB'")
extract.add_argument("-l", "--length", default=1024, type=int, help="number of EMD digits to extract")
extract.add_argument("-m", "--mode", default="RGB", help="image mode")
extract.add_argument("-w", "--write", action="store_true", help="write data to a file")
hide.add_argument("image", type=ts.file_exists, help="image path")
hide.add_argument("data", help="data to be hidden")
initialize(noargs_action="demo")
getattr(EMD(**vars(args)), args.command)(**vars(args))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment