This commit is contained in:
kura 2025-11-28 13:27:24 +08:00
commit f821697a79
15 changed files with 631 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

31
.eslintrc.js Normal file
View 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: {},
}

2
app.js Normal file
View File

@ -0,0 +1,2 @@
// app.js
App({})

24
app.json Normal file
View 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
View 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

Binary file not shown.

BIN
pages/.DS_Store vendored Normal file

Binary file not shown.

209
pages/index/index.js Normal file
View 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
View File

@ -0,0 +1,3 @@
{
}

84
pages/index/index.wxml Normal file
View 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
View 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
View 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
View 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
}
}

View 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
View File

@ -0,0 +1,7 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
}