init
This commit is contained in:
commit
f821697a79
31
.eslintrc.js
Normal file
31
.eslintrc.js
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Eslint config file
|
||||
* Documentation: https://eslint.org/docs/user-guide/configuring/
|
||||
* Install the Eslint extension before using this feature.
|
||||
*/
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
ecmaFeatures: {
|
||||
modules: true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
wx: true,
|
||||
App: true,
|
||||
Page: true,
|
||||
getCurrentPages: true,
|
||||
getApp: true,
|
||||
Component: true,
|
||||
requirePlugin: true,
|
||||
requireMiniProgram: true,
|
||||
},
|
||||
// extends: 'eslint:recommended',
|
||||
rules: {},
|
||||
}
|
||||
24
app.json
Normal file
24
app.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationStyle": "custom"
|
||||
},
|
||||
"style": "v2",
|
||||
"renderer": "skyline",
|
||||
"rendererOptions": {
|
||||
"skyline": {
|
||||
"defaultDisplayBlock": true,
|
||||
"defaultContentBox": true,
|
||||
"tagNameStyleIsolation": "legacy",
|
||||
"disableABTest": true,
|
||||
"sdkVersionBegin": "3.0.0",
|
||||
"sdkVersionEnd": "15.255.255"
|
||||
}
|
||||
},
|
||||
"componentFramework": "glass-easel",
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"lazyCodeLoading": "requiredComponents"
|
||||
}
|
||||
10
app.wxss
Normal file
10
app.wxss
Normal file
@ -0,0 +1,10 @@
|
||||
/**app.wxss**/
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 200rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
BIN
convert_image_to_webp.wasm
Executable file
BIN
convert_image_to_webp.wasm
Executable file
Binary file not shown.
BIN
pages/.DS_Store
vendored
Normal file
BIN
pages/.DS_Store
vendored
Normal file
Binary file not shown.
209
pages/index/index.js
Normal file
209
pages/index/index.js
Normal file
@ -0,0 +1,209 @@
|
||||
// index.js
|
||||
const { wasmMgr } = require('./utils.js');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
originalImage: '', // 原始图片路径
|
||||
compressedImage: '', // 压缩后图片路径
|
||||
originalSize: 0, // 原始图片大小
|
||||
compressedSize: 0, // 压缩后图片大小
|
||||
compressing: false, // 是否正在压缩
|
||||
quality: 0.8, // 压缩质量
|
||||
targetWidth: 800, // 目标宽度
|
||||
targetHeight: 600, // 目标高度
|
||||
originalWidth: 0, // 原始宽度
|
||||
originalHeight: 0, // 原始高度
|
||||
showResult: false // 是否显示结果
|
||||
},
|
||||
|
||||
// 选择图片
|
||||
chooseImage() {
|
||||
wx.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['original'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
const tempFilePath = res.tempFilePaths[0];
|
||||
|
||||
// 获取图片信息
|
||||
wx.getImageInfo({
|
||||
src: tempFilePath,
|
||||
success: (imgInfo) => {
|
||||
this.setData({
|
||||
originalImage: tempFilePath,
|
||||
originalWidth: imgInfo.width,
|
||||
originalHeight: imgInfo.height,
|
||||
targetWidth: imgInfo.width,
|
||||
targetHeight: imgInfo.height,
|
||||
showResult: false
|
||||
});
|
||||
|
||||
// 获取文件大小
|
||||
wx.getFileInfo({
|
||||
filePath: tempFilePath,
|
||||
success: (fileInfo) => {
|
||||
this.setData({
|
||||
originalSize: (fileInfo.size / 1024).toFixed(2) // KB
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
wx.showToast({
|
||||
title: '选择图片失败',
|
||||
icon: 'none'
|
||||
});
|
||||
console.error('选择图片失败', err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 压缩图片
|
||||
async compressImage() {
|
||||
if (!this.data.originalImage) {
|
||||
wx.showToast({
|
||||
title: '请先选择图片',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setData({ compressing: true });
|
||||
|
||||
try {
|
||||
wx.showLoading({
|
||||
title: '压缩中...'
|
||||
});
|
||||
|
||||
const compressedBase64 = await wasmMgr.compressImg(
|
||||
this.data.originalImage,
|
||||
this.data.quality,
|
||||
this.data.originalWidth,
|
||||
this.data.originalHeight,
|
||||
this.data.targetWidth,
|
||||
this.data.targetHeight
|
||||
);
|
||||
console.log(compressedBase64);
|
||||
// 将base64保存为临时文件
|
||||
const tempFilePath = await this.saveBase64ToFile(compressedBase64);
|
||||
|
||||
// 获取压缩后文件大小
|
||||
wx.getFileInfo({
|
||||
filePath: tempFilePath,
|
||||
success: (fileInfo) => {
|
||||
this.setData({
|
||||
compressedImage: tempFilePath,
|
||||
compressedSize: (fileInfo.size / 1024).toFixed(2), // KB
|
||||
showResult: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wx.hideLoading();
|
||||
wx.showToast({
|
||||
title: '压缩成功',
|
||||
icon: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
wx.hideLoading();
|
||||
wx.showToast({
|
||||
title: '压缩失败',
|
||||
icon: 'none'
|
||||
});
|
||||
console.error('压缩失败', error);
|
||||
} finally {
|
||||
this.setData({ compressing: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 将base64保存为临时文件
|
||||
saveBase64ToFile(base64Data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 去掉data:image/webp;base64,前缀
|
||||
const base64 = base64Data.replace(/^data:image\/\w+;base64,/, '');
|
||||
|
||||
// 创建文件管理器
|
||||
const fs = wx.getFileSystemManager();
|
||||
|
||||
// 生成临时文件路径
|
||||
const tempFilePath = `${wx.env.USER_DATA_PATH}/compressed_${Date.now()}.webp`;
|
||||
|
||||
// 将base64写入文件
|
||||
fs.writeFile({
|
||||
filePath: tempFilePath,
|
||||
data: base64,
|
||||
encoding: 'base64',
|
||||
success: () => {
|
||||
resolve(tempFilePath);
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('保存文件失败', err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 保存压缩后的图片到相册
|
||||
saveImageToAlbum() {
|
||||
if (!this.data.compressedImage) {
|
||||
wx.showToast({
|
||||
title: '没有可保存的图片',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: this.data.compressedImage,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '保存成功',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg.includes('auth deny')) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要您授权保存相册',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.openSetting();
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '保存失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
console.error('保存图片失败', err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 质量滑块变化
|
||||
onQualityChange(e) {
|
||||
this.setData({
|
||||
quality: e.detail.value/100
|
||||
});
|
||||
},
|
||||
|
||||
// 宽度输入变化
|
||||
onWidthChange(e) {
|
||||
this.setData({
|
||||
targetWidth: parseInt(e.detail.value) || 0
|
||||
});
|
||||
},
|
||||
|
||||
// 高度输入变化
|
||||
onHeightChange(e) {
|
||||
this.setData({
|
||||
targetHeight: parseInt(e.detail.value) || 0
|
||||
});
|
||||
}
|
||||
});
|
||||
3
pages/index/index.json
Normal file
3
pages/index/index.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
|
||||
}
|
||||
84
pages/index/index.wxml
Normal file
84
pages/index/index.wxml
Normal file
@ -0,0 +1,84 @@
|
||||
<!--index.wxml-->
|
||||
<navigation-bar title="图片压缩工具" back="{{false}}" color="black" background="#FFF"></navigation-bar>
|
||||
<scroll-view class="scrollarea" scroll-y type="list">
|
||||
<view class="container">
|
||||
<!-- 选择图片区域 -->
|
||||
<view class="section">
|
||||
<view class="section-title">选择图片</view>
|
||||
<view class="image-picker" bindtap="chooseImage">
|
||||
<image wx:if="{{originalImage}}" src="{{originalImage}}" mode="aspectFit" class="preview-image"></image>
|
||||
<view wx:else class="placeholder">
|
||||
<text class="placeholder-text">点击选择图片</text>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{originalImage}}" class="image-info">
|
||||
<text>原始尺寸: {{originalWidth}} × {{originalHeight}}</text>
|
||||
<text>原始大小: {{originalSize}} KB</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 压缩设置区域 -->
|
||||
<view wx:if="{{originalImage}}" class="section">
|
||||
<view class="section-title">压缩设置</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">压缩比例: {{(quality * 100)}}%</text>
|
||||
<slider
|
||||
class="setting-slider"
|
||||
min="10"
|
||||
max="100"
|
||||
value="{{quality * 100}}"
|
||||
bindchange="onQualityChange"
|
||||
activeColor="#07C160"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">目标宽度 可以填0为原始宽高</text>
|
||||
<input
|
||||
class="setting-input"
|
||||
type="number"
|
||||
value="{{targetWidth}}"
|
||||
bindinput="onWidthChange"
|
||||
placeholder="输入宽度"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">目标高度 可以填0为原始宽高</text>
|
||||
<input
|
||||
class="setting-input"
|
||||
type="number"
|
||||
value="{{targetHeight}}"
|
||||
bindinput="onHeightChange"
|
||||
placeholder="输入高度"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<button
|
||||
class="compress-btn"
|
||||
type="primary"
|
||||
bindtap="compressImage"
|
||||
loading="{{compressing}}"
|
||||
disabled="{{compressing}}"
|
||||
>
|
||||
{{compressing ? '压缩中...' : '开始压缩'}}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 压缩结果区域 -->
|
||||
<view wx:if="{{showResult}}" class="section">
|
||||
<view class="section-title">压缩结果</view>
|
||||
<view class="result-container">
|
||||
<view class="result-image">
|
||||
<image src="{{compressedImage}}" mode="aspectFit" class="preview-image"></image>
|
||||
</view>
|
||||
<view class="result-info">
|
||||
<text>压缩后大小: {{compressedSize}} KB</text>
|
||||
<text>压缩率: {{((1 - compressedSize/originalSize) * 100)}}%</text>
|
||||
<button class="save-btn" type="default" bindtap="saveImageToAlbum">保存到相册</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
95
pages/index/index.wxss
Normal file
95
pages/index/index.wxss
Normal file
@ -0,0 +1,95 @@
|
||||
/**index.wxss**/
|
||||
page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.scrollarea {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.image-picker {
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.image-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.setting-slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.result-image {
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.result-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
font-size: 30rpx;
|
||||
border-radius: 40rpx;
|
||||
margin-top: 20rpx;
|
||||
background-color: #fff;
|
||||
color: #07C160;
|
||||
border: 1rpx solid #07C160;
|
||||
}
|
||||
131
pages/index/utils.js
Normal file
131
pages/index/utils.js
Normal file
@ -0,0 +1,131 @@
|
||||
// wasm图片压缩工具类
|
||||
const wasmMgr = {
|
||||
// 存储已加载的wasm模块
|
||||
imageCompressModule: null,
|
||||
|
||||
// 获取图片压缩模块
|
||||
getCompressImg() {
|
||||
if (this.imageCompressModule) {
|
||||
return Promise.resolve(this.imageCompressModule);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const wasmImports = {
|
||||
__assert_fail: (condition, filename, line, func) => {
|
||||
console.log(condition, filename, line, func);
|
||||
},
|
||||
emscripten_resize_heap: (size, old_size) => {
|
||||
console.log(size, old_size);
|
||||
},
|
||||
fd_close: (fd) => {
|
||||
console.log(fd);
|
||||
},
|
||||
fd_seek: (fd, offset, whence) => {
|
||||
console.log(fd, offset, whence);
|
||||
},
|
||||
fd_write: (fd, buf, len, pos) => {
|
||||
console.log(fd, buf, len, pos);
|
||||
},
|
||||
emscripten_memcpy_js: (dest, src, len) => {
|
||||
this.imageCompressModule.HEAPU8.copyWithin(dest, src, src + len);
|
||||
},
|
||||
};
|
||||
|
||||
// 微信小程序环境
|
||||
if (typeof WXWebAssembly !== "undefined") {
|
||||
WXWebAssembly.instantiate(
|
||||
"/convert_image_to_webp.wasm",
|
||||
{
|
||||
env: wasmImports,
|
||||
wasi_snapshot_preview1: wasmImports,
|
||||
}
|
||||
).then((result) => {
|
||||
this.imageCompressModule = {
|
||||
_convert_image_to_webp: result.instance.exports.convert_image_to_webp,
|
||||
_malloc: result.instance.exports.malloc,
|
||||
_free: result.instance.exports.free,
|
||||
};
|
||||
this.imageCompressModule.HEAPU8 = new Uint8Array(
|
||||
result.instance.exports.memory.buffer
|
||||
);
|
||||
console.log("convert_image_to_webp加载成功");
|
||||
resolve(this.imageCompressModule);
|
||||
}).catch((err) => {
|
||||
console.error("Failed to load wasm script", err);
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
// H5环境或其他环境
|
||||
console.error("当前环境不支持WebAssembly");
|
||||
reject(new Error("当前环境不支持WebAssembly"));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 图片压缩函数
|
||||
async compressImg(file, quality = 0.5, w, h, target_w, target_h) {
|
||||
const compressImgHandler = (inputData, module, isOrgin = false) => {
|
||||
const inputDataPtr = module._malloc(inputData.length);
|
||||
module.HEAPU8.set(inputData, inputDataPtr);
|
||||
const outputSizePtr = module._malloc(4);
|
||||
const webpPtr = module._convert_image_to_webp(
|
||||
inputDataPtr,
|
||||
inputData.length,
|
||||
w,
|
||||
h,
|
||||
target_w,
|
||||
target_h,
|
||||
80 * (quality > 1 ? 1 : quality),
|
||||
outputSizePtr,
|
||||
1,
|
||||
isOrgin ? 1 : 0
|
||||
);
|
||||
const outputSize =
|
||||
module.HEAPU8[outputSizePtr] |
|
||||
(module.HEAPU8[outputSizePtr + 1] << 8) |
|
||||
(module.HEAPU8[outputSizePtr + 2] << 16) |
|
||||
(module.HEAPU8[outputSizePtr + 3] << 24);
|
||||
const webpData = new Uint8Array(module.HEAPU8.buffer, webpPtr, outputSize);
|
||||
module._free(webpPtr);
|
||||
module._free(outputSizePtr);
|
||||
module._free(inputDataPtr);
|
||||
//如果只需要二进制原始数据,可以直接返回webpdata 减少base64转换
|
||||
// return webpData
|
||||
return 'data:image/webp;base64,' + this.arrayBufferToBase64(webpData);
|
||||
};
|
||||
|
||||
try {
|
||||
const module = await this.getCompressImg();
|
||||
|
||||
if (file instanceof Uint8Array) {
|
||||
return compressImgHandler(file, module, true);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.getFileSystemManager().readFile({
|
||||
filePath: file,
|
||||
success: res => {
|
||||
resolve(compressImgHandler(new Uint8Array(res.data), module));
|
||||
},
|
||||
fail: e => {
|
||||
console.error("读取文件失败", e);
|
||||
reject(e);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("图片压缩失败", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// 将ArrayBuffer转换为Base64
|
||||
arrayBufferToBase64(buffer) {
|
||||
|
||||
return wx.arrayBufferToBase64(buffer);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
wasmMgr
|
||||
};
|
||||
28
project.config.json
Normal file
28
project.config.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"appid": "touristappid",
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "3.11.1",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"setting": {
|
||||
"coverView": true,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true,
|
||||
"enhance": true,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"packNpmRelationList": [],
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
}
|
||||
},
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
}
|
||||
}
|
||||
7
project.private.config.json
Normal file
7
project.private.config.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||
"projectname": "wasm_webp",
|
||||
"setting": {
|
||||
"compileHotReLoad": true
|
||||
}
|
||||
}
|
||||
7
sitemap.json
Normal file
7
sitemap.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
|
||||
"rules": [{
|
||||
"action": "allow",
|
||||
"page": "*"
|
||||
}]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user