1/* 2 * Copyright (c) 2023 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import { StringUtil } from '../../utils/StringUtil'; 17import { Log } from '../../utils/Log'; 18import { Constants } from './Constants' 19import { ImageUtil } from '../../utils/ImageUtil'; 20import userFileManager from '@ohos.filemanagement.userFileManager'; 21import image from '@ohos.multimedia.image'; 22import { BrowserDataFactory } from '../../interface/BrowserDataFactory'; 23import { TraceControllerUtils } from '../../utils/TraceControllerUtils'; 24import { WorkerThreadPool } from './WorkerThreadPool'; 25import { MessageEvents } from '@ohos.worker'; 26 27const TAG = 'HistogramManager'; 28 29/** 30 * PREPARING: 直方图数据准备中 31 * READY: 直方图数据准备完毕,可以开始绘制 32 */ 33export enum HistogramStatus { 34 PREPARING, 35 READY, 36 LOADING_FINISHED 37} 38 39/** 40 * 直方图工具类 41 * 42 * 步骤: 43 * 1、使用前必须初始化三个属性: context, width, height 44 * 2、startDraw() 开始执行绘制 45 * 3、使用完毕,需要执行clear()清空绘制内容 46 * 47 */ 48export class HistogramManager { 49 private static histogramManagerInstance: HistogramManager; 50 private width: number; 51 private height: number; 52 private context: CanvasRenderingContext2D = undefined; 53 private pixelArray: Uint32Array; 54 private startTime: number; 55 private endTime: number; 56 57 private constructor() { 58 } 59 60 public static getInstance(): HistogramManager { 61 if (!this.histogramManagerInstance) { 62 Log.info(TAG, 'instance create'); 63 this.histogramManagerInstance = new HistogramManager(); 64 } 65 return this.histogramManagerInstance; 66 } 67 68 public getContext(): CanvasRenderingContext2D { 69 return this.context; 70 } 71 72 public setContext(ctx: CanvasRenderingContext2D): HistogramManager { 73 this.context = ctx; 74 this.context.globalCompositeOperation = 'lighter' 75 this.context.lineWidth = Constants.HISTOGRAM_LINE_WIDTH; 76 return this; 77 } 78 79 public clear(): void { 80 Log.info(TAG, 'clear context and pixel array'); 81 this.context && this.context.clearRect(0, 0, this.width, this.height); 82 this.pixelArray = undefined; 83 this.startTime = 0; 84 this.endTime = 0; 85 } 86 87 public setWidth(width: number): HistogramManager { 88 this.width = width; 89 return this; 90 } 91 92 public getWidth(): number { 93 return this.width; 94 } 95 96 public setHeight(height: number): HistogramManager { 97 this.height = height; 98 return this; 99 } 100 101 /** 102 * 图像解码,读取pixel map存入ArrayBuffer 103 * @param mediaItem 104 */ 105 public async extractPixels(uri: string): Promise<ArrayBuffer> { 106 TraceControllerUtils.startTrace('extractPixels'); 107 this.startTime = Date.now(); 108 Log.info(TAG, 'extract pixels, start time: ' + this.startTime); 109 let dataImpl = BrowserDataFactory.getFeature(BrowserDataFactory.TYPE_PHOTO); 110 let fileAsset: userFileManager.FileAsset = await dataImpl.getDataByUri(uri); 111 if (!fileAsset) { 112 Log.error(TAG, 'get file asset failed.'); 113 return null; 114 } 115 let size: image.Size = { 116 width: fileAsset.get(userFileManager.ImageVideoKey.WIDTH.toString()) as number, 117 height: fileAsset.get(userFileManager.ImageVideoKey.HEIGHT.toString()) as number 118 } 119 // 修改size到option采样尺寸 120 ImageUtil.getScreenNailSize(size); 121 let pixelMap: PixelMap = await fileAsset.getThumbnail(size); 122 if (!pixelMap) { 123 Log.error(TAG, 'get pixel map failed'); 124 return null; 125 } 126 Log.debug(TAG, 'create pixel map success'); 127 let pixelBuffer: ArrayBuffer; 128 await pixelMap.getImageInfo().then((imageInfo) => { 129 Log.info(TAG, `image info get from pixelmap: ${JSON.stringify(imageInfo)}`); 130 pixelBuffer = new ArrayBuffer(imageInfo.size.height * imageInfo.size.width * Constants.RGBA_CHANNELS); 131 }); 132 Log.debug(TAG, 'read pixels to arraybuffer start'); 133 await pixelMap.readPixelsToBuffer(pixelBuffer); 134 Log.debug(TAG, 'read pixels to arraybuffer end'); 135 pixelMap.release(); 136 TraceControllerUtils.finishTrace('extractPixels'); 137 return pixelBuffer; 138 } 139 140 /** 141 * 直方图数据准备,利用worker执行耗时操作 142 * @param itemId 143 * @param mediaType 144 */ 145 async dataPrepare(uri: string): Promise<void> { 146 Log.info(TAG, 'photo detail, histogram start processing'); 147 let pixelBuffer = await this.extractPixels(uri); 148 TraceControllerUtils.startTrace('dataPrepare'); 149 let resultArray = new Uint32Array(Constants.PIXEL_NUMBER * Constants.RGB_CHANNELS); 150 let counter = WorkerThreadPool.CAPACITY; 151 152 // 定义worker主线程回调函数 153 let mainThreadCallback = function (e: MessageEvents, name: string): void { 154 Log.info(TAG, 'histogram main thread onMessage, received from ' + name); 155 let arrayBuffer: ArrayBuffer = e.data; 156 let channelArray = new Uint32Array(arrayBuffer); 157 // 三通道计算结果累加 158 resultArray = resultArray.map(function (value, index): number { 159 return value + channelArray[index]; 160 }) 161 Log.info(TAG, `multiworker ${name} array byteLength ${resultArray.byteLength}`); 162 counter--; 163 if (counter == Constants.NUMBER_0) { 164 TraceControllerUtils.finishTrace('dataPrepare'); 165 Log.info(TAG, `histogram workers data process finished`); 166 // 重置status,防止被上次任务回调干扰 167 AppStorage.SetOrCreate<number>(Constants.HISTOGRAM_READY_STATUS_KEY, HistogramStatus.PREPARING); 168 // arrayBuffer直接传入,会丢失类型,需要转为string 169 Log.info(TAG, 'serialize the pixel array for histogram draw, pixel array length: ' + resultArray.length); 170 AppStorage.SetOrCreate<string>(Constants.HISTOGRAM_ARRAY_BUFFER_KEY, StringUtil.arraybufferSerialize(resultArray.buffer)); 171 AppStorage.SetOrCreate<number>(Constants.HISTOGRAM_READY_STATUS_KEY, HistogramStatus.READY); 172 } 173 } 174 175 let workerPath = '/ets/workers/HistogramWorker.ts'; 176 WorkerThreadPool.getInstance().run(workerPath, pixelBuffer, Constants.RGBA_CHANNELS, mainThreadCallback); 177 } 178 179 /** 180 * 直方图绘制 181 */ 182 public startDraw(): void { 183 Log.info(TAG, 'data prepare finished, start draw histogram'); 184 TraceControllerUtils.startTrace('startDraw'); 185 this.initializePixelArray(); 186 const startIndex = 0; 187 this.drawSingleHistogram(this.pixelArray.slice(startIndex, Constants.HISTOGRAM_CONSTANT_256), Constants.HISTOGRAM_RED_FILL_COLOR); 188 this.drawSingleHistogram(this.pixelArray.slice(Constants.HISTOGRAM_CONSTANT_256, Constants.HISTOGRAM_CONSTANT_512), Constants.HISTOGRAM_GREEN_FILL_COLOR); 189 this.drawSingleHistogram(this.pixelArray.slice(Constants.HISTOGRAM_CONSTANT_512), Constants.HISTOGRAM_BLUE_FILL_COLOR); 190 this.internalStroke(); 191 this.endTime = Date.now(); 192 TraceControllerUtils.finishTrace('startDraw'); 193 Log.info(TAG, 'histogram time cost: ' + (this.endTime - this.startTime) + 'ms'); 194 } 195 196 /** 197 * 接收worker返回的数据 198 */ 199 private initializePixelArray(): void { 200 Log.info(TAG, 'get pixel array by deserialize'); 201 let serializedBuffer = AppStorage.get<string>(Constants.HISTOGRAM_ARRAY_BUFFER_KEY); 202 if (!serializedBuffer) { 203 Log.error(TAG, 'initializePixelArray: serializedBuffer is undefined'); 204 return; 205 } 206 // 反序列化 207 let arrayBuffer: ArrayBuffer = StringUtil.arraybufferDeserialize(serializedBuffer); 208 this.pixelArray = new Uint32Array(arrayBuffer); 209 Log.info(TAG, 'pixel array length: ' + this.pixelArray.length); 210 } 211 212 /** 213 * 单通道直方图绘制 214 * @param array 215 * @param color 216 */ 217 private drawSingleHistogram(array: Uint32Array, color: string): void { 218 let width = this.width; 219 let height = this.height; 220 const start_axis: number = 0; 221 let peakPoint: number = Math.max(...array); 222 if (peakPoint === 0) { 223 Log.warn(TAG, 'peakPoint is zero'); 224 return; 225 } 226 let x_step = width / Constants.PIXEL_NUMBER; 227 let y_step = height / peakPoint; 228 let region = new Path2D(); 229 // 移动到起始位置 230 region.moveTo(start_axis, height); 231 let last = 0; 232 for (let pixelValue = 0; pixelValue < array.length; pixelValue++) { 233 let v1 = pixelValue * x_step; 234 let v2 = y_step * array[pixelValue]; 235 let value = height - v2; 236 region.lineTo(v1, value); 237 last = v1; 238 } 239 region.lineTo(last, height); 240 region.lineTo(width, height); 241 region.lineTo(start_axis, height); // 回到原点,形成闭包 242 region.closePath(); 243 244 // 线条颜色 245 this.context.strokeStyle = Constants.HISTOGRAM_REGION_STROKE_COLOR; 246 this.context.stroke(region); 247 248 // 闭包填充颜色 249 this.context.fillStyle = color; 250 this.context.fill(region, "nonzero"); 251 } 252 253 /** 254 * 边框和内部描边 255 */ 256 private internalStroke(): void { 257 // 恢复叠加模式到初始值,避免叠加导致边框和线条被填充色掩盖 258 this.context.globalCompositeOperation = 'source-over'; 259 // 绘制边框 260 this.context.strokeStyle = Constants.HISTOGRAM_INTERNAL_STROKE_COLOR; 261 this.context.lineWidth = Constants.HISTOGRAM_STROKE_WIDTH; 262 let rectStartX: number = 0; 263 let rectStartY: number = 0; 264 this.context.strokeRect(rectStartX, rectStartY, this.width, this.height); 265 266 // 竖直两个线条 267 this.context.beginPath(); 268 this.context.strokeStyle = Constants.HISTOGRAM_INTERNAL_STROKE_COLOR; 269 // stroke线条实际宽度被边缘裁切一半,线条宽度要除以2与其保持一致 270 this.context.lineWidth = Constants.HISTOGRAM_STROKE_WIDTH / Constants.NUMBER_2; 271 let lineStartY: number = 0; 272 this.context.moveTo(this.width / Constants.HISTOGRAM_DIVIDER_REGION_NUM, lineStartY); 273 this.context.lineTo(this.width / Constants.HISTOGRAM_DIVIDER_REGION_NUM, this.height); 274 this.context.stroke(); 275 276 this.context.beginPath(); 277 this.context.strokeStyle = Constants.HISTOGRAM_INTERNAL_STROKE_COLOR; 278 this.context.moveTo(this.width * Constants.HISTOGRAM_DIVIDE_LINE_NUM / 279 Constants.HISTOGRAM_DIVIDER_REGION_NUM, lineStartY); 280 this.context.lineTo(this.width * Constants.HISTOGRAM_DIVIDE_LINE_NUM / 281 Constants.HISTOGRAM_DIVIDER_REGION_NUM, this.height); 282 this.context.stroke(); 283 } 284}