def test_write_string(self): w0 = wad.WadFile(self.buff, 'w') w0.writestr('test.cfg', b'bind ALT +strafe') w0.writestr(wad.WadInfo('readme.txt'), 'test') info = wad.WadInfo('bytes') info.file_size = len(b'bytes') info.type = wad.LumpType.LUMP w0.writestr(info, io.BytesIO(b'bytes')) w0.close() self.buff.seek(0) w1 = wad.WadFile(self.buff, 'r') self.assertTrue('test.cfg' in w1.namelist(), 'Cfg file should be in Wad file') self.assertTrue('readme.txt' in w1.namelist(), 'Txt file should be in Wad file') self.assertTrue('bytes' in w1.namelist(), 'Bytes should be in Wad file') self.assertEqual(w1.read('test.cfg'), b'bind ALT +strafe', 'Cfg file content should not change') self.assertEqual(w1.read('readme.txt').decode('ascii'), 'test', 'Txt file content should not change') self.assertEqual(w1.read('bytes'), b'bytes', 'Bytes content should not change') w1.close() self.buff.close()
def test_empty_pak_file(self): with wad.WadFile(self.buff, 'w'): pass self.buff.seek(0) with wad.WadFile(self.buff, 'r') as wad_file: self.assertEqual(len(wad_file.namelist()), 0, 'Wad file should have not entries') self.assertEqual(wad_file.end_of_data, 12, 'Directory should start immediately after header')
def test_zero_byte_file(self): with wad.WadFile(self.buff, 'w') as wad_file: wad_file.writestr('zero.txt', b'') self.buff.seek(0) with wad.WadFile(self.buff) as wad_file: info = wad_file.getinfo('zero.txt') self.assertEqual(info.file_offset, 12, 'File Info offset of test file should be 12') self.assertEqual(info.file_size, 0, 'File Info size of test file should be 0') data = wad_file.read('zero.txt') self.assertEqual(len(data), 0, 'Length of bytes read should be zero.')
def test_context_manager(self): with wad.WadFile('./test_data/test.wad', 'r') as wad_file: self.assertFalse(wad_file.fp.closed, 'File should be open') self.assertEqual(wad_file.mode, 'r', 'File mode should be \'r\'') fp = wad_file.fp wad_file._did_modify = False self.assertTrue(fp.closed, 'File should be closed') self.assertIsNone(wad_file.fp, 'File pointer should be cleaned up')
def test_append(self): f = open('./test_data/test.wad', 'rb') buff = io.BytesIO(f.read(-1)) f.close() wad_file = wad.WadFile(buff, 'a') wad_file.write('./test_data/test.bsp') wad_file.close() buff.seek(0) wad_file = wad.WadFile(buff, 'r') self.assertTrue('test.bsp' in wad_file.namelist(), 'Appended file should be in Wad file') fp = wad_file.fp wad_file.close() self.assertFalse(buff.closed, 'Wad file should not close passed file-like object') buff.close()
def test_write(self): wad_file = wad.WadFile(self.buff, 'w') self.assertFalse(wad_file.fp.closed, 'File should be open') wad_file.write('./test_data/test.mdl') wad_file.write('./test_data/test.bsp', 'e1m1.bsp') self.assertTrue('test.mdl' in wad_file.namelist(), 'Mdl file should be in Wad file') self.assertTrue('e1m1.bsp' in wad_file.namelist(), 'Bsp file should be in Wad file') fp = wad_file.fp wad_file.close() self.assertFalse(fp.closed, 'File should be open') self.assertIsNone(wad_file.fp, 'File pointer should be cleaned up') self.buff.close()
def test_read(self): wad_file = wad.WadFile('./test_data/test.wad', 'r') self.assertFalse(wad_file.fp.closed, 'File should be open') info = wad_file.getinfo('test') self.assertIsNotNone(info, 'FileInfo should not be None') self.assertEqual(info.filename, 'test') self.assertEqual(info.file_size, 5480, 'FileInfo size of test file should be 5480') self.assertEqual(info.file_offset, 12, 'FileInfo offset of test file should be 12') self.assertEqual(info.type, wad.LumpType.MIPTEX, 'FileInfo type of test file should be MIPTEX') file = wad_file.open('test') self.assertIsNotNone(file, 'File should not be None') file.close() fp = wad_file.fp wad_file.close() self.assertTrue(fp.closed, 'File should be closed') self.assertIsNone(wad_file.fp, 'File pointer should be cleaned up')
def make_quake_wad(start, end, progress): bsps = [] miptextures = [] for name in ['pak0.pak', 'pak1.pak']: with PakFile(str(dirs.data / 'id1' / name), 'r') as pak: for item in pak.namelist(): if item.endswith('.bsp'): bsps.append(pak.read(item)) update(progress, start, end, 1, 3) for bsp_data in bsps: with Bsp.open(bsp_data) as bsp: miptextures += [ mip for mip in bsp.miptextures if mip and mip.name not in [n.name for n in miptextures] ] update(progress, start, end, 2, 3) with wad.WadFile(str(dirs.data / 'id1' / 'quake.wad'), mode='w') as wad_file: for miptex in miptextures: if not miptex: continue buff = 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 wad_file.writestr(info, buff) update(progress, start, end, 3, 3)
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(): """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)