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
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:
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, Submenu
, Submenu::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.
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.
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:
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
🔗 Installation package address
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~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。