forked from yogstation13/Yogstation
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
kill me (presumably ported tg icon/map merger)
Conflicts: dependencies.sh tools/hooks/install.sh tools/hooks/python.sh tools/requirements.txt
- Loading branch information
Showing
27 changed files
with
1,203 additions
and
138 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
@call "%~dp0\..\bootstrap\python.bat" -m dmi.merge_driver --posthoc %* | ||
@pause |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
# Tools for working with modern DreamMaker icon files (PNGs + metadata) | ||
|
||
import math | ||
from PIL import Image | ||
from PIL.PngImagePlugin import PngInfo | ||
|
||
DEFAULT_SIZE = 32, 32 | ||
LOOP_UNLIMITED = 0 | ||
LOOP_ONCE = 1 | ||
|
||
NORTH = 1 | ||
SOUTH = 2 | ||
EAST = 4 | ||
WEST = 8 | ||
SOUTHEAST = SOUTH | EAST | ||
SOUTHWEST = SOUTH | WEST | ||
NORTHEAST = NORTH | EAST | ||
NORTHWEST = NORTH | WEST | ||
|
||
CARDINALS = [NORTH, SOUTH, EAST, WEST] | ||
DIR_ORDER = [SOUTH, NORTH, EAST, WEST, SOUTHEAST, SOUTHWEST, NORTHEAST, NORTHWEST] | ||
DIR_NAMES = { | ||
'SOUTH': SOUTH, | ||
'NORTH': NORTH, | ||
'EAST': EAST, | ||
'WEST': WEST, | ||
'SOUTHEAST': SOUTHEAST, | ||
'SOUTHWEST': SOUTHWEST, | ||
'NORTHEAST': NORTHEAST, | ||
'NORTHWEST': NORTHWEST, | ||
**{str(x): x for x in DIR_ORDER}, | ||
**{x: x for x in DIR_ORDER}, | ||
'0': SOUTH, | ||
None: SOUTH, | ||
} | ||
|
||
|
||
class Dmi: | ||
version = "4.0" | ||
|
||
def __init__(self, width, height): | ||
self.width = width | ||
self.height = height | ||
self.states = [] | ||
|
||
@classmethod | ||
def from_file(cls, fname): | ||
image = Image.open(fname) | ||
if image.mode != 'RGBA': | ||
image = image.convert('RGBA') | ||
|
||
# no metadata = regular image file | ||
if 'Description' not in image.info: | ||
dmi = Dmi(*image.size) | ||
state = dmi.state("") | ||
state.frame(image) | ||
return dmi | ||
|
||
# read metadata | ||
metadata = image.info['Description'] | ||
line_iter = iter(metadata.splitlines()) | ||
assert next(line_iter) == "# BEGIN DMI" | ||
assert next(line_iter) == f"version = {cls.version}" | ||
|
||
dmi = Dmi(*DEFAULT_SIZE) | ||
state = None | ||
|
||
for line in line_iter: | ||
if line == "# END DMI": | ||
break | ||
key, value = line.lstrip().split(" = ") | ||
if key == 'width': | ||
dmi.width = int(value) | ||
elif key == 'height': | ||
dmi.height = int(value) | ||
elif key == 'state': | ||
state = dmi.state(unescape(value)) | ||
elif key == 'dirs': | ||
state.dirs = int(value) | ||
elif key == 'frames': | ||
state._nframes = int(value) | ||
elif key == 'delay': | ||
state.delays = [parse_num(x) for x in value.split(',')] | ||
elif key == 'loop': | ||
state.loop = int(value) | ||
elif key == 'rewind': | ||
state.rewind = parse_bool(value) | ||
elif key == 'hotspot': | ||
x, y, frm = [int(x) for x in value.split(',')] | ||
state.hotspot(frm - 1, x, y) | ||
elif key == 'movement': | ||
state.movement = parse_bool(value) | ||
else: | ||
raise NotImplementedError(key) | ||
|
||
# cut image into frames | ||
width, height = image.size | ||
gridwidth = width // dmi.width | ||
i = 0 | ||
for state in dmi.states: | ||
for frame in range(state._nframes): | ||
for dir in range(state.dirs): | ||
px = dmi.width * (i % gridwidth) | ||
py = dmi.height * (i // gridwidth) | ||
im = image.crop((px, py, px + dmi.width, py + dmi.height)) | ||
assert im.size == (dmi.width, dmi.height) | ||
state.frames.append(im) | ||
i += 1 | ||
state._nframes = None | ||
|
||
return dmi | ||
|
||
def state(self, *args, **kwargs): | ||
s = State(self, *args, **kwargs) | ||
self.states.append(s) | ||
return s | ||
|
||
@property | ||
def default_state(self): | ||
return self.states[0] | ||
|
||
def get_state(self, name): | ||
for state in self.states: | ||
if state.name == name: | ||
return state | ||
raise KeyError(name) | ||
|
||
def _assemble_comment(self): | ||
comment = "# BEGIN DMI\n" | ||
comment += f"version = {self.version}\n" | ||
comment += f"\twidth = {self.width}\n" | ||
comment += f"\theight = {self.height}\n" | ||
for state in self.states: | ||
comment += f"state = {escape(state.name)}\n" | ||
comment += f"\tdirs = {state.dirs}\n" | ||
comment += f"\tframes = {state.framecount}\n" | ||
if state.framecount > 1 and len(state.delays): # any(x != 1 for x in state.delays): | ||
comment += "\tdelay = " + ",".join(map(str, state.delays)) + "\n" | ||
if state.loop != 0: | ||
comment += f"\tloop = {state.loop}\n" | ||
if state.rewind: | ||
comment += "\trewind = 1\n" | ||
if state.movement: | ||
comment += "\tmovement = 1\n" | ||
if state.hotspots and any(state.hotspots): | ||
current = None | ||
for i, value in enumerate(state.hotspots): | ||
if value != current: | ||
x, y = value | ||
comment += f"\thotspot = {x},{y},{i + 1}\n" | ||
current = value | ||
comment += "# END DMI" | ||
return comment | ||
|
||
def to_file(self, filename, *, palette=False): | ||
# assemble comment | ||
comment = self._assemble_comment() | ||
|
||
# assemble spritesheet | ||
W, H = self.width, self.height | ||
num_frames = sum(len(state.frames) for state in self.states) | ||
sqrt = math.ceil(math.sqrt(num_frames)) | ||
output = Image.new('RGBA', (sqrt * W, math.ceil(num_frames / sqrt) * H)) | ||
|
||
i = 0 | ||
for state in self.states: | ||
for frame in state.frames: | ||
output.paste(frame, ((i % sqrt) * W, (i // sqrt) * H)) | ||
i += 1 | ||
|
||
# save | ||
pnginfo = PngInfo() | ||
pnginfo.add_text('Description', comment, zip=True) | ||
if palette: | ||
output = output.convert('P') | ||
output.save(filename, 'png', optimize=True, pnginfo=pnginfo) | ||
|
||
|
||
class State: | ||
def __init__(self, dmi, name, *, loop=LOOP_UNLIMITED, rewind=False, movement=False, dirs=1): | ||
self.dmi = dmi | ||
self.name = name | ||
self.loop = loop | ||
self.rewind = rewind | ||
self.movement = movement | ||
self.dirs = dirs | ||
|
||
self._nframes = None # used during loading only | ||
self.frames = [] | ||
self.delays = [] | ||
self.hotspots = None | ||
|
||
@property | ||
def framecount(self): | ||
if self._nframes is not None: | ||
return self._nframes | ||
else: | ||
return len(self.frames) // self.dirs | ||
|
||
def frame(self, image, *, delay=1): | ||
assert image.size == (self.dmi.width, self.dmi.height) | ||
self.delays.append(delay) | ||
self.frames.append(image) | ||
|
||
def hotspot(self, first_frame, x, y): | ||
if self.hotspots is None: | ||
self.hotspots = [None] * self.framecount | ||
for i in range(first_frame, self.framecount): | ||
self.hotspots[i] = x, y | ||
|
||
def _frame_index(self, frame=0, dir=None): | ||
ofs = DIR_ORDER.index(DIR_NAMES[dir]) | ||
if ofs >= self.dirs: | ||
ofs = 0 | ||
return frame * self.dirs + ofs | ||
|
||
def get_frame(self, *args, **kwargs): | ||
return self.frames[self._frame_index(*args, **kwargs)] | ||
|
||
|
||
def escape(text): | ||
text = text.replace('\\', '\\\\') | ||
text = text.replace('"', '\\"') | ||
return f'"{text}"' | ||
|
||
|
||
def unescape(text, quote='"'): | ||
if text == 'null': | ||
return None | ||
if not (text.startswith(quote) and text.endswith(quote)): | ||
raise ValueError(text) | ||
text = text[1:-1] | ||
text = text.replace('\\"', '"') | ||
text = text.replace('\\\\', '\\') | ||
return text | ||
|
||
|
||
def parse_num(value): | ||
if '.' in value: | ||
return float(value) | ||
return int(value) | ||
|
||
|
||
def parse_bool(value): | ||
if value not in ('0', '1'): | ||
raise ValueError(value) | ||
return value == '1' |
Oops, something went wrong.