Skip to content

Instantly share code, notes, and snippets.

Last active April 23, 2024 04:39
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save dhondta/feaf4f5fb3ed8d1eb7515abe8cde4880 to your computer and use it in GitHub Desktop.
Save dhondta/feaf4f5fb3ed8d1eb7515abe8cde4880 to your computer and use it in GitHub Desktop.
Tinyscript steganography tool implementing the Pixel Value Differencing algorithm


This Tinyscript-based tool allows to apply steganography based on PVD (Pixel Value Differencing) in order to retrieve hidden data from an image.

$ pip install tinyscript
$ tsm install stegopvd

This tool is especially useful in the use cases hereafter.

Extract hidden data from an image using PVD stegano

$ stegopvd -v extract test.png --zigzag
12:34:56 [DEBUG] Image size: 600x800
12:34:56 [DEBUG] Channels RGB, zigzag True
12:34:56 [INFO] Hidden data:

Bruteforce PVD stegano parameters to recover hidden data from an image

This will display readable strings recovered using bruteforced parameters.

$ stegopvd bruteforce test.png
12:34:56 [INFO] [...]
# -*- coding: UTF-8 -*-
from PIL import Image
from tinyscript import *
__author__ = "Alexandre D'Hondt"
__version__ = "1.1"
__copyright__ = ("A. D'Hondt", 2020)
__license__ = "gpl-3.0"
__examples__ = ["extract test.png -z", "bruteforce test.png -c"]
__doc__ = """
*StegoPVD* allows to apply steganography based on PVD (Pixel Value Differencing) in order to retrieve hidden data from an image.
BANNER_FONT = "standard"
BANNER_STYLE = {'fgcolor': "lolcat"}
class PVD(object):
def __init__(self, image, secret=None):
self.__image = image
self.__secret = secret
self.__obj ='RGB')
logger.debug("Image size: {}x{}".format(*self.__obj.size))
def bruteforce(self, channels=False, nchars=16):
for ch in (ts.bruteforce(3, "RGB", repeat=False) if channels else \
for zigzag in [True, False]:
self.extract(ch, zigzag)
for s in ts.strings(, nchars):
if self.__secret:
def extract(self, channels="B", zigzag=True):
logger.debug("Channels {}, zigzag {}".format(channels, zigzag))
i = self.__obj
w, h = i.size
data = ""
for y in range(h):
for x in range(1, w, 2):
if zigzag and (y % 2 == 1):
x = -x % w
pixel = {k: v for k, v in zip("RGB", i.getpixel((x, y)))}
prev_pixel = {k: v for k, v in zip("RGB", i.getpixel((x-1, y)))}
for c in channels:
d = int(abs(pixel[c] - prev_pixel[c]))
if 0 <= d <= 7:
b, lower = 3, 0
elif 8 <= d <= 15:
b, lower = 3, 8
elif 16 <= d <= 31:
b, lower = 4, 16
elif 32 <= d <= 63:
b, lower = 5, 32
elif 64 <= d <= 127:
b, lower = 6, 64
elif 128 <= d <= 255:
b, lower = 7, 128
data += bin(d - lower)[2:].zfill(b) = ts.bin2str(data)
def hide(self, data):
bin_data = ts.str2bin(data)
#TODO: implement hiding data
bin_len = ts.int2bin(len(bin_data))
return self
def write(self, filename=None, content=None):
filename = self.__secret or filename
if filename is None:
filename = os.path.basename(self.__image)
filename, _ = os.path.splitext(filename)
filename = "{}-secret.txt".format(filename)
with open(filename, 'ab') as f:
f.write(b(content or
return self
if __name__ == "__main__":
parser.add_argument("-w", "--write", help="write data to a file")
subparsers = parser.add_subparsers(help="commands", dest="command")
extract = subparsers.add_parser('extract',
help="manually extract hidden data")
bruteforce = subparsers.add_parser('bruteforce', help="bruteforce "
"parameters for extracting hidden data")
extract.add_argument("image", type=ts.file_exists, help="image path")
extract.add_argument("-c", "--channels", default="RGB",
help="channels to be considered")
extract.add_argument("-z", "--zigzag", action="store_true",
help="apply Zig-Zag Traversing Scheme (ZZTS)")
bruteforce.add_argument("image", type=ts.file_exists, help="image path")
bruteforce.add_argument("-c", "--channels", action="store_true",
help="bruteforce the color channels",
note="if false, RGB are considered")
bruteforce.add_argument("-n", "--nchars", type=ts.pos_int, default=16,
help="minimal length for readable strings")
p = PVD(args.image, args.write)
if args.command == "bruteforce":
elif args.command == "extract":
p.extract()"Hidden data:\n" +
if args.write:
Copy link

since python3.9 and above it generates an HTMLParser error as the unescape() method has been removed from the html.parser.HTMParser class.
more details here:

example on my python 3.10

File "/usr/local/lib/python3.10/dist-packages/mdv/", line 970, in formatter
    t = html_parser.unescape(t)
AttributeError: 'HTMLParser' object has no attribute 'unescape'

Copy link

Yunoon commented Sep 1, 2022

some error happend, and this is solution.

cannot import name 'etree' from 'markdown.util'

Python-Markdown/markdown#1284 (comment)

Copy link

I'm confused. It looks like you're running stegopvd straight from the shell after the following?

$ pip install tinyscript
$ tsm install stegopvd

I don't seem to be able to run it this way.

Copy link

dhondta commented Feb 10, 2024

@thelulzy Thank you for mentioning. This came from a bug in Tinyscript. Please Pip-upgrade to version 1.30.7 and it should work fine ;

$ pip install --upgrade tinyscript
$ tsm install --force stegopvd
$ stegopvd --help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment