예제 #1
0
    def from_pil(self, pil: Image.Image, force_import=False) -> None:
        """
        Modify the image data in the BGP by importing the passed PIL.
        The passed PIL will be split into separate tiles and the tile's palette index
        is determined by the first pixel value of each tile in the PIL. The PIL
        must have a palette containing the 16 sub-palettes with 16 colors each (256 colors).

        If a pixel in a tile uses a color outside of it's 16 color range, an error is thrown or
        the color is replaced with 0 of the palette (transparent). This is controlled by
        the force_import flag.

        The image must have the size 256x192. For dimension calculating, see the constants of this module.
        """
        self.tiles, self.tilemap, self.palettes = from_pil(
            pil, BGP_PAL_NUMBER_COLORS, BGP_MAX_PAL, BGP_TILE_DIM,
            BGP_RES_WIDTH, BGP_RES_HEIGHT, 1, 1, force_import)

        if len(self.tiles) == 0x3FF:
            raise AttributeError(
                f"Error when importing: max tile count reached.")

        # Add the 0 tile (used to clear bgs)
        self.tiles.insert(0, bytearray(int(BGP_TILE_DIM * BGP_TILE_DIM / 2)))
        # Shift tile indices by 1
        for x in self.tilemap:
            x.idx += 1

        # Fill up the tiles and tilemaps to 1024, which seems to be the required default
        for _ in range(len(self.tiles), BGP_TOTAL_NUMBER_TILES_ACTUALLY):
            self.tiles.append(bytearray(int(BGP_TILE_DIM * BGP_TILE_DIM / 2)))
        for _ in range(len(self.tilemap), BGP_TOTAL_NUMBER_TILES_ACTUALLY):
            self.tilemap.append(TilemapEntry.from_int(u16(0)))
예제 #2
0
    def pil_to_chunks(
            self,
            image: Image.Image,
            force_import=True) -> Tuple[List[bytes], List[List[int]]]:
        """
        Imports chunks. Format same as for chunks_to_pil.
        Replaces tile mappings and returns the new tiles for storing them in a DPCI and the palettes
        for storing in a DPL.

        The PIL must have a palette containing the 16 sub-palettes with 16 colors each (256 colors).

        If a pixel in a tile uses a color outside of it's 16 color range, an error is thrown or
        the color is replaced with 0 of the palette (transparent). This is controlled by
        the force_import flag.
        """
        tiles, all_tilemaps, palettes = from_pil(image, DPL_PAL_LEN, 16,
                                                 DPCI_TILE_DIM, image.width,
                                                 image.height, DPC_TILING_DIM,
                                                 DPC_TILING_DIM, force_import)
        # Validate number of palettes
        palettes = palettes[:DPL_MAX_PAL]
        for tm in all_tilemaps:
            if tm.pal_idx > DPL_MAX_PAL - 1:
                raise ValueError(
                    f(
                        _("The image to import can only use the first 12 palettes. "
                          "Tried to use palette {tm.pal_idx}")))
        self.chunks = list(
            chunks(all_tilemaps, DPC_TILING_DIM * DPC_TILING_DIM))
        self.re_fill_chunks()
        return tiles, palettes
예제 #3
0
    def pil_to_tiles(self, image: Image.Image):
        """
        Converts a PIL image back to the BPA.
        The format is expected to be the same as tiles_to_pil. This means, that
        each rows of tiles is one image set and each column is one frame.
        """
        tiles, _, __ = from_pil(
            image, BPL_IMG_PAL_LEN, BPL_MAX_PAL, BPA_TILE_DIM,
            image.width, image.height, optimize=False
        )
        self.tiles = []
        self.number_of_frames = int(image.width / BPA_TILE_DIM)
        self.number_of_tiles = int(image.height / BPA_TILE_DIM)

        # We need to re-order the tiles to actually save them
        for frame_idx in range(0, self.number_of_frames):
            for tile_idx in range(0, self.number_of_tiles):
                self.tiles.append(tiles[tile_idx * self.number_of_frames + frame_idx])

        # Correct frame info size
        len_finfo = len(self.frame_info)
        if len_finfo > self.number_of_frames:
            self.frame_info = self.frame_info[:self.number_of_frames]
        elif len_finfo < self.number_of_frames:
            for i in range(len_finfo, self.number_of_frames):
                # If the length is shorter, we just copy the last entry
                self.frame_info.append(self.frame_info[len_finfo-1])
예제 #4
0
 def from_pil(self, index, img: Image.Image, import_palettes=True):
     tiles, tilemaps, palettes = from_pil(img, PAL_LEN, len(self.palettes),
                                          TILE_DIM, TILE_DIM * CHUNK_DIM,
                                          TILE_DIM * CHUNK_DIM)
     self.sprites[index] = tiles
     if import_palettes:
         self.palettes = palettes
예제 #5
0
    def pil_to_chunks(self,
                      layer: int,
                      image: Image.Image,
                      force_import=True) -> List[List[int]]:
        """
        Imports chunks. Format same as for chunks_to_pil.
        Replaces tiles, tile mappings and therefor also chunks.
        "Unsets" BPA assignments! BPAs have to be manually re-assigned by using set_tile or set_chunk. BPA
        indices are stored after BPC tile indices.

        The PIL must have a palette containing the 16 sub-palettes with 16 colors each (256 colors).

        If a pixel in a tile uses a color outside of it's 16 color range, an error is thrown or
        the color is replaced with 0 of the palette (transparent). This is controlled by
        the force_import flag.

        Returns the palettes stored in the image for further processing (eg. replacing the BPL palettes).
        """
        self.layers[layer].tiles, self.layers[
            layer].tilemap, palettes = from_pil(image, BPL_IMG_PAL_LEN,
                                                BPL_MAX_PAL, BPC_TILE_DIM,
                                                image.width, image.height, 3,
                                                3, force_import)
        self.layers[layer].number_tiles = len(self.layers[layer].tiles) - 1
        self.layers[layer].chunk_tilemap_len = int(
            len(self.layers[layer].tilemap) / self.tiling_width /
            self.tiling_height)
        return palettes
예제 #6
0
    def pil_to_tiles_separate(self, images: List[Image.Image]) -> None:
        frames = []
        first_image_dims = None
        for image in images:
            frames.append(
                from_pil(image,
                         BPL_IMG_PAL_LEN,
                         BPL_MAX_PAL,
                         BPA_TILE_DIM,
                         image.width,
                         image.height,
                         optimize=False)[0])
            if first_image_dims is None:
                first_image_dims = (image.width, image.height)
            if (image.width, image.height) != first_image_dims:
                raise ValueError(
                    _("The dimensions of all images must be the same."))
        self.tiles = []
        self.number_of_frames = u16_checked(len(frames))
        self.number_of_tiles = u16_checked(
            int((images[0].height * images[0].width) /
                (BPA_TILE_DIM * BPA_TILE_DIM)))

        for tile in frames:
            self.tiles += tile

        self._correct_frame_info()
예제 #7
0
 def pil_to_tiles(self, image: Image.Image):
     """
     Imports tiles that are in a format as described in the documentation for tiles_to_pil.
     """
     self.tiles, _, __ = from_pil(
         image, DPL_PAL_LEN, 16, DPCI_TILE_DIM,
         image.width, image.height, optimize=False
     )
예제 #8
0
 def pil_to_tiles(self, layer: int, image: Image.Image):
     """
     Imports tiles that are in a format as described in the documentation for tiles_to_pil.
     Tile mappings, chunks and palettes are not updated.
     """
     self.layers[layer].tiles, _, __ = from_pil(
         image, BPL_IMG_PAL_LEN, BPL_MAX_PAL, BPC_TILE_DIM,
         image.width, image.height, optimize=False
     )
     self.layers[layer].number_tiles = len(self.layers[layer].tiles) - 1
예제 #9
0
 def from_pil(self, img: Image.Image) -> None:
     tiles, _, pals = from_pil(img,
                               16,
                               1,
                               ICON_DIM_TILE,
                               ICON_DIM_IMG_PX,
                               ICON_DIM_IMG_PX,
                               optimize=False)
     self.bitmap = bytes(itertools.chain.from_iterable(tiles))
     self._palette = pals[0]
예제 #10
0
 def _read_in(cls, pil: Image, w_in_tiles,
              h_in_tiles) -> Tuple[List[int], bytes]:
     w = TILE_DIM * w_in_tiles
     h = TILE_DIM * h_in_tiles
     tiles, tile_mappings, pal = from_pil(pil,
                                          16,
                                          1,
                                          TILE_DIM,
                                          w,
                                          h,
                                          1,
                                          1,
                                          force_import=True,
                                          optimize=False)
     # todo: in theory the tiles could be out of order and we would need to check using the mappings,
     #       in practice they aren't.
     tiles_concat = bytes(itertools.chain.from_iterable(tiles))
     return pal[0], tiles_concat
    def _import_to_bpc(self, layer, image):
        new_tiles, new_tilemap, palettes = from_pil(
            image, BPL_IMG_PAL_LEN, BPL_MAX_PAL, BPC_TILE_DIM,
            image.width, image.height, 3, 3
        )
        # Correct the indices of the new tilemap
        # Correct the layer mappings to use the correct palettes
        start_offset = self.bpc.layers[0].number_tiles + 1
        for m in new_tilemap:
            m.idx += start_offset
            m.pal_idx += 6
        self.bpc.layers[layer].tiles += new_tiles
        self.bpc.layers[layer].tilemap += new_tilemap

        self.bpc.layers[layer].number_tiles += len(new_tiles)
        self.bpc.layers[layer].chunk_tilemap_len += int(len(new_tilemap) / self.bpc.tiling_width / self.bpc.tiling_height)

        return palettes
예제 #12
0
    def pil_to_tiles(self, image: Image.Image):
        """
        Converts a PIL image back to the BPA.
        The format is expected to be the same as tiles_to_pil. This means, that
        each rows of tiles is one image set and each column is one frame.
        """
        tiles, _, __ = from_pil(
            image, BPL_IMG_PAL_LEN, BPL_MAX_PAL, BPA_TILE_DIM,
            image.width, image.height, optimize=False
        )
        self.tiles = []
        self.number_of_frames = int(image.width / BPA_TILE_DIM)
        self.number_of_tiles = int(image.height / BPA_TILE_DIM)

        # We need to re-order the tiles to actually save them
        for frame_idx in range(0, self.number_of_frames):
            for tile_idx in range(0, self.number_of_tiles):
                self.tiles.append(tiles[tile_idx * self.number_of_frames + frame_idx])

        self._correct_frame_info()
예제 #13
0
    def from_pil(self,
                 bpc: Bpc,
                 bpl: Bpl,
                 lower_img: Image.Image = None,
                 upper_img: Image.Image = None,
                 force_import=False,
                 how_many_palettes_lower_layer=16):
        """
        Import an entire map from one or two images (for each layer).
        Changes all tiles, tilemappings and chunks in the BPC and re-writes the two layer mappings of the BMA.
        Imports the palettes of the image to the BPL.
        The palettes of the images passed into this method must either identical or can be merged.
        The how_many_palettes_lower_layer parameter controls how many palettes
        from the lower layer image will then be used.

        The passed PIL will be split into separate tiles and the tile's palette index in the tile mapping for this
        coordinate is determined by the first pixel value of each tile in the PIL. The PIL
        must have a palette containing up to 16 sub-palettes with 16 colors each (256 colors).

        If a pixel in a tile uses a color outside of it's 16 color range, an error is thrown or
        the color is replaced with 0 of the palette (transparent). This is controlled by
        the force_import flag.

        Does not import animations. BPA tiles must be manually mapped to the tilemappings of the BPC after the import.
        BPL palette animations are not modified.

        The input images must have the same dimensions as the BMA (same dimensions as to_pil_single_layer would export).
        The input image can have a different number of layers, than the BMA. BPC and BMA layers are changed accordingly.

        BMA collision and data layer are not modified.
        """
        expected_width = self.tiling_width * self.map_width_chunks * BPC_TILE_DIM
        expected_height = self.tiling_height * self.map_height_chunks * BPC_TILE_DIM
        if (False if lower_img is None else lower_img.width != expected_width) \
                or (False if upper_img is None else upper_img.width != expected_width):
            raise ValueError(
                f"Can not import map background: Width of both images must match the current map width: "
                f"{expected_width}px")
        if (False if lower_img is None else lower_img.height != expected_height) \
                or (False if upper_img is None else upper_img.height != expected_height):
            raise ValueError(
                f"Can not import map background: Height of both images must match the current map height: "
                f"{expected_height}px")
        upper_palette_palette_color_offset = 0
        if upper_img is not None and lower_img is not None and how_many_palettes_lower_layer < BPL_MAX_PAL:
            # Combine palettes
            lower_palette = lower_img.getpalette(
            )[:how_many_palettes_lower_layer * (BPL_PAL_LEN + 1) * 3]
            upper_palette = upper_img.getpalette()[:(
                BPL_MAX_PAL - how_many_palettes_lower_layer) *
                                                   (BPL_PAL_LEN + 1) * 3]
            new_palette = lower_palette + upper_palette
            lower_img.putpalette(new_palette)
            upper_img.putpalette(new_palette)
            # We need to offset the colors in the upper image now, when we read it.
            upper_palette_palette_color_offset = how_many_palettes_lower_layer

        # Adjust layer numbers
        number_of_layers = 2 if upper_img is not None else 1
        low_map_idx = 0 if lower_img is not None else 1
        if number_of_layers > self.number_of_layers:
            self.add_upper_layer()
            bpc.add_upper_layer()

        # Import tiles, tile mappings and chunks mappings
        for layer_idx in range(low_map_idx, number_of_layers):
            if layer_idx == 0:
                bpc_layer_id = 0 if bpc.number_of_layers == 1 else 1
                img = lower_img
                palette_offset = 0
            else:
                bpc_layer_id = 0
                img = upper_img
                palette_offset = upper_palette_palette_color_offset

            tiles, all_possible_tile_mappings, palettes = from_pil(
                img,
                BPL_IMG_PAL_LEN,
                BPL_MAX_PAL,
                BPC_TILE_DIM,
                img.width,
                img.height,
                3,
                3,
                force_import,
                palette_offset=palette_offset)
            bpc.import_tiles(bpc_layer_id, tiles)

            # Build a new list of chunks / tile mappings for the BPC based on repeating chunks
            # in the imported image. Generate chunk mappings.
            chunk_mappings = []
            chunk_mappings_counter = 1
            tile_mappings = []
            tiles_in_chunk = self.tiling_width * self.tiling_height
            for chk_fst_tile_idx in range(
                    0, self.map_width_chunks * self.map_height_chunks *
                    tiles_in_chunk, tiles_in_chunk):
                chunk = all_possible_tile_mappings[
                    chk_fst_tile_idx:chk_fst_tile_idx + tiles_in_chunk]
                start_of_existing_chunk = search_for_chunk(
                    chunk, tile_mappings)
                if start_of_existing_chunk is not None:
                    chunk_mappings.append(
                        int(start_of_existing_chunk / tiles_in_chunk) + 1)
                else:
                    tile_mappings += chunk
                    chunk_mappings.append(chunk_mappings_counter)
                    chunk_mappings_counter += 1

            bpc.import_tile_mappings(bpc_layer_id, tile_mappings)
            if layer_idx == 0:
                self.layer0 = chunk_mappings
            else:
                self.layer1 = chunk_mappings

        # Import palettes
        bpl.import_palettes(palettes)
예제 #14
0
    def from_pil(self,
                 dpc: Dpc,
                 dpci: Dpci,
                 dpl: Dpl,
                 img: Image.Image,
                 force_import: bool = False) -> None:
        """
        Import an entire background from an image.
        Changes all tiles, tile mappings and chunks in the DPC/DPCI and re-writes the mappings of the DBG.
        Imports the palettes of the image to the DPL.

        The passed PIL will be split into separate tiles and the tile's palette index in the tile mapping for this
        coordinate is determined by the first pixel value of each tile in the PIL. The PIL
        must have a palette containing up to 16 sub-palettes with 16 colors each (256 colors).

        If a pixel in a tile uses a color outside of it's 16 color range, an error is thrown or
        the color is replaced with 0 of the palette (transparent). This is controlled by
        the force_import flag.

        The input images must have the same dimensions as the DBG (same dimensions as to_pil_single_layer would export).
        """
        expected_width = DBG_TILING_DIM * DBG_WIDTH_AND_HEIGHT * DPCI_TILE_DIM
        expected_height = DBG_TILING_DIM * DBG_WIDTH_AND_HEIGHT * DPCI_TILE_DIM
        if img.width != expected_width:
            raise UserValueError(
                f(
                    _("Can not import map background: Width of image must match the expected width: "
                      "{expected_width}px")))
        if img.height != expected_height:
            raise UserValueError(
                f(
                    _("Can not import map background: Height of image must match the expected height: "
                      "{expected_height}px")))

        # Import tiles, tile mappings and chunks mappings
        tiles, all_possible_tile_mappings, palettes = from_pil(
            img, DPL_PAL_LEN, 16, DPCI_TILE_DIM, img.width, img.height, 3, 3,
            force_import)
        # Remove any extra colors
        palettes = palettes[:DPL_MAX_PAL]

        dpci.import_tiles(tiles)

        # Build a new list of chunks / tile mappings for the DPC based on repeating chunks
        # in the imported image. Generate chunk mappings.
        chunk_mappings = []
        chunk_mappings_counter = 1
        tile_mappings: List[TilemapEntryProtocol] = []
        tiles_in_chunk = DBG_TILING_DIM * DBG_TILING_DIM
        for chk_fst_tile_idx in range(
                0,
                DBG_WIDTH_AND_HEIGHT * DBG_WIDTH_AND_HEIGHT * tiles_in_chunk,
                tiles_in_chunk):
            chunk = all_possible_tile_mappings[
                chk_fst_tile_idx:chk_fst_tile_idx + tiles_in_chunk]
            start_of_existing_chunk = search_for_chunk(chunk, tile_mappings)
            if start_of_existing_chunk is not None:
                chunk_mappings.append(
                    u16(int(start_of_existing_chunk / tiles_in_chunk) + 1))
            else:
                tile_mappings += chunk
                chunk_mappings.append(u16(chunk_mappings_counter))
                chunk_mappings_counter += 1

        dpc.import_tile_mappings(
            list(chunks(tile_mappings,
                        DPC_TILING_DIM * DPC_TILING_DIM)))  # type: ignore
        self.mappings = chunk_mappings

        # Import palettes
        dpl.palettes = palettes