master
John Lauer export-wiki guide: rewrite for the current publish path (README.html, not an auto-rendered asset), add the three tscirc… ed21612 2mo ago
/**
 * Rewrite a GLB's embedded images as `data:` URIs, and drop the bufferViews
 * they vacated.
 *
 * Why not external files: Babylon's glTF loader still routes a relative image
 * URI through XHR + createObjectURL in a null-origin (sandboxed) document, and
 * the wiki's CSP is `img-src data: https:` with no `blob:`. A data: URI is
 * used directly by the loader, so it is the only embedded-texture form the
 * sandbox accepts.
 *
 * Usage: node glb-datauri-textures.mjs <in.glb> <out.glb>
 */
import fs from "fs"

const [inPath, outPath] = process.argv.slice(2)
const buf = fs.readFileSync(inPath)
if (buf.readUInt32LE(0) !== 0x46546c67) throw new Error("not a GLB")

const jsonLen = buf.readUInt32LE(12)
const json = JSON.parse(buf.slice(20, 20 + jsonLen).toString("utf8"))
let p = 20 + jsonLen
let bin = Buffer.alloc(0)
while (p < buf.length) {
  const len = buf.readUInt32LE(p), type = buf.readUInt32LE(p + 4)
  if (type === 0x004e4942) bin = buf.slice(p + 8, p + 8 + len)
  p += 8 + len + ((4 - (len % 4)) % 4)
}

const drop = new Set()
let n = 0
for (const [i, img] of (json.images || []).entries()) {
  if (img.bufferView === undefined) continue
  const bv = json.bufferViews[img.bufferView]
  const bytes = bin.slice(bv.byteOffset || 0, (bv.byteOffset || 0) + bv.byteLength)
  img.uri = `data:${img.mimeType || "image/png"};base64,${bytes.toString("base64")}`
  drop.add(img.bufferView)
  delete img.bufferView
  delete img.mimeType
  console.log(`  image ${i}: ${(bytes.length / 1e6).toFixed(2)} MB inlined as data: URI`)
  n++
}
if (!n) { console.log("  no embedded images"); process.exit(0) }

// Rebuild BIN without the vacated views, remapping every surviving index.
const keep = json.bufferViews.map((_, i) => i).filter(i => !drop.has(i))
const remap = new Map(keep.map((old, idx) => [old, idx]))
const parts = []
let off = 0
const newViews = keep.map(i => {
  const bv = json.bufferViews[i]
  const bytes = bin.slice(bv.byteOffset || 0, (bv.byteOffset || 0) + bv.byteLength)
  const pad = (4 - (off % 4)) % 4
  if (pad) { parts.push(Buffer.alloc(pad)); off += pad }
  const v = { ...bv, byteOffset: off }
  parts.push(bytes); off += bytes.length
  return v
})
json.bufferViews = newViews
for (const a of json.accessors || [])
  if (a.bufferView !== undefined) a.bufferView = remap.get(a.bufferView)
for (const m of json.meshes || [])
  for (const pr of m.primitives || [])
    if (pr.extensions?.KHR_draco_mesh_compression?.bufferView !== undefined)
      pr.extensions.KHR_draco_mesh_compression.bufferView =
        remap.get(pr.extensions.KHR_draco_mesh_compression.bufferView)
const newBin = Buffer.concat(parts)
json.buffers = [{ byteLength: newBin.length }]

const jsonBuf = Buffer.from(JSON.stringify(json), "utf8")
const jsonChunk = Buffer.concat([jsonBuf, Buffer.alloc((4 - (jsonBuf.length % 4)) % 4, 0x20)])
const binChunk = Buffer.concat([newBin, Buffer.alloc((4 - (newBin.length % 4)) % 4, 0)])
const head = Buffer.alloc(12)
head.writeUInt32LE(0x46546c67, 0); head.writeUInt32LE(2, 4)
head.writeUInt32LE(12 + 8 + jsonChunk.length + 8 + binChunk.length, 8)
const jh = Buffer.alloc(8); jh.writeUInt32LE(jsonChunk.length, 0); jh.writeUInt32LE(0x4e4f534a, 4)
const bh = Buffer.alloc(8); bh.writeUInt32LE(binChunk.length, 0); bh.writeUInt32LE(0x004e4942, 4)
fs.writeFileSync(outPath, Buffer.concat([head, jh, jsonChunk, bh, binChunk]))
console.log(`  ${(buf.length / 1e6).toFixed(2)} MB -> ${(fs.statSync(outPath).size / 1e6).toFixed(2)} MB`)