鴻蒙案例實戰——添加水印

本示例為開發者展示常用的水印添加能力,包括兩種方式給頁面添加水印、保存圖片添加水印、拍照圖片添加水印和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
英文地址,請注明出處:http://en.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…