UNPKG

savgy

Version:

Get self-contained SVGs or bitmaps from SVG

2,134 lines 71.9 kB
/**
 * create cache object to avoid
 * unnecessary recurring requests
 */
const assetCache = { fonts: {}, images: {}, css: {}, svg: {} };

const icons = {
    download: `<svg viewBox="0 0 100 100" class="icn-svg icn-download"><path stroke="currentColor" d="M49.4 67.3v-62.3m-40 80h80m-0.4 -53.7l-39.6 36l-39.6 -36"/></svg>`,


    copy: `<svg viewBox="0 0 86 100" class="icn-svg icn-copy "><path stroke="currentColor" d="M75.7 85h-47v-62.5h25l22 22zm-66 -5v-75h40"></path><path stroke="currentColor" stroke-linejoin="round" stroke-miterlimit="3" d="M75.7 44.5h-22v-22z"/></svg>`,

    file: `<svg viewBox="0 0 71 100" class="icn-svg icn-file"><path stroke="currentColor" d="M60.4 85h-50.8v-70h28.7l22 22z" /><path stroke="currentColor" stroke-linejoin="round" stroke-miterlimit="3" d="M60.4 37h-22v-22z" /></svg>`,

    spinner: `<svg class="icn-svg icn-spinner" viewBox="0 0 100 100">
            <path stroke="currentColor" stroke-linejoin="round" fill="none" d="M 50 10 a 40 40 0  1 1 -40 40">
            <animateTransform attributeName="transform" type="rotate" dur="0.75s" values="0 50 50;360 50 50" repeatCount="indefinite"></animateTransform>
            </path>
        </svg>`,

    checkbox_filled: `<svg viewBox="0 0 105 100" class="icn-svg icn-checkbox-filled"><path d="M18.3 9h62.2c4.9 0 8.9 4 8.9 8.9v62.2c0 4.9 -4 8.9 -8.9 8.9h-62.2c-4.9 0 -8.9 -4 -8.9 -8.9v-62.2c0 -4.9 4 -8.9 8.9 -8.9z" stroke="none" style="fill:var(--icon-bg, white)" /> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M36.1 44.6l13.3 13.3l44.4 -44.4m-4.4 35.5v31.1c0 4.9 -4 8.9 -8.9 8.9h-62.2c-4.9 0 -8.9 -4 -8.9 -8.9v-62.2c0 -4.9 4 -8.9 8.9 -8.9h48.9" /></svg>`,

    checkbox: `<svg viewBox="0 0 100 100" class="icn-svg icn-checkbox"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M18.5 9h62.2c4.9 0 8.9 4 8.9 8.9v62.2c0 4.9 -4 8.9 -8.9 8.9h-62.2c-4.9 0 -8.9 -4 -8.9 -8.9v-62.2c0.1 -4.9 4 -8.9 8.9 -8.9z" style="fill:var(--icon-bg, white)"></path></svg>`,

    chevron_down: `<svg viewBox="0 0 95 100" class="icn-svg icn-chevron-down"><path stroke="currentColor" d="M84.4 32l-37.5 37.5l-37.5 -37.5" /></svg>`,

    close: `<svg viewBox="0 0 100 100" class="icn-svg icn-close"><path stroke="currentColor" d="M89.1 10l-80 80m80 0l-80 -80" /></svg>`

};

function getToolbar(icons) {

    let toolbarMarkup =
        `<div class="savgy__toolbar">
    <div class="savgy__toolbar__inner">
        <div class="savgy__toolbar__inputwrap  --savgy__toolbar__grd">
            <div class="savgy__toolbar__row savgy__toolbar__row--2">
                <div class="savgy__toolbar__col savgy__grd-label">
                    <label class="savgy__toolbar__label savgy__toolbar__label--w">W:</label>   
                    <input class="savgy__toolbar__input savgy__toolbar__input--w" type="number" value="640">
                </div>

                <div class="savgy__toolbar__col savgy__grd-label">
                    <label class="savgy__toolbar__label savgy__toolbar__label--h">H:</label>   
                    <input class="savgy__toolbar__input savgy__toolbar__input--h" type="number" value="480">
                </div>
            </div>


            <div class="savgy__toolbar__row">
                <div class="savgy__toolbar__col savgy__grd-label">
                    <label class="savgy__toolbar__label">Quality:</label>  
                    <input class="savgy__toolbar__input savgy__toolbar__input--quality" type="number" value="0.9" min="0.1" max="1" step="0.1">
                </div>
            </div>


        </div>
        <div class="savgy__toolbar__inputwrap">
            <div class="savgy__toolbar__row">
                <div class="savgy__toolbar__col">
                    <select class="savgy__toolbar__select savgy__toolbar__select--format">
                        <option class="savgy__toolbar__option" value="png">PNG </option>
                        <option class="savgy__toolbar__option" value="webp">WebP </option>
                        <option class="savgy__toolbar__option" value="jpg">Jpeg </option>
                        <option class="savgy__toolbar__option" value="svg">SVG (original)</option>
                        <option class="savgy__toolbar__option" value="svg_self">SVG (self-contained)</option>
                    </select>
                </div>
            </div>

            <div class="savgy__toolbar__row">
                <div class="savgy__toolbar__col">
                    <select class="savgy__toolbar__select savgy__toolbar__select--format--img">
                        <option class="savgy__toolbar__option" value="original">SVG img format </option>
                        <option class="savgy__toolbar__option" value="png">PNG </option>
                        <option class="savgy__toolbar__option" value="webp">WebP </option>
                        <option class="savgy__toolbar__option" value="jpg">Jpeg </option>
                    </select>
                </div>
            </div>


            <div class="savgy__toolbar__row">
                <div class="savgy__toolbar__col">
                    <label class="savgy__toolbar__label savgy__toolbar__label--inline">
                        <input type="checkbox" value="1" class="savgy__toolbar__check savgy__toolbar__check--preview">
                        Show Preview
                    </label>
                    <label class="savgy__toolbar__label savgy__toolbar__label--inline">
                        <input type="checkbox" value="1" class="savgy__toolbar__check savgy__toolbar__check--intrinsic">
                        Intrinsic width/height
                    </label>
                    <label class="savgy__toolbar__label savgy__toolbar__label--inline">
                        <input type="checkbox" value="1" class="savgy__toolbar__check savgy__toolbar__check--crop">
                        Crop to content
                    </label>
                    <label class="savgy__toolbar__label savgy__toolbar__label--inline">
                        <input type="checkbox" value="1" class="savgy__toolbar__check savgy__toolbar__check--flatten">
                        Flatten transparency
                    </label>
                    <label class="savgy__toolbar__label savgy__toolbar__label--inline">
                        <input type="checkbox" value="1" class="savgy__toolbar__check savgy__toolbar__check--scaleImg">
                        Scale SVG images
                    </label>


                </div>
            </div>


        </div>

        <div class="savgy__toolbar__row savgy__toolbar__row--2">
            <div class="savgy__toolbar__col">
                <button type="button" class="savgy__button savgy__toolbar__button savgy__toolbar__button--copy"><span class="savgy__toolbar__button--save--download icn-svg savgy__icon">${icons.copy} </span> Copy</button>
            </div>
            <div class="savgy__toolbar__col">
                <button type="button" class="savgy__button--ready    savgy__button  savgy__toolbar__button savgy__toolbar__button--save"><span class="savgy__toolbar__button--save--download icn-svg savgy__icon savgy__icon--download">${icons.download} </span> <span class="savgy__toolbar__button--save--download icn-svg savgy__icon savgy__icon--spinner">${icons.spinner} </span> Save file</button>
                <a class="savgy__toolbar__a savgy__toolbar__button--a savgy__hidden" href="" download=""></a>
            </div>
        </div>
        <div class="saveg__filesize__wrp">
            <p class="saveg__filesize">Filesize: 0 KB</p>
        </div>


    </div>

    <div class="savgy__toolbar__toggle">
        <label class="savgy__toolbar__toggle__label">
            <span class="savgy__toolbar__toggle__icon savgy__toolbar__toggle__icon--download  savgy__icon savgy__icon--download">${icons.download}</span>
            <span class=" savgy__toolbar__toggle__icon savgy__toolbar__toggle__icon--close savgy__icon savgy__icon--close">${icons.close}</span>
            <input class="savgy__toolbar__toggle__input savgy__hidden" type="checkbox" value="1">
        </label>
    </div>
</div>
`;

    let toolbar = new DOMParser().parseFromString(toolbarMarkup, 'text/html').querySelector('div');

    let inpW = toolbar.querySelector('.savgy__toolbar__input--w');
    let inpH = toolbar.querySelector('.savgy__toolbar__input--h');
    let inpQ = toolbar.querySelector('.savgy__toolbar__input--quality');
    let inpF = toolbar.querySelector('.savgy__toolbar__select--format');
    let inpPreview = toolbar.querySelector('.savgy__toolbar__check--preview');
    //preferred format for images in SVG
    let inpFSVG = toolbar.querySelector('.savgy__toolbar__select--format--img');

    let btnCopy = toolbar.querySelector('.savgy__toolbar__button--copy');
    let btnSave = toolbar.querySelector('.savgy__toolbar__button--save');
    let linkFile = toolbar.querySelector('.savgy__toolbar__button--a');
    let inputToggleLabel = toolbar.querySelector('.savgy__toolbar__toggle__label');
    let inputToggle = toolbar.querySelector('.savgy__toolbar__toggle__input');
    let inputIntrinsic = toolbar.querySelector('.savgy__toolbar__check--intrinsic');
    let inputCrop = toolbar.querySelector('.savgy__toolbar__check--crop');
    let inputFlatten = toolbar.querySelector('.savgy__toolbar__check--flatten');
    let inputScaleImages = toolbar.querySelector('.savgy__toolbar__check--scaleImg');

    let selectSVGIMGFormat = toolbar.querySelector('.savgy__toolbar__select--format--img');
    let pFilesize = toolbar.querySelector('.saveg__filesize');


    return { toolbar, inpW, inpH, inpQ, inpF, inpFSVG, btnCopy, btnSave, linkFile, inpPreview, inputToggleLabel, inputToggle, inputIntrinsic, inputCrop, selectSVGIMGFormat, pFilesize, inputFlatten, inputScaleImages }

}

/**
 * create blob from
 * data URL
 */
function dataURLToBlob(dataUrl) {
    // Check if the data URL is valid
    if (!dataUrl.startsWith('data:')) {
        throw new Error('Invalid data URL format');
    }

    // Split into metadata and data parts
    const [metaPart, dataPart] = dataUrl.split(',');
    if (!metaPart || !dataPart) {
        throw new Error('Invalid data URL format');
    }

    // Extract MIME type (handle cases like `charset=utf8`)
    const mimeMatch = metaPart.match(/^data:([^;]+)/);
    const mimeString = mimeMatch ? mimeMatch[1] : 'application/octet-stream';

    let byteString;
    if (metaPart.includes('base64')) {
        // Handle base64-encoded data
        byteString = atob(dataPart);
    } else {
        // Handle URL-encoded data (e.g., SVG with %3C, %20, etc.)
        byteString = decodeURIComponent(dataPart);
    }

    // Convert to ArrayBuffer → Blob
    const buffer = new ArrayBuffer(byteString.length);
    const uintArray = new Uint8Array(buffer);
    for (let i = 0; i < byteString.length; i++) {
        uintArray[i] = byteString.charCodeAt(i);
    }

    return new Blob([buffer], { type: mimeString });
}


/**
 * convert blob to 
 * base64 data URL
 */
function blobToBase64(blob) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result);
        reader.onerror = (err) => reject(err);
        reader.readAsDataURL(blob);
    });
}

/**
 * inline references for patterns, masks, 
 * clipaths etc
 */

function inlineRefs(svg, assetCache={}) {
    let els = svg.querySelectorAll('path, polyline, polygon, rect, circle, ellipse, line, text, tspan');
    //attributes that may reference an external asset e.g a gradient, pattern, mask etc
    let props = ['fill', 'stroke', 'clip-path', 'mask', 'href', 'xlink:href'];

    // create defs elements
    let defs = svg.querySelector('defs');
    if (!defs) {
        defs = document.createElementNS(ns, 'defs');
        svg.insertBefore(defs, svg.children[0]);
    }


    for (let i = 0; i < els.length; i++) {
        let el = els[i];

        let styles = window.getComputedStyle(el);
        let refID, refEl, refEl_clone;

        for (let p = 0; p < props.length; p++) {
            let prop = props[p];
            let att = el.getAttribute(prop);
            let value = att ? att : styles.getPropertyValue(prop);
            let isHref = prop === 'href' || prop === 'xlink:href' ? true : false;

            /**
             * check props to find
             * patterns, gradients, masks etc
             */
            if (value.includes('url') || isHref) {

                //url = getUrls(value)
                refID = isHref ? value.substring(1) : getUrls(value).substring(1);
                //console.log('has ref', prop, url, refID);

                refEl = refID ? svg.getElementById(refID) : null;
                // already present in svg
                if (refEl) {
                    //console.log('exists');
                    continue;
                }

                // not in parent SVG - copy
                refEl = refID ? document.getElementById(refID) : null;
                if (refEl) {
                    refEl_clone = refEl.cloneNode(true);
                    //svg.insertBefore(refEl_clone, svg.children[0]);
                    defs.append(refEl_clone);
                }
            }
        }
    }

    function getUrls(str) {
        let regex = /url\((['"]?)([^'"()]+)\1\)/gi;
        let match = str.match(regex) ? str.match(regex)[0].split(/\(|\)/)[1].replace(/"|'/g, '') : '';
        return match
    }

}



async function inlineUseRefs(svg, assetCache = {}) {

    let useEls = svg.querySelectorAll('use');
    if (!useEls.length) return;

    const ns = "http://www.w3.org/2000/svg";
    const nsXlink = "http://www.w3.org/1999/xlink";

    // create defs elements
    let defs = svg.querySelector('defs');
    if (!defs) {
        defs = document.createElementNS(ns, 'defs');
        svg.insertBefore(defs, svg.children[0]);
    }


    for (let i = 0; i < useEls.length; i++) {
        let use = useEls[i];
        let href = use.getAttributeNS(nsXlink, 'href') ? use.getAttributeNS(nsXlink, 'href') : use.getAttribute('href');

        // xlink sucks - we normalize it to href
        use.removeAttribute('xlink:href');


        // find external use references
        let hrefArr = href.split('#').filter(Boolean);
        let [url, id] = hrefArr;

        let isExternal = hrefArr.length > 1;
        let inlineRef, inlineDef, defId;

        // use definition is in document
        let inlinedSymbol = document.getElementById(hrefArr[0]);
        if (inlinedSymbol) {
            inlineDef = inlinedSymbol.cloneNode(true);
            //svg.insertBefore(inlineDef, svg.children[0]);
            defs.append(inlineDef);
            inlineRef = `#${hrefArr[0]}`;
            use.setAttribute('href', inlineRef);
        }

        // fetch external ref
        else if (isExternal) {
            let spriteName = url.replace(/\./g, '_');

            // is cached
            if (assetCache[url] && assetCache[url][id]) {
                //console.log('is cached', id);
                inlineDef = assetCache[url][id].cloneNode(true);
            } else {

                // fetch and cache
                let res = await fetch(url);
                if (res.ok) {

                    let spriteMarkup = await res.text();
                    let sprite = new DOMParser().parseFromString(spriteMarkup, 'text/html').querySelector('svg');
                    let symbol = sprite.getElementById(id);

                    // cache
                    assetCache[url] = {};
                    assetCache[url][id] = symbol;
                    inlineDef = symbol.cloneNode(true);
                }
            }

            defId = `${spriteName}_${id}`;
            inlineDef.id = defId;

            // add to svg if not already done
            if (!svg.getElementById(defId)) {
                //svg.insertBefore(inlineDef, svg.children[0]);
                defs.append(inlineDef);
            }

            // new reference
            use.setAttribute('href', `#${defId}`);
        }
        //console.log(assetCache);
    }

}

async function optimizeSVGImgs(svgClone, assetCache = {}, addGlobalStyles = true, width = null, height = null, format = 'original', quality = 1, scaleImages = true, bgColor='transparent') {


    let svgOpt = svgClone.cloneNode(true);
    let images = svgOpt.querySelectorAll("image, img");
    let nsXlink = "http://www.w3.org/1999/xlink";

    if (!images.length || format === 'original') return svgOpt;

    // normalize format identifiers
    //console.log('format', format, assetCache);
    format = format ? format.replace(/image\//g, '').replace(/jpg/g, 'jpeg') : 'original';


    await Promise.all([...images].map(async (img) => {
        let type = img.nodeName.toLowerCase();
        let src = img.getAttributeNS(nsXlink, "href")
            ? img.getAttributeNS(nsXlink, "href")
            : (type === 'image' ? img.getAttribute("href") : img.getAttribute("src"));

        // all image src are already base64
        let base64 = src;

        /**
         * convert to other format
         * helps to optimize size for 
         * self-contained SVGs
         */
        if (format !== 'original') {

            let canvas = document.createElement("canvas");
            let ctx = canvas.getContext("2d");

            let imgTmp = new Image();
            imgTmp.crossOrigin = "anonymous";
            imgTmp.src = base64;

            // wait for image decoding
            await imgTmp.decode();

            // intrinsic dimensions
            let [width, height] = [imgTmp.naturalWidth, imgTmp.naturalHeight];

            // element dimension
            let [w, h] = [+img.getAttribute('width') || width, +img.getAttribute('height') || height];

            // scale down to layout size
            if (scaleImages) [width, height] = [w, h];


            imgTmp.width = width;
            imgTmp.height = height;
            canvas.width = width;
            canvas.height = height;

            // add white background for jpegs to flatten transparency
            if (format === 'jpeg') {
                let svgBG = window.getComputedStyle(svgOpt).backgroundColor;
                if ((svgBG === 'rgba(0, 0, 0, 0)' || svgBG !== 'transparent')) {
                    ctx.fillStyle = '#ffffff';
                    ctx.fillRect(0, 0, width, height);
                }
            }

            await ctx.drawImage(imgTmp, 0, 0, width, height);

            base64 = canvas.toDataURL(`image/${format}`, quality);
            canvas.remove();
        }


        if (type === 'image') {
            img.setAttribute("href", base64);
        } else {
            img.setAttribute("src", base64);
        }

    }));


    //console.log('svgOpt', svgOpt);

    return svgOpt;

}



/**
 * inline external
 * image sources
 */

async function inlineImages(svg, assetCache = {}) {

    let images = svg.querySelectorAll("image, img");
    let nsXlink = "http://www.w3.org/1999/xlink";


    await Promise.all([...images].map(async (img) => {
        let type = img.nodeName.toLowerCase();
        let src = img.getAttributeNS(nsXlink, "href")
            ? img.getAttributeNS(nsXlink, "href")
            : (type === 'image' ? img.getAttribute("href") : img.getAttribute("src"));

        let base64;

        // use cache
        if (assetCache.images[src]) {
            //console.log('is cached!', src);
            base64 = assetCache.images[src];
        } else {

            // check if src is already data URL
            if (src.startsWith('data:')) {
                //cache_key = `${src}_original_1`;
                base64 = src;
            } else {
                let blob = await (await fetch(src)).blob();
                base64 = await blobToBase64(blob);

                // add to cache
                assetCache.images[src] = base64;
            }
        }

        if (type === 'image') {
            img.setAttribute("href", base64);
        } else {
            img.setAttribute("src", base64);
        }
    }));

}




async function svg2Canvas2DataUrl(svg, width = null, height = null, format = 'png', quality = '1', bgColor='transparent', flattenTransparency=false, canvas = null) {

    let isString = typeof svg === 'string';
    let noDimensions = !width || !height;
    let nsAtt = 'xmlns="http://www.w3.org/2000/svg"';
    let hasNS = isString ? svg.includes(nsAtt) : svg.getAttribute('xmlns');

    if (!hasNS && isString) {
        svg = svg.replace('<svg ', `<svg ${nsAtt} `);
    }

    // Detect dimensions if not specified
    if (noDimensions) {

        // parse to retrieve width/height and viewBox
        if (isString) {
            svg = new DOMParser().parseFromString(svg, 'text/html').querySelector('svg');

            let viewBox = svg.getAttribute('viewBox') || '0 0 300 150';
            let [, , w, h] = viewBox.split(/,| /);

            [width, height] = +svg.getAttribute("width") && +svg.getAttribute("height") ?
                [svg.width, svg.height]
                : [w, h];

            // force serializing to add missing namespace
            isString = false;

        }

        // set explicit svg dimensions for better compatibility
        svg.setAttribute("width", width);
        svg.setAttribute("height", height);

    }



    let svgMarkup = isString ? svg : new XMLSerializer().serializeToString(svg);
    let dataUrl = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(svgMarkup);

    let img = new Image();
    img.crossOrigin = "anonymous";
    img.src = dataUrl;
    img.width = width;
    img.height = height;

    /**
     * create canvas
     * if not existent
     */
    if (!canvas) {
        canvas = document.createElement("canvas");
    }
    canvas.width = width;
    canvas.height = height;
    let ctx = canvas.getContext("2d");

    // normalize format identifiers
    format = format.replace(/image\//g, '').replace(/jpg/g, 'jpeg');

    // add white background for jpegs to flatten transparency
    if (format === 'jpeg' || flattenTransparency) {

        //console.log('flatten', flattenTransparency);
        bgColor = bgColor!=='transparent' ? bgColor : '#fff';
        ctx.fillStyle = bgColor;
        ctx.fillRect(0, 0, width, height);
    }

    //wait for image to decode
    await img.decode();
    await ctx.drawImage(img, 0, 0, width, height);

    return canvas.toDataURL(`image/${format}`, quality);

}

/**
 * convert all external font references to
 * inlined base64 encoded dataURLs
 */

async function externalFontsToBase64(css, assetCache = {}) {
    // Initialize fonts cache if not exists
    if (!assetCache.fonts) {
        assetCache.fonts = {};
    }

    let stylesheet = new CSSStyleSheet();
    stylesheet.replaceSync(css);

    let fontFaceRules = [...stylesheet.cssRules].filter((item) => item.type === 5);

    let urlArr = fontFaceRules.map(rule => {
        let src = rule.style.getPropertyValue('src');
        return src ? src.split(',').map(val => val.trim()) : [];
    });

    let urls = [];

    urlArr.forEach(fontSrc => {
        if (!fontSrc || !fontSrc.length) return;

        let fontArr = [];
        let fontSrcStr = [];
        let ruleStr = fontSrc.join(', ');

        fontSrc.forEach(url => {
            if (!url || url === 'undefined') return;

            let ext = url.split('/').slice(-1)[0].split('?')[0].split('.').slice(-1)[0];
            if (ext === 'eot' || ext === 'svg') {
                css = css.replaceAll(url, '');
            } else {
                fontSrcStr.push(url);
                let cleanUrl = url.replace(/url\(|\)|'|"/gi, '').split(' ')[0];
                if (cleanUrl && cleanUrl !== 'undefined') {
                    fontArr.push(cleanUrl);
                }
            }
        });

        if (fontSrcStr.length) {
            let ruleStrNew = fontSrcStr.join(', ');
            css = css.replace(ruleStr, ruleStrNew);
        }

        if (fontArr[0]) {
            urls.push(fontArr[0]);
        }
    });

    if (!urls.length) {
        return css;
    }

    const replacements = await Promise.all(urls.map(async (url) => {
        if (!url || url.startsWith('data:')) {
            return { url, base64: url };
        }

        try {
            if (assetCache.fonts[url]) {
                return { url, base64: assetCache.fonts[url] };
            }

            const res = await fetch(url);
            if (!res.ok) throw new Error(`Fetch failed with status ${res.status}`);

            const blob = await res.blob();
            const base64 = await blobToBase64(blob);
            assetCache.fonts[url] = base64;

            return { url, base64 };
        } catch (error) {
            console.error(`Error processing font ${url}:`, error);
            return { url, base64: url };
        }
    }));

    let processedCss = css;
    for (const { url, base64 } of replacements) {
        if (url) {
            processedCss = processedCss.replaceAll(url, base64);
        }
    }

    return processedCss;
}



/**
 * collect all fonts and characters
 * required for SVG rendering
 * used to remove unnecessary font-face rules
 * and apply google font API subsetting if possible
 */

function analizeSVGText(svg) {

    // collect used fonts
    let usedFonts = {};

    // collect subset strings for families
    let familyStrings = {};


    // query text elements
    let textEls = svg.querySelectorAll('text, tspan, textPath, foreignObject *');

    let allText = [... new Set([...[...textEls].map(el => el.textContent).join('').split('')])].join('');

    // if no text elements are present
    if (!textEls.length) return usedFonts;


    for (let i = 0, len = textEls.length; len && i < len; i++) {
        let text = textEls[i];

        //check if element has any text nodes
        let textNodes = [...text.childNodes].filter(node => node.nodeType === 3 && node.textContent.trim());
        if (!textNodes.length) continue;

        let style = window.getComputedStyle(text);
        let [fontFamily, fontWeight, fontStyle, fontStretch] = [style.getPropertyValue('font-family'), style.getPropertyValue('font-weight'), style.getPropertyValue('font-style'), style.getPropertyValue('font-stretch')||'100'];

        fontFamily = fontFamily.replace(/"|'| |%/g, '');

        // subset string
        let subsetChars = textNodes.map(node => node.textContent).join('').trim();
        let charsUnique = [...new Set([...subsetChars.split('')])].join('').replaceAll('\n', '');

        // check unicode range/language
        let language = detectLanguageSet(unicodeRangeFromString(charsUnique));
        // change PUA to latin for icon fonts
        if (language === 'PUA') language = 'latin';

        // create unique key for font style
        let key = [fontFamily, fontWeight, fontStyle, fontStretch, language].join('_').replace(/"|'| |%/g, '');

        if (!usedFonts[key]) {
            usedFonts[key] = [];
        }

        if (!familyStrings[fontFamily]) {
            familyStrings[fontFamily] = [];
        }
        familyStrings[fontFamily].push(charsUnique);

    }

    //flatten substring array
    for (let family in usedFonts) {
        let familyName = family.split('_')[0];
        usedFonts[family] = [...new Set(familyStrings[familyName].join('').split(''))].join('');
    }

    usedFonts.allText = allText;
    return usedFonts;

}


/**
 * detect unnecessary 
 * font face rules
 */
function checkFontCoverage(sheet, svgFontInfo = {}, isGoogleFont = false, subsets = [], subset = '', hasMulti = false, availableFonts={}) {
    let fontsToSubset = [];
    let exclude = [];

    // compare fonts with required
    for (let i = 0, len = sheet.cssRules.length; len && i < len; i++) {
        let rule = sheet.cssRules[i];
        let type = rule.type;

        // is fontface
        if (type === 5) {
            let [fontFamily, fontWeight, fontStyle, fontStretch] = [
                rule.style.getPropertyValue('font-family').replace(/"|'| /g, ''),
                rule.style.getPropertyValue('font-weight') || '400',
                rule.style.getPropertyValue('font-style') || 'normal',
                rule.style.getPropertyValue('font-stretch')||'100',
            ];
            let subsetCurrent = subset ? subset : (subsets[i] ? subsets[i] : 'latin');
            fontWeight = fontWeight.split(' ').map(val => convertFontValues(val, 'weight'));
            fontStretch = fontStretch.split(' ').map(val => convertFontValues(val, 'stretch'));


            let font_key = [fontFamily, fontWeight[0], fontStyle, fontStretch[0], subsetCurrent].join('_').replace(/"|'| /g, '').trim();
            let isVF = fontWeight.length > 1 || fontStretch.length > 1 ? true : false;


            if (isVF) {
                for (let key in svgFontInfo) {
                    if (key !== 'allText') {
                        let [family, weight, style, stretch, subset] = key.split('_');
                        if (family === fontFamily &&
                            style === fontStyle &&
                            (+weight >= fontWeight[0]) &&
                            (+stretch >= fontStretch[0]) &&
                            (subset === subsetCurrent)
                        ) {
                            if (isGoogleFont) fontsToSubset.push(font_key);
                        }
                    }
                }
            }

            // exact static match
            else if (svgFontInfo[font_key]) {
                //console.log('has font subset', font_key);
                if (isGoogleFont) fontsToSubset.push(font_key);
            }

            // no match try to adjust non existent weights or widths
            else {
                exclude.push(i);
            }
        }
    }
    return { fontsToSubset, exclude }
}


/**
 * colllect all style info from 
 * current stylesheet
 */
function checkAvailableFonts(sheet, subsets = [], subset = '') {

    let fontDataLoaded = {};

    // compare fonts with required
    for (let i = 0, len = sheet.cssRules.length; len && i < len; i++) {
        let rule = sheet.cssRules[i];
        let type = rule.type;

        // is fontface
        if (type === 5) {
            let [fontFamily, fontWeight, fontStyle, fontStretch] = [
                rule.style.getPropertyValue('font-family').replace(/"|'| /g, ''),
                rule.style.getPropertyValue('font-weight') || '400',
                rule.style.getPropertyValue('font-style') || 'normal',
                rule.style.getPropertyValue('font-stretch') || '100',
            ];
            let subsetCurrent = subset ? subset : (subsets[i] ? subsets[i] : 'latin');

            // collect all available weights and styles
            if (!fontDataLoaded[fontFamily]) {
                fontDataLoaded[fontFamily] = { weights: new Set([]), widths: new Set([]), styles: new Set([]), isVF: false, subsets: new Set([]), keys: new Set() };
            }

            // normalize weights and widths string literals            
            fontWeight = fontWeight.split(' ').map(val => convertFontValues(val, 'weight'));
            fontStretch = fontStretch.split(' ').map(val => convertFontValues(val, 'stretch')).filter(Boolean);

            let isVF = fontWeight.length > 1 || fontStretch.length > 1 ? true : false;

            // add weights
            fontWeight.forEach(wght => fontDataLoaded[fontFamily].weights.add(wght));
            fontStretch.forEach(wdth => fontDataLoaded[fontFamily].widths.add(wdth));
            fontDataLoaded[fontFamily].isVF = isVF;
            fontDataLoaded[fontFamily].subsets.add(subsetCurrent);
            fontDataLoaded[fontFamily].styles.add(fontStyle);

        }
    }

    return fontDataLoaded
}


/**
 * adjust font info to available 
 * weights and styles
 */

function updateFontInfo(availableFonts, svgFontInfo) {

    for (let key in svgFontInfo) {
        if (key !== 'allText') {
            let [family, weight, style, stretch, subset] = key.split('_');
            [weight, stretch] = [weight, stretch].map(Number);
            let fontItem = availableFonts[family];

            if (!fontItem) {
                continue;
            }

            if (
                fontItem.styles.has(style) &&
                fontItem.subsets.has(subset)
            ) {

                let stretchNew = stretch, weightNew = weight;

                // 1. check weights
                if (!fontItem.weights.has(weight)) {

                    let weights = [...fontItem.weights];
                    let weightMin = Math.min(...weights);
                    let weightMax = Math.max(...weights);
                    //console.log('fontItem match:', family, weight, weightMin, weightMax, fontItem);

                    // too bold
                    if (weight > weightMax) {
                        //console.log('too bold', family, weight);
                        weightNew = weightMax;
                    }

                    // too light
                    if (weight < weightMin) {
                        //console.log('too light', family, weight);
                        weightNew = weightMin;
                    }
                }


                // 2. check stretch
                if (!fontItem.widths.has(stretch)) {

                    let widths = [...fontItem.widths];
                    let widthMin = Math.min(...widths);
                    let widthMax = Math.max(...widths);

                    // too condensed
                    if (stretch < widthMin) {
                        //console.log('too condensed', family, weight);
                        stretchNew = widthMin;
                    }

                    // too expanded
                    if (stretch > widthMax) {
                        //console.log('too expanded', family, weight);
                        stretchNew = widthMax;
                    }
                }

                //update
                if(weightNew!=weight || stretchNew!==stretch){
                    let keyNew = [family, weightNew, style, stretchNew, subset].join('_');
                    svgFontInfo[keyNew] = svgFontInfo[key];

                    delete svgFontInfo[key];
                }
            }
        }
    }

    //console.log('svgFontInfo new', svgFontInfo);
    return svgFontInfo

}



/**
 * convert string literal font values 
 * to numeric
 */
function convertFontValues(value, type = 'weight') {
    if (!isNaN(value)) return parseFloat(value);
    value = value.trim().toLowerCase();

    if (type === 'stretch') {
        if (value.includes('%')) return parseFloat(value);

        const fontWidths = {
            'ultra-condensed': 50,
            'extra-condensed': 62.5,
            'condensed': 75,
            'semi-condensed': 87.5,
            'normal': 100,
            'semi-expanded': 112.5,
            'expanded': 125,
            'extra-expanded': 150,
            'ultra-expanded': 200,
        };
        return fontWidths[value] || 100; // default to normal if unknown
    }

    if (type === 'weight') {
        const fontWeights = {
            'thin': 100,
            'extra-light': 200,
            'ultra-light': 200,
            'light': 300,
            'normal': 400,
            'regular': 400,
            'medium': 500,
            'semi-bold': 600,
            'demi-bold': 600,
            'bold': 700,
            'extra-bold': 800,
            'ultra-bold': 800,
            'black': 900,
            'heavy': 900,
        };
        return fontWeights[value] || 400; // default to normal if unknown
    }

}




/**
 * detect unicode range
 */

function detectLanguageSet(unicodeRangeStr) {
    // Define known ranges (based on Unicode standards)
    let knownRanges = {
        'latin': [
            [0x0020, 0x007f], // Basic Latin
        ],

        'latin-ext': [
            [0x00a0, 0x00ff], // Latin-1 Supplement
            [0x0100, 0x017f], // Latin Extended-A
            //[0x0180, 0x024f] // Latin Extended-B
        ],

        'cyrillic': [
            [0x0400, 0x04ff], // Cyrillic
            //[0x0500, 0x052f], // Cyrillic Supplement
            //[0x2de0, 0x2dff], // Cyrillic Extended-A
            //[0xa640, 0xa69f] // Cyrillic Extended-B
        ],

        'cyrillic-ext': [
            [0x0500, 0x052f], // Cyrillic Supplement
            [0x2de0, 0x2dff], // Cyrillic Extended-A
            [0xa640, 0xa69f] // Cyrillic Extended-B
        ],

        'greek': [
            [0x0370, 0x03ff], // Greek and Coptic
            //[0x1f00, 0x1fff] // Greek Extended
        ],

        'greek-ext': [
            [0x0370, 0x03ff], // Greek and Coptic
            [0x1f00, 0x1fff] // Greek Extended
        ],

        'vietnamese': [
            [0x0102, 0x0103], // Vietnamese letters (Latin Extended-A subset)
            //[0x0110, 0x0111],
            //[0x0128, 0x0129],
            //[0x0168, 0x0169],
            //[0x01a0, 0x01a1],
            //[0x01af, 0x01b0],
            //[0x1ea0, 0x1ef9] // Vietnamese-specific Latin range
        ],


        'arabic': [
            [0x0600, 0x06ff], // Arabic
            //[0x0750, 0x077f], // Arabic Supplement
            //[0x08a0, 0x08ff], // Arabic Extended-A
            //[0xfb50, 0xfdff], // Arabic Presentation Forms-A
            //[0xfe70, 0xfeff] // Arabic Presentation Forms-B
        ],

        'hebrew': [
            [0x0590, 0x05ff], // Hebrew
            //[0xfb1d, 0xfb4f] // Hebrew Presentation Forms
        ],

        'PUA': [
            [0xE000, 0xF8FF],
            [0xF0000, 0xFFFFD],
            [0x100000, 0x10FFFD],
        ],

        /*
        'math': [
            [0x0020, 0x007f], // Basic Latin
            [0x0393, 0x25CA],
        ]
        */

        // Define more language ranges as needed
    };

    const parseUnicodeRange = (range) => {
        return range.split(",").map((part) => {
            const [start, end] = part.trim().replace("U+", "").split("-");
            const startCode = parseInt(start, 16);
            const endCode = end ? parseInt(end, 16) : startCode;
            return [startCode, endCode];
        });
    };

    const calculateAbsoluteOverlap = (userRanges, knownRange) => {
        let overlapCount = 0;

        knownRange.forEach(([knownStart, knownEnd]) => {
            userRanges.forEach(([userStart, userEnd]) => {
                const start = Math.max(knownStart, userStart);
                const end = Math.min(knownEnd, userEnd);
                if (start <= end) overlapCount += end - start + 1;
            });
        });

        return overlapCount;
    };

    // Parse user-specified unicode ranges
    const userRanges = parseUnicodeRange(unicodeRangeStr);

    // Calculate absolute overlaps for each language set
    const detectedSets = [];
    for (const [lang, ranges] of Object.entries(knownRanges)) {
        const overlapCount = calculateAbsoluteOverlap(userRanges, ranges);
        if (overlapCount > 0) {
            detectedSets.push({ language: lang, overlap: overlapCount });
        }
    }

    let bestMatch = detectedSets.sort((a, b) => b.overlap - a.overlap);
    bestMatch = bestMatch.length ? bestMatch[0].language : '';
    //console.log('bestMatch', bestMatch);
    // Return the best-matching language(s), sorted by overlap percentage
    return bestMatch;
}

function toUnicodeRange(codePoints) {
    //alert('oi')
    // Sort code points in ascending order
    codePoints.sort((a, b) => a - b);

    // Helper to format a single code point as U+XXXX
    let formatCodePoint = (point, addPrefix = true) => {
        let prefix = addPrefix ? 'U+' : '';
        return prefix + point.toString(16).toUpperCase().padStart(4, '0');
    };

    // Array to store ranges
    let ranges = [];
    let start = codePoints[0];
    let end = start;

    for (let i = 1; i < codePoints.length; i++) {
        if (codePoints[i] === end + 1) {
            // Continue the range if the next code point is consecutive
            end = codePoints[i];
        } else {
            // Add the current range to the list
            ranges.push(start === end ? formatCodePoint(start) : `${formatCodePoint(start)}-${formatCodePoint(end, false)}`);
            // Start a new range
            start = codePoints[i];
            end = start;
        }
    }

    // Add the final range
    ranges.push(start === end ? formatCodePoint(start) : `${formatCodePoint(start)}-${formatCodePoint(end, false)}`);

    // Join all ranges with commas
    return ranges.join(", ");
}


function unicodeRangeFromString(str) {
    let chars = [... new Set(str.split('').filter(Boolean))];
    let codePoints = chars.map(char => { return char.charCodeAt(0) });
    let range = toUnicodeRange(codePoints);
    return range
}

/**
 * inline global stylesheets for 
 * self-contained SVG
 */

async function inlineGlobalStyles(svg, svgFontInfo = {}, assetCache = {}) {
    // Initialize CSS cache if not exists
    if (!assetCache.css) {
        assetCache.css = {};
    }

    let globalCSS = '';
    let styleSheets = document.styleSheets;

    for (let sheet of styleSheets) {
        // skip inline SVG styles
        const ownerNode = sheet.ownerNode;
        if (ownerNode && ownerNode.closest('svg')) continue;

        // Process external stylesheets
        if (sheet.href) {
            let url = sheet.href;
            let isGoogleFont = url.includes('fonts.google');
            let css, subsets = [];
            let hasMulti = false;

            // Check cache first
            if (assetCache.css[url]) {
                css = assetCache.css[url].css;
                subsets = assetCache.css[url].subsets || [];
                hasMulti = assetCache.css[url].hasMulti || false;
            } else {
                // Fetch if not in cache
                try {
                    let res = await fetch(url);
                    if (res.ok) {
                        css = await res.text();
                        
                        // Remove imports
                        let reg = /@import\s+(?:url\()?["']?([^"')]+)["']?\)?[^;]*;?/gi;
                        let imports = css.match(reg) || [];
                        imports.forEach(imp => {
                            css = css.replace(imp, '');
                        });

                        // Fix relative URLs
                        css = relativeToAbsoluteUrls(css, url);

                        if (isGoogleFont) {
                            // Retrieve subsets from comments
                            let comments = [...css.matchAll(/\/\*\s*(.*?)\s*\*\//g)];
                            subsets = comments ? comments.map(match => match[1]) : [];
                            let families = url.split('family=').slice(1);
                            hasMulti = families.length > 1;
                        }

                        // Cache the response
                        assetCache.css[url] = {
                            css,
                            subsets,
                            hasMulti,
                            isGoogleFont
                        };
                    }
                } catch (error) {
                    console.error(`Error fetching stylesheet ${url}:`, error);
                    continue;
                }
            }

            if (!css) continue;

            let sheetInline = new CSSStyleSheet();
            sheetInline.replaceSync(css);

            // Check all available fonts
            let availableFonts = checkAvailableFonts(sheetInline, subsets);

            // Update fontinfo
            updateFontInfo(availableFonts, svgFontInfo);

            let { exclude, fontsToSubset } = checkFontCoverage(
                sheetInline, svgFontInfo, isGoogleFont, subsets, '', hasMulti, availableFonts
            );

            // Handle Google Font subsetting
            if (fontsToSubset.length && isGoogleFont) {
                let key = fontsToSubset[0];
                let subsetStr = hasMulti ? svgFontInfo.allText : svgFontInfo[key];
                let subsetQuery = url + '&text=' + encodeURIComponent(subsetStr);

                // Check cache for subset
                if (!assetCache.css[subsetQuery]) {
                    try {
                        let res = await fetch(subsetQuery);
                        if (res.ok) {
                            let subsetCss = await res.text();
                            assetCache.css[subsetQuery] = {
                                css: subsetCss,
                                isSubset: true
                            };
                            css = subsetCss;
                            sheetInline = new CSSStyleSheet();
                            sheetInline.replaceSync(css);

                            let subset = 'latin';
                            ({ exclude, fontsToSubset } = checkFontCoverage(
                                sheetInline, svgFontInfo, isGoogleFont, subsets, subset, hasMulti, availableFonts
                            ));
                        }
                    } catch (error) {
                        console.error(`Error fetching font subset ${subsetQuery}:`, error);
                    }
                } else {
                    // Use cached subset
                    css = assetCache.css[subsetQuery].css;
                    sheetInline = new CSSStyleSheet();
                    sheetInline.replaceSync(css);
                }
            }

            // Remove unused rules
            for (let len = exclude.length, i = len - 1; len && i >= 0; i--) {
                let indx = exclude[i];
                if (sheetInline.cssRules[indx]) {
                    sheetInline.deleteRule(indx);
                }
            }

            sheet = sheetInline;
        }

        if (sheet.cssRules.length) {
            // Check if SVG is affected by rules
            for (let i = 0, len = sheet.cssRules.length; len && i < len; i++) {
                let rule = sheet.cssRules[i];
                let type = rule.type;

                if (type === 1) {
                    let selector = rule.selectorText;
                    if (!svg.querySelector(selector)) continue;
                }

                globalCSS += rule.cssText + '\n\n';
            }
        }
    }

    return globalCSS;
}


/**
 * find all imports
 * "hidden" in external stylesheets or
 * defined in style elements
 */

function getAllImportRules() {
    let importRules = [];
    let styleSheets = document.styleSheets;
    let currentOrigin = window.location.origin;
    //console.log('styleSheets', styleSheets);

    // Loop through all stylesheets in the document
    for (const sheet of styleSheets) {

        let sheetOrigin = sheet.href ? new URL(sheet.href).origin : currentOrigin;
        //console.log('sheet.href', sheet.href);

        // ignore external stylesheets
        if (sheetOrigin !== currentOrigin) continue;

        // ignore previously tagged as external stylesheets
        //if(sheet.ownerNode.classList.contains('externalCSS') ) continue;

        try {
            // Loop through all rules in the stylesheet
            for (const rule of sheet.cssRules) {
                if (rule instanceof CSSImportRule) {
                    importRules.push(rule.href);
                }
            }
        } catch (err) {
            // CORS error: Cannot access rules from cross-origin sheets
            console.warn(`Cannot read rules from stylesheet: ${sheet.href || 'inline <style>'}`, sheetOrigin);
        }
    }

    // deduplicate
    importRules = [...new Set([...importRules])];

    return importRules;
}



/**
 * convert imports to links
 * to facilitate CSS rule parsing
 */
async function importRulesToLink(importRules=[]) {

    for(let i=0,l=importRules.length; l&& i<l; i++){
        //let imp = importRules[i];
        let href = importRules[i];

        let link = document.createElement('link');
        link.href = href;
        link.rel = "stylesheet";
        link.classList.add('internalCSS');

        document.head.append(link);

        // Wait for the link to load before removing the @import
        await new Promise((resolve) => {
            link.onload = resolve;
            link.onerror = resolve; // Handle errors gracefully
        });

    }


}

/**
 * convert relative URLs to absolute
 * to enable correct fetching
 */

function relativeToAbsoluteUrls(css, url) {

    const urlRegex = /url\(\s*['"]?(.*?)['"]?\s*\)/g;
    let urls = css.match(urlRegex);

    if (!urls) return css;

    // exclude ie crap fonts
    urls = urls.filter(url => !url.includes('.eot'));

    let baseUrl = url;
    let pathArr = url.split('/');

    urls.forEach(url => {

        url = url.split(/\(|\)/)[1].replace(/"|'/g, '');
        let urlAbs = url;

        /**
         * Absolute URLs: 
         * find relative paths or 
         * parent directory traversal
         */
        let dirs = url.split('../');
        let parentDirs = url.match(/\.\.\//g);
        let relativeDir = url.match(/\.\//g);
        let traverse = parentDirs ? parentDirs.length : 0;

        // traverse to parent directory
        if (traverse) {
            let index = -1 - traverse;
            let fontPath = dirs.slice(-1)[0];
            baseUrl = pathArr.slice(0, index).join('/') + '/';
            urlAbs = baseUrl + fontPath;
        }
        else if (relativeDir) {
            urlAbs = url.replaceAll('./', baseUrl);
        }

        css = css.replaceAll(url, urlAbs);
    });

    //console.log(css);
    return css;
}

async function svg2bitmap(svg, {
    addGlobalStyles = true,
    format = 'original',
    formatSVG = 'original',
    quality = 0.9,
    scaleImages = false,
    addToolbar = true,
    showPreview = false,
    crop = false,
    useIntrinsic = false,
    flattenTransparency = false,
    bgColor = 'transparent'

} = {}) {


    let dataUrl, dataUrlSVG, width, height, toolbar, inpW, inpH, inpQ, inpF, btnCopy, btnSave, linkFile, inpPreview, inpFSVG, inputToggle, inputIntrinsic, inputCrop, selectSVGIMGFormat, pFilesize, inputFlatten, inputScaleImages;

    let hasSelfContainedSVG = false;

    /**
     * if is image
     */
    let isString = typeof svg === 'string';
    let isElement = !isString ? svg instanceof HTMLElement || svg instanceof SVGElement : false;
    let isImg = isElement ? svg.nodeName.toLowerCase() === 'img' : false;
    let imgEl = isImg ? svg : svg;

    if(isImg){
        //imgEl.classList.add('savgy__hidden');
        let res = await fetch(svg.src);
        if(res.ok){
            svg = await res.text() ;
            addGlobalStyles = false;
            hasSelfContainedSVG = true;
        }
    }


    /**
     * if input is SVG markup parse
     * if element continue processing
     */
    let isSVGString = svg.nodeType ? false : true;

    // take only styles in SVG
    if (isSVGString) addGlobalStyles = false;

    // normalize string or node input
    svg = !isSVGString ? svg : new DOMParser().parseFromString(svg, "text/html").querySelector("svg");
    //console.log('isElement', isElement, isImg, addGlobalStyles, hasSelfContainedSVG);



    let savgyWrap = document.createElement('div');

    // clone and append
    let svgClone = svg.cloneNode(true);
    //console.log('svgClone', svgClone);

    //svgClone.id='';
    svgClone.classList.add('savgy_clone', 'savgy__hidden');
    let previewImg;


    /**
     * add toolbar UI
     */
    if (addToolbar) {


        // add toolbar stylesheet
        await addStyles('savgy', true);


        savgyWrap.classList.add('savgy__wrap');
        //document.body.insertBefore(savgyWrap, svg);
        document.body.insertBefore(savgyWrap, imgEl);

        // preview img
        previewImg = document.createElement('img');
        previewImg.classList.add('savgy__preview', 'savgy__preview--hidden');


        // toolbar
        ({ toolbar, inpW, inpH, inpQ, inpF, btnCopy, btnSave, linkFile, inpFSVG, inpPreview, inputToggle, inputIntrinsic, inputCrop, selectSVGIMGFormat, pFilesize, inputFlatten, inputScaleImages } = getToolbar(icons));

        //console.log(toolbar);
        //savgyWrap.append(svg, svgClone, previewImg, toolbar);
        savgyWrap.append(imgEl, svgClone, previewImg, toolbar);

    } else {
        savgyWrap.style = 'position:relative; height:0px; overflow:hidden; opacity:0';
        savgyWrap.append(svgClone);
        document.body.append(savgyWrap);
    }

    /**
     * get  rendered background color
     * for JPG transparency flattening
     */
    bgColor = bgColor !== 'transparent' ? bgColor : getVisualBackgroundColor(svgClone);


    /**
     * get dimensions of svg
     * either from viewBox or
     * width/height attributes
     */
    let viewBoxAtt = svgClone.getAttribute('viewBox');
    let viewBox = viewBoxAtt ? viewBoxAtt.split(/,| /).filter(Boolean).map(Number) : [0, 0, 300, 150];

    [width, height] = svgClone.getAttribute("width") && svgClone.getAttribute("height") ?
        [+svgClone.width, +svgClone.height] :
        [viewBox[2], viewBox[3]];


    let [widthInt, heightInt] = [width, height];

    let scale = 1;
    let bb;

    // get size from DOM element
    if (!isSVGString) {
        bb = svgClone.getBoundingClientRect();
        scale = bb.width / width;
        //let [width, height] = [bb.width, bb.height];
    }

    [width, height] = [width * scale, height * scale];
    let [widthCavnvas, heightCanvas] = [width, height];

    let svgBB = svgClone.getBBox();
    let [x, y, widthCrop, heightCrop] = [svgBB.x, svgBB.y, svgBB.width, svgBB.height];


    /**
     * toolbar inputs
     */

    let aspect = width / height;

    // round to integers
    [width, height] = [width, height].map(val => +(val).toFixed(0));


    if (toolbar) {


        // set default widths
        inpW.value = width;
        inpH.value = height;
        inpQ.value = quality;
        inpF.value = format ? format : 'png';

        let inputs = [inpW, inpH, inpQ, inpF, inpFSVG, inputCrop, inputIntrinsic, inpPreview, inputFlatten, inputScaleImages];


        inputToggle.addEventListener('click', async (e) => {

            // set open class
            toolbar.classList.toggle('savgy__toolbar--open');

            // get selfcontained svg on menu open
            //if (!hasSelfContainedSVG) svgClone = await getSelfcontainedSVG(svgClone, assetCache, addGlobalStyles);
            hasSelfContainedSVG = true;
        });


        // Store references to relevant inputs
        const dimensionInputs = [inpW, inpH];
        const checkboxInputs = [inputCrop, inputIntrinsic];

        // Combined event listener for both input fields and checkboxes
        [...inputs, ...checkboxInputs].forEach(inp => {
            inp.addEventListener('input', async (e) => {
                //console.log('Event triggered by:', e.currentTarget.id || e.currentTarget.name);

                const current = e.currentTarget;
                let newWidth = +inpW.value;
                let newHeight = +inpH.value;

                // Handle dimension inputs (width/height)
                if (dimensionInputs.includes(current)) {
                    if (current === inpW) {
                        newHeight = Math.round(newWidth / aspect);
                    }
                    else if (current === inpH) {
                        newWidth = Math.round(newHeight * aspect);
                    }
                    //console.log('Dimension input changed', newWidth, newHeight);
                }
                // Handle checkbox changes (crop/intrinsic)
                else if (checkboxInputs.includes(current)) {
                    //console.log('Checkbox changed:', current.id || current.name);
                    // When crop/intrinsic changes, use those dimensions
                    const scaleCanvas = inputIntrinsic.checked ? 1 : scale;

                    if (inputCrop.checked) {
                        [newWidth, newHeight] = [widthCrop * scaleCanvas, heightCrop * scaleCanvas];
                    } else if (inputIntrinsic.checked) {
                        [newWidth, newHeight] = [widthInt * scaleCanvas, heightInt * scaleCanvas];
                    }
                }

                // Update input fields if values changed
                //console.log('dim', newWidth, +inpW.value);

                if (newWidth !== +inpW.value) {
                    inpW.value = newWidth;
                }
                if (newHeight !== +inpH.value) {
                    inpH.value = newHeight;
                }

                // disable intrinsic
                if (newWidth != widthInt || newHeight != heightInt) {
                    useIntrinsic = false;
                    inputIntrinsic.checked = false;
                }


                // Update state variables
                width = +inpW.value;
                height = +inpH.value;
                quality = +inpQ.value;
                format = inpF.value;
                formatSVG = inpFSVG.value;
                crop = inputCrop.checked;
                useIntrinsic = inputIntrinsic.checked;
                showPreview = inpPreview.checked;
                flattenTransparency = inputFlatten.checked;
                scaleImages = inputScaleImages.checked;


                let scaleCanvas = useIntrinsic ? 1 : scale;
                let [widthCanvas, heightCanvas] = useIntrinsic
                    ? [widthInt * scaleCanvas, heightInt * scaleCanvas]
                    : [width, height];


                // Toggle SVG format UI
                selectSVGIMGFormat.classList.toggle(
                    'savgy__toolbar__select--format--img--active',
                    format === 'svg_self'
                );



                // adjust viewBox for cropped images
                if (crop) {
                    [widthCanvas, heightCanvas] = [widthCrop * scaleCanvas, heightCrop * scaleCanvas];
                    svgClone.setAttribute('viewBox', [x, y, widthCrop, heightCrop].join(' '));
                }
                // revert viewBox
                else {
                    svgClone.setAttribute('viewBox', viewBox.join(' '));
                }


                // Round to integers
                [widthCanvas, heightCanvas] = [widthCanvas, heightCanvas].map(val => Math.ceil(val));

                // Update preview if needed
                if (showPreview) {
                    if(isImg) imgEl.style.visibility = 'hidden';
                    previewImg.classList.replace('savgy__preview--hidden', 'savgy__preview--visible');

                    ({ dataUrl, dataUrlSVG } = await updateOutput(
                        svg, svgClone, assetCache, addGlobalStyles,
                        formatSVG, widthCanvas, heightCanvas,
                        format, quality, scaleImages, bgColor, flattenTransparency
                    ));

                    previewImg.src = dataUrl;

                    if (dataUrl) {
                        let blob = dataURLToBlob(dataUrl);
                        pFilesize.textContent = 'Filesize: ~' + (+(blob.size / 1024).toFixed(2)) + ' KB';
                    }
                } else {
                    if(isImg) imgEl.style.visibility = 'visible';
                    previewImg.classList.replace('savgy__preview--visible', 'savgy__preview--hidden');
                }
            });
        });

        /**
         * copy
         */
        btnCopy.addEventListener('click', async (e) => {

            let copyText;
            btnCopy.classList.add('savgy--copying');

            if (format === 'svg') {
                copyText = new XMLSerializer().serializeToString(svg);
            }
            else if (format === 'svg_self') {
                copyText = new XMLSerializer().serializeToString(svgClone);
            }

            // bitmap format
            else {
                if (!dataUrl) {
                    dataUrl = await svg2Canvas2DataUrl(svgClone, width, height, format, quality, bgColor, flattenTransparency);
                }
                copyText = dataUrl;
            }

            navigator.clipboard.writeText(copyText);
            btnCopy.classList.remove('savgy--copying');

        });

        /**
         * save to file
         */
        btnSave.addEventListener('click', async (e) => {

            btnSave.classList.replace('savgy__button--ready', 'savgy__button--loading');

            // generate dataURL if not already done due to active preview
            if (!showPreview) {
                ({ dataUrl, dataUrlSVG } = await updateOutput(svg, svgClone, assetCache, addGlobalStyles, formatSVG, widthCavnvas, heightCanvas, format, quality, scaleImages, bgColor, flattenTransparency));
            }


            // get filename from svg id
            let fileName = svgClone.id ? svgClone.id : 'savgy';
            let ext = format.replace('image/', '').replace('jpeg', 'jpg').replace('svg_self', 'svg');


            // set download link
            linkFile.download = `${fileName}.${ext}`;
            linkFile.href = dataUrl;
            linkFile.click();

            btnSave.classList.replace('savgy__button--loading', 'savgy__button--ready');

        });


        /**
         * check if toolbar is hidden
         * switch to bottom alignment
         */

        let toolbarTop = 0;

        window.addEventListener('resize', e => {
            toolbarTop = toolbar.getBoundingClientRect().top;
            console.log('toolbarTop', toolbarTop);

            if (toolbarTop < 0) {
                toolbar.classList.add('savgy__toolbar--bottom');

            } else {
                toolbar.classList.remove('savgy__toolbar--bottom');
            }
        });

        window.addEventListener('DOMContentLoaded', e => {
            toolbarTop = toolbar.getBoundingClientRect().top;
            console.log('toolbarTop', toolbar, toolbarTop, toolbar.getBoundingClientRect());
            if (toolbarTop < 0) toolbar.classList.add('savgy__toolbar--bottom');
        });


    }

    // generate image directly
    else {
        if (!hasSelfContainedSVG) svgClone = await getSelfcontainedSVG(svgClone, assetCache, addGlobalStyles);
        hasSelfContainedSVG = true;

        ({ dataUrl, dataUrlSVG } = await updateOutput(svg, svgClone, assetCache, addGlobalStyles, formatSVG, width, height, format, quality, scaleImages, bgColor, flattenTransparency));
    }

    return { bmp: dataUrl, svg: dataUrlSVG }

}



async function updateOutput(svg, svgClone, assetCache, addGlobalStyles, formatSVG, width, height, format, quality, scaleImages, bgColor = '#fff', flattenTransparency = false) {

    //console.log('formatSVG', formatSVG, width, height, 'format', format, quality, scaleImages);

    let dataUrl, dataUrlSVG;

    /**
     * minify SVG
     */

    // original SVG
    if (format === 'svg') {
        dataUrl = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(new XMLSerializer().serializeToString(svg));
        dataUrlSVG = dataUrl;
    }
    else if (format === 'svg_self') {
        let svgOpt = await optimizeSVGImgs(svgClone, assetCache, addGlobalStyles, width, height, formatSVG, quality, scaleImages);
        dataUrl = 'data:image/svg+xml; charset=utf8, ' + encodeURIComponent(new XMLSerializer().serializeToString(svgOpt));
        dataUrlSVG = dataUrl;
    }


    // bitmap format
    else {

        dataUrl = await svg2Canvas2DataUrl(svgClone, width, height, format, quality, bgColor, flattenTransparency);
    }

    return { dataUrl, dataUrlSVG }
}





async function getSelfcontainedSVG(svg, assetCache = {}, addGlobalStyles = true, format = 'original', quality = 1, scaleImages = false) {


    // Helper function to generate checksum
    async function generateChecksum(str) {
        const buffer = new TextEncoder().encode(str);
        const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
        const hashArray = Array.from(new Uint8Array(hashBuffer));
        return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    }

    let cache_key = await generateChecksum(svg);

    // is cached
    if (assetCache.svg[cache_key]) return assetCache.svg[cache_key];

    // final inlined CSS
    let css = '';


    /**
     * inline use refs
     */
    await inlineUseRefs(svg, assetCache);


    /**
     * search for masks, 
     * clip-paths, gradients, patterns etc.
     */
    inlineRefs(svg, assetCache);


    /**
     * inline external
     * image sources
     */
    format = 'original';
    quality = 1;
    scaleImages = false;
    await inlineImages(svg, assetCache);

    /**
     * check required font ranges
     */
    let svgFontInfo = analizeSVGText(svg);
    //console.log('svgFontInfo', svgFontInfo);


    /**
     * global CSS
     * inline external stylesheets
     * an import rules
     */

    if (addGlobalStyles) {

        let importRules = getAllImportRules();
        //console.log('importRules', importRules);

        /**
         * normalize imports
         * convert to link elements
         */
        await importRulesToLink(importRules);
        //await document.fonts.ready;

        if (addGlobalStyles) {
            css += await inlineGlobalStyles(svg, svgFontInfo, assetCache);
        }
    }


    /**
     * inline css
     */
    let cssEl = svg.querySelector('style');
    css += cssEl.textContent;


    /**
     * replace external font 
     * references with base64
     */
    css = await externalFontsToBase64(css, assetCache);
    //console.log('css', css);

    // update self-contained css
    cssEl.textContent = css;

    // add to cache
    //assetCache.svg[cache_key] = new XMLSerializer().serializeToString(svg);
    assetCache.svg[cache_key] = svg;

    //console.log('assetCache', assetCache);
    return svg;

}


/**
 * Gets the visual background color of an element by checking its own background
 * and traversing parent elements until a defined background is found.
 */
function getVisualBackgroundColor(element) {
    // Check if element is valid


    if (!element instanceof HTMLElement && !element instanceof SVGElement) {
        return 'white';
    }

    // Function to parse color values
    function parseColor(color) {
        // Check for transparent/undefined colors
        if (!color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)') {
            return null;
        }
        return color;
    }

    // Traverse up the DOM tree
    let currentElement = element;
    while (currentElement) {
        // Get computed style
        const computedStyle = window.getComputedStyle(currentElement);
        const bgColor = computedStyle.backgroundColor;

        // Check if background color is defined and not transparent
        const parsedColor = parseColor(bgColor);
        if (parsedColor) {
            return parsedColor;
        }

        // Move to parent element, stop at body
        if (currentElement === document.body) {
            break;
        }
        currentElement = currentElement.parentElement;
    }

    // Default to white if no background color found
    return '#fff';
}




async function addStyles(styleName = 'styles', addDist = false) {

    const getCurrentScriptUrl = () => {
        try {

            /** 1. try performance API */
            let urlPerf = performance.getEntries()
                .slice(1)[0].name.split('/')
                .slice(0, -1)
                .join('/');

            //if(urlPerf) return urlPerf;

            /** 2. try error API */
            let stackLines = new Error().stack.split('\n');
            let relevantLine = stackLines[1] || stackLines[2];
            if (!relevantLine) return null;

            // Extract URL using a more comprehensive regex
            let urlError = relevantLine.match(/(https?:\/\/[^\s]+)/)[1]
                .split('/')
                .slice(0, -1)
                .join('/');

            return urlError;

        } catch (e) {
            console.warn("Could not retrieve script path", e);
            return null;
        }
    };


    let scriptPath = getCurrentScriptUrl();

    // dist path is excluded in modules
    if (!scriptPath.includes('dist') && addDist) scriptPath += '/dist';

    let url = scriptPath + `/${styleName}.css`;
    //console.log('CSSurl', url);

    let linkEl = document.getElementById(`${styleName}_style`);
    if (!linkEl) {
        linkEl = document.createElement('link');
        linkEl.setAttribute('href', url);
        linkEl.setAttribute('rel', 'stylesheet');
        linkEl.id = `${styleName}_style`;
        document.head.append(linkEl);
    }
}

function savgy_core(
    input = '',
    {
        addGlobalStyles = true,
        format = 'webp',
        formatSVG = 'original',
        quality = 0.9,
        scaleImages = false,
        addToolbar = true,
        showPreview = false,
        crop = false,
        useIntrinsic = false,
        flattenTransparency = false,
        bgColor = 'transparent'
    } = {}
) {

    let options = { addGlobalStyles, format, formatSVG, quality, scaleImages, addToolbar, showPreview, crop, useIntrinsic, flattenTransparency, bgColor };


    /**
     * check input type
     * selector
     * element array
     * single element
     * markup
     * img
     */


    let isArray = Array.isArray(input);

    // normalize to array
    let inputArr = !isArray ? [input] : input;
    let svgs = [];



    for (let i = 0, l = inputArr.length; l && i < l; i++) {
        let input = inputArr[i];
        let isString = typeof input === 'string';
        isString ? document.querySelectorAll(input).length : false;
    

        let isSVGMarkup = isString ? input.trim().startsWith('<svg ') : false;
        let isElement = !isString ? input instanceof HTMLElement || input instanceof SVGElement : false;
        isElement ? input.nodeName.toLowerCase() === 'img' : false;

        let svg = isElement ? input : (isSVGMarkup ? new DOMParser().parseFromString(svg, "text/html").querySelector("svg") : '' );

        if(svg) svgs.push(svg);

        //console.log(input, isString, isSVGMarkup, isSelector, isElement, isImg);

        // convert
        svg2bitmap(svg, options);

    }



    this.svgs = svgs;

    return this;

}


// Self-executing function for IIFE
if (typeof window !== 'undefined') {
    window.savgy = savgy_core;
}

export { savgy_core as savgy };