介紹
是基于ArkTS的聲明式開(kāi)發(fā)范式的樣例,主要介紹了圖片編輯實(shí)現(xiàn)過(guò)程。樣例主要包含以下功能:
- 圖片的解碼。
- 使用PixelMap進(jìn)行圖片編輯,如裁剪、旋轉(zhuǎn)、亮度、透明度、飽和度等。
- 圖片的編碼。
相關(guān)概念
- [圖片解碼]:讀取不同格式的圖片文件,無(wú)壓縮的解碼為位圖格式。
- [PixelMap]:圖片解碼后的狀態(tài),用于對(duì)圖片像素進(jìn)行處理。
- [圖片編碼]:圖片經(jīng)過(guò)像素處理完成之后,需要重新進(jìn)行編碼打包,生成需要的圖片格式。
環(huán)境搭建
軟件要求
- [DevEco Studio]版本:DevEco Studio 3.1 Release。
- OpenHarmony SDK版本:API version 9。
硬件要求
- 開(kāi)發(fā)板類型:[潤(rùn)和RK3568開(kāi)發(fā)板]。
- OpenHarmony系統(tǒng):3.2 Release。
環(huán)境搭建
完成本篇Codelab我們首先要完成開(kāi)發(fā)環(huán)境的搭建,本示例以RK3568開(kāi)發(fā)板為例,參照以下步驟進(jìn)行:
- [獲取OpenHarmony系統(tǒng)版本]:標(biāo)準(zhǔn)系統(tǒng)解決方案(二進(jìn)制)。以3.2 Release版本為例:
- 搭建燒錄環(huán)境。
- [完成DevEco Device Tool的安裝]
- [完成RK3568開(kāi)發(fā)板的燒錄](méi)
- 搭建開(kāi)發(fā)環(huán)境。
代碼結(jié)構(gòu)解讀
本篇Codelab只對(duì)核心代碼進(jìn)行講解,對(duì)于完整代碼,我們會(huì)在gitee中提供。
├──entry/src/main/ets // 代碼區(qū)
│ ├──common
│ │ └──constant
│ │ └──CommonConstant.ets // 常量類
│ ├──entryability
│ │ └──EntryAbility.ts // 本地啟動(dòng)ability
│ ├──pages
│ │ └──HomePage.ets // 本地主頁(yè)面
│ ├──utils
│ │ ├──AdjustUtil.ets // 調(diào)節(jié)工具類
│ │ ├──CropUtil.ets // 裁剪工具類
│ │ ├──DecodeUtil.ets // 解碼工具類
│ │ ├──DrawingUtils.ets // Canvas畫圖工具類
│ │ ├──EncodeUtil.ets // 編碼工具類
│ │ ├──LoggerUtil.ets // 日志工具類
│ │ ├──MathUtils.ets // 坐標(biāo)轉(zhuǎn)換工具類
│ │ └──OpacityUtil.ets // 透明度調(diào)節(jié)工具類
│ ├──view
│ │ ├──AdjustContentView.ets // 色域調(diào)整視圖
│ │ └──ImageSelect.ets // Canvas選擇框?qū)崿F(xiàn)類
│ ├──viewmodel
│ │ ├──CropShow.ets // 選擇框顯示控制類
│ │ ├──CropType.ets // 按比例選取圖片
│ │ ├──IconListViewModel.ets // icon數(shù)據(jù)
│ │ ├──ImageEditCrop.ets // 圖片編輯操作類
│ │ ├──ImageFilterCrop.ets // 圖片操作收集類
│ │ ├──ImageSizeItem.ets // 圖片尺寸
│ │ ├──Line.ets // 線封裝類
│ │ ├──MessageItem.ets // 多線程封裝消息
│ │ ├──OptionViewModel.ets // 圖片處理封裝類
│ │ ├──PixelMapWrapper.ets // PixelMap封裝類
│ │ ├──Point.ets // 點(diǎn)封裝類
│ │ ├──Ratio.ets // 比例封裝類
│ │ ├──Rect.ets // 矩形封裝類
│ │ ├──RegionItem.ets // 區(qū)域封裝類
│ │ └──ScreenManager.ts // 屏幕尺寸計(jì)算工具類
│ └──workers
│ ├──AdjustBrightnessWork.ts // 亮度異步調(diào)節(jié)
│ └──AdjustSaturationWork.ts // 飽和度異步調(diào)節(jié)
└──entry/src/main/resources // 資源文件目錄
鴻蒙開(kāi)發(fā)指導(dǎo)文檔:[gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md
]
圖片解碼
在這個(gè)章節(jié)中,需要完成圖片解碼的操作,并將解碼后的圖片展示。效果如圖所示:
在進(jìn)行圖片編輯前需要先加載圖片,當(dāng)前文檔是在生命周期aboutToAppear開(kāi)始加載。具體實(shí)現(xiàn)步驟。
- 讀取資源文件。
- 將獲取的fd創(chuàng)建成圖片實(shí)例,通過(guò)實(shí)例獲取其pixelMap。
- 將解析好的pixelMap通過(guò)Image組件加載顯示。
// HomePage.ets
aboutToAppear() {
this.pixelInit();
...
}
build() {
Column() {
...
Column() {
if (this.isCrop && this.showCanvas && this.statusBar > 0) {
if (this.isSaveFresh) {
ImageSelect({
statusBar: this.statusBar
})
}
...
} else {
if (this.isPixelMapChange) {
Image(this.pixelMap)
.scale({ x: this.imageScale, y: this.imageScale, z: 1 })
.objectFit(ImageFit.None)
}
...
}
}
...
}
...
}
async getResourceFd(filename: string) {
const resourceMgr = getContext(this).resourceManager;
const context = getContext(this);
if (filename === CommonConstants.RAW_FILE_NAME) {
let imageBuffer = await resourceMgr.getMediaContent($r("app.media.ic_low"))
let filePath = context.cacheDir + '/' + filename;
let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
let writeLen = fs.writeSync(file.fd, imageBuffer.buffer);
fs.copyFileSync(filePath, context.cacheDir + '/' + CommonConstants.RAW_FILE_NAME_TEST);
return file.fd;
} else {
let filePath = context.cacheDir + '/' + filename;
let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
return file.fd;
}
}
async getPixelMap(fileName: string) {
const fd = await this.getResourceFd(fileName);
const imageSourceApi = image.createImageSource(fd);
if (!imageSourceApi) {
Logger.error(TAG, 'imageSourceAPI created failed!');
return;
}
const pixelMap = await imageSourceApi.createPixelMap({
editable: true
});
return pixelMap;
}
圖片處理
當(dāng)前章節(jié)需要完成圖片的裁剪、旋轉(zhuǎn)、色域調(diào)節(jié)(本章只介紹亮度、透明度、飽和度)等功能。
- 裁剪:選取圖片中的部分進(jìn)行裁剪生成新的圖片。
- 旋轉(zhuǎn):將圖片按照不同的角度進(jìn)行旋轉(zhuǎn),生成新的圖片。
- 色域調(diào)節(jié):當(dāng)前Codelab色域調(diào)節(jié)的亮度、透明度和飽和度,使用色域模型RGB-HSV來(lái)實(shí)現(xiàn)的。
- RGB:是我們接觸最多的顏色空間,分別為紅色(R),綠色(G)和藍(lán)色(B)。
- HSV:是用色相H,飽和度S,明亮度V來(lái)描述顏色的變化。
- H:色相H取值范圍為0°~360°,從紅色開(kāi)始按逆時(shí)針?lè)较蛴?jì)算,紅色為0°,綠色為120°,藍(lán)色為240°。
- S:飽和度S越高,顏色則深而艷。光譜色的白光成分為0,飽和度達(dá)到最高。通常取值范圍為0%~100%,值越大,顏色越飽和。
- V:明度V表示顏色明亮的程度,對(duì)于光源色,明度值與發(fā)光體的光亮度有關(guān);對(duì)于物體色,此值和物體的透射比或反射比有關(guān)。通常取值范圍為0%(黑)到100%(白)。
// AdjustUtil.ets
// rgb轉(zhuǎn)hsv
function rgb2hsv(red: number, green: number, blue: number) {
let hsvH: number = 0, hsvS: number = 0, hsvV: number = 0;
const rgbR: number = colorTransform(red);
const rgbG: number = colorTransform(green);
const rgbB: number = colorTransform(blue);
const maxValue = Math.max(rgbR, Math.max(rgbG, rgbB));
const minValue = Math.min(rgbR, Math.min(rgbG, rgbB));
hsvV = maxValue * CommonConstants.CONVERT_INT;
if (maxValue === 0) {
hsvS = 0;
} else {
hsvS = Number((1 - minValue / maxValue).toFixed(CommonConstants.DECIMAL_TWO)) * CommonConstants.CONVERT_INT;
}
if (maxValue === minValue) {
hsvH = 0;
}
if (maxValue === rgbR && rgbG >= rgbB) {
hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbG - rgbB) / (maxValue - minValue)));
}
if (maxValue === rgbR && rgbG < rgbB) {
hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbG - rgbB) / (maxValue - minValue)) + CommonConstants.ANGLE_360);
}
if (maxValue === rgbG) {
hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbB - rgbR) / (maxValue - minValue)) + CommonConstants.ANGLE_120);
}
if (maxValue === rgbB) {
hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbR - rgbG) / (maxValue - minValue)) + CommonConstants.ANGLE_240);
}
return [hsvH, hsvS, hsvV];
}
// hsv轉(zhuǎn)rgb
function hsv2rgb(hue: number, saturation: number, value: number) {
let rgbR: number = 0, rgbG: number = 0, rgbB: number = 0;
if (saturation === 0) {
rgbR = rgbG = rgbB = Math.round((value * CommonConstants.COLOR_LEVEL_MAX) / CommonConstants.CONVERT_INT);
return { rgbR, rgbG, rgbB };
}
const cxmC = (value * saturation) / (CommonConstants.CONVERT_INT * CommonConstants.CONVERT_INT);
const cxmX = cxmC * (1 - Math.abs((hue / CommonConstants.ANGLE_60) % CommonConstants.MOD_2 - 1));
const cxmM = (value - cxmC * CommonConstants.CONVERT_INT) / CommonConstants.CONVERT_INT;
const hsvHRange = Math.floor(hue / CommonConstants.ANGLE_60);
switch (hsvHRange) {
case AngelRange.ANGEL_0_60:
rgbR = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbG = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbB = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
break;
case AngelRange.ANGEL_60_120:
rgbR = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbG = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbB = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
break;
case AngelRange.ANGEL_120_180:
rgbR = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbG = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbB = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
break;
case AngelRange.ANGEL_180_240:
rgbR = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbG = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbB = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
break;
case AngelRange.ANGEL_240_300:
rgbR = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbG = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbB = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
break;
case AngelRange.ANGEL_300_360:
rgbR = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbG = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
rgbB = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
break;
default:
break;
}
return [
Math.round(rgbR),
Math.round(rgbG),
Math.round(rgbB)
];
}
圖片裁剪
- 通過(guò)pixelMap獲取圖片尺寸,為后續(xù)裁剪做準(zhǔn)備。
- 確定裁剪的方式,當(dāng)前裁剪默認(rèn)有自由選取、1:1選取、4:3選取、16:9選取。
- 通過(guò)pixelMap調(diào)用接口crop()進(jìn)行裁剪操作。
說(shuō)明: 當(dāng)前裁剪功能采用pixelMap裁剪能力直接做切割,會(huì)有疊加效果,后續(xù)會(huì)通過(guò)增加選取框?qū)Ξ?dāng)前功能進(jìn)行優(yōu)化。
// HomePage.ets
cropImage(index: CropType) {
this.currentCropIndex = index;
switch (this.currentCropIndex) {
case CropType.ORIGINAL_IMAGE:
this.cropRatio = CropRatioType.RATIO_TYPE_FREE;
break;
case CropType.SQUARE:
this.cropRatio = CropRatioType.RATIO_TYPE_1_1;
break;
case CropType.BANNER:
this.cropRatio = CropRatioType.RATIO_TYPE_4_3;
break;
case CropType.RECTANGLE:
this.cropRatio = CropRatioType.RATIO_TYPE_16_9;
break;
default:
this.cropRatio = CropRatioType.RATIO_TYPE_FREE;
break;
}
}
// ImageFilterCrop.ets
cropImage(pixelMap: PixelMapWrapper, realCropRect: RectF, callback: () = > void) {
let offWidth = realCropRect.getWidth();
let offHeight = realCropRect.getHeight();
if (pixelMap.pixelMap!== undefined) {
pixelMap.pixelMap.crop({
size:{ height: vp2px(offHeight), width: vp2px(offWidth) },
x: vp2px(realCropRect.left),
y: vp2px(realCropRect.top)
}, callback);
}
}
圖片旋轉(zhuǎn)
- 確定旋轉(zhuǎn)方向,當(dāng)前支持順時(shí)針和逆時(shí)針旋轉(zhuǎn)。
- 通過(guò)pixelMap調(diào)用接口rotate()進(jìn)行旋轉(zhuǎn)操作。
// HomePage.ets
rotateImage(rotateType: RotateType) {
if (rotateType === RotateType.CLOCKWISE) {
try {
if (this.pixelMap !== undefined) {
this.pixelMap.rotate(CommonConstants.CLOCK_WISE)
.then(() = > {
this.flushPixelMapNew();
})
}
} catch (error) {
Logger.error(TAG, `there is a error in rotate process with ${error?.code}`);
}
}
if (rotateType === RotateType.ANTI_CLOCK) {
try {
if (this.pixelMap !== undefined) {
this.pixelMap.rotate(CommonConstants.ANTI_CLOCK)
.then(() = > {
this.flushPixelMapNew();
})
}
} catch (error) {
Logger.error(TAG, `there is a error in rotate process with ${error?.code}`);
}
}
}
亮度調(diào)節(jié)
- 將pixelMap轉(zhuǎn)換成ArrayBuffer。
- 將生成好的ArrayBuffer發(fā)送到worker線程。
- 對(duì)每一個(gè)像素點(diǎn)的亮度值按倍率計(jì)算。
- 將計(jì)算好的ArrayBuffer發(fā)送回主線程。
- 將ArrayBuffer寫入pixelMap,刷新UI。
說(shuō)明: 當(dāng)前亮度調(diào)節(jié)是在UI層面實(shí)現(xiàn)的,未實(shí)現(xiàn)細(xì)節(jié)優(yōu)化算法,只做簡(jiǎn)單示例。調(diào)節(jié)后的圖片會(huì)有色彩上的失真。
// AdjustContentView.ets
// 轉(zhuǎn)化成pixelMap及發(fā)送buffer到worker,返回?cái)?shù)據(jù)刷新ui
postToWorker(type: AdjustId, value: number, workerName: string) {
let sliderValue = type === AdjustId.BRIGHTNESS ? this.brightnessLastSlider : this.saturationLastSlider;
try {
let workerInstance = new worker.ThreadWorker(workerName);
const bufferArray = new ArrayBuffer(this.pixelMap.getPixelBytesNumber());
this.pixelMap.readPixelsToBuffer(bufferArray).then(() = > {
let message = new MessageItem(bufferArray, sliderValue, value);
workerInstance.postMessage(message);
if (this.postState) {
this.deviceListDialogController.open();
}
this.postState = false;
workerInstance.onmessage = (event: MessageEvents) = > {
this.updatePixelMap(event)
};
if (type === AdjustId.BRIGHTNESS) {
this.brightnessLastSlider = Math.round(value);
} else {
this.saturationLastSlider = Math.round(value);
}
workerInstance.onexit = () = > {
if (workerInstance !== undefined) {
workerInstance.terminate();
}
}
});
} catch (error) {
Logger.error(`Create work instance fail, error message: ${JSON.stringify(error)}`)
}
}
// AdjustBrightnessWork.ts
// worker線程處理部分
workerPort.onmessage = function(event : MessageEvents) {
let bufferArray = event.data.buf;
let last = event.data.last;
let cur = event.data.cur;
let buffer = adjustImageValue(bufferArray, last, cur);
workerPort.postMessage(buffer);
workerPort.close();
}
// AdjustUtil.ets
// 倍率計(jì)算部分
export function adjustImageValue(bufferArray: ArrayBuffer, last: number, cur: number) {
return execColorInfo(bufferArray, last, cur, HSVIndex.VALUE);
}
透明度調(diào)節(jié)
- 獲取pixelMap。
- 調(diào)用接口opacity()進(jìn)行透明度調(diào)節(jié)。
// OpacityUtil.ets
export async function adjustOpacity(pixelMap: PixelMap, value: number) {
if (!pixelMap) {
return;
}
const newPixelMap = pixelMap;
await newPixelMap.opacity(value / CommonConstants.SLIDER_MAX);
return newPixelMap;
}
飽和度調(diào)節(jié)
- 將pixelMap轉(zhuǎn)換成ArrayBuffer。
- 將生成好的ArrayBuffer發(fā)送到worker線程。
- 對(duì)每一個(gè)像素點(diǎn)的飽和度按倍率計(jì)算。
- 將計(jì)算好的ArrayBuffer發(fā)送回主線程。
- 將ArrayBuffer寫入pixelMap,刷新UI。
說(shuō)明: 當(dāng)前飽和度調(diào)節(jié)是在UI層面實(shí)現(xiàn)的,未實(shí)現(xiàn)細(xì)節(jié)優(yōu)化算法,只做簡(jiǎn)單示例。調(diào)節(jié)后的圖片會(huì)有色彩上的失真。
// AdjustContentView.ets
// 轉(zhuǎn)化成pixelMap及發(fā)送buffer到worker,返回?cái)?shù)據(jù)刷新ui
postToWorker(type: AdjustId, value: number, workerName: string) {
let sliderValue = type === AdjustId.BRIGHTNESS ? this.brightnessLastSlider : this.saturationLastSlider;
try {
let workerInstance = new worker.ThreadWorker(workerName);
const bufferArray = new ArrayBuffer(this.pixelMap.getPixelBytesNumber());
this.pixelMap.readPixelsToBuffer(bufferArray).then(() = > {
let message = new MessageItem(bufferArray, sliderValue, value);
workerInstance.postMessage(message);
if (this.postState) {
this.deviceListDialogController.open();
}
this.postState = false;
workerInstance.onmessage = (event: MessageEvents) = > {
this.updatePixelMap(event)
};
if (type === AdjustId.BRIGHTNESS) {
this.brightnessLastSlider = Math.round(value);
} else {
this.saturationLastSlider = Math.round(value);
}
workerInstance.onexit = () = > {
if (workerInstance !== undefined) {
workerInstance.terminate();
}
}
});
} catch (error) {
Logger.error(`Create work instance fail, error message: ${JSON.stringify(error)}`);
}
}
// AdjustSaturationWork.ts
// worker線程處理部分
workerPort.onmessage = function(event : MessageEvents) {
let bufferArray = event.data.buf;
let last = event.data.last;
let cur = event.data.cur;
let buffer = adjustSaturation(bufferArray, last, cur)
workerPort.postMessage(buffer);
workerPort.close();
}
// AdjustUtil.ets
// 倍率計(jì)算部分
export function adjustSaturation(bufferArray: ArrayBuffer, last: number, cur: number) {
return execColorInfo(bufferArray, last, cur, HSVIndex.SATURATION);
}
圖片編碼
圖片位圖經(jīng)過(guò)處理之后,還是屬于解碼的狀態(tài),還需要進(jìn)行打包編碼成對(duì)應(yīng)的格式,本章講解編碼的具體過(guò)程。
- 通過(guò)image組件創(chuàng)建打包工具packer。
- 使用PackingOption進(jìn)行打包參數(shù)設(shè)定,比如格式、壓縮質(zhì)量等。
- 打包成圖片信息數(shù)據(jù)imageData。
- 創(chuàng)建媒體庫(kù)media,獲取公共路徑。
- 創(chuàng)建媒體文件asset,獲取其fd。
- 使用fs將打包好的圖片數(shù)據(jù)寫入到媒體文件asset中。
// ImageSelect.ets
async encode(pixelMap: PixelMap | undefined) {
if (pixelMap === undefined) {
return;
}
const newPixelMap = pixelMap;
// 打包圖片
const imagePackerApi = image.createImagePacker();
const packOptions: image.PackingOption = {
format: CommonConstants.ENCODE_FORMAT,
quality: CommonConstants.ENCODE_QUALITY
}
const imageData = await imagePackerApi.packing(newPixelMap, packOptions);
Logger.info(TAG, `imageData's length is ${imageData.byteLength}`);
// 獲取相冊(cè)路徑
const context = getContext(this);
const media = mediaLibrary.getMediaLibrary(context);
const publicPath = await media.getPublicDirectory(mediaLibrary.DirectoryType.DIR_IMAGE);
const currentTime = new Date().getTime();
// 創(chuàng)建圖片資源
const imageAssetInfo = await media.createAsset(
mediaLibrary.MediaType.IMAGE,
`${CommonConstants.IMAGE_PREFIX}_${currentTime}${CommonConstants.IMAGE_FORMAT}`,
publicPath
);
const imageFd = await imageAssetInfo.open(CommonConstants.ENCODE_FILE_PERMISSION);
await fs.write(imageFd, imageData);
// 釋放資源
await imageAssetInfo.close(imageFd);
imagePackerApi.release();
await media.release();
}
審核編輯 黃宇
-
鴻蒙
+關(guān)注
關(guān)注
57文章
2410瀏覽量
43291 -
HarmonyOS
+關(guān)注
關(guān)注
79文章
1987瀏覽量
31076 -
OpenHarmony
+關(guān)注
關(guān)注
25文章
3768瀏覽量
17020
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
51單片機(jī)應(yīng)用開(kāi)發(fā)案例精選(代碼及圖片)
HarmonyOS IoT 硬件開(kāi)發(fā)案例分享
【HarmonyOS HiSpark Wi-Fi IoT套件】HarmonyOS IoT 硬件開(kāi)發(fā)案例分享
【潤(rùn)和直播課預(yù)告@華為開(kāi)發(fā)者學(xué)院】HarmonyOS設(shè)備開(kāi)發(fā)基礎(chǔ)課程|HiSpark WiFi-IoT 智能小車套件開(kāi)發(fā)案例
HarmonyOS教程—基于圖片處理能力,實(shí)現(xiàn)一個(gè)圖片編輯模板
51單片機(jī)應(yīng)用開(kāi)發(fā)案例精選-源代碼

許思維老師HarmonyOS IoT硬件開(kāi)發(fā)案例分享

華為開(kāi)發(fā)者HarmonyOS零基礎(chǔ)入門:Word圖片資源支持預(yù)覽效果

華為開(kāi)發(fā)者分論壇HarmonyOS學(xué)生公開(kāi)課-OpenHarmony Codelabs開(kāi)發(fā)案例

OpenHarmony上實(shí)現(xiàn)圖片編輯功能

HarmonyOS開(kāi)發(fā)實(shí)例:【圖片編輯應(yīng)用】

評(píng)論