UNPKG

bddvdsubreader

Version:

Parse VobSub and PGS subtitles with NodeJS

188 lines (163 loc) 5.45 kB
const decodePGSIndexRLE = (pts, rleBuffer, w, h, strict = true) => { const pixels = new Uint8Array(w * h); let ptr = 0; let pos = 0; while (pos < w * h && ptr < rleBuffer.length) { let x = pos % w; let y = Math.floor(pos / w); let code = rleBuffer[ptr++]; if (code !== 0) { pixels[pos++] = code; continue; } code = rleBuffer[ptr++]; if (code === 0) { if (x !== 0) { while (x < w && pos < w * h) { pixels[pos++] = 0; x++; } } else { pos = y * w; } continue; } let length; let color = 0; const prefix = code >> 6; const lowBits = code & 0x3F; switch (prefix) { case 0: length = lowBits; color = 0; break; case 1: if (ptr >= rleBuffer.length) throw new Error('Incomplete RLE data'); length = (lowBits << 8) | rleBuffer[ptr++]; color = 0; break; case 2: if (ptr >= rleBuffer.length) throw new Error('Incomplete RLE data'); length = lowBits; color = rleBuffer[ptr++]; break; case 3: if (ptr + 1 >= rleBuffer.length) throw new Error('Incomplete RLE data'); length = (lowBits << 8) | rleBuffer[ptr++]; color = rleBuffer[ptr++]; break; default: throw new Error('Invalid RLE prefix'); } for (let i = 0; i < length && pos < w * h; i++) { x = pos % w; y = Math.floor(pos / w); pixels[pos++] = color; if (x + 1 >= w && pos < w * h) { pos = (y + 1) * w; } } } if (pos < w * h) { throw new Error(`RLE data incomplete: filled ${pos} of ${w * h} pixels`); } if (ptr < rleBuffer.length) { if (ptr === rleBuffer.length - 2 && rleBuffer[ptr] === 0 && rleBuffer[ptr + 1] === 0) { ptr += 2; } else if (strict) { const rem = rleBuffer.length - ptr; const remBufHEX = rleBuffer.slice(ptr).slice(0, 40).toString('hex'); console.warn(` - PTS: ${pts} - Excess RLE data: ${rem} bytes remaining, bytes: ${remBufHEX}...`); throw new Error('Excess RLE data'); } } return pixels; } const indicesToRGBA = (index, w, h, entries) => { const rgba = Buffer.alloc(w * h * 4); for (let i = 0; i < index.length; i++) { const px = entries.get(index[i]) || [0,0,0,0]; const o = i * 4; rgba[o] = px[0]; rgba[o + 1] = px[1]; rgba[o + 2] = px[2]; rgba[o + 3] = px[3]; } return rgba; }; const blitRGBA = (dst, dw, dh, src, sw, sh, dx, dy) => { for (let y = 0; y < sh; y++) { const yy = dy + y; if (yy < 0 || yy >= dh) continue; for (let x = 0; x < sw; x++) { const xx = dx + x; if (xx < 0 || xx >= dw) continue; const di = (yy * dw + xx) * 4; const si = (y * sw + x) * 4; const sr = src[si], sg = src[si+1], sb = src[si+2], sa = src[si+3] / 255; if (sa === 0) continue; const dr = dst[di], dg = dst[di+1], db = dst[di+2], da = dst[di+3] / 255; // source-over alpha blend const outA = sa + da * (1 - sa); const outR = (sr * sa + dr * da * (1 - sa)) / (outA || 1); const outG = (sg * sa + dg * da * (1 - sa)) / (outA || 1); const outB = (sb * sa + db * da * (1 - sa)) / (outA || 1); dst[di] = outR | 0; dst[di+1] = outG | 0; dst[di+2] = outB | 0; dst[di+3] = Math.round(outA * 255); } } }; class RGBAImage { #pts; #compW; #compH; #plt; #ods; #ref; constructor(pts, w, h, plt, ods, ref) { this.#pts = pts; this.#compW = w; this.#compH = h; this.#plt = plt; this.#ods = ods; this.#ref = ref; } get(){ const frame = Buffer.alloc(this.#compW * this.#compH * 4); for (const ref of this.#ref) { const obj = this.#ods.get(ref.objectId); if (!obj || !obj.rle || obj.rle.length === 0) continue; const index = decodePGSIndexRLE(this.#pts, obj.rle, obj.w, obj.h); const rgba = indicesToRGBA(index, obj.w, obj.h, this.#plt); blitRGBA(frame, this.#compW, this.#compH, rgba, obj.w, obj.h, ref.pos_x, ref.pos_y); } return frame; } } const composeFrame = (record) => { const pts = record.pts; const end = record.end; const pcs = record.pcs; if (!pts) return null; if (!end) return null; if (!pcs) return null; const palette = record.pds.get(pcs.paletteId); if (!palette) return null; const frame = new RGBAImage(pts, pcs.w, pcs.h, palette.entries, record.ods, record.ref); return { forced: pcs.forced, pts: pts, end: end, width: pcs.w, height: pcs.h, rgba: frame, }; }; export default composeFrame;