diff --git a/core/assets-src/Makefile b/core/assets-src/Makefile index 6d9afc95c..a171f2f3f 100644 --- a/core/assets-src/Makefile +++ b/core/assets-src/Makefile @@ -6,8 +6,6 @@ TOOLS_DIR ?= ../../tools FINAL_OUT_DIR ?= ../../android/assets -ASESPLIT = $(TOOLS_DIR)/asetools/asesplit - all: hud anims stills vehicles tiles helicopter ui map-icons championship-icons input-icons clean: @@ -22,7 +20,7 @@ stills: $(OUT_STILL_IMAGES) $(OUT_DIR)/sprites/%.png: sprites/%.still.ase mkdir -p $(OUT_DIR)/sprites mkdir -p $(OUT_DIR)/sprites/tires - $(ASESPLIT) $< $@ + asesplit $< $@ # Anims ANIM_IMAGES := $(wildcard sprites/*.anim.ase sprites/tires/*.anim.ase) @@ -35,14 +33,14 @@ $(OUT_DIR)/sprites/%_0.png: sprites/%.anim.ase mkdir -p $(OUT_DIR)/sprites/tires # $(@D) is the directory part of the target # $(*F) is the % part of the target - $(ASESPLIT) $< "$(@D)/$(*F)_{frame}.png" + asesplit $< "$(@D)/$(*F)_{frame}.png" # Helicopter helicopter: $(OUT_DIR)/sprites/helicopter-body.png $(OUT_DIR)/sprites/helicopter-body.png: sprites/helicopter.ase mkdir -p $(OUT_DIR)/sprites - $(ASESPLIT) --split-layers --trim $< "$(OUT_DIR)/sprites/helicopter-{layer}.png" + asesplit --split-layers --trim $< "$(OUT_DIR)/sprites/helicopter-{layer}.png" # hud-* # Just enough to trigger the target @@ -51,16 +49,16 @@ hud: $(OUT_DIR)/sprites/hud-pause.png $(OUT_DIR)/sprites/lap-icon.png $(OUT_DIR)/sprites/hud-pause.png: sprites/hud/hud-pie-buttons.ase sprites/hud/hud-sides-buttons.ase sprites/hud/hud.py mkdir -p $(OUT_DIR)/sprites - $(ASESPLIT) --split-layers --trim sprites/hud/hud-pie-buttons.ase \ + asesplit --split-layers --trim sprites/hud/hud-pie-buttons.ase \ "sprites/hud/hud-{layer}.png" - $(ASESPLIT) --split-slices sprites/hud/hud-sides-buttons.ase \ + asesplit --split-slices sprites/hud/hud-sides-buttons.ase \ "sprites/hud/{slice}.png" sprites/hud/hud.py $(OUT_DIR)/sprites $(OUT_DIR)/sprites/lap-icon.png: sprites/hud/lap-icon.ase - $(ASESPLIT) $< $@ + asesplit $< $@ #- sprites/vehicles/ ---------------------------------------------------------- VEHICLE_IMAGES := $(wildcard sprites/vehicles/*.ase) @@ -70,7 +68,7 @@ vehicles: $(OUT_VEHICLE_IMAGES) $(OUT_DIR)/sprites/vehicles/%_0.png: sprites/vehicles/%.ase mkdir -p $(OUT_DIR)/sprites/vehicles - $(ASESPLIT) --rotate -90 $< "$(OUT_DIR)/sprites/vehicles/$(*F)_{frame}.png" + asesplit --rotate -90 $< "$(OUT_DIR)/sprites/vehicles/$(*F)_{frame}.png" #- maps/ ---------------------------------------------------------------------- TILE_IMAGES := $(wildcard maps/*.ase) @@ -81,7 +79,7 @@ tiles: $(OUT_TILE_IMAGES) $(FINAL_OUT_DIR)/maps/%.png: TMP_PNG = $(@:%.png=%-tmp.png) $(FINAL_OUT_DIR)/maps/%.png: maps/%.ase mkdir -p $(FINAL_OUT_DIR)/maps - $(ASESPLIT) $< $(TMP_PNG) + asesplit $< $(TMP_PNG) convert -alpha set -channel RGBA \ -fill '#22203460' -opaque '#ff00ff' \ -fill '#ffffff20' -opaque '#00ffff' \ @@ -96,7 +94,7 @@ OUT_UI_STILL_IMAGES := $(UI_STILL_IMAGES:%.still.ase=$(OUT_DIR)/%.png) $(OUT_DIR)/ui/%.png: ui/%.still.ase mkdir -p $(OUT_DIR)/ui - $(ASESPLIT) $< $@ + asesplit $< $@ # Anims UI_ANIM_IMAGES := $(wildcard ui/*.anim.ase) @@ -105,7 +103,7 @@ OUT_UI_ANIM_FIRST_FRAMES := $(UI_ANIM_IMAGES:%.anim.ase=$(OUT_DIR)/%_0.png) $(OUT_DIR)/ui/%_0.png: ui/%.anim.ase mkdir -p $(OUT_DIR)/ui # $(*F) is the % part of the target - $(ASESPLIT) $< "$(OUT_DIR)/ui/$(*F)_{frame}.png" + asesplit $< "$(OUT_DIR)/ui/$(*F)_{frame}.png" # Slices. We don't know the name of the slices, so use a "timestamp" file to # track the target status @@ -114,7 +112,7 @@ OUT_UI_SLICES_IMAGES := $(UI_SLICES_IMAGES:%.slices.ase=$(OUT_DIR)/%.slices.time $(OUT_DIR)/ui/%.slices.timestamp: ui/%.slices.ase mkdir -p $(OUT_DIR)/ui - $(ASESPLIT) --split-slices $< "$(OUT_DIR)/ui/{slice}.png" + asesplit --split-slices $< "$(OUT_DIR)/ui/{slice}.png" touch $@ ui: $(OUT_UI_STILL_IMAGES) $(OUT_UI_ANIM_FIRST_FRAMES) $(OUT_UI_SLICES_IMAGES) @@ -127,7 +125,7 @@ map-icons: $(OUT_MAP_ICONS) $(OUT_DIR)/ui/map-icons/%.png: ui/map-icons/%.ase mkdir -p $(OUT_DIR)/ui/map-icons - $(ASESPLIT) $< $@ + asesplit $< $@ #- ui/championship-icons/ ----------------------------------------------------- CHAMPIONSHIP_ICONS := $(wildcard ui/championship-icons/*.ase) @@ -137,7 +135,7 @@ championship-icons: $(OUT_CHAMPIONSHIP_ICONS) $(OUT_DIR)/ui/championship-icons/%.png: ui/championship-icons/%.ase mkdir -p $(OUT_DIR)/ui/championship-icons - $(ASESPLIT) $< $@ + asesplit $< $@ #- ui/input-icons/ ------------------------------------------------------------ INPUT_ICONS := ui/input-icons/input-icons.ase @@ -148,4 +146,4 @@ input-icons: $(OUT_INPUT_ICONS_DIR)/sides.png $(OUT_INPUT_ICONS_DIR)/sides.png: $(INPUT_ICONS) mkdir -p $(OUT_INPUT_ICONS_DIR) - $(ASESPLIT) --split-slices $< "$(OUT_INPUT_ICONS_DIR)/{slice}.png" + asesplit --split-slices $< "$(OUT_INPUT_ICONS_DIR)/{slice}.png" diff --git a/tools/README.md b/tools/README.md index 4002f01ba..a3cdca549 100644 --- a/tools/README.md +++ b/tools/README.md @@ -2,12 +2,6 @@ This directory contains tools used to build or work on Pixel Wheels. -## asetools/ - -Command-line tools to turn Aseprite images into PNG usable by the game. - -More details in [asetools/README.md](asetools/README.md). - ## fonts/ Scripts to process fonts shipped with the game, to reduce their size. diff --git a/tools/asetools/README.md b/tools/asetools/README.md deleted file mode 100644 index e128e24cd..000000000 --- a/tools/asetools/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# asetools - -[Aseprite][] is a wonderful pixelart tool. Unfortunately its license is not OSI compliant even if the source code is available, making it complicated to rely on the tool being available everywhere it's needed. This is a problem for CI servers or open-source application stores like F-Droid. - -This directory contains open-source command-line tools to work with Aseprite images. - -[Aseprite]: https://aseprite.com - -## Tools - -### asesplit - -The `asesplit` tool turns ase images into pngs. It can extract individual layers and/or slices, trim and rotate them. Check `asesplit --help` for more details. - -### aseinfo - -The `aseinfo` gives you information about the content of an ase file. - -## Tests - -You can run tests using `pytest`. Just run `pytest` in this directory. - -## Warning - -You are welcome to use these tools in your project, but note that support for ase files is not complete: it only support the subset used for Pixel Wheels assets. In particular: it only supports sprites with a color palette. diff --git a/tools/asetools/aseinfo b/tools/asetools/aseinfo deleted file mode 100755 index b74e2077c..000000000 --- a/tools/asetools/aseinfo +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -""" -Display info about an aseprite file -""" -import argparse -import sys - -from aseprite import AsepriteImage - - -def main(): - parser = argparse.ArgumentParser() - parser.description = __doc__ - - parser.add_argument("ase_file") - - args = parser.parse_args() - - image = AsepriteImage(args.ase_file) - - print(f"size={image.size[0]}x{image.size[1]}") - print(f"depth={image.depth}") - print(f"color_count={image.color_count}") - print(f"frame_count={image.frame_count}") - print(f"transparent_color={image.transparent_color}") - print("# layers") - for idx, layer in reversed(list(enumerate(image.layers))): - print(f"{idx}: {layer.name:40} visible={layer.visible} group={layer.is_group}") - print("# palette (RGBA)") - for idx, color in enumerate(image.palette): - r, g, b, a = color - print(f"{idx:3}: #{r:02x}{b:02x}{g:02x}{a:02x}") - print("# slices") - for slice_ in image.slices: - print(f"{slice_.name:40} pos={slice_.position} size={slice_.size}") - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) -# vi: ts=4 sw=4 et diff --git a/tools/asetools/aseprite.py b/tools/asetools/aseprite.py deleted file mode 100644 index d35edd932..000000000 --- a/tools/asetools/aseprite.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python3 -""" -Load an Aseprite file -""" -import zlib -from io import BytesIO -from struct import unpack -from typing import List - -""" -Aseprite file format doc: - -https://github.com/aseprite/aseprite/blob/master/docs/ase-file-specs.md - -An aseprite image can be considered as a 2D array: layers are the rows and -frames are the columns. - -The main class, AsepriteImage, contains both layers (a list of Layer instances) -and frames (a list of Frame instances). the Frame class contains Cels -instances. Cels are the individual cells of the array: the intersection of a -row and a column. -""" - -MAGIC = 0xA5E0 -FRAME_MAGIC = 0xF1FA - -LAYER_CHUNK = 0x2004 -CEL_CHUNK = 0x2005 -PALETTE_CHUNK = 0x2019 -SLICE_CHUNK = 0x2022 - -LINKED_CEL_TYPE = 1 -COMPRESSED_IMAGE_CEL_TYPE = 2 - - -class NotAsepriteFile(Exception): - pass - - -class NotSupported(Exception): - pass - - -class ChildLayerWithoutParent(Exception): - pass - - -class Layer: - def __init__(self, image: "AsepriteImage", name: str, child_level: int): - self.image = image - self.visible = True - self.is_group = False - self.name = name - self.child_level = child_level - self.parent = None - - def is_really_visible(self): - if not self.visible: - return False - if self.parent is None: - return True - return self.parent.is_really_visible() - - -class Cel: - def __init__(self, layer: Layer): - self.layer = layer - self.position = [0, 0] - self.size = [0, 0] - self.pixels = [] - - -class Slice: - def __init__(self, name: str, pos, size): - self.name = name - self.position = pos - self.size = size - - -class Frame: - def __init__(self, image: "AsepriteImage"): - self.image = image - self.cels = [] - - def append_cel(self, layer: Layer): - self.cels.append(Cel(layer)) - - -class AsepriteImage: - def __init__(self, filename: str): - self.palette = [] - self.size = [0, 0] - self.frame_count = 0 - self.transparent_color = 0 - self.depth = 0 - self.color_count = 0 - self.layers = [] - self.frames = [] - self.slices = [] - - with open(str(filename), "rb") as fp: - self.read_header(fp) - for _ in range(self.frame_count): - self.read_frame(fp) - - def read_header(self, fp): - data = fp.read(44) - ( - file_size, - magic, - self.frame_count, - self.size[0], - self.size[1], - self.depth, - flags, - speed, - zero1, - zero2, - self.transparent_color, - self.color_count, - px_width, - px_height, - grid_x, - grid_y, - grid_width, - grid_height, - ) = unpack(" 1: - for layer in self.layers: - frame.append_cel(layer) - for _ in range(chunk_count): - self.read_chunk(fp) - - def read_chunk(self, fp): - chunk_size, chunk_type = unpack(" 0: - self.set_layer_parent(layer) - - self.layers.append(layer) - # Create a matching cel in the first frame, so that read_cel_chunk has - # a place to write - self.frames[0].append_cel(layer) - - def read_cel_chunk(self, fp): - index, pos_x, pos_y, opacity, cel_type = unpack(" 1: - raise NotSupported("Multi-key slices") - if flags != 0: - raise NotSupported("Slice flags {}".format(flags)) - name = str(fp.read(name_length), "utf-8") - - frame_number, x, y, width, height = unpack(" Image: - box = image.getbbox() - return image.crop(box) - - -def transpose_method_from_angle(angle: int): - angle %= 360 - while angle < 0: - angle += 360 - - if angle == 90: - return Image.ROTATE_90 - elif angle == 180: - return Image.ROTATE_180 - elif angle == 270: - return Image.ROTATE_270 - return None - - -def main(): - parser = argparse.ArgumentParser() - parser.description = __doc__ - - parser.add_argument("ase_file") - parser.add_argument( - "format", - default="{title}.png", - help="Define the name of the generated files. Supported keywords:" - " {title}, {layer}, {frame}, {slice}", - ) - parser.add_argument("--split-layers", action="store_true") - parser.add_argument("--split-slices", action="store_true") - parser.add_argument("--trim", action="store_true") - parser.add_argument( - "--rotate", - type=int, - metavar="ANGLE", - help="Rotate image by ANGLE degrees counter-clockwise", - ) - parser.add_argument("--dry-run", action="store_true") - - args = parser.parse_args() - - if args.rotate is not None: - transpose_method = transpose_method_from_angle(args.rotate) - if transpose_method is None: - parser.error("Invalid angle value") - else: - transpose_method = None - - ase_file = Path(args.ase_file) - - title = ase_file.stem - ase_image = AsepriteImage(ase_file) - - def finish_image(image, context, **extra_context_args): - if transpose_method is not None: - image = image.transpose(transpose_method) - if args.trim: - image = trim(image) - - ctx = dict(context) - ctx.update(extra_context_args) - path = args.format.format(**ctx) - if args.dry_run: - print("Would create {}".format(path)) - else: - print("Creating {}".format(path)) - image.save(path) - - for frame_idx, frame in enumerate(ase_image.frames): - context = dict(frame=frame_idx, title=title) - if args.split_layers: - for layer_idx, layer in enumerate(ase_image.layers): - image = pil_image_for_cel(frame.cels[layer_idx]) - finish_image(image, context, layer=layer.name) - else: - image = render_frame(frame) - if args.split_slices: - for slice_ in ase_image.slices: - x, y = slice_.position - w, h = slice_.size - box = [x, y, (x + w), (y + h)] - sliced_image = image.crop(box) - finish_image(sliced_image, context, slice=slice_.name) - else: - finish_image(image, context) - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) -# vi: ts=4 sw=4 et diff --git a/tools/asetools/fixtures/layer-groups.ase b/tools/asetools/fixtures/layer-groups.ase deleted file mode 100644 index 323948bd5..000000000 Binary files a/tools/asetools/fixtures/layer-groups.ase and /dev/null differ diff --git a/tools/asetools/fixtures/layer-visibility.ase b/tools/asetools/fixtures/layer-visibility.ase deleted file mode 100644 index 67470e64b..000000000 Binary files a/tools/asetools/fixtures/layer-visibility.ase and /dev/null differ diff --git a/tools/asetools/imageops.py b/tools/asetools/imageops.py deleted file mode 100644 index 68fbb41ce..000000000 --- a/tools/asetools/imageops.py +++ /dev/null @@ -1,33 +0,0 @@ -from io import BytesIO -from typing import BinaryIO - -# PIL does not properly handle palette images with alpha, so we use pypng to -# convert the image to RGBA -import png -from aseprite import Cel, Frame -from PIL import Image - - -def save_cel_as_png(cel: Cel, fp: BinaryIO): - width, height = cel.size - lines = [cel.pixels[x : x + width] for x in range(0, len(cel.pixels), width)] - writer = png.Writer(width, height, palette=cel.layer.image.palette, bitdepth=8) - writer.write(fp, lines) - - -def pil_image_for_cel(cel: Cel) -> Image: - fp = BytesIO() - save_cel_as_png(cel, fp) - return Image.open(fp).convert("RGBA") - - -def render_frame(frame: Frame) -> Image: - cels = [ - x for x in frame.cels if x.layer.is_really_visible() and not x.layer.is_group - ] - assert cels, "No visible layers!" - dest_image = Image.new("RGBA", frame.image.size) - for cel in cels: - cel_image = pil_image_for_cel(cel) - dest_image.paste(cel_image, box=cel.position, mask=cel_image) - return dest_image diff --git a/tools/asetools/test_aseprite.py b/tools/asetools/test_aseprite.py deleted file mode 100644 index a2c3fb16b..000000000 --- a/tools/asetools/test_aseprite.py +++ /dev/null @@ -1,77 +0,0 @@ -import os - -from aseprite import AsepriteImage - - -def load_fixture(name): - return AsepriteImage(os.path.join("fixtures", name)) - - -def test_layer_group_order(): - image = load_fixture("layer-groups.ase") - # Layer groups in this image form a hierarchy like this: - # - # g2 - # 2.1 - # g2.2 - # 2.2.2 - # 2.2.1 - # g1 - # 1.1 - layers = image.layers - assert len(layers) == 7 - # Groups first, then their children. Image layers from bottom to top - expected_layer_names = ["g1", "1.1", "g2", "g2.2", "2.2.1", "2.2.2", "2.1"] - assert [x.name for x in layers] == expected_layer_names - - -def test_layer_group_parents(): - image = load_fixture("layer-groups.ase") - - def layer_by_name(name): - for layer in image.layers: - if layer.name == name: - return layer - assert False, "No layer named {}".format(name) - - g2 = layer_by_name("g2") - g1 = layer_by_name("g1") - g22 = layer_by_name("g2.2") - layer221 = layer_by_name("2.2.1") - - assert g2.parent is None - assert g1.parent is None - assert g22.parent is g2 - assert layer221.parent is g22 - - -def test_layer_visibility(): - image = load_fixture("layer-visibility.ase") - # Layers in this image look like this: - # - # 4 (hidden) - # 3 - # g2 (hidden) - # 2.1 - # g1 - # 1.1 (hidden) - - layers = image.layers - assert len(layers) == 6 - - def layer_by_name(name): - for layer in image.layers: - if layer.name == name: - return layer - assert False, "No layer named {}".format(name) - - assert layer_by_name("g1").visible - assert not layer_by_name("1.1").visible - - # Because its parent is not visible - assert not layer_by_name("2.1").is_really_visible() - assert layer_by_name("2.1").visible - - assert not layer_by_name("g2").visible - assert layer_by_name("3").visible - assert not layer_by_name("4").visible