如何使用canvas添加水印

使用canvas添加水印

HarmonyOS
2024-05-26 14:01:11
1.8w浏览
收藏 0
回答 1
回答 1
按赞同
/
按时间
e_lion

需要实现为image添加文字、增加马赛克、水印等功能。

需要支持如下功能点:

  • 图库图片导入
  • 添加水印/实现一个全局都浮在界面上面的一个Component文字
  • 手动绘制马赛克
  • 导出到图库

当前已实现功能:

  •  图库图片导入
  •  添加水印
  •  导出到图库

暂未实现功能点:

  •  相机拍摄导入
  •  绘制马赛克

使用的核心API

核心代码解释

功能1:从图片导入图片

导入图片主要使用picker.PhotoViewPicker()API拉起图片选择图片,相关代码如下:

// 选取图像 
export async function pickerImage(): Promise<ArrayBuffer> { 
  try { 
    let PhotoSelectOptions = new picker.PhotoSelectOptions(); 
    PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; 
    PhotoSelectOptions.maxSelectNumber = 1; 
    let photoPicker = new picker.PhotoViewPicker(); 
    let result = await photoPicker.select(PhotoSelectOptions) 
    let file = fs.openSync(result.photoUris[0]) 
    let length = fs.statSync(file.fd).size 
    let buffer = new ArrayBuffer(length) 
    fs.readSync(file.fd, buffer) 
    return buffer 
  } catch (error) { 
    let err: BusinessError = error as BusinessError; 
    console.error('PhotoViewPicker failed with err: ' + JSON.stringify(err)); 
  } 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

从图库中读取图片后使用fs的相关接口将其转化为ArrayBuffer方便后期操作,拿到该图片的buffer以后就可以在canvas中绘制该图片了,因为屏幕大小是固定的,显示图片的区域也是固定的,而图片的大小是可变的,所以在绘制图片的时候我们需要进行一些比例换算,这里只做了竖图的适配,暂未实现横图适配,核心代码如下:

// 加载图片 
onComplete(imageInfo: ArrayBuffer) { 
  let imageSource: image.ImageSource = image.createImageSource(imageInfo); 
  imageSource.getImageInfo((err, value) => { 
    if (err) { 
      return; 
    } 
    this.hValue = Math.round(value.size.height * 1); 
    this.wValue = Math.round(value.size.width * 1); 
    let defaultSize: image.Size = { 
      height: this.hValue, 
      width: this.wValue 
    }; 
 
    let opts: image.DecodingOptions = { 
      editable: true, 
      desiredSize: defaultSize 
    }; 
    imageSource.createPixelMap(opts, (err, pixelMap) => { 
      if (err) { 
      } else { 
        let rect = Utils.getComponentRect("imageContainer") 
        this.imageScale = (rect.right - rect.left) / this.wValue 
        this.imageHeight = this.hValue * this.imageScale 
        this.context.transform(this.imageScale, 0, 0, this.imageScale, 0, 0) 
        this.pixelMap = pixelMap 
        this.context.drawImage(this.pixelMap, 0, 0) 
      } 
    }) 
  }) 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.

代码解释:

主要逻辑就是在createPixelMap的回调中,通过getComponentRect("imageContainer")接口获取屏幕中实际显示图片区域的宽度,然后除以图片的实际宽度得到一个缩放比例imageScale,在绘制的时候将画笔进行矩阵变换x与y轴都缩放该比例,然后使用drawImage从画布的左上角开始绘制,这样可以得到一张等比例的图片会知道界面中

功能2:添加水印/文字

当前实现的水印就是文字水印,也可以绘制图片水印等等,这个可以根据需要自己自定义,主要实现思路就是在当前的画布中在指定的位置继续绘制文字/图片即可

Button("添加水印").onClick(async () => { 
  if (this.isDrawable) { 
    this.context.beginPath() 
    this.context.font = `宋体 ${100 / this.imageScale}px}` 
    this.context.textBaseline = "top" 
    this.context.fillStyle = "#80b2bec3" 
    this.context.rotate(Math.PI / 180 * 30) 
    this.context.fillText("水印水印水印水印", 100 / this.imageScale, 100 / this.imageScale) 
    this.context.rotate(-Math.PI / 180 * 30) 
    this.context.closePath() 
  } 
})
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

功能3:保存到本地图库

这里保存可以有两种方式,一种是保存到图库,一种是保存到文件管理,实现方式不一样

保存到图库

ps: 这里有一点说明下,当前实现的仅仅只是恢复原图的像素比例,还是会有图片被压缩导致的轻微失真现象,该问题在想办法优化中

保存到图库官网接口更改了需要使用安全控件来保存,核心代码如下

SaveButton(this.saveButtonOptions) 
  .onClick(async (event, result: SaveButtonOnClickResult) => { 
    if (result == SaveButtonOnClickResult.SUCCESS) { 
      try { 
        let context = getContext(); 
        let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context); 
        let uri = await phAccessHelper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg'); // 创建媒体文件 
        if (this.pixelMap) { 
          let imageInfo = await this.pixelMap.getImageInfo() 
          let offCanvas = new OffscreenCanvas(px2vp(imageInfo.size.width), px2vp(imageInfo.size.height)) 
          let offContext = offCanvas.getContext("2d") 
          let contextPixelMap = this.context.getPixelMap(0, 0, this.context.width, this.context.height) 
          offContext.drawImage(contextPixelMap, 0, 0, offCanvas.width, offCanvas.height) 
          saveToFile(offContext.getPixelMap(0, 0, offCanvas.width, offCanvas.height), uri); 
        } 
      } catch (err) { 
        console.error('createAsset failed, message = ', err); 
      } 
    } else { 
      console.error('SaveButtonOnClickResult createAsset failed'); 
    } 
  })
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

代码解释:

主要是使用photoAccessHelperAPI接口获取到图库的uri地址,然后因为我们在绘制图片的时候是对图片进行了缩放的,现在需要吧缩放的图片还原回原来的大小,所以我们新建了一个OffscreenCanvas,大小就是原图的大小,然后获取我们添加了水印的图片——也就是我们页面中能看到的canvas,主要是使用getPixelMap接口,将该数据绘制在OffscreenCanvas中然后通过saveToFile保存下来,saveToFile代码如下

// 保存图片 
export async function saveToFile(pixelMap: image.PixelMap, imagePath: string): Promise<void> { 
  try { 
    const filePath = imagePath; 
    const imagePacker = image.createImagePacker(); 
    const imageBuffer = await imagePacker.packing(pixelMap, { 
      format: 'image/jpeg', 
      quality: 100 
    }); 
    const mode = fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE; 
    fd = (await fs.open(filePath, mode)).fd; 
    await fs.truncate(fd); 
    await fs.write(fd, imageBuffer); 
  } catch (err) { 
  } finally { 
    if (fd) { 
      fs.close(fd); 
    } 
  } 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

完整代码

Index.ets

import { pickerImage, saveToFile, Utils } from '../utils/utils'; 
import image from '@ohos.multimedia.image'; 
import photoAccessHelper from '@ohos.file.photoAccessHelper' 
 
@Entry 
@Component 
struct Index { 
  @State isDrawable: boolean = false 
  private settings: RenderingContextSettings = new RenderingContextSettings(true) 
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings) 
  private scroller: Scroller = new Scroller() 
  imageScale: number = 1 
  @State imageHeight: number = 0 
  @State pixelMap: PixelMap | undefined = undefined 
  hValue: number = 0 
  wValue: number = 0 
  @State saveButtonOptions: SaveButtonOptions = { 
    icon: SaveIconStyle.FULL_FILLED, 
    text: SaveDescription.SAVE_IMAGE, 
    buttonType: ButtonType.Capsule 
  } // 设置安全控件按钮属性 
 
  build() { 
    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) { 
      Text("使用canvas处理图像") 
        .width("100%") 
        .fontColor(Color.White) 
        .height(45) 
        .fontSize(24) 
        .backgroundColor("#70a1ff") 
        .textAlign(TextAlign.Center) 
 
      Column() { 
        Scroll(this.scroller) { 
          Canvas(this.context) 
            .backgroundColor(Color.Yellow) 
            .onReady(() => { 
              this.isDrawable = true 
            }) 
            .id("imageContainer") 
            .width("100%") 
            .height(px2vp(this.imageHeight)) 
        } 
        .scrollable(ScrollDirection.Vertical) 
        .margin({ bottom: 8 }) 
        .border({ style: BorderStyle.Solid, width: 2, color: Color.Red }) 
        .borderRadius(10) 
        .width("100%") 
        .aspectRatio(1) 
        .scrollBar(BarState.Off) 
 
        Scroll() { 
          Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap, alignContent: FlexAlign.SpaceEvenly }) { 
            Button("选取图像").onClick(async () => { 
              let buffer = await pickerImage() 
              this.onComplete(buffer) 
            }) 
            Button("添加水印").onClick(async () => { 
              if (this.isDrawable) { 
                this.context.beginPath() 
                this.context.font = `宋体 ${100 / this.imageScale}px}` 
                this.context.textBaseline = "top" 
                this.context.fillStyle = "#80b2bec3" 
                this.context.rotate(Math.PI / 180 * 30) 
                this.context.fillText("水印水印水印水印", 100 / this.imageScale, 100 / this.imageScale) 
                this.context.rotate(-Math.PI / 180 * 30) 
                this.context.closePath() 
              } 
            }) 
            SaveButton(this.saveButtonOptions) 
              .onClick(async (event, result: SaveButtonOnClickResult) => { 
                if (result == SaveButtonOnClickResult.SUCCESS) { 
                  try { 
                    let context = getContext(); 
                    let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context); 
                    let uri = await phAccessHelper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg'); // 创建媒体文件 
                    if (this.pixelMap) { 
                      let imageInfo = await this.pixelMap.getImageInfo() 
                      let offCanvas = new OffscreenCanvas(px2vp(imageInfo.size.width), px2vp(imageInfo.size.height)) 
                      let offContext = offCanvas.getContext("2d") 
                      let contextPixelMap = this.context.getPixelMap(0, 0, this.context.width, this.context.height) 
                      offContext.drawImage(contextPixelMap, 0, 0, offCanvas.width, offCanvas.height) 
                      saveToFile(offContext.getPixelMap(0, 0, offCanvas.width, offCanvas.height), uri); 
                    } 
                  } catch (err) { 
                    console.error('createAsset failed, message = ', err); 
                  } 
                } else { 
                  console.error('SaveButtonOnClickResult createAsset failed'); 
                } 
              }) 
          } 
        }.backgroundColor(Color.White) 
        .width("100%") 
        .layoutWeight(1) 
        .borderRadius(10) 
      } 
      .padding(8) 
      .width("100%") 
      .layoutWeight(1) 
      .backgroundColor("#f1f2f6") 
    }.width("100%").height("100%") 
  } 
 
  // 加载图片 
  onComplete(imageInfo: ArrayBuffer) { 
    let imageSource: image.ImageSource = image.createImageSource(imageInfo); 
    imageSource.getImageInfo((err, value) => { 
      if (err) { 
        return; 
      } 
      this.hValue = Math.round(value.size.height * 1); 
      this.wValue = Math.round(value.size.width * 1); 
      let defaultSize: image.Size = { 
        height: this.hValue, 
        width: this.wValue 
      }; 
 
      let opts: image.DecodingOptions = { 
        editable: true, 
        desiredSize: defaultSize 
      }; 
      imageSource.createPixelMap(opts, (err, pixelMap) => { 
        if (err) { 
        } else { 
          let rect = Utils.getComponentRect("imageContainer") 
          this.imageScale = (rect.right - rect.left) / this.wValue 
          this.imageHeight = this.hValue * this.imageScale 
          this.context.transform(this.imageScale, 0, 0, this.imageScale, 0, 0) 
          this.pixelMap = pixelMap 
          this.context.drawImage(this.pixelMap, 0, 0) 
        } 
      }) 
    }) 
  } 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.

utils.ts

import image from '@ohos.multimedia.image' 
import fs from '@ohos.file.fs'; 
import picker from '@ohos.file.picker'; 
import { BusinessError } from '@ohos.base'; 
 
let fd; 
 
// 保存图片 
export async function saveToFile(pixelMap: image.PixelMap, imagePath: string): Promise<void> { 
  try { 
    const filePath = imagePath; 
    const imagePacker = image.createImagePacker(); 
    const imageBuffer = await imagePacker.packing(pixelMap, { 
      format: 'image/jpeg', 
      quality: 100 
    }); 
    const mode = fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE; 
    fd = (await fs.open(filePath, mode)).fd; 
    await fs.truncate(fd); 
    await fs.write(fd, imageBuffer); 
  } catch (err) { 
  } finally { 
    if (fd) { 
      fs.close(fd); 
    } 
  } 
} 
 
// 选取图像 
export async function pickerImage(): Promise<ArrayBuffer> { 
  try { 
    let PhotoSelectOptions = new picker.PhotoSelectOptions(); 
    PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; 
    PhotoSelectOptions.maxSelectNumber = 1; 
    let photoPicker = new picker.PhotoViewPicker(); 
    let result = await photoPicker.select(PhotoSelectOptions) 
    let file = fs.openSync(result.photoUris[0]) 
    let length = fs.statSync(file.fd).size 
    let buffer = new ArrayBuffer(length) 
    fs.readSync(file.fd, buffer) 
    return buffer 
  } catch (error) { 
    let err: BusinessError = error as BusinessError; 
    console.error('PhotoViewPicker failed with err: ' + JSON.stringify(err)); 
  } 
} 
 
export class Utils { 
  static rect_left: number 
  static rect_top: number 
  static rect_right: number 
  static rect_bottom: number 
  static rect_value: Record<string, number> 
 
  //获取组件所占矩形区域坐标 
  static getComponentRect(key: string): Record<string, number> { 
    let strJson = getInspectorByKey(key) 
    let obj: Record<string, string> = JSON.parse(strJson) 
    console.info("[getInspectorByKey] current component obj is: " + JSON.stringify(obj)) 
    let rectInfo: string[] = JSON.parse('[' + obj.$rect + ']') 
    console.info("[getInspectorByKey] rectInfo is: " + rectInfo) 
    Utils.rect_left = JSON.parse('[' + rectInfo[0] + ']')[0] 
    Utils.rect_top = JSON.parse('[' + rectInfo[0] + ']')[1] 
    Utils.rect_right = JSON.parse('[' + rectInfo[1] + ']')[0] 
    Utils.rect_bottom = JSON.parse('[' + rectInfo[1] + ']')[1] 
    return Utils.rect_value = { 
      "left": Utils.rect_left, "top": Utils.rect_top, "right": Utils.rect_right, "bottom": Utils.rect_bottom 
    } 
  } 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
分享
微博
QQ
微信
回复
2024-05-27 16:51:23


相关问题
HarmonyOS拍照后图片添加水印
336浏览 • 1回复 待解决
HarmonyOS是否支持图片添加水印
663浏览 • 1回复 待解决
HarmonyOS 如何给 app 添加水印
883浏览 • 1回复 待解决
HarmonyOS windows级别添加水印
348浏览 • 1回复 待解决
HarmonyOS 如何在app内全页面添加水印
464浏览 • 1回复 待解决
HarmonyOS PDF添加水印后展示白屏
449浏览 • 1回复 待解决
HarmonyOS 有没有对UI添加水印的方法
418浏览 • 1回复 待解决
canvas如何实现水印效果
1450浏览 • 1回复 待解决
HarmonyOS 拍照后的图片加水印
399浏览 • 1回复 待解决
HarmonyOS 使用canvas进行图片水印操作
485浏览 • 1回复 待解决
基于原生的水印添加能力
1300浏览 • 1回复 待解决
HarmonyOS 如何使用全局水印
332浏览 • 1回复 待解决
HarmonyOS 水印相机、水印视频
578浏览 • 1回复 待解决
HarmonyOS 如何使用Canvas画扇形
668浏览 • 1回复 待解决
如何使用canvas绘制圆角矩形
927浏览 • 1回复 待解决
HarmonyOS 如何使用canvas绘制虚线
315浏览 • 1回复 待解决
HarmonyOS使用canvas如何使文字垂直居中
1056浏览 • 1回复 待解决