HarmonyOS 鸿蒙Next中关于增加图片水印耗时
HarmonyOS 鸿蒙Next中关于增加图片水印耗时 项目中有个截图功能,截图后需要增加图片水印覆盖整个图片,再进行保存或者分享微信好友,图片水印是发在本地的资源文件,引用id,我目前用以下方式实现的:
public static async addWatermark(context:common.UIAbilityContext,basePixelMap: image.PixelMap,
watermarkResId: number,baseHeight:number,baseWidth:number): Promise<image.PixelMap> {
try {
const overlayPixelMap =await ImageUtil.loadResourcePixelMap(context,watermarkResId,baseHeight,baseWidth)
const offScreenCanvas = new OffscreenCanvas(baseWidth, baseHeight);
const offScreenContext = offScreenCanvas.getContext('2d');
offScreenContext.drawImage(basePixelMap, 0, 0, baseWidth, baseHeight);
offScreenContext.drawImage(overlayPixelMap, 0, 0, baseWidth, baseHeight);
let outPixelMap = offScreenContext.getPixelMap(0, 0, baseWidth, baseHeight);
// 生成新PixelMap
return outPixelMap
} catch (err) {
LogUtil.e('图片处理 增加水印 addWatermark failed:');
return Promise.reject(err); // 明确返回拒绝状态的Promise
}
}
这个过程耗时需要4s,这正常吗,有没有更优的方案做图片水印,而且会卡顿,卡顿问题我倒是解决了,就是把这个操作放到Worker中处理,就是这个耗时4s也太久了,我中间还考虑用布局中加载水印,再一起截图,但是发现截图出来的图片内容文字有锯齿,就是我没加水印,截图出来时正常的,没用锯齿,很奇怪。所以两个问题,有没有解决方法。
1、上面的方法生成图片水印耗时的,有没有更好的处理方式,两次drawImage这里就差不多两秒了
2、布局加图片水印后一起截图,内容怎么会出现锯齿,如何解决
更多关于HarmonyOS 鸿蒙Next中关于增加图片水印耗时的实战教程也可以访问 https://www.itying.com/category-93-b0.html
1.楼主这个处理过程应该存在异常,我通过官方示例改造了一个添加图片水印的方法处理,直接是同步的
【实现效果】
【参考源码】
import { unifiedDataChannel, uniformDataStruct, uniformTypeDescriptor } from '@kit.ArkData';
import { drawing } from '@kit.ArkGraphics2D';
import { display, TextModifier, SubHeader, LengthUnit } from '@kit.ArkUI';
import { BusinessError, systemDateTime } from '@kit.BasicServicesKit';
import { fileIo as fs, fileUri } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';
import { resourceManager } from '@kit.LocalizationKit';
import { Constants } from '../../common/Constants';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BreakpointType } from '../../common/Utils';
const TAG = '[Watermark]';
@Builder
export function PageWatermarkBuilder() {
WatermarkDrag();
}
@Entry
@Component
struct WatermarkDrag {
@Consume('NavPathStack') pageStack: NavPathStack;
@State targetImage: string = '';
@State primaryModifier: TextModifier =
new TextModifier().fontColor(Color.Gray).fontSize(18).fontWeight(FontWeight.Medium);
@StorageProp('topRectHeight') topRectHeight: number = 0;
@StorageLink('currentBreakpoint') curBp: string = Constants.BREAK_POINT_SM;
context?: Context = this.getUIContext().getHostContext();
time: string = '0';
getTimeWatermark(str: number): string {
let time: string = '';
let date: Date = new Date(str);
try {
let year: number = date.getFullYear();
let month: string | number = (date.getMonth() + 1) < 10 ? '0' + (date.getMonth() + 1) : (date.getMonth() + 1);
let day: string | number = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
let hour: string | number = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
let min: string | number = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
let second: string | number = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
time = year + '-' + month + '-' + day + ' ' + hour + ':' + min + ':' + second;
hilog.info(0x0000, TAG, `%{public}s`, `getTimeWatermark`);
} catch (error) {
hilog.error(0x0000, TAG,
`Failed to get currentTime, code = ${(error as BusinessError).code}, message = ${(error as BusinessError).message}`);
}
return time;
}
getDataFromUdmfRetry(event: DragEvent, callback: (data: DragEvent) => void) {
try {
let data: UnifiedData = event.getData();
if (!data) {
return false;
}
let records: unifiedDataChannel.UnifiedRecord[] = data.getRecords();
if (!records || records.length <= 0) {
return false;
}
callback(event);
return true;
} catch (error) {
hilog.error(0x0000, TAG,
`getData failed, code = ${(error as BusinessError).code}, message = ${(error as BusinessError).message}`);
return false;
}
}
getDataFromUdmf(event: DragEvent, callback: (data: DragEvent) => void) {
if (this.getDataFromUdmfRetry(event, callback)) {
return;
}
setTimeout(() => {
this.getDataFromUdmfRetry(event, callback);
}, 1500);
}
// [Start waterMarkImageAddWaterMarkFunction]
addWaterMark(watermark: string, pixelMap: image.PixelMap) {
try {
if (!canIUse('SystemCapability.Graphics.Drawing')) {
hilog.error(0x0000, TAG, `%{public}s`, `watermark is not supported`);
return pixelMap;
}
watermark = this.context!.resourceManager.getStringSync($r('app.string.drag_time').id) + watermark;
const imageInfo: image.Size = pixelMap.getImageInfoSync().size;
const imageWidth: number = imageInfo.width;
const imageHeight: number = imageInfo.height;
const imageScale: number = imageWidth / display.getDefaultDisplaySync().width;
const canvas: drawing.Canvas = new drawing.Canvas(pixelMap);
const pen: drawing.Pen = new drawing.Pen();
const brush: drawing.Brush = new drawing.Brush();
pen.setColor({
alpha: 102,
red: 255,
green: 255,
blue: 255
});
brush.setColor({
alpha: 102,
red: 255,
green: 255,
blue: 255
});
const font: drawing.Font = new drawing.Font();
font.setSize(48 * imageScale);
let textWidth: number = font.measureText(watermark, drawing.TextEncoding.TEXT_ENCODING_UTF8);
const textBlob: drawing.TextBlob =
drawing.TextBlob.makeFromString(watermark, font, drawing.TextEncoding.TEXT_ENCODING_UTF8);
canvas.attachBrush(brush);
canvas.attachPen(pen);
// canvas.drawTextBlob(textBlob, imageWidth - 24 * imageScale - textWidth, imageHeight - 32 * imageScale);
const aa:image.PixelMap = this.getImagePixelMap($r('app.media.app_icon'))
canvas.drawImage(aa,15,15)
canvas.detachBrush();
canvas.detachPen();
} catch (error) {
hilog.error(0x0000, TAG, '%{public}s', 'addWaterMark failed:', (error as BusinessError).message);
}
return pixelMap;
}
getImagePixelMap(resource: Resource): image.PixelMap {
const data: Uint8Array = this.getUIContext().getHostContext()?.resourceManager.getMediaContentSync(resource.id) as Uint8Array;
const arrayBuffer: ArrayBuffer = data.buffer.slice(data.byteOffset, data.byteLength + data.byteOffset);
const imageSource: image.ImageSource = image.createImageSource(arrayBuffer);
return this.imageSource2PixelMap(imageSource);
}
imageSource2PixelMap(imageSource: image.ImageSource): image.PixelMap {
const imageInfo: image.ImageInfo = imageSource.getImageInfoSync();
const height = imageInfo.size.height;
const width = imageInfo.size.width;
const options: image.DecodingOptions = {
editable: true,
desiredSize: { height, width }
};
const pixelMap: PixelMap = imageSource.createPixelMapSync(options);
// const result: image.PixelMap = { , width, height };
return pixelMap;
}
// [End waterMarkImageAddWaterMarkFunction]
build() {
NavDestination() {
Scroll() {
Column() {
Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceAround }) {
// [Start waterMarkImageToDrag]
// [Start waterMarkImageOnDragStart]
// [Start waterMarkImageAddWaterMark]
// [Start waterMarkImagePack]
Image($rawfile('river.png'))
// [StartExclude waterMarkImageToDrag]
// [StartExclude waterMarkImageOnDragStart]
// [StartExclude waterMarkImageAddWaterMark]
// [StartExclude waterMarkImagePack]
.width('100%')
.height(new BreakpointType('180vp', '224vp', '391vp').getValue(this.curBp))
.fitOriginalSize(true)
.borderRadius(16)
// [EndExclude waterMarkImageToDrag]
.draggable(true)
// [End waterMarkImageToDrag]
// [EndExclude waterMarkImageOnDragStart]
// [EndExclude waterMarkImageAddWaterMark]
// [EndExclude waterMarkImagePack]
.onDragStart((event: DragEvent) => {
// [StartExclude waterMarkImagePack]
// [StartExclude waterMarkImageAddWaterMark]
const resourceMgr: resourceManager.ResourceManager = this.context!.resourceManager;
let rawFileDescriptor = resourceMgr.getRawFdSync('river.png');
const imageSourceApi: image.ImageSource = image.createImageSource(rawFileDescriptor);
let pixelMap: image.PixelMap = imageSourceApi.createPixelMapSync();
// [StartExclude waterMarkImageOnDragStart]
imageSourceApi?.release();
// [EndExclude waterMarkImageAddWaterMark]
this.time = this.getTimeWatermark(systemDateTime.getTime(false));
let markPixelMap: image.PixelMap = this.addWaterMark(this.time, pixelMap);
// [StartExclude waterMarkImageAddWaterMark]
// [EndExclude waterMarkImagePack]
let packOpts: image.PackingOption = { format: 'image/png', quality: 20 };
let file: fs.File =
fs.openSync(`${this.context!.filesDir}/watermark.png`, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
const imagePackerApi: image.ImagePacker = image.createImagePacker();
imagePackerApi.packToFile(markPixelMap, file.fd, packOpts);
imagePackerApi?.release();
let imgData: uniformDataStruct.FileUri = {
uniformDataType: 'general.file-uri',
oriUri: fileUri.getUriFromPath(`${this.context!.filesDir}/watermark.png`),
fileType: 'general.image'
}
let unifiedRecord =
new unifiedDataChannel.UnifiedRecord(uniformTypeDescriptor.UniformDataType.FILE_URI, imgData);
let unifiedData = new unifiedDataChannel.UnifiedData(unifiedRecord);
event.setData(unifiedData);
fs.closeSync(file.fd);
// [StartExclude waterMarkImagePack]
pixelMap.release();
markPixelMap.release();
// [EndExclude waterMarkImageAddWaterMark]
// [EndExclude waterMarkImageOnDragStart]
// [EndExclude waterMarkImagePack]
})
// [End waterMarkImageOnDragStart]
// [End waterMarkImageAddWaterMark]
// [End waterMarkImagePack]
.onDragEnd((event) => {
if (event.getResult() === DragResult.DRAG_SUCCESSFUL) {
this.getUIContext().getPromptAction().showToast({
duration: 100,
bottom: '80vp',
message: $r('app.string.drag_successfully')
});
} else if (event.getResult() === DragResult.DRAG_FAILED) {
this.getUIContext().getPromptAction().showToast({ duration: 100, bottom: '80vp', message: $r('app.string.drag_failed') });
}
})
}
SubHeader({
primaryTitle: $r('app.string.area_can_drag'),
primaryTitleModifier: this.primaryModifier,
contentMargin: {
start: {
value: 0,
unit: LengthUnit.VP
}
}
})
Column() {
Image(this.targetImage)
.constraintSize({ maxWidth: '100%' })
.width('100%')
.height(new BreakpointType('180vp', '224vp', '391vp').getValue(this.curBp))
.fitOriginalSize(true)
.borderRadius(16)
.draggable(true)
}
.alignItems(HorizontalAlign.Center)
.allowDrop([uniformTypeDescriptor.UniformDataType.IMAGE])
.onDrop((dragEvent?: DragEvent) => {
try {
this.getDataFromUdmf((dragEvent as DragEvent), (event: DragEvent) => {
let records: unifiedDataChannel.UnifiedRecord[] = event.getData().getRecords();
for (let i = 0; i < records.length; i++) {
let types = records[i].getTypes();
if (types.includes(uniformTypeDescriptor.UniformDataType.FILE_URI)) {
const fileUriUds =
records[i].getEntry(uniformTypeDescriptor.UniformDataType.FILE_URI) as uniformDataStruct.FileUri;
let typeDescriptor = uniformTypeDescriptor.getTypeDescriptor(fileUriUds.fileType);
if (typeDescriptor.belongsTo(uniformTypeDescriptor.UniformDataType.IMAGE)) {
this.targetImage = fileUriUds.oriUri;
}
}
}
event.useCustomDropAnimation = false;
event.setResult(DragResult.DRAG_SUCCESSFUL);
})
} catch (error) {
const err = error as BusinessError;
hilog.error(0x0000, TAG, `startDataLoading errorCode: ${err.code}, errorMessage: ${err.message}`);
}
})
.backgroundColor($r('sys.color.comp_background_primary'))
.constraintSize({ maxWidth: '100%' })
.borderRadius(16)
}
.padding({
left: '16vp',
right: '16vp',
top: '8vp',
bottom: '16vp'
})
}
}
.title(this.context!.resourceManager.getStringSync($r('app.string.watermark_title').id))
.backgroundColor($r('sys.color.background_secondary'))
.padding({ top: this.getUIContext().px2vp(this.topRectHeight) })
}
}
【参考文档】
添加、删除水印-pdfService能力-PDF Kit(PDF服务)-应用服务 - 华为HarmonyOS开发者
【参考代码仓库】
DragFramework: 本示例设置组件响应拖拽事件,实现图片、富文本、文本、输入框、列表等组件的拖拽功能。 - Gitee.com
更多关于HarmonyOS 鸿蒙Next中关于增加图片水印耗时的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
谢谢,我看下,
用这种实现确实没问题,不耗时,
找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17
- drawImage 是 Canvas 的同步操作,大图 + 高分辨率 会导致主线程阻塞。
- 如果你用的是 原图尺寸 绘制,4K 图 + 两次 drawImage 确实可能耗时 2s+。
- 浏览器在 Worker 中不支持 DOM 和 Canvas 2D,但你可以用 OffscreenCanvas + WebGL 或 ImageBitmap 来加速。
// 使用Overlay组件实现实时水印
@Component
struct WatermarkOverlay {
build() {
Overlay()
.width('100%')
.height('100%')
.opacity(0.5)
.backgroundImage($r('app.media.watermark'))
.backgroundImageSize(ImageSize.Cover)
}
}
试试这种方法
找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17
多谢,我之前时直接贴在布局,没用这种方式,我也尝试下,
在HarmonyOS Next中,增加图片水印的耗时主要受图片分辨率、水印复杂度和系统资源影响。系统通过图形引擎处理图片数据,使用GPU加速渲染水印元素。优化方法包括:降低输出图片质量、使用预渲染水印模板、采用异步处理避免阻塞UI线程。具体耗时需通过DevEco Studio的性能分析工具实际测量。
针对你提出的两个问题,我来分析并提供解决方案:
1. 图片水印耗时优化方案
当前使用OffscreenCanvas进行两次drawImage操作确实会产生较大开销。建议采用以下优化方案:
- 使用PixelMap叠加:直接通过PixelMap的createPixelMap方法创建新的PixelMap,利用图像处理API进行叠加,避免Canvas渲染开销
- 预加载水印资源:将水印PixelMap提前加载并缓存,避免每次都需要从资源文件解析
- 降低处理精度:对于非高清要求的场景,可适当降低输出图片质量
优化后的代码框架:
const imageSource = image.createImageSource(overlayPixelMap);
const editing = imageSource.createPixelMap();
// 使用图像编辑API直接叠加
2. 布局截图锯齿问题解决方案
截图出现锯齿通常是由于渲染缩放导致的,可通过以下方式解决:
- 设置抗锯齿:在布局渲染时明确设置抗锯齿参数
- 提高截图分辨率:使用更高分辨率的截图参数,确保文字渲染质量
- 检查缩放比例:确认布局渲染时的设备像素比是否正确
建议在截图前确保:
- 布局已完成完全渲染
- 使用与屏幕密度匹配的截图尺寸
- 开启图形渲染的硬件加速
这些优化应该能显著减少处理时间并解决锯齿问题。