2
头图

公众号名片
作者名片

foreword

As a front-end developer, are you fed up with the trouble that every time the UI is given a cutout that is so big that you want to carry a bucket and run away? Before you plan to carry a bucket, please stop and run after reading this article. late~

Show your face first

Image Tiny

I call it Image Tiny .

Roughly look at the compression rate, png and jpg formats can basically reach about 80%, and gif can reach about 11%.

Excited 💓, is it not bad?

Features

  • Supported image formats include png, jpg, gif
  • Support image compression above 5M
  • Does not depend on the network, does not depend on the server, based on client-side local compression
  • Support drag and drop image files to compress
  • Support compression quality parameter adjustment
  • Support window on top
  • Support single image saving
  • Support one-click packaging, save all the pictures in the list into a zip package and save it locally

Compression ratio vs TinyPNG

TinyPNG is an online image compression website that must be familiar to front-end developers. It supports image compression in png, jpg, and webp formats, and the compression rate is also very good. There must be many friends who have used it. So Image Tiny chooses to compare with TinyPNG .

The following is a comparison of the compressed data of 4 png, 2 jpg, and 2 gif images:

Image Tiny

TinyPNG

For images in png format, the compression rate of Image Tiny can basically reach the level of TinyPNG ;

For pictures in jpg format, after testing, adjusting the compression quality of Image Tiny to below 80% can surpass TinyPNG ;

For pictures in gif format, sorry, TinyPNG does not support;

For pictures of 5 MB and above, sorry, TinyPNG does not support it;

Overall, our Image Tiny is still very good, and it is completely free, does not depend on the network, and does not depend on the server.

Technical realization

compression core

Image compression is achieved with libimagequant, libpng, libjpeg, and gifsicle libraries in C language.
Use the Emscripten SDK (emsdk) to compile the C code into a wasm file for the browser to call.

The specific compressed code will not be shown here. This project has an open source plan, and you can naturally see it at that time.

application framework

Tauri + Rust + Vue3.0 + Vite

If you are unfamiliar with Tauri, you can read an article published earlier:

Nugget link 🔗: Throw away Electron and embrace Tauri based on Rust

WeChat official account link 🔗: Throw away Electron and embrace Tauri developed based on Rust

code peek

1. Window top function

 import { window } from '@tauri-apps/api';

// 窗口置顶
function handleWindowTop() {
  let curWin = window.getCurrent();
  if (datas.winTop === '窗口置顶') {
    curWin.setAlwaysOnTop(true);
    datas.winTop = '取消置顶';
  } else {
    curWin.setAlwaysOnTop(false);
    datas.winTop = '窗口置顶';
  }
}

@tauri-apps/api 2615865a6620e7bf9bffe3a8547b13db---引入window api, window.getCurrent方法获取到当前窗口实例, setAlwaysOnTop方法,通过参数true\false You can control the window to be on top or cancel it.

As for why you want to add the window top function to the application, we will dig a hole here and fill it in later.

2. Add application menu items and shortcut keys

main.rs

 use tauri::{Menu, MenuItem, Submenu};

fn main() {
  let submenu_main = Submenu::new(
    "ImageTiny".to_string(),
    Menu::new()
      .add_native_item(MenuItem::Minimize)
      .add_native_item(MenuItem::Hide)
      .add_native_item(MenuItem::Separator)
      .add_native_item(MenuItem::CloseWindow)
      .add_native_item(MenuItem::Quit),
  );

  let menu = Menu::new().add_submenu(submenu_main);

  tauri::Builder::default()
    .menu(menu)
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

Menu, MenuItem, SubmenuSubmenu::new()菜单项, add_native_item原生的MenuItem项在其中,然后新建一个Menu , add Submenu to the menu through the Menu::new().add_submenu() method, and finally register the application through tauri::Builder::default().menu() .

3. Drag and drop pictures to compress

 <div
  class="middle-con"
  @dragenter="dragenterEvent"
  @dragover="dragoverEvent"
  @dragleave="dragleaveEvent"
  @drop="dropEvent"
>
  ...
</div>
 function dragenterEvent(event) {
  event.stopPropagation();
  event.preventDefault();
}
function dragoverEvent(event) {
  event.stopPropagation();
  event.preventDefault();
}
function dragleaveEvent(event) {
  event.stopPropagation();
  event.preventDefault();
}
function dropEvent(event) {
  event.stopPropagation();
  event.preventDefault();
  const files = event.dataTransfer.files;
  displayChsFile(files);
}

The browser's drag event is used to monitor the drag and drop, and the file information is obtained in the drop event.

drop The parameter returned by the event is a DragEvent object, which has a dataTransfer field, which has a files storage field under it is what we need FileList , which is what we dragged File Object ;
After obtaining FileList , pass it to the displayChsFile method to traverse the compressed image file.

FileList

Tips: You need to disable the file drag event provided by tauri, the code is as follows

tauri.conf.json

 {
  ...
  "tauri": {
    "windows": [
      {
        ...
        "fileDropEnabled": false,
        ...
      }
    ],
  }
  ...
}

4. Image file compression

In order to facilitate the access of other projects of the company to the image compression function, we encapsulate this core code with a private npm plug-in to facilitate the access of various projects. Let’s take a general look at the code below:

 import pngtiny from '../plugins/pngtiny'

/**
 * @description: 图像压缩
 * @param {File} file 原始 File 文件对象
 * @param {Number} quality 压缩质量,10-90,建议 80
 * @return {Promise<File>} 压缩过的 File 文件对象
 */
const imageTiny = (file, quality = 80) => {
  pngtiny.run()
  return new Promise((resolve, reject) => {
    try {
      const reader = new FileReader()
      reader.readAsArrayBuffer(file)
      reader.onload = function(e) {
        const fcont = new Uint8Array(e.target.result)
        const fsize = fcont.byteLength
        const dataptr = pngtiny._malloc(fsize)
        const retdata = pngtiny._malloc(4)
        pngtiny.HEAPU8.set(fcont, dataptr)
        pngtiny._tiny(dataptr, fsize, retdata, quality)
        let rdata = new Int32Array(pngtiny.HEAPU8.buffer, retdata, 1)
        const size = rdata[0]
        rdata = new Uint8Array(pngtiny.HEAPU8.buffer, dataptr, size)
        const blob = new Blob([rdata], { type: file.type })
        let outFile = new File([blob], file.name, { type: file.type })
        if (outFile.size === 0) {
          outFile = file
        }
        resolve(outFile)
        pngtiny._free(dataptr)
        pngtiny._free(retdata)
      }
    } catch (error) {
      reject(error)
    }
  })
}
export default imageTiny

emsdk C 代码编译WebAssembly时,会.wasm文件和一个.js ,这个js 胶水代码Will process the wasm file, we only need to use the exported pngtiny object, which contains the methods we need to use.

The input parameters of the imageTiny method are:

  • file: File file object
  • quality: compression quality

The output is:

  • Compressed File file object

5. Save a single picture function

 import { writeBinaryFile } from '@tauri-apps/api/fs';
import { path, dialog } from '@tauri-apps/api';
// 保存单个图片
async function handleSaveFile(file) {
  datas.tip = '图片保存中...';
  const basePath = await path.downloadDir();
  let selPath = await dialog.save({
    defaultPath: basePath,
  });
  selPath = selPath.replace(/Untitled$/, '');
  const reader = new FileReader();
  reader.readAsArrayBuffer(file.data);
  reader.onload = function (e) {
    let fileU8A = new Uint8Array(e.target.result);
    writeBinaryFile({ contents: fileU8A, path: `${selPath}${file.data.name}` });
    datas.tip = '图片保存成功';
  };
}

Introduce the api of tauri: writeBinaryFile、path、dialog

Use the dialog.save() method to open a file save popup box for the user to choose the save path,
This method has a defaultPath parameter to set the default save path, and the return value of the method is the final file path to be saved. We choose the default download path of the system as the default save path. You can obtain the default download path of the system through the path.downloadDir() method.

FileList

Special attention should be paid to because the default file name provided in the file save dialog box is Untitled , if the developer does not modify or delete it, then the path returned by the dialog.save() method will contain a first-level path. Untitled directory, we can manually intercept it.

handleSaveFile The parameter received by the method is a File Object , which contains the basic information of the current image file and some of our customized information, as shown below:

FileList

Read the Uint8Array data of the picture file through the FileReader api,

Write the file locally through the writeBinaryFile api provided by tauri. writeBinaryFile accepts an object parameter containing contents and path fields,
contents is the file Uint8Array data, path is the path to be saved.

For the convenience of users, we spliced the file name to the path, so that the user does not need to manually fill in the file name in the save file dialog box, just save it directly.

6. One-click save function

 import JSZip from 'jszip';

// 一键打包保存
async function handleDownloadAll() {
  const len = datas.imgList.length;
  if (len === 0) {
    return;
  }
  datas.tip = 'zip 保存中...';
  const zip = new JSZip();
  for (let i = 0; i < len; i++) {
    zip.file(datas.imgList[i].name, datas.imgList[i].data);
  }
  const date = new Date();
  const mon = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '_';
  const day = date.getDate() + '_';
  const hour = date.getHours() + '_';
  const min = date.getMinutes();

  const basePath = await path.downloadDir();
  let selPath = await dialog.save({
    defaultPath: basePath,
  });
  selPath = selPath.replace(/Untitled$/, '');

  zip.generateAsync({ type: 'blob' }).then((content) => {
    let file = new FileReader();
    file.readAsArrayBuffer(content);
    file.onload = function (e) {
      let fileU8A = new Uint8Array(e.target.result);
      writeBinaryFile({ contents: fileU8A, path: `${selPath}IMG_${mon + day + hour + min}.zip` });
      datas.tip = 'zip 保存成功';
    };
  });
}

We use the jszip plugin to type the file into a zip package.

new JSZip()来新建一下zip ,遍历压缩过的文件列表, zip.file()方法将文件添加进去, file() The method receives two parameters, one is the file name and the other is the content data of the file.

Then use the dialog.save() api to call the file to save the bullet box, get the path to be saved, call the zip.generateAsync method to generate the zip package ArrayBuffer data, pass writeBinaryFile api, write the file locally.

Tips: The naming method of the zip package chooses to obtain the year, month, and date information to name it.

stepped pit

1. Selection of tauri version

This can be said to be the biggest pit. Why do you say this, let's look down.

At the beginning of the project construction, I chose the latest version of tauri with full expectation, but when the function was developed to the file system related api, I encountered a serious problem: Unhandled Promise Rejection: cannot traverse directory . Tauri's fs-related api cannot read files in any path. What? How is this fun to play? It is still possible to use it before, is it because of the version problem? I communicated with the tauri community with questions. The final answer is: For security reasons, in the new version, the fs-related api has security restrictions, and can only access files under several system paths provided by tauri.

This definitely won't work, we can't let users compress a picture and have to put the picture in the specified directory before they can access it.

So they reported this problem to the tauri community, and they said that subsequent versions will plan to add the path selected by the user to the whitelist to bypass this restriction.

After getting the answer, the solution is to roll back the tauri version, so we can only go back to the old version. The specific version information is as follows:

package.json

 "@tauri-apps/api": "=1.0.0-beta.8",
"@tauri-apps/cli": "=1.0.0-beta.10",

Cargo.toml

 [build-dependencies]
tauri-build = {version = "=1.0.0-beta.4"}

[dependencies]
serde = {version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = {version = "=1.0.0-beta.8", features = ["api-all"] }

If other small partners also encounter file access restrictions when using tauri, you can refer to the rollback to the above version.

2. The choice of file upload method

In general, there are three ways, let's look at them one by one.

1. The input upload method is banned by tauri, and the file selection dialog box cannot be opened at all.

2. Global drag events provided by tauri

 import { listen } from '@tauri-apps/api/event';
listen('tauri://file-drop', async (event) => {
    console.log(event);
});

By listening to the tauri://file-drop event provided by tauri, you can get the event object of the event, which will return the file path. Yes, only the file path list is returned, we also need to traverse the file path list and use the fs related api to read the file corresponding to each path. This process is long and time-consuming, and the experience is extremely poor.

3. Drap & drop events

By monitoring the drop event, you can directly obtain the uploaded FileList object, which contains the specific information of the file, which is convenient and quick, so this solution is also the solution adopted in this article.

Fill in the hole:

Why add the window to the top function?

Because Image Tiny 's image upload method is drag and drop upload, if there is no window top function, it is easy to be blocked by other applications, which will greatly reduce the user experience. With the top function, users do not need to worry about the problem of occlusion.

package contribution

🔗 Code repository

🔗 Installation package address

image-tiny-package

Welcome to download, install and use. If it is useful, don't forget to like the article 👍🏻, it would be better if it can be forwarded , so that more friends can see it.

Summarize

It took me nearly a week to develop the entire application. During this period, I also stepped on countless pits, constantly struggling to find solutions to problems. But when the development is completed, I am still very happy, and I hope to use Tauri to develop more gadgets to bring you a little convenience~

For more exciting things, please pay attention to our public account "Hundred Bottles Technology", there are irregular benefits!


百瓶技术
127 声望18 粉丝

「百瓶」App 技术团队官方账号。