singe/util/embedGlbImages.py
2026-09-12 22:36:00 -05:00

67 lines
2.8 KiB
Python

# Embeds a .glb's externally referenced images into the file itself, which is what Singe's loader
# needs: it reads images from buffer views and will not go looking for a file beside the model.
# Downloaded packs often ship a .glb that still points at a shared texture atlas (Kenney's arcade
# models name "Textures/colormap.png"), and those load untextured until this is run over them.
# packGlb.py does the same job for a .gltf with external buffers; this one leaves the buffer alone
# and only pulls the images in.
# Usage: python3 util/embedGlbImages.py in.glb out.glb [--base DIRECTORY]
import argparse
import json
import os
import struct
import sys
mimeTypes = {'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'ktx2': 'image/ktx2', 'webp': 'image/webp', 'basis': 'image/basis'}
parser = argparse.ArgumentParser()
parser.add_argument('source')
parser.add_argument('output')
parser.add_argument('--base', default=None, help='where the external images live (default: beside the source)')
args = parser.parse_args()
base = args.base if args.base else os.path.dirname(args.source)
with open(args.source, 'rb') as f:
magic, version, length = struct.unpack('<III', f.read(12))
if magic != 0x46546C67:
sys.exit('%s is not a .glb' % args.source)
jsonLength, jsonType = struct.unpack('<II', f.read(8))
gltf = json.loads(f.read(jsonLength).decode('utf-8'))
binary = bytearray()
rest = f.read()
if len(rest) >= 8:
binLength, binType = struct.unpack('<II', rest[:8])
binary = bytearray(rest[8:8 + binLength])
views = gltf.setdefault('bufferViews', [])
embedded = 0
for image in gltf.get('images', []):
uri = image.get('uri')
if uri is None or uri.startswith('data:'):
continue
path = os.path.join(base, uri)
if not os.path.exists(path):
sys.exit('%s references %s, which is not beside it' % (args.source, uri))
with open(path, 'rb') as f:
data = f.read()
while len(binary) % 4:
binary.append(0)
views.append({'buffer': 0, 'byteOffset': len(binary), 'byteLength': len(data)})
binary.extend(data)
image['bufferView'] = len(views) - 1
image['mimeType'] = mimeTypes.get(uri.rsplit('.', 1)[-1].lower(), 'image/png')
del image['uri']
embedded += 1
gltf['buffers'] = [{'byteLength': len(binary)}]
jsonChunk = json.dumps(gltf, separators=(',', ':')).encode('utf-8')
while len(jsonChunk) % 4:
jsonChunk += b' '
while len(binary) % 4:
binary.append(0)
with open(args.output, 'wb') as f:
f.write(struct.pack('<III', 0x46546C67, 2, 12 + 8 + len(jsonChunk) + 8 + len(binary)))
f.write(struct.pack('<II', len(jsonChunk), 0x4E4F534A))
f.write(jsonChunk)
f.write(struct.pack('<II', len(binary), 0x004E4942))
f.write(binary)
print('%s: %d image(s) embedded, %d bytes' % (args.output, embedded, os.path.getsize(args.output)))