Skip to content

Instantly share code, notes, and snippets.

@mattdesl
Last active December 18, 2024 18:22
Show Gist options
  • Save mattdesl/17e86ef59059e1f0c3292ebbcb384b3c to your computer and use it in GitHub Desktop.
Save mattdesl/17e86ef59059e1f0c3292ebbcb384b3c to your computer and use it in GitHub Desktop.
Filigree file encoder and decoder. MIT License, free to use.
/**
* A decoder for the Filigree file format, which are assets stored on IPFS.
* Since this code ended up on-chain, it is written in such a way as to compress well.
* @license MIT
* @author Matt DesLauriers (@mattdesl)
**/
export function decodeStream(buffer, headers, layer, cell) {
let dv = new DataView(buffer);
let byteOffset = 0;
const u8 = () => {
const r = dv.getUint8(byteOffset);
byteOffset += 1;
return r;
};
const bool = () => {
return u8() === 0xff;
};
const u16 = () => {
const r = dv.getUint16(byteOffset);
byteOffset += 2;
return r;
};
const u32 = () => {
const r = dv.getUint32(byteOffset);
byteOffset += 4;
return r;
};
const i16 = () => {
const r = dv.getInt16(byteOffset);
byteOffset += 2;
return r;
};
const i32 = () => {
const r = dv.getInt32(byteOffset);
byteOffset += 4;
return r;
};
const f32 = () => {
const r = dv.getFloat32(byteOffset);
byteOffset += 4;
return r;
};
const f64 = () => {
const r = dv.getFloat64(byteOffset);
byteOffset += 8;
return r;
};
const version = u8();
// random seed state
const seed = [u32(), u32(), u32(), u32()];
const width = f32();
const height = f32();
const margin = f32();
const canvasWidth = f32();
const canvasHeight = f32();
const highlighting = bool();
const highlightLayerIndex = u8();
const highlightColor = [u8(), u8(), u8()];
const paletteCount = u8();
const palette = [];
for (let i = 0; i < paletteCount; i++) {
const rgb = [];
for (let j = 0; j < 3; j++) {
rgb.push(u8());
}
palette.push(rgb);
}
headers(
version,
seed,
width,
height,
margin,
canvasWidth,
canvasHeight,
highlighting,
highlightLayerIndex,
highlightColor,
palette
);
const layerCount = u16();
const layers = [];
for (let i = 0; i < layerCount; i++) {
const columns = u16();
const rows = u16();
const highlight = bool();
const length = f32();
const thickness = f32();
const cellCount = u32();
layer(columns, rows, highlight, length, thickness);
let lastIndex = 0;
if (cellCount > 0) {
lastIndex = u32();
}
for (let j = 0; j < cellCount; j++) {
const forward = u32();
const index = lastIndex + forward;
const column = Math.floor(index % columns);
const row = Math.floor(index / columns);
lastIndex = index;
const colorIndex = highlight ? highlightLayerIndex : u8();
cell(
column,
row,
colorIndex,
columns,
rows,
highlight,
length,
thickness
);
}
layers.push(layer);
}
}
export function decode(buffer) {
// somewhat of an awkward API so that the
// on-chain minified code has no objects with named fields
// which do not minify very well comared to array destructuring
const data = {};
let curLayer;
const layers = [];
const onHeader = (
version,
seed,
width,
height,
margin,
canvasWidth,
canvasHeight,
highlighting,
highlightLayerIndex,
highlightColor,
palette
) => {
Object.assign(data, {
version,
seed,
width,
height,
margin,
canvasWidth,
canvasHeight,
highlighting,
highlightLayerIndex,
highlightColor,
palette,
layers,
});
};
const onLayer = (columns, rows, highlight, length, thickness) => {
curLayer = {
columns,
rows,
highlight,
length,
thickness,
cells: [],
};
layers.push(curLayer);
};
const onCell = (
column,
row,
colorIndex,
columns,
rows,
highlight,
length,
thickness
) => {
curLayer.cells.push({
column,
row,
colorIndex,
});
};
decodeStream(buffer, onHeader, onLayer, onCell);
return data;
}
/**
* An encoder for the Filigree file format, which are assets stored on IPFS.
* Unlike the decoder, this code was not on-chain, so it was written in a more convenient API style.
* @license MIT
* @author Matt DesLauriers (@mattdesl)
**/
function Writer(len = 2048) {
let buffer = new ArrayBuffer(len);
let dv = new DataView(buffer);
let byteOffset = 0;
return {
get view() {
return dv;
},
get buffer() {
return buffer;
},
bytes() {
return buffer.slice(0, byteOffset);
},
get byteOffset() {
return byteOffset;
},
forward(n) {
byteOffset += n;
},
u8(v) {
this.ensureSize(1);
dv.setUint8(byteOffset, v);
byteOffset += 1;
},
u16(v) {
this.ensureSize(2);
dv.setUint16(byteOffset, v);
byteOffset += 2;
},
u32(v) {
this.ensureSize(4);
dv.setUint32(byteOffset, v);
byteOffset += 4;
},
i16(v) {
this.ensureSize(2);
dv.setInt16(byteOffset, v);
byteOffset += 2;
},
i32(v) {
this.ensureSize(4);
dv.setInt32(byteOffset, v);
byteOffset += 4;
},
f64(v) {
this.ensureSize(8);
dv.setFloat64(byteOffset, v);
byteOffset += 8;
},
f32(v) {
this.ensureSize(4);
dv.setFloat32(byteOffset, v);
byteOffset += 4;
},
bool(v) {
return Boolean(v) ? this.u8(0xff) : this.u8(0x00);
},
ensureSize(wordSizeInBytes = 0) {
if (byteOffset + wordSizeInBytes >= buffer.byteLength) {
const newBuf = new ArrayBuffer(buffer.byteLength << 1);
const u8Array = new Uint8Array(newBuf);
u8Array.set(new Uint8Array(buffer));
buffer = newBuf;
dv = new DataView(buffer);
}
},
};
}
export function encode(opts = {}) {
const {
//
layers,
width,
height,
margin,
seed = [0, 1, 2, 3],
palette = [],
} = opts;
const version = 0;
let out = Writer();
const writeRGB = (rgb) => {
for (let i = 0; i < 3; i++) {
out.u8(rgb[i] || 0x00);
}
};
out.u8(version);
// random seed state
out.u32(seed[0]);
out.u32(seed[1]);
out.u32(seed[2]);
out.u32(seed[3]);
out.f32(width);
out.f32(height);
out.f32(margin);
out.f32(opts.canvasWidth);
out.f32(opts.canvasHeight);
out.bool(opts.highlighting);
out.u8(opts.highlightLayerIndex != null ? opts.highlightLayerIndex : 0);
writeRGB(opts.highlightColor || [0, 0, 0]);
out.u8(palette.length);
palette.forEach((rgb) => {
writeRGB(rgb);
});
out.u16(layers.length);
layers.forEach((layer) => {
const highlight = layer.highlight;
const columns = layer.columns;
out.u16(layer.columns);
out.u16(layer.rows);
out.bool(highlight);
out.f32(layer.length);
out.f32(layer.thickness);
out.u32(layer.cells.length);
const sortedCells = layer.cells.slice();
sortedCells.sort((a, b) => {
const i0 = a.column + a.row * columns;
const i1 = b.column + b.row * columns;
return i0 - i1;
});
// write first index
let lastIndex;
if (sortedCells.length > 0) {
const cell0 = sortedCells[0];
const index0 = cell0.column + cell0.row * columns;
if (index0 > 4294967295) {
throw new Error("Index out of u32 range");
}
out.u32(index0);
lastIndex = index0;
}
sortedCells.forEach((cell, i) => {
const index = cell.column + cell.row * columns;
if (index > 4294967295) {
throw new Error("Index out of u32 range");
}
const indexDiffForward = index - lastIndex;
if (indexDiffForward < 0) {
throw new Error(
"expected positive index step, got " + indexDiffForward
);
}
if (i > 0 && indexDiffForward == 0) {
throw new Error(`Expected positive integer for index step`);
}
out.u32(indexDiffForward);
if (!highlight) out.u8(cell.colorIndex);
lastIndex = index;
});
});
return out.bytes();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment