• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}