Skip to content

Instantly share code, notes, and snippets.

@CatTrinket
Last active June 20, 2026 15:08
Show Gist options
  • Select an option

  • Save CatTrinket/46091482961dcbff84f9d2f2348cea87 to your computer and use it in GitHub Desktop.

Select an option

Save CatTrinket/46091482961dcbff84f9d2f2348cea87 to your computer and use it in GitHub Desktop.
Script to rip images from CARDPVR.PAC from Magic: the Gathering for the Dreamcast
import os
import os.path
import struct
import sys
import zlib
import pvr2image
def rip_cards(pac, outdir):
assert pac.read(4) == b'PAC\x00'
(card_count,) = struct.unpack('<L', pac.read(4))
# Includes one extra offset for the end of the last file
offsets = [
offset for (offset,) in
struct.iter_unpack('<L', pac.read(4 * (card_count + 1)))
]
temp_pvr_path = os.path.join(outdir, 'temp.pvr')
temp_png_path = os.path.join(outdir, 'temp_png')
for (i, (start, end)) in enumerate(zip(offsets, offsets[1:])):
pac.seek(start)
assert pac.read(4) == b'NZIP'
(decompressed_length,) = struct.unpack('<L', pac.read(4))
nzip = pac.read(end - start - 8)
pvr = zlib.decompress(nzip, -15)
assert len(pvr) == decompressed_length
with open(temp_pvr_path, 'wb') as temp_pvr:
temp_pvr.write(pvr)
# pvr2image hardcodes \ as dir separator, whereas Linux will just treat
# that as part of the target filename. The following *should* work
# either way, but I've only tested it on Linux.
pvr2image.decode([temp_pvr_path], 'png', temp_png_path, '')
os.rename(
rf'{temp_png_path}\temp.png',
os.path.join(outdir, f'{i}.png')
)
os.remove(temp_pvr_path)
def main():
if len(sys.argv) != 3:
exit(
f'Usage: {sys.argv[0]} CARDPVR.PAC outdir\n'
'Requires pvr2image from PyPI'
)
with open(sys.argv[1], 'rb') as cardpvr_pac:
rip_cards(cardpvr_pac, sys.argv[2])
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment