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

10 回复

1.楼主这个处理过程应该存在异常,我通过官方示例改造了一个添加图片水印的方法处理,直接是同步的

【实现效果】

cke_2255.png

【参考源码】

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) })
  }
}

【参考文档】

水印添加-图形绘制-图形 - 华为HarmonyOS开发者

添加、删除水印-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. 布局截图锯齿问题解决方案

截图出现锯齿通常是由于渲染缩放导致的,可通过以下方式解决:

  • 设置抗锯齿:在布局渲染时明确设置抗锯齿参数
  • 提高截图分辨率:使用更高分辨率的截图参数,确保文字渲染质量
  • 检查缩放比例:确认布局渲染时的设备像素比是否正确

建议在截图前确保:

  • 布局已完成完全渲染
  • 使用与屏幕密度匹配的截图尺寸
  • 开启图形渲染的硬件加速

这些优化应该能显著减少处理时间并解决锯齿问题。

回到顶部