def addFlippedImgData(frameData, metaFrame, old_frame, new_frame): x_border = old_frame[2][0] newMetaFrame = [] for piece in metaFrame: newPiece = MetaFramePiece(piece.imgIndex, piece.attr0, piece.attr1, piece.attr2) newPiece.setHFlip(True) # move the piece in the reflected position. range = newPiece.GetBounds() endX = range[2] origEndX = x_border + endX flipStartX = -origEndX + old_frame[0].size[0] newX = flipStartX - x_border newPiece.setXOffset(newX) newMetaFrame.append(newPiece) # regardless of flip, the center point may be treated differently between frames. correct it. point_diff = exUtils.addLoc(new_frame[2], old_frame[2], True) # add it to all metaframe components for piece in newMetaFrame: new_offset = (piece.getXOffset() - point_diff[0], piece.getYOffset() - point_diff[1]) piece.setXOffset(new_offset[0]) piece.setYOffset(new_offset[1]) frameData.append(newMetaFrame)
def addImgData(imgData, frameData, palette_map, transparent, frame): img = frame[0] pt_zero = frame[2] # chop the frames into images and metaframes - need psy's algorithm for this piece_locs = chopImgToPieceLocs(img, transparent) metaFrame = [] # create new metaframe piece data from the pieces cur_tile = 0 for idx, piece_loc in enumerate(piece_locs): piece = piece_loc[0] loc = piece_loc[1] metaFramePiece = MetaFramePiece(len(imgData) + idx, 0, 0, 0) # set coordinates result_loc = exUtils.addLoc(loc, pt_zero, True) metaFramePiece.setXOffset(result_loc[0]) metaFramePiece.setYOffset(result_loc[1]) # set dimensions block_size = (piece.size[0] // TEX_SIZE, piece.size[1] // TEX_SIZE) res_type = DIM_TABLE.index(block_size) metaFramePiece.setResolutionType(res_type) # set RnS parameter - always true when not disabled; when reading in we are never disabled metaFramePiece.setRotAndScalingOn(True) # set tile index metaFramePiece.setTileNum(cur_tile) # priority is ALWAYS 3 metaFramePiece.setPriority(3) # set last if this is the last if idx == len(piece_locs) - 1: metaFramePiece.setIsLast(True) metaFrame.append(metaFramePiece) # increase tile index blocks_occupied = max(1, block_size[0] * block_size[1] // 4) cur_tile += blocks_occupied frameData.append(metaFrame) # add each piece for piece, _ in piece_locs: imgStrip = convertPieceToImgStrip(piece, palette_map) imgData.append(imgStrip)
def ImportSheets(inDir, strict=False): if DEBUG_PRINT: if not os.path.isdir(os.path.join(inDir, '_pieces_in')): os.makedirs(os.path.join(inDir, '_pieces_in')) if not os.path.isdir(os.path.join(inDir, '_frames_in')): os.makedirs(os.path.join(inDir, '_frames_in')) anim_stats = {} anim_names = {} tree = ET.parse(os.path.join(inDir, 'AnimData.xml')) root = tree.getroot() sdwSize = int(root.find('ShadowSize').text) if sdwSize < 0 or sdwSize > 2: raise ValueError("Invalid shadow size: {0}".format(sdwSize)) anims_node = root.find('Anims') for anim_node in anims_node.iter('Anim'): name = anim_node.find('Name').text index = -1 index_node = anim_node.find('Index') if index_node is not None: index = int(index_node.text) backref_node = anim_node.find('CopyOf') if backref_node is not None: backref = backref_node.text anim_stat = AnimStat(index, name, None, backref) else: frame_width = anim_node.find('FrameWidth') frame_height = anim_node.find('FrameHeight') anim_stat = AnimStat( index, name, (int(frame_width.text), int(frame_height.text)), None) rush_frame = anim_node.find('RushFrame') if rush_frame is not None: anim_stat.rushFrame = int(rush_frame.text) hit_frame = anim_node.find('HitFrame') if hit_frame is not None: anim_stat.hitFrame = int(hit_frame.text) return_frame = anim_node.find('ReturnFrame') if return_frame is not None: anim_stat.returnFrame = int(return_frame.text) durations_node = anim_node.find('Durations') for dur_node in durations_node.iter('Duration'): duration = int(dur_node.text) anim_stat.durations.append(duration) anim_names[name.lower()] = index if index == -1 and strict: raise ValueError( "{0} has its own sheet and does not have an index!".format( name)) if index > -1: if index in anim_stats: raise ValueError( "{0} and {1} both have the an index of {2}!".format( anim_stats[index].name, name, index)) anim_stats[index] = anim_stat copy_indices = {} for idx in anim_stats: stat = anim_stats[idx] if stat.backref is not None: back_idx = anim_names[stat.backref.lower()] if back_idx not in copy_indices: copy_indices[back_idx] = [] copy_indices[back_idx].append(idx) # read all sheets extra_sheets = [] anim_sheets = {} for filepath in glob.glob(os.path.join(inDir, '*-Anim.png')): _, file = os.path.split(filepath) anim_parts = file.split('-') anim_name = anim_parts[0] if anim_name.lower() not in anim_names: extra_sheets.append(anim_name) else: index = anim_names[anim_name.lower()] del anim_names[anim_name.lower()] anim_img = Image.open(os.path.join(inDir, anim_name + '-Anim.png')).convert("RGBA") offset_img = Image.open( os.path.join(inDir, anim_name + '-Offsets.png')).convert("RGBA") shadow_img = Image.open( os.path.join(inDir, anim_name + '-Shadow.png')).convert("RGBA") anim_sheets[index] = (anim_img, offset_img, shadow_img, anim_name) # raise warning if there exist anim stats without anims, or anims without anim stats if len(anim_names) > 0: orphans = [] for k in anim_names: orphans.append(k) raise ValueError("Xml found with no sheet: {0}".format( ', '.join(orphans))) if len(extra_sheets) > 0: raise ValueError("Sheet found with no xml: {0}".format( ', '.join(extra_sheets))) animGroupData = [] frames = [] frameToSequence = [] for idx in range(MAX_ANIMS): if idx in anim_sheets: anim_img, offset_img, shadow_img, anim_name = anim_sheets[idx] tileSize = anim_stats[idx].size durations = anim_stats[idx].durations # check against inconsistent sizing if anim_img.size != offset_img.size or anim_img.size != shadow_img.size: raise ValueError( "Anim, Offset, and Shadow sheets for {0} must be the same size!" .format(anim_name)) if anim_img.size[0] % tileSize[0] != 0 or anim_img.size[ 1] % tileSize[1] != 0: raise ValueError( "Sheet for {4} is {0}x{1} pixels and is not divisible by {2}x{3} in xml!" .format(anim_img.size[0], anim_img.size[1], tileSize[0], tileSize[1], anim_name)) total_frames = anim_img.size[0] // tileSize[0] # check against inconsistent duration counts if total_frames != len(durations): raise ValueError( "Number of frames in {0} does not match count of durations ({1}) specified in xml!" .format(anim_name, len(durations))) if anim_stats[idx].rushFrame >= len(durations): raise ValueError( "RushFrame of {0} is greater than the number of frames ({1}) in {2}!" .format(anim_stats[idx].rushFrame, len(durations), anim_name)) if anim_stats[idx].hitFrame >= len(durations): raise ValueError( "HitFrame of {0} is greater than the number of frames ({1}) in {2}!" .format(anim_stats[idx].hitFrame, len(durations), anim_name)) if anim_stats[idx].returnFrame >= len(durations): raise ValueError( "ReturnFrame of {0} is greater than the number of frames ({1}) in {2}!" .format(anim_stats[idx].returnFrame, len(durations), anim_name)) group = [] total_dirs = anim_img.size[1] // tileSize[1] for dir in range(8): if dir >= total_dirs: break sequence = [] for jj in range(anim_img.size[0] // tileSize[0]): rel_center = (tileSize[0] // 2 - DRAW_CENTER_X, tileSize[1] // 2 - DRAW_CENTER_Y) tile_rect = (jj * tileSize[0], dir * tileSize[1], tileSize[0], tileSize[1]) tile_bounds = (tile_rect[0], tile_rect[1], tile_rect[0] + tile_rect[2], tile_rect[1] + tile_rect[3]) bounds = exUtils.getCoveredBounds(anim_img, tile_bounds) emptyBounds = False if bounds[0] >= bounds[2]: bounds = (rel_center[0], rel_center[1], rel_center[0] + 1, rel_center[1] + 1) emptyBounds = True rect = (bounds[0], bounds[1], bounds[2] - bounds[0], bounds[3] - bounds[1]) abs_bounds = exUtils.addToBounds( bounds, (tile_rect[0], tile_rect[1])) frame_tex = anim_img.crop(abs_bounds) shadow_offset = exUtils.getOffsetFromRGB( shadow_img, tile_bounds, False, False, False, False, True) frame_offset = exUtils.getOffsetFromRGB( offset_img, tile_bounds, True, True, True, True, False) offsets = FrameOffset(None, None, None, None) if frame_offset[2] is None: # raise warning if there's missing shadow or offsets if strict: raise ValueError( "No frame offset found in frame {0} for {1}". format((jj, dir), anim_name)) offsets = FrameOffset(rel_center, rel_center, rel_center, rel_center) else: offsets.center = frame_offset[2] if frame_offset[0] is None: offsets.head = frame_offset[2] else: offsets.head = frame_offset[0] offsets.lhand = frame_offset[1] offsets.rhand = frame_offset[3] offsets.AddLoc((-rect[0], -rect[1])) shadow = rel_center if shadow_offset[4] is not None: shadow = shadow_offset[4] elif strict: raise ValueError( "No shadow offset found in frame {0} for {1}". format((jj, dir), anim_name)) shadow_diff = exUtils.addLoc(shadow, rect, True) shadow = exUtils.addLoc(shadow, rel_center, True) if emptyBounds and shadow_offset[ 4] is None and frame_offset[2] is None: continue frames.append((frame_tex, offsets, shadow_diff)) frame = SequenceFrame(-1, durations[jj], 0, shadow, shadow) if anim_stats[idx].rushFrame == jj: frame.SetRushPoint(True) if anim_stats[idx].hitFrame == jj: frame.SetHitPoint(True) if anim_stats[idx].returnFrame == jj: frame.SetReturnPoint(True) sequence.append(frame) frameToSequence.append((idx, dir, jj)) group.append(sequence) animGroupData.append(group) else: animGroupData.append([]) # get all unique frames and map them to the animations # same with shadows and offsets frame_map = [None] * len(frames) final_frames = [] mapDuplicateImportImgs(frames, final_frames, frame_map) # center the frame based on shadow placements reverse_frame_map = {} for idx, mapping in enumerate(frame_map): dest = mapping[0] if dest not in reverse_frame_map: reverse_frame_map[dest] = [] reverse_frame_map[dest].append(idx) for key in reverse_frame_map: shadow_diffs = [] for start in reverse_frame_map[key]: new_diff = frames[start][2] shadow_diffs.append(new_diff) # choose the mode? median? as the true offset freq = {} for diff in shadow_diffs: if diff not in freq: freq[diff] = 0 freq[diff] += 1 chosen_diff = shadow_diffs[0] for diff in freq: if freq[diff] > freq[chosen_diff]: chosen_diff = diff # now that we have our chosen diff # set the frame in final_frames to the chosen diff final_frames[key] = (final_frames[key][0], final_frames[key][1], chosen_diff, final_frames[key][3]) # and then set the diff mapping to their shadow diff - chosen diff for start in reverse_frame_map[key]: frame_map[start] = (frame_map[start][0], exUtils.addLoc(chosen_diff, frames[start][2], True)) # now, the frame will treat chosenDiff as its center # and all diffs will be applied to the offsets of the currently created animGroups # final_frames is now a list of unique graphics where flips are treated as separate but refer to the originals # now, create metaframes and image data # palette is needed first. get palette data # use tilequant to modify all anim images at once and force to 16 colors or less (including transparent) # first, generate an image containing all frames in final_frames max_width = 0 max_height = 0 for frame in final_frames: frame_tex = frame[0] max_width = max(max_width, frame_tex.size[0]) max_height = max(max_height, frame_tex.size[1]) max_width = exUtils.roundUpToMult(max_width, 2) max_height = exUtils.roundUpToMult(max_height, 2) max_tiles = int(math.ceil(math.sqrt(len(final_frames)))) combinedImg = Image.new('RGBA', (max_tiles * max_width, max_tiles * max_height), (0, 0, 0, 0)) crop_bounds = [] for idx, frame in enumerate(final_frames): frame_tex = frame[0] round_width = exUtils.roundUpToMult(frame_tex.size[0], 2) round_height = exUtils.roundUpToMult(frame_tex.size[1], 2) tile_pos = (idx % max_tiles * max_width, idx // max_tiles * max_height) paste_bounds = (tile_pos[0] + (max_width - round_width) // 2, tile_pos[1] + (max_height - round_height) // 2, tile_pos[0] + (max_width - round_width) // 2 + frame_tex.size[0], tile_pos[1] + (max_height - round_height) // 2 + frame_tex.size[1]) crop_bounds.append(paste_bounds) combinedImg.paste(frame_tex, paste_bounds, frame_tex) colors = combinedImg.getcolors() if strict and len(colors) > 16: raise ValueError( "Number of (nontransparent) colors over 15: {0}".format( len(colors))) transparent = (0, 127, 151, 255) foundTrans = True while foundTrans: foundTrans = False for count, color in colors: if color == transparent: transparent = (0, 127, transparent[3] - 1, 255) foundTrans = True break datas = combinedImg.getdata() return_datas = [] for idx in range(len(datas)): if datas[idx][3] == 0: return_datas.append(transparent) else: return_datas.append(datas[idx]) combinedImg.putdata(return_datas) # TODO: wan can actually handle more than 16 colors so long as a single image piece itself only has 16 colors # to actually allow over 16 colors, an algorithm would be needed to get the color list for every individual frame # and then combine the color lists such that there are as few distinct palettes as possible # and that no palettes have over 16 colors (transparency included) # then, run through simple_quant reducedImg = simple_quant(combinedImg).convert("RGBA") datas = reducedImg.getdata() for idx in range(len(datas)): if return_datas[idx] != transparent: return_datas[idx] = datas[idx] reducedImg.putdata(return_datas) palette = reducedImg.getcolors() palette_map = {} singlePalette = [] for count, rgba in palette: palette_map[rgba] = len(singlePalette) singlePalette.append(rgba) # transparent is always 0 if rgba == transparent: prev_zero = singlePalette[0] singlePalette[0] = rgba singlePalette[-1] = prev_zero palette_map[prev_zero] = palette_map[rgba] palette_map[rgba] = 0 # then, cut up the image and return to the original final_frames for idx in range(len(final_frames)): frame_tex, offsets, shadow_diff, flip = final_frames[idx] frame_tex = reducedImg.crop(crop_bounds[idx]) final_frames[idx] = (frame_tex, offsets, shadow_diff, flip) while len(singlePalette) < 16: singlePalette.append((32, 169, 32, 255)) singlePalette.append((65, 117, 100, 255)) singlePalette.append((105, 110, 111, 255)) singlePalette.append((32, 50, 48, 255)) singlePalette.append((50, 49, 32, 255)) while len(singlePalette) > 16: singlePalette.pop() # create imgData, metaframe data, and offset data imgData = [] frameData = [] offsetData = [] for idx, frame in enumerate(final_frames): if DEBUG_PRINT: frame[0].save( os.path.join(inDir, '_frames_in', 'F-' + format(idx, '02d') + '.png')) shadow_diff = frame[2] flip = frame[3] if flip > -1: flipped_frame = frameData[flip] addFlippedImgData(frameData, flipped_frame, final_frames[flip], frame) else: # will append to imgData and frameData addImgData(imgData, frameData, palette_map, transparent, frame) offsets = frame[1] offsets.AddLoc((-shadow_diff[0], -shadow_diff[1])) offsetData.append(offsets) # apply the mappings to the animations, correcting the frame indices and shadow offsets for idx, frame in enumerate(frames): frame_seq_mapping = frameToSequence[idx] group = animGroupData[frame_seq_mapping[0]] sequence = group[frame_seq_mapping[1]] animFrame = sequence[frame_seq_mapping[2]] mapFrame = frame_map[idx] animFrame.frameIndex = mapFrame[0] shadow_diff = mapFrame[1] animFrame.offset = exUtils.addLoc(animFrame.offset, shadow_diff) for idx in copy_indices: copies = copy_indices[idx] for copy_idx in copies: animGroupData[copy_idx] = exWanUtils.duplicateAnimGroup( animGroupData[idx]) wan = WanFile() wan.imgData = imgData wan.frameData = frameData wan.animGroupData = animGroupData wan.offsetData = offsetData wan.customPalette = [singlePalette] wan.sdwSize = sdwSize # return the wan file return wan
def AddLoc(self, loc): self.head = exUtils.addLoc(self.head, loc) self.lhand = exUtils.addLoc(self.lhand, loc) self.rhand = exUtils.addLoc(self.rhand, loc) self.center = exUtils.addLoc(self.center, loc)