#!/usr/bin/env python # blorbtool.py: A (semi-)multifunctional Blorb utility # Created by Andrew Plotkin (erkyrath@eblong.com) # Last updated: June 12, 2017 # This script is in the public domain. # When listing chunks, you'll see output that looks like: # 'GLUL' (232192 bytes, start 60) # "60" means that the IFF chunk starts at byte 60 in the blorb file. There's # always an eight-byte header, so the actual Glulx data file starts at byte # 68 (and is then 232192 bytes long). # # For AIFF chunks, you'll see: # 'FORM'/'AIFF' (8536+8 bytes, start 324266) # The AIFF data implicitly includes the eight-byte header, which is why the # length says "+8". Start at byte 324266 and read 8544 bytes. # We use the print() function for Python 2/3 compatibility from __future__ import print_function # We use the Py2 raw_input() function. In Py3 there is no such function, # but we define a back-polyfill. (I'm lazy.) try: raw_input except NameError: raw_input = input import sys import os import optparse import re import collections import struct import base64 import json from chunk import Chunk try: import readline except: pass try: # Python 3.3 and up os_replace = os.replace except AttributeError: if (os.name != 'nt'): # Older Python (on Unix) os_replace = os.rename else: # On Windows, os.rename can't replace an existing file. def os_replace(src, dst): try: os.remove(dst) except: pass os.rename(src, dst) popt = optparse.OptionParser(usage='blorbtool.py BLORBFILE [ command ]') popt.add_option('-n', '--new', action='store_true', dest='newfile', help='create a new blorb file instead of loading one in') popt.add_option('-o', '--output', action='store', dest='output', metavar='BLORBFILE', help='blorb file to write to (if requested)') popt.add_option('-f', '--force', action='store_true', dest='force', help='overwrite files without confirming') popt.add_option('-v', '--verbose', action='store_true', dest='verbose', help='verbose stack traces on error') popt.add_option('-l', '--commands', action='store_true', dest='listcommands', help='list all commands (and exit)') (opts, args) = popt.parse_args() def dict_append(map, key, val): ls = map.get(key) if (not ls): ls = [] map[key] = ls ls.append(val) def confirm_input(prompt): ln = raw_input(prompt+' >') if (ln.lower().startswith('y')): return True class BlorbChunk: def __init__(self, blorbfile, typ, start, len, formtype=None): self.blorbfile = blorbfile self.type = typ self.start = start self.len = len self.formtype = formtype self.literaldata = None self.filedata = None self.filestart = None def __repr__(self): return '' % (typestring(self.type), self.start, self.len) def data(self, max=None): if (self.literaldata): if (max is not None): return self.literaldata[0:max] else: return self.literaldata if (self.filedata): fl = open(self.filedata, 'rb') if (self.filestart is not None): fl.seek(self.filestart) if (max is not None): dat = fl.read(max) else: dat = fl.read() fl.close() return dat self.blorbfile.formchunk.seek(self.start) toread = self.len if (max is not None): toread = min(self.len, max) return self.blorbfile.formchunk.read(toread) def describe(self): if (not self.formtype): return '%s (%d bytes, start %d)' % (typestring(self.type), self.len, self.start) else: return '%s/%s (%d+8 bytes, start %d)' % (typestring(self.type), typestring(self.formtype), self.len, self.start) def display(self): print('* %s' % (self.describe(),)) if (self.type == b'RIdx'): # Index chunk dat = self.data() (subdat, dat) = (dat[:4], dat[4:]) num = struct.unpack('>I', subdat)[0] print('%d resources:' % (num,)) while (dat): (subdat, dat) = (dat[:12], dat[12:]) subls = struct.unpack('>4c2I', subdat) usage = b''.join(subls[0:4]) print(' %s %d: starts at %d' % (typestring(usage), subls[-2], subls[-1])) elif (self.type == b'IFmd'): # Metadata chunk dat = self.data() print(dat.decode('utf-8')) elif (self.type == b'Fspc'): # Frontispiece chunk dat = self.data() if (len(dat) != 4): print('Warning: invalid contents!') else: num = struct.unpack('>I', dat[0:4])[0] print('Frontispiece is pict number', num) elif (self.type == b'RDes'): # Resource description chunk dat = self.data() (subdat, dat) = (dat[:4], dat[4:]) count = struct.unpack('>I', subdat)[0] print('%d entries:' % (count,)) for ix in range(count): if (len(dat) < 12): print('Warning: contents too short!') break (subdat, dat) = (dat[:12], dat[12:]) subls = struct.unpack('>4c2I', subdat) restype = b''.join(subls[0:4]) strlen = subls[-1] num = subls[-2] if (len(dat) < strlen): print('Warning: contents too short!') break (subdat, dat) = (dat[:strlen], dat[strlen:]) print(' %s resource %d: "%s"' % (typestring(restype), num, subdat.decode('utf-8'))) if (len(dat) > 0): print('Warning: contents too long!') elif (self.type == b'APal'): # Adaptive palette dat = self.data() if (len(dat) % 4 != 0): print('Warning: invalid contents!') else: ls = [] while (dat): (subdat, dat) = (dat[:4], dat[4:]) num = struct.unpack('>I', subdat)[0] ls.append(str(num)) print('Picts using adaptive palette:', ' '.join(ls)) elif (self.type == b'Loop'): # Looping dat = self.data() if (len(dat) % 8 != 0): print('Warning: invalid contents!') else: while (dat): (subdat, dat) = (dat[:8], dat[8:]) (num, count) = struct.unpack('>II', subdat) print('Sound %d repeats %d times' % (num, count)) elif (self.type == b'RelN'): # Release number dat = self.data() if (len(dat) != 2): print('Warning: invalid contents!') else: num = struct.unpack('>H', dat)[0] print('Release number', num) elif (self.type == b'SNam'): # Story name (obsolete) dat = self.data() if (len(dat) % 2 != 0): print('Warning: invalid contents!') else: ls = [] while (dat): (subdat, dat) = (dat[:2], dat[2:]) num = struct.unpack('>H', subdat)[0] ls.append(chr(num)) print('Story name:', ''.join(ls)) elif (self.type in (b'TEXT', b'ANNO', b'AUTH', b'(c) ')): dat = self.data() print(dat.decode()) elif (self.type == b'Reso'): # Resolution chunk dat = self.data() if (len(dat)-24) % 28 != 0: print('Warning: invalid contents!') else: (subdat, dat) = (dat[:24], dat[24:]) subls = struct.unpack('>6I', subdat) print('Standard window size %dx%d, min %dx%d, max %dx%d' % subls) while (dat): (subdat, dat) = (dat[:28], dat[28:]) subls = struct.unpack('>7I', subdat) print('Pict %d: standard ratio: %d/%d, min %d/%d, max %d/%d' % subls) else: dat = self.data(16) strdat = repr(dat) if (re.match('[a-z][\'\"]', strdat)): strdat = strdat[1:] if (len(dat) == self.len): print('contents: %s' % (strdat,)) else: print('beginning: %s' % (strdat,)) class BlorbFile: def __init__(self, filename, outfilename=None): self.chunks = [] self.chunkmap = {} self.chunkatpos = {} self.usages = [] self.usagemap = {} self.filename = filename self.outfilename = outfilename if (not self.outfilename): self.outfilename = self.filename if (not self.filename): # No loading; create an empty file. self.file = None self.formchunk = None self.changed = True chunk = BlorbChunk(self, b'RIdx', -1, 4) chunk.literaldata = struct.pack('>I', 0) self.add_chunk(chunk, None, None, 0) return self.changed = False self.file = open(filename, 'rb') formchunk = Chunk(self.file) self.formchunk = formchunk if (formchunk.getname() != b'FORM'): raise Exception('This does not appear to be a Blorb file.') formtype = formchunk.read(4) if (formtype != b'IFRS'): raise Exception('This does not appear to be a Blorb file.') formlen = formchunk.getsize() while formchunk.tell() < formlen: chunk = Chunk(formchunk) start = formchunk.tell() size = chunk.getsize() formtype = None if chunk.getname() == b'FORM': formtype = chunk.read(4) subchunk = BlorbChunk(self, chunk.getname(), start, size, formtype) self.chunks.append(subchunk) chunk.skip() chunk.close() for chunk in self.chunks: self.chunkatpos[chunk.start] = chunk dict_append(self.chunkmap, chunk.type, chunk) # Sanity checks. Also get the usage list. ls = self.chunkmap.get(b'RIdx') if (not ls): raise Exception('No resource index chunk!') elif (len(ls) != 1): print('Warning: too many resource index chunks!') else: chunk = ls[0] if (self.chunks[0] is not chunk): print('Warning: resource index chunk is not first!') dat = chunk.data() numres = struct.unpack('>I', dat[0:4])[0] if (numres*12+4 != chunk.len): print('Warning: resource index chunk has wrong size!') for ix in range(numres): subdat = dat[4+ix*12 : 16+ix*12] typ = struct.unpack('>4c', subdat[0:4]) typ = b''.join(typ) num = struct.unpack('>I', subdat[4:8])[0] start = struct.unpack('>I', subdat[8:12])[0] subchunk = self.chunkatpos.get(start) if (not subchunk): print('Warning: resource (%s, %d) refers to a nonexistent chunk!' % (typestring(typ), num)) self.usages.append( (typ, num, subchunk) ) self.usagemap[(typ, num)] = subchunk def close(self): if (self.formchunk): self.formchunk.close() self.formchunk = None if (self.file): self.file.close() self.file = None def sanity_check(self): if (len(self.usages) != len(self.usagemap)): print('Warning: internal mismatch (usages)!') if (len(self.chunks) != len(self.chunkatpos)): print('Warning: internal mismatch (chunks)!') def chunk_position(self, chunk): try: return self.chunks.index(chunk) except: return None def save_if_needed(self): if self.changed: try: self.save() except CommandError as ex: print(str(ex)) def canonicalize(self): self.sanity_check() try: indexchunk = self.chunkmap[b'RIdx'][0] except: raise CommandError('There is no index chunk, so this cannot be a legal blorb file.') indexchunk.len = 4 + 12*len(self.usages) pos = 12 for chunk in self.chunks: chunk.savestart = pos pos = pos + 8 + chunk.len if (pos % 2): pos = pos+1 self.usages.sort(key=lambda tup:tup[2].savestart) ls = [] ls.append(struct.pack('>I', len(self.usages))) for (typ, num, chunk) in self.usages: ls.append(typ) ls.append(struct.pack('>II', num, chunk.savestart)) dat = b''.join(ls) if (len(dat) != indexchunk.len): print('Warning: index chunk length does not match!') indexchunk.literaldata = dat def save(self, outfilename=None): if (outfilename): self.outfilename = outfilename if (not self.changed and (self.outfilename == self.filename)): raise CommandError('No changes need saving.') if (not self.outfilename): raise CommandError('No pathname supplied for saving.') if (os.path.exists(self.outfilename) and not opts.force): if (not confirm_input('File %s exists. Rewrite?' % (self.outfilename,))): print('Cancelled.') return self.canonicalize() tmpfilename = self.outfilename + '~TEMP' fl = open(tmpfilename, 'wb') fl.write(b'FORM----IFRS') pos = 12 for chunk in self.chunks: fl.write(chunk.type) fl.write(struct.pack('>I', chunk.len)) pos = pos+8 dat = chunk.data() fl.write(dat) pos = pos+len(dat) if (pos % 2): fl.write(b'\0') pos = pos+1 fl.seek(4) fl.write(struct.pack('>I', pos-8)) fl.close() os_replace(tmpfilename, self.outfilename) print('Wrote file:', self.outfilename) return self.outfilename def delete_chunk(self, delchunk): self.chunks = [ chunk for chunk in self.chunks if (chunk is not delchunk) ] ls = self.chunkmap[delchunk.type] ls = [ chunk for chunk in ls if (chunk is not delchunk) ] if (ls): self.chunkmap[delchunk.type] = ls else: self.chunkmap.pop(delchunk.type) self.chunkatpos.pop(delchunk.start) self.usages = [ tup for tup in self.usages if (tup[2] is not delchunk) ] ls = [ key for (key,val) in self.usagemap.items() if (val is delchunk) ] for key in ls: self.usagemap.pop(key) self.changed = True def add_chunk(self, chunk, use=None, num=None, pos=None): if (pos is None): self.chunks.append(chunk) else: self.chunks.insert(pos, chunk) self.chunkatpos[chunk.start] = chunk dict_append(self.chunkmap, chunk.type, chunk) if (use is not None): self.usages.append( (use, num, chunk) ) self.usagemap[(use,num)] = chunk self.changed = True class CommandError(Exception): pass class BlorbTool: def show_commands(): print('blorbtool commands:') print() print('list -- list all chunks') print('index -- list all resources in the index chunk') print('display -- display contents of all chunks') print('display TYPE -- contents of chunk(s) of that type') print('display USE NUM -- contents of chunk by use and number (e.g., "display Exec 0")') print('export TYPE FILENAME -- export the chunk of that type to a file') print('export USE NUM FILENAME -- export a chunk by use and number') print('import TYPE FILENAME -- import a file as a chunk of that type') print('import USE NUM TYPE FILENAME -- import a file as a resource of that use, number, and type') print('delete TYPE -- delete chunk(s) of that type') print('delete USE NUM -- delete chunk by use and number') print('giload DIRECTORY -- export the Exec and Pict chunks for use with Quixe') print('save -- write out changes') print('reload -- discard changes and reload existing blorb file') show_commands = staticmethod(show_commands) def __init__(self): self.is_interactive = False self.has_quit = False def set_interactive(self, val): self.is_interactive = val def quit_yet(self): return self.has_quit def handle(self, args=None): try: if (self.is_interactive): args = raw_input('>').split() if (not args): return argname = args.pop(0) if (argname in self.aliasmap): argname = self.aliasmap[argname] cmd = getattr(self, 'cmd_'+argname, None) if (not cmd): raise CommandError('Unknown command: ' + argname) return cmd(args) except KeyboardInterrupt: # EOF or interrupt. Pass it on. raise except EOFError: # EOF or interrupt. Pass it on. raise except CommandError as ex: print(str(ex)) except Exception as ex: # Unexpected exception: print it. print(ex.__class__.__name__+':', str(ex)) if (opts.verbose): raise def parse_int(self, val, label=''): if (label): label = label+': ' try: return int(val) except: raise CommandError(label+'integer required') def parse_chunk_type(self, val, label=''): if (label): label = label+': ' if len(val) > 4: raise CommandError(label+'chunk type must be 1-4 characters') return val.ljust(4).encode() aliasmap = { '?':'help', 'q':'quit', 'write':'save', 'restart':'reload', 'restore':'reload' } def cmd_quit(self, args): if (args): raise CommandError('usage: quit') self.has_quit = True def cmd_help(self, args): if (args): raise CommandError('usage: help') self.show_commands() def cmd_list(self, args): if (args): raise CommandError('usage: list') print(len(blorbfile.chunks), 'chunks:') for chunk in blorbfile.chunks: print(' %s' % (chunk.describe(),)) def cmd_index(self, args): if (args): raise CommandError('usage: index') print(len(blorbfile.usages), 'resources:') for (use, num, chunk) in blorbfile.usages: print(' %s %d: %s' % (typestring(use), num, chunk.describe())) def cmd_display(self, args): if (not args): ls = blorbfile.chunks elif (len(args) == 1): typ = self.parse_chunk_type(args[0], 'display') ls = [ chunk for chunk in blorbfile.chunks if chunk.type == typ ] if (not ls): raise CommandError('No chunks of type %s' % (typestring(typ),)) elif (len(args) == 2): use = self.parse_chunk_type(args[0], 'display') num = self.parse_int(args[1], 'display (second argument)') chunk = blorbfile.usagemap.get( (use, num) ) if (not chunk): raise CommandError('No resource with usage %s, number %d' % (typestring(use), num)) ls = [ chunk ] else: raise CommandError('usage: display | display TYPE | display USE NUM') for chunk in ls: chunk.display() def cmd_export(self, args): if (len(args) == 2): typ = self.parse_chunk_type(args[0], 'export') ls = [ chunk for chunk in blorbfile.chunks if chunk.type == typ ] if (not ls): raise CommandError('No chunks of type %s' % (typestring(typ),)) if (len(ls) != 1): raise CommandError('%d chunks of type %s' % (len(ls), typestring(typ),)) chunk = ls[0] elif (len(args) == 3): use = self.parse_chunk_type(args[0], 'export') num = self.parse_int(args[1], 'export (second argument)') chunk = blorbfile.usagemap.get( (use, num) ) if (not chunk): raise CommandError('No resource with usage %s, number %d' % (typestring(use), num)) else: raise CommandError('usage: export TYPE FILENAME | export USE NUM FILENAME') outfilename = args[-1] if (outfilename == blorbfile.filename): raise CommandError('You can\'t export a chunk over the original blorb file!') if (os.path.exists(outfilename) and not opts.force): if (not confirm_input('File %s exists. Overwrite?' % (outfilename,))): print('Cancelled.') return outfl = open(outfilename, 'wb') if (chunk.formtype and chunk.formtype != b'FORM'): # For an AIFF file, we must include the FORM/length header. # (Unless it's an overly nested AIFF.) outfl.write(b'FORM') outfl.write(struct.pack('>I', chunk.len)) outfl.write(chunk.data()) finallen = outfl.tell() outfl.close() print('Wrote %d bytes to %s.' % (finallen, outfilename)) def cmd_import(self, args): origchunk = None if (len(args) == 2): typ = self.parse_chunk_type(args[0], 'import') use = None num = None ls = [ chunk for chunk in blorbfile.chunks if chunk.type == typ ] if (ls): origchunk = ls[0] elif (len(args) == 4): use = self.parse_chunk_type(args[0], 'import') num = self.parse_int(args[1], 'import (second argument)') typ = self.parse_chunk_type(args[2], 'import (third argument)') origchunk = blorbfile.usagemap.get( (use, num) ) else: raise CommandError('usage: import TYPE FILENAME | import USE NUM TYPE FILENAME') infilename = args[-1] if (infilename == blorbfile.filename): raise CommandError('You can\'t import the original blorb file as a chunk!') fl = open(infilename, 'rb') filestart = None formtype = None dat = fl.read(5) if (dat[0:4] == b'FORM' and ord(dat[4]) < 0x20): # This is an AIFF file, and must be embedded filestart = 8 fl.seek(8, 0) formtype = fl.read(4) if (typ != b'FORM'): # We accept the formtype as a synonym here, if the user # got it right. if (typ != formtype): raise CommandError('This IFF file has form type \'%s\', not \'%s\'.' % (formtype, typ)) typ = b'FORM' fl.seek(0, 2) filelen = fl.tell() fl.close() if (filestart): filelen = filelen - 8 fakestart = min(list(blorbfile.chunkatpos.keys()) + [0]) - 1 if origchunk: # Replace existing chunk pos = blorbfile.chunk_position(origchunk) blorbfile.delete_chunk(origchunk) else: pos = None chunk = BlorbChunk(blorbfile, typ, fakestart, filelen) chunk.filedata = infilename if (filestart): chunk.filestart = filestart chunk.formtype = formtype blorbfile.add_chunk(chunk, use, num, pos) if pos is None: print('Added chunk, length %d' % (filelen,)) else: print('Replaced chunk, new length %d' % (filelen,)) def cmd_giload(self, args): prefix = '' if (len(args) == 1): outdirname = args[0] elif (len(args) == 2): outdirname = args[0] prefix = args[1] else: raise CommandError('usage: giload DIRECTORY | giload DIRECTORY PREFIX') if (not (os.path.exists(outdirname) and os.path.isdir(outdirname))): raise CommandError('Not a directory: %s' % (outdirname)) chunk = blorbfile.usagemap.get( (b'Exec', 0) ) if (not chunk): raise CommandError('No resource with usage %s, number %d' % (typestring(use), num)) chunkdat = chunk.data() if (chunk.formtype and chunk.formtype != b'FORM'): chunkdat = b'FORM' + struct.pack('>I', chunk.len) + chunkdat outfl = open(os.path.join(outdirname, 'game.ulx.js'), 'w') chunkdatenc = base64.b64encode(chunkdat).decode() outfl.write('$(document).ready(function() {\n') outfl.write(" GiLoad.load_run(null, '%s', 'base64');\n" % (chunkdatenc,)) outfl.write('});\n') outfl.close() alttexts = {} ls = blorbfile.chunkmap.get(b'RDes') if (ls): chunk = ls[0] alttexts = analyze_resourcedescs(chunk) outfl = open(os.path.join(outdirname, 'resourcemap.js'), 'w') outfl.write('/* resourcemap.js generated by blorbtool.py */\n') outfl.write('StaticImageInfo = {\n') usages = [ (num, chunk) for (use, num, chunk) in blorbfile.usages if (use == b'Pict') ] usages.sort() # on num first = True wholemap = collections.OrderedDict() for (num, chunk) in usages: try: (suffix, size) = analyze_pict(chunk) except Exception as ex: print('Error on Pict chunk %d: %s' % (num, ex)) continue picfilename = 'pict-%d.%s' % (num, suffix) map = collections.OrderedDict() map['image'] = num map['url'] = os.path.join(prefix, picfilename) if (b'Pict', num) in alttexts: map['alttext'] = alttexts.get( (b'Pict',num) ).decode('utf-8') map['width'] = size[0] map['height'] = size[1] wholemap['pict-%d' % (num,)] = map indexdat = json.dumps(map, indent=2) if (first): first = False else: outfl.write(',\n') outfl.write('%d: %s\n' % (num, indexdat)) outfl2 = open(os.path.join(outdirname, picfilename), 'wb') if (chunk.formtype and chunk.formtype != b'FORM'): outfl2.write(b'FORM') outfl2.write(struct.pack('>I', chunk.len)) outfl2.write(chunk.data()) outfl2.close() outfl.write('};\n') outfl.close() outfl = open(os.path.join(outdirname, 'resourcemap.json'), 'w') json.dump(wholemap, outfl, indent=2) outfl.write('\n') outfl.close() print('Wrote Quixe-compatible data to directory "%s".' % (outdirname,)) def cmd_delete(self, args): if (len(args) == 1): typ = self.parse_chunk_type(args[0], 'delete') ls = [ chunk for chunk in blorbfile.chunks if chunk.type == typ ] if (not ls): raise CommandError('No chunks of type %s' % (typestring(typ),)) elif (len(args) == 2): use = self.parse_chunk_type(args[0], 'delete') num = self.parse_int(args[1], 'delete (second argument)') chunk = blorbfile.usagemap.get( (use, num) ) if (not chunk): raise CommandError('No resource with usage %s, number %d' % (typestring(use), num)) ls = [ chunk ] else: raise CommandError('usage: delete TYPE | delete USE NUM') for chunk in ls: blorbfile.delete_chunk(chunk) print('Deleted %d chunk%s' % (len(ls), ('' if len(ls)==1 else 's'))) def cmd_reload(self, args): global blorbfile if (args): raise CommandError('usage: reload') filename = blorbfile.filename blorbfile.close() blorbfile = BlorbFile(filename) print('Reloaded %s.' % (filename,)) def cmd_save(self, args): global blorbfile if (len(args) == 0): outfilename = None elif (len(args) == 1): outfilename = args[0] else: raise CommandError('usage: save | save FILENAME') filename = blorbfile.save(outfilename) if (filename): # Reload, so that the blorbfile's Chunk (and its chunks) # refer to the new file. (The reloaded blorbfile will have # changed == False, too.) blorbfile.close() blorbfile = BlorbFile(filename) def cmd_dump(self, args): print('### chunks:', blorbfile.chunks) print('### chunkmap:', blorbfile.chunkmap) print('### chunkatpos:', blorbfile.chunkatpos) print('### usages:', blorbfile.usages) print('### usagemap:', blorbfile.usagemap) # Some utility functions. def typestring(dat): return "'" + dat.decode() + "'" def bytes_to_intarray(dat): if (bytes is str): # Python 2 return [ ord(val) for val in dat ] else: # Python 3 return [ val for val in dat ] def intarray_to_bytes(arr): if (bytes is str): # Python 2 return b''.join([ chr(val) for val in arr ]) else: # Python 3 return bytes(arr) def analyze_resourcedescs(chunk): res = {} dat = chunk.data() (subdat, dat) = (dat[:4], dat[4:]) count = struct.unpack('>I', subdat)[0] for ix in range(count): if (len(dat) < 12): break (subdat, dat) = (dat[:12], dat[12:]) subls = struct.unpack('>4c2I', subdat) usage = b''.join(subls[0:4]) strlen = subls[-1] num = subls[-2] if (len(dat) < strlen): break (subdat, dat) = (dat[:strlen], dat[strlen:]) res[(usage, num)] = subdat return res def analyze_pict(chunk): if (chunk.type == b'JPEG'): size = parse_jpeg(chunk.data()) return ('jpeg', size) if (chunk.type == b'PNG '): size = parse_png(chunk.data()) return ('png', size) raise Exception('Unrecognized Pict type: %s' % (chunk.type,)) def parse_png(dat): dat = bytes_to_intarray(dat) pos = 0 sig = dat[pos:pos+8] pos += 8 if sig != [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]: raise Exception('PNG signature does not match') while pos < len(dat): clen = (dat[pos] << 24) | (dat[pos+1] << 16) | (dat[pos+2] << 8) | dat[pos+3] pos += 4 ctyp = intarray_to_bytes(dat[pos:pos+4]) pos += 4 #print('Chunk:', ctyp, 'len', clen) if ctyp == b'IHDR': width = (dat[pos] << 24) | (dat[pos+1] << 16) | (dat[pos+2] << 8) | dat[pos+3] pos += 4 height = (dat[pos] << 24) | (dat[pos+1] << 16) | (dat[pos+2] << 8) | dat[pos+3] pos += 4 return (width, height) pos += clen pos += 4 raise Exception('No PNG header block found') def parse_jpeg(dat): dat = bytes_to_intarray(dat) #print('Length:', len(dat)) pos = 0 while pos < len(dat): if dat[pos] != 0xFF: raise Exception('marker is not FF') while dat[pos] == 0xFF: pos += 1 marker = dat[pos] pos += 1 if marker == 0x01 or (marker >= 0xD0 and marker <= 0xD9): #print('FF%02X*' % (marker,)) continue clen = (dat[pos] << 8) | dat[pos+1] #print('FF%02X, len %d' % (marker, clen)) if (marker >= 0xC0 and marker <= 0xCF and marker != 0xC8): if clen <= 7: raise Exception('SOF block is too small') bits = dat[pos+2] height = (dat[pos+3] << 8) | dat[pos+4] width = (dat[pos+5] << 8) | dat[pos+6] return (width, height) pos += clen raise Exception('SOF block not found') # Actual work begins here. if (opts.listcommands): BlorbTool.show_commands() sys.exit(-1) if (not args and not opts.newfile): popt.print_help() sys.exit(-1) filename = None if (args): filename = args.pop(0) if (opts.newfile and not opts.output): opts.output = filename filename = None try: blorbfile = BlorbFile(filename, opts.output) except Exception as ex: print(ex.__class__.__name__+':', str(ex)) if (opts.verbose): raise sys.exit(-1) # If args exist, execute them as a command. If not, loop grabbing and # executing commands until we discover that the user has executed Quit. # (The handler catches all exceptions except KeyboardInterrupt.) try: tool = BlorbTool() if (args): tool.set_interactive(False) tool.handle(args) blorbfile.sanity_check() blorbfile.save_if_needed() else: tool.set_interactive(True) while (not tool.quit_yet()): tool.handle() blorbfile.sanity_check() blorbfile.save_if_needed() print('') except KeyboardInterrupt: print('') except EOFError: print('') blorbfile.close()