美文网首页
一次基于electron的图片上传插件的开发过程

一次基于electron的图片上传插件的开发过程

作者: 千茉紫依 | 来源:发表于2020-06-18 14:54 被阅读0次
    背景:

    项目前端有个图片压缩包上传功能,用户上传的时候会选择单反拍摄的巨幅图片,由于前端打不开压缩包,也没法读取压缩率并重新编码,只能原样发送,这给后台存储造成相当大的压力.同时由于压缩包内容涉及隐私,所以需要制作专门的加密压缩工具,使压缩包无法用通用的压缩工具打开.由于同组的C++同事都在忙别的项目,这个工具就由我来开发了.

    预期功能点:
    • 选择文件夹,使用node遍历出所有图片文件,压缩编码成640*480分辨率以下的图片
    • 将图片打包生成压缩包,不勾选加密生成.zip,勾选加密生成biu包(biu是我起的加密压缩包名字)
    • 具备加密biu包和不加密zip包互相转换的功能
    • 识别无效的照片,包括非jpg、jpeg格式的图片
    选型:
    • 遍历文件夹和生成压缩文件属于IO操作,选用node
    • 要兼容多版本windows系统,使用electron-vue
    • 要对图像进行压缩,选用resize-optimize-images,能知道这个包还是非常巧合,网上的博客,适合node图片压缩的包有两个: gm和images, images在win10中有很大的兼容性问题,而gm在electron中编译一直失败. 所以我在github上以images和resize为关键字,搜索结果中排名前三十的包全部试用了一遍,大部分在electron都存在问题,最后逼得我准备在electron中调用OpenCV库的时候,忽然发现了这个包非常小巧,而且正好能够满足要求
    • 获取图片分辨率,选用get-pixels
    • 打包图片生成压缩包,使用jszip
    • 加密压缩包,手写加密函数

    下面是最终的成品: 耗时两天完成


    工具界面
    详细流程:
    • electron进程间通信:
      electron主进程捕获渲染进程发出的打开文件夹事件, 在electron渲染进程中设置监听,然后通过ipcRenderer.send发送给主进程,主进程通过ipcMain.on监听到消息后,调用 dialog.showOpenDialog就可以实现打开文件夹/文件操作了
      实现关键代码:
    渲染进程
    <template>
         <button class="btn"
                    @click="showModalHandler">选择要压缩的文件夹</button>
    </template>
    
    <script>
    export default {
      methods: {
        showModalHandler() {
          ipcRenderer.send("open-directory-dialog", {
            openMethod: "openDirectory",  // 参数openDirectory为打开文件夹,参数openFile为打开文件
          });
        },
    </script>
    
    主进程
    ipcMain.on('open-directory-dialog', function (event, compressOpt) {
        dialog.showOpenDialog({
            properties: [compressOpt.openMethod] 
        }, function (files) {
            if (files) {// 如果有选中
                // 发送选择的对象给子进程, files[0]是打开的文件绝对路径
                // console.log("path", files[0]);
                // TODO 文件操作,压缩打包
                event.sender.send('selectedItem', files[0])
            }
        })
    });
    
    
    • node的文件操作:
    1. 路径是否存在: fs.existsSync(path)
    2. 文件信息: info = fs.statSync(path)
    3. 是目录:info.isDirectory()
    4. 是文件:info.isFile()
    5. 新建文件夹:fs.mkdirSync(path)
    6. 删除空文件夹,必须为空才能删除:fs.rmdirSync(path)
    7. 创建文件:fs.writeFileSync(filename,arraybuffer)
    8. 删除文件:fs.unlinkSync(path)
    9. 读取文件:content =fs.readFileSync(filePath)
    10. 打开文件夹,使用cmd调用explorer.exe:
    const exec = require('child_process').exec
    exec(`explorer.exe /e, /root,${dirPath}`)
    
    1. 删除目录代码(目录有文件也能删除)
    function delPath(path) {
        if (!fs.existsSync(path)) {
            console.log("路径不存在");
            return "路径不存在";
        }
        var info = fs.statSync(path);
        if (info.isDirectory()) {//目录
            var data = fs.readdirSync(path);
            if (data.length > 0) {
                for (var i = 0; i < data.length; i++) {
                    delPath(`${path}/${data[i]}`); //使用递归
                    if (i == data.length - 1) { //删了目录里的内容就删掉这个目录
                        delPath(`${path}`);
                    }
                }
            } else {
                fs.rmdirSync(path);//删除空目录
            }
        } else if (info.isFile()) {
            fs.unlinkSync(path);//删除文件
        }
    }
    
    • 判断图片是否是jpg图片
      在实践中我发现单纯根据后缀名来判断并不准确,因为有相当多的jpg图片是被用户用其他类型图片(png,jfif等)改过来的,所以我编写了对jpg图片的校验函数。使用jpg文件头来校验
      const JPGBuffer = fs.readFileSync(JPGPath);
      testJPGHeader(JPGBuffer)
    
      function testJPGHeader(JPGBuffer) {
            const JPGHeader = JPGBuffer.slice(0, 3);
            const HT_STD_HEADER = [0xff, 0xd8, 0xff];
            let notJPG = false;
            JPGHeader.forEach((v, i) => {
                if (HT_STD_HEADER[i] !== v) notJPG = true;
            });
            return notJPG;
        }
    
    • ipc重复消息事件处理
      由于是通过点击调用进程通信,处理事件,所以我将ipc监听注册在了点击事件中。但是这样有一个弊端,当重复点击时,会造成ipc监听函数被反复注册,解决的方法一种是使用ipcRenderer.once使注册的监听函数只运行一次,即被释放,这适用于唯一的确定的消息,比如开始、结束等。另一种是使用ipcRenderer.on注册,并在注册之前使用ipcRenderer.removeAllListeners取消以往的注册,这适用于频繁的、不确定数量的消息,比如发送图片缩放失败的消息。
    监听函数被反复注册,造成失败消息被重复发送.jpg
    html:
     <button class="btn"
                  @click="showModalHandler">选择要打包的文件夹</button>
    
    js:
     showModalHandler() {
        const that = this;
        this.init();
        //取消失败消息监听,避免反复注册
        ipcRenderer.removeAllListeners("extension-error");
        //发送打开目录消息
        ipcRenderer.send("open-directory-dialog", {
          openMethod: "openDirectory",
          compress: this.compress
        });
         //打开文件消息
        ipcRenderer.once("selectedItem", function(e, path) {
          console.log("selectedItem", path);
          that.loading = true;
        });
         //完成压缩消息
        ipcRenderer.once("patchZip-Done", function(e, zipName) {
          console.log("zipName", zipName);
          if (zipName === "error") {
            that.zipFill = true;
          }
          that.loading = false;
        });
        //完成压缩消息
        ipcRenderer.on("extension-error", function(e, filename) {
          console.log("extension-error-filename", filename);
          that.errfiles.push(filename);
        });
      },
    
    • 图片压缩
      要对图像进行压缩,选用resize-optimize-images,能知道这个包还是非常巧合,网上的博客,适合node图片压缩的包有两个: gm和images, images在win10中有很大的兼容性问题,而gm在electron中编译一直失败. 所以我在github上以images和resize为关键字,搜索结果中排名前三十的包全部试用了一遍,大部分在electron都存在问题,node-OpenCV是一个万能的图形库, 但是十分臃肿, 正当我准备手撸插值压缩算法的时候,忽然发现了resize-optimize-images这个包非常小巧,而且正好能够满足要求

      压缩的基本思路是在图片中找到相对比较短的边, 以他为基准压缩到一个比较小的比例(如640*480),然后将原始图片复制出来,再将压缩后的图片写进去即可. 图像的大小最终取决于三个因素:图片像素、编码质量和位深度,图形像素在执行完这条个函数之后就达到一个比较小的范围了,一般不会超过100k,而编码质量是一种类似于PS中表面模糊的效果,使用的是插值算法,一般编程80-90区间内可以对图像进行进一步的压缩,但是不建议压的太多,会失真。真彩色的位深度一般是四个字节

        getPixels(fileAbPath, function (err, pixels) {
            if (err) {
                return
            }
            let compressRio = 0.9;
            let [imageWidth, imageHeight] = pixels.shape
             //找到比较小的边
            let loopVar = imageWidth > imageHeight ? imageHeight : imageWidth;
            while (loopVar >= 480) {
                loopVar = loopVar * compressRio;
                imageWidth = imageWidth * compressRio;
                imageHeight = imageHeight * compressRio;
            }
            imageWidth = Math.floor(imageWidth)
            imageHeight = Math.floor(imageHeight)
            const content = pixels.data
            fs.writeFile(outPath, content, async function (err) {
                const options = {
                    images: [outPath],
                    width: imageWidth,
                    height: imageHeight,
                    quality: 85
                };
                // 执行压缩.
                await resizeOptimizeImages(options);
            });
        })
    

    相关文章

      网友评论

          本文标题:一次基于electron的图片上传插件的开发过程

          本文链接:https://www.haomeiwen.com/subject/qutzwhtx.html