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.

目前為 2023-04-14 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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.2
// @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(() => {
        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);
            }
        }
    }).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

})();