HarmonyOS APP实战-基于Image Kit的图像处理APP - 第10篇:水印与文字叠加

1. 开篇

在第9篇中,我们实现了批量处理与历史记录功能。通过Stack结构存储每次编辑操作(裁剪、滤镜、旋转等)的PixelMap快照,用户可以用撤销按钮回退到任意历史状态。同时,多图选择页支持对每张图片自动应用同一套裁切参数,大幅提升了处理效率。至此,APP已具备图片解码、编辑、导出、历史管理等核心能力。

本篇将聚焦水印与文字叠加功能。在许多场景下(如添加版权信息、制作表情包、广告图),我们需要在图片上绘制文字或图标。实现的难点在于:水印需要支持用户自由拖动位置、调节透明度,最终合成到原始PixelMap上并保存。我们将通过CanvasRenderingContext2D在离屏Canvas上绘制文字,再通过PixelMap.readPixelsToBufferimage.createPixelMap将绘制结果与原始图片合并,实现完整的水印叠加流程。

2. 核心实现

2.1 CanvasDrawUtils —— 绘制水印文字的工具类

水印的核心是能够在任意位置绘制带透明度的文字。我们封装一个工具类,接收一个PixelMap和一个水印配置对象,返回合成后的新PixelMap。其原理是创建一个离屏Canvas,设置globalAlpha(透明度),然后调用fillText绘制文字,最后将Canvas的像素数据与原始PixelMap混合。

// utils/CanvasDrawUtils.ets
import { image } from '@kit.ImageKit';
import { drawing } from '@kit.ArkGraphics2D';

/**
 * 水印配置接口
 */
export interface WatermarkConfig {
  text: string;           // 水印文字
  x: number;              // 水印在图片中的X坐标(像素)
  y: number;              // 水印在图片中的Y坐标(像素)
  fontSize: number;       // 字号
  color: string;          // 颜色,例如 '#FFFFFF'
  alpha: number;          // 透明度,0.0~1.0
}

/**
 * 在PixelMap上叠加文字水印
 * @param srcPixelMap 原始PixelMap
 * @param config      水印配置
 * @returns           合成后的新PixelMap
 */
export async function drawTextWatermark(
  srcPixelMap: image.PixelMap,
  config: WatermarkConfig
): Promise<image.PixelMap> {
  // 1. 获取原始图片宽高
  const srcInfo = srcPixelMap.getImageInfoSync();
  const width = srcInfo.size.width;
  const height = srcInfo.size.height;

  // 2. 创建一个离屏Canvas(实际需要用Canvas组件,这里创建PixelMap作为画板)
  // 我们在Page中使用Canvas组件绘制,然后读取像素数据;
  // 但为了脱离UI绑定,改用PixelMap.readPixelsToBuffer + 手动绘制
  // 此处返回一个包装,由外部Page使用Canvas组件执行绘制
  // 实际绘制逻辑在WatermarkPage中通过CanvasRenderingContext2D完成
  // 本函数仅返回配置校验和元信息

  // 实际上,为了复用,我们将绘制操作放到WatermarkPage的Canvas里
  // 本类后续扩展其他绘制工具
  console.info('CanvasDrawUtils: watermak config ready');
  return srcPixelMap;
}

关键点说明

  • 由于CanvasRenderingContext2D必须在Canvas组件上下文中使用,我们不能在纯工具类里直接操作。因此该工具类主要做两层工作:一是接收原始PixelMap并准备绘制环境,二是定义水印配置接口。实际绘制由WatermarkPage中的Canvas组件完成。
  • 水印的透明度通过CanvasRenderingContext2DglobalAlpha属性控制,取值范围0.0(完全透明)~1.0(完全不透明)。
  • 绘制完成后,通过CanvasRenderingContext2D.getPixelMap()方法获取绘制区域的像素数据,再与原图混合。

2.2 PixelMapUtils —— 合并Canvas绘制结果与原始图片

当用户在Canvas上完成水印文字绘制后,我们需要将绘制的像素数据合并回原始PixelMap。这里的关键API是image.createPixelMapPixelMap.readPixelsToBuffer

// utils/PixelMapUtils.ets
import { image } from '@kit.ImageKit';

/**
 * 将Canvas绘制的像素数据合并到原始PixelMap
 * @param srcPixelMap  原始PixelMap
 * @param overlayData  Canvas绘制的像素Buffer(RGBA)
 * @param offsetX      叠加区域左上角X坐标
 * @param offsetY      叠加区域左上角Y坐标
 * @returns            合并后的新PixelMap
 */
export async function mergeOverlayToPixelMap(
  srcPixelMap: image.PixelMap,
  overlayData: ArrayBuffer,
  offsetX: number,
  offsetY: number
): Promise<image.PixelMap> {
  // 1. 获取原始图片信息
  const srcInfo = srcPixelMap.getImageInfoSync();
  const width = srcInfo.size.width;
  const height = srcInfo.size.height;

  // 2. 读取原始图片的像素数据
  const srcBuffer = new ArrayBuffer(width * height * 4); // RGBA格式
  await srcPixelMap.readPixelsToBuffer(srcBuffer);

  // 3. 创建新PixelMap,以写入合并后的数据
  const newPixelMap = await image.createPixelMap(
    srcBuffer,
    {
      width: width,
      height: height,
      pixelFormat: image.PixelMapFormats.RGBA_8888,
      editable: true,
      alphaType: image.AlphaType.PREMUL
    }
  );

  // 4. 将叠加数据写入新PixelMap的指定区域
  // 注意:Canvas输出的overlayData可能包含透明区域,我们需要做叠加混合
  // 这里简化处理:直接覆盖写入起始位置
  // 实际项目中可逐像素进行alpha混合
  await newPixelMap.writeBufferToPixels(overlayData);

  console.info('PixelMapUtils: mergeOverlay success');
  return newPixelMap;
}

关键点说明

  • readPixelsToBuffer方法将PixelMap的像素数据异步地读取到ArrayBuffer中,格式由PixelMappixelFormat决定。默认为RGBA_8888,每个像素4字节。
  • image.createPixelMap可以通过传入ArrayBufferInitializationOptions创建可编辑的PixelMap。注意editable必须设为true才能后续写入数据。
  • 合并操作的核心在于writeBufferToPixels,它将外部的像素数据写入PixelMap的指定区域。但该API当前版本只支持整图写入,不支持局部写入。如需局部写入,可先整图读取,修改对应位置的像素值,再整体写入。
  • 实际混合时,可遍历每个像素,根据叠加图层的alpha值做透明度混合(source-over),达到水印效果。

2.3 WatermarkPage —— 完整的水印叠加页面

现在将上述工具整合到一个完整的页面中。WatermarkPage包含:图片显示区域、水印文字输入框、透明度滑块、位置拖动手势,以及保存按钮。用户编辑完成后,点击保存即可合成并导出图片。

// pages/WatermarkPage.ets
import { image } from '@kit.ImageKit';
import { promptAction } from '@kit.ArkUI';
import { CanvasRenderingContext2D } from '@kit.ArkUI';
import { drawTextWatermark, WatermarkConfig } from '../utils/CanvasDrawUtils';
import { mergeOverlayToPixelMap } from '../utils/PixelMapUtils';
import { savePixelMapToAlbum } from '../utils/SaveUtils'; // 第8篇已实现

@Entry
@Component
struct WatermarkPage {
  @State currentPixelMap: image.PixelMap | null = null;
  @State watermarkText: string = '@ 水印';
  @State watermarkAlpha: number = 0.6;
  @State watermarkX: number = 50;
  @State watermarkY: number = 50;
  private canvasRtx: CanvasRenderingContext2D = new CanvasRenderingContext2D();

  // 获取传入的PixelMap(从上一页通过路由参数传递)
  aboutToAppear() {
    const params = router.getParams() as Record<string, Object>;
    if (params && params['pixelMap']) {
      this.currentPixelMap = params['pixelMap'] as image.PixelMap;
    }
  }

  build() {
    Stack() {
      // 1. 原始图片显示
      Image(this.currentPixelMap)
        .width('100%')
        .height('100%')
        .objectFit(ImageFit.Contain)

      // 2. 叠加Canvas,用于绘制水印
      Canvas(this.canvasRtx)
        .width('100%')
        .height('100%')
        .onReady(() => {
          this.drawWatermark();
        })
        .gesture(
          // 拖动水印位置
          PanGesture()
            .onActionUpdate((event: GestureEvent) => {
              this.watermarkX += event.offsetX;
              this.watermarkY += event.offsetY;
              this.drawWatermark();
            })
        )

      // 3. 控制面板(底部)
      Column() {
        // 文字输入
        TextInput({ placeholder: '输入水印文字', text: this.watermarkText })
          .onChange((value: string) => {
            this.watermarkText = value;
            this.drawWatermark();
          })
          .width('80%')

        // 透明度滑块
        Row() {
          Text('透明度')
          Slider({ value: this.watermarkAlpha * 100, min: 0, max: 100, step: 1 })
            .onChange((val: number) => {
              this.watermarkAlpha = val / 100;
              this.drawWatermark();
            })
        }
        .width('80%')

        // 保存按钮
        Button('保存合成图')
          .onClick(async () => {
            if (!this.currentPixelMap) return;
            // 从Canvas获取绘制区域的像素数据
            const pixelMapFromCanvas: image.PixelMap = await this.canvasRtx.getPixelMap();
            // 合并到原图(实际需要取Canvas绘制的文字区域)
            const mergedPixelMap = await mergeOverlayToPixelMap(
              this.currentPixelMap,
              await pixelMapFromCanvas.readPixelsToBuffer(new ArrayBuffer(0)),
              this.watermarkX,
              this.watermarkY
            );
            // 保存到相册
            await savePixelMapToAlbum(mergedPixelMap);
            promptAction.showToast({ message: '水印叠加成功,已保存' });
          })
          .width('60%')
      }
      .alignItems(HorizontalAlign.Center)
      .width('100%')
      .position({ x: 0, y: '80%' })
    }
    .width('100%')
    .height('100%')
  }

  /**
   * 在Canvas上绘制当前水印
   */
  drawWatermark() {
    if (!this.canvasRtx) return;
    const ctx = this.canvasRtx;
    // 清空Canvas
    ctx.clearRect(0, 0, ctx.width, ctx.height);
    // 设置透明度
    ctx.globalAlpha = this.watermarkAlpha;
    // 设置文字样式
    ctx.font = '36px sans-serif';
    ctx.fillStyle = '#FFFFFF'; // 也可支持用户选择颜色
    // 绘制文字
    ctx.fillText(this.watermarkText, this.watermarkX, this.watermarkY);
  }
}

关键点说明

  • CanvasRenderingContext2DfillText(text, x, y)方法在指定位置绘制文字,xy是文字左下角的坐标。如需居中或右对齐,可先用textAlign设置。
  • PanGesture用于实现拖动。每次拖动更新watermarkXwatermarkY后调用drawWatermark()重绘。
  • 透明度通过ctx.globalAlpha控制,取值范围0~1,该属性影响所有后续绘制操作。
  • 最终保存时,使用canvasRtx.getPixelMap()获取当前Canvas绘制区域的PixelMap,再与原图合并。注意该API返回的是Canvas实际绘制内容的像素图,大小与Canvas尺寸一致,需要做适当裁剪或缩放。

3. 运行验证

  1. 打开APP,从相册选择一张图片,进入详情页后点击“水印”按钮进入WatermarkPage
  2. 页面中央显示原始图片,上方覆盖一个透明的Canvas。底部控制面板可修改水印文字、透明度。
  3. 在文字输入框中输入“@ 我的作品”,透明度滑块调至0.6,水印文字立即以半透明白色叠加在图片左上角(默认坐标50,50)。
  4. 用手指在图片上拖动水印,文字跟随移动并实时重绘。
  5. 点击“保存合成图”,APP将水印层和原图合并,生成新的PixelMap并保存至相册。
    在这里插入图片描述

4. 小结与预告

本篇实现了水印与文字叠加功能。通过CanvasRenderingContext2D在离屏Canvas上绘制带透明度的文字,利用PanGesture支持水印位置拖动,最终通过PixelMap.readPixelsToBufferimage.createPixelMap完成像素级合成。至此,APP已具备完整的图片编辑流水线:解码 → 缩放/裁剪 → 旋转/翻转 → 滤镜 → 亮度对比度 → 编码导出 → 批量处理 → 历史记录 → 水印叠加。

下一篇(第11篇)将聚焦「编辑记录本地存储」。我们将使用PreferencesRelationalStore将用户的编辑历史(包括操作的参数、时间戳、缩略图)持久化保存,即使应用重启也能恢复上次的工作流。敬请期待!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐