commit f821697a79a7477e9ead6c8c3985d18c8db24d17 Author: kura Date: Fri Nov 28 13:27:24 2025 +0800 init diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..51707bc Binary files /dev/null and b/.DS_Store differ diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..115cc02 --- /dev/null +++ b/.eslintrc.js @@ -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: {}, +} diff --git a/app.js b/app.js new file mode 100644 index 0000000..4af33be --- /dev/null +++ b/app.js @@ -0,0 +1,2 @@ +// app.js +App({}) diff --git a/app.json b/app.json new file mode 100644 index 0000000..56a9d30 --- /dev/null +++ b/app.json @@ -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" +} \ No newline at end of file diff --git a/app.wxss b/app.wxss new file mode 100644 index 0000000..06c6fc9 --- /dev/null +++ b/app.wxss @@ -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; +} diff --git a/convert_image_to_webp.wasm b/convert_image_to_webp.wasm new file mode 100755 index 0000000..5d92b31 Binary files /dev/null and b/convert_image_to_webp.wasm differ diff --git a/pages/.DS_Store b/pages/.DS_Store new file mode 100644 index 0000000..55021bd Binary files /dev/null and b/pages/.DS_Store differ diff --git a/pages/index/index.js b/pages/index/index.js new file mode 100644 index 0000000..3f015ea --- /dev/null +++ b/pages/index/index.js @@ -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 + }); + } +}); diff --git a/pages/index/index.json b/pages/index/index.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/pages/index/index.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/pages/index/index.wxml b/pages/index/index.wxml new file mode 100644 index 0000000..65d755c --- /dev/null +++ b/pages/index/index.wxml @@ -0,0 +1,84 @@ + + + + + + + 选择图片 + + + + 点击选择图片 + + + + 原始尺寸: {{originalWidth}} × {{originalHeight}} + 原始大小: {{originalSize}} KB + + + + + + 压缩设置 + + + 压缩比例: {{(quality * 100)}}% + + + + + 目标宽度 可以填0为原始宽高 + + + + + 目标高度 可以填0为原始宽高 + + + + + + + + + 压缩结果 + + + + + + 压缩后大小: {{compressedSize}} KB + 压缩率: {{((1 - compressedSize/originalSize) * 100)}}% + + + + + + diff --git a/pages/index/index.wxss b/pages/index/index.wxss new file mode 100644 index 0000000..2e9e6e1 --- /dev/null +++ b/pages/index/index.wxss @@ -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; +} diff --git a/pages/index/utils.js b/pages/index/utils.js new file mode 100644 index 0000000..02dfc85 --- /dev/null +++ b/pages/index/utils.js @@ -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 +}; \ No newline at end of file diff --git a/project.config.json b/project.config.json new file mode 100644 index 0000000..32076de --- /dev/null +++ b/project.config.json @@ -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 + } +} \ No newline at end of file diff --git a/project.private.config.json b/project.private.config.json new file mode 100644 index 0000000..9641cf3 --- /dev/null +++ b/project.private.config.json @@ -0,0 +1,7 @@ +{ + "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", + "projectname": "wasm_webp", + "setting": { + "compileHotReLoad": true + } +} \ No newline at end of file diff --git a/sitemap.json b/sitemap.json new file mode 100644 index 0000000..cd24f35 --- /dev/null +++ b/sitemap.json @@ -0,0 +1,7 @@ +{ + "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", + "rules": [{ + "action": "allow", + "page": "*" + }] +} \ No newline at end of file