nes-chr-decode.py with syntax coloring

This HTML file was generated with Kalle's syntaxcolor.py


   1"""
   2Decodes an NES CHR-ROM data file into a PNG file.
   3By Kalle (http://qalle.net)
   4"""
   5
   6import sys
   7import os.path
   8import re
   9
  10try:
  11    import png
  12except ImportError:
  13    exit("PyPNG module (png.py, Pure Python PNG Reader/Writer) not found.")
  14
  15DEFAULT_OUTPUT_PALETTE = "000000,555555,aaaaaa,ffffff"
  16
  17HELP_TEXT = """\
  18Converts an NES CHR-ROM data file to a PNG image.
  19
  20Args: SourceFile TargetFile [Palette]
  21    SourceFile
  22        Name of CHR-ROM file to be read. Size must be a multiple of 256 bytes.
  23        Note: full iNES ROM files (.nes) are not supported.
  24    TargetFile
  25        Name of PNG file to (over)write.
  26    Palette
  27        Optional. Four colors in hexadecimal RRGGBB format, separated by
  28        commas. For example, if Palette is "000000,555555,aaaaaa,ffffff",
  29        CHR-ROM colors 0-3 will be represented as black, dark gray, light gray
  30        and white in the PNG image, respectively.\
  31"""
  32
  33def parse_palette_argument(palette):
  34    """Convert the palette argument to a list of (R, G, B) tuples."""
  35
  36    if re.search("^([0-9a-f]{6},){3}[0-9a-f]{6}$", palette, re.I) is None:
  37        exit("Invalid palette argument.")
  38
  39    return [
  40        (color >> 16, (color >> 8) & 0xff, color & 0xff) for color in
  41        (int(colorStr, 16) for colorStr in palette.split(","))
  42    ]
  43
  44def generate_output_lines(sourceHnd):
  45    """
  46    Generate PNG from CHR. For each output line, yield 128 two-bit integers.
  47
  48    Byte order in NES character data:
  49      - first character (16 bytes):
  50        - less significant bitplane (8 bytes):
  51          - topmost 8*1 pixels (1 byte)
  52          - next 8*1 pixels (1 byte)
  53          - etc.
  54        - more significant bitplane
  55      - second character
  56      - etc.
  57    """
  58
  59    # 1 row of characters = 16*1 characters = 256 bytes
  60    rowCount = sourceHnd.seek(0, 2) >> 8
  61    sourceHnd.seek(0)
  62
  63    for rowY in range(rowCount):
  64        charRow = sourceHnd.read(256)
  65
  66        for pixY in range(8):
  67            outputLine = []
  68
  69            for col in range(16):
  70                # process 8*1 pixels
  71
  72                # less/more significant bitplane
  73                loBits = charRow[(col << 4) | pixY]
  74                hiBits = charRow[(col << 4) | 8 | pixY]
  75
  76                # bit interleave algorithm:
  77                # http://graphics.stanford.edu/~seander/
  78                # bithacks.html#InterleaveBMN
  79
  80                # bits: abcdefgh -> 0a0b0c0d0e0f0g0h
  81                loBits = ((loBits << 4) | loBits) & 0x0f0f0f0f
  82                loBits = ((loBits << 2) | loBits) & 0x33333333
  83                loBits = ((loBits << 1) | loBits) & 0x55555555
  84
  85                # bits: ABCDEFGH -> 0A0B0C0D0E0F0G0H
  86                hiBits = ((hiBits << 4) | hiBits) & 0x0f0f0f0f
  87                hiBits = ((hiBits << 2) | hiBits) & 0x33333333
  88                hiBits = ((hiBits << 1) | hiBits) & 0x55555555
  89
  90                # interleaved bits: AaBbCcDdEeFfGgHh
  91                allBits = (hiBits << 1) | loBits
  92
  93                outputLine += [
  94                    (allBits >> shift) & 3 for shift in range(14, -1, -2)
  95                ]
  96
  97            # generate 128*1 pixels
  98            yield outputLine
  99
 100def decode_CHR_data(sourceHnd, targetHnd, palette):
 101    """Convert CHR file to PNG file. Return number of bytes written."""
 102
 103    fileSize = sourceHnd.seek(0, 2)
 104    if fileSize % 256 > 0 or fileSize == 0:
 105        exit("CHR file size must be a multiple of 256 bytes.")
 106
 107    # initialize PNG writer
 108    writer = png.Writer(
 109        width = 128,
 110        height = fileSize >> 5,
 111        bitdepth = 2,
 112        palette = palette,
 113    )
 114
 115    # write output image
 116    targetHnd.seek(0)
 117    writer.write(targetHnd, generate_output_lines(sourceHnd))
 118
 119    return targetHnd.seek(0, 2)
 120
 121def main():
 122    if not 3 <= len(sys.argv) <= 4:
 123        exit(HELP_TEXT)
 124
 125    # read args
 126    (source, target) = sys.argv[1:3]
 127    if len(sys.argv) == 4:
 128        paletteStr = sys.argv[3]
 129    else:
 130        paletteStr = DEFAULT_OUTPUT_PALETTE
 131        print('Using default palette {:s}'.format(paletteStr))
 132
 133    palette = parse_palette_argument(paletteStr)
 134
 135    # files must not be the same
 136    try:
 137        if os.path.samefile(source, target):
 138            exit("Source and target files must not be the same.")
 139    except OSError:
 140        pass
 141
 142    # decode source image into target image
 143    try:
 144        with open(source, "rb") as sourceHnd, open(target, "wb") as targetHnd:
 145            bytesWritten = decode_CHR_data(sourceHnd, targetHnd, palette)
 146    except OSError:
 147        exit("Error reading input file or writing output file.")
 148
 149    print("OK. {:d} bytes written.".format(bytesWritten))
 150
 151if __name__ == "__main__":
 152    main()