def main(): parser = Parser( prog='bsp2svg', description='Create an svg document from the given bsp file.', epilog='example: bsp2svg e1m1.bsp => creates the svg file e1m1.svg') parser.add_argument('file', metavar='file.bsp', action=ResolvePathAction) parser.add_argument('-d', metavar='file.svg', dest='dest', default=os.getcwd(), action=ResolvePathAction, help='svg file to create') parser.add_argument('-i', '--ignore', dest='ignore', metavar='name', nargs='*', default=[], help='texture names to ignore') parser.add_argument('-q', dest='quiet', action='store_true', help='quiet mode') parser.add_argument('-v', '--version', dest='version', action='version', help=argparse.SUPPRESS, version=f'{parser.prog} version {qcli.__version__}') args = parser.parse_args() if not bsp.is_bspfile(args.file): print(f'{parser.prog}: cannot find or open {args.file}', file=sys.stderr) # Validate or create out file if args.dest == os.getcwd(): svg_path = os.path.dirname(args.file) svg_name = f'{os.path.basename(args.file).split(".")[0]}.svg' args.dest = os.path.join(svg_path, svg_name) dest_dir = os.path.dirname(args.dest) or '.' if not os.path.exists(dest_dir): os.makedirs(dest_dir, exist_ok=True) converter.convert(args.file, args.dest, args) sys.exit(0)
def main(): parser = Parser( prog='pak', description='Default action is to add or replace pak files ' 'entries from list.\nIf list is omitted, pak will ' 'use stdin.', epilog='example: pak tex.pak image.png => adds image.png to tex.pak' ) parser.add_argument( 'file', metavar='file.pak', action=ResolvePathAction, help='pak file to create' ) parser.add_argument( 'list', nargs='*', action=ResolvePathAction, default=read_from_stdin() ) parser.add_argument( '-q', dest='quiet', action='store_true', help='quiet mode' ) parser.add_argument( '-v', '--version', dest='version', action='version', help=argparse.SUPPRESS, version='{} version {}'.format(parser.prog, qcli.__version__) ) args = parser.parse_args() if not args.list: parser.error('the following arguments are required: list') dir = os.path.dirname(args.file) or '.' if not os.path.exists(dir): os.makedirs(dir) filemode = 'a' if not os.path.isfile(args.file): filemode = 'w' with pak.PakFile(args.file, filemode) as pak_file: if not args.quiet: print(f'Archive: {os.path.basename(args.file)}') # Process input files for file in args.list: # Walk directories if os.path.isdir(file): for root, dirs, files in os.walk(file): for name in [f for f in files if not f.startswith('.')]: fullpath = os.path.join(root, name) relpath = os.path.relpath(fullpath, os.getcwd()) if not args.quiet: print(f' adding: {relpath}') pak_file.write(relpath) else: relpath = os.path.relpath(file, os.getcwd()) if not args.quiet: print(f' adding: {relpath}') pak_file.write(relpath) sys.exit(0)
def main(): """CLI entrypoint""" parser = Parser( prog='unwad', description= 'Default action is to convert files to png format and extract to xdir.', epilog='example: unwad gfx.wad -d ./out => extract all files to ./out') parser.add_argument('file', metavar='file.wad', action=ResolvePathAction) parser.add_argument('-l', '--list', action='store_true', help='list files') parser.add_argument('-d', metavar='xdir', default=os.getcwd(), dest='dest', action=ResolvePathAction, help='extract files into xdir') parser.add_argument('-q', dest='quiet', action='store_true', help='quiet mode') parser.add_argument('-f', dest='format', default='png', choices=['bmp', 'gif', 'png', 'tga'], help='image format to convert to') parser.add_argument( '-v', '--version', dest='version', action='version', help=argparse.SUPPRESS, version=f'{parser.prog} version {qcli.unwad.__version__}') args = parser.parse_args() archive_name = os.path.basename(args.file) if not wad.is_wadfile(args.file): print(f'{parser.prog}: cannot find or open {args.file}', file=sys.stderr) sys.exit(1) if args.list: with wad.WadFile(args.file) as wad_file: info_list = sorted(wad_file.infolist(), key=lambda i: i.filename) lump_types = { 0: 'NONE', 1: 'LABEL', 64: 'LUMP', 65: 'QTEX', 66: 'QPIC', 67: 'SOUND', 68: 'MIPTEX' } def lump_type(num): if num in lump_types: return lump_types[num] return num headers = ['Length', 'Type', 'Name'] table = [[i.file_size, lump_type(i.type), i.filename] for i in info_list] length = sum([i.file_size for i in info_list]) count = len(info_list) table.append( [length, '', f'{count} file{"s" if count > 1 else ""}']) separator = [] for i in range(len(headers)): t = max(len(str(length)), len(headers[i]) + 2) separator.append('-' * t) table.insert(-1, separator) print(f'Archive: {archive_name}') print(tabulate(table, headers=headers)) sys.exit(0) if not os.path.exists(args.dest): os.makedirs(args.dest) with wad.WadFile(args.file) as wad_file: if not args.quiet: print(f'Archive: {archive_name}') # Flatten out palette palette = [] for p in quake.palette: palette += p for item in wad_file.infolist(): filename = item.filename fullpath = os.path.join(args.dest, filename) fullpath_ext = '{0}.{1}'.format(fullpath, args.format) data = None size = None # Pictures if item.type == wad.LumpType.QPIC: with wad_file.open(filename) as lmp_file: lump = lmp.Lmp.open(lmp_file) size = lump.width, lump.height data = array.array('B', lump.pixels) # Special cases elif item.type == wad.LumpType.MIPTEX: # Console characters if item.file_size == 128 * 128: size = 128, 128 with wad_file.open(filename) as lump: data = lump.read(item.file_size) else: # Miptextures try: with wad_file.open(filename) as mip_file: mip = wad.Miptexture.read(mip_file) data = mip.pixels[:mip.width * mip.height] data = array.array('B', data) size = mip.width, mip.height except: print(f' failed to extract resource: {item.filename}', file=sys.stderr) continue try: # Convert to image file if data is not None and size is not None: img = Image.frombuffer('P', size, data, 'raw', 'P', 0, 1) img.putpalette(palette) img.save(fullpath_ext) if not args.quiet: print(f' extracting: {fullpath_ext}') # Extract as raw file else: wad_file.extract(filename, args.dest) if not args.quiet: print(f' extracting: {fullpath}') except: print(f'{parser.prog}: error: {sys.exc_info()[1]}', file=sys.stderr) sys.exit(0)
def main(): parser = Parser( prog='bsp2wad', description='Default action is to create a wad archive from ' 'miptextures extracted from the given bsp file.' '\nIf list is omitted, pak will use stdin.', epilog='example: bsp2wad e1m1.bsp => creates the wad file e1m1.wad') parser.add_argument('list', nargs='*', action=ResolvePathAction, default=read_from_stdin()) parser.add_argument('-d', metavar='file.wad', dest='dest', default=os.getcwd(), action=ResolvePathAction, help='wad file to create') parser.add_argument('-q', dest='quiet', action='store_true', help='quiet mode') parser.add_argument( '-v', '--version', dest='version', action='version', help=argparse.SUPPRESS, version=f'{parser.prog} version {qcli.bsp2wad.__version__}') args = parser.parse_args() if not args.list: parser.error('the following arguments are required: list') miptextures = [] for file in args.list: if not bsp.is_bspfile(file): print('{0}: cannot find or open {1}'.format(parser.prog, file), file=sys.stderr) continue bsp_file = bsp.Bsp.open(file) miptextures += [ mip for mip in bsp_file.miptextures if mip and mip.name not in [n.name for n in miptextures] ] if args.dest == os.getcwd(): wad_path = os.path.dirname(file) if len(args.list) == 1: wad_name = f'{os.path.basename(file).split(".")[0]}.wad' else: wad_name = 'out.wad' args.dest = os.path.join(wad_path, wad_name) dir = os.path.dirname(args.dest) or '.' if not os.path.exists(dir): os.makedirs(dir) with wad.WadFile(args.dest, mode='w') as wad_file: if not args.quiet: print(f'Archive: {os.path.basename(args.dest)}') for miptex in miptextures: if not miptex: continue buff = io.BytesIO() wad.Miptexture.write(buff, miptex) buff.seek(0) info = wad.WadInfo(miptex.name) info.file_size = 40 + len(miptex.pixels) info.disk_size = info.file_size info.compression = wad.CompressionType.NONE info.type = wad.LumpType.MIPTEX if not args.quiet: print(f' adding: {info.filename}') wad_file.writestr(info, buff) sys.exit(0)
def main(): parser = Parser( prog='image2spr', description='Default action is to convert an image file(s) to an ' 'spr.\nIf image file is omitted, image2spr will use stdin.', epilog='example: image2spr anim.spr anim.gif => converts anim.gif to ' 'anim.spr') parser.add_argument('dest_file', metavar='file.spr', action=ResolvePathAction, help='spr file to create') parser.add_argument('source_files', nargs='*', metavar='file.gif', action=ResolvePathAction, default=read_from_stdin(), help='image source file') parser.add_argument('-t', dest='type', default=0, help='sprite orientation type') parser.add_argument('-q', dest='quiet', action='store_true', help='quiet mode') parser.add_argument('-v', '--version', dest='version', action='version', help=argparse.SUPPRESS, version=f'{parser.prog} version {qcli.__version__}') args = parser.parse_args() # Flatten out palette quake_palette = [channel for rgb in vgio.quake.palette for channel in rgb] # Create palette image for Image.quantize() quake_palette_image = Image.frombytes('P', (16, 16), bytes(quake_palette)) quake_palette_image.putpalette(quake_palette) images = [] # Build a list of source images for source_file in args.source_files: if not os.path.exists(source_file): print(f'{parser.prog}: cannot find or open {source_file}', file=sys.stderr) continue # Open source image source_image = Image.open(source_file) size = source_image.size source_mode = source_image.mode global_transparency = source_image.info.get('transparency') # Decompose the source image frames into a sequence of images try: while True: if source_image.mode != 'P': alpha = source_image.split()[-1] # Set all alpha pixels to a known color source_image = source_image.convert('RGB') mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) transparent_color = tuple(quake_palette[-3:]) source_image.paste(transparent_color, mask) source_image.info['transparency'] = 255 source_image = source_image.quantize( palette=quake_palette_image) source_image.putpalette(bytes(quake_palette)) # Set the current palette's transparent color to Quake's local_transparency = source_image.info.get('transparency') source_palette = source_image.palette.palette source_palette = list( struct.unpack(f'{len(source_palette)}B', source_palette)) if local_transparency: source_palette[local_transparency * 3:local_transparency * 3 + 3] = vgio.quake.palette[-1] if global_transparency and global_transparency != local_transparency: source_palette[global_transparency * 3:global_transparency * 3 + 3] = vgio.quake.palette[-1] source_palette = bytes(source_palette) # Create new image from current frame data = source_image.tobytes() sub_image = Image.frombytes('P', size, data, 'raw', 'P', 0, 1) if local_transparency: sub_image.info['transparency'] = local_transparency sub_image.putpalette(source_palette) # Convert from indexed color to RGB color then quantize to Quake's palette sub_image = sub_image.convert('RGB', dither=None) sub_image = sub_image.quantize(palette=quake_palette_image) sub_image.info['transparency'] = 255 sub_image.putpalette(bytes(quake_palette)) images.append(sub_image) source_image.seek(source_image.tell() + 1) except EOFError: pass if not images: print(f'{parser.prog}: no usable source images given', file=sys.stderr) sys.exit(1) # Normalize image sizes if len(images) > 1: sizes = [image.size for image in images] images_all_same_size = all([size[0] == size for size in sizes]) if not images_all_same_size: resized_images = [] max_width = max([size[0] for size in sizes]) max_height = max([size[1] for size in sizes]) for image in images: resized_image = Image.new('P', (max_width, max_height), 255) resized_image.putpalette(bytes(quake_palette)) top = (max_height - image.size[1]) // 2 left = (max_width - image.size[0]) // 2 top_left = top, left resized_image.paste(image, box=top_left) resized_images.append(resized_image) images = resized_images # Build Quake sprite with spr.Spr.open(args.dest_file, 'w') as spr_file: spr_file.width, spr_file.height = size spr_file.number_of_frames = len(images) spr_file.type = int(args.type) origin = -size[0] // 2, size[1] // 2 for image in images: frame = spr.SpriteFrame() frame.width, frame.height = size frame.origin = origin data = image.tobytes() data = struct.unpack(f'{frame.width * frame.height}B', data) frame.pixels = data spr_file.frames.append(frame) sys.exit(0)
def main(): parser = Parser( prog='unpak', description='Default action is to extract files to xdir.', epilog='example: unpak PAK0.PAK -d ./out => extract all files to ./out' ) parser.add_argument( 'file', metavar='file.pak', action=ResolvePathAction ) parser.add_argument( '-l', '--list', action='store_true', help='list files' ) parser.add_argument( '-d', metavar='xdir', dest='dest', default=os.getcwd(), action=ResolvePathAction, help='extract files into xdir' ) parser.add_argument( '-q', dest='quiet', action='store_true', help='quiet mode' ) parser.add_argument( '-v', '--version', dest='version', action='version', help=argparse.SUPPRESS, version=f'{parser.prog} version {qcli.unpak.__version__}' ) args = parser.parse_args() if not pak.is_pakfile(args.file): print(f'{parser.prog}: cannot find or open {args.file}', file=sys.stderr) sys.exit(1) if args.list: with pak.PakFile(args.file) as pak_file: info_list = sorted(pak_file.infolist(), key=lambda i: i.filename) headers = ['Length', 'Name'] table = [[i.file_size, i.filename] for i in info_list] length = sum([i.file_size for i in info_list]) count = len(info_list) table.append([length, f'{count} file{"s" if count == 1 else ""}']) separator = [] for i in range(len(headers)): t = max(len(str(length)), len(headers[i]) + 2) separator.append('-' * t) table.insert(-1, separator) print(f'Archive: {os.path.basename(args.file)}') print(tabulate(table, headers=headers)) sys.exit(0) with pak.PakFile(args.file) as pak_file: info_list = pak_file.infolist() for item in sorted(info_list, key=lambda i: i.filename): filename = item.filename fullpath = os.path.join(args.dest, filename) if not args.quiet: print(f' extracting: {fullpath}') try: pak_file.extract(filename, args.dest) except: print(f'{parser.prog}: error: {sys.exc_info()[0]}', file=sys.stderr) sys.exit(0)
def main(): # Fix for frozen packages def handleSIGINT(signum, frame): raise KeyboardInterrupt signal.signal(signal.SIGINT, handleSIGINT) parser = Parser( prog='qmount', description= 'Default action is to mount the given pak file as a logical volume.', epilog= 'example: qmount TEST.PAK => mounts TEST.PAK as a logical volume.') parser.add_argument('file', metavar='file.pak', action=ResolvePathAction, help='pak file to mount') parser.add_argument('-f', '--file-browser', dest='open_file_browser', action='store_true', help='opens a file browser once mounted') parser.add_argument('--verbose', dest='verbose', action='store_true', help='verbose mode') parser.add_argument( '-v', '--version', dest='version', action='version', help=argparse.SUPPRESS, version=f'{parser.prog} version {qcli.qmount.__version__}') args = parser.parse_args() dir = os.path.dirname(args.file) or '.' if not os.path.exists(dir): os.makedirs(dir) archive_name = os.path.basename(args.file) context = {'dirty': False} files = {} # If the pak file exists put the contents into the file dictionary if os.path.exists(args.file): with pak.PakFile(args.file) as pak_file: for info in pak_file.infolist(): name = info.filename files[name] = pak_file.read(name) else: context['dirty'] = True temp_directory = platforms.temp_volume(archive_name) # Copy pak file contents into the temporary directory for filename in files: abs_path = os.path.join(temp_directory, filename) dir = os.path.dirname(abs_path) if not os.path.exists(dir): os.makedirs(dir) with open(abs_path, 'wb') as out_file: out_file.write(files[filename]) # Open a native file browser if args.open_file_browser: platforms.open_file_browser(temp_directory) # Start file watching observer = Observer() handler = TempPakFileHandler( context, temp_directory, files, args.verbose, ignore_patterns=['*/.DS_Store', '*/Thumbs.db'], ignore_directories=True) observer.schedule(handler, path=temp_directory, recursive=True) print('Press Ctrl+C to save and quit') observer.start() # Wait for user to terminate try: while True: time.sleep(1) # Detect the deletion of the watched directory. if not os.path.exists(temp_directory): raise KeyboardInterrupt except KeyboardInterrupt: print() try: observer.stop() except: """This is a temporary workaround. Watchdog will raise an exception if the watched media is ejected.""" observer.join() # Write out updated files if context['dirty']: print(f'Updating changes to {archive_name}') with pak.PakFile(args.file, 'w') as pak_file: for filename in files: pak_file.writestr(filename, files[filename]) else: print(f'No changes detected to {archive_name}') # Clean up temp directory platforms.unmount_temp_volume(temp_directory) sys.exit(0)
def main(): parser = Parser( prog='spr2image', description='Default action is to convert a spr file to a gif.', epilog='example: spr2image bubble.spr => convert bubble.spr to bubble.gif' ) parser.add_argument( 'file', metavar='file.spr', action=ResolvePathAction ) parser.add_argument( '-d', metavar='file.gif', dest='dest', default=os.getcwd(), action=ResolvePathAction, help='image file to create' ) parser.add_argument( '-q', dest='quiet', action='store_true', help='quiet mode' ) parser.add_argument( '-v', '--version', dest='version', action='version', help=argparse.SUPPRESS, version=f'{parser.prog} version {qcli.__version__}' ) args = parser.parse_args() # Validate source file if not spr.is_sprfile(args.file): print(f'{parser.prog}: cannot find or open {args.file}', file=sys.stderr) sys.exit(1) # Validate or create out file if args.dest == os.getcwd(): image_path = os.path.dirname(args.file) image_name = os.path.basename(args.file).split('.')[0] + '.gif' args.dest = os.path.join(image_path, image_name) dest_dir = os.path.dirname(args.dest) or '.' if not os.path.exists(dest_dir): os.makedirs(dest_dir) image_filename = os.path.basename(args.dest) image_extension = image_filename.split('.')[-1] with spr.Spr.open(args.file) as spr_file: if not args.quiet: print(f'Converting: {os.path.basename(args.file)}') # Flatten out palette palette = [channel for rgb in vgio.quake.palette for channel in rgb] # Default frame animation is 10 frames per second default_duration = 10 / 60 * 1000 # Build a sequence of images from spr frames images = [] for frame in spr_file.frames: if frame.type == spr.SINGLE: size = frame.width, frame.height data = array.array('B', frame.pixels) img = Image.frombuffer('P', size, data, 'raw', 'P', 0, 1) img.putpalette(palette) images.append(img) else: print(f'{parser.prog}: frame groups are not supported', file=sys.stderr) sys.exit(1) # Save as gif if image_extension.upper() == 'GIF': first_frame = images[0] first_frame.putpalette(palette) remaining_frames = images[1:] first_frame.save( args.dest, save_all=True, append_images=remaining_frames, duration=default_duration, loop=0, optimize=False, #transparency=255, palette=palette ) else: image_directory = os.path.dirname(args.dest) image_name = image_filename.split('.')[0] for image_index, image in enumerate(images): filename = '{}_{}.{}'.format(image_name, image_index, image_extension) image.save( os.path.join(image_directory, filename), optimize=False, #transparency=255, palette=palette ) sys.exit(0)
def main(): """CLI entrypoint""" # Create and configure argument parser parser = Parser( prog='wad', description='Default action is to add or replace wad file entries from' ' list.\nIf list is omitted, wad will use stdin.', formatter_class=argparse.RawTextHelpFormatter, epilog='example:\n wad tex.wad image.png => adds image.png to tex.wad' ) parser.add_argument('file', metavar='file.wad', action=ResolvePathAction, help='wad file to add entries to') parser.add_argument('list', nargs='*', action=ResolvePathAction, default=read_from_stdin()) parser.add_argument('-t', dest='type', default='MIPTEX', choices=['LUMP', 'QPIC', 'MIPTEX'], help='list data type [default: MIPTEX]') parser.add_argument('-q', dest='quiet', action='store_true', help='quiet mode') parser.add_argument('-v', '--version', dest='version', action='version', version='{} version {}'.format(parser.prog, qcli.__version__)) # Parse the arguments args = parser.parse_args() if not args.list: parser.error('the following arguments are required: list') if args.quiet: def log(message): pass else: def log(message): print(message) # Ensure directory structure dir = os.path.dirname(args.file) or '.' os.makedirs(dir, exist_ok=True) filemode = 'a' if not os.path.isfile(args.file): filemode = 'w' with wad.WadFile(args.file, filemode) as wad_file: log(f'Archive: {os.path.basename(args.file)}') # Flatten out palette palette = [] for p in quake.palette: palette += p # Create palette image for Image.quantize() palette_image = Image.frombytes('P', (16, 16), bytes(palette)) palette_image.putpalette(palette) # Process input files for file in args.list: if args.type == 'LUMP': log(f' adding: {file}') wad_file.write(file) elif args.type == 'QPIC': img = Image.open(file).convert(mode='RGB') img = img.quantize(palette=palette_image) pixels = img.tobytes() name = os.path.basename(file).split('.')[0] qpic = lmp.Lmp() qpic.width = img.width qpic.height = img.height qpic.pixels = pixels buff = io.BytesIO() lmp.Lmp.write(buff, qpic) file_size = buff.tell() buff.seek(0) info = wad.WadInfo(name) info.file_size = file_size info.disk_size = info.file_size info.compression = wad.CompressionType.NONE info.type = wad.LumpType.QPIC log(f' adding: {file}') wad_file.writestr(info, buff) else: try: img = Image.open(file).convert(mode='RGB') img = img.quantize(palette=palette_image) name = os.path.basename(file).split('.')[0] mip = wad.Miptexture() mip.name = name mip.width = img.width mip.height = img.height mip.offsets = [40] mip.pixels = [] # Build mip maps for i in range(4): resized_image = img.resize( (img.width // pow(2, i), img.height // pow(2, i))) data = resized_image.tobytes() mip.pixels += struct.unpack(f'<{len(data)}B', data) if i < 3: mip.offsets += [mip.offsets[-1] + len(data)] buff = io.BytesIO() wad.Miptexture.write(buff, mip) buff.seek(0) info = wad.WadInfo(name) info.file_size = 40 + len(mip.pixels) info.disk_size = info.file_size info.compression = wad.CompressionType.NONE info.type = wad.LumpType.MIPTEX log(f' adding: {file}') wad_file.writestr(info, buff) except: parser.error(sys.exc_info()[1]) sys.exit(0)