From 82cdb793d8c54e80edbd13db9a75cbf7e98a3ecf Mon Sep 17 00:00:00 2001 From: Mauricio Date: Tue, 4 Aug 2020 00:53:53 -0400 Subject: [PATCH] fully implemented apng character support --- gameview.py | 102 +++++++++++++++++++++++++++++++++++++++------------- images.py | 39 ++++++++++++-------- 2 files changed, 102 insertions(+), 39 deletions(-) diff --git a/gameview.py b/gameview.py index 6b72940..20b4161 100644 --- a/gameview.py +++ b/gameview.py @@ -5,6 +5,8 @@ from os.path import exists from functools import partial from ConfigParser import ConfigParser +import images + AOpath = "base/" #AOpath = 'I:/aovanilla1.7.5/client/base/' @@ -120,7 +122,7 @@ class ChatLogs(QtGui.QTextEdit): self.type = logtype self.savelog = ini.read_ini_bool(AOpath+"AO2XP.ini", "General", "save logs") self.combinelog = ini.read_ini_bool(AOpath+"AO2XP.ini", "General", "combined logs") - if not os.path.exists("chatlogs"): + if not exists("chatlogs"): os.mkdir("chatlogs") if self.savelog: @@ -164,8 +166,10 @@ class ChatLogs(QtGui.QTextEdit): class AOCharMovie(QtGui.QLabel): done = QtCore.pyqtSignal() - use_pillow = False + use_pillow = 0 pillow_frames = [] + pillow_frame = 0 + pillow_speed = 0 def __init__(self, parent): QtGui.QLabel.__init__(self, parent) @@ -183,6 +187,7 @@ class AOCharMovie(QtGui.QLabel): self.pillow_timer.setSingleShot(True) self.preanim_timer.timeout.connect(self.timer_done) + self.pillow_timer.timeout.connect(self.pillow_frame_change) self.m_movie.frameChanged.connect(self.frame_change) def set_flipped(self, flip): @@ -199,7 +204,8 @@ class AOCharMovie(QtGui.QLabel): emote_prefix = "" p_emote = emote - self.pillow_frames = [] + self.pillow_frames = [] + self.pillow_frame = 0 original_path = AOpath+"characters/"+p_char+"/"+emote_prefix+p_emote+".gif" alt_path = AOpath+"characters/"+p_char+"/"+p_emote+".png" @@ -210,7 +216,7 @@ class AOCharMovie(QtGui.QLabel): if exists(apng_path): gif_path = apng_path - self.use_pillow = True + self.use_pillow = 1 else: if ini.read_ini_bool(AOpath+"AO2XP.ini", "General", "download characters"): url = "http://s3.wasabisys.com/webao/base/characters/"+p_char.lower()+"/"+emote_prefix+p_emote.lower()+".apng" @@ -221,7 +227,7 @@ class AOCharMovie(QtGui.QLabel): if exists(webp_path): gif_path = webp_path - self.use_pillow = True + self.use_pillow = 2 else: if ini.read_ini_bool(AOpath+"AO2XP.ini", "General", "download characters"): url = "http://s3.wasabisys.com/webao/base/characters/"+p_char.lower()+"/"+p_emote.lower()+".webp" @@ -232,7 +238,7 @@ class AOCharMovie(QtGui.QLabel): if exists(original_path): gif_path = original_path - self.use_pillow = False + self.use_pillow = 0 else: if ini.read_ini_bool(AOpath+"AO2XP.ini", "General", "download characters"): url = "http://s3.wasabisys.com/webao/base/characters/"+p_char.lower()+"/"+emote_prefix+p_emote.lower()+".gif" @@ -243,7 +249,7 @@ class AOCharMovie(QtGui.QLabel): if exists(alt_path): gif_path = alt_path - self.use_pillow = False + self.use_pillow = 0 else: if ini.read_ini_bool(AOpath+"AO2XP.ini", "General", "download characters"): url = "http://s3.wasabisys.com/webao/base/characters/"+p_char.lower()+"/"+emote_prefix+p_emote.lower()+".png" @@ -256,30 +262,50 @@ class AOCharMovie(QtGui.QLabel): gif_path = placeholder_path else: gif_path = "" - self.use_pillow = False + self.use_pillow = 0 + + if not self.use_pillow: + self.m_movie.stop() + self.m_movie.setFileName(gif_path) + self.m_movie.start() + + elif self.use_pillow == 1: # apng + self.pillow_frames = images.load_apng(apng_path) + #print apng_path, self.pillow_frames[0], int(self.pillow_frames[0][1] * self.pillow_speed) if len(self.pillow_frames[0]) > 1 else 0 + if len(self.pillow_frames) > 1: self.pillow_timer.start(int(self.pillow_frames[0][1] * self.pillow_speed)) + self.set_pillow_frame() + + elif self.use_pillow == 2: # webp + self.pillow_frames = images.load_webp(webp_path) + if len(self.pillow_frames) > 1: self.pillow_timer.start(int(self.pillow_frames[0][1] * self.pillow_speed)) + self.set_pillow_frame() - self.m_movie.stop() - self.m_movie.setFileName(gif_path) - self.show() - self.m_movie.start() def play_pre(self, p_char, p_emote, duration): gif_path = AOpath+"characters/"+p_char+"/"+p_emote+".gif" - - self.m_movie.stop() - self.clear() - self.m_movie.setFileName(gif_path) - self.m_movie.jumpToFrame(0) - + apng_path = AOpath+"characters/"+p_char+"/"+p_emote+".apng" + webp_path = AOpath+"characters/"+p_char+"/"+p_emote+".webp" + full_duration = duration * self.time_mod real_duration = 0 - + self.play_once = False - - for n_frame in range(self.m_movie.frameCount()): - real_duration += self.m_movie.nextFrameDelay() - self.m_movie.jumpToFrame(n_frame + 1) + self.m_movie.stop() + self.clear() + + if exists(apng_path): + real_duration = images.get_apng_duration(apng_path) + + elif exists(webp_path): + real_duration = images.get_webp_duration(webp_path) + + elif exists(gif_path): + self.m_movie.setFileName(gif_path) + self.m_movie.jumpToFrame(0) + for n_frame in range(self.m_movie.frameCount()): + real_duration += self.m_movie.nextFrameDelay() + self.m_movie.jumpToFrame(n_frame + 1) percentage_modifier = 100.0 @@ -290,6 +316,7 @@ class AOCharMovie(QtGui.QLabel): if percentage_modifier > 100.0 or percentage_modifier < 0.0: percentage_modifier = 100.0 + self.pillow_fullduration = full_duration if full_duration == 0 or full_duration >= real_duration: self.play_once = True else: @@ -298,6 +325,7 @@ class AOCharMovie(QtGui.QLabel): self.preanim_timer.start(full_duration) self.m_movie.setSpeed(int(percentage_modifier)) + self.pillow_speed = percentage_modifier / 100. self.play(p_char, p_emote, "") def play_talking(self, p_char, p_emote): @@ -310,6 +338,7 @@ class AOCharMovie(QtGui.QLabel): self.play_once = False self.m_movie.setSpeed(100) + self.pillow_speed = 1 self.play(p_char, p_emote, '(b)') def play_idle(self, p_char, p_emote): @@ -322,6 +351,7 @@ class AOCharMovie(QtGui.QLabel): self.play_once = False self.m_movie.setSpeed(100) + self.pillow_speed = 1 self.play(p_char, p_emote, '(a)') def stop(self): @@ -341,6 +371,28 @@ class AOCharMovie(QtGui.QLabel): if self.m_movie.frameCount() - 1 == n_frame and self.play_once: self.preanim_timer.start(self.m_movie.nextFrameDelay()) + @QtCore.pyqtSlot() + def pillow_frame_change(self): + if len(self.pillow_frames)-1 == self.pillow_frame: + if self.play_once: + self.preanim_timer.start(int(self.pillow_frames[self.pillow_frame][1] * self.pillow_speed)) + elif len(self.pillow_frames) > 1: + self.pillow_frame = 0 + self.pillow_timer.start(int(self.pillow_frames[self.pillow_frame][1] * self.pillow_speed)) + else: + self.pillow_frame += 1 + self.pillow_timer.start(int(self.pillow_frames[self.pillow_frame][1] * self.pillow_speed)) + + self.set_pillow_frame() + + def set_pillow_frame(self): + f_img = self.pillow_frames[self.pillow_frame][0].mirrored(self.m_flipped, False) + if f_img.size().width() != 256 or f_img.size().height() != 192: + f_img = f_img.scaled(256, 192, transformMode=QtCore.Qt.SmoothTransformation) + + f_pixmap = QtGui.QPixmap.fromImage(f_img) + self.setPixmap(f_pixmap) + @QtCore.pyqtSlot() def timer_done(self): self.done.emit() @@ -1655,7 +1707,9 @@ class gui(QtGui.QWidget): preanim_duration = ao2_duration anim_to_find = AOpath+"characters/"+f_char+"/"+f_preanim+".gif" - if not exists(anim_to_find) or preanim_duration < 0: + apng_to_find = AOpath+"characters/"+f_char+"/"+f_preanim+".apng" + webp_to_find = AOpath+"characters/"+f_char+"/"+f_preanim+".webp" + if (not exists(anim_to_find) and not exists(apng_to_find) and not exists(webp_to_find)) or preanim_duration < 0: if noninterrupting: self.anim_state = 4 else: diff --git a/images.py b/images.py index 9df53c9..aaab320 100644 --- a/images.py +++ b/images.py @@ -8,6 +8,8 @@ APNG_DISPOSE_OP_BACKGROUND = 1 APNG_DISPOSE_OP_PREVIOUS = 2 APNG_BLEND_OP_SOURCE = 0 APNG_BLEND_OP_OVER = 1 +disposes = ["APNG_DISPOSE_OP_NONE", "APNG_DISPOSE_OP_BACKGROUND", "APNG_DISPOSE_OP_PREVIOUS"] +blends = ["APNG_BLEND_OP_SOURCE", "APNG_BLEND_OP_OVER"] # layman's terms of above apng constants so i can easily wrap my head around it: # @@ -18,42 +20,49 @@ APNG_BLEND_OP_OVER = 1 # APNG_BLEND_OP_SOURCE: replace frame region # APNG_BLEND_OP_OVER: blend with frame -def load_apng(file): +def load_apng(file): # this one was hell to implement compared to the three funcs below img = APNG.open(file) frames = [] pilframes = [] dispose_op = 0 width, height = img.frames[0][0].width, img.frames[0][0].height + outputbuf = Image.new("RGBA", (width, height), (255,255,255,0)) + prev_frame = None for frame, frame_info in img.frames: i = img.frames.index((frame, frame_info)) pilframe = Image.open(io.BytesIO(frame.to_bytes())).convert("RGBA") - if dispose_op == APNG_DISPOSE_OP_BACKGROUND: - pilframe2 = Image.new("RGBA", (width, height), (255,255,255,0)) - pilframe2.paste(pilframe, (frame_info.x_offset, frame_info.y_offset), pilframe.convert("RGBA")) + #print str(i)+"\t"+disposes[dispose_op]+"\t\t"+blends[frame_info.blend_op] + + if dispose_op == APNG_DISPOSE_OP_BACKGROUND or (i == 0 and dispose_op == APNG_DISPOSE_OP_PREVIOUS): + prev_frame = outputbuf.copy() + emptyrect = Image.new("RGBA", (img.frames[i-1][0].width, img.frames[i-1][0].height), (255,255,255,0)) + outputbuf.paste(emptyrect, (img.frames[i-1][1].x_offset, img.frames[i-1][1].y_offset)) elif dispose_op == APNG_DISPOSE_OP_PREVIOUS: - pilframe2 = pilframes[-2].copy() - pilframe2.paste(pilframe, (frame_info.x_offset, frame_info.y_offset), pilframe.convert("RGBA")) + outputbuf = prev_frame.copy() else: - if i == 0: - pilframe2 = pilframe - else: - pilframe2 = pilframes[-1].copy() - pilframe2.paste(pilframe, (frame_info.x_offset, frame_info.y_offset), pilframe.convert("RGBA") if frame_info.blend_op == APNG_BLEND_OP_OVER else None) + prev_frame = outputbuf.copy() - pilframes.append(pilframe2) if frame_info: - frames.append([pilframe2.toqimage(), frame_info.delay*10]) # convert delay from centiseconds to milliseconds + outputbuf.paste(pilframe, (frame_info.x_offset, frame_info.y_offset), pilframe.convert("RGBA") if frame_info.blend_op == APNG_BLEND_OP_OVER else None) + else: + outputbuf.paste(pilframe, (0, 0)) + + final_frame = outputbuf.copy() + pilframes.append(final_frame) + if frame_info: + frames.append([final_frame.toqimage(), frame_info.delay*10]) # convert delay from centiseconds to milliseconds dispose_op = frame_info.depose_op else: - frames.append([pilframe2.toqimage(), 0]) - + frames.append([final_frame.toqimage(), 0]) + for frame in pilframes: frame.close() return frames + #return pilframes def load_webp(file): img = Image.open(file)