diff --git a/CHANGELOG.md b/CHANGELOG.md index de68d80..df09217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 0.0.3 +- Added test image generation to GH0STB1T imagestego + ## 0.0.2 - 2026-02-05 ### Added diff --git a/README.md b/README.md index 9f78531..60a6512 100644 --- a/README.md +++ b/README.md @@ -218,9 +218,26 @@ ghostbit image analyze -i +
+Create Test Files + +
+ + +```bash +# Audio Creation for Testing +ghostbit audio test -o test_audio + +# Image Creation for Testing +ghostbit image test -o test_images + +``` + +
+
-### šŸ”— Python API +### Python API GH0STB1T provides a Python API for seamless integration into existing applications and workflows. diff --git a/pyproject.toml b/pyproject.toml index c3a37ce..edd1025 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ dependencies = [ "PyWavelets>=1.4.0", "argon2-cffi>=25.1.0", "cryptography>=46.0.3", + "svgwrite>=1.4.3", ] [project.urls] @@ -86,7 +87,8 @@ image = [ "scipy>=1.11.0", "PyWavelets>=1.4.0", "argon2-cffi>=25.1.0", - "cryptography>=46.0.3" + "cryptography>=46.0.3", + "svgwrite>=1.4.3" ] # video = [ diff --git a/src/ghostbit/audiostego/cli/audiostego_cli.py b/src/ghostbit/audiostego/cli/audiostego_cli.py index b70d6dd..00c4305 100644 --- a/src/ghostbit/audiostego/cli/audiostego_cli.py +++ b/src/ghostbit/audiostego/cli/audiostego_cli.py @@ -430,11 +430,9 @@ def info_command(self) -> Optional[int]: logger.debug("Info display complete") return 0 - def create_test_files_command( - self, output_dir: str, create_carrier: bool - ) -> Optional[int]: + def create_test_files_command(self, output_dir: str) -> Optional[int]: """Create test files for demonstration""" - logger.info(f"Creating test files in {output_dir}, carrier={create_carrier}") + logger.info(f"Creating test files in {output_dir}") self._print_header("Creating Test Files", "šŸ“„") @@ -465,68 +463,63 @@ def create_test_files_command( print(f"āŒ Error creating text files: {e}") return 1 - if create_carrier: + try: + logger.info("Creating carrier audio files") + print("\nšŸŽµ Creating test carrier WAV file...") + + wav_path = f"{output_dir}/test_carrier.wav" + logger.debug(f"Creating WAV file: {wav_path}") + + with wave.open(wav_path, "w") as wav: + wav.setnchannels(1) # Mono + wav.setsampwidth(2) # 16-bit + wav.setframerate(44100) # 44.1kHz + + duration = 5 + logger.debug(f"Generating {duration} seconds of audio") + for i in range(44100 * duration): + value = int(32767 * 0.3 * math.sin(2 * math.pi * 440 * i / 44100)) + wav.writeframes(struct.pack(" Optional[int]: {C.BOLD}{C.BLUE}Analyze:{C.RESET} {C.BOLD}{C.PINK}ghostbit audio{C.RESET} {C.GREEN}analyze{C.RESET} {C.GREEN}-i{C.RESET} {C.CYAN}audio.wav{C.RESET} {C.GREEN}-v{C.RESET} - + + {C.BOLD}{C.BLUE}Test Audio Creation:{C.RESET} + {C.BOLD}{C.PINK}ghostbit audio{C.RESET} {C.GREEN}test{C.RESET} {C.GREEN}-o{C.RESET} {C.CYAN}test_audio{C.RESET} """, ) @@ -769,7 +764,7 @@ def main() -> Optional[int]: "test", formatter_class=ColorHelpFormatter, add_help=False, - help=f"{C.CYAN}Create test secret files{C.RESET}", + help=f"{C.CYAN}Create test audio files{C.RESET}", ) test_parser.add_argument( "-h", "--help", action="help", help=f"{C.CYAN}Show help message{C.RESET}" @@ -778,14 +773,9 @@ def main() -> Optional[int]: "-o", "--output_dir", required=False, - default="testcases", + default="test_audio", help=f"{C.CYAN}Output folder for test files{C.RESET}", ) - test_parser.add_argument( - "--create-carrier", - action="store_true", - help=f"{C.CYAN}Create a test carrier audio files (AIFF|WAV|MP3|FLAC|M4A){C.RESET}", - ) test_parser.add_argument( "-v", "--verbose", @@ -826,12 +816,13 @@ def main() -> Optional[int]: ) elif args.subparser_command == "capacity": return cli.capacity_command(args.input_file, args.quality) + elif args.subparser_command == "info": return cli.info_command() + elif args.subparser_command == "test": - return cli.create_test_files_command( - args.output_dir, create_carrier=args.create_carrier - ) + return cli.create_test_files_command(args.output_dir) + else: parser.print_help() return 1 diff --git a/src/ghostbit/imagestego/cli/imagestego_cli.py b/src/ghostbit/imagestego/cli/imagestego_cli.py index df9e061..3767f03 100755 --- a/src/ghostbit/imagestego/cli/imagestego_cli.py +++ b/src/ghostbit/imagestego/cli/imagestego_cli.py @@ -11,7 +11,9 @@ Colors as C, ) from ghostbit.imagestego.core.image_multiformat_coder import ( + ImageGenerator, ImageMultiFormatCoder, + ImageTestCreationException, ImageMultiFormatCoderException, ) @@ -32,7 +34,7 @@ def _print_header(self, title: str, emoji: Optional[str] = None) -> None: print(f"\n{emoji} {C.BOLD}{C.BRIGHT_BLUE}{title}{C.RESET}") else: print(f"\n{C.BOLD}{C.BRIGHT_BLUE}{title}{C.RESET}") - print(f"{C.WHITE}{'─' * 70}{C.RESET}\n") + print(f"{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}\n") def encode_command( self, @@ -81,7 +83,7 @@ def encode_command( password=password, show_stats=show_stats, ) - print(f"{C.WHITE}{'─' * 70}{C.RESET}\n") + print(f"{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}\n") return 0 except ImageMultiFormatCoderException as e: @@ -121,7 +123,7 @@ def decode_command( self._print_header("Decoding Files", "šŸ”“") stego = ImageMultiFormatCoder() stego.decode(input_file, output_filepath, password) - print(f"{C.WHITE}{'─' * 70}{C.RESET}\n") + print(f"{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}\n") return 0 except ImageMultiFormatCoderException as e: @@ -151,7 +153,7 @@ def capacity_command(self, input_file: str) -> Optional[int]: print(f" • {result['capacity_bytes']:,} bytes") print(f" • {result['capacity_kb']:.2f} KB") print(f" • {result['capacity_mb']:.2f} MB") - print(f"\n{C.WHITE}{'─' * 70}{C.RESET}\n") + print(f"\n{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}\n") return 0 except ImageMultiFormatCoderException as e: @@ -183,26 +185,40 @@ def analyze_command( print("\nšŸ” Steganography Details:") print( - f" • Hidden Data: {'āœ“ YES' if result['has_hidden_data'] else 'āœ— NO'}" + f" • Hidden Data: {'āœ“ YES' if result['has_hidden_data'] else 'āœ— NO'}" ) if result["has_hidden_data"]: print( - f" • Algorithm: {result['algorithm'].name if result['algorithm'] else 'Unknown'}" + f" • Algorithm: {result['algorithm'].name if result['algorithm'] else 'Unknown'}" ) - print(f" • Encrypted: {'Yes' if result['encrypted'] else 'No'}") + print(f" • Encrypted: {'Yes' if result['encrypted'] else 'No'}") print("\nšŸ’” Next Steps:") if result["encrypted"]: - print(f" • ghostbit image decode -i {input_file} -p") + print(f" • ghostbit image decode -i {input_file} -p") else: - print(f" • ghostbit image decode -i {input_file}") - print(f"\n{C.WHITE}{'─' * 70}{C.RESET}\n") + print(f" • ghostbit image decode -i {input_file}") + print(f"\n{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}\n") return 0 except ImageMultiFormatCoderException as e: logger.error( f"Image Analysis failed with ImageMultiFormatCoderException: {e}" ) - print(f"\nāŒ Image Analysis failed: {e}") + print(f"\n Image Analysis failed: {e}") + return 1 + + def create_test_files_command(self, output_dir: str): + self._print_header("Creating Test Images", "šŸ“") + try: + outputdir = os.path.join("output", output_dir) + logger.debug(f"Output directory: {outputdir}") + print(f"šŸ“ Output Directory: '{outputdir}'\n") + image_gen = ImageGenerator(out_dir=outputdir) + image_gen.generate_all() + print(f"\n{C.BRIGHT_BLUE}{'─' * 70}{C.RESET}\n") + except ImageTestCreationException as e: + logger.error(f"Image Test File Creation failed: {e}") + print(f"\n Image Test File Creation failed: {e}") return 1 @@ -229,7 +245,9 @@ def main(): {C.BOLD}{C.BLUE}Analyze:{C.RESET} {C.BOLD}{C.PINK}ghostbit image{C.RESET} {C.GREEN}analyze{C.RESET} {C.GREEN}-i{C.RESET} {C.CYAN}suspicious.webp{C.RESET} {C.GREEN}-v{C.RESET} - + + {C.BOLD}{C.BLUE}Test Image Creation:{C.RESET} + {C.BOLD}{C.PINK}ghostbit image{C.RESET} {C.GREEN}test{C.RESET} {C.GREEN}-o{C.RESET} {C.CYAN}test_images{C.RESET} """, ) @@ -363,6 +381,29 @@ def main(): help=f"{C.CYAN}Enable verbose output{C.RESET}", ) + test_parser = subparsers.add_parser( + "test", + formatter_class=ColorHelpFormatter, + add_help=False, + help=f"{C.CYAN}Create test image files{C.RESET}", + ) + test_parser.add_argument( + "-h", "--help", action="help", help=f"{C.CYAN}Show help message{C.RESET}" + ) + test_parser.add_argument( + "-o", + "--output_dir", + required=False, + default="test_images", + help=f"{C.CYAN}(Optional) Output folder for test files{C.RESET}", + ) + test_parser.add_argument( + "-v", + "--verbose", + action="store_true", + help=f"{C.CYAN}Enable verbose output{C.RESET}", + ) + args = parser.parse_args() if not getattr(args, "subparser_command", None): @@ -391,6 +432,13 @@ def main(): elif args.subparser_command == "analyze": return cli.analyze_command(args.input_file) + elif args.subparser_command == "test": + return cli.create_test_files_command(args.output_dir) + + else: + parser.print_help() + return 1 + except ImageMultiFormatCoderException as e: print(f"āŒ ImageMultiFormatCoder Error: {e}") return 1 diff --git a/src/ghostbit/imagestego/core/image_multiformat_coder.py b/src/ghostbit/imagestego/core/image_multiformat_coder.py index 1bbabc0..45c213a 100755 --- a/src/ghostbit/imagestego/core/image_multiformat_coder.py +++ b/src/ghostbit/imagestego/core/image_multiformat_coder.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 import os import sys +import time import struct import logging +import svgwrite +import numpy as np from PIL import Image from typing import Optional, List, Dict, Union, TypedDict, Any @@ -29,6 +32,12 @@ class ImageMultiFormatCoderException(Exception): pass +class ImageTestCreationException(Exception): + """Base exception for image test file creation operations""" + + pass + + class CapacityResult(TypedDict): format: str algorithm: str @@ -136,8 +145,8 @@ def analyze(self, image_path: str) -> AnalysisResult: "encrypted": None, } - print(f"šŸ“ Input File: '{os.path.basename(image_path)}'") - print(f" • Format: {format}") + print(f" Input File: '{os.path.basename(image_path)}'") + print(f" • Format: {format}") print("\nšŸ“Š Statistical Analysis:") @@ -166,10 +175,10 @@ def analyze(self, image_path: str) -> AnalysisResult: raise ImageStatisticsException("Invalid frames data") print( - f" • Chi-Square (avg): Palette - {avg_palette:.2f} {'(Low detection risk)' if avg_palette < 100 else '(Moderate detection risk)' if avg_palette < 300 else '(High detection risk)'}" + f" • Chi-Square (avg): Palette - {avg_palette:.2f} {'(Low detection risk)' if avg_palette < 100 else '(Moderate detection risk)' if avg_palette < 300 else '(High detection risk)'}" f", Pixel - {avg_pixel:.4f} {'(Low detection risk)' if avg_pixel < .1 else '(Moderate detection risk)' if avg_pixel < .3 else '(High detection risk)'}" ) - print(" • Chi-Square (per frame):") + print(" • Chi-Square (per frame):") for idx, chi in enumerate(frames_palette): palette_risk = ( "(High detection risk)" @@ -191,9 +200,9 @@ def analyze(self, image_path: str) -> AnalysisResult: ) ) print( - f" • Frame {idx}: Palette - {chi:.2f} {palette_risk}, Pixel - {pixel_chi:.4f} {pixel_risk}" + f" • Frame {idx}: Palette - {chi:.2f} {palette_risk}, Pixel - {pixel_chi:.4f} {pixel_risk}" ) - print(f" • Entropy: Palette - {entropy_stats}") + print(f" • Entropy: Palette - {entropy_stats}") elif format == "SVG": stats = self.stats.analyze_svg(image_path) @@ -216,11 +225,11 @@ def analyze(self, image_path: str) -> AnalysisResult: count_val = numerical_stats.get("count") print( - f" • Entropy: {entropy:.6f} bits per byte " + f" • Entropy: {entropy:.6f} bits per byte " f"{'(Low)' if entropy < 4 else '(Medium)' if entropy < 6 else '(High - suspicious)'}" ) print( - f" • Suspicious Patterns: { {k: v for k, v in suspicious_patterns.items() if v > 0} or 'None'}" + f" • Suspicious Patterns: { {k: v for k, v in suspicious_patterns.items() if v > 0} or 'None'}" ) mean_str = ( @@ -239,7 +248,7 @@ def analyze(self, image_path: str) -> AnalysisResult: ) print( - f" • Numerical Stats: count={count_val}, " + f" • Numerical Stats: count={count_val}, " f"mean={mean_str}, variance={var_str}, " f"min={min_str}, max={max_str}" ) @@ -251,11 +260,11 @@ def analyze(self, image_path: str) -> AnalysisResult: "Low" if chi_avg < 100 else "Moderate" if chi_avg < 300 else "High" ) print( - f" • Chi-Square Average: {chi_avg:.2f} ({risk_level} detection risk)" + f" • Chi-Square Average: {chi_avg:.2f} ({risk_level} detection risk)" ) - print(f" • Chi-Square R: {chi_stats['R']:.2f}") - print(f" • Chi-Square G: {chi_stats['G']:.2f}") - print(f" • Chi-Square B: {chi_stats['B']:.2f}") + print(f" • Chi-Square R: {chi_stats['R']:.2f}") + print(f" • Chi-Square G: {chi_stats['G']:.2f}") + print(f" • Chi-Square B: {chi_stats['B']:.2f}") except Exception as e: logger.debug(f"Could not perform statistical analysis: {e}") @@ -539,8 +548,8 @@ def encode( algorithm = self.select_algorithm(format) stego = self.algorithms[algorithm] - print(f" • File Format: {format}") - print(f" • Algorithm Selected: {algorithm.name}") + print(f" • File Format: {format}") + print(f" • Algorithm Selected: {algorithm.name}") secret_files_info_items: List[SecretFileInfoItem] = [] for secret_file in secret_files: @@ -554,7 +563,7 @@ def encode( logger.info( f"Added secret file: '{info.file_name}' ({info.file_size} bytes)" ) - print(f" • File: '{info.file_name}' ({info.file_size_mb})") + print(f" • File: '{info.file_name}' ({info.file_size_mb})") if output_dir: logger.debug(f"Creating output directory: {output_dir}") @@ -603,7 +612,7 @@ def encode( try: logger.info(f"Processing {format} format") - print("\nšŸ”„ Encoding Secret Files...") + print("šŸ”„ Encoding Secret Files...") if hasattr(stego, "encode"): stego_result = stego.encode(cover_path, payload) @@ -612,9 +621,9 @@ def encode( f"Algorithm {algorithm.name} does not support encoding" ) - print(f" āœ“ Successfully hidden {len(payload):,} bytes") + print(f" āœ“ Successfully hidden {len(payload):,} bytes") print("\nšŸ”„ Creating Final Output...") - print(f" • Output File: '{output_filepath}'") + print(f" • Output File: '{output_filepath}'") if format == "SVG": if not isinstance(stego_result, str): @@ -688,12 +697,12 @@ def encode( stego_result.save(output_filepath, format=format, **save_kwargs) stego_image = stego_result - print(f" • Capacity Used: {capacity_percent:.2f}%") + print(f" • Capacity Used: {capacity_percent:.2f}%") print( - f" • Remaining Capacity: {capacity_remaining:,} bytes ({(capacity_remaining/(1024 ** 2)):.2f} MB)" + f" • Remaining Capacity: {capacity_remaining:,} bytes ({(capacity_remaining/(1024 ** 2)):.2f} MB)" ) print( - f" • Output File Size: {(os.path.getsize(output_filepath)/(1024 ** 2)):.2f} MB" + f" • Output File Size: {(os.path.getsize(output_filepath)/(1024 ** 2)):.2f} MB" ) if show_stats: @@ -748,12 +757,12 @@ def decode( payload = stego.decode(stego_path) if not payload: logger.info("No hidden data found in file") - print(" šŸ˜– No hidden data found") + print(" āœ— No hidden data found") print("\nāœ… Decoding Complete!\n") return 0 else: logger.info("Hidden data found in file") - print(" šŸ˜Ž Hidden Data Found!") + print(" Hidden Data Found!") print(f" • Algorithm detected: {algorithm.name}") else: raise ImageSteganographyException( @@ -779,12 +788,12 @@ def decode( if header_data[:4] != self.MAGIC: logger.debug("No hidden data found in image") - print(" šŸ˜– No hidden data found") + print(" āœ— No hidden data found") print("\nāœ… Decoding Complete!\n") return 0 else: logger.info("Hidden data found in file") - print(" šŸ˜Ž Hidden Data Found!") + print(" Hidden Data Found!") print(f" • Algorithm detected: {algorithm.name}") algorithm = Algorithm(header_data[5]) encrypted = header_data[6] @@ -807,7 +816,7 @@ def decode( logger.error(f"āŒ Image Decoding failed: {e}") return 1 - print(" šŸ“„ Hidden Files:") + print(" Hidden Files:") for file_info in extracted_files: print(f" • {file_info.file_name} ({file_info.file_size_mb})") output_path = os.path.join(output_dir, file_info.full_path) @@ -816,7 +825,7 @@ def decode( os.makedirs(output_file_dir, exist_ok=True) logger.debug(f"Created directory: {output_file_dir}") print("\nšŸ”„ Extracting Hidden Files...") - print(f" • Output Directory: '{os.path.dirname(output_path)}'") + print(f" • Output Directory: '{os.path.dirname(output_path)}'") file_data = file_info.file_data if file_data is None: @@ -836,3 +845,167 @@ def decode( except Exception as e: raise ImageSteganographyException(f"Error decoding image: {e}") + + +class ImageGenerator: + def __init__(self, out_dir, width=512, height=512, frames_per_gif=20): + self.WIDTH = width + self.HEIGHT = height + self.FRAMES_PER_GIF = frames_per_gif + os.makedirs(out_dir, exist_ok=True) + self.OUT = out_dir + self.RUN_SEED = int(time.time_ns()) + self.rng = np.random.default_rng(self.RUN_SEED) + + self.PALETTE_IMG = Image.new("P", (1, 1)) + self.PALETTE_IMG.putpalette(self.fixed_rgb_palette()) + + def create_secret_files(self): + try: + logger.debug("Creating test_secret_small.txt") + with open(f"{self.OUT}/test_secret_small.txt", "w") as f: + f.write("YOLO") + print(" āœ“ Created test_secret_small.txt") + logger.info("Created test_secret_small.txt") + + logger.debug("Creating test_secret.txt") + with open(f"{self.OUT}/test_secret.txt", "w") as f: + f.write( + "https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExY3JqdDJ2Y3VhcHR0OXY1d2RkMGQxdmJmbXdobGI1bnp1dWx0a3NxMiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/Yxg7MDkPj4kmI/giphy.gif" + ) + print(" āœ“ Created test_secret.txt") + logger.info("Created test_secret.txt") + except Exception as e: + logger.exception("Failed to create text files") + print(f"āŒ Error creating text files: {e}") + return 1 + + def strip_metadata(self, image: Image.Image) -> Image.Image: + clean = Image.new(image.mode, image.size) + clean.putdata(list(image.getdata())) + return clean + + def fixed_rgb_palette(self): + palette = [] + for r in range(0, 256, 51): + for g in range(0, 256, 51): + for b in range(0, 256, 51): + palette.extend([r, g, b]) + return palette[:768] + + def generate_pattern(self, design_type: str) -> np.ndarray: + base = self.rng.integers( + 0, 256, size=(self.HEIGHT, self.WIDTH, 3), dtype=np.uint8 + ) + + if design_type == "gradient": + x = np.linspace(0, 255, self.WIDTH, dtype=np.uint8) + y = np.linspace(0, 255, self.HEIGHT, dtype=np.uint8) + base[..., 0] = np.tile(x, (self.HEIGHT, 1)) + base[..., 1] = np.tile(y[:, None], (1, self.WIDTH)) + base[..., 2] = np.flipud(np.tile(x, (self.HEIGHT, 1))) + base = (base.astype(np.uint16) + self.rng.integers(0, 256, size=3)).astype( + np.uint8 + ) + + elif design_type == "channels": + idx = self.rng.permutation(3) + base = base[..., idx] + + elif design_type == "waves": + xv, yv = np.meshgrid( + np.linspace(0, 2 * np.pi, self.WIDTH), + np.linspace(0, 2 * np.pi, self.HEIGHT), + ) + base[..., 0] = ( + (np.sin(xv + self.rng.random() * 2 * np.pi) * 127 + 128) % 256 + ).astype(np.uint8) + base[..., 1] = ( + (np.sin(yv + self.rng.random() * 2 * np.pi) * 127 + 128) % 256 + ).astype(np.uint8) + base[..., 2] = ( + (np.sin(xv + yv + self.rng.random() * 2 * np.pi) * 127 + 128) % 256 + ).astype(np.uint8) + + return base + + def save_static_formats(self): + design_type = self.rng.choice(["noise", "gradient", "channels", "waves"]) + rgb = self.generate_pattern(design_type) + img = self.strip_metadata(Image.fromarray(rgb, "RGB")) + + img.save(os.path.join(self.OUT, "image.bmp")) + print(" āœ“ Created image.bmp") + img.save(os.path.join(self.OUT, "image.png"), compress_level=9) + print(" āœ“ Created image.png") + img.save( + os.path.join(self.OUT, "image.jpg"), + quality=95, + subsampling=0, + optimize=False, + progressive=False, + ) + print(" āœ“ Created image.jpg") + img.save(os.path.join(self.OUT, "image.tiff"), compression="raw") + print(" āœ“ Created image.tiff") + img.save( + os.path.join(self.OUT, "image.webp"), + format="WEBP", + lossless=True, + quality=100, + method=6, + ) + print(" āœ“ Created image.webp") + + gif_static = img.quantize(palette=self.PALETTE_IMG, dither=Image.Dither.NONE) + gif_static.save(os.path.join(self.OUT, "image_static.gif"), optimize=False) + print(" āœ“ Created image_static.gif") + + def save_animated_gif(self): + frames = [] + designs = ["noise", "gradient", "channels", "waves"] + for _ in range(self.FRAMES_PER_GIF): + design_type = self.rng.choice(designs) + frame_rgb = self.generate_pattern(design_type) + frame = self.strip_metadata(Image.fromarray(frame_rgb, "RGB")) + frame = frame.quantize(palette=self.PALETTE_IMG, dither=Image.Dither.FLOYDSTEINBERG) + frames.append(frame) + + frames[0].save( + os.path.join(self.OUT, "image_animated.gif"), + save_all=True, + append_images=frames[1:], + duration=80, + loop=0, + optimize=False, + ) + print(" āœ“ Created image_animated.gif") + + def save_svg(self): + dwg = svgwrite.Drawing( + os.path.join(self.OUT, "image.svg"), + size=(self.WIDTH, self.HEIGHT), + profile="full", + ) + + c1 = self.rng.integers(0, 256, size=3) + c2 = self.rng.integers(0, 256, size=3) + c3 = self.rng.integers(0, 256, size=3) + + gradient = dwg.linearGradient(start=(0, 0), end=(1, 1), id="rgbGradient") + gradient.add_stop_color(0.0, f"rgb({c1[0]},{c1[1]},{c1[2]})") + gradient.add_stop_color(0.5, f"rgb({c2[0]},{c2[1]},{c2[2]})") + gradient.add_stop_color(1.0, f"rgb({c3[0]},{c3[1]},{c3[2]})") + dwg.defs.add(gradient) + dwg.add( + dwg.rect(insert=(0, 0), size=("100%", "100%"), fill="url(#rgbGradient)") + ) + dwg.save() + print(" āœ“ Created image.svg") + + def generate_all(self): + self.create_secret_files() + self.save_static_formats() + self.save_animated_gif() + self.save_svg() + print("\nāœ… Image Creation Complete!") diff --git a/src/ghostbit/imagestego/core/image_steganography.py b/src/ghostbit/imagestego/core/image_steganography.py index 97a0a4c..bd16d52 100755 --- a/src/ghostbit/imagestego/core/image_steganography.py +++ b/src/ghostbit/imagestego/core/image_steganography.py @@ -141,7 +141,7 @@ def build_payload( if len(base_name) > max_base_len: base_name = base_name[:max_base_len] logger.info("Truncating secret file name") - print(f" • Truncating filename: {base_name}{ext}") + print(f" • Truncating filename: {base_name}{ext}") full_name = base_name + ext @@ -158,29 +158,29 @@ def build_payload( ) payload_parts.append(header + file_data) - logger.debug(f" • Added part: {len(header + file_data)} bytes") + logger.debug(f" • Added part: {len(header + file_data)} bytes") combined_data = b"".join(payload_parts) logger.debug(f"Combined data (before end marker): {len(combined_data)} bytes") - print(f" • Combined: {len(combined_data)} bytes") + print(f" • Combined: {len(combined_data)} bytes") combined_data += b"IMGF" logger.debug(f"Combined data (with end marker): {len(combined_data)} bytes") - print(f" • With end marker: {len(combined_data)} bytes") + print(f" • With end marker: {len(combined_data)} bytes") compressed = zlib.compress(combined_data, level=9) logger.debug(f"Compressed data: {len(compressed)} bytes") - print(f" • Compressed: {len(compressed)} bytes") + print(f" • Compressed: {len(compressed)} bytes") if password: salt, nonce, ciphertext = self._encrypt_data(compressed, password) logger.debug( f"Encrypted: salt={len(salt)}, nonce={len(nonce)}, ciphertext={len(ciphertext)}" ) - print(f" • Encrypted: {len(ciphertext)} bytes") + print(f" • Encrypted: {len(ciphertext)} bytes\n") payload = ( self.MAGIC + struct.pack("B", self.VERSION) diff --git a/tests/test_audiostego_cli.py b/tests/test_audiostego_cli.py index d93dc19..b097091 100644 --- a/tests/test_audiostego_cli.py +++ b/tests/test_audiostego_cli.py @@ -461,7 +461,7 @@ def test_create_test_files_basic(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: result = cli.create_test_files_command( - output_dir=tmpdir, create_carrier=False + output_dir=tmpdir ) assert result == 0 @@ -482,7 +482,7 @@ def test_create_test_files_with_carrier( with tempfile.TemporaryDirectory() as tmpdir: result = cli.create_test_files_command( - output_dir=tmpdir, create_carrier=True + output_dir=tmpdir ) assert result == 0 @@ -496,7 +496,7 @@ def test_create_test_files_carrier_exception(self, mock_wave: MagicMock) -> None with tempfile.TemporaryDirectory() as tmpdir: result = cli.create_test_files_command( - output_dir=tmpdir, create_carrier=True + output_dir=tmpdir ) assert result == 0 @@ -957,7 +957,7 @@ def test_create_test_files_with_carrier_full_workflow(self) -> None: cli = AudioStegoCLI() with tempfile.TemporaryDirectory() as tmpdir: - result = cli.create_test_files_command(tmpdir, create_carrier=False) + result = cli.create_test_files_command(tmpdir) assert result == 0 @@ -979,7 +979,7 @@ def test_create_test_files_carrier_partial_failure( mock_audio_segment.from_wav.side_effect = Exception("Conversion error") with tempfile.TemporaryDirectory() as tmpdir: - result = cli.create_test_files_command(tmpdir, create_carrier=True) + result = cli.create_test_files_command(tmpdir) assert result == 0 @@ -989,7 +989,7 @@ def test_create_test_files_output_directory_created(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: test_dir = os.path.join(tmpdir, "testcases") - result = cli.create_test_files_command(test_dir, create_carrier=False) + result = cli.create_test_files_command(test_dir) output_path = os.path.join("output", test_dir) assert os.path.exists(output_path)