Download lib/formats.js from SharpMeow/unroll: direct link, hf CLI and curl.
- Browser
- Download file 17.5 kB
-
https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/formats.js
- Command line
-
hf download hf://spaces/SharpMeow/unroll/lib/formats.js
-
curl -L -o formats.js https://huggingface.co/spaces/SharpMeow/unroll/resolve/main/lib/formats.js
17.5 kB
| // File formats for "Open your own", all decoded in the browser: OBJ and PLY meshes with UVs, | |
| // baseline TIFF images, and zip archives (read and write). No dependencies. | |
| // ------------------------------------------------------------ meshes | |
| // Both parsers return { pos: Float32Array (xyz), uv: Float32Array (uv), index: Uint32Array, vertices, faces }. | |
| // A vertex is a unique (position, uv) pair; polygons are fanned into triangles. | |
| export function parseOBJ(text) { | |
| const P = [], T = [], key = new Map(), pos = [], uv = [], idx = []; | |
| let line = 0; | |
| const vert = (tok) => { | |
| const k = tok; | |
| let v = key.get(k); | |
| if (v !== undefined) return v; | |
| const [a, b] = tok.split('/'); | |
| let pi = parseInt(a, 10), ti = parseInt(b, 10); | |
| if (!Number.isFinite(pi)) throw new Error(`OBJ line ${line}: bad face vertex "${tok}"`); | |
| if (!Number.isFinite(ti)) throw new Error(`OBJ line ${line}: face vertex "${tok}" has no texture coordinate. Every face needs v/vt indices (UVs).`); | |
| pi = pi < 0 ? P.length / 3 + pi : pi - 1; | |
| ti = ti < 0 ? T.length / 2 + ti : ti - 1; | |
| if (pi < 0 || pi * 3 >= P.length) throw new Error(`OBJ line ${line}: vertex index out of range`); | |
| if (ti < 0 || ti * 2 >= T.length) throw new Error(`OBJ line ${line}: texture index out of range`); | |
| v = pos.length / 3; | |
| pos.push(P[pi * 3], P[pi * 3 + 1], P[pi * 3 + 2]); | |
| uv.push(T[ti * 2], T[ti * 2 + 1]); | |
| key.set(k, v); | |
| return v; | |
| }; | |
| let i0 = 0; | |
| while (i0 < text.length) { | |
| let i1 = text.indexOf('\n', i0); | |
| if (i1 < 0) i1 = text.length; | |
| const s = text.slice(i0, i1).trim(); | |
| i0 = i1 + 1; line++; | |
| if (!s || s[0] === '#') continue; | |
| const t = s.split(/\s+/); | |
| if (t[0] === 'v') P.push(+t[1], +t[2], +t[3]); | |
| else if (t[0] === 'vt') T.push(+t[1], +(t[2] ?? 0)); | |
| else if (t[0] === 'f') { | |
| const f = t.slice(1).map(vert); | |
| for (let k = 1; k + 1 < f.length; k++) idx.push(f[0], f[k], f[k + 1]); | |
| } | |
| } | |
| if (!P.length) throw new Error('OBJ has no vertices ("v" lines).'); | |
| if (!T.length) throw new Error('OBJ has no texture coordinates ("vt" lines). The mesh needs UVs to place the image layers.'); | |
| if (!idx.length) throw new Error('OBJ has no faces ("f" lines).'); | |
| return finish(pos, uv, idx); | |
| } | |
| function finish(pos, uv, idx) { | |
| const out = { pos: Float32Array.from(pos), uv: Float32Array.from(uv), index: Uint32Array.from(idx) }; | |
| out.vertices = out.pos.length / 3; out.faces = out.index.length / 3; | |
| for (let i = 0; i < out.pos.length; i++) if (!Number.isFinite(out.pos[i])) throw new Error('The mesh has a vertex with a non-numeric coordinate.'); | |
| for (let i = 0; i < out.uv.length; i++) if (!Number.isFinite(out.uv[i])) throw new Error('The mesh has a non-numeric texture coordinate.'); | |
| return out; | |
| } | |
| const PLY_TYPES = { | |
| char: ['getInt8', 1], int8: ['getInt8', 1], uchar: ['getUint8', 1], uint8: ['getUint8', 1], | |
| short: ['getInt16', 2], int16: ['getInt16', 2], ushort: ['getUint16', 2], uint16: ['getUint16', 2], | |
| int: ['getInt32', 4], int32: ['getInt32', 4], uint: ['getUint32', 4], uint32: ['getUint32', 4], | |
| float: ['getFloat32', 4], float32: ['getFloat32', 4], double: ['getFloat64', 8], float64: ['getFloat64', 8], | |
| }; | |
| export function parsePLY(buf) { | |
| const u8 = new Uint8Array(buf); | |
| const head = new TextDecoder().decode(u8.subarray(0, Math.min(u8.length, 8192))); | |
| const end = head.indexOf('end_header'); | |
| if (!head.startsWith('ply') || end < 0) throw new Error('Not a PLY file (no "ply" header).'); | |
| const hlen = head.indexOf('\n', end) + 1; | |
| const lines = head.slice(0, hlen).split(/\r?\n/).map((s) => s.trim()).filter(Boolean); | |
| let fmt = null; const els = []; | |
| for (const l of lines) { | |
| const t = l.split(/\s+/); | |
| if (t[0] === 'format') fmt = t[1]; | |
| else if (t[0] === 'element') els.push({ name: t[1], n: +t[2], props: [] }); | |
| else if (t[0] === 'property') { | |
| const e = els[els.length - 1]; | |
| if (t[1] === 'list') e.props.push({ list: true, ct: t[2], it: t[3], name: t[4] }); | |
| else e.props.push({ type: t[1], name: t[2] }); | |
| } | |
| } | |
| if (!['ascii', 'binary_little_endian', 'binary_big_endian'].includes(fmt)) throw new Error(`PLY format "${fmt}" is not supported.`); | |
| const V = els.find((e) => e.name === 'vertex'); | |
| if (!V) throw new Error('PLY has no vertex element.'); | |
| const pn = V.props.map((p) => p.name); | |
| const uName = ['u', 's', 'texture_u', 'texture_s'].find((n) => pn.includes(n)); | |
| const vName = ['v', 't', 'texture_v', 'texture_t'].find((n) => pn.includes(n)); | |
| if (!uName || !vName) throw new Error('PLY vertices have no texture coordinates (u, v or s, t). The mesh needs UVs to place the image layers.'); | |
| const pos = new Float32Array(V.n * 3), uv = new Float32Array(V.n * 2), idx = []; | |
| const store = (e, i, rec) => { | |
| if (e === V) { | |
| pos[i * 3] = rec.x; pos[i * 3 + 1] = rec.y; pos[i * 3 + 2] = rec.z; | |
| uv[i * 2] = rec[uName]; uv[i * 2 + 1] = rec[vName]; | |
| } else if (e.name === 'face') { | |
| const f = rec.vertex_indices || rec.vertex_index; | |
| if (!f) throw new Error('PLY faces have no vertex_indices list.'); | |
| for (let k = 1; k + 1 < f.length; k++) idx.push(f[0], f[k], f[k + 1]); | |
| } | |
| }; | |
| if (fmt === 'ascii') { | |
| const body = new TextDecoder().decode(u8.subarray(hlen)).split(/\s+/).filter(Boolean); | |
| let p = 0; | |
| for (const e of els) for (let i = 0; i < e.n; i++) { | |
| const rec = {}; | |
| for (const pr of e.props) { | |
| if (pr.list) { const n = +body[p++]; rec[pr.name] = body.slice(p, p + n).map(Number); p += n; } | |
| else rec[pr.name] = +body[p++]; | |
| } | |
| store(e, i, rec); | |
| } | |
| } else { | |
| const dv = new DataView(buf, 0), le = fmt === 'binary_little_endian'; | |
| let o = hlen; | |
| const rd = (type) => { const [fn, n] = PLY_TYPES[type] || []; if (!fn) throw new Error(`PLY type "${type}" is not supported.`); const v = dv[fn](o, le); o += n; return v; }; | |
| for (const e of els) for (let i = 0; i < e.n; i++) { | |
| const rec = {}; | |
| for (const pr of e.props) { | |
| if (pr.list) { const n = rd(pr.ct); const a = new Array(n); for (let k = 0; k < n; k++) a[k] = rd(pr.it); rec[pr.name] = a; } | |
| else rec[pr.name] = rd(pr.type); | |
| } | |
| if (o > buf.byteLength) throw new Error('PLY file ends early.'); | |
| store(e, i, rec); | |
| } | |
| } | |
| for (const k of idx) if (k >= V.n) throw new Error('PLY face index out of range.'); | |
| if (!idx.length) throw new Error('PLY has no faces.'); | |
| return finish(pos, uv, idx); | |
| } | |
| // ------------------------------------------------------------ TIFF | |
| // Baseline TIFF: 8, 16 or 32 bit (int or float), 1 to 4 samples, strips or tiles, | |
| // no compression, LZW, Deflate or PackBits, horizontal predictor. Returns ImageData (RGBA, 8 bit). | |
| export async function decodeTIFF(buf, maxSide = 8192) { | |
| const dv = new DataView(buf); | |
| const bo = dv.getUint16(0); | |
| if (bo !== 0x4949 && bo !== 0x4d4d) throw new Error('Not a TIFF file.'); | |
| const le = bo === 0x4949; | |
| if (dv.getUint16(2, le) === 43) throw new Error('BigTIFF is not supported; save as a standard TIFF or PNG.'); | |
| const ifd = dv.getUint32(4, le); | |
| const n = dv.getUint16(ifd, le); | |
| const tags = {}; | |
| const SZ = { 1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 6: 1, 7: 1, 8: 2, 9: 4, 10: 8, 11: 4, 12: 8, 16: 8 }; | |
| for (let i = 0; i < n; i++) { | |
| const e = ifd + 2 + i * 12; | |
| const tag = dv.getUint16(e, le), type = dv.getUint16(e + 2, le), cnt = dv.getUint32(e + 4, le); | |
| const size = (SZ[type] || 1) * cnt; | |
| const off = size <= 4 ? e + 8 : dv.getUint32(e + 8, le); | |
| const vals = []; | |
| for (let k = 0; k < Math.min(cnt, 1 << 20); k++) { | |
| if (type === 3) vals.push(dv.getUint16(off + k * 2, le)); | |
| else if (type === 4) vals.push(dv.getUint32(off + k * 4, le)); | |
| else if (type === 1 || type === 7) vals.push(dv.getUint8(off + k)); | |
| else if (type === 16) vals.push(Number(dv.getBigUint64(off + k * 8, le))); | |
| else break; | |
| } | |
| tags[tag] = vals; | |
| } | |
| const W = tags[256]?.[0], H = tags[257]?.[0]; | |
| if (!W || !H) throw new Error('TIFF has no width or height.'); | |
| if (W > maxSide || H > maxSide) throw new Error(`TIFF is ${W} x ${H} px; the limit is ${maxSide} px on a side.`); | |
| const bps = tags[258]?.[0] || 1, spp = tags[277]?.[0] || 1, comp = tags[259]?.[0] || 1; | |
| const pred = tags[317]?.[0] || 1, fmt = tags[339]?.[0] || 1, photo = tags[262]?.[0] ?? 1; | |
| if (![8, 16, 32].includes(bps)) throw new Error(`TIFF with ${bps} bits per sample is not supported (use 8, 16 or 32).`); | |
| if ((tags[284]?.[0] || 1) !== 1 && spp > 1) throw new Error('Planar TIFF is not supported.'); | |
| if (photo === 3) throw new Error('Palette TIFF is not supported.'); | |
| const tiled = !!tags[322]; | |
| const tw = tiled ? tags[322][0] : W, th = tiled ? tags[323][0] : (tags[278]?.[0] || H); | |
| const offs = tiled ? tags[324] : tags[273], cnts = tiled ? tags[325] : tags[279]; | |
| if (!offs || !cnts) throw new Error('TIFF has no image data offsets.'); | |
| const B = bps / 8, rowB = tw * spp * B; | |
| const raw = new Uint8Array(W * H * spp * B); | |
| const across = Math.ceil(W / tw); | |
| for (let c = 0; c < offs.length; c++) { | |
| let chunk = new Uint8Array(buf, offs[c], cnts[c]); | |
| if (comp === 5) chunk = lzw(chunk, rowB * th); | |
| else if (comp === 8 || comp === 32946) chunk = await inflate(chunk, 'deflate'); | |
| else if (comp === 32773) chunk = packbits(chunk, rowB * th); | |
| else if (comp !== 1) throw new Error(`TIFF compression ${comp} is not supported (use none, LZW or Deflate).`); | |
| const x0 = tiled ? (c % across) * tw : 0, y0 = tiled ? Math.floor(c / across) * th : c * th; | |
| for (let y = 0; y < th && y0 + y < H; y++) { | |
| const row = chunk.subarray(y * rowB, (y + 1) * rowB); | |
| if (pred === 2) { | |
| if (B === 1) for (let i = spp; i < row.length; i++) row[i] = (row[i] + row[i - spp]) & 255; | |
| else if (B === 2) { const d = new DataView(row.buffer, row.byteOffset, row.length); for (let i = spp; i < tw * spp; i++) d.setUint16(i * 2, (d.getUint16(i * 2, le) + d.getUint16((i - spp) * 2, le)) & 65535, le); } | |
| } | |
| const w = Math.min(tw, W - x0) * spp * B; | |
| raw.set(row.subarray(0, w), ((y0 + y) * W + x0) * spp * B); | |
| } | |
| } | |
| // to 8-bit RGBA, stretching 16- and 32-bit data to the range it actually uses | |
| const N = W * H, d = new DataView(raw.buffer); | |
| const get = B === 1 ? (i) => raw[i] : B === 2 ? (i) => d.getUint16(i * 2, le) | |
| : fmt === 3 ? (i) => d.getFloat32(i * 4, le) : (i) => d.getUint32(i * 4, le); | |
| let lo = 0, hi = 255; | |
| if (B > 1) { | |
| lo = Infinity; hi = -Infinity; | |
| for (let i = 0; i < N * spp; i += Math.max(1, Math.floor(N * spp / 200000))) { const v = get(i); if (Number.isFinite(v)) { if (v < lo) lo = v; if (v > hi) hi = v; } } | |
| if (!(hi > lo)) { lo = 0; hi = 1; } | |
| } | |
| const k = 255 / (hi - lo); | |
| const out = new Uint8ClampedArray(N * 4); | |
| for (let p = 0; p < N; p++) { | |
| const s = p * spp; | |
| const r = (get(s) - lo) * k; | |
| const g = spp >= 3 ? (get(s + 1) - lo) * k : r, b = spp >= 3 ? (get(s + 2) - lo) * k : r; | |
| const a = spp === 4 ? (get(s + 3) - lo) * k : spp === 2 ? (get(s + 1) - lo) * k : 255; | |
| const inv = photo === 0 ? 255 : 0; | |
| out[p * 4] = Math.abs(inv - r); out[p * 4 + 1] = Math.abs(inv - g); out[p * 4 + 2] = Math.abs(inv - b); out[p * 4 + 3] = a; | |
| } | |
| return new ImageData(out, W, H); | |
| } | |
| function lzw(src, expect) { | |
| const out = new Uint8Array(Math.max(expect, 16)); | |
| let o = 0, bit = 0, size = 9; | |
| let dict = []; | |
| const reset = () => { dict = []; for (let i = 0; i < 256; i++) dict.push([i]); dict.push(null, null); size = 9; }; | |
| const read = () => { | |
| let v = 0; | |
| for (let i = 0; i < size; i++) { const B = src[(bit + i) >> 3]; v = (v << 1) | ((B >> (7 - ((bit + i) & 7))) & 1); } | |
| bit += size; return v; | |
| }; | |
| reset(); | |
| let prev = null; | |
| while ((bit + size) <= src.length * 8) { | |
| const c = read(); | |
| if (c === 257) break; | |
| if (c === 256) { reset(); prev = null; continue; } | |
| let e; | |
| if (c < dict.length && dict[c]) e = dict[c]; | |
| else if (prev) e = prev.concat(prev[0]); | |
| else throw new Error('Damaged LZW data in TIFF.'); | |
| for (const x of e) { if (o < out.length) out[o++] = x; } | |
| if (prev) dict.push(prev.concat(e[0])); | |
| prev = e; | |
| if (dict.length + 1 >= (1 << size) && size < 12) size++; | |
| } | |
| return out; | |
| } | |
| function packbits(src, expect) { | |
| const out = new Uint8Array(expect); let o = 0, i = 0; | |
| while (i < src.length && o < expect) { | |
| const n = (src[i++] << 24) >> 24; | |
| if (n >= 0) { for (let k = 0; k <= n; k++) out[o++] = src[i++]; } | |
| else if (n !== -128) { const v = src[i++]; for (let k = 0; k < 1 - n; k++) out[o++] = v; } | |
| } | |
| return out; | |
| } | |
| async function inflate(bytes, fmt) { | |
| const ds = new DecompressionStream(fmt); | |
| const r = new Response(new Blob([bytes]).stream().pipeThrough(ds)); | |
| return new Uint8Array(await r.arrayBuffer()); | |
| } | |
| // ------------------------------------------------------------ zip | |
| export async function readZip(buf, limits) { | |
| const u8 = new Uint8Array(buf), dv = new DataView(buf); | |
| let eocd = -1; | |
| for (let i = u8.length - 22; i >= Math.max(0, u8.length - 65557); i--) if (dv.getUint32(i, true) === 0x06054b50) { eocd = i; break; } | |
| if (eocd < 0) throw new Error('This is not a zip file, or it is damaged.'); | |
| const count = dv.getUint16(eocd + 10, true); | |
| let p = dv.getUint32(eocd + 16, true); | |
| const files = []; | |
| let total = 0; | |
| for (let n = 0; n < count; n++) { | |
| if (dv.getUint32(p, true) !== 0x02014b50) throw new Error('Damaged zip directory.'); | |
| const method = dv.getUint16(p + 10, true), csize = dv.getUint32(p + 20, true), usize = dv.getUint32(p + 24, true); | |
| const nl = dv.getUint16(p + 28, true), xl = dv.getUint16(p + 30, true), cl = dv.getUint16(p + 32, true); | |
| const flags = dv.getUint16(p + 8, true); | |
| const lho = dv.getUint32(p + 42, true); | |
| const name = new TextDecoder().decode(u8.subarray(p + 46, p + 46 + nl)); | |
| p += 46 + nl + xl + cl; | |
| if (name.endsWith('/')) continue; | |
| if (flags & 1) throw new Error(`${name} in the zip is encrypted.`); | |
| if (usize === 0xffffffff || csize === 0xffffffff) throw new Error('Zip64 archives are not supported.'); | |
| if (limits && usize > limits.maxFileBytes) throw new Error(`${name} is ${(usize / 1e6).toFixed(1)} MB unzipped; the limit per file is ${(limits.maxFileBytes / 1048576).toFixed(0)} MB.`); | |
| total += usize; | |
| if (limits && total > limits.maxTotalBytes) throw new Error(`The zip unpacks to more than ${(limits.maxTotalBytes / 1048576).toFixed(0)} MB.`); | |
| const ln = dv.getUint16(lho + 26, true), lx = dv.getUint16(lho + 28, true); | |
| const data = u8.subarray(lho + 30 + ln + lx, lho + 30 + ln + lx + csize); | |
| let bytes; | |
| if (method === 0) bytes = data.slice(); | |
| else if (method === 8) bytes = await inflate(data, 'deflate-raw'); | |
| else throw new Error(`${name} uses zip compression method ${method}; only stored and deflated files are supported.`); | |
| files.push({ name, blob: new Blob([bytes]), size: bytes.length }); | |
| } | |
| return files; | |
| } | |
| const CRC = (() => { const t = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c >>> 0; } return t; })(); | |
| function crc32(u8) { let c = 0xffffffff; for (let i = 0; i < u8.length; i++) c = CRC[(c ^ u8[i]) & 255] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } | |
| // a stored (uncompressed) zip from [{ name, bytes: Uint8Array }] | |
| export function writeZip(entries) { | |
| const enc = new TextEncoder(); | |
| const parts = [], cen = []; | |
| let off = 0; | |
| for (const e of entries) { | |
| const name = enc.encode(e.name), crc = crc32(e.bytes), n = e.bytes.length; | |
| const h = new DataView(new ArrayBuffer(30)); | |
| h.setUint32(0, 0x04034b50, true); h.setUint16(4, 20, true); h.setUint16(6, 0x0800, true); | |
| h.setUint32(14, crc, true); h.setUint32(18, n, true); h.setUint32(22, n, true); h.setUint16(26, name.length, true); | |
| parts.push(new Uint8Array(h.buffer), name, e.bytes); | |
| const c = new DataView(new ArrayBuffer(46)); | |
| c.setUint32(0, 0x02014b50, true); c.setUint16(4, 20, true); c.setUint16(6, 20, true); c.setUint16(8, 0x0800, true); | |
| c.setUint32(16, crc, true); c.setUint32(20, n, true); c.setUint32(24, n, true); c.setUint16(28, name.length, true); c.setUint32(42, off, true); | |
| cen.push(new Uint8Array(c.buffer), name); | |
| off += 30 + name.length + n; | |
| } | |
| const csz = cen.reduce((a, b) => a + b.length, 0); | |
| const z = new DataView(new ArrayBuffer(22)); | |
| z.setUint32(0, 0x06054b50, true); z.setUint16(8, entries.length, true); z.setUint16(10, entries.length, true); | |
| z.setUint32(12, csz, true); z.setUint32(16, off, true); | |
| return new Blob([...parts, ...cen, new Uint8Array(z.buffer)], { type: 'application/zip' }); | |
| } | |
| // Packed int16 geometry: the little-endian int16 stream split into a low-byte plane and a | |
| // high-byte plane, then gzipped (tools/pack_bin.py). Splitting the planes roughly halves the | |
| // gzip size (3.48 MB to 1.62 MB for PHerc0139); unpacking gives back the exact original bytes. | |
| export function unshuffleInt16(planes) { | |
| const b = new Uint8Array(planes), n = b.length >> 1; | |
| if (b.length & 1) throw new Error('packed int16 data has an odd length'); | |
| const out = new Uint8Array(b.length); | |
| for (let i = 0; i < n; i++) { out[2 * i] = b[i]; out[2 * i + 1] = b[n + i]; } | |
| return out.buffer; | |
| } | |
| export async function gunzip(buf) { | |
| const s = new Blob([buf]).stream().pipeThrough(new DecompressionStream('gzip')); | |
| return new Response(s).arrayBuffer(); | |
| } | |
| export async function unpackInt16(buf) { return unshuffleInt16(await gunzip(buf)); } | |