Files
Hatenatools3/UGO.py
T

625 lines
19 KiB
Python

#UGO.py by pbsds
#AGPL3 licensed
#
#This class reads and writes UGO files.
#It can also export and import it in a xml format
#Extended functionality added by PPM.py
#
#Credits:
#
# -Jsafive for supplying .ugo files
#
#Note:
# The current implentation can possibly mess up the files stored in Section #2, but will do for most needs
# Next version will probably bring a big change in the ugoxml format
#
import sys, os
from base64 import b64encode, b64decode
import xml.etree.ElementTree as ET
try:
import PPM
HasPPM = True
except ImportError:
HasPPM = 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 zipalign(length, r=4):
return length + (4 - length % r) if length % r else length
def indentXML(elem, level=0):#"borrowed" from: http://effbot.org/zone/element-lib.htm#prettyprint
i = "\n" + level*"\t"
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + "\t"
if not elem.tail or not elem.tail.strip():
elem.tail = i
for elem in elem:
indentXML(elem, level+1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
#class UGO:
class UGO:
def __init__(self):
self.Loaded = False
def ReadFile(self, path):
f = open(path, "rb")
ret = self.Read(f.read())
f.close()
return ret
def Read(self, data):
#Filestructure:
# - Header
# - Table of Contents
# - Padding, normally of length 2
# - Extra Data
# - Padding
#The padding seems to do zipaligning to a length of 4
global HasPPM
#Header:
if data[:4] <> "UGAR": return False#The file isn't a UGO file
Sections = AscDec(data[ 4: 8], True)#could also be version
if Sections >= 1:
self.TableLength = AscDec(data[ 8:12], True)
if Sections >= 2:
self.ExtraLength = AscDec(data[12:16], True)
if Sections > 2:
print "Warning: This UGO file has more than the 2 known sections:",Sections
print "Please send this UGO file to pbsds over at pbsds.net"
print "This file could possibly be read incorrectly..."
headerlength = 8 + Sections*4
#Read table of contents:
#A table where the rows are seperated with newlines, and colons with tabs
if Sections >= 1:
TableOfContents = tuple((i.split("\t") for i in data[headerlength:headerlength+self.TableLength].split("\n")))
else:
TableOfContents = []
#Extra data:
if Sections >= 2:
ExtraData = data[zipalign(headerlength+self.TableLength) : zipalign(headerlength+self.TableLength)+self.ExtraLength]
#Parse data:
self.Items = []
#todo: implement this:
self.Files = []#[i] = (filename, filecontent)
#see todo.txt
pos = 0#Extra Data position
tmbcount = 1#used if HasPPM is False
ntftcount = 1#used if no label
names = []
for i in TableOfContents:
type = int(i[0])
if type == 0:#layout
#I've always seen just numbers here.
#The amount of numbers also differ.
#maybe related to color scheme? or the button scheme/layout
#all pages containing TMBs have so far been: i = ["0", "2", "1"]
self.Items.append(("layout", map(int, i[1:])))
continue
elif type == 1:#Text on topscreen
num = int(i[1])#always seen as 0, unknown purpose
labels = [b64decode(i[n]).decode("UTF-16LE") for n in xrange(2,7)]#5 labels probably one label for each line of text
self.Items.append(("topscreen text", labels, num))
continue
elif type == 2:#catogories(like "reccomended" and "new flipnotes" and "most popular")
#this one may visually change greatly depending on what layout is set in type==0
link = i[1]
label = b64decode(i[2]).decode("UTF-16LE")
selected = int(i[3]) <> 0#bool
self.Items.append(("category", link, label, selected))
continue
elif type == 3:#POST button/link to POST form. "Post Flipnote" uses this
link = i[1]
label = b64decode(i[2]).decode("UTF-16LE")
self.Items.append(("post", link, label))
continue
elif type == 4:#Button
link = i[1]
trait = int(i[2])
label = b64decode(i[3]).decode("UTF-16LE")
other = i[4:]#varies
#extra data
file = None#== ("filename", "filedata")
if trait < 100 and ExtraData:
if ExtraData == "\x20":#empty
pass#no files
elif ExtraData[pos:pos+4] == "PARA":#tmb file
file = ExtraData[pos:pos+0x6A0]
pos += 0x6A0
if HasPPM:
tmb = PPM.TMB().Read(file)
name = tmb.CurrentFilename[:-4]
del tmb
else:
name = "embedded tmb #%i" % tmbcount
tmbcount += 1
if name+".tmb" in names:
j = 2
while "%s_%i.tmb" % (name, j) not in names:
j += 1
name = "%s_%i" % (name, j)
file = (name+".tmb", file)
names.append(name+".tmb")
else:#ntft icon
name = label.encode("ascii", "ignore")
if not name:
name = "nameless ntft %i" % ntftcount
ntftcount += 1
if name+".ntft" in names:
j = 2
while "%s_%i.ntft" % (name, j) not in names:
j += 1
name = "%s_%i" % (name, j)
file = (name+".ntft", ExtraData[pos:pos+2048])
names.append(name+".ntft")
pos += 2048
self.Items.append(("button", trait, label, link, other, file))
# if subtype == 3:#flipnote
# tmb = PPM.TMB().Read(ExtraData[pos:pos+0x6A0]); pos += 0x6A0
# unknown1 = i[3]#empty
# stars = int(i[4])#not sure
# unknown2 = map(int, i[5:8])#unknown = [765, 573, 0]
# self.Items.append(("flipnote", link, tmb, stars, unknown1, unknown2))
# continue
# elif subtype == 100: pass
# elif subtype == 101: pass
# elif subtype == 102: pass
# elif subtype == 104:#list item? like mails and announcements
# label = b64decode(i[3]).decode("UTF-16LE")
# unknown = i[4]
# num = int(i[5])#only seen as 0
# # self.Items.append(("list item?", link, label, unknown, num))
# # continue
# pass
# elif subtype == 115:#Labeled button link(size of a flipnote thumbnail, commonly "next page")
# label = b64decode(i[3]).decode("UTF-16LE")
# unknown = i[4:6]
# self.Items.append(("thumbnail link", link, label, unknown))
# continue
# elif subtype == 117: pass
continue
#if not recognized:
self.Items.append(("unknown", i))
print "Unknown UGO item discovered:", i
self.Loaded = True
return self
def WriteFile(self, path):
if self.Loaded:
out = self.Pack()
if out:
f = open(path, "wb")
f.write(out)
f.close()
return True
else:
return False
def Pack(self):
if not self.Loaded: return False
Header = ["UGAR", None]
TableOfContents = []
ExtraData = []
#Encode data:
for i in self.Items:
if i[0] == "unknown":
TableOfContents.append("\t".join(i[1]))
elif i[0] == "layout":#0
TableOfContents.append("\t".join(["0"] + map(str, list(i[1]))))
elif i[0] == "topscreen text":#1
labels, num = i[1:]
num = str(num)
for i in xrange(5):
labels[i] = b64encode(labels[i].encode("UTF-16LE"))
TableOfContents.append("\t".join(("1", num, labels[0], labels[1], labels[2], labels[3], labels[4])))
elif i[0] == "category":#2
link, label, selected = i[1:]
label = b64encode(label.encode("UTF-16LE"))
selected = str(1*selected)
TableOfContents.append("\t".join(("2", link, label, selected)))
elif i[0] == "post":#3
link, label = i[1:]
label = b64encode(label.encode("UTF-16LE"))
TableOfContents.append("\t".join(("3", link, label)))
elif i[0] == "button":#4
trait, label, link, other, file = i[1:]
trait = str(trait)
label = b64encode(label.encode("UTF-16LE"))
TableOfContents.append("\t".join(["4", link, trait, label] + list(other)))
if file:
ExtraData.append(file[1])
else:
print "Unrecognized entry in self.Items:", i
TableOfContents = "\n".join(TableOfContents)
ExtraData = "".join(ExtraData)
#Format data:
Sections = 0
if TableOfContents:
Sections += 1
Header.append(DecAsc(len(TableOfContents), 4, True))
#padding/zipaligning
if len(TableOfContents) % 4:
TableOfContents += "\0" * (4 - len(TableOfContents) % 4)
if ExtraData:
Sections += 1
Header.append(DecAsc(len(ExtraData), 4, True))
#padding/zipaligning
if len(ExtraData) % 4:
ExtraData += "\0" * (4 - len(ExtraData) % 4)
Header[1] = DecAsc(Sections, 4, True)
#Zip up and send the file:
return "".join(Header) + TableOfContents + ExtraData
#XML
def WriteXML(self, xmlname="content.ugoxml", folder="content.ugoxml embedded"):#WIP
if not self.Loaded: return False
path, xmlname = os.path.split(xmlname)
ugo_xml = ET.Element("ugo_xml")
files = []
for i in self.Items:
if i[0] == "unknown":
elem = ET.SubElement(ugo_xml, "raw", type=i[1][0])
for value in i[1][1:]:
ET.SubElement(elem, "value").text = value
continue
elif i[0] == "layout":#0
elem = ET.SubElement(ugo_xml, "layout")
for value in i[1]:
ET.SubElement(elem, "value").text = str(value)
continue
elif i[0] == "topscreen text":#1
elem = ET.SubElement(ugo_xml, "title")
labels, num = i[1:]
for label in labels:
ET.SubElement(elem, "label").text = label
ET.SubElement(elem, "num").text = str(num)
continue
elif i[0] == "category":#2
elem = ET.SubElement(ugo_xml, "category")
link, label, selected = i[1:]
ET.SubElement(elem, "label").text = label
ET.SubElement(elem, "address").text = link
ET.SubElement(elem, "selected").text = str(selected).lower()
elif i[0] == "post":#3
elem = ET.SubElement(ugo_xml, "post")
link, label = i[1:]
ET.SubElement(elem, "label").text = label
ET.SubElement(elem, "address").text = link
elif i[0] == "button":#4
elem = ET.SubElement(ugo_xml, "button")
trait, label, link, other, file = i[1:]
ET.SubElement(elem, "label").text = label
ET.SubElement(elem, "address").text = link
ET.SubElement(elem, "trait").text = str(trait)#todo: add names
for n, value in enumerate(other):
entry = ET.SubElement(elem, "value")
entry.text = value
if n == 0 and trait == 3:
entry.attrib["tip"] = "stars"
if file:
ET.SubElement(elem, "embedded_file").text = os.path.join(folder, file[0])
files.append((os.path.join(folder, file[0]), file[1]))
#intend
indentXML(ugo_xml)
#pack xml
files.append((xmlname, ET.tostring(ugo_xml, encoding="UTF-8")))
#write files
if not os.path.isdir(os.path.join(path, folder)):
os.mkdir(os.path.join(path, folder))
for name, data in files:
f = open(os.path.join(path, name), "wb")
f.write(data)
f.close()
def ReadXML(self, xmlfile, silent=True):
ugo_xml = ET.parse(xmlfile).getroot()
xmlpath = os.path.split(xmlfile)[0]
Items = []
for elem in ugo_xml:
if elem.tag == "raw":
if "type" not in elem.attrib:
if not silent: print "Invalid formatting. <raw> without \"type\" attribute"
return False
values = [elem.attrib["type"]]
for value in elem:
if value.tag <> "value":
if not silent: print "Invalid formatting. <%s> found within <unknown>" % value.tag
return False
values.append(value.text if value.text else "")
Items.append(("unknown", values))
elif elem.tag == "layout":#0
values = []
for value in elem:
if value.tag <> "value":
if not silent: print "Invalid formatting. <%s> found within <layout>" % value.tag
return False
if not value.text.isdigit():
if not silent: print "Invalid entry. <value> in <layout> is not a number" % value.tag
return False
values.append(int(value.text))
Items.append(("layout", values))
elif elem.tag == "title":#1
labels = ["", "", "", "", ""]
num = 0
pos = 0
numset = False
for value in elem:
if value.tag not in ("label", "num"):
if not silent: print "Invalid formatting. <%s> found within <title>" % value.tag
return False
if value.tag == "label":
if pos >= 5:
if not silent: print "Invalid formatting. More than 5 <labels> in <title>"
return False
if value.text: labels[pos] = value.text
pos += 1
elif value.tag == "num":
if numset:
if not silent: print "Invalid formatting. Multible <num> in <title>"
return False
if not value.text.isdigit():
if not silent: print "Invalid entry. <num> in <title> is not a number!"
return False
num = int(value.text)
numset = True
Items.append(("topscreen text", labels, num))
elif elem.tag == "category":#2
link = None
label = None
selected = None
for value in elem:
if value.tag not in ("label", "address", "selected"):
if not silent: print "Invalid formatting. <%s> found within <category>" % value.tag
return False
if value.tag == "address":
if isinstance(link, str):
if not silent: print "Invalid formatting. multible <address> within <category>"
return False
link = value.text if value.text else ""
elif value.tag == "label":
if isinstance(label, str):
if not silent: print "Invalid formatting. multible <label> within <category>"
return False
label = value.text if value.text else ""
elif value.tag == "selected":
if selected in (True, False):
if not silent: print "Invalid formatting. multible <selected> within <category>"
return False
selected = value.text[0].lower() in "t1"
Items.append(("category", link, label, selected))
elif elem.tag == "post":#3
label = None
link = None
for value in elem:
if value.tag == "label":
if isinstance(label, str):
if not silent: print "Invalid formatting. Multible <label> within <post>"
return False
label = value.text if value.text else ""
elif value.tag == "address":
if isinstance(link, str):
if not silent: print "Invalid formatting. Multible <address> within <post>"
return False
link = value.text if value.text else ""
if None in (link, label):
if not silent: print "Invalid formatting. <button> lacks either a <address> or <label>"
return False
Items.append(("post", link, label))
elif elem.tag == "button":#4
trait = None#todo: add names
label = None
link = None
other = []
file = None
for value in elem:
if value.tag not in ("label", "address", "trait", "value", "embedded_file"):
if not silent: print "Invalid formatting. <%s> found within <button>" % value.tag
return False
if value.tag == "label":
if isinstance(label, str):
if not silent: print "Invalid formatting. Multible <label> within <button>"
return False
label = value.text if value.text else ""
elif value.tag == "address":
if isinstance(link, str):
if not silent: print "Invalid formatting. Multible <address> within <button>"
return False
link = value.text if value.text else ""
elif value.tag == "trait":#todo: add names
if isinstance(trait, str):
if not silent: print "Invalid formatting. Multible <trait> within <button>"
return False
if not value.text.isdigit():
if not silent: print "Invalid entry. <trait> in <button> is not a number"
return False
trait = int(value.text)
elif value.tag == "value":
other.append(value.text if value.text else "")
elif value.tag == "embedded_file":
if file <> None:
if not silent: print "Invalid formatting. Multible <embedded_file> within <button>"
return False
path = os.path.join(xmlpath, value.text)
if not os.path.isfile(path):
if not silent: print "Invalid entry. Embedded file \"%s\" not found!" % value.text
print path
return False
f = open(path, "rb")
file = (os.path.split(value.text)[1], f.read())
f.close()
if None in (trait, label, link):
if not silent: print "Invalid formatting. <button> lacks either a <trait>, a <address> or a <label>"
return False
Items.append(("button", trait, label, link, other, file))
else:
if not silent:
print "Invalid formatting: <%s> found within <ugo_xml>" % elem.tag
self.Items = Items
self.Loaded = True
return self
if __name__ == "__main__":
print " == UGO.py =="
print " == by pbsds =="
print " == v0.92 =="
print
if len(sys.argv) < 2:
print "Usage:"
print " UGO.py [<mode>] <input> [<output> [<foldername>]]"
print ""
print " <Mode>:"
print " -d: Converts the UGO file in <input> to a UGOXML file with the same"
print " name, unless <output> is specified. Any embedded files will be"
print " written to a folder called UGOXML-filename + \" embedded\" unless"
print " <foldername> is given."
print " <foldername> is relative to the XML."
print " -e: Converts the UGOXML file in <input> to a UGO file with the same"
print " name, unless <output> is specified."
print " If mode is not specified, it will try to find out for itself"
sys.exit()
mode = sys.argv[1]
if mode not in ("-d", "-e"):
if os.path.exists(mode):#the mode is actually the file
f = open(mode, "rb")
magic = f.read(4)
f.close()
if magic == "UGAR":
mode = "-d"
print "No mode specified. UGO -> UGOXML chosen"
else:
mode = "-e"
print "No mode specified. UGOXML -> UGO chosen"
sys.argv.insert(1, mode)
else:
print "Invalid <mode> given!"
sys.exit()
if mode == "-d":
input = sys.argv[2]
output = sys.argv[3] if len(sys.argv) >= 4 else sys.argv[2]+"xml"
foldername = sys.argv[4] if len(sys.argv) >= 5 else os.path.split(output)[1] + " embedded"
print "Reading %s..." % os.path.split(input)[1]
ugo = UGO().ReadFile(input)
if not ugo:
print "Error!\n The given file is not a UGO file!"
sys.exit()
print "Done!"
print "Writing XML..."
ugo.WriteXML(output, foldername)
print "Done!\n\nHave a nice day!"
if mode == "-e":
input = sys.argv[2]
output = sys.argv[3] if len(sys.argv) >= 4 else ".".join(sys.argv[2].split(".")[:-1]) + ".ugo"
print "Reading %s..." % os.path.split(input)[1]
try:
ugo = UGO().ReadXML(input, False)
except EL.ParseError:
print "Error!\nThe given file is not in the XML format!"
ugo = False
if not ugo:
#it prints sufficient errormessages
#print "Error!\n The given file is not a UGO file!"
sys.exit()
print "Done!"
print "Writing UGO..."
ugo.WriteFile(output)
print "Done"