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, 171 StringUtil.arraybufferSerialize(resultArray.buffer)); 172 AppStorage.setOrCreate<number>(Constants.HISTOGRAM_READY_STATUS_KEY, HistogramStatus.READY); 173 } 174 } 175 176 let workerPath = '/ets/workers/HistogramWorker.ts'; 177 WorkerThreadPool.getInstance().run(workerPath, pixelBuffer, Constants.RGBA_CHANNELS, mainThreadCallback); 178 } 179 180 /** 181 * 直方图绘制 182 */ 183 public startDraw(): void { 184 Log.info(TAG, 'data prepare finished, start draw histogram'); 185 TraceControllerUtils.startTrace('startDraw'); 186 this.initializePixelArray(); 187 const startIndex = 0; 188 this.drawSingleHistogram(this.pixelArray.slice(startIndex, Constants.HISTOGRAM_CONSTANT_256), 189 Constants.HISTOGRAM_RED_FILL_COLOR); 190 this.drawSingleHistogram(this.pixelArray.slice(Constants.HISTOGRAM_CONSTANT_256, 191 Constants.HISTOGRAM_CONSTANT_512), Constants.HISTOGRAM_GREEN_FILL_COLOR); 192 this.drawSingleHistogram(this.pixelArray.slice(Constants.HISTOGRAM_CONSTANT_512), 193 Constants.HISTOGRAM_BLUE_FILL_COLOR); 194 this.internalStroke(); 195 this.endTime = Date.now(); 196 TraceControllerUtils.finishTrace('startDraw'); 197 Log.info(TAG, 'histogram time cost: ' + (this.endTime - this.startTime) + 'ms'); 198 } 199 200 /** 201 * 接收worker返回的数据 202 */ 203 private initializePixelArray(): void { 204 Log.info(TAG, 'get pixel array by deserialize'); 205 let serializedBuffer = AppStorage.get<string>(Constants.HISTOGRAM_ARRAY_BUFFER_KEY); 206 if (!serializedBuffer) { 207 Log.error(TAG, 'initializePixelArray: serializedBuffer is undefined'); 208 return; 209 } 210 // 反序列化 211 let arrayBuffer: ArrayBuffer = StringUtil.arraybufferDeserialize(serializedBuffer); 212 this.pixelArray = new Uint32Array(arrayBuffer); 213 Log.info(TAG, 'pixel array length: ' + this.pixelArray.length); 214 } 215 216 /** 217 * 单通道直方图绘制 218 * @param array 219 * @param color 220 */ 221 private drawSingleHistogram(array: Uint32Array, color: string): void { 222 let width = this.width; 223 let height = this.height; 224 const startAxis: number = 0; 225 let peakPoint: number = Math.max(...array); 226 if (peakPoint === 0) { 227 Log.warn(TAG, 'peakPoint is zero'); 228 return; 229 } 230 let xStep = width / Constants.PIXEL_NUMBER; 231 let yStep = height / peakPoint; 232 let region = new Path2D(); 233 // 移动到起始位置 234 region.moveTo(start_axis, height); 235 let last = 0; 236 for (let pixelValue = 0; pixelValue < array.length; pixelValue++) { 237 let v1 = pixelValue * xStep; 238 let v2 = yStep * array[pixelValue]; 239 let value = height - v2; 240 region.lineTo(v1, value); 241 last = v1; 242 } 243 region.lineTo(last, height); 244 region.lineTo(width, height); 245 region.lineTo(start_axis, height); // 回到原点,形成闭包 246 region.closePath(); 247 248 // 线条颜色 249 this.context.strokeStyle = Constants.HISTOGRAM_REGION_STROKE_COLOR; 250 this.context.stroke(region); 251 252 // 闭包填充颜色 253 this.context.fillStyle = color; 254 this.context.fill(region, 'nonzero'); 255 } 256 257 /** 258 * 边框和内部描边 259 */ 260 private internalStroke(): void { 261 // 恢复叠加模式到初始值,避免叠加导致边框和线条被填充色掩盖 262 this.context.globalCompositeOperation = 'source-over'; 263 // 绘制边框 264 this.context.strokeStyle = Constants.HISTOGRAM_INTERNAL_STROKE_COLOR; 265 this.context.lineWidth = Constants.HISTOGRAM_STROKE_WIDTH; 266 let rectStartX: number = 0; 267 let rectStartY: number = 0; 268 this.context.strokeRect(rectStartX, rectStartY, this.width, this.height); 269 270 // 竖直两个线条 271 this.context.beginPath(); 272 this.context.strokeStyle = Constants.HISTOGRAM_INTERNAL_STROKE_COLOR; 273 // stroke线条实际宽度被边缘裁切一半,线条宽度要除以2与其保持一致 274 this.context.lineWidth = Constants.HISTOGRAM_STROKE_WIDTH / Constants.NUMBER_2; 275 let lineStartY: number = 0; 276 this.context.moveTo(this.width / Constants.HISTOGRAM_DIVIDER_REGION_NUM, lineStartY); 277 this.context.lineTo(this.width / Constants.HISTOGRAM_DIVIDER_REGION_NUM, this.height); 278 this.context.stroke(); 279 280 this.context.beginPath(); 281 this.context.strokeStyle = Constants.HISTOGRAM_INTERNAL_STROKE_COLOR; 282 this.context.moveTo(this.width * Constants.HISTOGRAM_DIVIDE_LINE_NUM / 283 Constants.HISTOGRAM_DIVIDER_REGION_NUM, lineStartY); 284 this.context.lineTo(this.width * Constants.HISTOGRAM_DIVIDE_LINE_NUM / 285 Constants.HISTOGRAM_DIVIDER_REGION_NUM, this.height); 286 this.context.stroke(); 287 } 288}