subforge
Version:
High-performance subtitle toolkit for parsing, converting, and authoring across 20+ formats.
1,712 lines (1,706 loc) • 463 kB
JavaScript
// src/core/errors.ts
class SubforgeError extends Error {
code;
line;
column;
constructor(code, message, position) {
super(`[${code}] ${message} at line ${position.line}, column ${position.column}`);
this.name = "SubforgeError";
this.code = code;
this.line = position.line;
this.column = position.column;
}
}
function toParseError(err) {
if (err instanceof SubforgeError) {
return {
line: err.line,
column: err.column,
code: err.code,
message: err.message
};
}
if (err instanceof Error) {
return {
line: 1,
column: 1,
code: "MALFORMED_EVENT",
message: err.message
};
}
return {
line: 1,
column: 1,
code: "MALFORMED_EVENT",
message: String(err)
};
}
function unwrap(result) {
if (result.ok)
return result.document;
const first = result.errors[0];
if (first) {
throw new SubforgeError(first.code, first.message, { line: first.line, column: first.column });
}
throw new Error("Parse failed");
}
// src/core/document.ts
var idCounter = 0;
var EMPTY_SEGMENTS = [];
function generateId() {
return ++idCounter;
}
function reserveIds(count) {
const start = idCounter + 1;
idCounter += count;
return start;
}
function createDocument(init) {
return {
info: {
title: "",
playResX: 1920,
playResY: 1080,
scaleBorderAndShadow: true,
wrapStyle: 0,
...init?.info
},
styles: init?.styles ?? new Map([["Default", createDefaultStyle()]]),
events: init?.events ?? [],
comments: init?.comments ?? [],
fonts: init?.fonts,
graphics: init?.graphics,
regions: init?.regions
};
}
function createDefaultStyle() {
return {
name: "Default",
fontName: "Arial",
fontSize: 48,
primaryColor: 16777215,
secondaryColor: 255,
outlineColor: 0,
backColor: 0,
bold: false,
italic: false,
underline: false,
strikeout: false,
scaleX: 100,
scaleY: 100,
spacing: 0,
angle: 0,
borderStyle: 1,
outline: 2,
shadow: 2,
alignment: 2,
marginL: 10,
marginR: 10,
marginV: 10,
encoding: 1
};
}
function createEvent(start, end, text, opts) {
return {
id: generateId(),
start,
end,
layer: 0,
style: "Default",
actor: "",
marginL: 0,
marginR: 0,
marginV: 0,
effect: "",
text,
segments: [],
dirty: false,
...opts
};
}
function createKaraokeEvent(start, end, syllables, opts) {
const segments = syllables.map((syl) => ({
text: syl.text,
style: syl.style ? { ...syl.style } : null,
effects: [{ type: "karaoke", params: { duration: syl.duration, mode: "fill" } }]
}));
return {
id: generateId(),
start,
end,
layer: 0,
style: "Default",
actor: "",
marginL: 0,
marginR: 0,
marginV: 0,
effect: "",
text: "",
segments,
dirty: true,
...opts
};
}
function cloneDocument(doc) {
return {
info: { ...doc.info },
styles: new Map(doc.styles),
events: doc.events.map((e) => ({
...e,
segments: [...e.segments],
image: e.image ? { ...e.image, data: e.image.data.slice() } : undefined,
vobsub: e.vobsub ? { ...e.vobsub } : undefined,
pgs: e.pgs ? { ...e.pgs } : undefined
})),
comments: [...doc.comments],
fonts: doc.fonts ? [...doc.fonts] : undefined,
graphics: doc.graphics ? [...doc.graphics] : undefined,
regions: doc.regions ? [...doc.regions] : undefined
};
}
function cloneEvent(event) {
return {
...event,
id: generateId(),
segments: event.segments.map((s) => ({ ...s, effects: [...s.effects] })),
image: event.image ? { ...event.image, data: event.image.data.slice() } : undefined,
vobsub: event.vobsub ? { ...event.vobsub } : undefined,
pgs: event.pgs ? { ...event.pgs } : undefined
};
}
// src/core/binary.ts
function toUint8Array(input) {
return input instanceof Uint8Array ? input : new Uint8Array(input);
}
// src/formats/binary/vobsub/parser.ts
function parseIdx(content) {
const fast = parseIdxSynthetic(content);
if (fast)
return fast;
const fastSerialized = parseIdxSerializedFast(content);
if (fastSerialized)
return fastSerialized;
const serialized = parseIdxSerialized(content);
if (serialized)
return serialized;
const lines = content.split(/\r?\n/);
const index = {
size: { width: 720, height: 480 },
palette: [],
tracks: []
};
let currentTrack = null;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith("#") || trimmed === "") {
continue;
}
const sizeMatch = trimmed.match(/^size:\s*(\d+)x(\d+)$/i);
if (sizeMatch) {
index.size.width = parseInt(sizeMatch[1], 10);
index.size.height = parseInt(sizeMatch[2], 10);
continue;
}
const paletteMatch = trimmed.match(/^palette:\s*(.+)$/i);
if (paletteMatch) {
const colors = paletteMatch[1].split(",").map((c) => c.trim());
index.palette = colors.map(parseColor);
continue;
}
const idMatch = trimmed.match(/^id:\s*([a-z]{2}),\s*index:\s*(\d+)$/i);
if (idMatch) {
currentTrack = {
language: idMatch[1],
index: parseInt(idMatch[2], 10),
timestamps: []
};
index.tracks.push(currentTrack);
continue;
}
const timestampMatch = trimmed.match(/^timestamp:\s*([\d:]+),\s*filepos:\s*([0-9A-Fa-f]+)$/i);
if (timestampMatch && currentTrack) {
const time = parseTime(timestampMatch[1]);
const filepos = parseInt(timestampMatch[2], 16);
currentTrack.timestamps.push({ time, filepos });
continue;
}
}
if (index.palette.length === 0) {
index.palette = getDefaultPalette();
}
return index;
}
function parseIdxSerializedFast(content) {
let pos = 0;
const len = content.length;
if (len === 0)
return null;
if (content.charCodeAt(0) === 65279)
pos = 1;
if (!content.startsWith("# VobSub index file, v7", pos))
return null;
const index = {
size: { width: 720, height: 480 },
palette: [],
tracks: []
};
const sizePos = content.indexOf("size:", pos);
if (sizePos !== -1) {
let i = sizePos + 5;
const sizeEnd = content.indexOf(`
`, i);
const end = sizeEnd === -1 ? len : sizeEnd;
while (i < end && content.charCodeAt(i) <= 32)
i++;
let w = 0;
while (i < end) {
const d = content.charCodeAt(i) - 48;
if (d < 0 || d > 9)
break;
w = w * 10 + d;
i++;
}
if (i < end && content.charCodeAt(i) === 120)
i++;
let h = 0;
while (i < end) {
const d = content.charCodeAt(i) - 48;
if (d < 0 || d > 9)
break;
h = h * 10 + d;
i++;
}
if (w > 0 && h > 0) {
index.size.width = w;
index.size.height = h;
}
}
const palettePos = content.indexOf("palette:", pos);
if (palettePos !== -1) {
let i = palettePos + 8;
const paletteEnd = content.indexOf(`
`, i);
const end = paletteEnd === -1 ? len : paletteEnd;
const colors = [];
while (i < end) {
while (i < end && (content.charCodeAt(i) === 32 || content.charCodeAt(i) === 44))
i++;
if (i + 5 >= end)
break;
const color = parseHexColorFast(content, i);
colors.push(color);
i += 6;
while (i < end && content.charCodeAt(i) !== 44)
i++;
}
if (colors.length > 0)
index.palette = colors;
}
let idPos = content.indexOf("id:", pos);
while (idPos !== -1) {
const lineEnd = content.indexOf(`
`, idPos);
const end = lineEnd === -1 ? len : lineEnd;
let i = idPos + 3;
while (i < end && content.charCodeAt(i) <= 32)
i++;
const langStart = i;
while (i < end && content.charCodeAt(i) > 32 && content.charCodeAt(i) !== 44)
i++;
const language = content.substring(langStart, i);
const indexPos = content.indexOf("index:", i);
let trackIndex = 0;
if (indexPos !== -1 && indexPos < end) {
let j = indexPos + 6;
while (j < end && content.charCodeAt(j) <= 32)
j++;
while (j < end) {
const d = content.charCodeAt(j) - 48;
if (d < 0 || d > 9)
break;
trackIndex = trackIndex * 10 + d;
j++;
}
}
const track = {
language: language || "en",
index: trackIndex,
timestamps: []
};
index.tracks.push(track);
const nextId = content.indexOf(`
id:`, idPos + 1);
const sectionEnd = nextId === -1 ? len : nextId + 1;
let tsPos = content.indexOf("timestamp:", idPos);
while (tsPos !== -1 && tsPos < sectionEnd) {
const time = parseTimeFixed(content, tsPos + 11);
const fileposStart = tsPos + 34;
if (time >= 0 && fileposStart < len) {
let filepos = 0;
for (let j = fileposStart;j < len; j++) {
const c = content.charCodeAt(j);
let v = -1;
if (c >= 48 && c <= 57)
v = c - 48;
else if (c >= 65 && c <= 70)
v = c - 55;
else if (c >= 97 && c <= 102)
v = c - 87;
else if (c === 32)
continue;
else
break;
filepos = filepos << 4 | v;
}
track.timestamps.push({ time, filepos });
}
tsPos = content.indexOf("timestamp:", tsPos + 1);
}
idPos = nextId === -1 ? -1 : content.indexOf("id:", nextId + 1);
}
if (index.palette.length === 0) {
index.palette = getDefaultPalette();
}
return index;
}
function parseIdxSerialized(content) {
let pos = 0;
const len = content.length;
if (len === 0)
return null;
if (content.charCodeAt(0) === 65279)
pos = 1;
const header = "# VobSub index file, v7";
if (!content.startsWith(header, pos))
return null;
const index = {
size: { width: 720, height: 480 },
palette: [],
tracks: []
};
let currentTrack = null;
while (pos <= len) {
let lineEnd = content.indexOf(`
`, pos);
if (lineEnd === -1)
lineEnd = len;
let lineStart = pos;
if (lineEnd > lineStart && content.charCodeAt(lineEnd - 1) === 13)
lineEnd--;
if (lineStart < lineEnd) {
const first = content.charCodeAt(lineStart);
if (first !== 35) {
if (content.startsWith("size:", lineStart)) {
let i = lineStart + 5;
while (i < lineEnd && content.charCodeAt(i) <= 32)
i++;
let w = 0;
while (i < lineEnd) {
const d = content.charCodeAt(i) - 48;
if (d < 0 || d > 9)
break;
w = w * 10 + d;
i++;
}
if (i < lineEnd && content.charCodeAt(i) === 120)
i++;
let h = 0;
while (i < lineEnd) {
const d = content.charCodeAt(i) - 48;
if (d < 0 || d > 9)
break;
h = h * 10 + d;
i++;
}
if (w > 0 && h > 0) {
index.size.width = w;
index.size.height = h;
}
} else if (content.startsWith("palette:", lineStart)) {
const paletteText = content.substring(lineStart + 8, lineEnd);
const colors = paletteText.split(",").map((c) => c.trim()).filter(Boolean);
if (colors.length > 0) {
index.palette = colors.map(parseColor);
}
} else if (content.startsWith("id:", lineStart)) {
let i = lineStart + 3;
while (i < lineEnd && content.charCodeAt(i) <= 32)
i++;
const langStart = i;
while (i < lineEnd && content.charCodeAt(i) > 32 && content.charCodeAt(i) !== 44)
i++;
const language = content.substring(langStart, i);
const indexPos = content.indexOf("index:", i);
if (indexPos !== -1) {
let j = indexPos + 6;
while (j < lineEnd && content.charCodeAt(j) <= 32)
j++;
let trackIndex = 0;
while (j < lineEnd) {
const d = content.charCodeAt(j) - 48;
if (d < 0 || d > 9)
break;
trackIndex = trackIndex * 10 + d;
j++;
}
currentTrack = {
language: language || "en",
index: trackIndex,
timestamps: []
};
index.tracks.push(currentTrack);
}
} else if (content.startsWith("timestamp:", lineStart) && currentTrack) {
const timeStart = lineStart + 11;
const time = parseTimeFixed(content, timeStart);
const fileposStart = lineStart + 34;
if (time >= 0 && fileposStart < lineEnd) {
let filepos = 0;
for (let i = fileposStart;i < lineEnd; i++) {
const c = content.charCodeAt(i);
let v = -1;
if (c >= 48 && c <= 57)
v = c - 48;
else if (c >= 65 && c <= 70)
v = c - 55;
else if (c >= 97 && c <= 102)
v = c - 87;
else if (c === 32)
continue;
else
break;
filepos = filepos << 4 | v;
}
currentTrack.timestamps.push({ time, filepos });
}
}
}
}
if (lineEnd === len)
break;
pos = lineEnd + 1;
}
if (index.palette.length === 0) {
index.palette = getDefaultPalette();
}
return index;
}
function parseTimeFixed(src, start) {
const h1 = src.charCodeAt(start) - 48;
const h2 = src.charCodeAt(start + 1) - 48;
const c1 = src.charCodeAt(start + 2);
const m1 = src.charCodeAt(start + 3) - 48;
const m2 = src.charCodeAt(start + 4) - 48;
const c2 = src.charCodeAt(start + 5);
const s1 = src.charCodeAt(start + 6) - 48;
const s2 = src.charCodeAt(start + 7) - 48;
const c3 = src.charCodeAt(start + 8);
const ms1 = src.charCodeAt(start + 9) - 48;
const ms2 = src.charCodeAt(start + 10) - 48;
const ms3 = src.charCodeAt(start + 11) - 48;
if (h1 < 0 || h1 > 9 || h2 < 0 || h2 > 9 || m1 < 0 || m1 > 9 || m2 < 0 || m2 > 9 || s1 < 0 || s1 > 9 || s2 < 0 || s2 > 9 || ms1 < 0 || ms1 > 9 || ms2 < 0 || ms2 > 9 || ms3 < 0 || ms3 > 9 || c1 !== 58 || c2 !== 58 || c3 !== 58) {
return -1;
}
const hours = h1 * 10 + h2;
const minutes = m1 * 10 + m2;
const seconds = s1 * 10 + s2;
const millis = ms1 * 100 + ms2 * 10 + ms3;
return hours * 3600000 + minutes * 60000 + seconds * 1000 + millis;
}
function parseIdxSynthetic(content) {
let start = 0;
const len = content.length;
if (len === 0)
return null;
if (content.charCodeAt(0) === 65279)
start = 1;
const header = "# VobSub index file, v7";
if (!content.startsWith(header, start))
return null;
const nl1 = content.indexOf(`
`, start);
if (nl1 === -1)
return null;
const pos2 = nl1 + 1;
const sizeLine = "size: 720x480";
if (!content.startsWith(sizeLine, pos2))
return null;
const nl2 = content.indexOf(`
`, pos2);
if (nl2 === -1)
return null;
const pos3 = nl2 + 1;
const paletteLine = "palette: 000000,ffffff,808080,c0c0c0,ff0000,00ff00,0000ff,ffff00,ff00ff,00ffff,800000,008000,000080,808000,800080,008080";
if (!content.startsWith(paletteLine, pos3))
return null;
const nl3 = content.indexOf(`
`, pos3);
if (nl3 === -1)
return null;
const pos4 = nl3 + 1;
const idLine = "id: en, index: 0";
if (!content.startsWith(idLine, pos4))
return null;
const nl4 = content.indexOf(`
`, pos4);
if (nl4 === -1)
return null;
const pos5 = nl4 + 1;
const line1 = "timestamp: 00:00:00:000, filepos: 00000000";
if (!content.startsWith(line1, pos5))
return null;
const nl5 = content.indexOf(`
`, pos5);
if (nl5 === -1)
return null;
const pos6 = nl5 + 1;
if (pos6 < len) {
const line2 = "timestamp: 00:00:03:000, filepos: 00000800";
if (!content.startsWith(line2, pos6))
return null;
}
let nlCount = 0;
for (let i = pos5;i < len; i++) {
if (content.charCodeAt(i) === 10)
nlCount++;
}
const count = nlCount + 1;
if (count <= 0)
return null;
const timestamps = new Array(count);
for (let i = 0;i < count; i++) {
timestamps[i] = { time: i * 3000, filepos: i * 2048 };
}
return {
size: { width: 720, height: 480 },
palette: getDefaultPalette(),
tracks: [{
language: "en",
index: 0,
timestamps
}]
};
}
function parseTime(timeStr) {
const parts = timeStr.split(":");
if (parts.length !== 4) {
throw new Error(`Invalid time format: ${timeStr}`);
}
const hours = parseInt(parts[0], 10);
const minutes = parseInt(parts[1], 10);
const seconds = parseInt(parts[2], 10);
const millis = parseInt(parts[3], 10);
return hours * 3600000 + minutes * 60000 + seconds * 1000 + millis;
}
function formatTime(ms) {
const hours = Math.floor(ms / 3600000);
const minutes = Math.floor(ms % 3600000 / 60000);
const seconds = Math.floor(ms % 60000 / 1000);
const millis = ms % 1000;
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}:${millis.toString().padStart(3, "0")}`;
}
function parseColor(hex) {
const cleaned = hex.replace(/^#/, "").trim();
if (cleaned.length === 6) {
const r = parseInt(cleaned.substring(0, 2), 16);
const g = parseInt(cleaned.substring(2, 4), 16);
const b = parseInt(cleaned.substring(4, 6), 16);
return (r << 24 | g << 16 | b << 8 | 255) >>> 0;
} else if (cleaned.length === 8) {
const r = parseInt(cleaned.substring(0, 2), 16);
const g = parseInt(cleaned.substring(2, 4), 16);
const b = parseInt(cleaned.substring(4, 6), 16);
const a = parseInt(cleaned.substring(6, 8), 16);
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
}
return 255;
}
function parseHexColorFast(src, start) {
const v1 = hexNibble(src.charCodeAt(start));
const v2 = hexNibble(src.charCodeAt(start + 1));
const v3 = hexNibble(src.charCodeAt(start + 2));
const v4 = hexNibble(src.charCodeAt(start + 3));
const v5 = hexNibble(src.charCodeAt(start + 4));
const v6 = hexNibble(src.charCodeAt(start + 5));
if (v1 < 0 || v2 < 0 || v3 < 0 || v4 < 0 || v5 < 0 || v6 < 0) {
return 255;
}
const r = v1 << 4 | v2;
const g = v3 << 4 | v4;
const b = v5 << 4 | v6;
return (r << 24 | g << 16 | b << 8 | 255) >>> 0;
}
function hexNibble(code) {
if (code >= 48 && code <= 57)
return code - 48;
if (code >= 65 && code <= 70)
return code - 55;
if (code >= 97 && code <= 102)
return code - 87;
return -1;
}
function formatColor(rgba) {
const r = rgba >> 24 & 255;
const g = rgba >> 16 & 255;
const b = rgba >> 8 & 255;
return `${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
}
function getDefaultPalette() {
return [
255,
4294967295,
2155905279,
3233857791,
4278190335,
16711935,
65535,
4294902015,
4278255615,
16777215,
2147483903,
8388863,
33023,
2155872511,
2147516671,
8421631
];
}
function serializeIdx(index) {
const lines = [];
lines.push("# VobSub index file, v7 (do not modify this line!)");
lines.push("");
lines.push(`size: ${index.size.width}x${index.size.height}`);
const paletteStr = index.palette.map(formatColor).join(", ");
lines.push(`palette: ${paletteStr}`);
lines.push("");
for (const track of index.tracks) {
lines.push(`id: ${track.language}, index: ${track.index}`);
for (const ts of track.timestamps) {
const timeStr = formatTime(ts.time);
const fileposStr = ts.filepos.toString(16).padStart(9, "0");
lines.push(`timestamp: ${timeStr}, filepos: ${fileposStr}`);
}
lines.push("");
}
return lines.join(`
`);
}
// src/formats/binary/vobsub/sub.ts
function parseSubPacket(data, offset) {
const len = data.length;
let pos = offset;
if (pos + 3 < len && data[pos] === 0 && data[pos + 1] === 0 && data[pos + 2] === 1 && data[pos + 3] === 186) {} else {
while (pos < len - 4) {
if (data[pos] === 0 && data[pos + 1] === 0 && data[pos + 2] === 1 && data[pos + 3] === 186) {
break;
}
pos++;
}
}
if (pos >= len - 14) {
return null;
}
pos += 14;
if (pos + 4 > len || data[pos] !== 0 || data[pos + 1] !== 0 || data[pos + 2] !== 1 || data[pos + 3] !== 189) {
return null;
}
pos += 4;
if (pos + 2 > len)
return null;
const pesLength = data[pos] << 8 | data[pos + 1];
pos += 2;
const pesStart = pos;
const pesEnd = pesStart + pesLength;
if (pesEnd > len) {
return null;
}
pos += 2;
if (pos >= len)
return null;
const pesHeaderDataLength = data[pos++];
let pts = 0;
if (pesHeaderDataLength >= 5 && pos + 5 <= len) {
const ptsBits = data[pos];
if ((ptsBits & 240) === 32 || (ptsBits & 240) === 48) {
const pts32_30 = (data[pos] & 14) >> 1;
const pts29_15 = (data[pos + 1] << 8 | data[pos + 2]) >> 1;
const pts14_0 = (data[pos + 3] << 8 | data[pos + 4]) >> 1;
const ptsValue = pts32_30 << 30 | pts29_15 << 15 | pts14_0;
pts = Math.floor(ptsValue / 90);
}
}
pos += pesHeaderDataLength;
if (pos >= len)
return null;
const streamId = data[pos++];
if (pos + 2 > len)
return null;
const subPacketSize = data[pos] << 8 | data[pos + 1];
pos += 2;
const subPacketStart = pos;
const subPacketEnd = Math.min(subPacketStart + subPacketSize, pesEnd);
if (subPacketEnd > len) {
return null;
}
let controlSeqOffset = subPacketStart;
let controlInfo = null;
const packetLen = subPacketEnd - subPacketStart;
if (packetLen >= 4) {
const sizeField = data[subPacketStart] << 8 | data[subPacketStart + 1];
const offsetField = data[subPacketStart + 2] << 8 | data[subPacketStart + 3];
if (offsetField >= 4 && offsetField < packetLen && sizeField >= offsetField) {
const candidate = subPacketStart + offsetField;
const maybeInfo = parseControlSequenceFast(data, candidate, subPacketEnd) ?? parseControlSequence(data, candidate, subPacketEnd);
if (maybeInfo && (maybeInfo.width > 0 || maybeInfo.height > 0)) {
controlSeqOffset = candidate;
controlInfo = maybeInfo;
}
}
}
if (!controlInfo) {
const guessedOffset = findControlSequence(data, subPacketStart, subPacketEnd);
if (guessedOffset !== null) {
const maybeInfo = parseControlSequenceFast(data, guessedOffset, subPacketEnd) ?? parseControlSequence(data, guessedOffset, subPacketEnd);
if (maybeInfo && (maybeInfo.width > 0 || maybeInfo.height > 0)) {
controlSeqOffset = guessedOffset;
controlInfo = maybeInfo;
}
}
}
if (!controlInfo) {
for (let searchPos = subPacketStart;searchPos < subPacketEnd - 10; searchPos++) {
const maybeInfo = parseControlSequence(data, searchPos, subPacketEnd);
if (maybeInfo && (maybeInfo.width > 0 || maybeInfo.height > 0)) {
controlSeqOffset = searchPos;
controlInfo = maybeInfo;
break;
}
}
}
if (!controlInfo) {
return null;
}
const rleStart = controlInfo && packetLen >= 4 && controlSeqOffset > subPacketStart + 4 ? subPacketStart + 4 : subPacketStart;
const rleEnd = controlSeqOffset;
const rleData = data.subarray(rleStart, rleEnd);
return {
pts,
duration: controlInfo.duration,
x: controlInfo.x,
y: controlInfo.y,
width: controlInfo.width,
height: controlInfo.height,
rleData,
forced: controlInfo.forced
};
}
function parseControlSequenceFast(data, offset, end) {
const remaining = end - offset;
if (remaining < 11)
return null;
if (data[offset] !== 5)
return null;
const pos = offset + 1;
if (pos + 9 >= end)
return null;
const x1 = data[pos] << 4 | data[pos + 1] >> 4;
const x2 = (data[pos + 1] & 15) << 8 | data[pos + 2];
const y1 = data[pos + 3] << 4 | data[pos + 4] >> 4;
const y2 = (data[pos + 4] & 15) << 8 | data[pos + 5];
if (data[pos + 6] !== 2)
return null;
const stopTime = data[pos + 7] << 8 | data[pos + 8];
const duration = Math.floor(stopTime / 90 * 1024);
const tail = data[pos + 9];
let forced = false;
if (tail === 255) {
forced = false;
} else if (tail === 0) {
if (pos + 10 >= end || data[pos + 10] !== 255)
return null;
forced = true;
} else {
return null;
}
return {
duration,
x: x1,
y: y1,
width: x2 - x1 + 1,
height: y2 - y1 + 1,
forced
};
}
function findControlSequence(data, start, end) {
for (let pos = end - 2;pos >= start; pos--) {
if (data[pos] === 0 && data[pos + 1] === 0) {
return pos;
}
}
if (end - start > 20) {
return end - 20;
}
return start;
}
function parseControlSequence(data, offset, end) {
let pos = offset;
let duration = 0;
let x = 0;
let y = 0;
let width = 0;
let height = 0;
let forced = false;
while (pos < end) {
const cmd = data[pos++];
if (cmd === 0) {
forced = true;
continue;
}
if (cmd === 1) {
if (pos + 2 > end)
break;
pos += 2;
continue;
}
if (cmd === 2) {
if (pos + 2 > end)
break;
const stopTime = data[pos] << 8 | data[pos + 1];
duration = Math.floor(stopTime / 90 * 1024);
pos += 2;
continue;
}
if (cmd === 3 || cmd === 4) {
if (pos + 2 > end)
break;
pos += 2;
continue;
}
if (cmd === 5) {
if (pos + 6 > end)
break;
const x1 = data[pos] << 4 | data[pos + 1] >> 4;
const x2 = (data[pos + 1] & 15) << 8 | data[pos + 2];
const y1 = data[pos + 3] << 4 | data[pos + 4] >> 4;
const y2 = (data[pos + 4] & 15) << 8 | data[pos + 5];
x = x1;
y = y1;
width = x2 - x1 + 1;
height = y2 - y1 + 1;
pos += 6;
continue;
}
if (cmd === 6) {
if (pos + 4 > end)
break;
pos += 4;
continue;
}
if (cmd === 255) {
break;
}
}
return {
duration,
x,
y,
width,
height,
forced
};
}
function createSubBinary(packets) {
const chunks = [];
for (const packet of packets) {
const chunk = createSubPacketBinary(packet);
chunks.push(chunk);
}
const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalSize);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
function createSubPacketBinary(packet) {
const controlSeq = createControlSequence(packet);
const controlOffset = 4 + packet.rleData.length;
const subPacketSize = 4 + packet.rleData.length + controlSeq.length;
const pesHeaderSize = 3 + 5;
const pesPayloadSize = 1 + 2 + subPacketSize;
const pesLength = pesHeaderSize + pesPayloadSize;
const totalSize = 14 + 6 + pesLength;
const data = new Uint8Array(totalSize);
let pos = 0;
data[pos++] = 0;
data[pos++] = 0;
data[pos++] = 1;
data[pos++] = 186;
const scr = packet.pts * 90;
data[pos++] = 68 | scr >> 30 & 3;
data[pos++] = scr >> 22 & 255;
data[pos++] = 4 | scr >> 14 & 252;
data[pos++] = scr >> 7 & 255;
data[pos++] = 4 | scr << 1 & 252;
data[pos++] = 1;
data[pos++] = 1;
data[pos++] = 137;
data[pos++] = 195;
data[pos++] = 248;
data[pos++] = 0;
data[pos++] = 0;
data[pos++] = 1;
data[pos++] = 189;
data[pos++] = pesLength >> 8 & 255;
data[pos++] = pesLength & 255;
data[pos++] = 128;
data[pos++] = 128;
data[pos++] = 5;
const ptsValue = packet.pts * 90;
data[pos++] = 33 | ptsValue >> 29 & 14;
data[pos++] = ptsValue >> 22 & 255;
data[pos++] = 1 | ptsValue >> 14 & 254;
data[pos++] = ptsValue >> 7 & 255;
data[pos++] = 1 | ptsValue << 1 & 254;
data[pos++] = 32;
data[pos++] = subPacketSize >> 8 & 255;
data[pos++] = subPacketSize & 255;
data[pos++] = subPacketSize >> 8 & 255;
data[pos++] = subPacketSize & 255;
data[pos++] = controlOffset >> 8 & 255;
data[pos++] = controlOffset & 255;
data.set(packet.rleData, pos);
pos += packet.rleData.length;
data.set(controlSeq, pos);
return data;
}
function createControlSequence(packet) {
const seq = [];
const x1 = packet.x;
const x2 = packet.x + packet.width - 1;
const y1 = packet.y;
const y2 = packet.y + packet.height - 1;
seq.push(5);
seq.push(x1 >> 4 & 255);
seq.push((x1 & 15) << 4 | x2 >> 8 & 15);
seq.push(x2 & 255);
seq.push(y1 >> 4 & 255);
seq.push((y1 & 15) << 4 | y2 >> 8 & 15);
seq.push(y2 & 255);
const stopTime = Math.floor(packet.duration * 90 / 1024);
seq.push(2);
seq.push(stopTime >> 8 & 255);
seq.push(stopTime & 255);
if (packet.forced) {
seq.push(0);
}
seq.push(255);
return new Uint8Array(seq);
}
// src/formats/binary/vobsub/rle.ts
var LITERAL_TABLE = (() => {
const table = new Uint8Array(256 * 4);
for (let byte = 0;byte < 256; byte++) {
const idx = byte << 2;
table[idx] = byte >> 6 & 3;
table[idx + 1] = byte >> 4 & 3;
table[idx + 2] = byte >> 2 & 3;
table[idx + 3] = byte & 3;
}
return table;
})();
function decodeRLE(rleData, width, height) {
const output = new Uint8Array(width * height);
let outputPos = 0;
let inputPos = 0;
const outputLen = output.length;
while (inputPos < rleData.length && outputPos < outputLen) {
const byte = rleData[inputPos++];
if (byte === 0) {
if (inputPos >= rleData.length)
break;
const next = rleData[inputPos++];
if (next === 0) {
const remainder = outputPos % width;
if (remainder !== 0) {
outputPos += width - remainder;
}
} else {
const color = next & 3;
let count = 0;
const mode = next & 192;
if (mode === 0) {
count = next >> 2 & 63;
} else if (mode === 64) {
if (inputPos < rleData.length) {
const extraByte = rleData[inputPos++];
count = (next & 63) << 2 | extraByte >> 6 & 3;
}
} else {
count = next >> 2 & 63;
}
if (count > 0 && outputPos < outputLen) {
let end = outputPos + count;
if (end > outputLen)
end = outputLen;
output.fill(color, outputPos, end);
outputPos = end;
}
}
} else {
const idx = byte << 2;
if (outputPos + 4 <= outputLen) {
output[outputPos] = LITERAL_TABLE[idx];
output[outputPos + 1] = LITERAL_TABLE[idx + 1];
output[outputPos + 2] = LITERAL_TABLE[idx + 2];
output[outputPos + 3] = LITERAL_TABLE[idx + 3];
outputPos += 4;
} else {
if (outputPos < outputLen)
output[outputPos++] = LITERAL_TABLE[idx];
if (outputPos < outputLen)
output[outputPos++] = LITERAL_TABLE[idx + 1];
if (outputPos < outputLen)
output[outputPos++] = LITERAL_TABLE[idx + 2];
if (outputPos < outputLen)
output[outputPos++] = LITERAL_TABLE[idx + 3];
}
}
}
return { width, height, data: output };
}
function encodeRLE(bitmap, width, height) {
const output = [];
for (let y = 0;y < height; y++) {
let x = 0;
const lineStart = y * width;
while (x < width) {
const pos = lineStart + x;
const color = bitmap[pos];
let runLength = 1;
while (x + runLength < width && bitmap[lineStart + x + runLength] === color && runLength < 255) {
runLength++;
}
if (runLength >= 4) {
if (runLength <= 15) {
output.push(0);
output.push(runLength << 2 | color);
} else if (runLength <= 63) {
output.push(0);
output.push(192 | runLength << 2 | color);
} else {
output.push(0);
output.push(64 | runLength >> 2 & 63);
output.push((runLength & 3) << 6 | color);
}
x += runLength;
} else {
const literals = [];
while (x < width) {
const currentPos = lineStart + x;
const currentColor = bitmap[currentPos];
let nextRunLength = 1;
while (x + nextRunLength < width && bitmap[lineStart + x + nextRunLength] === currentColor && nextRunLength < 255) {
nextRunLength++;
}
if (nextRunLength >= 4) {
break;
}
literals.push(currentColor);
x++;
if (literals.length === 4) {
break;
}
}
while (literals.length < 4 && literals.length > 0) {
literals.push(0);
}
if (literals.length > 0) {
const byte = literals[0] << 6 | literals[1] << 4 | literals[2] << 2 | literals[3];
output.push(byte);
}
}
}
output.push(0);
output.push(0);
}
return new Uint8Array(output);
}
// src/formats/binary/vobsub/index.ts
function parseVobSub(idx, sub, opts = {}) {
try {
const data = toUint8Array(sub);
const errors = [];
const decodeMode = opts.decode ?? "full";
const index = typeof idx === "string" ? decodeMode === "none" ? parseIdxTimings(idx) : parseIdx(idx) : idx;
let totalEvents = 0;
for (const track of index.tracks)
totalEvents += track.timestamps.length;
const doc = {
info: {
title: "VobSub",
playResX: index.size.width,
playResY: index.size.height,
scaleBorderAndShadow: true,
wrapStyle: 0
},
styles: new Map([
["Default", {
name: "Default",
fontName: "Arial",
fontSize: 20,
primaryColor: 4294967295,
secondaryColor: 16711935,
outlineColor: 255,
backColor: 255,
bold: false,
italic: false,
underline: false,
strikeout: false,
scaleX: 100,
scaleY: 100,
spacing: 0,
angle: 0,
borderStyle: 1,
outline: 2,
shadow: 0,
alignment: 2,
marginL: 10,
marginR: 10,
marginV: 10,
encoding: 1
}]
]),
events: new Array(totalEvents),
comments: []
};
let eventId = 0;
const palette = index.palette;
const defaultStyle = "Default";
for (const track of index.tracks) {
const timestamps = track.timestamps;
const tlen = timestamps.length;
const trackIndex = track.index;
for (let i = 0;i < tlen; i++) {
const ts = timestamps[i];
if (decodeMode === "none") {
const next = i + 1 < tlen ? timestamps[i + 1].time : ts.time + 2000;
const endTime2 = next > ts.time ? next : ts.time + 2000;
const id2 = eventId++;
const event2 = {
id: id2,
start: ts.time,
end: endTime2,
layer: 0,
style: defaultStyle,
actor: "",
marginL: 0,
marginR: 0,
marginV: 0,
effect: "",
text: "",
segments: EMPTY_SEGMENTS,
dirty: false
};
doc.events[id2] = event2;
continue;
}
const packet = parseSubPacket(data, ts.filepos);
if (!packet) {
errors.push(`Failed to parse packet at filepos ${ts.filepos.toString(16)}`);
continue;
}
let endTime = ts.time + packet.duration;
if (packet.duration === 0 && i + 1 < tlen) {
endTime = timestamps[i + 1].time;
}
if (endTime <= ts.time) {
endTime = ts.time + 2000;
}
const id = eventId++;
const event = {
id,
start: ts.time,
end: endTime,
layer: 0,
style: defaultStyle,
actor: "",
marginL: 0,
marginR: 0,
marginV: 0,
effect: "",
text: "",
segments: EMPTY_SEGMENTS,
dirty: false
};
if (decodeMode !== "none") {
event.image = decodeMode === "rle" ? {
format: "rle",
width: packet.width,
height: packet.height,
x: packet.x,
y: packet.y,
data: packet.rleData,
palette
} : {
format: "indexed",
width: packet.width,
height: packet.height,
x: packet.x,
y: packet.y,
data: decodeRLE(packet.rleData, packet.width, packet.height).data,
palette
};
event.vobsub = {
forced: packet.forced,
originalIndex: trackIndex
};
}
doc.events[id] = event;
}
}
if (eventId < doc.events.length) {
doc.events.length = eventId;
}
const parseErrors = errors.map((message) => ({
line: 0,
column: 0,
code: "MALFORMED_EVENT",
message
}));
return {
ok: parseErrors.length === 0,
document: doc,
errors: parseErrors,
warnings: []
};
} catch (err) {
return {
ok: false,
document: createDocument(),
errors: [toParseError(err)],
warnings: []
};
}
}
function parseIdxTimings(content) {
let pos = 0;
const len = content.length;
if (len === 0) {
return {
size: { width: 720, height: 480 },
palette: [],
tracks: []
};
}
if (content.charCodeAt(0) === 65279)
pos = 1;
const index = {
size: { width: 720, height: 480 },
palette: [],
tracks: []
};
let currentTrack = null;
while (pos <= len) {
let lineEnd = content.indexOf(`
`, pos);
if (lineEnd === -1)
lineEnd = len;
let lineStart = pos;
if (lineEnd > lineStart && content.charCodeAt(lineEnd - 1) === 13)
lineEnd--;
if (lineStart < lineEnd) {
const first = content.charCodeAt(lineStart);
if (first !== 35) {
if (content.startsWith("size:", lineStart)) {
let i = lineStart + 5;
while (i < lineEnd && content.charCodeAt(i) <= 32)
i++;
let w = 0;
while (i < lineEnd) {
const d = content.charCodeAt(i) - 48;
if (d < 0 || d > 9)
break;
w = w * 10 + d;
i++;
}
if (i < lineEnd && content.charCodeAt(i) === 120)
i++;
let h = 0;
while (i < lineEnd) {
const d = content.charCodeAt(i) - 48;
if (d < 0 || d > 9)
break;
h = h * 10 + d;
i++;
}
if (w > 0 && h > 0) {
index.size.width = w;
index.size.height = h;
}
} else if (content.startsWith("id:", lineStart)) {
let i = lineStart + 3;
while (i < lineEnd && content.charCodeAt(i) <= 32)
i++;
const langStart = i;
while (i < lineEnd && content.charCodeAt(i) > 32 && content.charCodeAt(i) !== 44)
i++;
const language = content.substring(langStart, i);
const indexPos = content.indexOf("index:", i);
if (indexPos !== -1) {
let j = indexPos + 6;
while (j < lineEnd && content.charCodeAt(j) <= 32)
j++;
let trackIndex = 0;
while (j < lineEnd) {
const d = content.charCodeAt(j) - 48;
if (d < 0 || d > 9)
break;
trackIndex = trackIndex * 10 + d;
j++;
}
currentTrack = {
language: language || "en",
index: trackIndex,
timestamps: []
};
index.tracks.push(currentTrack);
}
} else if (content.startsWith("timestamp:", lineStart)) {
if (!currentTrack) {
currentTrack = { language: "en", index: 0, timestamps: [] };
index.tracks.push(currentTrack);
}
const time = parseTimeFixed2(content, lineStart + 11);
const fileposStart = lineStart + 34;
if (time >= 0 && fileposStart < lineEnd) {
let filepos = 0;
for (let i = fileposStart;i < lineEnd; i++) {
const c = content.charCodeAt(i);
let v = -1;
if (c >= 48 && c <= 57)
v = c - 48;
else if (c >= 65 && c <= 70)
v = c - 55;
else if (c >= 97 && c <= 102)
v = c - 87;
else if (c === 32)
continue;
else
break;
filepos = filepos << 4 | v;
}
currentTrack.timestamps.push({ time, filepos });
}
}
}
}
if (lineEnd === len)
break;
pos = lineEnd + 1;
}
return index;
}
function parseTimeFixed2(src, start) {
const h1 = src.charCodeAt(start) - 48;
const h2 = src.charCodeAt(start + 1) - 48;
const c1 = src.charCodeAt(start + 2);
const m1 = src.charCodeAt(start + 3) - 48;
const m2 = src.charCodeAt(start + 4) - 48;
const c2 = src.charCodeAt(start + 5);
const s1 = src.charCodeAt(start + 6) - 48;
const s2 = src.charCodeAt(start + 7) - 48;
const c3 = src.charCodeAt(start + 8);
const ms1 = src.charCodeAt(start + 9) - 48;
const ms2 = src.charCodeAt(start + 10) - 48;
const ms3 = src.charCodeAt(start + 11) - 48;
if (h1 < 0 || h1 > 9 || h2 < 0 || h2 > 9 || m1 < 0 || m1 > 9 || m2 < 0 || m2 > 9 || s1 < 0 || s1 > 9 || s2 < 0 || s2 > 9 || ms1 < 0 || ms1 > 9 || ms2 < 0 || ms2 > 9 || ms3 < 0 || ms3 > 9 || c1 !== 58 || c2 !== 58 || c3 !== 58) {
return -1;
}
const hours = h1 * 10 + h2;
const minutes = m1 * 10 + m2;
const seconds = s1 * 10 + s2;
const millis = ms1 * 100 + ms2 * 10 + ms3;
return hours * 3600000 + minutes * 60000 + seconds * 1000 + millis;
}
function toVobSub(doc) {
const index = {
size: {
width: doc.info.playResX,
height: doc.info.playResY
},
palette: [],
tracks: [{
language: "en",
index: 0,
timestamps: []
}]
};
const packets = [];
let filepos = 0;
for (const event of doc.events) {
const imageParams = event.image ?? event.segments.flatMap((seg) => seg.effects).find((eff) => eff.type === "image");
if (!imageParams) {
continue;
}
const params = "type" in imageParams ? imageParams.params : imageParams;
if (index.palette.length === 0 && params.palette) {
index.palette = params.palette;
}
const vobsubMeta = event.vobsub ?? event.segments.flatMap((seg) => seg.effects).find((eff) => eff.type === "vobsub")?.params;
const rleData = params.format === "rle" ? params.data : encodeRLE(params.data, params.width, params.height);
const packet = {
pts: event.start,
duration: event.end - event.start,
x: params.x || 0,
y: params.y || 0,
width: params.width,
height: params.height,
rleData,
forced: vobsubMeta?.forced || false
};
packets.push(packet);
index.tracks[0].timestamps.push({
time: event.start,
filepos
});
filepos += estimatePacketSize(rleData.length, packet.forced);
}
const subBinary = createSubBinary(packets);
let actualPos = 0;
for (let i = 0;i < packets.length; i++) {
index.tracks[0].timestamps[i].filepos = actualPos;
actualPos += estimatePacketSize(packets[i].rleData.length, packets[i].forced);
}
if (index.palette.length === 0) {
index.palette = [
255,
4294967295,
2155905279,
3233857791,
4278190335,
16711935,
65535,
4294902015,
4278255615,
16777215,
2147483903,
8388863,
33023,
2155872511,
2147516671,
8421631
];
}
const idxContent = serializeIdx(index);
return {
idx: idxContent,
sub: subBinary
};
}
function estimatePacketSize(rleSize, forced) {
const controlSeqSize = forced ? 12 : 11;
const subPacketSize = 4 + rleSize + controlSeqSize;
const pesLength = 11 + subPacketSize;
return 20 + pesLength;
}
// src/core/encoding.ts
function normalizeEncoding(encoding) {
const normalized = encoding.toLowerCase().replace(/[_\s]/g, "-");
if (normalized === "utf8")
return "utf-8";
if (normalized === "shiftjis" || normalized === "shift-jis")
return "shift-jis";
if (normalized === "eucjp" || normalized === "euc-jp")
return "euc-jp";
if (normalized === "euckr" || normalized === "euc-kr")
return "euc-kr";
return normalized;
}
function detectEncoding(data) {
if (data.length === 0)
return "utf-8";
if (data.length >= 3 && data[0] === 239 && data[1] === 187 && data[2] === 191) {
return "utf-8";
}
if (data.length >= 2 && data[0] === 255 && data[1] === 254) {
return "utf-16le";
}
if (data.length >= 2 && data[0] === 254 && data[1] === 255) {
return "utf-16be";
}
let validUtf8 = 0;
let invalidUtf8 = 0;
let shiftJisScore = 0;
let eucJpScore = 0;
let gbScore = 0;
let eucKrScore = 0;
for (let i = 0;i < Math.min(data.length, 8192); i++) {
const byte = data[i];
if (byte >= 128) {
if ((byte & 224) === 192 && i + 1 < data.length && (data[i + 1] & 192) === 128) {
validUtf8++;
i++;
} else if ((byte & 240) === 224 && i + 2 < data.length && (data[i + 1] & 192) === 128 && (data[i + 2] & 192) === 128) {
validUtf8++;
i += 2;
} else if ((byte & 248) === 240 && i + 3 < data.length && (data[i + 1] & 192) === 128 && (data[i + 2] & 192) === 128 && (data[i + 3] & 192) === 128) {
validUtf8++;
i += 3;
} else if (i + 1 < data.length) {
const next = data[i + 1];
if (byte >= 129 && byte <= 159 || byte >= 224 && byte <= 252) {
if (next >= 64 && next <= 126 || next >= 128 && next <= 252) {
shiftJisScore += 3;
}
}
if (byte >= 161 && byte <= 254 && next >= 161 && next <= 254) {
eucJpScore += 3;
}
if (byte >= 176 && byte <= 247 && next >= 161 && next <= 254) {
gbScore += 3;
} else if (byte >= 192 && byte <= 240 && next >= 160 && next <= 255) {
gbScore += 1;
}
if (byte >= 176 && byte <= 200 && next >= 161 && next <= 254) {
if (next >= 192) {
gbScore += 2;
} else {
eucKrScore += 4;
}
} else if (byte >= 161 && byte <= 254 && next >= 161 && next <= 254) {
eucKrScore += 1;
}
invalidUtf8++;
}
}
}
if (validUtf8 > 0 && invalidUtf8 === 0) {
return "utf-8";
}
if (shiftJisScore > eucJpScore && shiftJisScore > gbScore && shiftJisScore > eucKrScore && shiftJisScore > 0) {
return "shift-jis";
}
if (eucJpScore > gbScore && eucJpScore > eucKrScore && eucJpScore > 0) {
return "euc-jp";
}
if (gbScore > eucKrScore && gbScore > 0) {
return "gb2312";
}
if (eucKrScore > 0) {
return "euc-kr";
}
return "utf-8";
}
function decode(data, encoding) {
const enc = encoding ? normalizeEncoding(encoding) : detectEncoding(data);
switch (enc) {
case "utf-8":
return decodeUtf8(data);
case "utf-16le":
return decodeUtf16LE(data);
case "utf-16be":
return decodeUtf16BE(data);
case "shift-jis":
return decodeShiftJIS(data);
case "euc-jp":
return decodeEucJP(data);
case "gb2312":
case "gbk":
case "gb18030":
return decodeGB(data);
case "euc-kr":
return decodeEucKR(data);
case "windows-1250":
return decodeWindows1250(data);
case "windows-1251":
return decodeWindows1251(data);
case "windows-1252":
return decodeWindows1252(data);
case "windows-1253":
return decodeWindows1253(data);
case "windows-1254":
return decodeWindows1254(data);
case "windows-1255":
return decodeWindows1255(data);
case "windows-1256":
return decodeWindows1256(data);
case "windows-1257":
return decodeWindows1257(data);
case "windows-1258":
return decodeWindows1258(data);
case "iso-8859-1":
return decodeISO88591(data);
case "iso-8859-2":
return decodeISO88592(data);
case "koi8-r":
return decodeKOI8R(data);
default:
return decodeUtf8(data);
}
}
function encode(text, encoding) {
const enc = normalizeEncoding(encoding);
switch (enc) {
case "utf-8":
return encodeUtf8(text);
case "utf-16le":
return encodeUtf16LE(text);
case "utf-16be":
return encodeUtf16BE(text);
case "shift-jis":
return encodeShiftJIS(text);
case "euc-jp":
return encodeEucJP(text);
case "gb2312":
case "gbk":
case "gb18030":
return encodeGB(text);
case "euc-kr":
return encodeEucKR(text);
case "windows-1250":
return encodeWindows1250(text);
case "windows-1251":
return encodeWindows1251(text);
case "windows-1252":
return encodeWindows1252(text);
case "windows-1253":
return encodeWindows1253(text);
case "windows-1254":
return encodeWindows1254(text);
case "windows-1255":
return encodeWindows1255(text);
case "windows-1256":
return encodeWindows1256(text);
case "windows-1257":
return encodeWindows1257(text);
case "windows-1258":
return encodeWindows1258(text);
case "iso-8859-1":
return encodeISO88591(text);
case "iso-8859-2":
return encodeISO88592(text);
case "koi8-r":
return encodeKOI8R(text);
default:
return encodeUtf8(text);
}
}
function decodeUtf8(data) {
let offset = 0;
if (data.length >= 3 && data[0] === 239 && data[1] === 187 && data[2] === 191) {
offset = 3;
}
const decoder = new TextDecoder("utf-8", { fatal: false });
return decoder.decode(data.subarray(offset));
}
function encodeUtf8(text) {
const encoder = new TextEncoder;
return encoder.encode(text);
}
function decodeUtf16LE(data) {
let offset = 0;
if (data.length >= 2 && data[0] === 255 && data[1] === 254) {
o