diablo2-web/scripts/build-d2-ttf.ts

690 lines
21 KiB
TypeScript

/**
* Authentic Diablo II v1.13c TrueType (`.ttf`) WebFont Builder.
*
* Vectorizes the 256-glyph DC6 bitmap font tables from `d2data.mpq`
* (`data\local\font\latin\font42.dc6` + `font42.tbl` for `D2Exocet`, and
* `data\local\font\latin\fontformal12.dc6` + `fontformal12.tbl` for `D2Formal436`)
* into standards-compliant TrueType (`sfnt` 0x00010000) font files with zero
* external network dependencies.
*/
import type { SpriteFrame } from '../src/formats/sprite.ts'
interface Point {
readonly x: number
readonly y: number
}
/**
* Trace closed polygon contours (clockwise outer boundaries, counter-clockwise holes
* in +X right, +Y up coordinates) from a binary foreground mask `mask[y * width + x]`.
*/
export function traceBitmapContours(
mask: Uint8Array,
width: number,
height: number,
baselineY: number,
scale: number,
): Point[][] {
interface Edge {
x0: number
y0: number
x1: number
y1: number
dir: number // 0=up, 1=right, 2=down, 3=left
used: boolean
}
const edgesByStart = new Map<string, Edge[]>()
const allEdges: Edge[] = []
const addEdge = (x0: number, y0: number, x1: number, y1: number, dir: number): void => {
const e: Edge = { x0, y0, x1, y1, dir, used: false }
allEdges.push(e)
const k = `${x0},${y0}`
let list = edgesByStart.get(k)
if (!list) {
list = []
edgesByStart.set(k, list)
}
list.push(e)
}
const get = (x: number, y: number): number => {
if (x < 0 || x >= width || y < 0 || y >= height) return 0
return mask[y * width + x] ?? 0
}
for (let y = 0; y < height; y++) {
const yUp = baselineY - y
for (let x = 0; x < width; x++) {
if (!get(x, y)) continue
if (!get(x - 1, y)) addEdge(x, yUp - 1, x, yUp, 0)
if (!get(x, y - 1)) addEdge(x, yUp, x + 1, yUp, 1)
if (!get(x + 1, y)) addEdge(x + 1, yUp, x + 1, yUp - 1, 2)
if (!get(x, y + 1)) addEdge(x + 1, yUp - 1, x, yUp - 1, 3)
}
}
const contours: Point[][] = []
for (const startEdge of allEdges) {
if (startEdge.used) continue
const rawPts: { x: number; y: number; dir: number }[] = []
let cur: Edge | undefined = startEdge
while (cur && !cur.used) {
cur.used = true
rawPts.push({ x: cur.x0, y: cur.y0, dir: cur.dir })
const candidates = edgesByStart.get(`${cur.x1},${cur.y1}`)
if (!candidates) break
let next: Edge | undefined
const prefDirs = [(cur.dir + 1) % 4, cur.dir, (cur.dir + 3) % 4]
for (const pd of prefDirs) {
next = candidates.find(c => !c.used && c.dir === pd)
if (next) break
}
if (!next) {
next = candidates.find(c => !c.used)
}
cur = next
}
if (rawPts.length < 4) continue
const simplified: Point[] = []
for (let i = 0; i < rawPts.length; i++) {
const prev = rawPts[(i + rawPts.length - 1) % rawPts.length]!
const curr = rawPts[i]!
if (prev.dir !== curr.dir) {
simplified.push({
x: Math.round(curr.x * scale),
y: Math.round(curr.y * scale),
})
}
}
if (simplified.length >= 3) {
contours.push(simplified)
}
}
return contours
}
export interface Dc6FrameLike {
readonly width: number
readonly height: number
readonly indices: Uint8Array
}
export interface BuildTrueTypeFontOptions {
readonly familyName: string
readonly postScriptName: string
readonly frames: readonly Dc6FrameLike[]
readonly tblBytes: Uint8Array
readonly baselineY: number
readonly scale: number
readonly isForeground: (paletteIdx: number) => boolean
readonly minContourAreaPixels?: number
readonly refineMask?: (mask: Uint8Array, frame: Dc6FrameLike) => void
}
function computePolygonSignedArea(pts: readonly Point[]): number {
let area2 = 0
for (let i = 0; i < pts.length; i++) {
const a = pts[i]!
const b = pts[(i + 1) % pts.length]!
area2 += a.x * b.y - b.x * a.y
}
return area2 * 0.5
}
/**
* Build a complete TrueType (`.ttf`) binary buffer from a 256-glyph DC6 sheet and `.tbl` metrics.
*/
export function buildTrueTypeFont(options: BuildTrueTypeFontOptions): Uint8Array {
const {
familyName,
postScriptName,
frames,
tblBytes,
baselineY,
scale,
isForeground,
minContourAreaPixels = 0,
refineMask,
} = options
interface BuiltGlyph {
contours: Point[][]
advanceWidth: number
xMin: number
yMin: number
xMax: number
yMax: number
}
const glyphs: BuiltGlyph[] = []
const unicodeToGlyph = new Map<number, number>()
// Glyph 0: .notdef
glyphs.push({
contours: [
[
{ x: 40, y: 0 },
{ x: 40, y: 600 },
{ x: 440, y: 600 },
{ x: 440, y: 0 },
],
[
{ x: 100, y: 60 },
{ x: 380, y: 60 },
{ x: 380, y: 540 },
{ x: 100, y: 540 },
],
],
advanceWidth: 500,
xMin: 40,
yMin: 0,
xMax: 440,
yMax: 600,
})
const win1252Map: Record<number, number> = {
0x80: 0x20ac,
0x82: 0x201a,
0x83: 0x0192,
0x84: 0x201e,
0x85: 0x2026,
0x86: 0x2020,
0x87: 0x2021,
0x88: 0x02c6,
0x89: 0x2030,
0x8a: 0x0160,
0x8b: 0x2039,
0x8c: 0x0152,
0x8e: 0x017d,
0x91: 0x2018,
0x92: 0x2019,
0x93: 0x201c,
0x94: 0x201d,
0x95: 0x2022,
0x96: 0x2013,
0x97: 0x2014,
0x98: 0x02dc,
0x99: 0x2122,
0x9a: 0x0161,
0x9b: 0x203a,
0x9c: 0x0153,
0x9e: 0x017e,
0x9f: 0x0178,
}
const qRecOff = 12 + 0x3f * 14
const qFrameIdx = qRecOff + 8 < tblBytes.length ? (tblBytes[qRecOff + 8] ?? 0x3f) : 0x3f
const placeholderCandidates = [
frames[qFrameIdx] ?? frames[0x3f],
frames[0x7f],
frames[0x81],
frames[0x8d],
frames[0x8f],
frames[0x90],
frames[0x9d],
].filter((f): f is Dc6FrameLike => Boolean(f && f.width > 0 && f.height > 0))
const isSameFrame = (a: Dc6FrameLike | undefined, b: Dc6FrameLike | undefined): boolean => {
if (!a || !b || a.width === 0 || b.width === 0) return false
if (a.width !== b.width || a.height !== b.height) return false
for (let i = 0; i < a.indices.length; i++) {
if (isForeground(a.indices[i] ?? 0) !== isForeground(b.indices[i] ?? 0)) return false
}
return true
}
const isPlaceholderQuestionMark = (f: Dc6FrameLike | undefined): boolean =>
placeholderCandidates.some(cand => isSameFrame(f, cand))
const dotRecOff = 12 + 0x2e * 14
const dotMetricW = dotRecOff + 3 < tblBytes.length ? (tblBytes[dotRecOff + 3] || 6) : 6
const dotFrameIdx = dotRecOff + 8 < tblBytes.length ? (tblBytes[dotRecOff + 8] ?? 0x2e) : 0x2e
const dotFrame = frames[dotFrameIdx] ?? frames[0x2e]
const dashRecOff = 12 + 0x2d * 14
const dashMetricW = dashRecOff + 3 < tblBytes.length ? (tblBytes[dashRecOff + 3] || 8) : 8
const dashFrameIdx = dashRecOff + 8 < tblBytes.length ? (tblBytes[dashRecOff + 8] ?? 0x2d) : 0x2d
const dashFrame = frames[dashFrameIdx] ?? frames[0x2d]
for (let code = 0x20; code <= 0xff; code++) {
const recOff = 12 + code * 14
let metricWidth = recOff + 3 < tblBytes.length ? (tblBytes[recOff + 3] || 8) : 8
const frameIdx = recOff + 8 < tblBytes.length ? (tblBytes[recOff + 8] ?? code) : code
let frame = frames[frameIdx] ?? frames[code]
let glyphBaselineY = baselineY
let yShiftUnits = 0
if ((code === 0xb7 || code === 0x95) && dotFrame && dotFrame.width > 0 && dotFrame.height > 0) {
frame = dotFrame
metricWidth = dotMetricW
yShiftUnits = Math.round(baselineY * 0.35 * scale)
} else if (code !== 0x3f && code !== 0x20 && code !== 0xa0 && isPlaceholderQuestionMark(frame)) {
if ((code === 0x96 || code === 0x97) && dashFrame && dashFrame.width > 0 && dashFrame.height > 0) {
frame = dashFrame
metricWidth = dashMetricW
} else {
continue
}
}
let contours: Point[][] = []
if (code !== 0x20 && code !== 0xa0 && frame && frame.width > 0 && frame.height > 0) {
const mask = new Uint8Array(frame.width * frame.height)
for (let i = 0; i < mask.length; i++) {
mask[i] = isForeground(frame.indices[i] ?? 0) ? 1 : 0
}
refineMask?.(mask, frame)
contours = traceBitmapContours(mask, frame.width, frame.height, glyphBaselineY, scale)
if (minContourAreaPixels > 0 && contours.length > 1) {
const minAreaUnits = minContourAreaPixels * scale * scale
const filtered = contours.filter(c => Math.abs(computePolygonSignedArea(c)) > minAreaUnits)
if (filtered.length > 0) contours = filtered
}
if (yShiftUnits !== 0) {
contours = contours.map(c => c.map(p => ({ x: p.x, y: p.y + yShiftUnits })))
}
}
let xMin = 0
let yMin = 0
let xMax = 0
let yMax = 0
if (contours.length > 0) {
xMin = Infinity
yMin = Infinity
xMax = -Infinity
yMax = -Infinity
for (const c of contours) {
for (const p of c) {
if (p.x < xMin) xMin = p.x
if (p.y < yMin) yMin = p.y
if (p.x > xMax) xMax = p.x
if (p.y > yMax) yMax = p.y
}
}
}
const advanceWidth = Math.max(
Math.round(metricWidth * scale),
contours.length > 0 ? xMax + Math.round(scale) : Math.round(6 * scale),
)
const gIdx = glyphs.length
glyphs.push({
contours,
advanceWidth,
xMin,
yMin,
xMax,
yMax,
})
if (code < 0x7f || code >= 0xa0) {
unicodeToGlyph.set(code, gIdx)
} else if (win1252Map[code]) {
unicodeToGlyph.set(win1252Map[code]!, gIdx)
}
}
const glyfChunks: Uint8Array[] = []
const locaOffsets: number[] = []
let currentGlyfOffset = 0
let fontXMin = Infinity
let fontYMin = Infinity
let fontXMax = -Infinity
let fontYMax = -Infinity
let maxPoints = 0
let maxContours = 0
let advanceWidthMax = 0
let minLsb = Infinity
let minRsb = Infinity
let xMaxExtent = -Infinity
for (const g of glyphs) {
locaOffsets.push(currentGlyfOffset)
if (g.advanceWidth > advanceWidthMax) advanceWidthMax = g.advanceWidth
if (g.contours.length === 0) {
minLsb = Math.min(minLsb, 0)
minRsb = Math.min(minRsb, g.advanceWidth)
xMaxExtent = Math.max(xMaxExtent, 0)
continue
}
if (g.xMin < fontXMin) fontXMin = g.xMin
if (g.yMin < fontYMin) fontYMin = g.yMin
if (g.xMax > fontXMax) fontXMax = g.xMax
if (g.yMax > fontYMax) fontYMax = g.yMax
if (g.xMin < minLsb) minLsb = g.xMin
const rsb = g.advanceWidth - g.xMax
if (rsb < minRsb) minRsb = rsb
if (g.xMax > xMaxExtent) xMaxExtent = g.xMax
const numContours = g.contours.length
if (numContours > maxContours) maxContours = numContours
const allPts: Point[] = []
const endPts: number[] = []
for (const c of g.contours) {
for (const p of c) allPts.push(p)
endPts.push(allPts.length - 1)
}
if (allPts.length > maxPoints) maxPoints = allPts.length
const rawLen = 10 + numContours * 2 + 2 + allPts.length * 5
const paddedLen = (rawLen + 3) & ~3
const buf = new Uint8Array(paddedLen)
const dv = new DataView(buf.buffer)
dv.setInt16(0, numContours, false)
dv.setInt16(2, g.xMin, false)
dv.setInt16(4, g.yMin, false)
dv.setInt16(6, g.xMax, false)
dv.setInt16(8, g.yMax, false)
let off = 10
for (const ep of endPts) {
dv.setUint16(off, ep, false)
off += 2
}
dv.setUint16(off, 0, false) // instructionLength = 0
off += 2
for (let i = 0; i < allPts.length; i++) {
buf[off++] = 0x01
}
let prevX = 0
for (const pt of allPts) {
dv.setInt16(off, pt.x - prevX, false)
prevX = pt.x
off += 2
}
let prevY = 0
for (const pt of allPts) {
dv.setInt16(off, pt.y - prevY, false)
prevY = pt.y
off += 2
}
glyfChunks.push(buf)
currentGlyfOffset += paddedLen
}
locaOffsets.push(currentGlyfOffset)
const glyfTable = new Uint8Array(currentGlyfOffset)
let gPos = 0
for (const chunk of glyfChunks) {
glyfTable.set(chunk, gPos)
gPos += chunk.length
}
const locaTable = new Uint8Array(locaOffsets.length * 4)
const locaDv = new DataView(locaTable.buffer)
locaOffsets.forEach((o, idx) => locaDv.setUint32(idx * 4, o, false))
const headTable = new Uint8Array(54)
const headDv = new DataView(headTable.buffer)
headDv.setUint32(0, 0x00010000, false)
headDv.setUint32(4, 0x00010000, false)
headDv.setUint32(8, 0, false)
headDv.setUint32(12, 0x5f0f3cf5, false)
headDv.setUint16(16, 0x000b, false)
headDv.setUint16(18, 1024, false)
headDv.setInt16(36, fontXMin, false)
headDv.setInt16(38, fontYMin, false)
headDv.setInt16(40, fontXMax, false)
headDv.setInt16(42, fontYMax, false)
headDv.setUint16(44, 0, false)
headDv.setUint16(46, 8, false)
headDv.setInt16(48, 2, false)
headDv.setInt16(50, 1, false)
headDv.setInt16(52, 0, false)
const ascender = Math.max(832, fontYMax)
const descender = Math.min(-192, fontYMin)
const hheaTable = new Uint8Array(36)
const hheaDv = new DataView(hheaTable.buffer)
hheaDv.setUint32(0, 0x00010000, false)
hheaDv.setInt16(4, ascender, false)
hheaDv.setInt16(6, descender, false)
hheaDv.setInt16(8, 0, false)
hheaDv.setUint16(10, advanceWidthMax, false)
hheaDv.setInt16(12, minLsb, false)
hheaDv.setInt16(14, minRsb, false)
hheaDv.setInt16(16, xMaxExtent, false)
hheaDv.setInt16(18, 1, false)
hheaDv.setInt16(20, 0, false)
hheaDv.setInt16(22, 0, false)
hheaDv.setInt16(32, 0, false)
hheaDv.setUint16(34, glyphs.length, false)
const maxpTable = new Uint8Array(32)
const maxpDv = new DataView(maxpTable.buffer)
maxpDv.setUint32(0, 0x00010000, false)
maxpDv.setUint16(4, glyphs.length, false)
maxpDv.setUint16(6, maxPoints, false)
maxpDv.setUint16(8, maxContours, false)
maxpDv.setUint16(10, 0, false)
maxpDv.setUint16(12, 0, false)
maxpDv.setUint16(14, 2, false)
const hmtxTable = new Uint8Array(glyphs.length * 4)
const hmtxDv = new DataView(hmtxTable.buffer)
glyphs.forEach((g, idx) => {
hmtxDv.setUint16(idx * 4, g.advanceWidth, false)
hmtxDv.setInt16(idx * 4 + 2, g.contours.length > 0 ? g.xMin : 0, false)
})
const os2Table = new Uint8Array(96)
const os2Dv = new DataView(os2Table.buffer)
const avgWidth = Math.round(
glyphs.reduce((acc, g) => acc + g.advanceWidth, 0) / Math.max(1, glyphs.length),
)
os2Dv.setUint16(0, 4, false)
os2Dv.setInt16(2, avgWidth, false)
os2Dv.setUint16(4, 400, false)
os2Dv.setUint16(6, 5, false)
os2Dv.setUint16(8, 0, false)
os2Dv.setInt16(10, 650, false)
os2Dv.setInt16(12, 600, false)
os2Dv.setInt16(14, 0, false)
os2Dv.setInt16(16, 75, false)
os2Dv.setInt16(18, 650, false)
os2Dv.setInt16(20, 600, false)
os2Dv.setInt16(22, 0, false)
os2Dv.setInt16(24, 350, false)
os2Dv.setInt16(26, 50, false)
os2Dv.setInt16(28, 300, false)
os2Dv.setInt16(30, 0, false)
os2Table.set([2, 0, 5, 3, 0, 0, 0, 0, 0, 0], 32)
os2Dv.setUint32(42, 0x00000003, false)
os2Table.set([0x42, 0x4c, 0x49, 0x5a], 58)
os2Dv.setUint16(62, 0x0040, false)
const sortedCodes = [...unicodeToGlyph.keys()].sort((a, b) => a - b)
os2Dv.setUint16(64, sortedCodes[0] ?? 0x0020, false)
os2Dv.setUint16(66, sortedCodes[sortedCodes.length - 1] ?? 0x00ff, false)
os2Dv.setInt16(68, ascender, false)
os2Dv.setInt16(70, descender, false)
os2Dv.setInt16(72, 0, false)
os2Dv.setUint16(74, ascender, false)
os2Dv.setUint16(76, Math.abs(descender), false)
os2Dv.setUint32(78, 0x00000001, false)
os2Dv.setInt16(86, 512, false)
os2Dv.setInt16(88, 704, false)
os2Dv.setUint16(90, 0, false)
os2Dv.setUint16(92, 0x0020, false)
os2Dv.setUint16(94, 1, false)
interface Seg {
start: number
end: number
delta: number
}
const segs: Seg[] = []
for (const code of sortedCodes) {
const gIdx = unicodeToGlyph.get(code)!
const delta = (gIdx - code) & 0xffff
const prev = segs[segs.length - 1]
if (prev && prev.end + 1 === code && prev.delta === delta) {
prev.end = code
} else {
segs.push({ start: code, end: code, delta })
}
}
segs.push({ start: 0xffff, end: 0xffff, delta: 1 })
const segCount = segs.length
const segCountX2 = segCount * 2
const maxPow2 = 1 << Math.floor(Math.log2(segCount))
const searchRangeCmap = maxPow2 * 2
const entrySelectorCmap = Math.floor(Math.log2(segCount))
const rangeShiftCmap = segCountX2 - searchRangeCmap
const subtableLen = 16 + segCount * 8
const cmapTable = new Uint8Array(4 + 8 * 2 + subtableLen)
const cmapDv = new DataView(cmapTable.buffer)
cmapDv.setUint16(0, 0, false)
cmapDv.setUint16(2, 2, false)
cmapDv.setUint16(4, 0, false)
cmapDv.setUint16(6, 3, false)
cmapDv.setUint32(8, 20, false)
cmapDv.setUint16(12, 3, false)
cmapDv.setUint16(14, 1, false)
cmapDv.setUint32(16, 20, false)
let cOff = 20
cmapDv.setUint16(cOff, 4, false)
cmapDv.setUint16(cOff + 2, subtableLen, false)
cmapDv.setUint16(cOff + 4, 0, false)
cmapDv.setUint16(cOff + 6, segCountX2, false)
cmapDv.setUint16(cOff + 8, searchRangeCmap, false)
cmapDv.setUint16(cOff + 10, entrySelectorCmap, false)
cmapDv.setUint16(cOff + 12, rangeShiftCmap, false)
cOff += 14
for (const s of segs) {
cmapDv.setUint16(cOff, s.end, false)
cOff += 2
}
cmapDv.setUint16(cOff, 0, false)
cOff += 2
for (const s of segs) {
cmapDv.setUint16(cOff, s.start, false)
cOff += 2
}
for (const s of segs) {
cmapDv.setUint16(cOff, s.delta, false)
cOff += 2
}
for (let i = 0; i < segs.length; i++) {
cmapDv.setUint16(cOff, 0, false)
cOff += 2
}
const nameRecords: { nameId: number; str: string }[] = [
{ nameId: 1, str: familyName },
{ nameId: 2, str: 'Regular' },
{ nameId: 3, str: `${familyName}-1.13c` },
{ nameId: 4, str: familyName },
{ nameId: 5, str: 'Version 1.13c' },
{ nameId: 6, str: postScriptName },
]
const encodedStrings: Uint8Array[] = nameRecords.map(r => {
const b = new Uint8Array(r.str.length * 2)
const dv = new DataView(b.buffer)
for (let i = 0; i < r.str.length; i++) {
dv.setUint16(i * 2, r.str.charCodeAt(i), false)
}
return b
})
const storageOffset = 6 + nameRecords.length * 12
const totalStrBytes = encodedStrings.reduce((a, b) => a + b.length, 0)
const nameTable = new Uint8Array(storageOffset + totalStrBytes)
const nameDv = new DataView(nameTable.buffer)
nameDv.setUint16(0, 0, false)
nameDv.setUint16(2, nameRecords.length, false)
nameDv.setUint16(4, storageOffset, false)
let strCur = 0
nameRecords.forEach((rec, idx) => {
const strBytes = encodedStrings[idx]!
const rOff = 6 + idx * 12
nameDv.setUint16(rOff, 3, false)
nameDv.setUint16(rOff + 2, 1, false)
nameDv.setUint16(rOff + 4, 0x0409, false)
nameDv.setUint16(rOff + 6, rec.nameId, false)
nameDv.setUint16(rOff + 8, strBytes.length, false)
nameDv.setUint16(rOff + 10, strCur, false)
nameTable.set(strBytes, storageOffset + strCur)
strCur += strBytes.length
})
const postTable = new Uint8Array(32)
const postDv = new DataView(postTable.buffer)
postDv.setUint32(0, 0x00030000, false)
postDv.setInt32(4, 0, false)
postDv.setInt16(8, -100, false)
postDv.setInt16(10, 50, false)
postDv.setUint32(12, 0, false)
const tables: { tag: string; data: Uint8Array }[] = [
{ tag: 'OS/2', data: os2Table },
{ tag: 'cmap', data: cmapTable },
{ tag: 'glyf', data: glyfTable },
{ tag: 'head', data: headTable },
{ tag: 'hhea', data: hheaTable },
{ tag: 'hmtx', data: hmtxTable },
{ tag: 'loca', data: locaTable },
{ tag: 'maxp', data: maxpTable },
{ tag: 'name', data: nameTable },
{ tag: 'post', data: postTable },
].sort((a, b) => (a.tag < b.tag ? -1 : a.tag > b.tag ? 1 : 0))
const calcChecksum = (bytes: Uint8Array): number => {
const paddedLen = (bytes.length + 3) & ~3
const padded = new Uint8Array(paddedLen)
padded.set(bytes)
const dv = new DataView(padded.buffer)
let sum = 0
for (let i = 0; i < paddedLen; i += 4) {
sum = (sum + dv.getUint32(i, false)) >>> 0
}
return sum
}
const numTables = tables.length
const dirSize = 12 + numTables * 16
let totalSize = dirSize
const offsets: number[] = []
for (const t of tables) {
offsets.push(totalSize)
totalSize += (t.data.length + 3) & ~3
}
const out = new Uint8Array(totalSize)
const outDv = new DataView(out.buffer)
outDv.setUint32(0, 0x00010000, false)
outDv.setUint16(4, numTables, false)
outDv.setUint16(6, 128, false)
outDv.setUint16(8, 3, false)
outDv.setUint16(10, 32, false)
let headOffsetInFile = 0
tables.forEach((t, idx) => {
const entryOff = 12 + idx * 16
for (let c = 0; c < 4; c++) {
out[entryOff + c] = t.tag.charCodeAt(c)
}
const chksum = calcChecksum(t.data)
const fileOff = offsets[idx]!
if (t.tag === 'head') headOffsetInFile = fileOff
outDv.setUint32(entryOff + 4, chksum, false)
outDv.setUint32(entryOff + 8, fileOff, false)
outDv.setUint32(entryOff + 12, t.data.length, false)
out.set(t.data, fileOff)
})
const fileSum = calcChecksum(out)
const checkSumAdjustment = (0xb1b0afba - fileSum) >>> 0
outDv.setUint32(headOffsetInFile + 8, checkSumAdjustment, false)
return out
}