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)))
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
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])
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
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
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()
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 )
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
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]
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
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()
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)
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