diff --git a/binary_qr.py b/binary_qr.py new file mode 100644 index 0000000..2f3fe8c --- /dev/null +++ b/binary_qr.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +Binary QR Converter - A utility to convert binary files to QR codes and back. +""" + +import argparse +import logging +import sys +from pathlib import Path +from typing import List, Optional + +from converter import BinaryQRConverter + +__version__ = '0.1.0' + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description="Convert binary files to QR codes and back", + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + subparsers = parser.add_subparsers(dest="command", help="Command to execute") + + # Encode command + encode_parser = subparsers.add_parser("encode", help="Encode a binary file to QR codes") + encode_parser.add_argument("file", type=str, help="Path to the binary file to encode") + encode_parser.add_argument( + "-o", "--output-dir", + type=str, + default="./qrcodes", + help="Directory to save QR code images" + ) + encode_parser.add_argument( + "-c", "--chunk-size", + type=int, + default=1024, + help="Size of binary chunks in bytes" + ) + encode_parser.add_argument( + "-v", "--qr-version", + type=int, + default=40, + choices=range(1, 41), + help="QR code version (1-40, higher means more capacity)" + ) + encode_parser.add_argument( + "-z", "--compression-level", + type=int, + default=9, + help="Zlib compression level (0-9)" + ) + + # Decode command + decode_parser = subparsers.add_parser("decode", help="Decode QR codes back to a binary file") + decode_parser.add_argument( + "images", + type=str, + nargs="+", + help="Paths to QR code images or directory containing QR code images" + ) + decode_parser.add_argument( + "-o", "--output-dir", + type=str, + default="./output", + help="Directory to save the reconstructed file" + ) + + # Version command + version_parser = subparsers.add_parser("version", help="Show version information") + + return parser.parse_args() + +def get_image_paths(image_args: List[str]) -> List[Path]: + """ + Get a list of image paths from command line arguments. + + Args: + image_args: List of image paths or directories from command line + + Returns: + List of image file paths + """ + image_paths = [] + + for path_str in image_args: + path = Path(path_str) + + if path.is_dir(): + # If path is a directory, find all PNG files in it + image_paths.extend(sorted(path.glob("*.png"))) + elif path.is_file(): + # If path is a file, add it directly + image_paths.append(path) + else: + # If path is a glob pattern, expand it + expanded_paths = list(Path().glob(path_str)) + if expanded_paths: + image_paths.extend(expanded_paths) + + return image_paths + +def main(): + """Main entry point for the command-line interface.""" + args = parse_args() + + if args.command == "version": + print(f"Binary QR Converter version {__version__}") + return 0 + + if args.command == "encode": + file_path = Path(args.file) + output_dir = Path(args.output_dir) + + if not file_path.exists(): + logger.error(f"File not found: {file_path}") + return 1 + + converter = BinaryQRConverter( + chunk_size=args.chunk_size, + qr_version=args.qr_version, + compression_level=args.compression_level + ) + + try: + qr_image_paths = converter.encode_file(file_path, output_dir) + logger.info(f"Created {len(qr_image_paths)} QR code images in {output_dir}") + return 0 + except Exception as e: + logger.error(f"Encoding failed: {str(e)}") + return 1 + + elif args.command == "decode": + image_paths = get_image_paths(args.images) + output_dir = Path(args.output_dir) + + if not image_paths: + logger.error("No image files found") + return 1 + + logger.info(f"Found {len(image_paths)} image files") + + converter = BinaryQRConverter() + + try: + output_path = converter.decode_qr_images(image_paths, output_dir) + if output_path: + logger.info(f"Successfully decoded to {output_path}") + return 0 + else: + logger.error("Decoding failed") + return 1 + except Exception as e: + logger.error(f"Decoding failed: {str(e)}") + return 1 + + else: + logger.error("No command specified. Use --help for usage information.") + return 1 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/converter.py b/converter.py new file mode 100644 index 0000000..e722c5d --- /dev/null +++ b/converter.py @@ -0,0 +1,330 @@ +""" +Core functionality for converting binary files to QR codes and back. +""" + +import os +import base64 +import zlib +import json +import logging +import hashlib +from typing import List, Dict, Optional, Tuple +from pathlib import Path + +import qrcode +from qrcode.constants import ERROR_CORRECT_H +from PIL import Image +from pyzbar.pyzbar import decode, ZBarSymbol + +# Configure logging +logger = logging.getLogger(__name__) + +class BinaryQRConverter: + """ + A class to convert binary files to QR codes and back. + + This class provides methods to: + - Encode binary files into a series of QR code images + - Decode QR code images back into the original binary file + """ + + def __init__( + self, + chunk_size: int = 1024, + error_correction: int = ERROR_CORRECT_H, + qr_version: int = 40, + compression_level: int = 9 + ): + """ + Initialize the BinaryQRConverter. + + Args: + chunk_size: Size in bytes of each binary chunk to encode + error_correction: QR code error correction level + qr_version: QR code version (1-40, higher means more capacity) + compression_level: Zlib compression level (0-9) + """ + self.chunk_size = chunk_size + self.error_correction = error_correction + self.qr_version = qr_version + self.compression_level = compression_level + + def _calculate_file_hash(self, file_path: Path) -> str: + """ + Calculate SHA-256 hash of a file. + + Args: + file_path: Path to the file + + Returns: + Hexadecimal string representation of the hash + """ + sha256_hash = hashlib.sha256() + + with open(file_path, "rb") as f: + # Read and update hash in chunks + for byte_block in iter(lambda: f.read(4096), b""): + sha256_hash.update(byte_block) + + return sha256_hash.hexdigest() + + def _compress_data(self, data: bytes) -> bytes: + """ + Compress binary data using zlib. + + Args: + data: Binary data to compress + + Returns: + Compressed binary data + """ + return zlib.compress(data, level=self.compression_level) + + def _decompress_data(self, compressed_data: bytes) -> bytes: + """ + Decompress binary data using zlib. + + Args: + compressed_data: Compressed binary data + + Returns: + Decompressed binary data + """ + return zlib.decompress(compressed_data) + + def _encode_chunk( + self, + chunk: bytes, + chunk_index: int, + total_chunks: int, + filename: str, + file_hash: str + ) -> str: + """ + Encode a binary chunk with metadata as a JSON string. + + Args: + chunk: Binary chunk data + chunk_index: Index of this chunk + total_chunks: Total number of chunks + filename: Original filename + file_hash: Hash of the original file + + Returns: + JSON string with chunk data and metadata + """ + # Base64 encode the binary chunk + b64_data = base64.b64encode(chunk).decode('utf-8') + + # Create a dictionary with chunk data and metadata + chunk_dict = { + 'data': b64_data, + 'chunk_index': chunk_index, + 'total_chunks': total_chunks, + 'filename': filename, + 'file_hash': file_hash + } + + # Convert to JSON + return json.dumps(chunk_dict) + + def _create_qr_code(self, data: str) -> Image.Image: + """ + Create a QR code image from data. + + Args: + data: String data to encode in the QR code + + Returns: + PIL Image object containing the QR code + """ + qr = qrcode.QRCode( + version=self.qr_version, + error_correction=self.error_correction, + box_size=10, + border=4, + ) + qr.add_data(data) + qr.make(fit=True) + + return qr.make_image(fill_color="black", back_color="white") + + def encode_file(self, file_path: Path, output_dir: Path) -> List[Path]: + """ + Encode a binary file into a series of QR code images. + + Args: + file_path: Path to the binary file + output_dir: Directory to save QR code images + + Returns: + List of paths to the generated QR code images + """ + file_path = Path(file_path) + output_dir = Path(output_dir) + + # Create output directory if it doesn't exist + output_dir.mkdir(parents=True, exist_ok=True) + + # Calculate file hash for integrity verification + file_hash = self._calculate_file_hash(file_path) + logger.info(f"File hash: {file_hash}") + + # Get file size and calculate number of chunks + file_size = file_path.stat().st_size + compressed_size = 0 + original_filename = file_path.name + + # Read and compress the entire file first to get accurate chunk count + with open(file_path, 'rb') as f: + file_data = f.read() + + compressed_data = self._compress_data(file_data) + compressed_size = len(compressed_data) + logger.info(f"Original size: {file_size} bytes, Compressed size: {compressed_size} bytes") + + total_chunks = (compressed_size + self.chunk_size - 1) // self.chunk_size + logger.info(f"Splitting into {total_chunks} chunks") + + qr_image_paths = [] + + for i in range(total_chunks): + start_pos = i * self.chunk_size + end_pos = min(start_pos + self.chunk_size, compressed_size) + chunk = compressed_data[start_pos:end_pos] + + # Encode chunk with metadata + chunk_data = self._encode_chunk( + chunk, i, total_chunks, original_filename, file_hash + ) + + # Create QR code + qr_img = self._create_qr_code(chunk_data) + + # Save QR code image + image_filename = f"{original_filename}_chunk_{i+1}_of_{total_chunks}.png" + image_path = output_dir / image_filename + qr_img.save(image_path) + qr_image_paths.append(image_path) + + logger.info(f"Created QR code {i+1}/{total_chunks}: {image_path}") + + return qr_image_paths + + def decode_qr_image(self, image_path: Path) -> Optional[Dict]: + """ + Decode a QR code image to extract chunk data and metadata. + + Args: + image_path: Path to the QR code image + + Returns: + Dictionary with chunk data and metadata, or None if decoding fails + """ + try: + # Open the image + img = Image.open(image_path) + + # Decode QR code + decoded_objects = decode(img, symbols=[ZBarSymbol.QRCODE]) + + if not decoded_objects: + logger.error(f"No QR code found in {image_path}") + return None + + # Get the data from the first QR code found + qr_data = decoded_objects[0].data.decode('utf-8') + + # Parse JSON data + chunk_data = json.loads(qr_data) + + return chunk_data + + except Exception as e: + logger.error(f"Error decoding QR code {image_path}: {str(e)}") + return None + + def decode_qr_images(self, image_paths: List[Path], output_dir: Path) -> Optional[Path]: + """ + Decode a series of QR code images back to the original binary file. + + Args: + image_paths: List of paths to QR code images + output_dir: Directory to save the reconstructed file + + Returns: + Path to the reconstructed file, or None if decoding fails + """ + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # Decode all QR codes + chunk_data_list = [] + + for image_path in image_paths: + chunk_data = self.decode_qr_image(image_path) + if chunk_data: + chunk_data_list.append(chunk_data) + else: + logger.error(f"Failed to decode {image_path}") + return None + + # Sort chunks by index + chunk_data_list.sort(key=lambda x: x['chunk_index']) + + # Verify we have all chunks + if not chunk_data_list: + logger.error("No chunks were successfully decoded") + return None + + # Get metadata from the first chunk + first_chunk = chunk_data_list[0] + total_chunks = first_chunk['total_chunks'] + original_filename = first_chunk['filename'] + file_hash = first_chunk['file_hash'] + + if len(chunk_data_list) != total_chunks: + logger.error(f"Missing chunks: got {len(chunk_data_list)}, expected {total_chunks}") + return None + + # Verify all chunks have the same metadata + for i, chunk in enumerate(chunk_data_list): + if chunk['total_chunks'] != total_chunks: + logger.error(f"Chunk {i} has inconsistent total_chunks") + return None + if chunk['filename'] != original_filename: + logger.error(f"Chunk {i} has inconsistent filename") + return None + if chunk['file_hash'] != file_hash: + logger.error(f"Chunk {i} has inconsistent file_hash") + return None + if chunk['chunk_index'] != i: + logger.error(f"Expected chunk index {i}, got {chunk['chunk_index']}") + return None + + # Reconstruct the compressed data + compressed_data = b'' + for chunk in chunk_data_list: + chunk_bytes = base64.b64decode(chunk['data']) + compressed_data += chunk_bytes + + # Decompress the data + try: + original_data = self._decompress_data(compressed_data) + except zlib.error as e: + logger.error(f"Decompression failed: {str(e)}") + return None + + # Write the reconstructed file + output_path = output_dir / original_filename + with open(output_path, 'wb') as f: + f.write(original_data) + + # Verify file hash + reconstructed_hash = self._calculate_file_hash(output_path) + if reconstructed_hash != file_hash: + logger.error(f"Hash verification failed: expected {file_hash}, got {reconstructed_hash}") + return None + + logger.info(f"Successfully reconstructed {original_filename}") + return output_path \ No newline at end of file diff --git a/example.py b/example.py new file mode 100644 index 0000000..93743df --- /dev/null +++ b/example.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +""" +Example usage of the Binary QR Converter. +""" + +import os +import tempfile +from pathlib import Path + +from converter import BinaryQRConverter + +def main(): + """Run an example of encoding and decoding a binary file.""" + # Create a temporary directory for our example + with tempfile.TemporaryDirectory() as temp_dir: + temp_dir_path = Path(temp_dir) + + # Create a test binary file + test_file_path = temp_dir_path / "test_file.bin" + with open(test_file_path, 'wb') as f: + f.write(os.urandom(1000000)) # 1MB of random data + + print(f"Created test file: {test_file_path}") + + # Create output directories + qr_output_dir = temp_dir_path / "qrcodes" + decode_output_dir = temp_dir_path / "output" + + # Create converter + converter = BinaryQRConverter(chunk_size=500) + + # Encode the test file to QR codes + print("Encoding file to QR codes...") + qr_image_paths = converter.encode_file(test_file_path, qr_output_dir) + + print(f"Created {len(qr_image_paths)} QR code images:") + for path in qr_image_paths: + print(f" - {path}") + + # Decode the QR codes back to a file + print("\nDecoding QR codes back to file...") + decoded_file_path = converter.decode_qr_images(qr_image_paths, decode_output_dir) + + if decoded_file_path: + print(f"Successfully decoded to: {decoded_file_path}") + + # Verify the decoded file matches the original + with open(test_file_path, 'rb') as f1, open(decoded_file_path, 'rb') as f2: + original_data = f1.read() + decoded_data = f2.read() + + if original_data == decoded_data: + print("Verification successful: Decoded file matches the original!") + else: + print("Verification failed: Decoded file does not match the original.") + else: + print("Decoding failed.") + +if __name__ == "__main__": + main() \ No newline at end of file