This commit is contained in:
tcsenpai 2025-02-27 00:25:41 +01:00
parent 5bde7ab10c
commit 24db27aa21
3 changed files with 559 additions and 0 deletions

169
binary_qr.py Normal file
View File

@ -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())

330
converter.py Normal file
View File

@ -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

60
example.py Normal file
View File

@ -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()