diff --git a/default_image.jpg b/default_image.jpg
new file mode 100644
index 0000000..414b225
Binary files /dev/null and b/default_image.jpg differ
diff --git a/libstegano.py b/libstegano.py
new file mode 100644
index 0000000..e2c7164
--- /dev/null
+++ b/libstegano.py
@@ -0,0 +1,145 @@
+"""
+A library for the steganography program
+Copyright (C) 2022 Valentin Moguérou
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+
+from typing import BinaryIO
+from PIL import Image
+
+# ================= BINARY OPERATIONS =================
+
+def write_lsb_in_bin(byte, bit):
+ "Write the specified bit in the LSB of the specified byte"
+ return byte & 254 | bit if bit==1 else byte & 254
+
+def read_lsb_in_bin(byte):
+ return byte & 1
+
+def combine_bits_to_byte(bits):
+ """
+ Turns a sequence of bits into an integer.
+ Example: turns [0, 1, 1, 0, 1, 1, 0, 1] into 0b01101101 or 109
+ """
+ return sum(bits[-1-i]<>(7-i)&1 for i in range(8)]
+
+def hex_print(byte_list, margin=0, line_width=16):
+ "Prints a byte list in hexadecimal"
+
+ for line in range(0, len(byte_list), line_width):
+ print(' '*margin + ' '.join(f'{byte:02X}' for byte in byte_list[line:line+line_width]))
+
+# ================= STRING OPERATIONS =================
+
+def encode_string(string: str) -> bytearray:
+ "Turns the given string into a byte array"
+ return bytearray(ord(ch) for ch in string+'\0')
+
+def decode_string(bytelist: bytearray) -> str:
+ "Turns the given byte array into a string"
+ return ''.join(chr(b) for b in bytelist)
+
+# ================= WRITE OPERATIONS =================
+
+def write_band(band: bytearray, byte_list: bytearray, starting_pos=0) -> bool:
+ "Writes a byte sequence in the given band"
+
+ for index in range(starting_pos, len(band), 8):
+ band[index:index+8] = (write_lsb_in_bin(band[index+i], bit) for i, bit in enumerate(split_byte_into_bits(byte_list[index//8])))
+ if byte_list[index//8] == 0:
+ return True, index+8 # finished writing
+ return False, index+8 # didn't finish writing, return last index
+
+def write_image(img: Image.Image, byte_list, verbose=False):
+ data = [bytearray(band.tobytes()) for band in img.split()]
+
+ ok, position = write_band(data[0], byte_list)
+ if not ok:
+ ok, position = write_band(data[1], byte_list, position)
+ if not ok:
+ ok, position = write_band(data[2], byte_list, position)
+ if not ok:
+ raise ValueError("The byte sequence is too long.")
+
+ if verbose:
+ print(f"{position} bits, {position//8} bytes successfully written.")
+
+ return Image.merge(img.mode, [Image.frombytes('L', img.size, bytes(band)) for band in data])
+
+def write_file(from_file: BinaryIO, to_file: BinaryIO, byte_list, verbose=False):
+ write_image(Image.open(from_file), byte_list, verbose=verbose).save(to_file)
+
+# ================= READ OPERATIONS =================
+
+def read_band(band: bytes, byte_list: bytearray) -> bool:
+ "Turns a sequence of pixels into a byte list"
+
+ for i in range(0, len(band), 8):
+ bits = [read_lsb_in_bin(b) for b in band[i:i+8]]
+ if all(b==0 for b in bits):
+ return True # finished reading
+ byte_list.append(combine_bits_to_byte(bits))
+ return False # didn't finish reading
+
+def read_image(img: Image.Image, verbose=False):
+ "Read the whole image"
+
+ data = [band.tobytes() for band in img.split()]
+ byte_list = bytearray()
+
+ if read_band(data[0], byte_list):
+ if verbose:
+ print("Successfully finished reading.")
+ elif read_band(data[1], byte_list):
+ if verbose:
+ print("Successfully finished reading.")
+ print("Read the whole red band.")
+ elif read_band(data[1], byte_list):
+ if verbose:
+ print("Successfully finished reading.")
+ print("Read the whole green band.")
+ else:
+ if verbose:
+ print("Read the whole blue band.")
+ raise ValueError("Invalid image, did not find end control sequence.")
+
+ return byte_list
+
+def read_file(from_file: BinaryIO, verbose=False):
+ return read_image(Image.open(from_file), verbose=verbose)
+
+# ================= EXAMPLE PROGRAM =================
+
+def main():
+ s = "This is a string"
+ oldimg = Image.open('guitar.jpg')
+ oldimg.load()
+ print(encode_string(s))
+ newimg = write_image(oldimg, encode_string(s))
+ newimg.save('dissimulated.png', 'PNG')
+ print('Written...')
+
+ print(f"String: --> {read_image(Image.open('guitar.jpg'))} (guitar.jpg)")
+ print(f"String: --> {decode_string(read_image(Image.open('dissimulated.png')))} (dissimulated.png)")
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/steganocli.py b/steganocli.py
new file mode 100644
index 0000000..eb87ca5
--- /dev/null
+++ b/steganocli.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+
+"""
+A command-line interface for the steganography program
+Copyright (C) 2022 Valentin Moguérou
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+
+from argparse import ArgumentParser as ArgParser
+from pathlib import Path
+
+from libstegano import read_file, write_file, decode_string, encode_string, hex_print
+
+def read(args):
+ if args.verbose:
+ print("Read mode enabled.")
+
+ with open(args.from_file, 'rb') as from_file:
+ byte_list = read_file(from_file, verbose=args.verbose)
+
+ if args.verbose:
+ print(f"Read {(len(byte_list)+1)*8} bits, {len(byte_list)+1} bytes.")
+ print("======== BEGIN MESSAGE ========")
+ print(decode_string(byte_list))
+ print("========= END MESSAGE =========")
+ else:
+ print(decode_string(byte_list))
+
+def write(args):
+ byte_list = encode_string(args.string)
+
+ with open(args.from_file, 'rb') as from_file, open(args.to_file, 'wb') as to_file:
+ if args.verbose:
+ print("Write mode enabled.")
+ print("================== BYTE STREAM ==================")
+ hex_print(byte_list, margin=1)
+ print("================== BYTE STREAM ==================")
+
+ write_file(from_file, to_file, byte_list, verbose=args.verbose)
+
+def main():
+ parser = ArgParser()
+ parser.add_argument('-v', '--verbose', action='store_true')
+
+ subparsers = parser.add_subparsers(required=True)
+
+ parser_read = subparsers.add_parser('read', help='Retrieve data from an image')
+ parser_read.add_argument('from_file', type=Path)
+ parser_read.set_defaults(func=read)
+
+ parser_write = subparsers.add_parser('write', help='Dissimulate data in an image')
+ parser_write.add_argument('string', type=str)
+ parser_write.add_argument('to_file', type=Path)
+ parser_write.add_argument('--from', type=Path, dest='from_file', default=Path('default_image.jpg'))
+ parser_write.set_defaults(func=write)
+
+ args = parser.parse_args()
+ args.func(args)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file