diff --git a/fe/packages/render/src/core/runtime.js b/fe/packages/render/src/core/runtime.js index 72fc6f6f..ac2175bf 100644 --- a/fe/packages/render/src/core/runtime.js +++ b/fe/packages/render/src/core/runtime.js @@ -216,7 +216,7 @@ class Runtime { const { id, usingComponents, tplComponents } = pageModule.moduleInfo this.pageId = pageId const components = this.createComponent(path, bridgeId, usingComponents) - const self = this + const that = this const rootCom = 'dd-page' const sId = `data-v-${id}` return { @@ -248,15 +248,15 @@ class Runtime { provide('bridgeId', bridgeId) provide('path', path) provide(path, { - id: self.pageId, + id: that.pageId, }) provide('info', { - id: self.pageId, + id: that.pageId, sId, }) const instance = getCurrentInstance().proxy instance.__page__ = true - self.setModuleInstance(self.pageId, instance) + that.setModuleInstance(that.pageId, instance) let ticking = false const handleScroll = () => { @@ -267,7 +267,7 @@ class Runtime { target: 'service', body: { bridgeId, - moduleId: self.pageId, + moduleId: that.pageId, scrollTop: window.scrollY, }, }) @@ -285,7 +285,7 @@ class Runtime { target: 'service', body: { bridgeId, - moduleId: self.pageId, + moduleId: that.pageId, }, }) }) @@ -296,9 +296,9 @@ class Runtime { }) const data = reactive({}) - self.setupData.set(self.pageId, data) - const initData = await message.wait(self.pageId) - self.applyInitialData(self.pageId, data, initData) + that.setupData.set(that.pageId, data) + const initData = await message.wait(that.pageId) + that.applyInitialData(that.pageId, data, initData) return data }, components, @@ -386,7 +386,7 @@ class Runtime { } const components = {} - const self = this + const that = this const newDepthChain = [...depthChain, path] for (const [componentName, componentPath] of Object.entries(usingComponents)) { @@ -417,7 +417,7 @@ class Runtime { sId: parentInfo.sId, }) const vueInstance = getCurrentInstance() - const vueParentId = self.getParentModuleId(vueInstance) + const vueParentId = that.getParentModuleId(vueInstance) const parentId = vueParentId || parentInfo.id const pageInfo = inject(path) const pageId = pageInfo.id @@ -433,7 +433,7 @@ class Runtime { pageId, }) const instance = vueInstance.proxy - self.setModuleInstance(moduleId, instance) + that.setModuleInstance(moduleId, instance) const externalClasses = [] for (const [k, v] of Object.entries(module.props ?? {})) { @@ -482,7 +482,7 @@ class Runtime { _pendingResolved = true _resolvePending?.() } - self._pendingSetups.set(moduleId, new Promise(r => (_resolvePending = r))) + that._pendingSetups.set(moduleId, new Promise(r => (_resolvePending = r))) onMounted(() => { nextTick(() => { @@ -523,16 +523,16 @@ class Runtime { moduleId, }, }) - self.deleteModuleInstance(moduleId) - self.setupData.delete(moduleId) - self.initializedModules.delete(moduleId) - self.preInitUpdates.delete(moduleId) - self._pendingSetups.delete(moduleId) + that.deleteModuleInstance(moduleId) + that.setupData.delete(moduleId) + that.initializedModules.delete(moduleId) + that.preInitUpdates.delete(moduleId) + that._pendingSetups.delete(moduleId) _pendingResolve() }) const data = reactive({}) - self.setupData.set(moduleId, data) + that.setupData.set(moduleId, data) let skipInitialPropsNotify = true watch( @@ -572,9 +572,9 @@ class Runtime { ) const initData = await message.wait(moduleId) - self._pendingSetups.delete(moduleId) + that._pendingSetups.delete(moduleId) _pendingResolve() - self.applyInitialData(moduleId, data, initData) + that.applyInitialData(moduleId, data, initData) return data }, render: module.moduleInfo.render, @@ -1317,13 +1317,24 @@ class Runtime { ) const mimeType = fileType === 'jpg' || fileType === 'jpeg' ? 'image/jpeg' : 'image/png' - const tempFilePath = outputCanvas.toDataURL(mimeType, quality) - const result = { - errMsg: 'canvasToTempFilePath:ok', - tempFilePath, - } - this.triggerCallback(bridgeId, params.success, [result], result) - this.triggerCallback(bridgeId, params.complete, [result], result) + const dataURL = outputCanvas.toDataURL(mimeType, quality) + + // Forward to Container to write base64 to a temp file and return a real file path + message.invoke({ + type: 'invokeAPI', + target: 'container', + body: { + name: 'saveCanvasTempFile', + bridgeId, + params: { + dataURL, + fileType, + success: params.success, + fail: params.fail, + complete: params.complete, + }, + }, + }) } catch (error) { this.triggerCanvasFailure(bridgeId, params, `canvasToTempFilePath:fail ${error.message}`) diff --git a/fe/packages/service/src/api/common/index.js b/fe/packages/service/src/api/common/index.js index 16b39823..9df3a585 100644 --- a/fe/packages/service/src/api/common/index.js +++ b/fe/packages/service/src/api/common/index.js @@ -132,7 +132,7 @@ function invokeMessage(name, params, target) { } } -function invokePromiseAPI(name, params, target) { +export function invokePromiseAPI(name, params, target) { return new Promise((resolve, reject) => { let successId let failId diff --git a/fe/packages/service/src/api/core/ui/canvas/canvas-node.js b/fe/packages/service/src/api/core/ui/canvas/canvas-node.js index f930c163..e63713c2 100644 --- a/fe/packages/service/src/api/core/ui/canvas/canvas-node.js +++ b/fe/packages/service/src/api/core/ui/canvas/canvas-node.js @@ -397,6 +397,38 @@ function serializeCanvasArgs(args) { return Array.from(args).map(arg => serializeCanvasArg(arg)) } +const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' +const BASE64_LOOKUP = new Uint8Array(128) +for (let i = 0; i < BASE64_CHARS.length; i++) { + BASE64_LOOKUP[BASE64_CHARS.charCodeAt(i)] = i +} + +function base64ToUint8ClampedArray(base64) { + let end = base64.length + while (end > 0 && base64[end - 1] === '=') end-- + const byteLen = (end * 3 / 4) | 0 + const bytes = new Uint8ClampedArray(byteLen) + let j = 0 + for (let i = 0; i < end; i += 4) { + const a = BASE64_LOOKUP[base64.charCodeAt(i)] + const b = BASE64_LOOKUP[base64.charCodeAt(i + 1)] + const c = BASE64_LOOKUP[base64.charCodeAt(i + 2)] + const d = BASE64_LOOKUP[base64.charCodeAt(i + 3)] + bytes[j++] = (a << 2) | (b >> 4) + if (j < byteLen) bytes[j++] = ((b & 0xF) << 4) | (c >> 2) + if (j < byteLen) bytes[j++] = ((c & 0x3) << 6) | d + } + return bytes +} + +function deserializeImageData(res) { + return { + width: res.width, + height: res.height, + data: base64ToUint8ClampedArray(res.data), + } +} + function makeResourceId(prefix = 'canvas_resource') { return `${prefix}_${uuid()}` } @@ -520,6 +552,16 @@ class CanvasRenderingContext2DProxy { }) } + getImageData(x, y, w, h) { + return this.canvas.getImageData({ + contextId: this.contextId, x, y, width: w, height: h, + }) + } + + toDataURL(type, quality) { + return this.canvas.toDataURL(type, quality) + } + call(method, args) { if (method === 'measureText') { return { width: String(args[0] ?? '').length * 10 } @@ -748,6 +790,45 @@ export class CanvasNode { }) } + getImageData({ contextId, x, y, width, height }) { + return new Promise((resolve, reject) => { + const callbackId = callback.store((res) => { + try { + if (res.width && res.height && res.data) { + resolve(deserializeImageData(res)) + } else { + reject(new Error('getImageData: invalid format')) + } + } catch (error) { + reject(error) + } + }) + this.enqueueOperation({ + op: 'getImageData', + contextId, + x, + y, + width, + height, + callback: callbackId, + }) + }) + } + + toDataURL(type = 'image/png', quality) { + return new Promise((resolve) => { + const callbackId = callback.store((dataURL) => { + resolve(dataURL) + }) + this.enqueueOperation({ + op: 'toDataURL', + mimeType: type, + quality, + callback: callbackId, + }) + }) + } + enqueueOperation(operation) { this.pendingOperations.push(operation) if (this.flushScheduled) { diff --git a/fe/packages/service/src/api/core/wxml/intersection-observer/index.js b/fe/packages/service/src/api/core/wxml/intersection-observer/index.js index 80a710d7..0f9d45ce 100644 --- a/fe/packages/service/src/api/core/wxml/intersection-observer/index.js +++ b/fe/packages/service/src/api/core/wxml/intersection-observer/index.js @@ -59,13 +59,13 @@ class IntersectionObserver { * https://developers.weixin.qq.com/miniprogram/dev/api/wxml/IntersectionObserver.observe.html */ observe(targetSelector, listener) { - const self = this + const that = this const id = callback.store((res) => { - if (!self._disconnected) { - self._observerId = res.observerId + if (!that._disconnected) { + that._observerId = res.observerId } if (res.info) { - listener.call(self, res.info) + listener.call(that, res.info) } }, true) diff --git a/fe/packages/service/src/api/core/wxml/selector-query/index.js b/fe/packages/service/src/api/core/wxml/selector-query/index.js index b806e5de..a3bb8d28 100644 --- a/fe/packages/service/src/api/core/wxml/selector-query/index.js +++ b/fe/packages/service/src/api/core/wxml/selector-query/index.js @@ -94,21 +94,21 @@ class SelectorQuery { * @param {Function} callback */ exec(callback) { - const self = this + const that = this const bridgeId = getCurrentBridgeId() const data = { tasks: this.__taskQueue, success: (res) => { const hydratedRes = hydrateSelectorQueryResult(res, bridgeId) || [] hydratedRes.forEach((nodeInfo, index) => { - const cb = self.__cbQueue[index] + const cb = that.__cbQueue[index] if (isFunction(cb)) { - cb.call(self, nodeInfo) + cb.call(that, nodeInfo) } }) if (isFunction(callback)) { - callback.call(self, hydratedRes) + callback.call(that, hydratedRes) } }, } diff --git a/fe/packages/service/src/core/utils.js b/fe/packages/service/src/core/utils.js index 14c1a603..99911a8a 100644 --- a/fe/packages/service/src/core/utils.js +++ b/fe/packages/service/src/core/utils.js @@ -32,12 +32,12 @@ export { deepEqual } * 只能在实例初始化完成后从框架运行时读取。目前通过 __mpxProxy 访问, * 后续若框架侧暴露标准字段(如 moduleInfo.__computedKeys),可在此替换为更通用的读取方式。 */ -export function addComputedData(self) { - const computed = self.__mpxProxy?.options?.computed +export function addComputedData(ctx) { + const computed = ctx.__mpxProxy?.options?.computed if (computed) { for (const ck of Object.keys(computed)) { - if (!Object.prototype.hasOwnProperty.call(self.data, ck)) { - self.data[ck] = null + if (!Object.prototype.hasOwnProperty.call(ctx.data, ck)) { + ctx.data[ck] = null } } } diff --git a/harmony/dimina/src/main/cpp/utils.cpp b/harmony/dimina/src/main/cpp/utils.cpp index bcb9b1a1..855b2ab9 100644 --- a/harmony/dimina/src/main/cpp/utils.cpp +++ b/harmony/dimina/src/main/cpp/utils.cpp @@ -614,7 +614,7 @@ std::string getJsValueString(JSContext *ctx, JSValueConst jsValue, int indentLev } else if (JS_IsNumber(jsValue)) { int tag = JS_VALUE_GET_TAG(jsValue); if (tag == JS_TAG_INT) { - int value = jsValue.u.int32; + int value = JS_VALUE_GET_INT(jsValue); output = std::to_string(value); } else if (tag == JS_TAG_FLOAT64) { double num; diff --git a/harmony/dimina/src/main/ets/Bridges/DMPAppModuleManager.ets b/harmony/dimina/src/main/ets/Bridges/DMPAppModuleManager.ets index 7c50757c..3532f502 100644 --- a/harmony/dimina/src/main/ets/Bridges/DMPAppModuleManager.ets +++ b/harmony/dimina/src/main/ets/Bridges/DMPAppModuleManager.ets @@ -35,6 +35,7 @@ import { DMPContainerBridgesModuleBluetooth } from './DMPContainerBridgesModule+ import { DMPContainerBridgesModuleVideo } from './DMPContainerBridgesModule+Video' import { DMPContainerBridgesModuleTabBar } from './DMPContainerBridgesModule+TabBar'; import { DMPContainerBridgesModuleFile } from './DMPContainerBridgesModule+File'; +import { DMPContainerBridgesModuleCanvas } from './DMPContainerBridgesModule+Canvas'; export class DMPAppModuleManager extends DMPModuleManager { @@ -101,6 +102,7 @@ export class DMPAppModuleManager extends DMPModuleManager { this.registerModule(new DMPContainerBridgesModuleNavigationBar(app)) this.registerModule(new DMPContainerBridgesModuleNavigateToMiniProgram(app)) this.registerModule(new DMPContainerBridgesModuleTabBar(app)) + this.registerModule(new DMPContainerBridgesModuleCanvas(app)) } } diff --git a/harmony/dimina/src/main/ets/Bridges/DMPContainerBridgesModule+Canvas.ets b/harmony/dimina/src/main/ets/Bridges/DMPContainerBridgesModule+Canvas.ets new file mode 100644 index 00000000..76de1346 --- /dev/null +++ b/harmony/dimina/src/main/ets/Bridges/DMPContainerBridgesModule+Canvas.ets @@ -0,0 +1,68 @@ +import { DMPContainerBridgesModule } from './DMPContainerBridgesModule' +import { DMPBridgeCallback } from './DMPTSUtil' +import { DMPMap } from '../Utils/DMPMap' +import { DMPFileUrlConvertor } from '../Bundle/Util/DMPFileUrlConvertor' +import { DMPFileManager } from '../Bundle/Util/DMPFileManager' +import { DMPApp } from '../DApp/DMPApp' +import { DMPLogger } from '../EventTrack/DMPLogger' +import fs from '@ohos.file.fs' +import { buffer } from '@kit.ArkTS' + +export class DMPContainerBridgesModuleCanvas extends DMPContainerBridgesModule { + getExportMethods(): string[] { + return ['saveCanvasTempFile'] + } + + saveCanvasTempFile(data: DMPMap, callback: DMPBridgeCallback) { + const dataURL = data.getString('dataURL') ?? '' + const fileType = data.getString('fileType') ?? 'png' + + if (!dataURL) { + this.invokeFailureCallback(callback, null, 'saveCanvasTempFile:fail dataURL is empty') + return + } + + try { + // Strip the data URL prefix (e.g. "data:image/png;base64,") + const base64Prefix = new RegExp('^data:image/\\w+;base64,') + const base64Data = dataURL.replace(base64Prefix, '') + + // Decode base64 to binary + const buf = buffer.from(base64Data, 'base64') + + // Build temp file path + const app: DMPApp = this.app + const fileMgr = DMPFileManager.sharedInstance() + const fileTmpDir = fileMgr.appTmpDirectory(app.appConfig.appId) + + // Ensure temp directory exists + try { + fs.mkdirSync(fileTmpDir, true) + } catch (e) { + DMPLogger.d('saveCanvasTempFile', 'tmp dir already exists or mkdir error: ' + e) + } + + const ext = (fileType === 'jpg' || fileType === 'jpeg') ? 'jpg' : 'png' + const timestamp = Date.now() + const random = Math.floor(Math.random() * 100000) + const fileName = `canvas_${timestamp}_${random}.${ext}` + const filePath = `${fileTmpDir}/${fileName}` + + // Write to file + const file = fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE) + fs.writeSync(file.fd, buf.buffer) + fs.closeSync(file) + + // Convert to virtual path + const tempFilePath = DMPFileUrlConvertor.vPathFromLocalPath(filePath) + + const result = new DMPMap() + result.set('tempFilePath', tempFilePath) + result.set('errMsg', 'canvasToTempFilePath:ok') + this.invokeSuccessCallback(callback, result) + } catch (error) { + DMPLogger.e('saveCanvasTempFile error: ' + error) + this.invokeFailureCallback(callback, null, `saveCanvasTempFile:fail ${error}`) + } + } +} diff --git a/shared/jssdk/config.json b/shared/jssdk/config.json index 82b8af09..5ef49686 100644 --- a/shared/jssdk/config.json +++ b/shared/jssdk/config.json @@ -1,4 +1,4 @@ { - "versionCode": 14, - "versionName": "1.0.13" + "versionCode": 15, + "versionName": "1.0.14" } diff --git a/shared/jssdk/main.zip b/shared/jssdk/main.zip index 6d6919ac..5b572ef4 100644 Binary files a/shared/jssdk/main.zip and b/shared/jssdk/main.zip differ