A tool to decompress CHD files (commonly used for MAME and emulator disk images) back to standard ISO format.
python3 chd_to_iso.py game.chd -f
python3 chd_to_iso.py game.chd --info
#!/usr/bin/env python3 """ CHD to ISO Converter Supports batch conversion, progress tracking, and verification """import os import sys import subprocess import argparse import hashlib from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed import logging from tqdm import tqdm
class CHDToISOConverter: """Main converter class for CHD to ISO conversion""" convert chd to iso
def __init__(self, chdman_path='chdman', output_dir='converted', verify=True, max_workers=4): self.chdman_path = chdman_path self.output_dir = Path(output_dir) self.verify = verify self.max_workers = max_workers self.setup_logging() def setup_logging(self): """Configure logging for the converter""" logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('chd_conversion.log'), logging.StreamHandler() ] ) self.logger = logging.getLogger(__name__) def check_chdman(self): """Verify chdman is available""" try: subprocess.run([self.chdman_path, '-version'], capture_output=True, check=True) return True except (subprocess.CalledProcessError, FileNotFoundError): self.logger.error("chdman not found. Install MAME tools first.") return False def convert_single_file(self, chd_path, output_path=None, force=False): """Convert a single CHD file to ISO""" chd_path = Path(chd_path) if not chd_path.exists(): self.logger.error(f"File not found: chd_path") return False if not chd_path.suffix.lower() == '.chd': self.logger.warning(f"File chd_path doesn't have .chd extension") # Set output path if output_path is None: output_path = self.output_dir / chd_path.stem else: output_path = Path(output_path) # Determine output extension based on input type output_file = output_path.with_suffix('.iso') # Check if output already exists if output_file.exists() and not force: self.logger.warning(f"Output file output_file exists. Use --force to overwrite.") return False # Create output directory if needed output_file.parent.mkdir(parents=True, exist_ok=True) # Build chdman command cmd = [ self.chdman_path, 'extract', '-i', str(chd_path), '-o', str(output_file) ] self.logger.info(f"Converting chd_path to output_file") try: # Run conversion result = subprocess.run(cmd, capture_output=True, text=True, check=True) if result.returncode == 0: self.logger.info(f"✓ Successfully converted: chd_path.name") # Verify the conversion if self.verify: return self.verify_conversion(chd_path, output_file) return True else: self.logger.error(f"Conversion failed: result.stderr") return False except subprocess.CalledProcessError as e: self.logger.error(f"Error converting chd_path: e") return False def verify_conversion(self, chd_path, iso_path): """Verify ISO file is valid""" if not iso_path.exists(): self.logger.error(f"Output file not found: iso_path") return False # Check file size chd_size = chd_path.stat().st_size iso_size = iso_path.stat().st_size # ISO should generally be larger than CHD (compressed vs uncompressed) if iso_size <= chd_size: self.logger.warning(f"ISO size (iso_size) is not larger than CHD size (chd_size)") # Try to mount/read ISO header (optional - requires additional libraries) try: with open(iso_path, 'rb') as f: # Check for ISO9660 signature at offset 32768 f.seek(32768) header = f.read(6) if header == b'CD001': self.logger.info(f"✓ Verification passed: Valid ISO9660 format") return True else: self.logger.warning(f"ISO header check failed, but file may still be valid") return True except Exception as e: self.logger.warning(f"Verification skipped: e") return True def batch_convert(self, input_pattern, recursive=False, force=False): """Convert multiple CHD files""" input_path = Path(input_pattern) # Handle wildcard patterns if '*' in input_pattern or '?' in input_pattern: from glob import glob chd_files = [Path(f) for f in glob(input_pattern) if f.lower().endswith('.chd')] elif input_path.is_dir(): if recursive: chd_files = list(input_path.rglob('*.chd')) else: chd_files = list(input_path.glob('*.chd')) else: chd_files = [input_path] if input_path.suffix.lower() == '.chd' else [] if not chd_files: self.logger.error(f"No CHD files found matching: input_pattern") return False self.logger.info(f"Found len(chd_files) CHD file(s) to convert") # Create output directory self.output_dir.mkdir(parents=True, exist_ok=True) # Convert files with progress bar successful = 0 failed = 0 with tqdm(total=len(chd_files), desc="Converting", unit="file") as pbar: with ThreadPoolExecutor(max_workers=self.max_workers) as executor: future_to_file = executor.submit(self.convert_single_file, chd_file, None, force): chd_file for chd_file in chd_files for future in as_completed(future_to_file): chd_file = future_to_file[future] try: if future.result(): successful += 1 else: failed += 1 except Exception as e: self.logger.error(f"Unexpected error for chd_file: e") failed += 1 pbar.update(1) pbar.set_postfix(success=successful, failed=failed) self.logger.info(f"\nConversion complete: successful successful, failed failed") return failed == 0 def get_file_info(self, chd_path): """Get information about a CHD file without extracting""" cmd = [self.chdman_path, 'info', '-i', str(chd_path)] try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) return result.stdout except subprocess.CalledProcessError as e: self.logger.error(f"Failed to get info: e") return Nonedef main(): parser = argparse.ArgumentParser( description='Convert CHD (Compressed Hunks of Data) files to ISO format', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s game.chd # Convert single file %(prog)s /path/to/chds/ # Convert all CHD files in directory %(prog)s /path/to/chds/ -r # Recursive conversion %(prog)s "*.chd" -o ./isos/ # Convert with wildcards %(prog)s game.chd --info # Show CHD information only """ )
parser.add_argument('input', help='Input CHD file, directory, or pattern') parser.add_argument('-o', '--output-dir', default='./converted', help='Output directory for ISO files (default: ./converted)') parser.add_argument('-r', '--recursive', action='store_true', help='Search recursively for CHD files') parser.add_argument('-f', '--force', action='store_true', help='Overwrite existing ISO files') parser.add_argument('--no-verify', action='store_true', help='Skip ISO verification after conversion') parser.add_argument('--chdman-path', default='chdman', help='Path to chdman executable') parser.add_argument('-j', '--jobs', type=int, default=4, help='Number of parallel conversions (default: 4)') parser.add_argument('--info', action='store_true', help='Show CHD file information without converting') args = parser.parse_args() # Create converter instance converter = CHDToISOConverter( chdman_path=args.chdman_path, output_dir=args.output_dir, verify=not args.no_verify, max_workers=args.jobs ) # Check if chdman is available if not converter.check_chdman(): sys.exit(1) # Show info if requested if args.info: info = converter.get_file_info(args.input) if info: print(info) sys.exit(0) # Perform conversion success = converter.batch_convert(args.input, args.recursive, args.force) sys.exit(0 if success else 1)
if name == 'main': main()
You have two primary options to get chdman: A tool to decompress CHD files (commonly used
For Windows users: Place chdman.exe in a dedicated folder (e.g., C:\chd_tools) for easy command-line access.