Chd To Iso — Convert

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 None

def 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.