MNG Support

Replaces img, object, embed, and CSS background-image *.mng references with animated canvas elements

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         MNG Support
// @namespace    https://github.com/Denveous/mng.js
// @version      1.0.1
// @description  Replaces img, object, embed, and CSS background-image *.mng references with animated canvas elements
// @author       Denveous
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @connect      *
// @license      GPL-3.0-or-later
// ==/UserScript==

(function() {
const PNG_SIG = new Uint8Array([0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A]);
const MNG_SIG = new Uint8Array([0x8A,0x4D,0x4E,0x47,0x0D,0x0A,0x1A,0x0A]);

function readChunks(buf) {
    const data = new Uint8Array(buf), view = new DataView(buf);
    for (let i = 0; i < 8; i++) if (data[i] !== MNG_SIG[i]) throw new Error('Not MNG');
    const chunks = []; let pos = 8;
    while (pos < data.length - 12) {
        const len = view.getUint32(pos), type = String.fromCharCode(data[pos+4],data[pos+5],data[pos+6],data[pos+7]);
        chunks.push({ type, len, data: data.slice(pos+8, pos+8+len), offset: pos });
        pos += len + 12;
    }
    return chunks;
}

function parseFRAM(d) {
    let pos = 1;
    while (pos < d.length && d[pos]) pos++;
    pos++;
    if (pos >= d.length) return null;
    const hasDelay = d[pos++]; pos += 3;
    if (!hasDelay || pos + 4 > d.length) return null;
    return new DataView(d.buffer, d.byteOffset + pos).getUint32(0);
}

function parse(buf) {
    const raw = new Uint8Array(buf), chunks = readChunks(buf);
    let ticksPerSec = 100, pendingDelay = null, frameParts = [], inPNG = false;
    const frames = [];
    for (const c of chunks) {
        if (c.type === 'MHDR') { const t = new DataView(c.data.buffer, c.data.byteOffset).getUint32(8); if (t > 0) ticksPerSec = t; }
        else if (c.type === 'FRAM') { const t = parseFRAM(c.data); if (t != null) pendingDelay = Math.round(t / ticksPerSec * 1000); }
        else if (c.type === 'IHDR') { frameParts = [c]; inPNG = true; }
        else if (inPNG) {
            frameParts.push(c);
            if (c.type === 'IEND') {
                inPNG = false;
                const png = new Uint8Array(8 + frameParts.reduce((s,x) => s + x.len + 12, 0));
                png.set(PNG_SIG); let p = 8;
                for (const f of frameParts) { png.set(raw.slice(f.offset, f.offset + f.len + 12), p); p += f.len + 12; }
                frames.push({ png, delay: pendingDelay || 100 }); pendingDelay = null;
            }
        }
    }
    return frames;
}

function loadImages(frames) {
    return Promise.all(frames.map(f => new Promise((res, rej) => {
        const img = new Image(), url = URL.createObjectURL(new Blob([f.png], { type: 'image/png' }));
        img.onload = () => { URL.revokeObjectURL(url); res({ img, delay: f.delay }); };
        img.onerror = rej; img.src = url;
    })));
}

function makeCanvas(frames) {
    const canvas = document.createElement('canvas');
    canvas.width = frames[0].img.width; canvas.height = frames[0].img.height;
    const ctx = canvas.getContext('2d');
    let idx = 0, timer = null, running = false;
    const draw = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(frames[idx].img, 0, 0); };
    const tick = () => { draw(); timer = setTimeout(() => { idx = (idx + 1) % frames.length; tick(); }, frames[idx].delay); };
    canvas.play  = () => { if (!running) { running = true; tick(); } };
    canvas.pause = () => { clearTimeout(timer); running = false; };
    canvas.stop  = () => { canvas.pause(); idx = 0; draw(); };
    draw(); return canvas;
}

function gmFetch(url) {
    return new Promise((res, rej) => GM_xmlhttpRequest({
        method: 'GET', url, responseType: 'arraybuffer',
        onload: r => res(r.response), onerror: rej
    }));
}
function fetchBuf(url) {
    return fetch(url).then(r => r.arrayBuffer()).catch(() => gmFetch(url));
}

async function processEl(el, url) {
    if (el.dataset.mngProcessed) return;
    el.dataset.mngProcessed = '1';
    try {
        const buf = await fetchBuf(url);
        const frames = await loadImages(parse(buf));
        const canvas = makeCanvas(frames);
        canvas.style.cssText = el.style.cssText;
        if (el.width)  canvas.style.width  = el.width  + 'px';
        if (el.height) canvas.style.height = el.height + 'px';
        if (el.className) canvas.className = el.className;
        if (frames.length > 1) canvas.play();
        el.replaceWith(canvas);
    } catch(e) { console.warn('[MNG]', url, e.message); }
}

async function processBg(el) {
    if (el.dataset.mngBgProcessed) return;
    const url = el.style.backgroundImage.match(/url\(["']?([^"')]+\.mng)["']?\)/)?.[1];
    if (!url) return;
    el.dataset.mngBgProcessed = '1';
    try {
        const buf = await fetchBuf(url);
        const frames = await loadImages(parse(buf));
        const canvas = makeCanvas(frames);
        const cs = getComputedStyle(el);
        canvas.style.width = cs.width; canvas.style.height = cs.height;
        canvas.style.position = 'absolute'; canvas.style.top = '0'; canvas.style.left = '0';
        el.style.position = el.style.position || 'relative';
        el.style.backgroundImage = 'none';
        el.prepend(canvas);
        if (frames.length > 1) canvas.play();
    } catch(e) { console.warn('[MNG bg]', url, e.message); }
}

function scan(root) {
    if (!root.querySelectorAll) return;
    root.querySelectorAll('img[src$=".mng"]').forEach(el => processEl(el, el.src));
    root.querySelectorAll('object[data$=".mng"], embed[src$=".mng"]').forEach(el => processEl(el, el.data || el.src));
    root.querySelectorAll('*').forEach(el => { if (el.style.backgroundImage?.includes('.mng')) processBg(el); });
}

scan(document);
new MutationObserver(mutations => {
    for (const m of mutations)
        for (const n of m.addedNodes)
            if (n.nodeType === 1) scan(n);
}).observe(document.body, { childList: true, subtree: true });
})();