@react-native-ohos/react-native-image-crop-picker 0.40.4 → 0.40.5-rc.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,795 @@
1
+ import { image } from '@kit.ImageKit';
2
+ import Matrix4 from '@ohos.matrix4'
3
+ import fs from '@ohos.file.fs';
4
+ import router from '@ohos.router';
5
+ import display from '@ohos.display';
6
+ import { Constants } from '../utils/Constants';
7
+ import { getPixelMap } from '../utils/DecodeAndEncodeUtil';
8
+ import { RotateType } from '../viewmodel/viewAndModel';
9
+ import { encodeToPng } from '../utils/EncodeUtil';
10
+ import Logger from '../Logger';
11
+ import { CircleImageProcessor } from '../utils/CircleImageProcessor';
12
+
13
+ const TAG: string = 'CircleImageEditInfo';
14
+
15
+ @Extend(Row)
16
+ function bottomIconStyle() {
17
+ .size({ width: '20%', height: '100%' })
18
+ .justifyContent(FlexAlign.Center)
19
+ }
20
+
21
+ @Extend(Text)
22
+ function textStyle() {
23
+ .size({ width: '20%', height: '100%' })
24
+ .textAlign(TextAlign.Center)
25
+ }
26
+
27
+ @Extend(Image)
28
+ function iconStyle() {
29
+ .size({ width: 24, height: 24 })
30
+ .fillColor('#FFFFFF')
31
+ .opacity(0.9)
32
+ }
33
+
34
+ @Component
35
+ export struct CropView {
36
+ @StorageProp('filePath') filePath: string = '';
37
+ @StorageProp('cropperRotate') cropperRotate: string = '';
38
+ @StorageProp('initWidth') initWidth: number = 0;
39
+ @StorageProp('initHeight') initHeight: number = 0;
40
+ @StorageProp('textTitle') title: string = '';
41
+ @StorageProp('cancelText') cancel: string = '';
42
+ @StorageProp('cancelTextColor') cancelTextColor: string = '';
43
+ @StorageProp('chooseText') choose: string = '';
44
+ @StorageProp('chooseTextColor') chooseTextColor: string = '';
45
+ @StorageProp('freeStyleCropEnabled') freeStyleCropEnabled: boolean = false;
46
+ @StorageProp('cropperCircleOverlay') cropperCircleOverlay: boolean = false;
47
+ @StorageProp('enableRotationGesture') enableRotationGesture: boolean = false;
48
+ @State private model: CropModel = new CropModel();
49
+ private settings: RenderingContextSettings = new RenderingContextSettings(true);
50
+ private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
51
+ @State pm: PixelMap | undefined = undefined;
52
+ @State private matrix: object = Matrix4.identity()
53
+ .translate({ x: 0, y: 0 })
54
+ .scale({ x: this.model.scale, y: this.model.scale });
55
+ @State screenWidth: number = 0;
56
+ @State screenHeight: number = 0;
57
+ @State imageWidth: number = 0;
58
+ @State imageHeight: number = 0;
59
+ @State angle: number = 0;
60
+ @State gestureAngle: number = 0
61
+ private tempScale = 1;
62
+ private startOffsetX: number = 0;
63
+ private startOffsetY: number = 0;
64
+ private initialAngle: number = 0;
65
+ private initialScale: number = 1;
66
+ private initialOffsetX: number = 0;
67
+ private initialOffsetY: number = 0;
68
+ private initialImageWidth: number = 0;
69
+ private initialImageHeight: number = 0;
70
+ private isImageAnimation: boolean = true
71
+ private isStackAnimation: boolean = true
72
+ @StorageProp('bottomRectHeight')
73
+ bottomRectHeight: number = 0;
74
+ @StorageProp('topRectHeight')
75
+ topRectHeight: number = 0;
76
+
77
+ aboutToAppear(): void {
78
+ this.screenHeight = this.getUIContext().px2vp(display.getDefaultDisplaySync().height);
79
+ this.filePath = AppStorage.Get('filePath') || ''
80
+ this.model.setImage(this.filePath)
81
+ getPixelMap(this.filePath)
82
+ .then((pixelMap: image.PixelMap) => {
83
+ if (pixelMap) {
84
+ this.model.src = pixelMap;
85
+ pixelMap.getImageInfo().then((imageInfo) => {
86
+ this.imageHeight = imageInfo.size == undefined ? 0 : imageInfo.size?.height;
87
+ this.imageWidth = imageInfo.size == undefined ? 0 : imageInfo.size?.width;
88
+ this.initialImageWidth = this.imageWidth;
89
+ this.initialImageHeight = this.imageHeight;
90
+ })
91
+ }
92
+ })
93
+ }
94
+
95
+ @Builder
96
+ CropTitleBar() {
97
+ Row() {
98
+ Text(this.title)
99
+ .fontColor(Color.White)
100
+ .fontSize(20)
101
+ .margin(5)
102
+ }
103
+ .justifyContent(FlexAlign.Center)
104
+ .width('100%')
105
+ .position({ x: 0, y: 0 })
106
+ .backgroundColor(Color.Black)
107
+ .padding({
108
+ top: this.getUIContext().px2vp(this.topRectHeight)
109
+ })
110
+ }
111
+
112
+ @Builder
113
+ BottomToolbar() {
114
+ Row() {
115
+ Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.Center }) {
116
+ Text(this.cancel)
117
+ .textStyle()
118
+ .fontColor(this.cancelTextColor)
119
+ .onClick(async () => {
120
+ router.back()
121
+ });
122
+ Row() {
123
+ Image($r('app.media.ic_anti_clockwise'))
124
+ .iconStyle()
125
+ .visibility(this.cropperRotate === 'true' ? Visibility.Hidden : Visibility.Visible)
126
+ .onClick(async () => {
127
+ this.rotateImage(RotateType.ANTI_CLOCK);
128
+ });
129
+ }
130
+ .bottomIconStyle();
131
+
132
+ Row() {
133
+ Image($r('app.media.ic_reset'))
134
+ .iconStyle()
135
+ .onClick(() => {
136
+ this.resetAllTransformations();
137
+ });
138
+ }
139
+ .bottomIconStyle();
140
+
141
+ Row() {
142
+ Image($r('app.media.ic_clockwise'))
143
+ .iconStyle()
144
+ .visibility(this.cropperRotate === 'true' ? Visibility.Hidden : Visibility.Visible)
145
+ .onClick(() => {
146
+ this.rotateImage(RotateType.CLOCKWISE);
147
+ });
148
+ }
149
+ .bottomIconStyle();
150
+
151
+ Text(this.choose)
152
+ .textStyle()
153
+ .fontColor(this.chooseTextColor)
154
+ .onClick(async () => {
155
+ if (this.model.src) {
156
+ try {
157
+ // 进行普通的正方形裁剪
158
+ const squarePm = await this.model.crop(image.PixelMapFormat.RGBA_8888);
159
+
160
+ // 对裁剪后的图片应用旋转
161
+ const normalizedAngle = ((this.angle % 360) + 360) % 360;
162
+ if (normalizedAngle !== 0) {
163
+ await squarePm.rotate(normalizedAngle);
164
+ }
165
+
166
+ // 转换为真正的圆形图片
167
+ const circlePm = await CircleImageProcessor.createTrueCircleImage(squarePm);
168
+
169
+ // 保存为PNG格式(支持透明度)
170
+ let imgPath = await encodeToPng(this, circlePm);
171
+
172
+ // 清理资源
173
+ squarePm.release();
174
+ circlePm.release();
175
+
176
+ // 保存路径
177
+ AppStorage.setOrCreate('cropImagePath', imgPath);
178
+ AppStorage.setOrCreate('isCircleImage', true);
179
+ router.back()
180
+ } catch (error) {
181
+ Logger.error(TAG, "Circle crop failed:" + JSON.stringify(error));
182
+ }
183
+ }
184
+ })
185
+ }
186
+ .width('100%')
187
+ .height(56)
188
+ .backgroundColor(Color.Black)
189
+ .margin({ bottom: 30 })
190
+ }
191
+ .position({ x: 0, y: (this.screenHeight - 80) })
192
+ .backgroundColor(Color.Black)
193
+ .padding({
194
+ bottom: this.getUIContext().px2vp(this.bottomRectHeight)
195
+ })
196
+ }
197
+
198
+ build() {
199
+ Stack() {
200
+ Stack() {
201
+ Image(this.model.src)
202
+ .width('100%')
203
+ .height('100%')
204
+ .alt(this.model.previewSource)
205
+ .objectFit(ImageFit.Contain)
206
+ .transform(this.matrix)
207
+ .animation({ duration: this.isImageAnimation ? 300 : 0 })
208
+ .onComplete((msg) => {
209
+ if (msg) {
210
+ // 图片加载成功
211
+ this.model.imageWidth = msg.width;
212
+ this.model.imageHeight = msg.height;
213
+ this.model.componentWidth = msg.componentWidth;
214
+ this.model.componentHeight = msg.componentHeight;
215
+ this.saveInitialState();
216
+ this.checkImageAdapt();
217
+ if (this.model.imageLoadEventListener != null && msg.loadingStatus == 1) {
218
+ this.model.imageLoadEventListener.onImageLoaded(msg);
219
+ }
220
+ }
221
+ })
222
+ .onError((error) => {
223
+ if (this.model.imageLoadEventListener != null) {
224
+ this.model.imageLoadEventListener.onImageLoadError(error);
225
+ }
226
+ })
227
+ }
228
+ .rotate({ angle: this.angle })
229
+ .width('100%')
230
+ .height('100%')
231
+ .animation({ duration: this.isStackAnimation ? 300 : 0 })
232
+ .priorityGesture(
233
+ TapGesture({ count: 2, fingers: 1 })
234
+ .onAction((event: GestureEvent) => {
235
+ if (!event) {
236
+ return
237
+ }
238
+ if (this.model.zoomEnabled) {
239
+ if (this.model.scale != 1) {
240
+ this.model.scale = 1;
241
+ this.updateMatrix();
242
+ } else {
243
+ this.zoomTo(2);
244
+ }
245
+ }
246
+
247
+ this.checkImageAdapt();
248
+ })
249
+ )
250
+ .gesture(
251
+ GestureGroup(GestureMode.Parallel,
252
+ // 拖动手势
253
+ PanGesture({})
254
+ .onActionStart(() => {
255
+ Logger.info(TAG, "CropView Pan gesture start");
256
+ this.startOffsetX = this.model.offsetX;
257
+ this.startOffsetY = this.model.offsetY;
258
+ this.isImageAnimation = false
259
+ })
260
+ .onActionUpdate((event: GestureEvent) => {
261
+ Logger.info(TAG, "CropView Pan gesture update" + JSON.stringify(event));
262
+ if (event) {
263
+ if (this.model.panEnabled) {
264
+ let distanceX: number = this.startOffsetX + vp2px(event.offsetX) / this.model.scale;
265
+ let distanceY: number = this.startOffsetY + vp2px(event.offsetY) / this.model.scale;
266
+ this.model.offsetX = distanceX;
267
+ this.model.offsetY = distanceY;
268
+ this.updateMatrix()
269
+ }
270
+ }
271
+ })
272
+ .onActionEnd(() => {
273
+ Logger.info(TAG, "CropView Pan gesture end");
274
+ this.checkImageAdapt();
275
+ this.isImageAnimation = true
276
+ }),
277
+ // 缩放手势
278
+ PinchGesture({ fingers: 2 })
279
+ .onActionStart(() => {
280
+ this.isStackAnimation = false
281
+ this.tempScale = this.model.scale
282
+ })
283
+ .onActionUpdate((event) => {
284
+ if (event) {
285
+ if (!this.model.zoomEnabled) {
286
+ return;
287
+ }
288
+ this.zoomTo(Math.min(5.0, Math.max(0.1, (this.tempScale * event.scale))))
289
+ }
290
+ })
291
+ .onActionEnd((event) => {
292
+ this.checkImageAdapt()
293
+ this.updateMatrix()
294
+ this.isStackAnimation = true
295
+ }),
296
+ //旋转手势
297
+ RotationGesture({})
298
+ .onActionStart(() => {
299
+ if (this.enableRotationGesture) {
300
+ // 记录手势起始
301
+ this.gestureAngle = this.angle
302
+ // 关闭动画避免旋转错误
303
+ this.isStackAnimation = false
304
+ }
305
+ })
306
+ .onActionUpdate((event: GestureEvent) => {
307
+ if (this.enableRotationGesture) {
308
+ let normalizedAngle = event.angle
309
+ if (normalizedAngle < 0) {
310
+ normalizedAngle += 360
311
+ }
312
+ this.angle = this.gestureAngle + normalizedAngle
313
+ }
314
+ })
315
+ .onActionEnd(() => {
316
+ if (this.enableRotationGesture) {
317
+ this.isStackAnimation = false
318
+ const tmp = this.angle % 360
319
+ this.angle = tmp
320
+ let time = setTimeout(() => {
321
+ this.isStackAnimation = true
322
+ this.angle = closestAngle
323
+ this.updateMatrix()
324
+ this.checkImageAdapt()
325
+ clearTimeout(time)
326
+ }, 50)
327
+
328
+ // 计算最近的吸附角度
329
+ const snapAngles = [0, 90, 180, 270, 360]
330
+ let closestAngle = snapAngles[0]
331
+ let minDiff = Infinity
332
+
333
+ for (const snapAngle of snapAngles) {
334
+ const diff = Math.abs(tmp - snapAngle)
335
+ if (diff < minDiff) {
336
+ minDiff = diff
337
+ closestAngle = snapAngle
338
+ }
339
+ }
340
+ }
341
+ })
342
+ )
343
+ )
344
+
345
+ Canvas(this.context)
346
+ .width('100%')
347
+ .height('100%')
348
+ .backgroundColor(Color.Transparent)
349
+ .onReady(() => {
350
+ if (this.context == null) {
351
+ return
352
+ }
353
+ let height = this.context.height
354
+ let width = this.context.width
355
+ this.context.fillStyle = this.model.maskColor;
356
+ this.context.fillRect(0, 0, width, height)
357
+ let centerX = width / 2;
358
+ let centerY = height / 2;
359
+ this.context.globalCompositeOperation = 'destination-out'
360
+ this.context.fillStyle = 'white'
361
+ let frameWidthInVp = px2vp(this.model.frameWidth);
362
+ let frameHeightInVp = px2vp(this.model.getFrameHeight());
363
+ this.context.beginPath();
364
+ this.context.arc(centerX, centerY, px2vp(this.model.frameWidth / 2), 0, 2 * Math.PI);
365
+ this.context.fill();
366
+ this.context.globalCompositeOperation = 'source-over';
367
+ this.context.strokeStyle = this.model.strokeColor;
368
+ let radius = Math.min(frameWidthInVp, frameHeightInVp) / 2;
369
+ this.context.beginPath();
370
+ this.context.arc(centerX, centerY, radius, 0, 2 * Math.PI);
371
+ this.context.closePath();
372
+ this.context.lineWidth = 1;
373
+ this.context.stroke();
374
+ })
375
+ .enabled(false)
376
+
377
+ this.CropTitleBar()
378
+
379
+ this.BottomToolbar()
380
+ }
381
+ .backgroundColor(Color.Black)
382
+ }
383
+
384
+ /**
385
+ * 检查手势操作后,图片是否填满取景框,没填满则进行调整
386
+ */
387
+ private checkImageAdapt() {
388
+ let offsetX = this.model.offsetX;
389
+ let offsetY = this.model.offsetY;
390
+ let scale = this.model.scale;
391
+ Logger.info(TAG, "CropView offsetX: " + offsetX + ", offsetY: " + offsetY + ", scale: " + scale);
392
+
393
+ // 图片适配控件的时候也进行了缩放,计算出这个缩放比例
394
+ let widthScale = this.model.componentWidth / this.model.imageWidth;
395
+ let heightScale = this.model.componentHeight / this.model.imageHeight;
396
+ let adaptScale = Math.min(widthScale, heightScale);
397
+ Logger.info(TAG,
398
+ "CropView Image scale: " + adaptScale + "while attaching the component[" + this.model.componentWidth + ", " +
399
+ this.model.componentHeight);
400
+
401
+ // 经过两次缩放(适配控件、手势)后,图片的实际显示大小
402
+ let showWidth = this.model.imageWidth * adaptScale * this.model.scale;
403
+ let showHeight = this.model.imageHeight * adaptScale * this.model.scale;
404
+ let imageX = (this.model.componentWidth - showWidth) / 2;
405
+ let imageY = (this.model.componentHeight - showHeight) / 2;
406
+ Logger.info(TAG, "CropView Image left top is: (" + imageX + ", " + imageY + ")");
407
+
408
+ // 取景框的左上角坐标
409
+ let frameX = (this.model.componentWidth - this.model.frameWidth) / 2;
410
+ let frameY = (this.model.componentHeight - this.model.getFrameHeight()) / 2;
411
+
412
+ // 图片左上角坐标
413
+ let showX = imageX + offsetX * scale;
414
+ let showY = imageY + offsetY * scale;
415
+ Logger.info(TAG, "CropView Image show at (" + showX + ", " + showY + ")");
416
+
417
+ if (this.model.frameWidth > showWidth || this.model.getFrameHeight() > showHeight) { // 图片缩放后,大小不足以填满取景框
418
+ let xScale = this.model.frameWidth / showWidth;
419
+ let yScale = this.model.getFrameHeight() / showHeight;
420
+ let newScale = Math.max(xScale, yScale);
421
+ this.model.scale = this.model.scale * newScale;
422
+ showX *= newScale;
423
+ showY *= newScale;
424
+ }
425
+
426
+ // 调整x轴方向位置,使图像填满取景框
427
+ if (showX > frameX) {
428
+ showX = frameX;
429
+ } else if (showX + showWidth < frameX + this.model.frameWidth) {
430
+ showX = frameX + this.model.frameWidth - showWidth;
431
+ }
432
+ // 调整y轴方向位置,使图像填满取景框
433
+ if (showY > frameY) {
434
+ showY = frameY;
435
+ } else if (showY + showHeight < frameY + this.model.getFrameHeight()) {
436
+ showY = frameY + this.model.getFrameHeight() - showHeight;
437
+ }
438
+ this.model.offsetX = (showX - imageX) / scale;
439
+ this.model.offsetY = (showY - imageY) / scale;
440
+ this.updateMatrix();
441
+ }
442
+
443
+ public zoomTo(scale: number): void {
444
+ this.model.scale = scale;
445
+ this.updateMatrix();
446
+ }
447
+
448
+ public updateMatrix(): void {
449
+ this.matrix = Matrix4.identity()
450
+ .translate({ x: this.model.offsetX, y: this.model.offsetY })
451
+ .scale({ x: this.model.scale, y: this.model.scale })
452
+ }
453
+
454
+ public rotateImage(rotateType: RotateType) {
455
+ Logger.info(TAG, "into rotateImage rotateType : " + rotateType)
456
+ if (rotateType === RotateType.CLOCKWISE) {
457
+ if (!this.model.src) {
458
+ Logger.info(TAG, "into rotateImage return")
459
+ return;
460
+ }
461
+ try {
462
+ this.model.src.rotate(Constants.CLOCK_WISE)
463
+ .then(() => {
464
+ this.angle = this.angle + Constants.CLOCK_WISE;
465
+ Logger.info(TAG, `into rotateImage Constants.CLOCK_WISE return ${this.angle}`,)
466
+ })
467
+ } catch (error) {
468
+ }
469
+ }
470
+ if (rotateType === RotateType.ANTI_CLOCK) {
471
+ if (!this.model.src) {
472
+ Logger.info(TAG, "into rotateImage return")
473
+ return;
474
+ }
475
+ try {
476
+ this.model.src.rotate(Constants.ANTI_CLOCK)
477
+ .then(() => {
478
+ this.angle = this.angle + Constants.ANTI_CLOCK;
479
+ Logger.info(TAG, `into rotateImage Constants.ANTI_CLOCK return ${this.angle}`,)
480
+ })
481
+ } catch (error) {
482
+ }
483
+ }
484
+ }
485
+
486
+ // 保存初始状态方法
487
+ private saveInitialState(): void {
488
+ this.initialAngle = this.angle;
489
+ this.initialScale = this.model.scale;
490
+ this.initialOffsetX = this.model.offsetX;
491
+ this.initialOffsetY = this.model.offsetY;
492
+ Logger.info(TAG, "Initial state saved: " +
493
+ `angle=${this.initialAngle}, scale=${this.initialScale}, offsetX=${this.initialOffsetX}, offsetY=${this.initialOffsetY}`);
494
+ }
495
+
496
+ // 重置所有变换的方法
497
+ private resetAllTransformations(): void {
498
+ Logger.info(TAG, "Resetting all transformations...");
499
+
500
+ // 重置位置和缩放
501
+ this.model.scale = this.initialScale;
502
+ this.model.offsetX = this.initialOffsetX;
503
+ this.model.offsetY = this.initialOffsetY;
504
+
505
+ // 重置旋转角度
506
+ if (this.angle !== this.initialAngle) {
507
+ let angleDifference = this.initialAngle - this.angle;
508
+
509
+ // 如果有PixelMap且需要旋转
510
+ if (this.model.src && angleDifference !== 0) {
511
+ angleDifference = angleDifference % 360
512
+ // 异步旋转图片
513
+ this.model.src.rotate(angleDifference)
514
+ .then(() => {
515
+ this.angle = this.initialAngle;
516
+ Logger.info(TAG, `Image rotated back by ${angleDifference} degrees`);
517
+ this.updateMatrix();
518
+ this.checkImageAdapt();
519
+ })
520
+ .catch((error: Error) => {
521
+ Logger.error(TAG, "Failed to reset rotation: " + JSON.stringify(error));
522
+ // 即使旋转失败,也更新UI状态
523
+ this.angle = this.initialAngle;
524
+ this.updateMatrix();
525
+ this.checkImageAdapt();
526
+ });
527
+ } else {
528
+ // 无需旋转或没有PixelMap,直接更新
529
+ this.angle = this.initialAngle;
530
+ this.updateMatrix();
531
+ this.checkImageAdapt();
532
+ }
533
+ } else {
534
+ // 角度相同,直接更新
535
+ this.updateMatrix();
536
+ this.checkImageAdapt();
537
+ }
538
+
539
+ // 重置图片宽高到初始值
540
+ if (this.imageWidth !== this.initialImageWidth || this.imageHeight !== this.initialImageHeight) {
541
+ this.imageWidth = this.initialImageWidth;
542
+ this.imageHeight = this.initialImageHeight;
543
+ }
544
+
545
+ Logger.info(TAG, "All transformations have been reset: " +
546
+ `angle=${this.angle}, scale=${this.model.scale}, offsetX=${this.model.offsetX}, offsetY=${this.model.offsetY}`);
547
+ }
548
+ }
549
+
550
+ interface ImageLoadedEvent {
551
+ width: number;
552
+ height: number;
553
+ componentWidth: number;
554
+ componentHeight: number;
555
+ loadingStatus: number;
556
+ contentWidth: number;
557
+ contentHeight: number;
558
+ contentOffsetX: number;
559
+ contentOffsetY: number;
560
+ }
561
+
562
+ export interface ImageLoadEventListener {
563
+
564
+ onImageLoaded(msg: ImageLoadedEvent): void;
565
+
566
+ onImageLoadError(error: ImageError): void;
567
+ }
568
+
569
+ export class CropModel {
570
+ /**
571
+ * PixelMap图片对象
572
+ *
573
+ */
574
+ src?: image.PixelMap = undefined;
575
+ /**
576
+ * 图片地址
577
+ */
578
+ path: string = '';
579
+ /**
580
+ * 图片预览
581
+ */
582
+ previewSource: string | Resource = '';
583
+ /**
584
+ * 是否可以拖动
585
+ */
586
+ panEnabled: boolean = true;
587
+ /**
588
+ * 是否可以缩放
589
+ */
590
+ zoomEnabled: boolean = true;
591
+ /**
592
+ * 取景框宽度
593
+ */
594
+ frameWidth = 1000;
595
+ /**
596
+ * 取景框宽高比
597
+ */
598
+ frameRatio = 1;
599
+ /**
600
+ * 遮罩颜色
601
+ */
602
+ maskColor: string = '#AA000000';
603
+ /**
604
+ * 取景框边框颜色
605
+ */
606
+ strokeColor: string = '#FFFFFF';
607
+ /**
608
+ * 图片加载监听
609
+ */
610
+ imageLoadEventListener: ImageLoadEventListener | null = null;
611
+ /**
612
+ * 图片宽度
613
+ */
614
+ imageWidth: number = 0;
615
+ /**
616
+ * 图片高度
617
+ */
618
+ imageHeight: number = 0;
619
+ /**
620
+ * 控件宽度
621
+ */
622
+ componentWidth: number = 0;
623
+ /**
624
+ * 控件高度
625
+ */
626
+ componentHeight: number = 0;
627
+ /**
628
+ * 手势缩放比例
629
+ */
630
+ scale: number = 1;
631
+ /**
632
+ * x轴方向偏移量
633
+ */
634
+ offsetX: number = 0;
635
+ /**
636
+ * y轴方向偏移量
637
+ */
638
+ offsetY: number = 0;
639
+
640
+ public setImage(path: string, previewSource?: string | Resource): CropModel {
641
+ this.path = path;
642
+ if (!!previewSource) {
643
+ this.previewSource = previewSource;
644
+ }
645
+ return this;
646
+ }
647
+
648
+ public setScale(scale: number): CropModel {
649
+ this.scale = scale;
650
+ return this;
651
+ }
652
+
653
+ public isPanEnabled(): boolean {
654
+ return this.panEnabled;
655
+ }
656
+
657
+ public setPanEnabled(panEnabled: boolean): CropModel {
658
+ this.panEnabled = panEnabled;
659
+ return this;
660
+ }
661
+
662
+ public setZoomEnabled(zoomEnabled: boolean): CropModel {
663
+ this.zoomEnabled = zoomEnabled;
664
+ return this;
665
+ }
666
+
667
+ public setFrameWidth(frameWidth: number): CropModel {
668
+ this.frameWidth = frameWidth;
669
+ return this;
670
+ }
671
+
672
+ public setFrameRatio(frameRatio: number): CropModel {
673
+ this.frameRatio = frameRatio;
674
+ return this;
675
+ }
676
+
677
+ public setMaskColor(color: string): CropModel {
678
+ this.maskColor = color;
679
+ return this;
680
+ }
681
+
682
+ public setStrokeColor(color: string): CropModel {
683
+ this.strokeColor = color;
684
+ return this;
685
+ }
686
+
687
+ public setImageLoadEventListener(listener: ImageLoadEventListener): CropModel {
688
+ this.imageLoadEventListener = listener;
689
+ return this;
690
+ }
691
+
692
+ public getScale(): number {
693
+ return this.scale;
694
+ }
695
+
696
+ public isZoomEnabled(): boolean {
697
+ return this.zoomEnabled;
698
+ }
699
+
700
+ public getImageWidth(): number {
701
+ return this.imageWidth;
702
+ }
703
+
704
+ public getImageHeight(): number {
705
+ return this.imageHeight;
706
+ }
707
+
708
+ public getFrameHeight() {
709
+ return this.frameWidth / this.frameRatio;
710
+ }
711
+
712
+ public reset(): void {
713
+ this.scale = 1;
714
+ this.offsetX = 0;
715
+ this.offsetY = 0;
716
+ }
717
+
718
+ public async crop(format: image.PixelMapFormat): Promise<image.PixelMap> {
719
+
720
+ if (!this.path || this.path == '') {
721
+ throw new Error('Please set path first');
722
+ }
723
+ if (this.imageWidth == 0 || this.imageHeight == 0) {
724
+ throw new Error('The image is not loaded');
725
+ }
726
+
727
+ // 图片适配控件的时候也进行了缩放,计算出这个缩放比例
728
+ let widthScale = this.componentWidth / this.imageWidth;
729
+ let heightScale = this.componentHeight / this.imageHeight;
730
+ let adaptScale = Math.min(widthScale, heightScale);
731
+
732
+ // 经过两次缩放(适配控件、手势)后,图片的实际显示大小
733
+ let totalScale = adaptScale * this.scale;
734
+ let showWidth = this.imageWidth * totalScale;
735
+ let showHeight = this.imageHeight * totalScale;
736
+ let imageX = (this.componentWidth - showWidth) / 2;
737
+ let imageY = (this.componentHeight - showHeight) / 2;
738
+
739
+ // 取景框的左上角坐标
740
+ let frameX = (this.componentWidth - this.frameWidth) / 2;
741
+ let frameY = (this.componentHeight - this.getFrameHeight()) / 2;
742
+
743
+ // 图片左上角坐标
744
+ let showX = imageX + this.offsetX * this.scale;
745
+ let showY = imageY + this.offsetY * this.scale;
746
+
747
+ let x = (frameX - showX) / totalScale;
748
+ let y = (frameY - showY) / totalScale;
749
+ let file = fs.openSync(this.path, fs.OpenMode.READ_ONLY)
750
+ let imageSource: image.ImageSource = image.createImageSource(file.fd);
751
+ let decodingOptions: image.DecodingOptions = {
752
+ editable: true,
753
+ desiredPixelFormat: image.PixelMapFormat.BGRA_8888,
754
+ }
755
+
756
+ // 创建pixelMap
757
+ let pm = await imageSource.createPixelMap(decodingOptions);
758
+ let cp = await this.copyPixelMap(pm);
759
+ pm.release();
760
+ let region: image.Region =
761
+ { x: x, y: y, size: { width: this.frameWidth / totalScale, height: this.getFrameHeight() / totalScale } };
762
+ cp.cropSync(region);
763
+ return cp;
764
+ }
765
+
766
+ async copyPixelMap(pm: PixelMap): Promise<PixelMap> {
767
+ const imageInfo: image.ImageInfo = await pm.getImageInfo();
768
+ const buffer: ArrayBuffer = new ArrayBuffer(pm.getPixelBytesNumber());
769
+ await pm.readPixelsToBuffer(buffer);
770
+ const opts: image.InitializationOptions = {
771
+ editable: true,
772
+ pixelFormat: image.PixelMapFormat.RGBA_8888,
773
+ size: { height: imageInfo.size.height, width: imageInfo.size.width }
774
+ };
775
+ return image.createPixelMap(buffer, opts);
776
+ }
777
+ }
778
+
779
+ @Entry
780
+ @Component
781
+ export struct CircleImageInfo {
782
+ @State private model: CropModel = new CropModel();
783
+
784
+ build() {
785
+ Column() {
786
+ CropView({
787
+ model: this.model,
788
+ })
789
+ .layoutWeight(1)
790
+ .width('100%')
791
+ }
792
+ .height('100%')
793
+ .width('100%')
794
+ }
795
+ }