nes-chr-encode.py with syntax coloring

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


   1"""
   2Encodes a PNG file into an NES CHR-ROM data 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
  15HELP_TEXT = """\
  16Converts a PNG image to an NES CHR-ROM data file.
  17
  18Args: SourceFile TargetFile Palette
  19    SourceFile
  20        Name of PNG file to read. The image must be 128 pixels wide. Its height
  21        must be a multiple of eight pixels. The image must have no more than
  22        four unique colors.
  23    TargetFile
  24        Name of NES CHR-ROM data file to write. (The file size will be a
  25        multiple of 256 bytes.)
  26    Palette
  27        Defines which color in the PNG image becomes which CHR-ROM color.
  28        Consists of four colors in hexadecimal RRGGBB format ("000000" to
  29        "ffffff") separated by commas (","). Each color may also be omitted,
  30        in which case no PNG color will be mapped to that CHR color.
  31        Each color in SourceFile must appear in Palette exactly once.
  32        For example, if Palette is ",ffffff,000000," then white in SourceFile
  33        is mapped to color 1 in TargetFile and black to color 2. (TargetFile
  34        will have no pixels in color 0 or 3.)
  35        You can also omit the Palette argument to see a list of all valid
  36        values.\
  37"""
  38
  39def create_color_mapping(paletteStr):
  40    """
  41    Convert palette argument to a dict for mapping PNG colors to CHR colors.
  42    Return: {(red0, green0, blue0): CHRColor0, ...}
  43    """
  44
  45    if re.search(
  46        "^(([0-9a-f]{6})?,){3}([0-9a-f]{6})?$", paletteStr, re.I
  47    ) is None:
  48        exit("Invalid palette argument.")
  49
  50    # convert palette to list of 24-bit RGB integers or empty strings
  51    palette = [
  52        (int(colorStr, 16) if colorStr != "" else "")
  53        for colorStr in paletteStr.split(",")
  54    ]
  55
  56    # validate colors that were defined
  57    definedColors = [color for color in palette if color != ""]
  58    if len(definedColors) == 0:
  59        exit("Palette must contain at least one color.")
  60    if len(set(definedColors)) < len(definedColors):
  61        exit("Colors defined in palette must be unique.")
  62
  63    # create dict for mapping RGB colors to CHR colors
  64    RGBToCHR = {}
  65    for (CHR, RGB) in enumerate(palette):
  66        if RGB != "":
  67            RGBToCHR[(RGB >> 16, (RGB >> 8) & 0xff, RGB & 0xff)] = CHR
  68
  69    return RGBToCHR
  70
  71def generate_output_row(pixelRows, RGBToCHR):
  72    """
  73    Convert image to 2-bit indexed colors.
  74
  75    pixelRows: a generator that returns one line of source image pixels
  76               (128 pixels, 384 bytes) per call
  77    RGBToCHR: color mapping; {(red0, green0, blue0): CHRColor0, ...}
  78
  79    Yield 128 * 8 = 1024 pixels per call. (Bytearray; addresses: ccccyyyxxx;
  80    cccc = character, yyy = Y, xxx = X.)
  81    """
  82
  83    pixelsOut = bytearray(1024)
  84
  85    for (y, pixelRow) in enumerate(pixelRows):
  86        pixelOutPosY = (y & 0b111) << 3
  87
  88        for x in range(128):
  89            try:
  90                RGB = pixelRow[x * 3 : (x + 1) * 3]
  91                CHRColor = RGBToCHR[tuple(RGB)]
  92            except KeyError:
  93                exit(
  94                    "Error: image has an undefined color: {:02x}{:02x}{:02x}"
  95                    .format(*RGB)
  96                )
  97
  98            pixelOutPos = ((x & 0b1111000) << 3) | pixelOutPosY | (x & 0b111)
  99            pixelsOut[pixelOutPos] = CHRColor
 100
 101        if y & 0b111 == 0b111:
 102            yield pixelsOut
 103
 104def indexed_row_to_CHR(pixels):
 105    """
 106    Convert 16 characters from 2-bit pixels to CHR data.
 107
 108    pixels: 1024 two-bit pixel values (iterator; addresses: ccccyyyxxx;
 109            cccc = character, yyy = Y, xxx = X)
 110
 111    Return CHR data as 256 bytes (addresses: ccccbyyy; cccc = character,
 112    b = bitplane, yyy = Y).
 113    """
 114
 115    CHROut = bytearray()
 116
 117    for char in range(16):
 118        charAddr = char << 6
 119
 120        # bitplane 0
 121        for y in range(8):
 122            yAddr = charAddr | (y << 3)
 123            byte = 0x00
 124            for x in range(8):
 125                byte = (byte << 1) | (pixels[yAddr | x] & 0b1)
 126            CHROut.append(byte)
 127
 128        # bitplane 1
 129        for y in range(8):
 130            yAddr = charAddr | (y << 3)
 131            byte = 0x00
 132            for x in range(8):
 133                byte = (byte << 1) | (pixels[yAddr | x] >> 1)
 134            CHROut.append(byte)
 135
 136    return CHROut
 137
 138def encode_CHR_data(sourceHnd, targetHnd, RGBToCHR):
 139    """Convert PNG file to NES CHR file. Return number of bytes written."""
 140
 141    sourceHnd.seek(0)
 142    targetHnd.seek(0)
 143
 144    reader = png.Reader(sourceHnd)
 145    (width, height, pixelRows) = reader.asRGB8()[:3]
 146
 147    if width != 128:
 148        exit("Image must be 128 pixels wide.")
 149    if height % 8 > 0 or height == 0:
 150        exit("Image height must be a multiple of eight pixels.")
 151
 152    for pixelRow in generate_output_row(pixelRows, RGBToCHR):
 153        targetHnd.write(indexed_row_to_CHR(pixelRow))
 154
 155    return targetHnd.tell()
 156
 157def main():
 158    if len(sys.argv) != 4:
 159        exit(HELP_TEXT)
 160
 161    # read args
 162    (source, target, palette) = sys.argv[1:]
 163
 164    # create dict for mapping RGB colors to CHR colors
 165    RGBToCHR = create_color_mapping(palette)
 166
 167    # files must not be the same
 168    try:
 169        if os.path.samefile(source, target):
 170            exit("Source and target files must not be the same.")
 171    except OSError:
 172        pass
 173
 174    # encode image
 175    try:
 176        with open(source, "rb") as sourceHnd, open(target, "wb") as targetHnd:
 177            bytesWritten = encode_CHR_data(sourceHnd, targetHnd, RGBToCHR)
 178    except OSError:
 179        exit("Error reading source file or writing target file.")
 180
 181    print("OK. {:d} bytes written.".format(bytesWritten))
 182
 183if __name__ == "__main__":
 184    main()