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

背景:

项目前端有个图片压缩包上传功能,用户上传的时候会选择单反拍摄的巨幅图片,由于前端打不开压缩包,也没法读取压缩率并重新编码,只能原样发送,这给后台存储造成相当大的压力.同时由于压缩包内容涉及隐私,所以需要制作专门的加密压缩工具,使压缩包无法用通用的压缩工具打开.由于同组的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就可以实现打开文件夹/文件操作了
    实现关键代码:
渲染进程



主进程
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:
 

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的图片上传插件的开发过程)