/*
* Copyright (c) 2024 Huawei Device Co., Ltd.
* Licensed under the Apache License,Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { worker } from '@kit.ArkTS';
import { taskpool } from '@kit.ArkTS';
import { image } from '@kit.ImageKit';
import { adjustIconList, IconStatus } from '../viewModel/IconListViewModel';
import { execColorInfo } from '../utils/AdjustUtil';
import { AdjustId } from '../viewModel/OptionViewModel';
import { MessageItem } from '../viewModel/MessageItem';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { CommonConstants } from '../common/constant/CommonConstants';
const TAG = `AdjustContentView`;
@Component
export default struct AdjustContentView {
@State currentAdjustIndex: number = 0;
@Consume('currentAdjustData') currentAdjustData: Array<number>;
adjustIconList: Array<IconStatus> = adjustIconList;
@Builder
TabBuilder(index: number, name: ResourceStr) {
Column() {
Row() {
Image(this.currentAdjustIndex === index ? this.adjustIconList[index]?.chosen :
this.adjustIconList[index]?.normal)
.width(24)
.height(24)
}
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.Center)
.backgroundColor('#333333')
.borderRadius(20)
.width(40)
.height(40)
Text(name)
.fontColor(this.currentAdjustIndex === index ? '#006CDE' : Color.White)
.fontSize(10)
.padding({ top: 8 })
}
.width('100%')
}
build() {
Tabs({ barPosition: BarPosition.End }) {
TabContent() {
Column() {
SliderCustom({
currentIndex: AdjustId.BRIGHTNESS.valueOf(),
min: 1,
max: 100,
currentAdjustData: this.currentAdjustData
})
}
.justifyContent(FlexAlign.End)
.height('100%')
.padding({
bottom: 24
})
}
.tabBar(this.TabBuilder(AdjustId.BRIGHTNESS, $r('app.string.brightness')))
TabContent() {
Column() {
SliderCustom({
currentIndex: AdjustId.TRANSPARENCY.valueOf(),
min: 1,
max: 100,
currentAdjustData: this.currentAdjustData
})
}
.justifyContent(FlexAlign.End)
.height('100%')
.padding({
bottom: 24
})
}
.tabBar(this.TabBuilder(AdjustId.TRANSPARENCY, $r('app.string.transparency')))
TabContent() {
Column() {
SliderCustom({
currentIndex: AdjustId.SATURATION.valueOf(),
min: 1,
max: 100,
currentAdjustData: this.currentAdjustData
})
}
.justifyContent(FlexAlign.End)
.height('100%')
.padding({
bottom: 24
})
}
.tabBar(this.TabBuilder(AdjustId.SATURATION, $r('app.string.saturation')))
}
.barHeight(60)
.padding({ bottom: 24 })
.onChange((index: number) => {
this.currentAdjustIndex = index;
})
}
}
@Component
struct SliderCustom {
@Prop currentIndex: number;
@Link currentAdjustData: number[];
@Prop min: number;
@Prop max: number;
@Consume('pixelMap') pixelMap?: image.PixelMap;
@Consume('isPixelMapChange') isPixelMapChange: boolean;
private startEditPixelMap?: PixelMap;
private postState: boolean = true;
saturationLastSlider: number = 100;
brightnessLastSlider: number = 100;
deviceListDialogController: CustomDialogController = new CustomDialogController({
builder: Dialog(),
alignment: DialogAlignment.Center,
autoCancel: false,
customStyle: true
});
aboutToAppear(): void {
this.startEditPixelMap = this.pixelMap;
}
build() {
Column() {
Text(`${this.currentAdjustData[this.currentIndex]}`)
.fontColor(Color.White)
.margin({ top: -24 })
.fontSize(16)
Row() {
Slider({
value: this.currentAdjustData[this.currentIndex],
step: 10,
min: this.min,
max: this.max
})
.height(20)
.blockColor(Color.White)
.selectedColor('#E6E6E6')
.trackColor('#333333')
.width('100%')
.showSteps(true)
.padding({
right: 60,
left: 60
})
.onChange((value: number, mode: SliderChangeMode) => {
this.sliderChange(value > this.max ? this.max : value, mode);
})
}
.width('100%')
.justifyContent(FlexAlign.End)
}
}
// [Start handleImage]
// ImageEditTaskPool/entry/src/main/ets/view/AdjustContentView.ets
async sliderChange(value: number, mode: SliderChangeMode) {
// [StartExclude handleImage]
if ((mode === SliderChangeMode.End) && (value !== this.currentAdjustData[this.currentIndex])) {
if (this.postState) {
this.deviceListDialogController.open();
}
this.postState = false;
this.currentAdjustData[this.currentIndex] = Math.round(value);
const px = this.getStartEditPixelMap();
let buffer = new ArrayBuffer(px.getPixelBytesNumber());
px.readPixelsToBufferSync(buffer);
// [EndExclude handleImage]
const needBrightness = this.currentAdjustData[AdjustId.BRIGHTNESS] !== CommonConstants.SLIDER_MAX;
const needSaturation = this.currentAdjustData[AdjustId.SATURATION] !== CommonConstants.SLIDER_MAX;
if (needBrightness || needSaturation) {
try {
if (needBrightness) {
buffer = await this.execImageProcessing(buffer, AdjustId.BRIGHTNESS, this.currentAdjustData[AdjustId.BRIGHTNESS]);
}
if (needSaturation) {
buffer = await this.execImageProcessing(buffer, AdjustId.SATURATION, this.currentAdjustData[AdjustId.SATURATION]);
}
px.writeBufferToPixelsSync(buffer);
} catch (err) {
let error = err as BusinessError;
hilog.error(0x0000, TAG, `${error.code}, ${error.message}`);
}
}
if (this.currentAdjustData[AdjustId.TRANSPARENCY] !== CommonConstants.SLIDER_MAX) {
const opacity = this.currentAdjustData[AdjustId.TRANSPARENCY] / CommonConstants.SLIDER_MAX;
try {
px.opacitySync(opacity);
} catch (err) {
let error = err as BusinessError;
hilog.error(0x0000, TAG, `${error.code}, ${error.message}`);
}
}
// [StartExclude handleImage]
this.pixelMap = px;
this.isPixelMapChange = !this.isPixelMapChange;
this.deviceListDialogController.close();
this.postState = true;
// [EndExclude handleImage]
}
}
// [End handleImage]
// [Start execImageProcessing]
private async execImageProcessing(buffer: ArrayBuffer, type: AdjustId, value: number): Promise<ArrayBuffer> {
const buffers = splitArrayBuffer(buffer, 240);
const group = splitTask(buffers, type, value);
try {
return mergeArrayBuffers(await taskpool.execute(group, taskpool.Priority.HIGH) as ArrayBuffer[]);
} catch (err) {
let error = err as BusinessError;
hilog.error(0x0000, TAG, `${error.code}, ${error.message}`);
return buffer;
}
}
// [End execImageProcessing]
// [Start postProcess_start]
// ImageEditTaskPool/entry/src/main/ets/view/AdjustContentView.ets
async postProcess(type: AdjustId, value: number) {
// [StartExclude postProcess_start]
if (!this.pixelMap) {
return;
}
let sliderValue = type === AdjustId.BRIGHTNESS ? this.brightnessLastSlider : this.saturationLastSlider;
const bufferArray = new ArrayBuffer(this.pixelMap.getPixelBytesNumber());
// [EndExclude postProcess_start]
this.pixelMap.readPixelsToBuffer(bufferArray)
.then(() => {
const buffers: ArrayBuffer[] = splitArrayBuffer(bufferArray, 240);
const group = splitTask(buffers, type, value);
// [StartExclude postProcess_start]
// [Start execute_start]
taskpool.execute(group, taskpool.Priority.HIGH).catch((err: BusinessError) => {
hilog.error(0x0000, 'AdjustContentView', 'Failed to execute taskpool: ', JSON.stringify(err) ?? '');
}).then((ret) => {
// Combine the results of each task execution
const entireArrayBuffer = mergeArrayBuffers(ret as Object[]);
// Update the UI based on the calculation results
this.updatePixelMap(entireArrayBuffer);
});
// [End execute_start]
if (this.postState) {
this.deviceListDialogController.open();
}
this.postState = false;
if (type === AdjustId.BRIGHTNESS) {
this.brightnessLastSlider = Math.round(value);
} else {
this.saturationLastSlider = Math.round(value);
}
// [EndExclude postProcess_start]
})
}
// [StartExclude postProcess_start]
updatePixelMap(ret: ArrayBuffer) {
const newPixel = this.pixelMap as image.PixelMap;
newPixel.writeBufferToPixels(ret);
this.pixelMap = newPixel;
this.isPixelMapChange = !this.isPixelMapChange;
this.deviceListDialogController.close();
this.postState = true;
}
// [EndExclude postProcess_start]
// [End postProcess_start]
getStartEditPixelMap() {
return this.clonePixelMap(this.startEditPixelMap as image.PixelMap);
}
clonePixelMap(pixelMap: PixelMap, desiredPixelFormat?: image.PixelMapFormat): PixelMap {
try {
const imageInfo = pixelMap.getImageInfoSync();
const buffer = new ArrayBuffer(pixelMap.getPixelBytesNumber());
pixelMap.readPixelsToBufferSync(buffer);
const options: image.InitializationOptions = {
srcPixelFormat: imageInfo.pixelFormat,
pixelFormat: desiredPixelFormat ?? imageInfo.pixelFormat,
size: imageInfo.size
};
return image.createPixelMapSync(buffer, options);
} catch (err) {
let error = err as BusinessError;
hilog.error(0x0000, 'testTag', `${error.code}, ${error.message}`);
return pixelMap;
}
}
}
@CustomDialog
export struct Dialog {
controller?: CustomDialogController;
build() {
Column() {
LoadingProgress()
.color(Color.White)
.width('30%')
.height('30%')
}
}
}
// [Start postProcess_start]
// [Start execImageProcessing]
/**
* Each task processes a portion of the pixel data and adds the task to the task group.
*
*/
function splitTask(buffers: ArrayBuffer[], type: AdjustId, value: number): taskpool.TaskGroup {
// Creating a Task Group
let group: taskpool.TaskGroup = new taskpool.TaskGroup();
for (const buffer of buffers) {
try {
group.addTask(imageProcessing, {
// Add a task to a task group
value: value,
buffer: buffer,
type: type
});
} catch (err) {
hilog.error(0x0000, 'AdjustContentView', 'Failed to add the task: ', JSON.stringify(err) ?? '');
}
}
return group;
}
// [End execImageProcessing]
// [End postProcess_start]
@Concurrent
async function imageProcessing(args: ImageProcessing) {
const buf = execColorInfo(args.buffer, CommonConstants.SLIDER_MAX, args.value, args.type) as ArrayBuffer;
return buf;
}
interface ImageProcessing {
type: AdjustId;
buffer: ArrayBuffer;
value: number;
}
// [Start split_buffer1]
//Split the picture pixel data ArrayBuffer according to the number of tasks N.
function splitArrayBuffer(buffer: ArrayBuffer, taskCount: number): ArrayBuffer[] {
const BYTES_PER_PIXEL = 4; // RGBA
const bytesPerTask = Math.floor(buffer.byteLength / taskCount / BYTES_PER_PIXEL) * BYTES_PER_PIXEL;
let result: ArrayBuffer[] = [];
for (let i = 0; i < taskCount; i++) {
if (i === taskCount - 1) {
// The final block contains all the remaining data
result[i] = buffer.slice(i * bytesPerTask);
} else {
result[i] = buffer.slice(i * bytesPerTask, (i + 1) * bytesPerTask);
}
}
return result;
}
// [StartExclude split_buffer1]
function buffer(bufferArray: ESObject, taskNum: number, sliderValue: number, value: number) {
let Workers: ESObject
// [EndExclude split_buffer1]
// Assign the split pixels to the Worker instance.
const buffers: ArrayBuffer[] = splitArrayBuffer(bufferArray, taskNum);
let messages: MessageItem[] = [];
for (let i = 0; i < taskNum; i++) { // Encapsulating corresponding task data according to the number of tasks.
let message = new MessageItem(buffers[i], sliderValue, value); //Construct task message
messages.push(message);
}
let n: number = 0;
let allocation: number = taskNum; // Number of tasks to be assigned
for (let index = 0; index < taskNum; index++) {
Workers[index].postMessage(messages[n]); // Distribute the task to the corresponding Worker child thread instance.
allocation = allocation - 1; // Number of remaining tasks to be assigned
n += 1;
}
// [End split_buffer1]
}
function mergeArrayBuffers(buffers: Object[]) {
let thisBuffers = buffers as ArrayBuffer[];
// Calculate the combined total length
let totalLength = thisBuffers.reduce((length, buffer) => {
length += buffer.byteLength;
return length;
}, 0);
// Create a new ArrayBuffer
let mergedBuffer = new ArrayBuffer(totalLength);
// Create a Uint8Array to operate the new Uint8Array
let mergedArray = new Uint8Array(mergedBuffer);
let offset = 0;
for (let buffer of thisBuffers) {
let array = new Uint8Array(buffer);
mergedArray.set(array, offset);
offset += array.length;
}
return mergedBuffer;
}
function taskNum(WorkerName: string) {
// [Start task1]
let taskNum: number = 14; // The number of concurrent tasks is controlled, which can be adjusted according to the demand.
let curTaskNum: number = taskNum <= 64 ? taskNum : 64; // Control allows up to 64 Worker instances to run at the same time.
let Workers: worker.ThreadWorker[] = [];
for (let i = 0; i < curTaskNum; i++) { // Control the number of instantiations of the Worker according to the limit.
let WorkerInstance = new worker.ThreadWorker(WorkerName);
Workers.push(WorkerInstance);
}
// [End task1]
}