Skip to content

Instantly share code, notes, and snippets.

@pethesdaniel
Created September 13, 2026 12:08
Show Gist options
  • Select an option

  • Save pethesdaniel/1a3aada68f959f637f54c8e0e0cd4928 to your computer and use it in GitHub Desktop.

Select an option

Save pethesdaniel/1a3aada68f959f637f54c8e0e0cd4928 to your computer and use it in GitHub Desktop.
b5i2iso.py
#!/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())
@pethesdaniel

Copy link
Copy Markdown
Author

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment