# -*- coding: utf-8 -*- from __future__ import unicode_literals, print_function try: import Image import ImageChops except ImportError: from PIL import Image from PIL import ImageChops import os import struct import base64 from . import atlas from . import process def isImagesIdentical(im1, im2): if im1.size[0] == 0 or im1.size[1] == 0: return False if im2.size[0] == 0 or im2.size[1] == 0: return False bbox = ImageChops.difference(im1, im2).getbbox() r = bbox is None return r def as_int(attr, df=0): if not attr: return df return int(attr) def as_float(attr, df=0): if not attr: return df return float(attr) def as_bool(attr, df=False): if not attr: return df lw = attr.lower() return lw == "true" or lw == "1" def fixImage_unused(image): if image.mode != "RGBA": return image data = image.load() for y in range(image.size[1]): for x in range(image.size[0]): a = data[x, y][3] # if a == 0 or a == 1: if a == 0: data[x, y] = (0, 0, 0, 0) return image def premultipliedAlpha(image): if image.mode != "RGBA": return image data = image.load() for y in range(image.size[1]): for x in range(image.size[0]): dt = data[x, y] a = dt[3] data[x, y] = ((dt[0] * a) / 255, (dt[1] * a) / 255, (dt[2] * a) / 255, a) return image def bbox(x, y, w, h): return (x, y, x + w, y + h) class alphaData(object): def __init__(self, w, h, data): self.w = w self.h = h self.data = data class frame(object): def __init__(self, image, bbox, image_element, rs, adata, extend): self.image = image self.image_element = image_element self.adata = adata self.extend = extend self.node = None self.resanim = rs self.border_top = self.border_left = 2 self.border_right = self.border_bottom = 1 self.identicalWith = None if not bbox: bbox = (0, 0, 1, 1) self.bbox = bbox class ResAnim(object): def __init__(self): self.frames = [] self.name = "" self.frame_size2 = (1, 1) # original size * scale_factor self.frame_scale2 = 1.0 self.columns = 0 self.rows = 0 self.walker = None def frame_cmp_sort(f1, f2): return f2.image.size[0] - f1.image.size[0] def applyScale(intVal, scale): return int(intVal * scale + 0.5) def applyScale2(x, scale): initialX = x best = None while 1: X = x * scale d = abs(X - int(X)) if not best: best = (d, X) if best[0] > d: best = (d, X) eps = 0.00000001 if d < eps: return int(X) if x > initialX * 2: return int(best[1]) x += 1 def nextPOT(v): v = v - 1 v = v | (v >> 1) v = v | (v >> 2) v = v | (v >> 4) v = v | (v >> 8) v = v | (v >> 16) return v + 1 class settings(object): def __init__(self): self.get_size = None self.set_node = None self.padding = 1 self.max_w = 2048 self.max_h = 2048 self.atlasses = [] self.square = False self.npot = False def makeAlpha(a): def roundUp(v, multiple): if multiple == 0: return v rem = v % multiple if rem == 0: return v res = v + multiple - rem return res try: asmall = a.resize( (int(a.size[0] / 4), int(a.size[1] / 4)), Image.ANTIALIAS) except ValueError: return None b = asmall.getextrema() if not b: return None if b[0] > 10: return None asmall_size = asmall.size BITS = 32 val = roundUp(asmall_size[0], BITS) lineLen = val // BITS buff = b'' for y in range(asmall_size[1]): line = [0 for x in range(lineLen)] for x in range(asmall_size[0]): p = asmall.getpixel((x, y)) if p > 5: n = x // BITS b = x % BITS line[n] |= 1 << b for v in line: buff += struct.pack(" mx: yield mx break else: mn = nextPOT(mn) mx = nextPOT(mx) while 1: yield mn mn *= 2 if mn > mx: break def pck(st, frames): if 0: st = settings() while frames: sq = 0 min_w = 64 min_h = 64 for f in frames: size = st.get_size(f) sq += size[0] * size[1] min_w = max(min_w, size[0]) min_h = max(min_h, size[1]) max_w = st.max_w max_h = st.max_h if st.square: min_w = min_h = max(min_w, min_h) max_w = max_h = max(max_w, max_h) sizes_w = list(get_pow2list(st.npot, min_w, max_w)) sizes_h = list(get_pow2list(st.npot, min_h, max_h)) for sw in sizes_w: for sh in sizes_h: end = sh == sizes_h[-1] and sw == sizes_w[-1] if st.square and sw != sh: continue if sw * sh < sq and not end: continue not_packed, bn = pack(st, frames, sw, sh) st.atlasses.append(bn) if not not_packed: return if end: frames = not_packed else: st.atlasses.pop() def processRS(context, walker): image_el = walker.root image_name = image_el.getAttribute("file") if not image_name: return None file_path = walker.getPath("file") # print image_path image = None # fn = self._getExistsFile(image_path) # virtual_width = 1 # virtual_height = 1 path = context.src_data + file_path try: image = Image.open(path) except IOError: pass if image: # virtual_width = int(image.size[0] * scale + 0.001) # virtual_height= int(image.size[1] * scale + 0.001) pass else: context.error("can't find image:\n%s\n" % (path, )) image = Image.new("RGBA", (0, 0)) resAnim = ResAnim() resAnim.walker = walker resAnim.image = image resAnim.name = image_name columns = as_int(image_el.getAttribute("cols")) frame_width = as_int(image_el.getAttribute("frame_width")) rows = as_int(image_el.getAttribute("rows")) frame_height = as_int(image_el.getAttribute("frame_height")) border = as_int(image_el.getAttribute("border")) trim = as_bool(image_el.getAttribute("trim"), True) extend = as_bool(image_el.getAttribute("extend"), True) if not extend: pass # sq = as_float(image_el.getAttribute("scale_quality"), 1) # next.scale_quality *= sq if not columns: columns = 1 if not rows: rows = 1 if frame_width: columns = image.size[0] / frame_width else: frame_width = image.size[0] / columns if frame_height: rows = image.size[1] / frame_height else: frame_height = image.size[1] / rows size_warning = False if frame_width * columns != image.size[0]: size_warning = True context.warning("image has width %d and %d columns:" % (image.size[0], columns)) if frame_height * rows != image.size[1]: size_warning = True context.warning(" max_scale: if not context.args.upscale: finalScale = max_scale else: upscale = True resAnim.frame_scale2 = 1.0 / finalScale finalScale = finalScale * scale_factor frame_size = (applyScale(frame_width, finalScale), applyScale(frame_height, finalScale)) resAnim.frame_size2 = (applyScale(frame_width, scale_factor), applyScale(frame_height, scale_factor)) resAnim.columns = columns resAnim.rows = rows for row in range(rows): for col in range(columns): rect = (int(col * frame_width), int(row * frame_height), int((col + 1) * frame_width), int((row + 1) * frame_height)) frame_image = image.crop(rect) def resize(): ax = applyScale2(frame_width, finalScale) ay = applyScale2(frame_height, finalScale) bx = int(ax / finalScale) by = int(ay / finalScale) im = Image.new("RGBA", (bx, by)) im.paste(frame_image, (0, 0, frame_image.size[ 0], frame_image.size[1])) frame_image = im.resize((ax, ay), Image.ANTIALIAS) frame_image = frame_image.crop( (0, 0, frame_size[0], frame_size[1])) resize_filter = Image.ANTIALIAS if upscale: resize_filter = Image.BICUBIC if context.args.resize: if as_bool(image_el.getAttribute("trueds")) or (not trim and not extend) or context.args.simple_downsample: frame_image = frame_image.resize( (frame_size[0], frame_size[1]), resize_filter) else: ax = applyScale2(frame_width, finalScale) ay = applyScale2(frame_height, finalScale) bx = int(ax / finalScale) by = int(ay / finalScale) im = Image.new("RGBA", (bx, by)) im.paste(frame_image, (0, 0, frame_image.size[ 0], frame_image.size[1])) frame_image = im.resize((ax, ay), resize_filter) frame_image = frame_image.crop( (0, 0, frame_size[0], frame_size[1])) adata = None if image.mode == "RGBA" and trim: r, g, b, a = frame_image.split() tt = context.args.trim_threshold if tt: a = a.point(lambda p: p - tt) if walker.hit_test: adata = makeAlpha(a) frame_bbox = a.getbbox() else: frame_bbox = (0, 0, frame_image.size[0], frame_image.size[1]) if not frame_bbox: frame_bbox = (0, 0, 0, 0) frame_image = frame_image.crop(frame_bbox) fr = frame(frame_image, frame_bbox, image_el, resAnim, adata, extend) if border: fr.border_left = fr.border_right = fr.border_top = fr.border_bottom = border if not extend: fr.border_left = fr.border_right = fr.border_top = fr.border_bottom = 0 for f in resAnim.frames: if isImagesIdentical(f.image, fr.image): fr.identicalWith = f if f.identicalWith: fr.identicalWith = f.identicalWith resAnim.frames.append(fr) return resAnim class atlas_Processor(process.Process): node_id = "atlas" def __init__(self): self.atlas_group_id = 0 def process(self, context, walker): self.atlas_group_id += 1 # meta = context.add_meta() anims = [] frames = [] alphaData = "" while True: next = walker.next() if 0: import xml_processor next = xml_processor.XmlWalker() if not next: break anim = processRS(context, next) if anim: anims.append(anim) frames.extend(anim.frames) # sort frames by size #frames = sorted(frames, key = lambda fr: -fr.image.size[1]) #frames = sorted(frames, key = lambda fr: -fr.image.size[0]) frames = sorted(frames, key=lambda fr: -1 * max( fr.image.size[0], fr.image.size[1]) * max(fr.image.size[0], fr.image.size[1])) sq = 0 for f in frames: sq += f.image.size[0] * f.image.size[1] compression = context.compression def get_aligned_frame_size(frame): def align_pixel(p): if not compression or compression == "no": return p + 1 align = 4 v = p % align if v == 0: p += align else: p += 8 - v return p sz = frame.image.size if frame.extend: return align_pixel(sz[0] + frame.border_left + frame.border_right), align_pixel(sz[1] + frame.border_top + frame.border_bottom) return sz def get_original_frame_size(frame): sz = frame.image.size return sz def set_node(frame, node): frame.node = node st = settings() st.npot = context._npot st.get_size = get_aligned_frame_size st.set_node = set_node st.max_w = context.args.max_width st.max_h = context.args.max_height st.square = context.compression == "pvrtc" st.padding = 0 if len(frames) == 1: st.get_size = get_original_frame_size pck(st, frames) # print "done" for atlas_id, atl in enumerate(st.atlasses): image = Image.new("RGBA", (atl.w, atl.h)) for node in atl.nodes: fr = node.data x = node.rect.x + fr.border_left y = node.rect.y + fr.border_top sz = fr.image.size rect = bbox(x, y, sz[0], sz[1]) if fr.extend: part = fr.image.crop(bbox(0, 0, sz[0], 1)) image.paste(part, bbox(x, y - 1, sz[0], 1)) part = fr.image.crop(bbox(0, sz[1] - 1, sz[0], 1)) image.paste(part, bbox(x, y + sz[1], sz[0], 1)) part = fr.image.crop(bbox(0, 0, 1, sz[1])) image.paste(part, bbox(x - 1, y, 1, sz[1])) part = fr.image.crop(bbox(sz[0] - 1, 0, 1, sz[1])) image.paste(part, bbox(x + sz[0], y, 1, sz[1])) image.paste(fr.image, rect) fr.atlas_id = atlas_id image_atlas_el = walker.root_meta.ownerDocument.createElement( "atlas") walker.root_meta.insertBefore( image_atlas_el, anims[0].walker.root_meta) # meta.appendChild(image_atlas_el) base_name = "%d_%d" % (self.atlas_group_id, atlas_id) ox_fmt = "r8g8b8a8" def compress(src, dest, fmt): cmd = context.helper.path_pvrtextool + \ " -i %s -f %s,UBN,lRGB -o %s" % (src, fmt, dest) cmd += " -l" # alpha bleed if context.args.quality == "best": cmd += " -q pvrtcbest" else: cmd += " -q pvrtcfast" if context.args.dither: cmd += " -dither" cmd += " -shh" # silent os.system(cmd) if compression == "etc1": # premultipliedAlpha(v) r, g, b, a = image.split() rgb = Image.merge("RGB", (r, g, b)) alpha = Image.merge("RGB", (a, a, a)) base_alpha_name = base_name + "_alpha" alpha_path = context.get_inner_dest(base_alpha_name + ".png") alpha.save(alpha_path) image_atlas_el.setAttribute("alpha", base_alpha_name + ".png") path_base = base_name + ".png" rs = ".pvr" rgb_path = context.get_inner_dest(path_base) path = context.get_inner_dest(path_base) rgb.save(path) pkm_rgb = base_name + rs pkm_alpha = base_alpha_name + rs def compress_etc1(src, dest): compress(src, dest, "etc1") #os.system(context.etc1tool + "%s -o %s -f etc1" %(src, dest)) ox_fmt = "ETC1" compress_etc1(rgb_path, context.get_inner_dest() + pkm_rgb) os.remove(rgb_path) image_atlas_el.setAttribute("file", pkm_rgb) compress_etc1(alpha_path, context.get_inner_dest() + pkm_alpha) os.remove(alpha_path) image_atlas_el.setAttribute("alpha", pkm_alpha) else: if context.args.nopng: path_base = base_name + ".tga" else: path_base = base_name + ".png" path = context.get_inner_dest(path_base) image_atlas_el.setAttribute("file", path_base) image.save(path) if context.compression == "pvrtc": ox_fmt = "PVRTC_4RGBA" compress(path, context.get_inner_dest( base_name + ".pvr"), "PVRTC1_4") image_atlas_el.setAttribute("file", base_name + ".pvr") os.remove(path) if context.compression == "pvrtc2": ox_fmt = "PVRTC2_4RGBA" compress(path, context.get_inner_dest( base_name + ".pvr"), "PVRTC2_4") image_atlas_el.setAttribute("file", base_name + ".pvr") os.remove(path) image_atlas_el.setAttribute("format", ox_fmt) image_atlas_el.setAttribute("w", str(image.size[0])) image_atlas_el.setAttribute("h", str(image.size[1])) alpha = b'' for anim in anims: if 0: anim = ResAnim() image_frames_el = anim.walker.root_meta image_frames_el.setAttribute("fs", "%d,%d,%d,%d,%f" % (anim.columns, anim.rows, anim.frame_size2[ 0], anim.frame_size2[1], anim.frame_scale2)) adata = anim.frames[0].adata if adata: image_frames_el.setAttribute("ht", "%d,%d,%d,%d" % ( len(alpha), len(adata.data), adata.w, adata.h)) if context.debug: image_frames_el.setAttribute("debug_image", anim.name) data = "" for item in anim.frames: fr = item if item.identicalWith: fr = item.identicalWith data += "%d,%d,%d,%d,%d,%d,%d;" % (fr.atlas_id, fr.node.rect.x + fr.border_left, fr.node.rect.y + fr.border_top, fr.bbox[0], fr.bbox[1], fr.bbox[2] - fr.bbox[0], fr.bbox[3] - fr.bbox[1]) if fr.adata: alpha += fr.adata.data text = image_frames_el.ownerDocument.createTextNode(data) image_frames_el.appendChild(text) if alpha: doc = walker.root_meta.ownerDocument alpha_el = doc.createElement("ht") adata_str = base64.b64encode(alpha) text = doc.createTextNode(adata_str.decode("utf-8")) alpha_el.setAttribute("len", str(len(adata_str))) alpha_el.appendChild(text) walker.root_meta.appendChild(alpha_el)