diff --git a/NTFT.py b/NTFT.py index 0820790..153d5fa 100644 --- a/NTFT.py +++ b/NTFT.py @@ -1,305 +1,212 @@ -#NTFT.py by pbsds -#AGPL3 licensed -# -#PIL is required to read and write images to disk -# -#Credits: -# -# -The guys behind TiledGGD. This sped up my work a lit. -# -Jsafive for supplying .ugo files -# -import sys, os, numpy as np +# NTFT.py by pbsds +# Hatenatools3 Python 3 port by RekuNote aka rekushiiiiii +# AGPL3 licensed +# Python 3 port + +from __future__ import annotations + +import os +import sys +import numpy as np + try: - import Image - hasPIL = True + from PIL import Image + hasPIL = True except ImportError: - hasPIL = False + Image = None + hasPIL = False -#helpers -def AscDec(ascii, LittleEndian=False):#Converts a ascii string into a decimal - ret = 0 - l = map(ord, ascii) - if LittleEndian: l.reverse() - for i in l: - ret = (ret<<8) | i - return ret -def DecAsc(dec, length=None, LittleEndian=False):#Converts a decimal into an ascii string of chosen length - out = [] - while dec <> 0: - out.insert(0, dec&0xFF) - dec >>= 8 - #"".join(map(chr, out)) - - if length: - if len(out) > length: - #return "".join(map(chr, out[-length:])) - out = out[-length:] - if len(out) < length: - #return "".join(map(chr, [0]*(length-len(out)) + out)) - out = [0]*(length-len(out)) + out - - if LittleEndian: out.reverse() - return "".join(map(chr, out)) -def clamp(value, min, max): - if value > max: return max - if value < min: return min - return value +def AscDec(data, LittleEndian=False): + if isinstance(data, str): + data = data.encode("latin-1") + return int.from_bytes(bytes(data), "little" if LittleEndian else "big") + + +def DecAsc(dec, length=None, LittleEndian=False): + if dec < 0: + raise ValueError("DecAsc does not support negative values") + if length is None: + length = max(1, (dec.bit_length() + 7) // 8) if dec else 0 + if length == 0: + return b"" + dec &= (1 << (length * 8)) - 1 + return dec.to_bytes(length, "little" if LittleEndian else "big") + + +def clamp(value, minimum, maximum): + if value > maximum: + return maximum + if value < minimum: + return minimum + return value + -#Class NTFT: -# -# The NTFT image format stores RGB values as 5 bits each: a value between 0 and 31. -# It has 1 bit of transparancy, which means its either vidssible or invisible. No gradients. -# -# How to use: -# -# Converting to NTFT file: -# -# image = ReadImage(input_path) -# NTFT().SetImage(image).WriteFile(output_path) -# -# Reading NTFT file: -# -# ntft = NTFT().ReadFile(input_path, (width, height)) -# WriteImage(ntft.Image, output_path) -# class NTFT: - def __init__(self): - self.Loaded = False - def ReadFile(self, path, size): - f = open(path, "rb") - ret = self.Read(f.read(), size) - f.close() - return ret - def Read(self, data, (w, h)): - #the actual stored data is a image with the sizes padded to the nearest power of 2. The image is then clipped out from it. - psize = [] - for i in (w, h): - p = 1 - while 1<

len(data): - print "Invalid sizes" - return False - - #JUST DO IT! - #self.Image = [[None for _ in xrange(h)] for _ in xrange(w)] - self.Image = np.zeros((w, h), dtype=">u4") - for y in xrange(h): - for x in xrange(w): - pos = (x + y*pw)*2 - byte = AscDec(data[pos:pos+2], True) - - #ARGB1555 -> RGBA8: - a = (byte >> 15 ) * 0xFF - b = (byte >> 10 & 0x1F) * 0xFF / 0x1F - g = (byte >> 5 & 0x1F) * 0xFF / 0x1F - r = (byte & 0x1F) * 0xFF / 0x1F - - #self.Image[x][y] = (r<<24) | (g<<16) | (b<<8) | a#RGBA8 - self.Image[x, y] = (r<<24) | (g<<16) | (b<<8) | a#RGBA8 - - self.Loaded = True - return self - def WriteFile(self, path): - if self.Loaded: - f = open(path, "wb") - f.write(self.Pack()) - f.close() - return True - else: - return False - def Pack(self): - if not self.Loaded: - return False - - #h = len(self.Image[0]) - #w = len(self.Image) - w, h = self.Image.shape - - #the actual stored data is a image with the sizes padded to the nearest power of 2 - psize = [] - for i in (w, h): - p = 1 - while 1<

> 24 - g = (c >> 16) & 0xFF - b = (c >> 8 ) & 0xFF - a = c & 0xFF - - #convert - a = 1 if a >= 0x80 else 0 - r = r * 0x1F / 0xFF - g = g * 0x1F / 0xFF - b = b * 0x1F / 0xFF - - #store - out.append(DecAsc((a<<15) | (b<<10) | (g<<5) | r, 2, True)) - - return "".join(out) - def SetImage(self, Image): - self.Image = Image - self.Loaded = True - return self + """Reader/writer for Nintendo's raw ARGB1555 NTFT image data.""" + + def __init__(self): + self.Loaded = False + self.Image = None + + def ReadFile(self, path, size): + with open(path, "rb") as f: + return self.Read(f.read(), size) + + def Read(self, data, size): + w, h = size + data = bytes(data) + + psize = [] + for dimension in (w, h): + p = 1 + while 1 << p < dimension: + p += 1 + psize.append(1 << p) + pw, ph = psize + + if pw * ph * 2 != len(data): + print("Invalid sizes") + return False + + self.Image = np.zeros((w, h), dtype=">u4") + for y in range(h): + for x in range(w): + pos = (x + y * pw) * 2 + value = AscDec(data[pos : pos + 2], True) + + a = ((value >> 15) & 0x1) * 0xFF + b = (((value >> 10) & 0x1F) * 0xFF + 15) // 0x1F + g = (((value >> 5) & 0x1F) * 0xFF + 15) // 0x1F + r = ((value & 0x1F) * 0xFF + 15) // 0x1F + self.Image[x, y] = (r << 24) | (g << 16) | (b << 8) | a + + self.Loaded = True + return self + + def WriteFile(self, path): + if not self.Loaded: + return False + with open(path, "wb") as f: + f.write(self.Pack()) + return True + + def Pack(self): + if not self.Loaded or self.Image is None: + return False + + w, h = self.Image.shape + psize = [] + for dimension in (w, h): + p = 1 + while 1 << p < dimension: + p += 1 + psize.append(1 << p) + + out = bytearray() + for y in range(psize[1]): + for x in range(psize[0]): + c = int(self.Image[clamp(x, 0, w - 1), clamp(y, 0, h - 1)]) + r = c >> 24 + g = (c >> 16) & 0xFF + b = (c >> 8) & 0xFF + a = c & 0xFF + + a = 1 if a >= 0x80 else 0 + r = (r * 0x1F + 127) // 0xFF + g = (g * 0x1F + 127) // 0xFF + b = (b * 0x1F + 127) // 0xFF + value = (a << 15) | (b << 10) | (g << 5) | r + out.extend(DecAsc(value, 2, True)) + return bytes(out) + + def SetImage(self, image): + self.Image = image + self.Loaded = True + return self + -#Function WriteImage: -# -# Writes a 2D array of uint32 RGBA values as a image file. -# Designed to work with NTFT.Image -# -# This function requires the PIl imaging module def WriteImage(image, outputPath): - if not hasPIL: - print "Error: PIL not found!" - return False - #if not image: return False - - out = image.tostring("F") - - # out = [] - # for y in xrange(len(image[0])): - # for x in xrange(len(image)): - # out.append(DecAsc(image[x][y], 4)) - - out = Image.fromstring("RGBA", (len(image), len(image[0])), out) - - filetype = outputPath[outputPath.rfind(".")+1:] - out.save(outputPath, filetype) - - return True + if not hasPIL: + print("Error: PIL not found!") + return False -#Function ReadImage: -# -# Returns a 2D list of uint32 RGBA values of the image file. -# This can be passed into NTFT().SetImage() -# -# This function requires the PIl imaging module -def ReadImage(path):#TODO: make it support numpy - if not hasPIL: return False - - image = Image.open(path) - pixeldata = image.getdata() - w, h = image.size - - if len(pixeldata[0]) < 4: - def Combine((r, g, b)): - return (r << 24) | (g << 16) | (b << 8) | 0xFF - else: - def Combine((r, g, b, a)): - return (r << 24) | (g << 16) | (b << 8) | a - - #ret = [] - ret = np.zeros((w, h), dtype=">u4") - for x in xrange(w): - #line = [] - for y in xrange(h): - ret[x, y] = Combine(pixeldata[y*w + x])#maybe make a more numpy efficient way? - #line.append(Combine(pixeldata[y*w + x])) - #ret.append(line) - - return ret + out = image.tobytes(order="F") + pil_image = Image.frombytes("RGBA", (len(image), len(image[0])), out) + filetype = outputPath[outputPath.rfind(".") + 1 :] + pil_image.save(outputPath, filetype) + return True +def ReadImage(path): + if not hasPIL: + return False -#testing: -# i = NTFT().ReadFile("NTFTtests/kaeru.ntft", (36, 30)) -# WriteImage(i.Image, "NTFTtests/kaeru.png") + image = Image.open(path).convert("RGBA") + pixeldata = np.asarray(image, dtype=np.uint8) + w, h = image.size + ret = np.zeros((w, h), dtype=">u4") + for x in range(w): + for y in range(h): + r, g, b, a = (int(value) for value in pixeldata[y, x]) + ret[x, y] = (r << 24) | (g << 16) | (b << 8) | a + return ret -# i = NTFT().ReadFile("NTFTtests/News.ntft", (32, 32)) -# WriteImage(i.Image, "NTFTtests/News.png") -# i = NTFT().ReadFile("NTFTtests/Special Room.ntft", (32, 32)) -# WriteImage(i.Image, "NTFTtests/Special Room.png") +def _main(): + print(" == NTFT.py ==") + print(" == by pbsds ==") + print(" == v0.95 ==") + print() + if not hasPIL: + print("PIL not found! Exiting...") + return 1 + + if len(sys.argv) < 2: + print("Usage:") + print(" NTFT.py [ [ ]]") + print() + print("Can convert an NTFT to PNG or the other way around.") + print("32x32 is the normal resolution for button icons in UGO files.") + return 0 + + input_path = sys.argv[1] + encode = not (input_path[-4:].lower() == "ntft" or len(sys.argv) >= 5) + print("Mode: image -> NTFT" if encode else "Mode: NTFT -> image") + + width = height = None + if len(sys.argv) >= 3: + output = sys.argv[2] + if len(sys.argv) >= 5: + if not sys.argv[3].isdigit() or not sys.argv[4].isdigit(): + print("Invalid size input!") + return 1 + width, height = int(sys.argv[3]), int(sys.argv[4]) + if not (width and height) and not encode: + print("Image size not provided!") + return 1 + else: + output = ".".join(input_path.split(".")[:-1]) + (".ntft" if encode else ".png") + + print("Converting...") + try: + if encode: + image = ReadImage(input_path) + if image is False: + return 1 + NTFT().SetImage(image).WriteFile(output) + else: + ntft = NTFT().ReadFile(input_path, (width, height)) + if not ntft: + return 1 + WriteImage(ntft.Image, output) + except OSError as err: + print(err) + return 1 + + print("Done!") + return 0 -#i = NTFT() -#i.Loaded = True -#i.Image = ReadImage("NTFTtests/geh.png") -#i.WriteFile("NTFTtests/geh.ntft") if __name__ == "__main__": - print " == NTFT.py ==" - print " == by pbsds ==" - print " == v0.95 ==" - print - - if not hasPIL: - print "PIL not found! Exiting..." - sys.exit() - - if len(sys.argv) < 2: - print "Usage:" - print " NTFT.py [ [ ]]" - print "" - print "Can convert a NTFT to PNG or the other way around." - print "if isn't specified it will be set to with an another extension" - print "" - print "The NTFT file contain only the colordata, so it's up to the user to find or" - print "store the resolution of the image. and is required" - print "to convert a NTFT file to a image." - print "32x32 is the normal resolution for button icons in UGO files." - sys.exit() - - input = sys.argv[1] - - if input[-4:].lower() == "ntft" or len(sys.argv) >= 5: - print "Mode: NTFT -> image" - Encode = False - else: - print "Mode: image -> NTFT" - Encode = True#if false it'll decode - - if len(sys.argv) >= 3: - output = sys.argv[2] - - width, height = None, None - if len(sys.argv) >= 5: - if (not sys.argv[3].isdigit()) or (not sys.argv[4].isdigit()): - print "Invalid size input!" - sys.exit() - width = int(sys.argv[3]) - height = int(sys.argv[4]) - - if not (width and height) and not Encode: - print "Image size not provided!" - sys.exit() - - else: - output = ".".join(input.split(".")[:-1]) + (".ntft" if Encode else ".png") - - print "Converting..." - if Encode: - try: - image = ReadImage(input) - except IOError as err: - print err - sys.exit() - - i = NTFT() - i.Loaded = True - i.Image = image - i.WriteFile(output) - else: - try: - ntft = NTFT().ReadFile(input, (width, height)) - except IOError as err: - print err - sys.exit() - - if not ntft:#eeror message already printed - sys.exit() - - WriteImage(ntft.Image, output) - print "Done!" \ No newline at end of file + raise SystemExit(_main()) diff --git a/PPM.py b/PPM.py index 8f887b2..101b97d 100755 --- a/PPM.py +++ b/PPM.py @@ -1,5 +1,5 @@ -#!/usr/bin/env python -#PPM.py by pbsds for python 2.7 +#!/usr/bin/env python3 +#PPM.py by pbsds; Hatenatools3 Python 3 port by RekuNote aka rekushiiiiii #AGPL3 licensed # #Numpy is required @@ -12,7 +12,7 @@ # - Jsafive for supplying .tmb files # - Austin Burk, Midmad on hatena haiku and WDLmaster on hcs64.com for determining the sound codec # -import sys, wave, audioop, re, os #needs os and time aswell in CMD mode (UPDATE 2018/07/04: os used to get devnull for discarding subprocess output) +import sys, wave, re, os, struct # Python 3: audioop was removed in 3.13 import numpy as np import subprocess # used for ffmpeg import tempfile, shutil # temporary directory whilst exporting, shutil to clean up after @@ -21,7 +21,7 @@ try: from PIL import Image hasPIL = True except ImportError: - print "Warning: PIL not found, image extraction won't work!" + print("Warning: PIL not found, image extraction won't work!") hasPIL = False # Test for ffmpeg by executing ffmpeg -h; an OSError indicates ffmpeg is not installed @@ -32,39 +32,65 @@ try: stderr=null) hasffmpeg = True except OSError: - print "Warning: ffmpeg not found, video export unavailable. Please make sure ffmepg is installed and can be accessed on your path." + print("Warning: ffmpeg not found, video export unavailable. Please make sure ffmepg is installed and can be accessed on your path.") hasffmpeg = False #helpers: -def AscDec(ascii, LittleEndian=False):#Converts a ascii string into a decimal - ret = 0 - l = map(ord, ascii) - if LittleEndian: l.reverse() - for i in l: - ret = (ret<<8) | i - return ret -def DecAsc(dec, length=None, LittleEndian=False):#Converts a decimal into an ascii string of chosen length - out = [] - while dec <> 0: - out.insert(0, dec&0xFF) - dec >>= 8 - #"".join(map(chr, out)) - - if length: - if len(out) > length: - #return "".join(map(chr, out[-length:])) - out = out[-length:] - if len(out) < length: - #return "".join(map(chr, [0]*(length-len(out)) + out)) - out = [0]*(length-len(out)) + out - - if LittleEndian: out.reverse() - return "".join(map(chr, out)) +def AscDec(data, LittleEndian=False):#Converts a byte string into a decimal + if isinstance(data, str): + data = data.encode("latin-1") + return int.from_bytes(bytes(data), "little" if LittleEndian else "big") + +def DecAsc(dec, length=None, LittleEndian=False):#Converts a decimal into bytes of chosen length + if dec < 0: + raise ValueError("DecAsc does not support negative values") + if length is None: + length = max(1, (dec.bit_length() + 7) // 8) if dec else 0 + if length == 0: + return b"" + dec &= (1 << (length * 8)) - 1 + return dec.to_bytes(length, "little" if LittleEndian else "big") + def AddPadding(i,pad = 0x10):#used mainly for zipaligning offsets - if i % pad <> 0: + if i % pad != 0: return i + pad - (i % pad) return i +# Python 3.13 removed audioop. This is the IMA/DVI ADPCM decoder used by +# audioop.adpcm2lin(..., 2, None), retaining the original initial state. +_IMA_INDEX_TABLE = (-1, -1, -1, -1, 2, 4, 6, 8) +_IMA_STEP_TABLE = ( + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, + 37, 41, 45, 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, 130, 143, 157, + 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449, 494, 544, + 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, + 1878, 2066, 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, + 5358, 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, + 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767, +) + +def _decode_ima_adpcm(data): + valpred = 0 + index = 0 + out = bytearray() + for byte in data: + for delta in (byte >> 4, byte & 0x0F): + step = _IMA_STEP_TABLE[index] + vpdiff = step >> 3 + if delta & 4: vpdiff += step + if delta & 2: vpdiff += step >> 1 + if delta & 1: vpdiff += step >> 2 + if delta & 8: + valpred -= vpdiff + else: + valpred += vpdiff + valpred = max(-32768, min(32767, valpred)) + index += _IMA_INDEX_TABLE[delta & 7] + index = max(0, min(88, index)) + out.extend(struct.pack(" "PARA" or len(data) <= 0x6a0: + if data[:4] != b"PARA" or len(data) <= 0x6a0: return False @@ -144,22 +170,22 @@ class PPM: AudioLenght = AscDec(data[8:12], True) self.FrameCount = AscDec(data[12:14], True) + 1 - self.Locked = ord(data[0x10]) & 0x01 == 1 + self.Locked = data[0x10] & 0x01 == 1 self.ThumbnailFrameIndex = AscDec(data[0x12:0x14], True)#which frame the thumbnnail represents self.OriginalAuthorName = data[0x14:0x2A].decode("UTF-16LE").split(u"\0")[0] self.EditorAuthorName = data[0x2A:0x40].decode("UTF-16LE").split(u"\0")[0] self.Username = data[0x40:0x56].decode("UTF-16LE").split(u"\0")[0] - self.OriginalAuthorID = data[0x56:0x5e][::-1].encode("HEX").upper() - self.EditorAuthorID = data[0x5E:0x66][::-1].encode("HEX").upper()#the last user to save the file + self.OriginalAuthorID = data[0x56:0x5e][::-1].hex().upper() + self.EditorAuthorID = data[0x5E:0x66][::-1].hex().upper()#the last user to save the file self.OriginalFilenameC = data[0x66:0x78]#compressed self.CurrentFilenameC = data[0x78:0x8a]#compressed - self.OriginalFilename = "%s_%s_%s.ppm" % (self.OriginalFilenameC[:3].encode("HEX").upper(), self.OriginalFilenameC[3:-2], str(AscDec(self.OriginalFilenameC[-2:], True)).zfill(3)) - self.CurrentFilename = "%s_%s_%s.ppm" % (self.CurrentFilenameC[:3].encode("HEX").upper(), self.CurrentFilenameC[3:-2], str(AscDec(self.CurrentFilenameC[-2:], True)).zfill(3)) + self.OriginalFilename = "%s_%s_%s.ppm" % (self.OriginalFilenameC[:3].hex().upper(), self.OriginalFilenameC[3:-2].decode("ascii", "replace"), str(AscDec(self.OriginalFilenameC[-2:], True)).zfill(3)) + self.CurrentFilename = "%s_%s_%s.ppm" % (self.CurrentFilenameC[:3].hex().upper(), self.CurrentFilenameC[3:-2].decode("ascii", "replace"), str(AscDec(self.CurrentFilenameC[-2:], True)).zfill(3)) - self.PreviousEditAuthorID = data[0x8a:0x92][::-1].encode("HEX").upper()#don't know what this really is + self.PreviousEditAuthorID = data[0x8a:0x92][::-1].hex().upper()#don't know what this really is #self.PartialFilenameC = data[0x92:0x9a]#compressed @@ -172,21 +198,21 @@ class PPM: self.Loaded[0] = True #read the animation sequence header: - self.Looped = ord(data[0x06A6]) >> 1 & 0x01 == 1#Unverified? + self.Looped = data[0x06A6] >> 1 & 0x01 == 1#Unverified? AnimationOffset = 0x6a8 + AscDec(data[0x6a0:0x6a4], True) - FrameOffsets = [AnimationOffset + AscDec(data[0x06a8+i*4:0x06a8+i*4+4], True) for i in xrange(self.FrameCount)] + FrameOffsets = [AnimationOffset + AscDec(data[0x06a8+i*4:0x06a8+i*4+4], True) for i in range(self.FrameCount)] #Read the audio header: - self.SFXUsage = [(i&0x1<>0, i&0x2<>0, i&0x4<>0) for i in map(ord, data[AudioOffset:AudioOffset+self.FrameCount])]#SFXUsage[frame] = (sfx1, sfx2, sfx2) shere sfxX is either 0 or 1 + self.SFXUsage = [(i&0x1!=0, i&0x2!=0, i&0x4!=0) for i in data[AudioOffset:AudioOffset+self.FrameCount]]#SFXUsage[frame] = (sfx1, sfx2, sfx2) shere sfxX is either 0 or 1 SoundSize =(AscDec(data[AddPadding(AudioOffset+self.FrameCount, 4) :AddPadding(AudioOffset+self.FrameCount, 4)+ 4], True),#BG music AscDec(data[AddPadding(AudioOffset+self.FrameCount, 4)+ 4:AddPadding(AudioOffset+self.FrameCount, 4)+ 8], True),#SFX1 AscDec(data[AddPadding(AudioOffset+self.FrameCount, 4)+ 8:AddPadding(AudioOffset+self.FrameCount, 4)+12], True),#SFX2 AscDec(data[AddPadding(AudioOffset+self.FrameCount, 4)+12:AddPadding(AudioOffset+self.FrameCount, 4)+16], True))#SFX3 if self.forced_speed == None: - self.Framespeed = 8 - ord(data[AddPadding(AudioOffset+self.FrameCount, 4) + 16]) + self.Framespeed = 8 - data[AddPadding(AudioOffset+self.FrameCount, 4) + 16] else: self.Framespeed = self.forced_speed - self.BGMFramespeed = 8 - ord(data[AddPadding(AudioOffset+self.FrameCount, 4) + 17])#framespeed when the bgm was recorded + self.BGMFramespeed = 8 - data[AddPadding(AudioOffset+self.FrameCount, 4) + 17]#framespeed when the bgm was recorded ## self.BGMFramespeed = self.forced_speed @@ -195,11 +221,11 @@ class PPM: self.Frames = []#self.Frames[frame] = [inverted(bool), (color1(0-2), color2(0-2)), frame[layer, x, y] = bool] for i, offset in enumerate(FrameOffsets): #Read frame header: - Inverted = ord(data[offset]) & 0x01 == 0 + Inverted = data[offset] & 0x01 == 0 #Reads which color that will be used: - Colors = (ord(data[offset]) >> 1 & 0x03, - ord(data[offset]) >> 3 & 0x03) + Colors = (data[offset] >> 1 & 0x03, + data[offset] >> 3 & 0x03) Frame = self.ExtractFrame(data, offset, self.Frames[i-1][2] if i else None) @@ -214,7 +240,7 @@ class PPM: if ReadSound: self.SoundData = [] pos = AddPadding(AudioOffset+self.FrameCount+32, 4) - for i in xrange(4): + for i in range(4): self.SoundData.append(data[pos:pos+SoundSize[i]]) pos += SoundSize[i] @@ -264,7 +290,7 @@ class PPM: for y in range(8): for x in range(0,8,2): #two colors stored in each byte: - byte = ord(self.RawThumbnail[(ty*512+tx*64+y*8+x)/2]) + byte = self.RawThumbnail[(ty*512+tx*64+y*8+x)//2] out[x+tx*8 , y+ty*8] = palette[byte & 0xF] out[x+tx*8+1, y+ty*8] = palette[byte >> 4] @@ -275,12 +301,12 @@ class PPM: if self.SoundData[index]: #reverse nibbles: data = [] - for i in map(ord,self.SoundData[index]): - data.append(chr((i&0xF)<< 4 | (i>>4))) - data = "".join(data) + for i in self.SoundData[index]: + data.append((i&0xF)<< 4 | (i>>4)) + data = bytes(data) #4bit ADPCM decode - decoded = audioop.adpcm2lin(data, 2, None)[0] + decoded = _decode_ima_adpcm(data) #write to wav: if outputpath: @@ -307,19 +333,19 @@ class PPM: #Color2Frame = [[0 for i in range(192)] for i in range(256)] Encoding = [[], []] - #Frame = [[[False for _ in xrange(192)] for _ in xrange(256)] for _ in xrange(2)] + #Frame = [[[False for _ in range(192)] for _ in range(256)] for _ in range(2)] Frame = np.zeros((2, 256, 192), dtype=np.bool_) #Read tags: - NewFrame = ord(data[offset]) & 0x80 <> 0 - Unknown = ord(data[offset]) >> 5 & 0x03 + NewFrame = data[offset] & 0x80 != 0 + Unknown = data[offset] >> 5 & 0x03 offset += 1 #WIP - framemove: FrameMove = [0,0] if Unknown & 0x2:#doesn't work 100%... - print "FrameMove at offset ",offset-1 + print("FrameMove at offset ",offset-1) move_x = AscDec(data[offset+0:offset+1], True) move_y = AscDec(data[offset+1:offset+2], True) @@ -327,12 +353,12 @@ class PPM: FrameMove[1] = move_y if move_y <= 127 else move_y-256 offset += 2 elif Unknown: - print "Unknown tags:",Unknown,"at offset ",offset-1 + print("Unknown tags:",Unknown,"at offset ",offset-1) #read the line encoding of the layers: - for layer in xrange(2): - for byte in map(ord, data[offset:offset+48]): + for layer in range(2): + for byte in data[offset:offset+48]: Encoding[layer].append(byte & 0x03) Encoding[layer].append(byte >> 2 & 0x03) Encoding[layer].append(byte >> 4 & 0x03) @@ -340,8 +366,8 @@ class PPM: offset += 48 #read layers: - for layer in xrange(2): - for y in xrange(192): + for layer in range(2): + for y in range(192): if Encoding[layer][y] == 0:#Nothing pass elif Encoding[layer][y] == 1:#Normal @@ -350,9 +376,9 @@ class PPM: x = 0 while UseByte & 0xFFFFFFFF: if UseByte & 0x80000000: - byte = ord(data[offset]) + byte = data[offset] offset += 1 - for _ in xrange(8): + for _ in range(8): if byte & 0x01: Frame[layer][x][y] = True x += 1 @@ -366,7 +392,7 @@ class PPM: x = 0 while UseByte&0xFFFFFFFF: if UseByte & 0x80000000: - byte = ord(data[offset]) + byte = data[offset] offset += 1 for _ in range(8): if not byte & 0x01: @@ -382,7 +408,7 @@ class PPM: elif Encoding[layer][y] == 3:#Raw/full x = 0 for _ in range(32): - byte = ord(data[offset]) + byte = data[offset] offset += 1 for _ in range(8): if byte & 0x01: @@ -391,7 +417,7 @@ class PPM: byte >>= 1 #Merges this frame with the previous frame if NewFrame isn't true: - if not NewFrame and PrevFrame.all() <> None:#maybe optimize this better for numpy... + if not NewFrame and PrevFrame is not None:#maybe optimize this better for numpy... if FrameMove[0] or FrameMove[1]:#Moves the previous frame if specified: NewPrevFrame = np.zeros((2, 256, 192), dtype=np.bool_) @@ -406,7 +432,7 @@ class PPM: PrevFrame = NewPrevFrame #merge the frames: - Frame = Frame <> PrevFrame + Frame = Frame != PrevFrame return Frame @@ -422,9 +448,9 @@ class PPM: # - instance.FrameCount: The number of frames this flipnote consists of # - instance.Locked: A boolean telling us wether it's locked or not # - instance.ThumbnailFrameIndex: Which frame the thumbnail is(starting at 0) -# - instance.OriginalAuthorName: The name of the original author in unicode -# - instance.EditorAuthorName: The name of the (previous?) editing author in unicode -# - instance.Username: The username in unicode +# - instance.OriginalAuthorName: The name of the original author in str +# - instance.EditorAuthorName: The name of the (previous?) editing author in str +# - instance.Username: The username in str # - instance.OriginalAuthorID: The ID of the original author # - instance.EditorAuthorID: The ID of the last editor, the last user to save the file # - instance.PreviousEditAuthorID: The ID of the previous editor @@ -443,7 +469,7 @@ class TMB: f.close() return ret def Read(self, data, DecodeThumbnail=False): - if data[:4] <> "PARA" or len(data) < 0x6a0: + if data[:4] != b"PARA" or len(data) < 0x6a0: return False #Read the header: @@ -451,25 +477,25 @@ class TMB: self.AudioLenght = AscDec(data[8:12], True)#only stored for self.Pack() self.FrameCount = AscDec(data[12:14], True) + 1 - self.Locked = ord(data[0x10]) & 0x01 == 1 + self.Locked = data[0x10] & 0x01 == 1 self.ThumbnailFrameIndex = AscDec(data[0x12:0x14], True)#which frame is in the thumbnnail - #self.OriginalAuthorName = u"".join(unichr(AscDec(data[0x14+i*2:0x14+i*2+2], True)) for i in xrange(11)).split(u"\0")[0] - #self.EditorAuthorName = u"".join(unichr(AscDec(data[0x2A+i*2:0x2A+i*2+2], True)) for i in xrange(11)).split(u"\0")[0] - #self.Username = u"".join(unichr(AscDec(data[0x40+i*2:0x40+i*2+2], True)) for i in xrange(11)).split(u"\0")[0] + #self.OriginalAuthorName = u"".join(chr(AscDec(data[0x14+i*2:0x14+i*2+2], True)) for i in range(11)).split(u"\0")[0] + #self.EditorAuthorName = u"".join(chr(AscDec(data[0x2A+i*2:0x2A+i*2+2], True)) for i in range(11)).split(u"\0")[0] + #self.Username = u"".join(chr(AscDec(data[0x40+i*2:0x40+i*2+2], True)) for i in range(11)).split(u"\0")[0] self.OriginalAuthorName = data[0x14:0x2A].decode("UTF-16LE").split(u"\0")[0] self.EditorAuthorName = data[0x2A:0x40].decode("UTF-16LE").split(u"\0")[0] self.Username = data[0x40:0x56].decode("UTF-16LE").split(u"\0")[0] - self.OriginalAuthorID = data[0x56:0x5e][::-1].encode("HEX").upper() - self.EditorAuthorID = data[0x5E:0x66][::-1].encode("HEX").upper()#the last user to save the file + self.OriginalAuthorID = data[0x56:0x5e][::-1].hex().upper() + self.EditorAuthorID = data[0x5E:0x66][::-1].hex().upper()#the last user to save the file self.OriginalFilenameC = data[0x66:0x78]#compressed self.CurrentFilenameC = data[0x78:0x8a]#compressed - self.OriginalFilename = "%s_%s_%s.tmb" % (self.OriginalFilenameC[:3].encode("HEX").upper(), self.OriginalFilenameC[3:-2], str(AscDec(self.OriginalFilenameC[-2:], True)).zfill(3)) - self.CurrentFilename = "%s_%s_%s.tmb" % (self.CurrentFilenameC[:3].encode("HEX").upper(), self.CurrentFilenameC[3:-2], str(AscDec(self.CurrentFilenameC[-2:], True)).zfill(3)) + self.OriginalFilename = "%s_%s_%s.tmb" % (self.OriginalFilenameC[:3].hex().upper(), self.OriginalFilenameC[3:-2].decode("ascii", "replace"), str(AscDec(self.OriginalFilenameC[-2:], True)).zfill(3)) + self.CurrentFilename = "%s_%s_%s.tmb" % (self.CurrentFilenameC[:3].hex().upper(), self.CurrentFilenameC[3:-2].decode("ascii", "replace"), str(AscDec(self.CurrentFilenameC[-2:], True)).zfill(3)) - self.PreviousEditAuthorID = data[0x8a:0x92][::-1].encode("HEX").upper()#don't know what this really is + self.PreviousEditAuthorID = data[0x8a:0x92][::-1].hex().upper()#don't know what this really is self.PartialFilenameC = data[0x92:0x9a]#compressed @@ -497,27 +523,27 @@ class TMB: #realself = self #if ppm: self = ppm - out = ["PARA",#magic + out = [b"PARA",#magic DecAsc(self.AudioOffset-0x6a0, 4, True),#animation data size DecAsc(self.AudioLenght, 4, True),#audio data size DecAsc(self.FrameCount-1, 2, True),#frame count - "\x24\x00",#unknown - chr(self.Locked), "\0",#locked + b"\x24\x00",#unknown + bytes((int(bool(self.Locked)),)), b"\0",#locked DecAsc(self.ThumbnailFrameIndex, 2, True),#which frame is in the thumbnnail - self.OriginalAuthorName.encode("UTF-16LE") + "\0\0"*(11-len(self.OriginalAuthorName)),#Original Author Name - self.EditorAuthorName.encode("UTF-16LE") + "\0\0"*(11-len(self.EditorAuthorName)),#Editor Author Name - self.Username.encode("UTF-16LE") + "\0\0"*(11-len(self.Username)),#Username - self.OriginalAuthorID.decode("HEX")[::-1],#OriginalAuthorID - self.EditorAuthorID.decode("HEX")[::-1],#EditorAuthorID + self.OriginalAuthorName.encode("UTF-16LE") + b"\0\0"*(11-len(self.OriginalAuthorName)),#Original Author Name + self.EditorAuthorName.encode("UTF-16LE") + b"\0\0"*(11-len(self.EditorAuthorName)),#Editor Author Name + self.Username.encode("UTF-16LE") + b"\0\0"*(11-len(self.Username)),#Username + bytes.fromhex(self.OriginalAuthorID)[::-1],#OriginalAuthorID + bytes.fromhex(self.EditorAuthorID)[::-1],#EditorAuthorID self.OriginalFilenameC,#OriginalFilename self.CurrentFilenameC,#CurrentFilename - self.PreviousEditAuthorID.decode("HEX")[::-1],#EditorAuthorID + bytes.fromhex(self.PreviousEditAuthorID)[::-1],#EditorAuthorID self.PartialFilenameC,#PartialFilename DecAsc(self.Date, 4, True),#Date in seconds - "\0\0",#padding + b"\0\0",#padding self.PackThumbnail()]#thumbnail - return "".join(out) + return b"".join(out) def GetThumbnail(self, force=False): if (self.Thumbnail is None or force):# and self.RawThumbnail: global ThumbPalette @@ -535,7 +561,7 @@ class TMB: for y in range(8): for x in range(0,8,2): #two colors stored in each byte: - byte = ord(self.RawThumbnail[(ty*512+tx*64+y*8+x)/2]) + byte = self.RawThumbnail[(ty*512+tx*64+y*8+x)//2] out[x+tx*8 , y+ty*8] = palette[byte & 0xF] out[x+tx*8+1, y+ty*8] = palette[byte >> 4] @@ -560,7 +586,7 @@ class TMB: 0x00FF00FF)#F- - if not self.Thumbnail: + if self.Thumbnail is None: return self.RawThumbnail else: if Exact: @@ -572,12 +598,12 @@ class TMB: for y in range(8): for x in range(0,8,2): #two colors stored in each byte: - #pos = 0xa0+(ty*512+tx*64+y*8+x)/2 + #pos = 0xa0+(ty*512+tx*64+y*8+x)//2 p1 = palette.index(int(self.Thumbnail[x+tx*8 , y+ty*8])) p2 = palette.index(int(self.Thumbnail[x+tx*8+1, y+ty*8])) - out.append(chr(p2<<4 | p1)) + out.append(p2<<4 | p1) - self.RawThumbnail = "".join(out) + self.RawThumbnail = bytes(out) return self.RawThumbnail else: #not yet implented @@ -591,7 +617,7 @@ class TMB: # This function requires the PIl imaging module def WriteImage(image, outputPath, scale=1): if not hasPIL: - print "Error: PIL not found!" + print("Error: PIL not found!") return False #if not image: return False @@ -603,7 +629,7 @@ def WriteImage(image, outputPath, scale=1): else: newImg = image - out = newImg.tostring("F") + out = newImg.tobytes(order="F") out = Image.frombytes("RGBA", (len(newImg), len(newImg[0])), out) filetype = outputPath[outputPath.rfind(".")+1:] @@ -611,7 +637,9 @@ def WriteImage(image, outputPath, scale=1): return True -def get_metadata(flipnote): +def get_metadata(flipnote, filetype=None): + if filetype is None: + filetype = "ppm" if isinstance(flipnote, PPM) else "tmb" epoch = time.mktime(time.struct_time([2000, 1, 1, 0, 0, 0, 5, 1, -1])) meta = { @@ -636,8 +664,8 @@ def get_metadata(flipnote): return meta def DumpFrames(flipnote,directory,scale=1): - for i in xrange(flipnote.FrameCount): - print "Dumping frame #%i of %i..." % (i+1, flipnote.FrameCount) + for i in range(flipnote.FrameCount): + print("Dumping frame #%i of %i..." % (i+1, flipnote.FrameCount)) WriteImage(flipnote.GetFrame(i), os.path.join(directory, "frame %s.png" % str(i+1).zfill(3)), scale) def DumpSoundFiles(flipnote,directory,raw=False): @@ -657,161 +685,159 @@ def DumpSFXUsage(flipnote,directory): f.write("Frame %i:%s%s%s\n" % (i, " SFX1"*s1, " SFX2"*s2, " SFX3"*s3)) if __name__ == '__main__': - print " == PPM.py ==" - print " == by pbsds ==" - print " == v1.3 ==" - print - + print(" == PPM.py ==") + print(" == by pbsds ==") + print(" == v1.3 ==") + print() if len(sys.argv) < 3: - print "Usage:" - print " PPM.py [] [] [