예제 #1
0
def validate_palette(image, filename):
    palette = image.palette.palette
    if len(palette) != 768:
        raise generic.ImageError(
            "Invalid palette; does not contain 256 entries.", filename)
    for i, pal in enumerate(palette_data):
        if pal != palette: continue
        return palette_name[i]
    raise generic.ImageError("Palette is not recognized as a valid palette.",
                             filename)
예제 #2
0
def nml(inputfile, input_filename, output_debug, outputfiles, start_sprite_num,
        compress_grf, crop_sprites, enable_cache, forced_palette, md5_filename,
        rebuild_parser, debug_parser):
    """
    Compile an NML file.

    @param inputfile: File handle associated with the input file.
    @type  inputfile: C{File}

    @param input_filename: Filename of the input file, C{None} if receiving from L{sys.stdin}
    @type  input_filename: C{str} or C{None}

    @param outputfiles: Output streams to write to.
    @type  outputfiles: C{List} of L{output_base.OutputBase}

    @param start_sprite_num: Number of the first sprite.
    @type  start_sprite_num: C{int}

    @param compress_grf: Enable GRF sprite compression.
    @type  compress_grf: C{bool}

    @param crop_sprites: Enable sprite cropping.
    @type  crop_sprites: C{bool}

    @param enable_cache: Enable sprite cache.
    @type  enable_cache: C{bool}

    @param forced_palette: Palette to use for the file.
    @type  forced_palette: C{str}

    @param md5_filename: Filename to use for writing the md5 sum of the grf file. C{None} if the file should not be written.
    @type  md5_filename: C{str} or C{None}
    """
    generic.OnlyOnce.clear()

    generic.print_progress("Reading ...")

    try:
        script = inputfile.read()
    except UnicodeDecodeError as ex:
        raise generic.ScriptError(
            'Input file is not utf-8 encoded: {}'.format(ex))
    # Strip a possible BOM
    script = script.lstrip(str(codecs.BOM_UTF8, "utf-8"))

    if script.strip() == "":
        generic.print_error("Empty input file")
        return 4

    generic.print_progress("Init parser ...")

    nml_parser = parser.NMLParser(rebuild_parser, debug_parser)
    if input_filename is None:
        input_filename = 'input'

    generic.print_progress("Parsing ...")

    result = nml_parser.parse(script, input_filename)
    result.validate([])

    if output_debug > 0:
        result.debug_print(0)

    for outputfile in outputfiles:
        if isinstance(outputfile, output_nml.OutputNML):
            outputfile.open()
            outputfile.write(str(result))
            outputfile.close()

    generic.print_progress("Preprocessing ...")

    result.register_names()
    result.pre_process()
    tmp_actions = result.get_action_list()

    generic.print_progress("Generating actions ...")

    actions = []
    for action in tmp_actions:
        if isinstance(action, action1.SpritesetCollection):
            actions.extend(action.get_action_list())
        else:
            actions.append(action)
    actions.extend(action11.get_sound_actions())

    generic.print_progress("Assigning Action2 registers ...")

    action8_index = -1
    for i in range(len(actions) - 1, -1, -1):
        if isinstance(actions[i],
                      (action2var.Action2Var, action2layout.Action2Layout)):
            actions[i].resolve_tmp_storage()
        elif isinstance(actions[i], action8.Action8):
            action8_index = i

    generic.print_progress("Generating strings ...")

    if action8_index != -1:
        lang_actions = []
        # Add plural/gender/case tables
        for lang_pair in grfstrings.langs:
            lang_id, lang = lang_pair
            lang_actions.extend(action0.get_language_translation_tables(lang))
        # Add global strings
        lang_actions.extend(action4.get_global_string_actions())
        actions = actions[:action8_index +
                          1] + lang_actions + actions[action8_index + 1:]

    generic.print_progress("Collecting real sprites ...")

    # Collect all sprite files, and put them into buckets of same image and mask files
    sprite_files = dict()
    for action in actions:
        if isinstance(action, real_sprite.RealSpriteAction):
            for sprite in action.sprite_list:
                if sprite.is_empty: continue
                sprite.validate_size()

                file = sprite.file
                if file is not None:
                    file = file.value

                mask_file = sprite.mask_file
                if mask_file is not None:
                    mask_file = mask_file.value

                key = (file, mask_file)
                sprite_files.setdefault(key, []).append(sprite)

    # Check whether we can terminate sprite processing prematurely for
    #     dependency checks
    skip_sprite_processing = True
    for outputfile in outputfiles:
        if isinstance(outputfile, output_dep.OutputDEP):
            outputfile.open()
            for f in sprite_files:
                if f[0] is not None:
                    outputfile.write(f[0])
                if f[1] is not None:
                    outputfile.write(f[1])
            outputfile.close()
        skip_sprite_processing &= outputfile.skip_sprite_checks()

    if skip_sprite_processing:
        generic.clear_progress()
        return 0

    if not Image and len(sprite_files) > 0:
        generic.print_error(
            "PIL (python-imaging) wasn't found, no support for using graphics")
        sys.exit(3)

    generic.print_progress("Checking palette of source images ...")

    used_palette = forced_palette
    last_file = None
    for f_pair in sprite_files:
        # Palette is defined by mask_file, if present. Otherwise by the main file.
        f = f_pair[1]
        if f is None:
            f = f_pair[0]

        try:
            im = Image.open(generic.find_file(f))
        except IOError as ex:
            raise generic.ImageError(str(ex), f)
        if im.mode != "P":
            continue
        pal = palette.validate_palette(im, f)

        if forced_palette != "ANY" and pal != forced_palette and not (
                forced_palette == "DEFAULT" and pal == "LEGACY"):
            raise generic.ImageError(
                "Image has '{}' palette, but you forced the '{}' palette".
                format(pal, used_palette), f)

        if used_palette == "ANY":
            used_palette = pal
        elif pal != used_palette:
            if used_palette in ("LEGACY", "DEFAULT") and pal in ("LEGACY",
                                                                 "DEFAULT"):
                used_palette = "DEFAULT"
            else:
                raise generic.ImageError(
                    "Image has '{}' palette, but \"{}\" has the '{}' palette".
                    format(pal, last_file, used_palette), f)
        last_file = f

    palette_bytes = {"LEGACY": "W", "DEFAULT": "D", "ANY": "A"}
    if used_palette in palette_bytes:
        grf.set_palette_used(palette_bytes[used_palette])
    encoder = None
    for outputfile in outputfiles:
        outputfile.palette = used_palette  # used by RecolourSpriteAction
        if isinstance(outputfile, output_grf.OutputGRF):
            if encoder is None:
                encoder = spriteencoder.SpriteEncoder(compress_grf,
                                                      crop_sprites,
                                                      enable_cache,
                                                      used_palette)
            outputfile.encoder = encoder

    generic.clear_progress()

    # Read all image data, compress, and store in sprite cache
    if encoder is not None:
        encoder.open(sprite_files)

    #If there are any 32bpp sprites hint to openttd that we'd like a 32bpp blitter
    if alt_sprites.any_32bpp_sprites:
        grf.set_preferred_blitter("3")

    generic.print_progress("Linking actions ...")

    if action8_index != -1:
        actions = [sprite_count.SpriteCountAction(len(actions))] + actions

    for idx, action in enumerate(actions):
        num = start_sprite_num + idx
        action.prepare_output(num)

    # Processing finished, print some statistics
    action0.print_stats()
    actionF.print_stats()
    action7.print_stats()
    action1.print_stats()
    action2.print_stats()
    action6.print_stats()
    grf.print_stats()
    global_constants.print_stats()
    action4.print_stats()
    action11.print_stats()

    generic.print_progress("Writing output ...")

    md5 = None
    for outputfile in outputfiles:
        if isinstance(outputfile, output_grf.OutputGRF):
            outputfile.open()
            for action in actions:
                action.write(outputfile)
            outputfile.close()
            md5 = outputfile.get_md5()

        if isinstance(outputfile, output_nfo.OutputNFO):
            outputfile.open()
            for action in actions:
                action.write(outputfile)
            outputfile.close()

    if md5 is not None and md5_filename is not None:
        with open(md5_filename, 'w', encoding="utf-8") as f:
            f.write(md5 + '\n')

    if encoder is not None:
        encoder.close()

    generic.clear_progress()
    return 0
예제 #3
0
    def encode_sprite(self, sprite_info):
        """
        Crop and compress a real sprite.

        @param sprite_info: Sprite meta data
        @type  sprite_info: C{RealSprite}

        @return: size_x, size_y, xoffset, yoffset, compressed_data, info_byte, crop_rect, pixel_stats
        @rtype: C{tuple}
        """

        filename_8bpp = None
        filename_32bpp = None
        if sprite_info.bit_depth == 8:
            filename_8bpp = sprite_info.file
        else:
            filename_32bpp = sprite_info.file
            filename_8bpp = sprite_info.mask_file

        # Get initial info_byte and dimensions from sprite_info.
        # These values will be changed depending on cropping and compression.
        info_byte = INFO_NOCROP if (sprite_info.flags.value
                                    & real_sprite.FLAG_NOCROP) != 0 else 0
        size_x = sprite_info.xsize.value
        size_y = sprite_info.ysize.value
        xoffset = sprite_info.xrel.value
        yoffset = sprite_info.yrel.value

        # Select region of image bounded by x/ypos and x/ysize
        x = sprite_info.xpos.value
        y = sprite_info.ypos.value
        if sprite_info.bit_depth == 8 or sprite_info.mask_pos is None:
            mask_x, mask_y = x, y
        else:
            mask_x = sprite_info.mask_pos[0].value
            mask_y = sprite_info.mask_pos[1].value

        pixel_stats = {
            "total": size_x * size_y,
            "alpha": 0,
            "white": 0,
            "anim": 0
        }

        # Read and validate image data
        if filename_32bpp is not None:
            im = self.open_image_file(filename_32bpp.value)
            if im.mode not in ("RGB", "RGBA"):
                pos = generic.build_position(sprite_info.poslist)
                raise generic.ImageError(
                    "32bpp image is not a full colour RGB(A) image.",
                    filename_32bpp.value, pos)
            info_byte |= INFO_RGB
            if im.mode == "RGBA":
                info_byte |= INFO_ALPHA

            (im_width, im_height) = im.size
            if x < 0 or y < 0 or x + size_x > im_width or y + size_y > im_height:
                pos = generic.build_position(sprite_info.poslist)
                raise generic.ScriptError(
                    "Read beyond bounds of image file '{}'".format(
                        filename_32bpp.value), pos)
            try:
                sprite = im.crop((x, y, x + size_x, y + size_y))
            except OSError:
                pos = generic.build_position(sprite_info.poslist)
                raise generic.ImageError(
                    "Failed to crop 32bpp {} image".format(im.format),
                    filename_32bpp.value, pos)
            rgb_sprite_data = sprite.tobytes()

            if (info_byte & INFO_ALPHA) != 0:
                # Check for half-transparent pixels (not valid for ground sprites)
                pixel_stats["alpha"] = sum(0x00 < p < 0xFF
                                           for p in rgb_sprite_data[3::4])

        if filename_8bpp is not None:
            mask_im = self.open_image_file(filename_8bpp.value)
            if mask_im.mode != "P":
                pos = generic.build_position(sprite_info.poslist)
                raise generic.ImageError("8bpp image does not have a palette",
                                         filename_8bpp.value, pos)
            im_mask_pal = palette.validate_palette(mask_im,
                                                   filename_8bpp.value)
            info_byte |= INFO_PAL

            (im_width, im_height) = mask_im.size
            if mask_x < 0 or mask_y < 0 or mask_x + size_x > im_width or mask_y + size_y > im_height:
                pos = generic.build_position(sprite_info.poslist)
                raise generic.ScriptError(
                    "Read beyond bounds of image file '{}'".format(
                        filename_8bpp.value), pos)
            try:
                mask_sprite = mask_im.crop(
                    (mask_x, mask_y, mask_x + size_x, mask_y + size_y))
            except OSError:
                pos = generic.build_position(sprite_info.poslist)
                raise generic.ImageError(
                    "Failed to crop 8bpp {} image".format(mask_im.format),
                    filename_8bpp.value, pos)
            mask_sprite_data = self.palconvert(mask_sprite.tobytes(),
                                               im_mask_pal)

            # Check for white pixels; those that cause "artefacts" when shading
            pixel_stats["white"] = sum(p == 255 for p in mask_sprite_data)

            # Check for palette animation colours
            if self.palette == "DEFAULT":
                pixel_stats["anim"] = sum(0xE3 <= p <= 0xFE
                                          for p in mask_sprite_data)
            else:
                pixel_stats["anim"] = sum(0xD9 <= p <= 0xF4
                                          for p in mask_sprite_data)

        # Compose pixel information in an array of bytes
        sprite_data = array.array("B")
        if (info_byte & INFO_RGB) != 0 and (info_byte & INFO_PAL) != 0:
            mask_data = array.array("B",
                                    mask_sprite_data)  # Convert to numeric
            rgb_data = array.array("B", rgb_sprite_data)
            if (info_byte & INFO_ALPHA) != 0:
                for i in range(len(mask_sprite_data)):
                    sprite_data.extend(rgb_data[4 * i:4 * (i + 1)])
                    sprite_data.append(mask_data[i])
            else:
                for i in range(len(mask_sprite_data)):
                    sprite_data.extend(rgb_data[3 * i:3 * (i + 1)])
                    sprite_data.append(mask_data[i])
        elif (info_byte & INFO_RGB) != 0:
            sprite_data.frombytes(rgb_sprite_data)
        else:
            sprite_data.frombytes(mask_sprite_data)

        bpp = get_bpp(info_byte)
        assert len(sprite_data) == size_x * size_y * bpp
        if self.crop_sprites and ((info_byte & INFO_NOCROP) == 0):
            sprite_data, crop_rect = self.crop_sprite(sprite_data, size_x,
                                                      size_y, info_byte, bpp)
            size_x, size_y, xoffset, yoffset = self.recompute_offsets(
                size_x, size_y, xoffset, yoffset, crop_rect)
        else:
            crop_rect = None
        assert len(sprite_data) == size_x * size_y * bpp

        compressed_data = self.sprite_compress(sprite_data)
        # Try tile compression, and see if it results in a smaller file size
        tile_data = self.sprite_encode_tile(size_x, size_y, sprite_data,
                                            info_byte, bpp)
        if tile_data is not None:
            tile_compressed_data = self.sprite_compress(tile_data)
            # Tile compression adds another 4 bytes for the uncompressed chunked data in the header
            if len(tile_compressed_data) + 4 < len(compressed_data):
                info_byte |= INFO_TILE
                data_len = len(tile_data)
                compressed_data = array.array("B")
                compressed_data.append(data_len & 0xFF)
                compressed_data.append((data_len >> 8) & 0xFF)
                compressed_data.append((data_len >> 16) & 0xFF)
                compressed_data.append((data_len >> 24) & 0xFF)
                compressed_data.extend(tile_compressed_data)

        return (size_x, size_y, xoffset, yoffset, compressed_data, info_byte,
                crop_rect, pixel_stats)