fix(share): 截图改用 componentSnapshot 抓Web组件渲染像素(修WebGL黑屏) + 剪贴板图片改fd写入

- BridgeGameContainer: 大厅Web加 .id('web_lobby'),子游戏Web加 .id('web_subgame'),
  供 componentSnapshot.get 按组件id定位
- WebSlot: captureCanvas 整体替换为 componentSnapshot.get + image.ImagePacker.packToData,
  规避 WebGL canvas.toDataURL 黑屏;packToData 为 packing 的非废弃替代(§API13+);
  删除 parseJsString(仅被旧 captureCanvas 内联用,新实现不需要)
- ShareProvider: writeClipboardImage 改用 fileIo.openSync 取 fd 再
  createImageSource(fd),修复传路径字符串可能失败问题;补 import fileIo

API签名核对(devecocli docs):
  componentSnapshot.get(id, options) → Promise<PixelMap>(deprecated API18,仍可用)
  ImagePacker.packToData(pixelMap, PackingOption) → Promise<ArrayBuffer>(API13+,WARN无error)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-27 15:31:56 +08:00
co-authored by Claude Sonnet 4.6
parent 75b7c7fb07
commit 1902e4e5be
3 changed files with 38 additions and 38 deletions
@@ -347,6 +347,7 @@ export struct BridgeGameContainer {
Stack() { Stack() {
// 大厅 Web(常驻) // 大厅 Web(常驻)
Web({ src: this.lobbySrc(), controller: this.controllerL }) Web({ src: this.lobbySrc(), controller: this.controllerL })
.id('web_lobby')
.javaScriptAccess(true).domStorageAccess(true).fileAccess(true) .javaScriptAccess(true).domStorageAccess(true).fileAccess(true)
.mixedMode(MixedMode.All).cacheMode(CacheMode.None) .mixedMode(MixedMode.All).cacheMode(CacheMode.None)
.geolocationAccess(true).zoomAccess(false) .geolocationAccess(true).zoomAccess(false)
@@ -372,6 +373,7 @@ export struct BridgeGameContainer {
// 子游戏 Web(临时,subgameUrl 非空时存在) // 子游戏 Web(临时,subgameUrl 非空时存在)
if (this.subgameUrl !== '' && this.slotS !== undefined) { if (this.subgameUrl !== '' && this.slotS !== undefined) {
Web({ src: this.subgameUrl, controller: this.slotS.controller }) Web({ src: this.subgameUrl, controller: this.slotS.controller })
.id('web_subgame')
.javaScriptAccess(true).domStorageAccess(true).fileAccess(true) .javaScriptAccess(true).domStorageAccess(true).fileAccess(true)
.mixedMode(MixedMode.All).cacheMode(CacheMode.None) .mixedMode(MixedMode.All).cacheMode(CacheMode.None)
.geolocationAccess(true).zoomAccess(false) .geolocationAccess(true).zoomAccess(false)
+25 -35
View File
@@ -1,4 +1,7 @@
import { webview } from '@kit.ArkWeb'; import { webview } from '@kit.ArkWeb';
import { componentSnapshot } from '@kit.ArkUI';
import { image } from '@kit.ImageKit';
import { util } from '@kit.ArkTS';
import { common } from '@kit.AbilityKit'; import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit'; import { BusinessError } from '@kit.BasicServicesKit';
import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge'; import { BridgeController, WebviewControllerAdapter, BridgeJsLoader } from 'feature_bridge';
@@ -225,46 +228,33 @@ export class WebSlot {
} }
/** /**
* 截取本槽 Web 内的 canvas(截图分享)。脚本在 H5 内 toDataURL,返回 dataURL * 截取本槽 Web 组件已渲染像素(截图分享)。改用 componentSnapshot 抓组件合成结果,
* runJavaScript 回调的 result 是 JSON 字符串(外层带引号),需 JSON.parse 去引号还原 * 规避 WebGL canvas.toDataURL 黑屏问题;打包为 image/jpeg 的 dataURL 返回
* 非激活/出错一律返回空串(由 ShareProvider 降级网页分享,绝不抛错)。 * 非激活/出错一律返回空串(由 ShareProvider 降级,绝不抛错)。canvasId 参数保留兼容、当前忽略。
*/ */
private captureCanvas(canvasId: string): Promise<string> { private captureCanvas(canvasId: string): Promise<string> {
if (!this.active || this.disposed) { if (!this.active || this.disposed) {
return Promise.resolve(''); return Promise.resolve('');
} }
const sel: string = canvasId !== '' const compId: string = `web_${this.role}`;
? `document.getElementById(${JSON.stringify(canvasId)}).toDataURL('image/png')` return componentSnapshot.get(compId, { waitUntilRenderFinished: true })
: `document.querySelector('canvas').toDataURL('image/jpeg',0.8)`; .then((pm: image.PixelMap): Promise<string> => {
const script: string = `(function(){try{return ${sel};}catch(e){return '';}})()`; const packer: image.ImagePacker = image.createImagePacker();
return new Promise<string>((resolve: (v: string) => void) => { return packer.packToData(pm, { format: 'image/jpeg', quality: 90 })
try { .then((buf: ArrayBuffer): string => {
this.controller.runJavaScript(script, (err: BusinessError, result: string) => { const b64: string = new util.Base64Helper().encodeToStringSync(new Uint8Array(buf));
if (err) { WebSlot.log.i(`[${this.role}] snapshot ok bytes=${buf.byteLength}`);
WebSlot.log.w(`[${this.role}] captureCanvas runJavaScript error: ${err.message}`); return `data:image/jpeg;base64,${b64}`;
resolve(''); })
return; .finally(() => {
} pm.release();
resolve(WebSlot.parseJsString(result)); packer.release();
}); });
} catch (e) { })
WebSlot.log.w(`[${this.role}] captureCanvas failed: ${(e as BusinessError).message}`); .catch((e: Object): string => {
resolve(''); WebSlot.log.w(`[${this.role}] componentSnapshot failed: ${(e as Error).message}`);
} return '';
}); });
}
/** runJavaScript 返回值是 JSON 字符串(字符串结果外层带引号);去引号还原,异常返回空串。 */
private static parseJsString(result: string): string {
if (result === undefined || result === null || result === '' || result === 'null') {
return '';
}
try {
const parsed: Object = JSON.parse(result) as Object;
return typeof parsed === 'string' ? parsed as string : '';
} catch (e) {
return '';
}
} }
private flushPending(): void { private flushPending(): void {
@@ -15,7 +15,7 @@
*/ */
import { systemShare } from '@kit.ShareKit'; import { systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData'; import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { fileUri } from '@kit.CoreFileKit'; import { fileUri, fileIo } from '@kit.CoreFileKit';
import { common, bundleManager } from '@kit.AbilityKit'; import { common, bundleManager } from '@kit.AbilityKit';
import { BusinessError, pasteboard } from '@kit.BasicServicesKit'; import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
import { image } from '@kit.ImageKit'; import { image } from '@kit.ImageKit';
@@ -316,7 +316,14 @@ export class ShareProvider implements CapabilityProvider {
/** 图片写剪贴板(尽力;对方能否粘贴 PIXELMAP 不可保证,systemImage 才是可靠交付)。 */ /** 图片写剪贴板(尽力;对方能否粘贴 PIXELMAP 不可保证,systemImage 才是可靠交付)。 */
private writeClipboardImage(filePath: string): void { private writeClipboardImage(filePath: string): void {
const source: image.ImageSource = image.createImageSource(filePath); let file: fileIo.File;
try {
file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
} catch (e) {
this.log.w(`clipboard image open failed: ${(e as Error).message}`);
return;
}
const source: image.ImageSource = image.createImageSource(file.fd);
source.createPixelMap().then((pm: image.PixelMap) => { source.createPixelMap().then((pm: image.PixelMap) => {
let data: pasteboard.PasteData; let data: pasteboard.PasteData;
try { try {
@@ -326,14 +333,15 @@ export class ShareProvider implements CapabilityProvider {
pm.release(); pm.release();
return; return;
} }
// pm 须存活到 setData 完成(createData 可能持引用),故在 setData settle 后再 release。
pasteboard.getSystemPasteboard().setData(data) pasteboard.getSystemPasteboard().setData(data)
.then(() => this.log.i('clipboard image set ok'))
.catch((e: BusinessError) => this.log.w(`clipboard image failed: ${e.code} ${e.message}`)) .catch((e: BusinessError) => this.log.w(`clipboard image failed: ${e.code} ${e.message}`))
.finally(() => pm.release()); .finally(() => pm.release());
}).catch((e: BusinessError) => { }).catch((e: BusinessError) => {
this.log.w(`createPixelMap failed: ${e.code} ${e.message}`); this.log.w(`createPixelMap failed: ${e.code} ${e.message}`);
}).finally(() => { }).finally(() => {
source.release(); source.release();
fileIo.closeSync(file);
}); });
} }