介紹
基于canvas組件、圖片編解碼,介紹了圖片編輯實現(xiàn)過程。主要包含以下功能:
- 圖片的解碼和繪制。
- 使用PixelMap進行圖片編輯,如裁剪、旋轉(zhuǎn)、亮度調(diào)節(jié)、透明度調(diào)節(jié)、飽和度調(diào)節(jié)等操作。
相關概念
- [canvas組件]:提供畫布組件。用于自定義繪制圖形。
- [圖片處理]:提供圖片處理效果,包括通過屬性創(chuàng)建PixelMap、讀取圖像像素數(shù)據(jù)、讀取區(qū)域內(nèi)的圖片數(shù)據(jù)等。
相關權限
本篇Codelab使用了媒體文件存儲能力,需要在配置文件config.json里添加媒體文件讀寫權限:
環(huán)境搭建
鴻蒙開發(fā)指導文檔:[gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md
]
軟件要求
- [DevEco Studio]版本:DevEco Studio 3.1 Release及以上版本。
- OpenHarmony SDK版本:API version 9及以上版本。
硬件要求
- 開發(fā)板類型:[潤和RK3568開發(fā)板]。
- OpenHarmony系統(tǒng):3.2 Release及以上版本。
環(huán)境搭建
完成本篇Codelab我們首先要完成開發(fā)環(huán)境的搭建,本示例以RK3568開發(fā)板為例,參照以下步驟進行:
- [獲取OpenHarmony系統(tǒng)版本]:標準系統(tǒng)解決方案(二進制)。以3.2 Release版本為例:
- 搭建燒錄環(huán)境。
- [完成DevEco Device Tool的安裝]
- [完成RK3568開發(fā)板的燒錄]
- 搭建開發(fā)環(huán)境。
- 開始前請參考[工具準備],完成DevEco Studio的安裝和開發(fā)環(huán)境配置。
- 開發(fā)環(huán)境配置完成后,請參考[使用工程向?qū)創(chuàng)建工程(模板選擇“Empty Ability”)。
- 工程創(chuàng)建完成后,選擇使用[真機進行調(diào)測]。
代碼結(jié)構解讀
本篇Codelab只對核心代碼進行講解,對于完整代碼,我們會在gitee中提供。
├──entry/src/main/js // 代碼區(qū)
│ └──MainAbility
│ ├──common
│ │ ├──bean
│ │ │ └──messageItem.js // 多線程封裝消息
│ │ ├──constant
│ │ │ └──commonConstants.js // 常量
│ │ ├──images // 圖片資源
│ │ └──utils
│ │ ├──adjustUtil.js // 飽和度、亮度調(diào)節(jié)工具
│ │ ├──imageUtil.js // 圖片獲取、打包工具
│ │ ├──logger.js // 日志工具
│ │ ├──opacityUtil.js // 透明度調(diào)節(jié)工具
│ │ └──rotateUtil.js // 旋轉(zhuǎn)工具
│ ├──i18n // 國際化中英文
│ │ ├──en-US.json
│ │ └──zh-CN.json
│ ├──model
│ │ └──cropModel.js // 裁剪數(shù)據(jù)處理
│ ├──pages
│ │ └──index
│ │ ├──index.css // 首頁樣式文件
│ │ ├──index.hml // 首頁布局文件
│ │ └──index.js // 首頁業(yè)務處理文件
│ ├──workers
│ │ ├──adjustBrightnessWork.js // 亮度異步調(diào)節(jié)
│ │ └──adjustSaturationWork.js // 飽和度異步調(diào)節(jié)
│ └──app.js // 程序入口
└──entry/src/main/resources // 應用資源目錄
圖片解碼
本章節(jié)將介紹如何將圖片解碼,并顯示在canvas組件上。需要完成以下功能:
- 獲取圖片的PixelMap對象。
- 在canvas組件上繪制第一步獲取的PixelMap對象。
在index.js文件的onInit生命周期中初始化canvas畫布,具體有以下步驟:
- 調(diào)用imageUtil工具類的getImageFd方法,根據(jù)資源文件獲取圖片fd。
- 調(diào)用imageUtil工具類的getImagePixelMap方法,將獲取的fd創(chuàng)建成圖片實例,通過實例獲取其PixelMap。
- 通過canvas id獲取CanvasRenderingContext2D對象。
- 將獲取的PixelMap繪制到canvas組件上。
// index.js
export default {
onInit() {
...
this.initCanvas().then(() = > {
this.postState = false;
});
},
async initCanvas() {
...
// 獲取圖片fd
this.imageFd = await getImageFd(CommonConstants.IMAGE_NAME);
// 獲取圖片pixelMap
this.imagePixelMapAdjust = await getImagePixelMap(this.imageFd);
...
// 獲取canvas對象
const canvasOne = this.$element('canvasOne');
this.canvasContext = canvasOne.getContext('2d');
...
// 在canvas組件上繪圖
this.drawToCanvas(this.imagePixelMapAdjust, this.drawImageLeft, this.drawImageTop,
this.drawWidth, this.drawHeight);
}
}
// imageUtil.js
export async function getImageFd(imageName) {
let mResourceManager = await resourceManager.getResourceManager();
let rawImageDescriptor = await mResourceManager.getRawFd(imageName);
let fd = rawImageDescriptor?.fd;
return fd;
}
export async function getImagePixelMap(fd) {
let imageSource = image.createImageSource(fd);
if (!imageSource) {
return;
}
let pixelMap = await imageSource.createPixelMap({
editable: true,
desiredPixelFormat: CommonConstants.PIXEL_FORMAT
});
return pixelMap;
}
圖片裁剪
本篇Codelab提供四種裁剪比例,全圖裁剪、1:1裁剪、16:9裁剪、4:3裁剪。需要完成以下步驟實現(xiàn)裁剪功能:
- 根據(jù)裁剪比例,獲取canvas畫布上需要繪制的裁剪框?qū)捀摺?/li>
- 根據(jù)裁剪比例,獲取原圖需要裁剪的寬高。
- 根據(jù)第二步獲取的原圖需要裁剪的寬高,對圖片進行裁剪。
- 根據(jù)裁剪后原圖寬高,適配屏幕大小,重新繪制。
// index.js
export default {
// 任意點擊四種裁剪比例
cropClick(clickIndex) {
this.cropClickIndex = clickIndex;
switch (clickIndex) {
// 全圖裁剪
case CommonConstants.CropType.ORIGINAL:
cropOriginal(this);
break;
// 1:1裁剪
case CommonConstants.CropType.ONE_TO_ONE:
cropSquareImage(this);
break;
// 16:9裁剪
case CommonConstants.CropType.SIXTEEN_TO_NINE:
cropRectangleImage(this);
break;
// 4:3裁剪
case CommonConstants.CropType.FOUR_TO_THREE:
cropBannerImage(this);
break;
default:
break;
}
drawScreenSelection(this, this.canvasCropContext);
}
}
以1:1裁剪為例,調(diào)用cropSquareImage方法,獲取原圖需要裁剪的寬高以及裁剪框的寬高。點擊切換編輯類型或保存,調(diào)用cropDrawImage方法裁剪圖片,最后適配屏幕重新繪制。
// cropModel.js
export function cropSquareImage(context) {
...
let length = Math.min(context.originalImage.width, context.originalImage.height);
// 原圖需要裁剪的寬高
context.cropWidth = length;
context.cropHeight = length;
let drawLength = Math.min(context.drawWidth, context.drawHeight);
// 裁剪框?qū)捀?/span>
context.cropDrawWidth = drawLength;
context.cropDrawHeight = drawLength;
}
export async function cropDrawImage(context) {
...
let imagePixel = context.imagePixelMapAdjust;
let diffX = (context.originalImage.width - context.cropWidth) / CommonConstants.HALF;
let diffY = (context.originalImage.height - context.cropHeight) / CommonConstants.HALF;
context.cropLeft = Math.floor(diffX * accuracy) / accuracy;
context.cropTop = Math.floor(diffY * accuracy) / accuracy;
// 裁剪圖片
await imagePixel.crop({ x: context.cropLeft, y: context.cropTop,
size: {
height: context.cropHeight,
width: context.cropWidth
}
});
// 裁剪后原圖寬高
context.originalImage.width = context.cropWidth;
context.originalImage.height = context.cropHeight;
context.imagePixelMapAdjust = imagePixel;
}
// index.js
// 選擇裁剪框后,裁剪并適應屏幕顯示
async crop() {
await cropDrawImage(this);
// 適配屏幕
this.adjustSize();
this.canvasCropContext.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 重新繪制
this.drawToCanvas(this.imagePixelMapAdjust, this.drawImageLeft, this.drawImageTop, this.drawWidth,
this.drawHeight);
cropOriginal(this);
}
圖片旋轉(zhuǎn)
本篇Codelab提供逆時針旋轉(zhuǎn)、順時針旋轉(zhuǎn)兩種方式,每次旋轉(zhuǎn)角度為90度。在index.html文件中,使用兩個image組件實現(xiàn)逆時針旋轉(zhuǎn)、順時針旋轉(zhuǎn)按鈕。點擊對應圖片時,觸發(fā)onclick事件并回調(diào)onRotate方法。
< !-- index.html -- >
< div class="space-around-row adjust-width crop-height" >
< !-- 逆時針旋轉(zhuǎn) -- >
< image src="http://www.nxhydt.com/images/chaijie_default.png" class="edit-image" onclick="onRotate(-90)" >< /image >
< !-- 順時針旋轉(zhuǎn) -- >
< image src="http://www.nxhydt.com/images/chaijie_default.png" class="edit-image" onclick="onRotate(90)" >< /image >
< /div >
在index.js文件中,實現(xiàn)onRotate方法。根據(jù)方法入?yún)ngle,調(diào)用PixelMap接口提供的rotate方法,完成圖片旋轉(zhuǎn)功能。
// index.js
export default {
// 點擊逆時針旋轉(zhuǎn)
onRotate(angle) {
let that = this;
this.postState = true;
rotate(this.imagePixelMapAdjust, angle, () = > {
that.exchange();
});
}
}
// rotateUtil.js
export async function rotate(pixelMap, angle, callback) {
if (!pixelMap) {
return;
}
await pixelMap.rotate(angle);
callback();
}
圖片色域調(diào)節(jié)
本篇Codelab的色域調(diào)節(jié)是使用色域模型RGB-HSV來實現(xiàn)的。
- RGB:是我們接觸最多的顏色空間,分別為紅色(R)、綠色(G)和藍色(B)。
- HSV:是用色相H,飽和度S,明亮度V來描述顏色的變化
- H:色相H取值范圍為0°~360°,從紅色開始按逆時針方向計算,紅色為0°,綠色為120°,藍色為240°。
- S:飽和度S越高,顏色則深而艷。光譜色的白光成分為0,飽和度達到最高。通常取值范圍為0%~100%,值越大,顏色越飽和。
- V:明度V表示顏色明亮的程度,對于光源色,明度值與發(fā)光體的光亮度有關;對于物體色,此值和物體的透射比或反射比有關。通常取值范圍為0%(黑)到100%(白)。
亮度調(diào)節(jié)
完成以下步驟實現(xiàn)亮度調(diào)節(jié):
- 將PixelMap轉(zhuǎn)換成ArrayBuffer。
- 將生成好的ArrayBuffer發(fā)送到worker線程。
- 對每一個像素點的亮度值按倍率計算。
- 將計算好的ArrayBuffer發(fā)送回主線程。
- 將ArrayBuffer寫入PixelMap,重新繪圖。
說明: 當前亮度調(diào)節(jié)是在UI層面實現(xiàn)的,未實現(xiàn)細節(jié)優(yōu)化算法,只做簡單示例。調(diào)節(jié)后的圖片會有色彩上的失真。
// index.js
export default {
// pixelMap轉(zhuǎn)換ArrayBuffer及發(fā)送ArrayBuffer到worker,
postToWorker(type, value, workerName) {
let sliderValue = type === CommonConstants.AdjustId.BRIGHTNESS ? this.brightnessValue : this.saturationValue;
this.workerInstance = new worker.ThreadWorker(workerName);
const bufferArray = new ArrayBuffer(this.imagePixelMapAdjust.getPixelBytesNumber());
this.imagePixelMapAdjust.readPixelsToBuffer(bufferArray).then(() = > {
let message = new MessageItem(bufferArray, sliderValue, value);
this.workerInstance.postMessage(message);
this.postState = true;
// 收到worker線程完成的消息
this.workerInstance.onmessage = this.updatePixelMap.bind(this);
this.workerInstance.onexit = () = > {
if (type === CommonConstants.AdjustId.BRIGHTNESS) {
this.brightnessValue = Math.floor(value);
} else {
this.saturationValue = Math.floor(value);
}
}
});
}
}
// AdjustBrightnessWork.js
// worker線程處理部分
workerPort.onmessage = function (event) {
let bufferArray = event.data.buffer;
let lastValue = event.data.lastValue;
let currentValue = event.data.currentValue;
let buffer = adjustImageValue(bufferArray, lastValue, currentValue);
workerPort.postMessage(buffer);
}
// adjustUtil.js
// 倍率計算部分
export function adjustImageValue(bufferArray, last, cur) {
return execColorInfo(bufferArray, last, cur, CommonConstants.HSVIndex.VALUE);
}
透明度調(diào)節(jié)
PixelMap接口提供了圖片透明度調(diào)節(jié)的功能。拖動滑塊調(diào)節(jié)透明度,回調(diào)setOpacityValue方法,獲取需要調(diào)節(jié)的透明度,最后調(diào)用opacity方法完成透明度調(diào)節(jié)。
// index.js
export default {
setOpacityValue(event) {
let slidingOpacityValue = event.value;
let slidingMode = event.mode;
if (slidingMode === CommonConstants.SLIDER_MODE_END || slidingMode === CommonConstants.SLIDER_MODE_CLICK) {
adjustOpacity(this.imagePixelMapAdjust, slidingOpacityValue).then(pixelMap = > {
this.imagePixelMapAdjust = pixelMap;
this.drawToCanvas(this.imagePixelMapAdjust, this.drawImageLeft, this.drawImageTop,
this.drawWidth, this.drawHeight);
this.opacityValue = Math.floor(slidingOpacityValue);
});
}
}
}
// opacityUtil.js
export async function adjustOpacity(pixelMap, value) {
if (!pixelMap) {
return;
}
pixelMap.opacity(parseInt(value) / CommonConstants.SLIDER_MAX_VALUE).catch(err = > {
Logger.error(`opacity err ${JSON.stringify(err)}`);
});
return pixelMap;
}
飽和度調(diào)節(jié)
飽和度調(diào)節(jié)與亮度調(diào)節(jié)步驟類似:
- 將PixelMap轉(zhuǎn)換成ArrayBuffer。
- 將生成好的ArrayBuffer發(fā)送到worker線程。
- 對每一個像素點的飽和度值按倍率計算。
- 將計算好的ArrayBuffer發(fā)送回主線程。
- 將ArrayBuffer寫入PixelMap,重新繪圖。
說明: 當前飽和度調(diào)節(jié)是在UI層面實現(xiàn)的,未實現(xiàn)細節(jié)優(yōu)化算法,只做簡單示例。調(diào)節(jié)后的圖片會有色彩上的失真。
// index.js
export default {
// pixelMap轉(zhuǎn)換ArrayBuffer及發(fā)送ArrayBuffer到worker,
postToWorker(type, value, workerName) {
let sliderValue = type === CommonConstants.AdjustId.BRIGHTNESS ? this.brightnessValue : this.saturationValue;
this.workerInstance = new worker.ThreadWorker(workerName);
const bufferArray = new ArrayBuffer(this.imagePixelMapAdjust.getPixelBytesNumber());
this.imagePixelMapAdjust.readPixelsToBuffer(bufferArray).then(() = > {
let message = new MessageItem(bufferArray, sliderValue, value);
this.workerInstance.postMessage(message);
this.postState = true;
// 收到worker線程完成的消息
this.workerInstance.onmessage = this.updatePixelMap.bind(this);
this.workerInstance.onexit = () = > {
if (type === CommonConstants.AdjustId.BRIGHTNESS) {
this.brightnessValue = Math.floor(value);
} else {
this.saturationValue = Math.floor(value);
}
}
});
}
}
// adjustSaturationWork.js
// worker線程處理部分
workerPort.onmessage = function (event) {
let bufferArray = event.data.buffer;
let lastValue = event.data.lastValue;
let currentValue = event.data.currentValue;
let buffer = adjustSaturation(bufferArray, lastValue, currentValue)
workerPort.postMessage(buffer);
}
// adjustUtil.js
// 倍率計算部分
export function adjustSaturation(bufferArray, last, cur) {
return execColorInfo(bufferArray, last, cur, CommonConstants.HSVIndex.SATURATION);
}
審核編輯 黃宇
-
Canvas
+關注
關注
0文章
16瀏覽量
10959 -
鴻蒙
+關注
關注
56文章
2267瀏覽量
42493 -
HarmonyOS
+關注
關注
79文章
1946瀏覽量
29742 -
OpenHarmony
+關注
關注
25文章
3549瀏覽量
15749
發(fā)布評論請先 登錄
相關推薦
評論