Better Figma Layer Exporter

A more convenient Figma layer export solution, featuring the following main functions: 1. Direct export of selected layers as PNGs and automatically assigning them to their corresponding DPI drawable folders; 2. Support for converting PNGs to WebP format before exporting; 3. Support for exporting SVGs optimized through SVGO.

// ==UserScript==
// @name         Better Figma Layer Exporter
// @name:zh-CN   Better Figma Layer Exporter
// @namespace    https://github.com/XuQK/Better-Figma-Layer-Exporter
// @version      1.1.3
// @license      MIT
// @description A more convenient Figma layer export solution, featuring the following main functions: 1. Direct export of selected layers as PNGs and automatically assigning them to their corresponding DPI drawable folders; 2. Support for converting PNGs to WebP format before exporting; 3. Support for exporting SVGs optimized through SVGO.
// @description:zh-CN  更方便的 Figma 图层导出,主要功能:1. 选定图层直接导出为 png 并按 dpi 分配到对应 dpi 的 drawable 文件夹; 2. 支持将 PNG 转换成 WebP 再导出; 3. 支持导出经 SVGO 优化的 svg 图片。
// @author       XuQK
// @match        https://www.figma.com/*
// @icon         https://github.com/XuQK/Better-Figma-Layer-Exporter/blob/master/assets/icon.jpeg?raw=true
// @grant        unsafeWindow
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @require      https://cdn.jsdelivr.net/npm/sweetalert2@11
// @connect      *
// @run-at       document-end
// ==/UserScript==

(function () {
    "use strict";

    const coloredToastStyle = document.createElement("style");
    coloredToastStyle.innerHTML = `
    .colored-toast.swal2-icon-success {
  background-color: #a5dc86 !important;
}

.colored-toast.swal2-icon-error {
  background-color: #f27474 !important;
}

.colored-toast.swal2-icon-warning {
  background-color: #f8bb86 !important;
}

.colored-toast.swal2-icon-info {
  background-color: #3fc3ee !important;
}

.colored-toast.swal2-icon-question {
  background-color: #87adbd !important;
}

.colored-toast .swal2-title {
  color: white;
}

.colored-toast .swal2-close {
  color: white;
}

.colored-toast .swal2-html-container {
  color: white;
}
    `;
    document.head.appendChild(coloredToastStyle);

    GM_registerMenuCommand("Settings/设置", showSettingsDialog, "S");

    function showSettingsDialog() {
        Toast.fire({
            title: "Settings / 设置",
            html: `
          <div style="display: flex; align-items: center">
        <label for="kd-figma-token" style="font-size: 18px; width: 10em">Figma token</label>
        <input id="kd-figma-token" class="swal2-input" style="margin: 8px" value="${figmaToken}">
    </div>
    <div style="display: flex; align-items: center">
        <label for="kd-server-svg-optimizer" style="font-size: 18px; width: 10em">Svg Optimizer Url</label>
        <input id="kd-server-svg-optimizer" class="swal2-input" style="margin: 8px" value="${svgOptimizerRequestUrl}">
    </div>
    <div style="display: flex; align-items: center">
        <label for="kd-server-png-convert-to-webp" style="font-size: 18px; width: 10em">Webp Converter url</label>
        <input id="kd-server-png-convert-to-webp" class="swal2-input" style="margin: 8px" value="${pngConvertToWebpRequestUrl}">
    </div>
    <div style="display: flex; align-items: center">
        <label for="kd-svg-precision" style="font-size: 18px; width: 10em">Svg precision</label>
        <input id="kd-svg-precision" class="swal2-input" style="margin: 8px" value="${svgPrecision}">
    </div>
    <div style="display: flex; align-items: center">
        <label for="kd-webp-quality" style="font-size: 18px; width: 10em">WebP quality</label>
        <input id="kd-webp-quality" class="swal2-input" style="margin: 8px" value="${webpQuality}">
    </div>
    <div style="display: flex; align-items: center; height: 4em">
        <label for="kd-mode" style="font-size: 18px; width: 10em">Day/Night Mode</label>
        <input id="kd-mode-day" name="kd-mode" value="day" type="radio" style="margin: 8px">Day</input>
        <input id="kd-mode-night" name="kd-mode" value="night" type="radio" style="margin: 8px">Night</input>
    </div>
    <p></p>
    <p style="text-align: start; color: #FF5252">PS:</p>
    <p style="text-align: start; font-size: 14px; color: #FF5252">1. SVG 优化和 PNG 转 WebP 的需要后台能力,目前是白嫖的 node 服务器,资源有限,请温柔使用~</p>
    <p style="text-align: start; font-size: 14px; color: #FF5252">2. 如果想将此 node 服务器运行在自己本地,参见 <a href="https://github.com/XuQK/Better-Figma-Layer-Exporter#扩展功能" target="_blank">https://github.com/XuQK/Better-Figma-Layer-Exporter#扩展功能</a></p>
    
    </div>
          `,
            width: 600,
            focusConfirm: false,
            showCancelButton: true,
            didOpen() {
                document.getElementById(`kd-mode-${mode}`).checked = true;
            },
            preConfirm: () => {
                return [
                    document.getElementById("kd-figma-token").value,
                    document.getElementById("kd-server-svg-optimizer").value,
                    document.getElementById("kd-server-png-convert-to-webp").value,
                    document.getElementById("kd-svg-precision").value,
                    document.getElementById("kd-webp-quality").value,
                    document.querySelector("input[name='kd-mode']:checked").value
                ];
            }
        }).then(value => {
            const params = value.value;
            figmaToken = params[0];
            svgOptimizerRequestUrl = params[1];
            pngConvertToWebpRequestUrl = params[2];
            svgPrecision = params[3];
            webpQuality = params[4];
            mode = params[5];

            GM_setValue("figmaToken", figmaToken);
            GM_setValue("svgOptimizerRequestUrl", svgOptimizerRequestUrl);
            GM_setValue("pngConvertToWebpRequestUrl", pngConvertToWebpRequestUrl);
            GM_setValue("svgPrecision", svgPrecision);
            GM_setValue("webpQuality", webpQuality);
            GM_setValue("mode", mode);
        });
    }

    // 默认配置
    let figmaToken = GM_getValue("figmaToken", "");
    let svgOptimizerRequestUrl = GM_getValue("svgOptimizerRequestUrl", "");
    let pngConvertToWebpRequestUrl = GM_getValue("pngConvertToWebpRequestUrl", "");
    // svg 专用
    let svgPrecision = GM_getValue("svgPrecision", 1);
    // png 专用
    // webp 转换质量,0-100,默认 75
    let webpQuality = GM_getValue("webpQuality", 75);
    // 是否暗色模式
    let mode = GM_getValue("mode", "day");

    class Image {
        /**
         * @type {string}
         */
        url;

        /**
         * @type {Blob} 从 figma 下载的原始图层内容,可能是 svg,也有可能是 png
         */
        originalContent;

        /**
         * @type {number}
         */
        scale;

        /**
         * @type {Blob} 经处理后的数据,可能是优化后的 svg,也有可能是经 png 转换过后的 webp
         */
        processedContent;

        /**
         * @type {string} 最终创建文件的格式/后缀名
         */
        format;

        /**
         * @type {Blob} 最终存储到文件的数据
         */
        finalContent;

        /**
         * @param id {string}
         * @param name {string}
         */
        constructor(id, name) {
            this.id = id;
            this.name = name;
        }
    }

    function dirNameToScaleMap() {
        if (mode === "day") {
            return _dirNameToScaleMapDay;
        } else {
            return _dirNameToScaleMapNight;
        }
    }

    function scaleToDirNameMap() {
        if (mode === "day") {
            return _scaleToDirNameMapDay;
        } else {
            return _scaleToDirNameMapNight;
        }
    }

    const _dirNameToScaleMapDay = new Map();
    _dirNameToScaleMapDay.set("drawable-ldpi", 0.75);
    _dirNameToScaleMapDay.set("drawable-mdpi", 1);
    _dirNameToScaleMapDay.set("drawable-hdpi", 1.5);
    _dirNameToScaleMapDay.set("drawable-xhdpi", 2);
    _dirNameToScaleMapDay.set("drawable-xxhdpi", 3);
    _dirNameToScaleMapDay.set("drawable-xxxhdpi", 4);

    const _scaleToDirNameMapDay = new Map();
    _scaleToDirNameMapDay.set(0.75, "drawable-ldpi");
    _scaleToDirNameMapDay.set(1, "drawable-mdpi");
    _scaleToDirNameMapDay.set(1.5, "drawable-hdpi");
    _scaleToDirNameMapDay.set(2, "drawable-xhdpi");
    _scaleToDirNameMapDay.set(3, "drawable-xxhdpi");
    _scaleToDirNameMapDay.set(4, "drawable-xxxhdpi");

    const _dirNameToScaleMapNight = new Map();
    _dirNameToScaleMapNight.set("drawable-night-ldpi", 0.75);
    _dirNameToScaleMapNight.set("drawable-night-mdpi", 1);
    _dirNameToScaleMapNight.set("drawable-night-hdpi", 1.5);
    _dirNameToScaleMapNight.set("drawable-night-xhdpi", 2);
    _dirNameToScaleMapNight.set("drawable-night-xxhdpi", 3);
    _dirNameToScaleMapNight.set("drawable-night-xxxhdpi", 4);

    const _scaleToDirNameMapNight = new Map();
    _scaleToDirNameMapNight.set(0.75, "drawable-night-ldpi");
    _scaleToDirNameMapNight.set(1, "drawable-night-mdpi");
    _scaleToDirNameMapNight.set(1.5, "drawable-night-hdpi");
    _scaleToDirNameMapNight.set(2, "drawable-night-xhdpi");
    _scaleToDirNameMapNight.set(3, "drawable-night-xxhdpi");
    _scaleToDirNameMapNight.set(4, "drawable-night-xxxhdpi");

    const svgButtonId = "svgo-button";
    const svgoButton = document.createElement("button");
    svgoButton.id = svgButtonId;
    svgoButton.className = "basic_form--btn--t7Y67 ellipsis--ellipsis--70pHK text--fontPos11--rO47d text--_fontBase--VaHfk button_row--btnNarrow--bKZDj button_row--btn--0W3Mm basic_form--btn--t7Y67 ellipsis--ellipsis--70pHK text--fontPos11--rO47d text--_fontBase--VaHfk";
    svgoButton.style.marginTop = "16px";
    svgoButton.style.width = "90%";
    svgoButton.style.marginLeft = "auto";
    svgoButton.style.marginRight = "auto";
    svgoButton.innerText = "经 SVGO 优化并导出";
    svgoButton.addEventListener("click", function () {
        onClickDownloadSvg().then();
    });

    const pngButtonId = "png-button";
    const pngButton = document.createElement("button");
    pngButton.id = pngButtonId;
    pngButton.className = "basic_form--btn--t7Y67 ellipsis--ellipsis--70pHK text--fontPos11--rO47d text--_fontBase--VaHfk button_row--btnNarrow--bKZDj button_row--btn--0W3Mm basic_form--btn--t7Y67 ellipsis--ellipsis--70pHK text--fontPos11--rO47d text--_fontBase--VaHfk";
    pngButton.style.marginTop = "16px";
    pngButton.style.width = "90%";
    pngButton.style.marginLeft = "auto";
    pngButton.style.marginRight = "auto";
    pngButton.innerText = "导出 PNG 到指定 res 目录";
    pngButton.addEventListener("click", function () {
        onClickDownloadPng(false).then();
    });

    const webpButtonId = "webp-button";
    const webpButton = document.createElement("button");
    webpButton.id = webpButtonId;
    webpButton.className = "basic_form--btn--t7Y67 ellipsis--ellipsis--70pHK text--fontPos11--rO47d text--_fontBase--VaHfk button_row--btnNarrow--bKZDj button_row--btn--0W3Mm basic_form--btn--t7Y67 ellipsis--ellipsis--70pHK text--fontPos11--rO47d text--_fontBase--VaHfk";
    webpButton.style.width = "90%";
    webpButton.style.marginTop = "16px";
    webpButton.style.marginLeft = "auto";
    webpButton.style.marginRight = "auto";
    webpButton.innerText = "导出 WebP 到指定 res 目录";
    webpButton.addEventListener("click", function () {
        onClickDownloadPng(true).then();
    });

    // 监听 body 元素变动,根据情况插入导出按钮(对于无编辑器权限的使用者)
    new MutationObserver(() => {
        try {
            let c = null;
            const anchorElemForGuest = document.querySelector("div.raw_components--panel--YDedw.export_panel--standalonePanel--yXYPM");
            if (anchorElemForGuest !== null) {
                c = anchorElemForGuest.parentElement;
            } else {
                const nodeList = document.querySelectorAll("div.draggable_list--panelTitleText--Bj2Hu")
                const anchorElemForOwner = Array.from(nodeList).find(node => node.innerText === "Export")
                if (anchorElemForOwner !== null) {
                    c = anchorElemForOwner.parentElement.parentElement.parentElement.parentElement.parentElement
                }
            }
            if (c !== null) {
                if (document.getElementById(svgButtonId) === null) {
                    c.appendChild(svgoButton);
                }
                if (document.getElementById(pngButtonId) === null) {
                    c.appendChild(pngButton);
                }
                if (document.getElementById(webpButtonId) === null) {
                    c.appendChild(webpButton);
                }
            }
        } catch (e) {
        }
    }).observe(document.body, {childList: true, subtree: true});
    // (对于有编辑权限的使用者)

    const Toast = Swal.mixin({
        position: "center",
        allowOutsideClick: false
    });

    // SVGO 优化下载功能 START
    async function onClickDownloadSvg() {
        const layerList = getSelectedLayerList();
        if (layerList.length === 0) {
            showError("未选择图层");
            return;
        }
        const fileKey = figma.fileKey;
        const dirHandle = await unsafeWindow.showDirectoryPicker({id: `${fileKey}-svg`, mode: "readwrite"});
        showExporting();
        try {
            const finalImageList = await downloadSelectedLayerAsSvg(dirHandle, fileKey, layerList);
            const successText = getSuccessText(finalImageList);
            showSuccess(successText);
        } catch (e) {
            console.error(e);
            showError(e.toString());
        }
    }

    /**
     * 将选中的图层下载为经 svgo 优化过后的 svg 图像,保存到指定地址
     * @async
     * @param dirHandle {FileSystemDirectoryHandle} 文件操作 Handle
     * @param fileKey {string} figma 文件 key
     * @param layerList {Image[]} 图层信息,格式为 [{"id": "svg id", "name": "svg name"}]
     * @return {Promise<Image[]>}
     */
    async function downloadSelectedLayerAsSvg(dirHandle, fileKey, layerList) {
        let optimizedImageList;
        // 1. 下载源 svg
        const imageList = await downloadImageFromFigma(fileKey, layerList, "svg", 1);
        if (imageList === undefined || imageList.length === 0) {
            throw new Error("从 figma 获取图片失败,请检查网络连接");
        }
        // 任何一张图层未下载成功,都判定整体失败
        if (!imageList.every(image => image.originalContent !== undefined)) {
            throw new Error("从 figma 下载图片内容失败,请检查网络连接");
        }
        // 2. 经 svgo 优化
        optimizedImageList = await optimizeSvg(imageList, svgPrecision);
        // 3. 保存到指定文件
        optimizedImageList.forEach(image => image.finalContent = image.processedContent);
        await saveImageWithDifferentDpiToDir(dirHandle, optimizedImageList);
        return optimizedImageList;
    }

    /**
     *
     * @param imageList {Image[]}
     * @param precision {number}
     * @returns
     */
    async function optimizeSvg(imageList, precision) {
        try {
            const svgContentList = await Promise.all(imageList.map(image => image.originalContent.text()));
            const requestBody = {
                precision: precision,
                svgContentList: svgContentList
            };
            const response = await fetch(getSvgOptimizerRequestUrl(), {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify(requestBody)
            });
            const responseJson = await response.json();
            imageList.forEach((image, index) => {
                image.processedContent = new Blob([responseJson[index]], {
                    type: image.originalContent.type
                });
            });

            return imageList;
        } catch (e) {
            console.error(e);
            throw new e;
        }
    }

    // SVGO 优化下载功能 END

    // PNG 下载及转换功能 START
    async function onClickDownloadPng(convertToWebp) {
        const layerList = getSelectedLayerList();
        if (layerList.length === 0) {
            showError("未选择图层");
            return;
        }
        const fileKey = figma.fileKey;
        let dirHandleId;
        if (convertToWebp) {
            dirHandleId = `${fileKey}-webp`;
        } else {
            dirHandleId = `${fileKey}-png`;
        }
        const dirHandle = await unsafeWindow.showDirectoryPicker({id: dirHandleId, mode: "readwrite"});
        showExporting();
        const scaleList = await getScaleList(dirHandle);
        if (scaleList.length === 0) {
            showError("所选目录下需要有指定 dpi 的\"drawable-*dpi\"的文件夹");
            return;
        }
        try {
            const finalImageList = await exportPng(convertToWebp, dirHandle, fileKey, layerList, scaleList);
            let successText;
            if (!finalImageList.every(image => image.format === "png")) {
                // 表示有导出为 webp 的文件
                successText = getSuccessText(finalImageList);
            }
            showSuccess(successText);
        } catch (e) {
            console.error(e);
            showError(e.toString());
        }
    }

    /**
     *
     * @param {boolean} convertToWebp 是否需要转换成 webp
     * @param {FileSystemDirectoryHandle} dirHandle
     * @param {string} fileKey figma 对应的文件 key
     * @param {Image[]} layerList 需要导出的图层信息,包括 id 和 name
     * @param {number[]} scaleList dpi 对应的缩放倍率
     * @returns {Promise<Image[]>}
     */
    async function exportPng(convertToWebp, dirHandle, fileKey, layerList, scaleList) {
        let imageList = await downloadSelectedLayerAsPng(dirHandle, fileKey, layerList, scaleList);
        if (convertToWebp) {
            imageList = await transferPngListToWebp(imageList, webpQuality);
            imageList.forEach((image) => {
                // 只有在 webp 小于 png 时,才存储为 webp
                if (image.processedContent.size > image.originalContent.size) {
                    image.format = "png";
                    image.finalContent = image.originalContent;
                } else {
                    image.format = "webp";
                    image.finalContent = image.processedContent;
                }
            });
        } else {
            imageList.forEach((image) => {
                image.format = "png";
                image.finalContent = image.originalContent;
            });
        }
        await saveImageWithDifferentDpiToDir(dirHandle, imageList);
        return imageList;
    }

    /**
     * 通过分析选中目录下的文件夹情况,得出需要下载的 dpi 对应的缩放倍率列表
     * @param {FileSystemDirectoryHandle} dirHandle
     * @return {Promise<number[]>}
     */
    async function getScaleList(dirHandle) {
        const scaleList = [];
        for await (const file of dirHandle.values()) {
            if (file.kind === "directory") {
                const scale = dirNameToScaleMap().get(file.name);
                if (scale !== undefined) {
                    scaleList.push(scale);
                }
            }
        }
        return scaleList;
    }

    /**
     * 将选中的图层根据给出的 scaleList 下载为 png
     * @param {FileSystemDirectoryHandle} dirHandle 文件操作 Handle
     * @param {string} fileKey figma 文件 key
     * @param {Image[]} layerList 图层信息,格式为 [{"id": "svg id", "name": "svg name"}]
     * @param {number[]} scaleList dpi 对应的缩放倍率
     * @return {Promise<Image[]>} 从 figma 下载下来的图片内容
     */
    async function downloadSelectedLayerAsPng(dirHandle, fileKey, layerList, scaleList) {
        const imageGroupByScale = await Promise.all(scaleList.map(scale => downloadImageFromFigma(fileKey, layerList, "png", scale)));
        /** @type {Image[]} */
        const imageList = imageGroupByScale.flat().filter(image => image !== undefined);
        if (imageList === undefined || imageList.length === 0) {
            throw new Error("从 figma 获取图片失败,请检查网络连接");
        }
        // 任何一张图层未下载成功,都判定整体失败
        if (!imageList.every(image => image.originalContent !== undefined)) {
            throw new Error("从 figma 下载图片内容失败,请检查网络连接");
        }
        return imageList;
    }

    /**
     * 批量转换 png 为 webp
     * @param {Image[]} imageList
     * @param {number} quality 质量
     * @return {Promise<Image[]>} 输出的值比参数 imageList 添加了 processedContent 属性
     *
     * @throws {Error} 操作失败会抛出异常
     */
    async function transferPngListToWebp(imageList, quality) {
        try {
            const responseList = await Promise.all(
                imageList.map(image => {
                    return fetch(getPngConvertToWebpRequestUrl(), {
                        method: "POST",
                        headers: {
                            "Content-Type": "application/octet-stream",
                            "quality": quality
                        },
                        body: image.originalContent
                    });
                })
            );

            for (const image of imageList) {
                const index = imageList.indexOf(image);
                image.processedContent = await responseList[index].blob();
            }
            return imageList;
        } catch (e) {
            console.error(e);
            throw new Error("png 转 webp 操作失败,请检查是否开启优化服务器");
        }
    }

    // PNG 下载及转换功能 END

    // 公共能力 START
    /**
     * 获取当前选中的图层,包括 id 和 name
     * @return {[Image]}
     */
    function getSelectedLayerList() {
        return figma.currentPage.selection.map(node => new Image(node.id, node.name.toLowerCase().replace(/[^a-z0-9_]/g, "_")));
    }

    /**
     * 生成的一个随机四位数,并以下划线开头,作为文件的前缀,以防重名时覆盖已有文件
     * @return {string}
     */
    function getRandomPrefix() {
        return "figma" + Math.floor(Math.random() * 9000 + 1000);
    }

    /**
     * 下载选中图层的内容,包括内容指向 url 和具体的文件内容
     * @async
     * @param  {string} figmaFileKey
     * @param {string} format 格式 svg, png
     * @param {number} scale 缩放大小
     * @param {Image[]} layerList 包含有 id 和 name 的图层信息列表
     * @returns {Promise<Image[]>} 从 figma 下载下来的图片内容
     */
    async function downloadImageFromFigma(figmaFileKey, layerList, format, scale) {
        try {
            // 此处必须深拷贝
            const imageList = layerList.map(layer => new Image(layer.id, layer.name));
            const ids = imageList.map(image => image.id);
            let url = `https://api.figma.com/v1/images/${figmaFileKey}?ids=${ids.join(",")}&format=${format}&scale=${scale}`;
            const res = await fetch(url,
                {
                    headers: {
                        "X-FIGMA-TOKEN": figmaToken
                    }
                }
            );
            if (res.status !== 200) return undefined;
            const originalImageListJson = await res.json();
            imageList.forEach(layer => {
                layer.url = originalImageListJson.images[layer.id];
                layer.scale = scale;
                layer.format = format;
            });
            // 下载 image 内容
            const originalContentList = await Promise.all(imageList.map(image => downloadOriginalImageContent(image.url)));
            originalContentList.forEach((originalContent, index) => {
                imageList[index].originalContent = originalContent;
            });
            return imageList;
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * 下载给定的 url 的内容
     * @async
     * @param url 资源目标 url
     * @returns {Promise<Blob>} 下载下来的二进制内容
     */
    async function downloadOriginalImageContent(url) {
        try {
            let res = await fetch(url);
            if (res.status === 200) {
                // 需要用二进制数据
                return await res.blob();
            } else {
                console.log("错误?" + res.status);
            }
        } catch (e) {
            console.error(e);
        }
    }

    /**
     * 保存内容到文件
     * @param {FileSystemDirectoryHandle} dirHandle
     * @param {Image[]} imageList
     */
    async function saveImageWithDifferentDpiToDir(dirHandle, imageList) {
        const prefix = getRandomPrefix();
        for (const image of imageList) {
            /** @type {FileSystemDirectoryHandle} */
            let drawableDirHandle;
            if (image.format === "svg") {
                // svg 图片直接保存到目录下
                drawableDirHandle = dirHandle;
            } else {
                // 其它图片需要保存到对应 dpi 的目录下
                const drawableDirName = scaleToDirNameMap().get(image.scale);
                drawableDirHandle = await dirHandle.getDirectoryHandle(drawableDirName);
            }
            const fileHandle = await drawableDirHandle.getFileHandle(`${prefix}_${image.name}.${image.format}`, {create: true});
            const writable = await fileHandle.createWritable();
            await writable.write(image.finalContent);
            await writable.close();
        }
    }

    /**
     * 格式化 bytes 数量为可读字符串
     * @param {number} bytesSize
     * @return {string}
     */
    function formatBytes(bytesSize) {
        if (bytesSize < 1024) {
            return bytesSize + " Bytes";
        } else if (bytesSize < 1024 * 1024) {
            return (bytesSize / 1024).toFixed(2) + " KB";
        } else {
            return (bytesSize / (1024 * 1024)).toFixed(2) + " MB";
        }
    }

    /**
     * 获取成功提示文字,主要是关于体积缩减大小
     * @param {Image[]} finalImageList
     * @return {string}
     */
    function getSuccessText(finalImageList) {
        const originalSize = finalImageList.reduce((accumulator, currentValue) => {
            return accumulator + currentValue.originalContent.size;
        }, 0);
        const finalSize = finalImageList.reduce((accumulator, currentValue) => {
            return accumulator + currentValue.finalContent.size;
        }, 0);
        return `成功缩减体积 ${formatBytes(originalSize - finalSize)}(${((originalSize - finalSize) * 100 / originalSize).toFixed(0)}%)`;
    }

    function showExporting() {
        Toast.fire({
            title: "图层导出中...",
            didOpen() {
                Swal.showLoading();
            }
        });
    }

    /**
     * @param {string} successText
     */
    function showSuccess(successText) {
        Toast.fire({
            icon: "success",
            title: "导出成功",
            text: successText
        });
    }

    /**
     * @param {string} errorText
     */
    function showError(errorText) {
        Toast.fire({
            icon: "error",
            text: errorText,
            title: "导出失败,请重试",
        });
    }

    function getSvgOptimizerRequestUrl() {
        if (svgOptimizerRequestUrl === "") {
            return "https://nifh3bnmc3.hk.aircode.run/svgOptimizer";
        } else {
            return svgOptimizerRequestUrl;
        }
    }

    function getPngConvertToWebpRequestUrl() {
        if (pngConvertToWebpRequestUrl === "") {
            return "https://nifh3bnmc3.hk.aircode.run/webpConvetor";
        } else {
            return pngConvertToWebpRequestUrl;
        }
    }

    // 公共能力 END

})();