如何使用canvas添加水印

使用canvas添加水印

HarmonyOS
2024-05-26 14:01:11
浏览
收藏 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)); 
  } 
}

从图库中读取图片后使用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) 
      } 
    }) 
  }) 
}

代码解释:

主要逻辑就是在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() 
  } 
})

功能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'); 
    } 
  })

代码解释:

主要是使用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); 
    } 
  } 
}

完整代码

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

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 
    } 
  } 
}
分享
微博
QQ
微信
回复
2024-05-27 16:51:23
相关问题
HarmonyOS是否支持图片添加水印
209浏览 • 1回复 待解决
HarmonyOS 如何给 app 添加水印
212浏览 • 1回复 待解决
canvas如何实现水印效果
848浏览 • 1回复 待解决
基于原生的水印添加能力
643浏览 • 1回复 待解决
如何使用canvas绘制圆角矩形
380浏览 • 1回复 待解决
HarmonyOS 绘制水印如何实现?
131浏览 • 1回复 待解决
HarmonyOS使用canvas如何使文字垂直居中
454浏览 • 1回复 待解决
HarmonyOS svg、canvas使用详情
332浏览 • 1回复 待解决
HarmonyOS 背景水印问题
235浏览 • 1回复 待解决
图片处理(加动态水印
232浏览 • 1回复 待解决
如何操作canvas重新绘制
929浏览 • 1回复 待解决
HarmonyOS Canvas如何重置clip
158浏览 • 1回复 待解决
Canvas绘制内容如何动态更新
1290浏览 • 1回复 待解决
Canvas如何触发刷新重复绘制?
788浏览 • 1回复 待解决
如何添加内容的添加渐变模糊
233浏览 • 1回复 待解决