Created
September 13, 2026 12:08
-
-
Save pethesdaniel/1a3aada68f959f637f54c8e0e0cd4928 to your computer and use it in GitHub Desktop.
b5i2iso.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| b5i2iso.py - convert BlindWrite B5I/BWI images to ISO 9660 (or BIN/CUE). | |
| Original: b5i2iso.c v0.2 by Salvatore Santagati (GPL v2 or later). | |
| Source: https://launchpad.net/ubuntu/trusty/+package/b5i2iso | |
| Ported to Python 3 by Claude Fable 5.1 | |
| """ | |
| import argparse | |
| import os | |
| import sys | |
| VERSION = "0.2-py" | |
| # Signatures --------------------------------------------------------------- | |
| ISO_9660 = bytes([0x01, 0x43, 0x44, 0x30, 0x30, 0x31, 0x01, 0x00]) # "\x01CD001\x01\x00" | |
| B5I_IMG = bytes([0x00] * 11 + [0x90, 0x00, 0x48, 0x00, 0xD8]) | |
| BWI_IMG = bytes([0x00] * 11 + [0x90, 0x00, 0xC1, 0x00, 0x12]) | |
| SYNC_HEADER_B5I_96 = bytes([0xFF] * 12 + [0x41, 0x01, 0x00, 0x00]) | |
| SYNC_HEADER_B5I = bytes([0x00] + [0xFF] * 10 + [0x00, 0x00, 0x00, 0x01, 0x01]) | |
| SYNC_HEADER_BWI = bytes([0x00] + [0xFF] * 10 + [0x00, 0x00, 0x02, 0x01, 0x01]) | |
| def progress(percent): | |
| bars = percent // 5 | |
| sys.stdout.write(f"{percent:3d}% [:{'=' * bars}>{' ' * (20 - bars)}:]\r") | |
| sys.stdout.flush() | |
| def write_cuesheet(dest): | |
| """Rename dest -> .bin and write a matching .cue next to it.""" | |
| base, _ = os.path.splitext(dest) | |
| cue_path = base + ".cue" | |
| bin_path = base + ".bin" | |
| with open(cue_path, "w") as fcue: | |
| fcue.write(f'FILE "{os.path.basename(bin_path)}" BINARY\n') | |
| fcue.write("TRACK 1 MODE1/2352\n") | |
| fcue.write("INDEX 1 00:00:00\n") | |
| # The C original had this condition inverted (!rename(...) == 0), | |
| # so it printed the error on success. This is the intended behaviour. | |
| try: | |
| os.replace(dest, bin_path) | |
| print(f"Create Bin : {bin_path}") | |
| except OSError as e: | |
| print(f"\nSorry, could not create {bin_path}: {e}") | |
| print(f"Create Cuesheets : {cue_path}") | |
| def default_output_name(source): | |
| base, ext = os.path.splitext(source) | |
| if ext.lower() in (".b5i", ".bwi"): | |
| return base + ".iso" | |
| return source + ".iso" | |
| def convert(source, dest, cue): | |
| with open(source, "rb") as fsrc: | |
| # Plain ISO already? | |
| fsrc.seek(32768) | |
| if fsrc.read(8) == ISO_9660: | |
| print("This is file iso9660 ;)") | |
| return 0 | |
| # Image type -> number of leading sectors to skip (B5I carries a | |
| # 150-sector / 2-second pregap; BWI does not). | |
| fsrc.seek(2336) | |
| sig = fsrc.read(16) | |
| if sig == B5I_IMG: | |
| skip = 150 | |
| elif sig == BWI_IMG: | |
| skip = 0 | |
| else: | |
| print("Sorry This file is not B5I or BWI image") | |
| return 1 | |
| # Sync header of the first sector decides the sector layout. | |
| fsrc.seek(2352) | |
| sync = fsrc.read(16) | |
| if sync == SYNC_HEADER_B5I_96: | |
| print("BAD IMAGE BLINDWRITE") | |
| sector_size = 2448 # 2352 + 96 bytes subchannel | |
| if cue: | |
| seek_head, sector_data, seek_ecc = 0, 2352, 96 # raw -> bin | |
| else: | |
| seek_head, sector_data, seek_ecc = 16, 2048, 384 # raw -> iso | |
| elif sync in (SYNC_HEADER_BWI, SYNC_HEADER_B5I): | |
| sector_size = 2352 | |
| if cue: | |
| seek_head, sector_data, seek_ecc = 0, 2352, 0 | |
| else: | |
| seek_head, sector_data, seek_ecc = 16, 2048, 288 | |
| else: | |
| print("Sorry I don't know this format :(") | |
| return 1 | |
| fsrc.seek(0, os.SEEK_END) | |
| total_sectors = fsrc.tell() // sector_size | |
| fsrc.seek(0) | |
| written_sectors = total_sectors - skip | |
| with open(dest, "wb") as fdst: | |
| last_percent = -1 | |
| for i in range(total_sectors): | |
| fsrc.seek(seek_head, os.SEEK_CUR) | |
| data = fsrc.read(sector_data) | |
| fsrc.seek(seek_ecc, os.SEEK_CUR) | |
| if i < skip: | |
| continue | |
| fdst.write(data) | |
| percent = (i - skip) * 100 // max(written_sectors, 1) | |
| if percent != last_percent: | |
| progress(percent) | |
| last_percent = percent | |
| print("100% [:====================:]") | |
| if cue: | |
| write_cuesheet(dest) | |
| else: | |
| print(f"Create iso9660 : {dest}") | |
| return 0 | |
| def main(argv=None): | |
| parser = argparse.ArgumentParser( | |
| prog="b5i2iso", | |
| description=f"b5i2iso v{VERSION} - convert BlindWrite B5I/BWI images to ISO", | |
| epilog="Python port of the original by Salvatore Santagati (GPL v2+).", | |
| ) | |
| parser.add_argument("--cue", action="store_true", help="generate BIN/CUE instead of ISO") | |
| parser.add_argument("source", help="input image (BASENAME.b5i / BASENAME.bwi)") | |
| parser.add_argument("dest", nargs="?", help="output file (default: BASENAME.iso)") | |
| args = parser.parse_args(argv) | |
| if not os.path.isfile(args.source): | |
| print(f"{args.source} : No such file") | |
| return 1 | |
| dest = args.dest or default_output_name(args.source) | |
| return convert(args.source, dest, args.cue) | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Tested on the Silent Hill 2: Directors Cut PC Redumps, it generated mountable iso files (unsure about copyprotection stuff, but I only needed to install the thing anyway)