鸿蒙案例实战——添加水印

本示例为开发者展示常用的水印添加能力,包括两种方式给页面添加水印、保存图片添加水印、拍照图片添加水印和pdf文件添加水印。

案例效果截图

首页

页面水印

图片水印

pdf水印

案例运用到的知识点

  1. 核心知识点
  • 页面添加水印:封装Canvas绘制水印组件,使用Stack层叠布局或overlay浮层属性,将水印组件与页面融合。
  • 保存图片添加水印:获取图片数据,createPixelMap,使用OffScreenContext在指定位置绘制水印,最后保存带水印图片。
  • 拍照图片添加水印:打开相机,获取存储fileUri,然后存入沙箱,获取图片数据,createPixelMap,绘制水印,最后保存带水印图片。
  • pdf文件添加水印: 使用PdfView预览组件预览pdf,使用pdfService服务加载pdf、添加水印、保存pdf。
  1. 其他知识点
  • ArkTS 语言基础
  • V2版状态管理:@ComponentV2/@Local/@Param
  • 自定义组件和组件生命周期
  • 内置组件:Stack/Scroll/Flex/Column/Row/Text/Image/Button
  • 提示框:promptAction
  • 图形绘制:Canvas
  • UI范式渲染控制:ForEach/if
  • 日志管理类的编写
  • 常量与资源分类的访问
  • MVVM模式

代码结构

├──entry/src/main/ets/
│  ├──component
│  │  ├──NavBar.ets                     // 顶部导航条
│  │  └──Watermark.ets                  // 页面水印组件
│  ├──constants
│  │  ├──Utils.ets                      // 工具类
│  │  └──Constants.ets                  // 公共常量类
│  ├──entryability
│  │  └──EntryAbility.ets               // 程序入口类
│  └──pages            
│     ├──CameraPage.ets                 // 拍照添加水印
│     ├──Index.ets                      // 首页
│     ├──SaveImagePage.ets              // 保存图片添加水印
│     ├──WatermarkPdfPage.ets           // pdf文件添加水印
│     ├──WatermarkStackPage.ets         // 使用Stack添加页面背景水印
│     └──WatermarkOverlay.ets           // 使用overlay添加页面背景水印
└──entry/src/main/resources             // 应用静态资源目录

公共文件与资源

本案例涉及到的常量类和工具类代码如下:

  1. 通用常量类
// entry/src/main/ets/constants/Constants.ets
export class Constants {static readonly INDEX_CONTENT_WIDTH = '91.1%'static readonly DIVIDER_HEIGHT = 0.5static readonly DIVIDER_WIDTH = '93%'static readonly DIVIDER_DRAWER_WIDTH = '90%'static readonly CARD_TITLE_HEIGHT = 20static readonly CARD_TEXT_HEIGHT = 48static readonly INDEX_TITLE_HEIGHT = 112static readonly FULL_WIDTH = '100%'static readonly FULL_HEIGHT = '100%'static readonly FONT_SIZE_UNCHECKED = 18static readonly FONT_SIZE_CHECKED = 24static readonly CONTENT_HEIGHT = 300static readonly LIST_HEIGHT = 48static readonly LIST_CARD_WIDTH = 272static readonly LIST_CARD_HEIGHT = 344static readonly LIST_CONTENT_HEIGHT = '110%'static readonly BACKGROUND_TAB_HEIGHT = 40static readonly INDEX_BUTTON_HEIGHT = 60static readonly BACKGROUND_TAB_WIDTH = 96static readonly DRAWER_WIDTH = 264static readonly SUB_TAB_WIDTH = 56static readonly SUB_SLIDE_TAB_WIDTH = 56static readonly SUB_TAB_BOT_HEIGHT = 25static readonly SUB_TAB_HEIGHT = 30static readonly SUB_LIST_WIDTH = '85%'static readonly SIDE_TAB_WIDTH = '27.8%'static readonly SIDE_CONTEND_WIDTH = '72.2%'static readonly TAB_INDEX_ZERO = 0static readonly TAB_INDEX_ONE = 1static readonly TAB_INDEX_TWO = 2static readonly TAB_INDEX_THREE = 3static readonly TAB_INDEX_FOUR = 4static readonly TAB_INDEX_FIVE = 5static readonly IMAGE_SIZE_TAB = 22static readonly IMAGE_SIZE_MIDDLE = 56static readonly MORE_IMAGE_WIDTH = 20static readonly MORE_IMAGE_HEIGHT = 15static readonly DRAWER_IMAGE_HEIGHT_WIDTH = 24static readonly LIST_IMAGE_HEIGHT_WIDTH = 40static readonly IMAGE_OFFSET = -15static readonly ICON_Offset = -3static readonly ANIMATION_DURATION = 300static readonly MARGIN_SIXTEEN = 16static readonly MARGIN_BUTTON_TOP = 48static readonly TRANSLATE_TOP = -40static readonly TRANSLATE_BOTTOM = 40static readonly BORDER_RADIUS_DRAWER = 16static readonly BORDER_RADIUS_INDEX_LIST = 18static readonly BORDER_RADIUS_DRAWER_CONTENT = 20static readonly BORDER_RADIUS_BACK_TAB = 21static readonly STROKE_WIDTH = 2static readonly SLICE_INDEX_ZERO = 0static readonly SLICE_INDEX_SIX = 6static readonly FONT_WEIGHT_TAB = 600static readonly STRING_WATERMARK_TEXT = '水印水印水印'static readonly FILL_STYLE_WATERMARK = '#10000000'static readonly FONT_WATERMARK = '16vp'static readonly TEXT_ALIGN_WATERMARK = 'center'static readonly BASELINE_WATERMARK = 'middle'static readonly TOAST_DURATION = 1500static readonly TOP_TAB_DATA = ['文学', '交友', '直播', '视频', '盛典', '潮玩']static readonly ROUTES: Route[] = [{title: $r('app.string.page_bg'),child: [{ text: $r('app.string.use_stack'), to: 'WatermarkStackPage' },{ text: $r('app.string.use_overlay'), to: 'WatermarkOverlayPage' }]},{title: $r('app.string.photo'),child: [{ text: $r('app.string.save_photo'), to: 'SaveImagePage' },{ text: $r('app.string.take_camera'), to: 'CameraPage' }]},{title: $r('app.string.file'),child: [{ text: $r('app.string.pdf_watermark'), to: 'WatermarkPdfPage' }]}]
}export interface Route {title: ResourceStrchild: Array<ChildRoute>
}export interface ChildRoute {text: ResourceStrto: string
}

本案例涉及到的资源文件如下:

  1. string.json
// entry/src/main/resources/base/element/string.json
{"string": [{"name": "module_desc","value": "module description"},{"name": "EntryAbility_desc","value": "description"},{"name": "EntryAbility_label","value": "Watermark"},{"name": "title","value": "水印添加能力"},{"name": "open_camera","value": "打开相机"},{"name": "image_reason","value": "生成水印照片需要写入图片权限"},{"name": "message_save_success","value": "图片保存成功"},{"name": "pdf_save_success","value": "pdf下载成功"},{"name": "watermark_text","value": "水印文字"},{"name": "watermark_screen_text","value": "水印水印水印"},{"name": "page_bg","value": "页面背景"},{"name": "photo","value": "图片"},{"name": "camera","value": "拍照"},{"name": "file","value": "文件"},{"name": "use_stack","value": "使用Stack组件添加添加水印"},{"name": "use_overlay","value": "使用overlay属性添加水印"},{"name": "save_photo","value": "保存图片添加水印"},{"name": "take_camera","value": "拍照图片添加水印"},{"name": "pdf_watermark","value": "PDF添加水印"},{"name": "button_text_add_watermark","value": "添加水印"}]
}
  1. float.json
// entry/src/main/resources/base/element/float.json
{"float": [{"name": "mainPage_baseTab_size","value": "24vp"},{"name": "mainPage_baseTab_top","value": "4vp"},{"name": "mainPage_barHeight","value": "52vp"},{"name": "rudder_barHeight","value": "90vp"},{"name": "tab_text_font_size","value": "10fp"},{"name": "content_font_size","value": "30fp"},{"name": "title_font_size","value": "30fp"},{"name": "text_size","value": "16fp"},{"name": "double_text_size","value": "18fp"},{"name": "current_text_size","value": "20fp"},{"name": "back_text_size","value": "14fp"},{"name": "text_line_height","value": "22vp"},{"name": "divider_width","value": "48vp"},{"name": "opacity_1","value": "1"},{"name": "opacity_0","value": "0"},{"name": "list_friction","value": "0.6"},{"name": "size_text","value": "16vp"},{"name": "margin_drawer_list","value": "4vp"},{"name": "margin_tab_text","value": "6vp"},{"name": "margin_under_tab","value": "7vp"},{"name": "margin_eight","value": "8vp"},{"name": "margin_index_top","value": "14vp"},{"name": "margin_sixteen","value": "16vp"},{"name": "margin_list","value": "17vp"},{"name": "margin_index_bottom","value": "18vp"},{"name": "margin_slide_top","value": "40vp"},{"name": "margin_button_bottom","value": "48vp"},{"name": "margin_side_tab_top","value": "74vp"},{"name": "margin_sidebar_content","value": "284vp"},{"name": "padding_double_tab_left","value": "4vp"},{"name": "padding_rudder_tab","value": "11vp"},{"name": "padding_bottom_tab","value": "12vp"},{"name": "padding_drawer_row","value": "13vp"},{"name": "padding_index_top","value": "84vp"},{"name": "drawer_padding","value": "104vp"},{"name": "navbar_back_width","value": "24"},{"name": "navbar_back_height","value": "24"},{"name": "navbar_back_opacity","value": "0.9"},{"name": "navbar_position_x","value": "24"},{"name": "navbar_position_y","value": "12"},{"name": "navbar_height","value": "45"},{"name": "navbar_title_size","value": "18"},{"name": "empty_img_width","value": "110"},{"name": "empty_img_height","value": "88"},{"name": "save_image_margin_bottom","value": "20"},{"name": "index_item_padding_left","value": "12"},{"name": "index_arrow_width","value": "24"},{"name": "index_arrow_height","value": "24"},{"name": "index_item_margin_right","value": "12"}]
}
  1. color.json
// entry/src/main/resources/base/element/color.json
{"color": [{"name": "start_window_background","value": "#FFFFFF"},{"name": "current_color","value": "#3388ff"},{"name": "tab_color","value": "#F3F4F5"},{"name": "text_color","value": "#E6000000"},{"name": "checked_color","value": "#0A59F7"},{"name": "back_color","value": "#0D000000"},{"name": "current_list_color","value": "#1A0A59F7"},{"name": "list_background_color","value": "#E9EAEC"},{"name": "content_background_color","value": "#c1c2c4"},{"name": "side_background_color","value": "#F1F3F5"},{"name": "side_selected_color","value": "#182431"},{"name": "side_text_color","value": "#99182431"},{"name": "side_content_color","value": "#FFFFFF"},{"name": "index_background_color","value": "#f0f3f7"},{"name": "index_divider_color","value": "#0D000000"},{"name": "index_text_color","value": "#99000000"}]
}

其他资源请到源码中获取。

水印添加能力首页

构建水印添加能力首页布局,实现页面背景、图片和文件添加水印的列表和路由。

// entry/src/main/ets/pages/Index.ets// 引入路由功能模块
import { router } from '@kit.ArkUI'
// 引入常量定义,包括路由、子路由、常量值等
import { ChildRoute, Constants, Route } from '../constants/Constants'
// 引入相机模块及相机选择器
import { camera, cameraPicker as picker } from '@kit.CameraKit'@Entry
@ComponentV2
struct Index {// 路由数组,用于渲染主界面菜单private routes: Route[] = Constants.ROUTES// 用于判断分割线是否是最后一个子项private one: number = 1// 本地状态:文件 URI(例如拍照后的图片路径)@Local fileUri: string = ''/*** 打开相机拍照,拍照后跳转到 CameraPage 页面* @param title 页面标题(来自菜单项)*/async openCamera(title: ResourceStr) {// 定义相机配置,使用后置摄像头const pickerProfile: picker.PickerProfile = {cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK}// 弹出拍照界面,只支持拍照(不支持录像)const pickerResult: picker.PickerResult = await picker.pick(getContext(this),[picker.PickerMediaType.PHOTO], pickerProfile)// 记录拍照后的文件路径this.fileUri = pickerResult.resultUri// 如果文件路径有效,则跳转到相应页面,并传递参数if (this.fileUri) {router.pushUrl({url: 'pages/CameraPage',params: {fileUri: this.fileUri,title}})}}build() {Column() {Row() {Text($r('app.string.title')) // 读取资源字符串作为标题.fontWeight(FontWeight.Bold).fontSize($r('app.float.title_font_size')).width(Constants.FULL_WIDTH).fontColor($r('app.color.text_color'))}.width(Constants.INDEX_CONTENT_WIDTH).height(Constants.INDEX_TITLE_HEIGHT)// 主体内容区域,包含多个一级路由模块Column() {ForEach(this.routes, (item: Route) => { // 遍历一级菜单项Row() {Text(item.title).width(Constants.INDEX_CONTENT_WIDTH).fontSize($r('app.float.double_text_size')).fontColor($r('app.color.index_text_color'))}.height(Constants.CARD_TITLE_HEIGHT)Column() {// 遍历子菜单ForEach(item.child, (itemChild: ChildRoute, index: number) => { Column() {Row() {// 子菜单文字Text(itemChild.text).height(Constants.CARD_TEXT_HEIGHT).fontWeight(FontWeight.Medium).padding({ left: $r('app.float.index_item_padding_left') }).fontSize($r('app.float.text_size'))// 空占位符,用于弹性布局Column().layoutWeight(1)// 右侧箭头图标Image($r('app.media.ic_public_arrow_right')).width($r('app.float.index_arrow_width')).height($r('app.float.index_arrow_height')).margin({ right: $r('app.float.index_item_margin_right') })}.justifyContent(FlexAlign.Start).alignItems(VerticalAlign.Center)// 非最后一个子项,显示底部分割线Stack() {if (item.child.length - this.one !== index) {Row().height(Constants.DIVIDER_HEIGHT).backgroundColor($r('app.color.index_divider_color')).width(Constants.DIVIDER_WIDTH)}}}.onClick(() => {// 如果点击的是打开相机的项目,调用 openCamera 方法if (itemChild.to === 'CameraPage') {this.openCamera(itemChild.text)} else {// 否则直接跳转到目标页面,并传递标题参数router.pushUrl({url: 'pages/' + itemChild.to,params: {title: itemChild.text}})}}).width(Constants.INDEX_CONTENT_WIDTH).height(Constants.CARD_TEXT_HEIGHT)}, (item: ChildRoute, index: number) => JSON.stringify(item) + index)}.margin({top: $r('app.float.margin_index_top'),bottom: $r('app.float.margin_index_bottom')}).borderRadius(Constants.BORDER_RADIUS_INDEX_LIST).backgroundColor(Color.White)}, (item: Route, index: number) => JSON.stringify(item) + index)}.width(Constants.FULL_WIDTH)}// 页面整体样式设置.padding({ top: $r('app.float.padding_index_top') }).translate({ y: Constants.TRANSLATE_TOP }).backgroundColor($r('app.color.side_background_color')).width(Constants.FULL_WIDTH).height(Constants.LIST_CONTENT_HEIGHT).alignItems(HorizontalAlign.Center)}
}

页面添加水印

封装Canvas绘制水印组件,使用Stack层叠布局或overlay浮层属性,将水印组件与页面融合。

  1. 使用Stack组件添加添加水印
  • Stack结构页面
// entry/src/main/ets/pages/WatermarkStackPage.ets// 导入自定义导航栏组件
import { NavBar } from '../component/NavBar'
// 导入自定义水印组件
import { Watermark } from '../component/Watermark'@Entry
@ComponentV2
struct CanvasPage {build() {// 外层垂直布局,内容居中对齐Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) {// 页面顶部导航栏组件NavBar()// 中间内容使用 Stack 叠层布局(用于图像叠加水印)Stack({ alignContent: Alignment.Center }) {Column() {// 显示占位图(empty 图片资源)Image($r('app.media.empty')).width(110).height(88)}// 水印组件叠加在图片上,设置旋转角度为 20°Watermark({ rotationAngle: 20 })}.layoutWeight(1).width('100%')}.width('100%').height('100%') }
}

在 main_pages.json 里添加 WatermarkStackPage 的路径源:

// entry/src/resource/base/profile/main_pages.json
{"src": ["pages/Index","pages/WatermarkStackPage",...]
}

本案例其他页面均需要添加路径源,届时不再赘述。

  • Canvas水印绘制组件
// 导入工具方法:用于获取多语言水印文本资源
import { getResourceString } from "../constants/Utils"@ComponentV2
export struct Watermark {// 创建 2D 渲染上下文设置,启用抗锯齿private settings: RenderingContextSettings = new RenderingContextSettings(true)// 创建 Canvas 2D 渲染上下文对象,用于绘图private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)// 以下为可配置参数(可通过使用组件时传入)@Param watermarkWidth: number = 120 // 单个水印区域的宽度@Param watermarkHeight: number = 120 // 单个水印区域的高度@Param watermarkText: string = this.getWatermarkText() // 水印文字,默认读取资源字符串@Param rotationAngle: number = -30 // 水印旋转角度(单位:度),默认 -30°@Param fillColor: string | number | CanvasGradient | CanvasPattern = '#10000000' // 填充颜色(含透明度)@Param font: string = '16vp' // 字体大小(单位为 vp)// 水印绘制逻辑draw() {// 设置填充样式与字体this.context.fillStyle = this.fillColorthis.context.font = this.font// 计算要铺满画布所需的列数和行数const colCount = Math.ceil(this.context.width / this.watermarkWidth)const rowCount = Math.ceil(this.context.height / this.watermarkHeight)// 外层循环控制列for (let col = 0; col <= colCount; col++) {let row = 0// 内层循环控制行for (; row <= rowCount; row++) {// 将角度转换为弧度const angle = this.rotationAngle * Math.PI / 180// 旋转坐标系this.context.rotate(angle)// 根据旋转方向调整文字绘制的位置const positionX = this.rotationAngle > 0 ? this.watermarkHeight * Math.tan(angle) : 0const positionY = this.rotationAngle > 0 ? 0 : this.watermarkWidth * Math.tan(-angle)// 绘制水印文字this.context.fillText(this.watermarkText, positionX, positionY)// 恢复旋转角度this.context.rotate(-angle)// 向下平移到下一行水印位置this.context.translate(0, this.watermarkHeight)}// 回退 Y 轴平移,准备进入下一列this.context.translate(0, -this.watermarkHeight * row)// 向右平移到下一列水印位置this.context.translate(this.watermarkWidth, 0)}}// 从资源中获取水印文字getWatermarkText() {return getResourceString($r('app.string.watermark_screen_text'), getContext(this))}// 渲染 Canvas,并在准备完成后触发绘制build() {Canvas(this.context).width('100%') // 画布宽度撑满容器.height('100%') // 画布高度撑满容器.hitTestBehavior(HitTestMode.Transparent) // 点击事件透传,不阻挡下层组件.onReady(() => this.draw()) // 在画布准备好后调用 draw() 绘制水印}
}
  1. 使用overlay属性添加水印
// entry/src/main/ets/pages/WatermarkOverlayPage.etsimport { NavBar } from '../component/NavBar'
import { Watermark } from '../component/Watermark'@Entry
@ComponentV2
struct OverlayPage {@BuilderwatermarkBuilder() {Column() {Watermark()}}build() {Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center }) {NavBar()Column() {Image($r('app.media.empty')).width(110).height(88)}.justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center).layoutWeight(1)// 通过给Column添加Overlay实现水印添加.overlay(this.watermarkBuilder()).width('100%')}.width('100%').height('100%')}
}

NavBar和Watermark共享已有的组件。

保存图片添加水印

获取图片数据,createPixelMap,使用OffScreenContext在指定位置绘制水印,最后保存带水印图片。

// entry/src/main/ets/pages/SaveImagePage.ets// 导入系统和自定义模块
import { promptAction } from '@kit.ArkUI' // 用于显示提示信息(如 Toast)
import { image } from '@kit.ImageKit' // 提供图像处理能力
import { NavBar } from '../component/NavBar' // 导航栏组件
import {addWatermark,              // 添加水印的工具函数getResourceString,         // 资源字符串获取工具ImagePixelMap,             // 自定义图像像素数据结构imageSource2PixelMap,      // 将 ImageSource 转换为 PixelMap 的工具函数saveToFile                 // 保存图像到文件的方法
} from '../constants/Utils'
import { Constants } from '../constants/Constants' // 常量定义
import { hilog } from '@kit.PerformanceAnalysisKit' // 日志记录工具const TAG = 'SaveImagePage' // 日志标签@Entry
@ComponentV2
struct SaveImagePage {// 用于存储已添加水印后的图像数据@Local addedWatermarkPixelMap: image.PixelMap | null = null// 显示“保存成功”的 Toast 提示showSuccess() {promptAction.showToast({message: $r('app.string.message_save_success'),duration: Constants.TOAST_DURATION})}// 获取水印文本,支持多语言getWatermarkText() {return getResourceString($r('app.string.watermark_text'), getContext(this))}// 从资源文件中读取图像并转换为 ImagePixelMap(包含 PixelMap、宽高)async getImagePixelMap(resource: Resource): Promise<ImagePixelMap> {const data: Uint8Array = await getContext(this).resourceManager.getMediaContent(resource)const arrayBuffer: ArrayBuffer = data.buffer.slice(data.byteOffset, data.byteLength + data.byteOffset)const imageSource: image.ImageSource = image.createImageSource(arrayBuffer)return await imageSource2PixelMap(imageSource)}// 页面构建方法build() {Column() {NavBar() // 自定义导航栏// 显示图像:如果已添加水印则显示处理后的图像,否则显示原图Image(this.addedWatermarkPixelMap || $r('app.media.img1')).width('100%')Row() {// 若尚未添加水印,显示“添加水印”按钮if (!this.addedWatermarkPixelMap) {Button($r('app.string.button_text_add_watermark')).height(40).width('100%').onClick(async () => {// 加载图像资源并添加水印const imagePixelMap = await this.getImagePixelMap($r('app.media.img1'))this.addedWatermarkPixelMap = addWatermark(imagePixelMap, this.getWatermarkText())})} else {// 已添加水印后显示“保存”按钮SaveButton().height(40).width('100%').onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => {if (result === SaveButtonOnClickResult.SUCCESS) {try {// 保存带水印图像await saveToFile(this.addedWatermarkPixelMap!, getContext(this))this.showSuccess()} catch (err) {hilog.error(0x0000, TAG, 'createAsset failed, error:', err)}} else {hilog.error(0x0000, TAG, 'SaveButtonOnClickResult createAsset failed')}})}}.padding({ left: 16, right: 16, bottom: 16 })}.width('100%').height('100%').justifyContent(FlexAlign.SpaceBetween)}
}

拍照图片添加水印

打开相机,获取存储fileUri,然后存入沙箱,获取图片数据,createPixelMap,绘制水印,最后保存带水印图片。

  1. CameraPage页面
// entry/src/main/ets/pages/CameraPage.ets// 引入 ArkUI 的提示动作和路由功能模块
import { promptAction, router } from '@kit.ArkUI'
// 引入图像处理模块
import { image } from '@kit.ImageKit'
// 引入文件操作模块
import { fileIo } from '@kit.CoreFileKit'
// 引入自定义工具函数:添加水印、读取资源字符串、转换像素图、保存文件等
import { addWatermark, getResourceString, ImagePixelMap, imageSource2PixelMap, saveToFile 
} from '../constants/Utils'
// 自定义顶部导航栏组件
import { NavBar } from '../component/NavBar'
// 引入全局常量配置
import { Constants } from '../constants/Constants'
// 引入系统日志工具,用于性能分析与调试
import { hilog } from '@kit.PerformanceAnalysisKit'// 日志打印标签
const TAG = 'CameraPage'@Entry
@ComponentV2
struct CameraPage {// 从路由参数中获取拍照后返回的图片 URIprivate fileUri: string = (router.getParams() as Record<string, string>).fileUri// 响应式状态:添加水印后的图片像素图(初始为 null)@Local addedWatermarkPixelMap: image.PixelMap | null = null/*** 显示保存成功的提示信息*/showSuccess() {promptAction.showToast({message: $r('app.string.message_save_success'), // 获取资源中的提示文本duration: Constants.TOAST_DURATION               // 显示时长常量})}/*** 获取水印文本(例如设备信息、拍摄时间等)*/getWatermarkText() {return getResourceString($r('app.string.watermark_text'), getContext(this))}/*** 从本地图片文件 URI 生成像素图* @param fileUri 本地文件路径* @returns 图片像素图对象(PixelMap)*/async getImagePixelMap(fileUri: string): Promise<ImagePixelMap> {// 打开文件,获取文件描述符const file = fileIo.openSync(fileUri)// 创建图像源对象const imageSource: image.ImageSource = image.createImageSource(file.fd)// 关闭文件fileIo.closeSync(file)// 将图像源转换为像素图return await imageSource2PixelMap(imageSource)}build() {Column() {NavBar() // 页面顶部导航栏(自定义组件)// 显示图片区域,支持滚动Scroll() {// 优先显示水印图,否则显示原图Image(this.addedWatermarkPixelMap || this.fileUri) .width('100%')}.layoutWeight(1).margin({ bottom: 10 })// 底部按钮区域Row() {if (!this.addedWatermarkPixelMap) {// 如果尚未添加水印,显示“添加水印”按钮Button($r('app.string.button_text_add_watermark')).height(40).width('100%').onClick(async () => {// 加载原始图片const imagePixelMap = await this.getImagePixelMap(this.fileUri)// 调用工具方法添加水印,并更新状态this.addedWatermarkPixelMap = addWatermark(imagePixelMap, this.getWatermarkText())})} else {// 如果已有水印图,显示“下载”按钮(使用自定义组件 SaveButton)SaveButton().height(40).width('100%').onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => {// 判断保存操作是否成功if (result === SaveButtonOnClickResult.SUCCESS) {try {// 尝试保存图片到相册或文件系统await saveToFile(this.addedWatermarkPixelMap!, getContext(this))// 显示成功提示this.showSuccess()} catch (err) {// 保存失败,打印错误日志hilog.error(0x0000, TAG, 'createAsset failed, error:', err)}} else {// 保存操作被取消或失败,打印错误日志hilog.error(0x0000, TAG, 'SaveButtonOnClickResult createAsset failed')}})}}.padding({ left: 16, right: 16, bottom: 16 })}.width('100%').height('100%').justifyContent(FlexAlign.SpaceBetween)}
}
  1. 页面NavBar
// entry/src/main/ets/component/NavBar.etsimport { router } from '@kit.ArkUI'@ComponentV2
export struct NavBar {@Param title: ResourceStr = (router.getParams() as Record<string, ResourceStr>).title@Param isWhiteIcon: boolean = falsebuild() {Row() {Button() {Image($r('app.media.back')).width(20).height(20).fillColor(this.isWhiteIcon ? Color.White : Color.Black).opacity(0.9)}.width(40).height(40).backgroundColor('rgba(0, 0, 0, 0.05)').margin({ right: 8 }).onClick(() => {router.back()})Text(this.title).fontSize(20).fontColor(this.isWhiteIcon ? Color.White : Color.Black).opacity(0.9).fontWeight(FontWeight.Bold)}.height($r('app.float.navbar_height')).width('100%').borderWidth({ bottom: 0 }).justifyContent(FlexAlign.Start).padding({ left: 16 })}
}
  1. 图片处理与保存工具类
// entry/src/main/ets/pages/WatermarkPdfPage.ets// 导入图像处理模块
import { image } from '@kit.ImageKit'
// 导入文件系统操作模块
import { fileIo } from '@kit.CoreFileKit'
// 导入日志模块,用于调试输出
import { hilog } from '@kit.PerformanceAnalysisKit'
// 导入相册访问模块,用于保存图片到图库
import { photoAccessHelper } from '@kit.MediaLibraryKit'
// 导入显示模块,用于获取屏幕宽度
import { display } from '@kit.ArkUI'
// 导入上下文类型,用于获取资源管理器等
import { Context } from '@kit.AbilityKit'// 日志标签常量
const TAG = 'Utils'// 文件描述符,用于打开和关闭文件句柄
let fd: number | null = null/*** 将 PixelMap 图像保存为 PNG 文件并写入图库* @param pixelMap 图像像素数据* @param context 当前上下文,用于访问系统接口*/
export async function saveToFile(pixelMap: image.PixelMap, context: Context
): Promise<void> {try {// 获取相册写入权限助手const phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context)// 创建一个新的图片文件路径(自动分配在图库)const filePath = await phAccessHelper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png')// 将 PixelMap 图像打包为 PNG 二进制数据const imagePacker = image.createImagePacker()const imageBuffer = await imagePacker.packToData(pixelMap, {format: 'image/png',quality: 100})// 打开文件用于写入(可读写 + 创建新文件)const mode = fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATEfd = (await fileIo.open(filePath, mode)).fd// 清空原内容(保险操作)await fileIo.truncate(fd)// 将图像数据写入文件await fileIo.write(fd, imageBuffer)} catch (err) {// 打印错误日志hilog.error(0x0000, TAG, 'saveToFile error:', JSON.stringify(err) ?? '')} finally {// 关闭文件描述符(不论成功或失败都执行)if (fd) {fileIo.close(fd)}}
}// 定义图像像素结构,包括宽高
export interface ImagePixelMap {pixelMap: image.PixelMapwidth: numberheight: number
}/*** 从 imageSource 对象创建图像像素图(PixelMap)* @param imageSource 图像源对象,来自本地文件或资源* @returns 图像像素图以及其尺寸*/
export async function imageSource2PixelMap(imageSource: image.ImageSource): Promise<ImagePixelMap> {// 获取图像信息,提取宽高const imageInfo: image.ImageInfo = await imageSource.getImageInfo()const height = imageInfo.size.heightconst width = imageInfo.size.width// 配置解码参数:支持编辑,并设置期望尺寸const options: image.DecodingOptions = {editable: true,desiredSize: { height, width }}// 解码并生成像素图const pixelMap: PixelMap = await imageSource.createPixelMap(options)// 返回结果对象const result: ImagePixelMap = { pixelMap, width, height }return result
}/*** 在图像像素图上绘制水印文本* @param imagePixelMap 原图像像素图* @param text 水印内容,默认 'watermark'* @param drawWatermark 可选自定义绘图回调(提供更灵活的水印样式)* @returns 添加水印后的图像像素图*/
export function addWatermark(imagePixelMap: ImagePixelMap,text: string = 'watermark',drawWatermark?: (OffscreenContext: OffscreenCanvasRenderingContext2D) => void
): image.PixelMap {// 将像素单位转换为可视单位(vp)const height = px2vp(imagePixelMap.height)const width = px2vp(imagePixelMap.width)// 创建离屏画布(不会直接显示)const offScreenCanvas = new OffscreenCanvas(width, height)const offScreenContext = offScreenCanvas.getContext('2d')// 将原图绘制到画布上offScreenContext.drawImage(imagePixelMap.pixelMap, 0, 0, width, height)// 如果传入了自定义水印绘制方法,则调用if (drawWatermark) {drawWatermark(offScreenContext)} else {// 默认水印绘制逻辑const imageScale = width / px2vp(display.getDefaultDisplaySync().width)offScreenContext.textAlign = 'right'offScreenContext.fillStyle = '#A2FFFFFF' // 半透明白色offScreenContext.font = 12 * imageScale + 'vp' // 动态字体大小const padding = 5 * imageScale // 与边缘保持距离offScreenContext.fillText(text, width - padding, height - padding)}// 返回绘制完成后的 PixelMap 对象return offScreenContext.getPixelMap(0, 0, width, height)
}/*** 同步获取资源字符串* @param resource 资源对象(如 $r('app.string.xxx'))* @param context 当前上下文* @returns 字符串内容(若失败则返回空串)*/
export function getResourceString(resource: Resource, context: Context): string {let result: string = ''try {// 尝试通过资源 ID 同步读取字符串内容result = context.resourceManager.getStringSync(resource.id)} catch (e) {// 打印失败日志hilog.error(0x0000, TAG, `[getResourceString]getStringSync failed, error:${JSON.stringify(e)}.`)}return result
}

pdf文件添加水印

使用PdfView预览组件预览pdf,使用pdfService服务加载pdf、添加水印、保存pdf。

// 导入 PDF、文件选择器、组件和日志工具模块
import { pdfService, PdfView, pdfViewManager } from '@kit.PDFKit'
import { fileIo, picker } from '@kit.CoreFileKit'
import { NavBar } from '../component/NavBar'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { promptAction } from '@kit.ArkUI'
import { Constants } from '../constants/Constants'const TAG = 'WatermarkPdfPage' // 日志标签@Entry
@ComponentV2
struct WatermarkPdfPage {// 创建 PDF 控制器对象,用于管理 PDF 加载、显示、释放等操作private controller: pdfViewManager.PdfController = new pdfViewManager.PdfController()// 标志 PDF 是否已经加了水印,用于切换按钮@Local hasWatermark: boolean = false// 显示保存成功的提示showSuccess() {promptAction.showToast({message: $r('app.string.pdf_save_success'),duration: Constants.TOAST_DURATION})}// 获取沙箱路径getSandboxPath(path: string) {const context = getContext()const sandboxDir = context.filesDirreturn `${sandboxDir}/${path}`}// 获取原始 PDF 文件的沙箱路径getPdfSandboxPath(): string {return this.getSandboxPath('input.pdf')}// 获取已加水印的 PDF 文件的沙箱路径getAddedWatermarkPdfSandboxPath(): string {return this.getSandboxPath('output.pdf')}// 将内置 PDF 文件复制到沙箱中,返回沙箱路径savePdfToSandbox(): string {const filePath = this.getPdfSandboxPath()fileIo.accessSync(filePath) // 确保路径存在const content: Uint8Array = getContext().resourceManager.getRawFileContentSync('watermark.pdf')const file = fileIo.openSync(filePath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE | fileIo.OpenMode.TRUNC)fileIo.writeSync(file.fd, content.buffer)fileIo.closeSync(file.fd)return filePath}// 页面即将出现时加载 PDFaboutToAppear(): void {const filePath = this.savePdfToSandbox()this.controller.loadDocument(filePath)}// 构造水印信息(文字水印)getWatermarkInfo() {const watermarkInfo: pdfService.TextWatermarkInfo = new pdfService.TextWatermarkInfo()watermarkInfo.watermarkType = pdfService.WatermarkType.WATERMARK_TEXTwatermarkInfo.content = 'This is Watermark'watermarkInfo.textSize = 32watermarkInfo.textColor = 200watermarkInfo.opacity = 0.3watermarkInfo.rotation = 45return watermarkInfo}// 向 PDF 添加水印并保存为新文件,然后更新视图addWatermark() {const filePath = this.getPdfSandboxPath()let pdfDocument: pdfService.PdfDocument = new pdfService.PdfDocument()pdfDocument.loadDocument(filePath)// 从第 0 页开始添加水印,直到最后一页pdfDocument.addWatermark(this.getWatermarkInfo(), 0, pdfDocument.getPageCount(), true, true)const watermarkFilePath = this.getAddedWatermarkPdfSandboxPath()pdfDocument.saveDocument(watermarkFilePath)this.showInPdfView(watermarkFilePath) // 显示带水印的 PDF}// 加载并显示指定路径的 PDFasync showInPdfView(filePath: string) {this.hasWatermark = truethis.controller.releaseDocument() // 必须先释放再加载,避免崩溃await this.controller.loadDocument(filePath)this.controller.setPageFit(pdfService.PageFit.FIT_WIDTH) // 适配宽度}// 弹出文件选择器保存带水印的 PDFasync savePdf() {const documentSaveOptions = new picker.DocumentSaveOptions()documentSaveOptions.newFileNames = ['watermark.pdf']const documentPicker = new picker.DocumentViewPicker(getContext(this))const saveResult = await documentPicker.save(documentSaveOptions)this.copyFileSync(this.getAddedWatermarkPdfSandboxPath(), saveResult[0])this.showSuccess()}// 同步复制文件:将 PDF 从沙箱复制到用户指定位置copyFileSync(srcPath: string, destPath: string) {const srcFile = fileIo.openSync(srcPath, fileIo.OpenMode.READ_WRITE)const destFile = fileIo.openSync(destPath, fileIo.OpenMode.READ_WRITE)fileIo.copyFileSync(srcFile.fd, destFile.fd)fileIo.closeSync(srcFile)fileIo.closeSync(destFile)}// 页面 UI 构建逻辑build() {Column() {NavBar()// 主视图区,显示 PDF 内容并展示控制按钮Stack({ alignContent: Alignment.Bottom }) {PdfView({controller: this.controller,pageFit: pdfService.PageFit.FIT_WIDTH}).id('pdfview_app_view').layoutWeight(1)// 底部按钮:添加水印或保存Row() {if (!this.hasWatermark) {Button($r('app.string.button_text_add_watermark')).height(40).width('100%').onClick(() => this.addWatermark())} else {SaveButton().height(40).width('100%').onClick(async (event: ClickEvent, result: SaveButtonOnClickResult) => {if (result === SaveButtonOnClickResult.SUCCESS) {try {this.savePdf()} catch (err) {hilog.error(0x0000, TAG, 'createAsset failed, error:', err)}} else {hilog.error(0x0000, TAG, 'SaveButtonOnClickResult createAsset failed')}})}}.padding({ left: 16, right: 16, bottom: 16 })}.layoutWeight(1)}.height('100%').width('100%')}
}

案例代码下载 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/87658.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/87658.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Qt工作总结07 <qBound和std::clamp>

一、qBound简介 1. 定义 是 Qt 框架中一个非常实用的边界限制函数&#xff08;也称为 "clamp" 函数&#xff09;&#xff0c;用于将一个值限制在指定的最小值和最大值之间。头文件&#xff1a;#include <QtGlobal> 2. 函数原型 template <typename T>…

53-Oracle sqlhc多版本实操含(23 ai)

SQLHC&#xff08;SQL Health Check&#xff09;作为 Oracle 数据库性能诊断的核心工具&#xff0c;其设计理念和核心功能在 Oracle 各版本中保持高度一致&#xff0c;但在技术实现和周边生态上存在渐进式优化。定期对关键业务 SQL 执行健康检查&#xff0c;特别是在版本升级或…

math.pow()和pow()的区别

math.pow() 和 pow() 的区别 ✅ 1. math.pow() 来自 math 模块参数&#xff1a;两个数&#xff08;底数&#xff0c;指数&#xff09;结果类型&#xff1a; 始终返回 float 类型 示例&#xff1a; import math print(math.pow(2, 3)) # 输出&#xff1a;8.0 &#xff08;…

邮科OEM摄像头POE供电技术的核心优势

安防监控中&#xff0c;供电方式影响系统稳定性、安装效率与维护成本。邮科摄像头采用POE技术&#xff0c;通过网线同时传输数据与电力&#xff0c;革新传统部署模式。本文解析其六大核心优势&#xff0c;展现其作为现代安防优选方案的价值。 一、布线简化&#xff0c;效率提升…

微信小程序-数据加密

npm install crypto-js utils/aes.js const CryptoJS require(crypto-js);// 默认的 KEY 与 iv 如果没有给 const KEY CryptoJS.enc.Utf8.parse(KrQ4KAYOEyAz66RS); // 十六位十六进制数作为密钥 const IV CryptoJS.enc.Utf8.parse(ep1YCmxXuxKe4eH1); // 十六位十六进制…

【时时三省】(C语言基础)善于利用指针

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 指针是C语言中的一个重要概念&#xff0c;也是C语言的一个重要特色。正确而灵活地运用它&#xff0c;可以使程序简洁、紧凑、高效。每一个学习和使用C语言的人&#xff0c;都应当深入地学习和…

单点登录进阶:基于芋道(yudao)授权码模式的单点登录流程、代码实现与安全设计

最近遇到需要单点登录的场景&#xff0c;我使用的是芋道框架&#xff0c;正好它手动实现了OAuth2的功能&#xff0c;可以为单点登录提供一些帮助&#xff0c;结合授权码的模式&#xff0c;在改动最小的情况下实现了单点登录。关键业务数据已经隐藏&#xff0c;后续将以以主认证…

关于Seata的一个小issue...

文章目录 引言原因&#x1f913;解决方法&#x1f635;总结❤️ 引言 某一天&#xff0c;笔者在逛着Github的时候&#xff0c;突然看到seata有个有趣的issue&#xff0c;是一个task。 相关描述&#xff1a; While running the DruidSQLRecognizerFactoryTest.testIsSqlSynta…

FTTR+软路由网络拓扑方案

文章目录 网络拓扑软路由配置FTTR光猫路由器TPLink路由器配置WAN设置LAN设置 参考 网络拓扑 软路由配置 配置静态IP地址&#xff1a;192.168.1.100设置网关指向主路由的IP 设置自定义DNS服务器 开启DHCP 这一步很关键&#xff0c;可以让连上wifi的所有设备自动趴强。 FTTR光猫…

RPC - 服务注册与发现模块

为什么要服务注册&#xff0c;服务注册是做什么 服务注册主要是实现分布式的系统&#xff0c;让系统更加的健壮&#xff0c;一个节点主机将自己所能提供的服务&#xff0c;在注册中心进行登记 为什么要服务发现&#xff0c;服务发现是要做什么 rpc调用者需要知道哪个节点主机…

分布式缓存:应对突发流量的缓存体系构建

文章目录 缓存全景图Pre背景与目标说明缓存原则与设计思路缓存体系架构缓存预热与缓存预加载库存操作与缓存结合防刷、限流与缓存缓存一致性与失效异步落地与消息队列监控与指标容灾与扩展示例小结 缓存全景图 Pre 分布式缓存&#xff1a;缓存设计三大核心思想 分布式缓存&am…

华为云Flexus+DeepSeek征文|CCE容器高可用部署搭建Dify-LLM平台部署AI Agent

华为云FlexusDeepSeek征文&#xff5c;CCE容器高可用部署搭建Dify-LLM平台部署AI Agent 前言 Dify是一款开源的大语言模型应用开发平台&#xff0c;融合了后端即服务和LLMOps的理念&#xff0c;使开发者可以快速搭建生产级的生成式AI应用&#xff0c;本文将详细介绍如何使用华…

Postman 的 Jenkins 管理 - 手动构建

目录 一、准备工作 二、postman 项目脚本准备并导出 1. 打开已完成并测试无误的 postman 项目脚本。 再次执行测试。 ​编辑2. 导出&#xff08; 测试用例集、环境变量 两个文件&#xff09;**“不 支 持 中 文”** —— 全部改成英文&#xff01; ​编辑3. 文件所在目录…

音视频之H.264/AVC解码器的原理和实现

系列文章&#xff1a; 1、音视频之视频压缩技术及数字视频综述 2、音视频之视频压缩编码的基本原理 3、音视频之H.264/AVC编码器原理 4、音视频之H.264的句法和语义 5、音视频之H.264/AVC解码器的原理和实现 6、音视频之H.264视频编码传输及其在移动通信中的应用 7、音视…

【智能安全帽新升级】搭载VTX316TTS语音合成芯片,让安全“听得见”!

在工地轰鸣的机械声中&#xff0c;一句清晰的指令可能比任何文字都更有力量。 当智能安全帽遇上VTX316语音合成芯片&#xff0c;安全防护从“被动响应”进化为“主动交互”&#xff0c;为高危行业戴上了一顶“会说话的智慧大脑”&#xff01; 传统安全帽的“沉默”危机 在建筑…

【目标检测】非极大值抑制(NMS)的原理与实现

&#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…

DB-GPT启动提示please install by running `pip install cryptography`

DB-GPT项目需要 cryptography 库来处理加密功能&#xff0c;但环境中没有安装它。cryptography 是一个用于安全和加密操作的Python库&#xff0c;许多项目&#xff08;包括DB-GPT&#xff09;依赖它来处理敏感数据的加密存储。 解决方案 1. 安装 cryptography 库 在激活的环…

局域网文件共享及检索系统

标题:局域网文件共享及检索系统 内容:1.摘要 随着信息技术的飞速发展&#xff0c;局域网在企业、学校等场景中得到广泛应用&#xff0c;大量文件在局域网内存储和流转。然而&#xff0c;目前局域网内文件共享与检索存在效率低、管理困难等问题。本文旨在设计并实现一个高效的局…

Spring Boot医疗系统高并发难题:达梦数据库死锁排查与优化实战

Spring Boot医疗系统高并发难题:达梦数据库死锁排查与优化实战 引言:医疗系统中的并发挑战 在现代医疗系统中,检查申请处理是关键业务场景之一,每天需要处理数以万计的检查记录。当多个操作同时更新同一患者的申请状态时,数据库层面的死锁问题成为高并发环境下的典型痛点…

Go语言中的文件与IO:bufio 和 scanner

Go 标准库中的 bufio 包提供了带缓冲的读写功能&#xff0c;可以显著提高文件和数据处理效率。而 bufio.Scanner 则是读取文本文件中每一行的利器&#xff0c;常用于日志、配置等文本处理场景。 一、为什么使用 bufio&#xff1f; 直接对文件进行 os.File.Read() 或 os.File.W…