@react-native-ohos/react-native-image-crop-picker 0.50.2-rc.3 → 0.50.2-rc.5
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.
- package/CHANGELOG.md +127 -5
- package/harmony/image_crop_picker/index.ets +1 -0
- package/harmony/image_crop_picker/oh-package.json5 +1 -1
- package/harmony/image_crop_picker/src/main/ets/ImageCropPickerTurboModule.ts +9 -1
- package/harmony/image_crop_picker/src/main/ets/pages/CircleImageInfo.ets +789 -0
- package/harmony/image_crop_picker/src/main/ets/utils/CircleImageProcessor.ets +125 -0
- package/harmony/image_crop_picker/src/main/ets/utils/Constants.ets +4 -0
- package/harmony/image_crop_picker/src/main/ets/utils/EncodeUtil.ets +21 -0
- package/harmony/image_crop_picker/src/main/resources/base/profile/main_pages.json +2 -1
- package/harmony/image_crop_picker.har +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,129 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
-
## 0.50.2-rc.3
|
|
3
|
-
pre-release version 0.50.2-rc.3
|
|
4
2
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
## 鸿蒙化Log
|
|
4
|
+
|
|
5
|
+
### v0.50.2-rc.2
|
|
6
|
+
|
|
7
|
+
- Fix the issue where the file size after compression is larger than the original file.
|
|
8
|
+
|
|
9
|
+
### v0.50.1
|
|
10
|
+
|
|
11
|
+
- react-native-image-crop-picker仓库迁移 ([e09dff16cc4c51bfdcec842921d3c30d49d39a19](https://gitee.com/openharmony-sig/rntpc_react-native-image-crop-picker/pulls/1))
|
|
12
|
+
- chore: add COMMITTERS.md ([6e71f31b17e5aba5a60215b58ddc6f656def74f9](https://gitee.com/openharmony-sig/rntpc_react-native-image-crop-picker/pulls/2))
|
|
13
|
+
- pre-release: @react-native-ohos/react-native-image-crop-picker@0.50.2-rc.2([db47a7fb5b3960c0e05a93f83edb13a5113eade2](https://gitee.com/openharmony-sig/rntpc_react-native-image-crop-picker/pulls/4))
|
|
14
|
+
|
|
15
|
+
## ReleaseLog
|
|
16
|
+
|
|
17
|
+
### v0.50.0
|
|
18
|
+
|
|
19
|
+
#### What's Changed
|
|
20
|
+
|
|
21
|
+
- fix: Resolve OutOfMemoryError in image/video processing by [@GeeksEra](https://github.com/GeeksEra) in [#2137](https://github.com/ivpusic/react-native-image-crop-picker/pull/2137)
|
|
22
|
+
- Update ImageCropPicker.m by [@uranashel44](https://github.com/uranashel44) in [#2129](https://github.com/ivpusic/react-native-image-crop-picker/pull/2129)
|
|
23
|
+
- [BugFix - ios] Keep selection order by [@sean5940](https://github.com/sean5940) in [#2099](https://github.com/ivpusic/react-native-image-crop-picker/pull/2099)
|
|
24
|
+
- Fix the issure in Android where the photo picker does not support a maxFiles selection limit. by [@xgtz421](https://github.com/xgtz421) in [#2147](https://github.com/ivpusic/react-native-image-crop-picker/pull/2147)
|
|
25
|
+
- If the "maxFiles" keyword is missing, assign a default value of 5 to align with the documentation. by [@xgtz421](https://github.com/xgtz421) in [#2148](https://github.com/ivpusic/react-native-image-crop-picker/pull/2148)
|
|
26
|
+
- New Architecture support by [@ivpusic](https://github.com/ivpusic) in [#2171](https://github.com/ivpusic/react-native-image-crop-picker/pull/2171)
|
|
27
|
+
|
|
28
|
+
#### New Contributors
|
|
29
|
+
|
|
30
|
+
- [@GeeksEra](https://github.com/GeeksEra) made their first contribution in [#2137](https://github.com/ivpusic/react-native-image-crop-picker/pull/2137)
|
|
31
|
+
- [@uranashel44](https://github.com/uranashel44) made their first contribution in [#2129](https://github.com/ivpusic/react-native-image-crop-picker/pull/2129)
|
|
32
|
+
- [@xgtz421](https://github.com/xgtz421) made their first contribution in [#2147](https://github.com/ivpusic/react-native-image-crop-picker/pull/2147)
|
|
33
|
+
|
|
34
|
+
### v0.42.0
|
|
35
|
+
|
|
36
|
+
#### What's Changed
|
|
37
|
+
|
|
38
|
+
- Implement the new official Android Photo Picker by [@Pauligrinder](https://github.com/Pauligrinder) in [#2093](https://github.com/ivpusic/react-native-image-crop-picker/pull/2093)
|
|
39
|
+
|
|
40
|
+
#### New Contributors
|
|
41
|
+
|
|
42
|
+
- [@Pauligrinder](https://github.com/Pauligrinder) made their first contribution in [#2093](https://github.com/ivpusic/react-native-image-crop-picker/pull/2093)
|
|
43
|
+
|
|
44
|
+
### v0.41.6
|
|
45
|
+
|
|
46
|
+
#### What's Changed
|
|
47
|
+
|
|
48
|
+
- Enhancement:: Add: Filename in response object for Android by [@AmeyShrivastava](https://github.com/AmeyShrivastava) in [#1945](https://github.com/ivpusic/react-native-image-crop-picker/pull/1945)
|
|
49
|
+
- Fix openCamera reported dimensions for portrait photos on Android by [@gaearon](https://github.com/gaearon) in [#2110](https://github.com/ivpusic/react-native-image-crop-picker/pull/2110)
|
|
50
|
+
|
|
51
|
+
#### New Contributors
|
|
52
|
+
|
|
53
|
+
- [@AmeyShrivastava](https://github.com/AmeyShrivastava) made their first contribution in [#1945](https://github.com/ivpusic/react-native-image-crop-picker/pull/1945)
|
|
54
|
+
- [@gaearon](https://github.com/gaearon) made their first contribution in [#2110](https://github.com/ivpusic/react-native-image-crop-picker/pull/2110)
|
|
55
|
+
|
|
56
|
+
### v0.41.5
|
|
57
|
+
|
|
58
|
+
#### What's Changed
|
|
59
|
+
|
|
60
|
+
- fix: [iOS] Image is being resized up instead of down & Error "User did not grant library permission." in Android 10 by [@xhirazi](https://github.com/xhirazi) in [#2103](https://github.com/ivpusic/react-native-image-crop-picker/pull/2103)
|
|
61
|
+
|
|
62
|
+
#### New Contributors
|
|
63
|
+
|
|
64
|
+
- [@xhirazi](https://github.com/xhirazi) made their first contribution in [#2103](https://github.com/ivpusic/react-native-image-crop-picker/pull/2103)
|
|
65
|
+
|
|
66
|
+
### v0.41.4
|
|
67
|
+
|
|
68
|
+
#### What's Changed
|
|
69
|
+
|
|
70
|
+
- Update example project by [@sean5940](https://github.com/sean5940) in [#2097](https://github.com/ivpusic/react-native-image-crop-picker/pull/2097)
|
|
71
|
+
|
|
72
|
+
#### New Contributors
|
|
73
|
+
|
|
74
|
+
- [@sean5940](https://github.com/sean5940) made their first contribution in [#2097](https://github.com/ivpusic/react-native-image-crop-picker/pull/2097)
|
|
75
|
+
|
|
76
|
+
### v0.41.3
|
|
77
|
+
|
|
78
|
+
#### What's Changed
|
|
79
|
+
|
|
80
|
+
- feat(android):RN-0.73 and AGP 8.0 Compatibility by [@dishantwalia](https://github.com/dishantwalia) in [#2018](https://github.com/ivpusic/react-native-image-crop-picker/pull/2018)
|
|
81
|
+
|
|
82
|
+
#### New Contributors
|
|
83
|
+
|
|
84
|
+
- [@dishantwalia](https://github.com/dishantwalia) made their first contribution in [#2018](https://github.com/ivpusic/react-native-image-crop-picker/pull/2018)
|
|
85
|
+
|
|
86
|
+
### v0.41.2
|
|
87
|
+
|
|
88
|
+
#### What's Changed
|
|
89
|
+
|
|
90
|
+
- Fixes an issue that would cause the compiler to crash in Xcode 16 beta1 by [@isnine](https://github.com/isnine) in [#2068](https://github.com/ivpusic/react-native-image-crop-picker/pull/2068)
|
|
91
|
+
|
|
92
|
+
#### New Contributors
|
|
93
|
+
|
|
94
|
+
- [@isnine](https://github.com/isnine) made their first contribution in [#2068](https://github.com/ivpusic/react-native-image-crop-picker/pull/2068)
|
|
95
|
+
|
|
96
|
+
### v0.41.1
|
|
97
|
+
|
|
98
|
+
#### What's Changed
|
|
99
|
+
|
|
100
|
+
- URGENT: Updated RNImageCropPicker.podspec by [@ShivamKJJW](https://github.com/ShivamKJJW) in [#2056](https://github.com/ivpusic/react-native-image-crop-picker/pull/2056)
|
|
101
|
+
- https://github.com/ivpusic/react-native-image-crop-picker/pull/2068)
|
|
102
|
+
|
|
103
|
+
#### New Contributors
|
|
104
|
+
|
|
105
|
+
- [@ShivamKJJW](https://github.com/ShivamKJJW) made their first contribution in [#2056](https://github.com/ivpusic/react-native-image-crop-picker/pull/2056)
|
|
106
|
+
|
|
107
|
+
### v0.41.0
|
|
108
|
+
|
|
109
|
+
#### What's Changed
|
|
110
|
+
|
|
111
|
+
- fix(ios): replace `UIGraphicsBeginImageContext` in Compression.m by [@remarkablemark](https://github.com/remarkablemark) in [#2055](https://github.com/ivpusic/react-native-image-crop-picker/pull/2055)
|
|
112
|
+
- Add Privacy manifest by [@ujeon](https://github.com/ujeon) in [#2045](https://github.com/ivpusic/react-native-image-crop-picker/pull/2045)
|
|
113
|
+
- add Dutch (nl) as a translation for iOS by [@HostenMarijn](https://github.com/HostenMarijn) in [#1918](https://github.com/ivpusic/react-native-image-crop-picker/pull/1918)
|
|
114
|
+
|
|
115
|
+
#### New Contributors
|
|
116
|
+
|
|
117
|
+
- [@remarkablemark](https://github.com/remarkablemark) made their first contribution in [#2055](https://github.com/ivpusic/react-native-image-crop-picker/pull/2055)
|
|
118
|
+
- [@ujeon](https://github.com/ujeon) made their first contribution in [#2045](https://github.com/ivpusic/react-native-image-crop-picker/pull/2045)
|
|
119
|
+
- [@HostenMarijn](https://github.com/HostenMarijn) made their first contribution in [#1918](https://github.com/ivpusic/react-native-image-crop-picker/pull/1918)
|
|
120
|
+
|
|
121
|
+
### v0.40.3
|
|
122
|
+
|
|
123
|
+
#### What's Changed
|
|
124
|
+
|
|
125
|
+
- Make list items accessible by [@v-miibr](https://github.com/v-miibr) in [#1439](https://github.com/ivpusic/react-native-image-crop-picker/pull/1439)
|
|
126
|
+
|
|
127
|
+
#### New Contributors
|
|
128
|
+
|
|
129
|
+
- [@v-miibr](https://github.com/v-miibr) made their first contribution in [#1439](https://github.com/ivpusic/react-native-image-crop-picker/pull/1439)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Please describe the basic information.",
|
|
4
4
|
"main": "index.ets",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"version": "0.50.2-rc.
|
|
6
|
+
"version": "0.50.2-rc.5",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@rnoh/react-native-openharmony": "file:../../node_modules/@react-native-oh/react-native-harmony/harmony/react_native_openharmony.har"
|
|
9
9
|
},
|
|
@@ -443,8 +443,8 @@ export class ImageCropPickerTurboModule extends TurboModule implements ImageCrop
|
|
|
443
443
|
results.size = length;
|
|
444
444
|
results.creationDate = stat.ctime + '';
|
|
445
445
|
results.modificationDate = stat.mtime + '';
|
|
446
|
-
results.path = this.isNullOrUndefined(tempFilePaths) ? null : filePrefix + value;
|
|
447
446
|
if (this.isImage(value)) {
|
|
447
|
+
results.path = this.isNullOrUndefined(tempFilePaths) ? null : filePrefix + value;
|
|
448
448
|
results.data = includeBase64 ? this.imageToBase64(value) : null;
|
|
449
449
|
results.mime = 'image/' + imageType;
|
|
450
450
|
Logger.info(`${TAG} into openPickerResult value : ${value}`);
|
|
@@ -463,6 +463,12 @@ export class ImageCropPickerTurboModule extends TurboModule implements ImageCrop
|
|
|
463
463
|
results.duration = null;
|
|
464
464
|
} else {
|
|
465
465
|
Logger.info(`${TAG} into getPickerResult video start`);
|
|
466
|
+
let qualityNumber = this.isNullOrUndefined(options.compressImageQuality) ? ImageQuality : options.compressImageQuality;
|
|
467
|
+
if(qualityNumber !== 1){
|
|
468
|
+
results.path = this.isNullOrUndefined(tempFilePaths) ? null : value;
|
|
469
|
+
} else {
|
|
470
|
+
results.path = this.isNullOrUndefined(tempFilePaths) ? null : filePrefix + value;
|
|
471
|
+
}
|
|
466
472
|
results.data = null;
|
|
467
473
|
results.mime = 'video/' + imageType;
|
|
468
474
|
let url = 'fd://' + file.fd;
|
|
@@ -919,6 +925,7 @@ export class ImageCropPickerTurboModule extends TurboModule implements ImageCrop
|
|
|
919
925
|
const showCropGuidelines: boolean = this.isNullOrUndefined(options?.showCropGuidelines) ? true : options?.showCropGuidelines;
|
|
920
926
|
const showCropFrame: boolean = this.isNullOrUndefined(options?.showCropFrame) ? true : options?.showCropFrame;
|
|
921
927
|
const freeStyleCropEnabled: boolean = this.isNullOrUndefined(options?.freeStyleCropEnabled) ? false : options?.freeStyleCropEnabled;
|
|
928
|
+
const cropperCircleOverlay: boolean = this.isNullOrUndefined(options?.cropperCircleOverlay) ? false : options?.cropperCircleOverlay;
|
|
922
929
|
const cropperRotate: string = options?.cropperRotateButtonsHidden + '';
|
|
923
930
|
AppStorage.setOrCreate('initWidth', initWidth);
|
|
924
931
|
AppStorage.setOrCreate('initHeight', initHeight);
|
|
@@ -932,6 +939,7 @@ export class ImageCropPickerTurboModule extends TurboModule implements ImageCrop
|
|
|
932
939
|
AppStorage.setOrCreate('showCropGuidelines', showCropGuidelines);
|
|
933
940
|
AppStorage.setOrCreate('showCropFrame', showCropFrame);
|
|
934
941
|
AppStorage.setOrCreate('freeStyleCropEnabled', freeStyleCropEnabled);
|
|
942
|
+
AppStorage.setOrCreate('cropperCircleOverlay', cropperCircleOverlay);
|
|
935
943
|
|
|
936
944
|
try {
|
|
937
945
|
let want: Want = {
|
|
@@ -0,0 +1,789 @@
|
|
|
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
|
+
Text(this.title)
|
|
98
|
+
.fontColor(Color.White)
|
|
99
|
+
.fontSize(20)
|
|
100
|
+
.textAlign(TextAlign.Center)
|
|
101
|
+
.width('100%')
|
|
102
|
+
.height(46)
|
|
103
|
+
.position({ x: 0, y: 20 })
|
|
104
|
+
.backgroundColor(Color.Black)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Builder
|
|
108
|
+
BottomToolbar() {
|
|
109
|
+
Row() {
|
|
110
|
+
Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.Center }) {
|
|
111
|
+
Text(this.cancel)
|
|
112
|
+
.textStyle()
|
|
113
|
+
.fontColor(this.cancelTextColor)
|
|
114
|
+
.onClick(async () => {
|
|
115
|
+
router.back()
|
|
116
|
+
});
|
|
117
|
+
Row() {
|
|
118
|
+
Image($r('app.media.ic_anti_clockwise'))
|
|
119
|
+
.iconStyle()
|
|
120
|
+
.visibility(this.cropperRotate === 'true' ? Visibility.Hidden : Visibility.Visible)
|
|
121
|
+
.onClick(async () => {
|
|
122
|
+
this.rotateImage(RotateType.ANTI_CLOCK);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
.bottomIconStyle();
|
|
126
|
+
|
|
127
|
+
Row() {
|
|
128
|
+
Image($r('app.media.ic_reset'))
|
|
129
|
+
.iconStyle()
|
|
130
|
+
.onClick(() => {
|
|
131
|
+
this.resetAllTransformations();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
.bottomIconStyle();
|
|
135
|
+
|
|
136
|
+
Row() {
|
|
137
|
+
Image($r('app.media.ic_clockwise'))
|
|
138
|
+
.iconStyle()
|
|
139
|
+
.visibility(this.cropperRotate === 'true' ? Visibility.Hidden : Visibility.Visible)
|
|
140
|
+
.onClick(() => {
|
|
141
|
+
this.rotateImage(RotateType.CLOCKWISE);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
.bottomIconStyle();
|
|
145
|
+
|
|
146
|
+
Text(this.choose)
|
|
147
|
+
.textStyle()
|
|
148
|
+
.fontColor(this.chooseTextColor)
|
|
149
|
+
.onClick(async () => {
|
|
150
|
+
if (this.model.src) {
|
|
151
|
+
try {
|
|
152
|
+
// 进行普通的正方形裁剪
|
|
153
|
+
const squarePm = await this.model.crop(image.PixelMapFormat.RGBA_8888);
|
|
154
|
+
|
|
155
|
+
// 对裁剪后的图片应用旋转
|
|
156
|
+
const normalizedAngle = ((this.angle % 360) + 360) % 360;
|
|
157
|
+
if (normalizedAngle !== 0) {
|
|
158
|
+
await squarePm.rotate(normalizedAngle);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 转换为真正的圆形图片
|
|
162
|
+
const circlePm = await CircleImageProcessor.createTrueCircleImage(squarePm);
|
|
163
|
+
|
|
164
|
+
// 保存为PNG格式(支持透明度)
|
|
165
|
+
let imgPath = await encodeToPng(this, circlePm);
|
|
166
|
+
|
|
167
|
+
// 清理资源
|
|
168
|
+
squarePm.release();
|
|
169
|
+
circlePm.release();
|
|
170
|
+
|
|
171
|
+
// 保存路径
|
|
172
|
+
AppStorage.setOrCreate('cropImagePath', imgPath);
|
|
173
|
+
AppStorage.setOrCreate('isCircleImage', true);
|
|
174
|
+
router.back()
|
|
175
|
+
} catch (error) {
|
|
176
|
+
Logger.error(TAG, "Circle crop failed:" + JSON.stringify(error));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
.width('100%')
|
|
182
|
+
.height(56)
|
|
183
|
+
.backgroundColor(Color.Black)
|
|
184
|
+
.margin({ bottom: 30 })
|
|
185
|
+
}
|
|
186
|
+
.width('100%')
|
|
187
|
+
.height(56)
|
|
188
|
+
.position({ x: 0, y: (this.screenHeight - 66) })
|
|
189
|
+
.backgroundColor(Color.Black)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
build() {
|
|
193
|
+
Stack() {
|
|
194
|
+
Stack() {
|
|
195
|
+
Image(this.model.path)
|
|
196
|
+
.width('100%')
|
|
197
|
+
.height('100%')
|
|
198
|
+
.alt(this.model.previewSource)
|
|
199
|
+
.objectFit(ImageFit.Contain)
|
|
200
|
+
.transform(this.matrix)
|
|
201
|
+
.animation({ duration: this.isImageAnimation ? 300 : 0 })
|
|
202
|
+
.onComplete((msg) => {
|
|
203
|
+
if (msg) {
|
|
204
|
+
// 图片加载成功
|
|
205
|
+
this.model.imageWidth = msg.width;
|
|
206
|
+
this.model.imageHeight = msg.height;
|
|
207
|
+
this.model.componentWidth = msg.componentWidth;
|
|
208
|
+
this.model.componentHeight = msg.componentHeight;
|
|
209
|
+
this.saveInitialState();
|
|
210
|
+
this.checkImageAdapt();
|
|
211
|
+
if (this.model.imageLoadEventListener != null && msg.loadingStatus == 1) {
|
|
212
|
+
this.model.imageLoadEventListener.onImageLoaded(msg);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
.onError((error) => {
|
|
217
|
+
if (this.model.imageLoadEventListener != null) {
|
|
218
|
+
this.model.imageLoadEventListener.onImageLoadError(error);
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
.rotate({ angle: this.angle })
|
|
223
|
+
.width('100%')
|
|
224
|
+
.height('100%')
|
|
225
|
+
.animation({ duration: this.isStackAnimation ? 300 : 0 })
|
|
226
|
+
.priorityGesture(
|
|
227
|
+
TapGesture({ count: 2, fingers: 1 })
|
|
228
|
+
.onAction((event: GestureEvent) => {
|
|
229
|
+
if (!event) {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
if (this.model.zoomEnabled) {
|
|
233
|
+
if (this.model.scale != 1) {
|
|
234
|
+
this.model.scale = 1;
|
|
235
|
+
this.updateMatrix();
|
|
236
|
+
} else {
|
|
237
|
+
this.zoomTo(2);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.checkImageAdapt();
|
|
242
|
+
})
|
|
243
|
+
)
|
|
244
|
+
.gesture(
|
|
245
|
+
GestureGroup(GestureMode.Parallel,
|
|
246
|
+
// 拖动手势
|
|
247
|
+
PanGesture({})
|
|
248
|
+
.onActionStart(() => {
|
|
249
|
+
Logger.info(TAG, "CropView Pan gesture start");
|
|
250
|
+
this.startOffsetX = this.model.offsetX;
|
|
251
|
+
this.startOffsetY = this.model.offsetY;
|
|
252
|
+
this.isImageAnimation = false
|
|
253
|
+
})
|
|
254
|
+
.onActionUpdate((event: GestureEvent) => {
|
|
255
|
+
Logger.info(TAG, "CropView Pan gesture update" + JSON.stringify(event));
|
|
256
|
+
if (event) {
|
|
257
|
+
if (this.model.panEnabled) {
|
|
258
|
+
let distanceX: number = this.startOffsetX + vp2px(event.offsetX) / this.model.scale;
|
|
259
|
+
let distanceY: number = this.startOffsetY + vp2px(event.offsetY) / this.model.scale;
|
|
260
|
+
this.model.offsetX = distanceX;
|
|
261
|
+
this.model.offsetY = distanceY;
|
|
262
|
+
this.updateMatrix()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
})
|
|
266
|
+
.onActionEnd(() => {
|
|
267
|
+
Logger.info(TAG, "CropView Pan gesture end");
|
|
268
|
+
this.checkImageAdapt();
|
|
269
|
+
this.isImageAnimation = true
|
|
270
|
+
}),
|
|
271
|
+
// 缩放手势
|
|
272
|
+
PinchGesture({ fingers: 2 })
|
|
273
|
+
.onActionStart(() => {
|
|
274
|
+
this.isStackAnimation = false
|
|
275
|
+
this.tempScale = this.model.scale
|
|
276
|
+
})
|
|
277
|
+
.onActionUpdate((event) => {
|
|
278
|
+
if (event) {
|
|
279
|
+
if (!this.model.zoomEnabled) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.zoomTo(Math.min(5.0, Math.max(0.1, (this.tempScale * event.scale))))
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
.onActionEnd((event) => {
|
|
286
|
+
this.checkImageAdapt()
|
|
287
|
+
this.updateMatrix()
|
|
288
|
+
this.isStackAnimation = true
|
|
289
|
+
}),
|
|
290
|
+
//旋转手势
|
|
291
|
+
RotationGesture({})
|
|
292
|
+
.onActionStart(() => {
|
|
293
|
+
if (this.enableRotationGesture) {
|
|
294
|
+
// 记录手势起始
|
|
295
|
+
this.gestureAngle = this.angle
|
|
296
|
+
// 关闭动画避免旋转错误
|
|
297
|
+
this.isStackAnimation = false
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
.onActionUpdate((event: GestureEvent) => {
|
|
301
|
+
if (this.enableRotationGesture) {
|
|
302
|
+
let normalizedAngle = event.angle
|
|
303
|
+
if (normalizedAngle < 0) {
|
|
304
|
+
normalizedAngle += 360
|
|
305
|
+
}
|
|
306
|
+
this.angle = this.gestureAngle + normalizedAngle
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
.onActionEnd(() => {
|
|
310
|
+
if (this.enableRotationGesture) {
|
|
311
|
+
this.isStackAnimation = false
|
|
312
|
+
const tmp = this.angle % 360
|
|
313
|
+
this.angle = tmp
|
|
314
|
+
let time = setTimeout(() => {
|
|
315
|
+
this.isStackAnimation = true
|
|
316
|
+
this.angle = closestAngle
|
|
317
|
+
this.updateMatrix()
|
|
318
|
+
this.checkImageAdapt()
|
|
319
|
+
clearTimeout(time)
|
|
320
|
+
}, 50)
|
|
321
|
+
|
|
322
|
+
// 计算最近的吸附角度
|
|
323
|
+
const snapAngles = [0, 90, 180, 270, 360]
|
|
324
|
+
let closestAngle = snapAngles[0]
|
|
325
|
+
let minDiff = Infinity
|
|
326
|
+
|
|
327
|
+
for (const snapAngle of snapAngles) {
|
|
328
|
+
const diff = Math.abs(tmp - snapAngle)
|
|
329
|
+
if (diff < minDiff) {
|
|
330
|
+
minDiff = diff
|
|
331
|
+
closestAngle = snapAngle
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
)
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
Canvas(this.context)
|
|
340
|
+
.width('100%')
|
|
341
|
+
.height('100%')
|
|
342
|
+
.backgroundColor(Color.Transparent)
|
|
343
|
+
.onReady(() => {
|
|
344
|
+
if (this.context == null) {
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
let height = this.context.height
|
|
348
|
+
let width = this.context.width
|
|
349
|
+
this.context.fillStyle = this.model.maskColor;
|
|
350
|
+
this.context.fillRect(0, 0, width, height)
|
|
351
|
+
let centerX = width / 2;
|
|
352
|
+
let centerY = height / 2;
|
|
353
|
+
this.context.globalCompositeOperation = 'destination-out'
|
|
354
|
+
this.context.fillStyle = 'white'
|
|
355
|
+
let frameWidthInVp = px2vp(this.model.frameWidth);
|
|
356
|
+
let frameHeightInVp = px2vp(this.model.getFrameHeight());
|
|
357
|
+
this.context.beginPath();
|
|
358
|
+
this.context.arc(centerX, centerY, px2vp(this.model.frameWidth / 2), 0, 2 * Math.PI);
|
|
359
|
+
this.context.fill();
|
|
360
|
+
this.context.globalCompositeOperation = 'source-over';
|
|
361
|
+
this.context.strokeStyle = this.model.strokeColor;
|
|
362
|
+
let radius = Math.min(frameWidthInVp, frameHeightInVp) / 2;
|
|
363
|
+
this.context.beginPath();
|
|
364
|
+
this.context.arc(centerX, centerY, radius, 0, 2 * Math.PI);
|
|
365
|
+
this.context.closePath();
|
|
366
|
+
this.context.lineWidth = 1;
|
|
367
|
+
this.context.stroke();
|
|
368
|
+
})
|
|
369
|
+
.enabled(false)
|
|
370
|
+
|
|
371
|
+
this.CropTitleBar()
|
|
372
|
+
|
|
373
|
+
this.BottomToolbar()
|
|
374
|
+
}
|
|
375
|
+
.backgroundColor(Color.Black)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* 检查手势操作后,图片是否填满取景框,没填满则进行调整
|
|
380
|
+
*/
|
|
381
|
+
private checkImageAdapt() {
|
|
382
|
+
let offsetX = this.model.offsetX;
|
|
383
|
+
let offsetY = this.model.offsetY;
|
|
384
|
+
let scale = this.model.scale;
|
|
385
|
+
Logger.info(TAG, "CropView offsetX: " + offsetX + ", offsetY: " + offsetY + ", scale: " + scale);
|
|
386
|
+
|
|
387
|
+
// 图片适配控件的时候也进行了缩放,计算出这个缩放比例
|
|
388
|
+
let widthScale = this.model.componentWidth / this.model.imageWidth;
|
|
389
|
+
let heightScale = this.model.componentHeight / this.model.imageHeight;
|
|
390
|
+
let adaptScale = Math.min(widthScale, heightScale);
|
|
391
|
+
Logger.info(TAG,
|
|
392
|
+
"CropView Image scale: " + adaptScale + "while attaching the component[" + this.model.componentWidth + ", " +
|
|
393
|
+
this.model.componentHeight);
|
|
394
|
+
|
|
395
|
+
// 经过两次缩放(适配控件、手势)后,图片的实际显示大小
|
|
396
|
+
let showWidth = this.model.imageWidth * adaptScale * this.model.scale;
|
|
397
|
+
let showHeight = this.model.imageHeight * adaptScale * this.model.scale;
|
|
398
|
+
let imageX = (this.model.componentWidth - showWidth) / 2;
|
|
399
|
+
let imageY = (this.model.componentHeight - showHeight) / 2;
|
|
400
|
+
Logger.info(TAG, "CropView Image left top is: (" + imageX + ", " + imageY + ")");
|
|
401
|
+
|
|
402
|
+
// 取景框的左上角坐标
|
|
403
|
+
let frameX = (this.model.componentWidth - this.model.frameWidth) / 2;
|
|
404
|
+
let frameY = (this.model.componentHeight - this.model.getFrameHeight()) / 2;
|
|
405
|
+
|
|
406
|
+
// 图片左上角坐标
|
|
407
|
+
let showX = imageX + offsetX * scale;
|
|
408
|
+
let showY = imageY + offsetY * scale;
|
|
409
|
+
Logger.info(TAG, "CropView Image show at (" + showX + ", " + showY + ")");
|
|
410
|
+
|
|
411
|
+
if (this.model.frameWidth > showWidth || this.model.getFrameHeight() > showHeight) { // 图片缩放后,大小不足以填满取景框
|
|
412
|
+
let xScale = this.model.frameWidth / showWidth;
|
|
413
|
+
let yScale = this.model.getFrameHeight() / showHeight;
|
|
414
|
+
let newScale = Math.max(xScale, yScale);
|
|
415
|
+
this.model.scale = this.model.scale * newScale;
|
|
416
|
+
showX *= newScale;
|
|
417
|
+
showY *= newScale;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 调整x轴方向位置,使图像填满取景框
|
|
421
|
+
if (showX > frameX) {
|
|
422
|
+
showX = frameX;
|
|
423
|
+
} else if (showX + showWidth < frameX + this.model.frameWidth) {
|
|
424
|
+
showX = frameX + this.model.frameWidth - showWidth;
|
|
425
|
+
}
|
|
426
|
+
// 调整y轴方向位置,使图像填满取景框
|
|
427
|
+
if (showY > frameY) {
|
|
428
|
+
showY = frameY;
|
|
429
|
+
} else if (showY + showHeight < frameY + this.model.getFrameHeight()) {
|
|
430
|
+
showY = frameY + this.model.getFrameHeight() - showHeight;
|
|
431
|
+
}
|
|
432
|
+
this.model.offsetX = (showX - imageX) / scale;
|
|
433
|
+
this.model.offsetY = (showY - imageY) / scale;
|
|
434
|
+
this.updateMatrix();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
public zoomTo(scale: number): void {
|
|
438
|
+
this.model.scale = scale;
|
|
439
|
+
this.updateMatrix();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
public updateMatrix(): void {
|
|
443
|
+
this.matrix = Matrix4.identity()
|
|
444
|
+
.translate({ x: this.model.offsetX, y: this.model.offsetY })
|
|
445
|
+
.scale({ x: this.model.scale, y: this.model.scale })
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
public rotateImage(rotateType: RotateType) {
|
|
449
|
+
Logger.info(TAG, "into rotateImage rotateType : " + rotateType)
|
|
450
|
+
if (rotateType === RotateType.CLOCKWISE) {
|
|
451
|
+
if (!this.model.src) {
|
|
452
|
+
Logger.info(TAG, "into rotateImage return")
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
this.model.src.rotate(Constants.CLOCK_WISE)
|
|
457
|
+
.then(() => {
|
|
458
|
+
this.angle = this.angle + Constants.CLOCK_WISE;
|
|
459
|
+
Logger.info(TAG, `into rotateImage Constants.CLOCK_WISE return ${this.angle}`,)
|
|
460
|
+
})
|
|
461
|
+
} catch (error) {
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (rotateType === RotateType.ANTI_CLOCK) {
|
|
465
|
+
if (!this.model.src) {
|
|
466
|
+
Logger.info(TAG, "into rotateImage return")
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
this.model.src.rotate(Constants.ANTI_CLOCK)
|
|
471
|
+
.then(() => {
|
|
472
|
+
this.angle = this.angle + Constants.ANTI_CLOCK;
|
|
473
|
+
Logger.info(TAG, `into rotateImage Constants.ANTI_CLOCK return ${this.angle}`,)
|
|
474
|
+
})
|
|
475
|
+
} catch (error) {
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 保存初始状态方法
|
|
481
|
+
private saveInitialState(): void {
|
|
482
|
+
this.initialAngle = this.angle;
|
|
483
|
+
this.initialScale = this.model.scale;
|
|
484
|
+
this.initialOffsetX = this.model.offsetX;
|
|
485
|
+
this.initialOffsetY = this.model.offsetY;
|
|
486
|
+
Logger.info(TAG, "Initial state saved: " +
|
|
487
|
+
`angle=${this.initialAngle}, scale=${this.initialScale}, offsetX=${this.initialOffsetX}, offsetY=${this.initialOffsetY}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 重置所有变换的方法
|
|
491
|
+
private resetAllTransformations(): void {
|
|
492
|
+
Logger.info(TAG, "Resetting all transformations...");
|
|
493
|
+
|
|
494
|
+
// 重置位置和缩放
|
|
495
|
+
this.model.scale = this.initialScale;
|
|
496
|
+
this.model.offsetX = this.initialOffsetX;
|
|
497
|
+
this.model.offsetY = this.initialOffsetY;
|
|
498
|
+
|
|
499
|
+
// 重置旋转角度
|
|
500
|
+
if (this.angle !== this.initialAngle) {
|
|
501
|
+
let angleDifference = this.initialAngle - this.angle;
|
|
502
|
+
|
|
503
|
+
// 如果有PixelMap且需要旋转
|
|
504
|
+
if (this.model.src && angleDifference !== 0) {
|
|
505
|
+
angleDifference = angleDifference % 360
|
|
506
|
+
// 异步旋转图片
|
|
507
|
+
this.model.src.rotate(angleDifference)
|
|
508
|
+
.then(() => {
|
|
509
|
+
this.angle = this.initialAngle;
|
|
510
|
+
Logger.info(TAG, `Image rotated back by ${angleDifference} degrees`);
|
|
511
|
+
this.updateMatrix();
|
|
512
|
+
this.checkImageAdapt();
|
|
513
|
+
})
|
|
514
|
+
.catch((error: Error) => {
|
|
515
|
+
Logger.error(TAG, "Failed to reset rotation: " + JSON.stringify(error));
|
|
516
|
+
// 即使旋转失败,也更新UI状态
|
|
517
|
+
this.angle = this.initialAngle;
|
|
518
|
+
this.updateMatrix();
|
|
519
|
+
this.checkImageAdapt();
|
|
520
|
+
});
|
|
521
|
+
} else {
|
|
522
|
+
// 无需旋转或没有PixelMap,直接更新
|
|
523
|
+
this.angle = this.initialAngle;
|
|
524
|
+
this.updateMatrix();
|
|
525
|
+
this.checkImageAdapt();
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
// 角度相同,直接更新
|
|
529
|
+
this.updateMatrix();
|
|
530
|
+
this.checkImageAdapt();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// 重置图片宽高到初始值
|
|
534
|
+
if (this.imageWidth !== this.initialImageWidth || this.imageHeight !== this.initialImageHeight) {
|
|
535
|
+
this.imageWidth = this.initialImageWidth;
|
|
536
|
+
this.imageHeight = this.initialImageHeight;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
Logger.info(TAG, "All transformations have been reset: " +
|
|
540
|
+
`angle=${this.angle}, scale=${this.model.scale}, offsetX=${this.model.offsetX}, offsetY=${this.model.offsetY}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
interface ImageLoadedEvent {
|
|
545
|
+
width: number;
|
|
546
|
+
height: number;
|
|
547
|
+
componentWidth: number;
|
|
548
|
+
componentHeight: number;
|
|
549
|
+
loadingStatus: number;
|
|
550
|
+
contentWidth: number;
|
|
551
|
+
contentHeight: number;
|
|
552
|
+
contentOffsetX: number;
|
|
553
|
+
contentOffsetY: number;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export interface ImageLoadEventListener {
|
|
557
|
+
|
|
558
|
+
onImageLoaded(msg: ImageLoadedEvent): void;
|
|
559
|
+
|
|
560
|
+
onImageLoadError(error: ImageError): void;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export class CropModel {
|
|
564
|
+
/**
|
|
565
|
+
* PixelMap图片对象
|
|
566
|
+
*
|
|
567
|
+
*/
|
|
568
|
+
src?: image.PixelMap = undefined;
|
|
569
|
+
/**
|
|
570
|
+
* 图片地址
|
|
571
|
+
*/
|
|
572
|
+
path: string = '';
|
|
573
|
+
/**
|
|
574
|
+
* 图片预览
|
|
575
|
+
*/
|
|
576
|
+
previewSource: string | Resource = '';
|
|
577
|
+
/**
|
|
578
|
+
* 是否可以拖动
|
|
579
|
+
*/
|
|
580
|
+
panEnabled: boolean = true;
|
|
581
|
+
/**
|
|
582
|
+
* 是否可以缩放
|
|
583
|
+
*/
|
|
584
|
+
zoomEnabled: boolean = true;
|
|
585
|
+
/**
|
|
586
|
+
* 取景框宽度
|
|
587
|
+
*/
|
|
588
|
+
frameWidth = 1000;
|
|
589
|
+
/**
|
|
590
|
+
* 取景框宽高比
|
|
591
|
+
*/
|
|
592
|
+
frameRatio = 1;
|
|
593
|
+
/**
|
|
594
|
+
* 遮罩颜色
|
|
595
|
+
*/
|
|
596
|
+
maskColor: string = '#AA000000';
|
|
597
|
+
/**
|
|
598
|
+
* 取景框边框颜色
|
|
599
|
+
*/
|
|
600
|
+
strokeColor: string = '#FFFFFF';
|
|
601
|
+
/**
|
|
602
|
+
* 图片加载监听
|
|
603
|
+
*/
|
|
604
|
+
imageLoadEventListener: ImageLoadEventListener | null = null;
|
|
605
|
+
/**
|
|
606
|
+
* 图片宽度
|
|
607
|
+
*/
|
|
608
|
+
imageWidth: number = 0;
|
|
609
|
+
/**
|
|
610
|
+
* 图片高度
|
|
611
|
+
*/
|
|
612
|
+
imageHeight: number = 0;
|
|
613
|
+
/**
|
|
614
|
+
* 控件宽度
|
|
615
|
+
*/
|
|
616
|
+
componentWidth: number = 0;
|
|
617
|
+
/**
|
|
618
|
+
* 控件高度
|
|
619
|
+
*/
|
|
620
|
+
componentHeight: number = 0;
|
|
621
|
+
/**
|
|
622
|
+
* 手势缩放比例
|
|
623
|
+
*/
|
|
624
|
+
scale: number = 1;
|
|
625
|
+
/**
|
|
626
|
+
* x轴方向偏移量
|
|
627
|
+
*/
|
|
628
|
+
offsetX: number = 0;
|
|
629
|
+
/**
|
|
630
|
+
* y轴方向偏移量
|
|
631
|
+
*/
|
|
632
|
+
offsetY: number = 0;
|
|
633
|
+
|
|
634
|
+
public setImage(path: string, previewSource?: string | Resource): CropModel {
|
|
635
|
+
this.path = path;
|
|
636
|
+
if (!!previewSource) {
|
|
637
|
+
this.previewSource = previewSource;
|
|
638
|
+
}
|
|
639
|
+
return this;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
public setScale(scale: number): CropModel {
|
|
643
|
+
this.scale = scale;
|
|
644
|
+
return this;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
public isPanEnabled(): boolean {
|
|
648
|
+
return this.panEnabled;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
public setPanEnabled(panEnabled: boolean): CropModel {
|
|
652
|
+
this.panEnabled = panEnabled;
|
|
653
|
+
return this;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
public setZoomEnabled(zoomEnabled: boolean): CropModel {
|
|
657
|
+
this.zoomEnabled = zoomEnabled;
|
|
658
|
+
return this;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
public setFrameWidth(frameWidth: number): CropModel {
|
|
662
|
+
this.frameWidth = frameWidth;
|
|
663
|
+
return this;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
public setFrameRatio(frameRatio: number): CropModel {
|
|
667
|
+
this.frameRatio = frameRatio;
|
|
668
|
+
return this;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
public setMaskColor(color: string): CropModel {
|
|
672
|
+
this.maskColor = color;
|
|
673
|
+
return this;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
public setStrokeColor(color: string): CropModel {
|
|
677
|
+
this.strokeColor = color;
|
|
678
|
+
return this;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
public setImageLoadEventListener(listener: ImageLoadEventListener): CropModel {
|
|
682
|
+
this.imageLoadEventListener = listener;
|
|
683
|
+
return this;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
public getScale(): number {
|
|
687
|
+
return this.scale;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
public isZoomEnabled(): boolean {
|
|
691
|
+
return this.zoomEnabled;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
public getImageWidth(): number {
|
|
695
|
+
return this.imageWidth;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
public getImageHeight(): number {
|
|
699
|
+
return this.imageHeight;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
public getFrameHeight() {
|
|
703
|
+
return this.frameWidth / this.frameRatio;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
public reset(): void {
|
|
707
|
+
this.scale = 1;
|
|
708
|
+
this.offsetX = 0;
|
|
709
|
+
this.offsetY = 0;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
public async crop(format: image.PixelMapFormat): Promise<image.PixelMap> {
|
|
713
|
+
|
|
714
|
+
if (!this.path || this.path == '') {
|
|
715
|
+
throw new Error('Please set path first');
|
|
716
|
+
}
|
|
717
|
+
if (this.imageWidth == 0 || this.imageHeight == 0) {
|
|
718
|
+
throw new Error('The image is not loaded');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// 图片适配控件的时候也进行了缩放,计算出这个缩放比例
|
|
722
|
+
let widthScale = this.componentWidth / this.imageWidth;
|
|
723
|
+
let heightScale = this.componentHeight / this.imageHeight;
|
|
724
|
+
let adaptScale = Math.min(widthScale, heightScale);
|
|
725
|
+
|
|
726
|
+
// 经过两次缩放(适配控件、手势)后,图片的实际显示大小
|
|
727
|
+
let totalScale = adaptScale * this.scale;
|
|
728
|
+
let showWidth = this.imageWidth * totalScale;
|
|
729
|
+
let showHeight = this.imageHeight * totalScale;
|
|
730
|
+
let imageX = (this.componentWidth - showWidth) / 2;
|
|
731
|
+
let imageY = (this.componentHeight - showHeight) / 2;
|
|
732
|
+
|
|
733
|
+
// 取景框的左上角坐标
|
|
734
|
+
let frameX = (this.componentWidth - this.frameWidth) / 2;
|
|
735
|
+
let frameY = (this.componentHeight - this.getFrameHeight()) / 2;
|
|
736
|
+
|
|
737
|
+
// 图片左上角坐标
|
|
738
|
+
let showX = imageX + this.offsetX * this.scale;
|
|
739
|
+
let showY = imageY + this.offsetY * this.scale;
|
|
740
|
+
|
|
741
|
+
let x = (frameX - showX) / totalScale;
|
|
742
|
+
let y = (frameY - showY) / totalScale;
|
|
743
|
+
let file = fs.openSync(this.path, fs.OpenMode.READ_ONLY)
|
|
744
|
+
let imageSource: image.ImageSource = image.createImageSource(file.fd);
|
|
745
|
+
let decodingOptions: image.DecodingOptions = {
|
|
746
|
+
editable: true,
|
|
747
|
+
desiredPixelFormat: image.PixelMapFormat.BGRA_8888,
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// 创建pixelMap
|
|
751
|
+
let pm = await imageSource.createPixelMap(decodingOptions);
|
|
752
|
+
let cp = await this.copyPixelMap(pm);
|
|
753
|
+
pm.release();
|
|
754
|
+
let region: image.Region =
|
|
755
|
+
{ x: x, y: y, size: { width: this.frameWidth / totalScale, height: this.getFrameHeight() / totalScale } };
|
|
756
|
+
cp.cropSync(region);
|
|
757
|
+
return cp;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async copyPixelMap(pm: PixelMap): Promise<PixelMap> {
|
|
761
|
+
const imageInfo: image.ImageInfo = await pm.getImageInfo();
|
|
762
|
+
const buffer: ArrayBuffer = new ArrayBuffer(pm.getPixelBytesNumber());
|
|
763
|
+
await pm.readPixelsToBuffer(buffer);
|
|
764
|
+
const opts: image.InitializationOptions = {
|
|
765
|
+
editable: true,
|
|
766
|
+
pixelFormat: image.PixelMapFormat.RGBA_8888,
|
|
767
|
+
size: { height: imageInfo.size.height, width: imageInfo.size.width }
|
|
768
|
+
};
|
|
769
|
+
return image.createPixelMap(buffer, opts);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
@Entry
|
|
774
|
+
@Component
|
|
775
|
+
export struct CircleImageInfo {
|
|
776
|
+
@State private model: CropModel = new CropModel();
|
|
777
|
+
|
|
778
|
+
build() {
|
|
779
|
+
Column() {
|
|
780
|
+
CropView({
|
|
781
|
+
model: this.model,
|
|
782
|
+
})
|
|
783
|
+
.layoutWeight(1)
|
|
784
|
+
.width('100%')
|
|
785
|
+
}
|
|
786
|
+
.height('100%')
|
|
787
|
+
.width('100%')
|
|
788
|
+
}
|
|
789
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { image } from '@kit.ImageKit';
|
|
2
|
+
import util from '@ohos.util';
|
|
3
|
+
|
|
4
|
+
export class CircleImageProcessor {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 将正方形图片转换为真正的圆形图片(带透明背景)
|
|
8
|
+
*/
|
|
9
|
+
static async createTrueCircleImage(squarePm: PixelMap): Promise<PixelMap> {
|
|
10
|
+
const info = await squarePm.getImageInfo();
|
|
11
|
+
const size = Math.min(info.size.width, info.size.height);
|
|
12
|
+
|
|
13
|
+
console.info(`Creating true circle image, size: ${size}x${size}`);
|
|
14
|
+
|
|
15
|
+
// 1. 读取原始像素(BGRA格式)
|
|
16
|
+
const originalBuffer = new ArrayBuffer(squarePm.getPixelBytesNumber());
|
|
17
|
+
await squarePm.readPixelsToBuffer(originalBuffer);
|
|
18
|
+
const originalPixels = new Uint8ClampedArray(originalBuffer);
|
|
19
|
+
|
|
20
|
+
// 2. 创建新的RGBA像素数组(支持透明度)
|
|
21
|
+
const pixelBuffer = new Uint8ClampedArray(size * size * 4);
|
|
22
|
+
|
|
23
|
+
const radius = size / 2;
|
|
24
|
+
const centerX = radius;
|
|
25
|
+
const centerY = radius;
|
|
26
|
+
const radiusSq = radius * radius;
|
|
27
|
+
|
|
28
|
+
// 3. 遍历每个像素,创建圆形遮罩
|
|
29
|
+
for (let y = 0; y < size; y++) {
|
|
30
|
+
for (let x = 0; x < size; x++) {
|
|
31
|
+
const dx = x - centerX;
|
|
32
|
+
const dy = y - centerY;
|
|
33
|
+
const distSq = dx * dx + dy * dy;
|
|
34
|
+
|
|
35
|
+
const srcIdx = (y * size + x) * 4;
|
|
36
|
+
const dstIdx = (y * size + x) * 4;
|
|
37
|
+
|
|
38
|
+
if (distSq <= radiusSq) {
|
|
39
|
+
// 圆形区域内:复制像素并转换 BGRA -> RGBA
|
|
40
|
+
// 同时进行抗锯齿处理(边缘半透明)
|
|
41
|
+
const distance = Math.sqrt(distSq);
|
|
42
|
+
let alpha = 255; // 完全不透明
|
|
43
|
+
|
|
44
|
+
// 边缘抗锯齿:距离半径3像素内的区域逐渐透明
|
|
45
|
+
if (distance > radius - 3) {
|
|
46
|
+
const edgeDistance = radius - distance;
|
|
47
|
+
alpha = Math.max(0, Math.min(255, Math.floor((edgeDistance / 3) * 255)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pixelBuffer[dstIdx] = originalPixels[srcIdx + 2]; // R
|
|
51
|
+
pixelBuffer[dstIdx + 1] = originalPixels[srcIdx + 1]; // G
|
|
52
|
+
pixelBuffer[dstIdx + 2] = originalPixels[srcIdx]; // B
|
|
53
|
+
pixelBuffer[dstIdx + 3] = alpha; // A(带抗锯齿)
|
|
54
|
+
} else {
|
|
55
|
+
// 圆形区域外:完全透明
|
|
56
|
+
pixelBuffer[dstIdx] = 0; // R
|
|
57
|
+
pixelBuffer[dstIdx + 1] = 0; // G
|
|
58
|
+
pixelBuffer[dstIdx + 2] = 0; // B
|
|
59
|
+
pixelBuffer[dstIdx + 3] = 0; // A(完全透明)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 4. 创建新的PixelMap(RGBA_8888支持透明度)
|
|
65
|
+
const opts: image.InitializationOptions = {
|
|
66
|
+
editable: true,
|
|
67
|
+
pixelFormat: image.PixelMapFormat.RGBA_8888,
|
|
68
|
+
size: { height: size, width: size },
|
|
69
|
+
alphaType: image.AlphaType.PREMUL // 预乘alpha,透明度效果更好
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
console.info('Circle image created successfully');
|
|
73
|
+
return await image.createPixelMap(pixelBuffer.buffer, opts);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 从矩形图片创建圆形图片
|
|
78
|
+
*/
|
|
79
|
+
static async cropAndCreateCircle(sourcePm: PixelMap, cropRegion: image.Region): Promise<PixelMap> {
|
|
80
|
+
// 1. 先裁剪为正方形
|
|
81
|
+
const squarePm = await CircleImageProcessor.cropToSquare(sourcePm, cropRegion);
|
|
82
|
+
|
|
83
|
+
// 2. 转换为圆形
|
|
84
|
+
const circlePm = await CircleImageProcessor.createTrueCircleImage(squarePm);
|
|
85
|
+
|
|
86
|
+
// 3. 清理临时资源
|
|
87
|
+
squarePm.release();
|
|
88
|
+
|
|
89
|
+
return circlePm;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 裁剪为正方形
|
|
94
|
+
*/
|
|
95
|
+
private static async cropToSquare(sourcePm: PixelMap, region: image.Region): Promise<PixelMap> {
|
|
96
|
+
// 确保是正方形区域
|
|
97
|
+
const squareSize = Math.min(region.size.width, region.size.height);
|
|
98
|
+
const squareRegion: image.Region = {
|
|
99
|
+
x: region.x + (region.size.width - squareSize) / 2,
|
|
100
|
+
y: region.y + (region.size.height - squareSize) / 2,
|
|
101
|
+
size: { width: squareSize, height: squareSize }
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// 深拷贝PixelMap然后裁剪
|
|
105
|
+
const copyPm = await CircleImageProcessor.copyPixelMap(sourcePm);
|
|
106
|
+
copyPm.cropSync(squareRegion);
|
|
107
|
+
return copyPm;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 深拷贝PixelMap
|
|
112
|
+
*/
|
|
113
|
+
private static async copyPixelMap(pm: PixelMap): Promise<PixelMap> {
|
|
114
|
+
const imageInfo: image.ImageInfo = await pm.getImageInfo();
|
|
115
|
+
const buffer: ArrayBuffer = new ArrayBuffer(pm.getPixelBytesNumber());
|
|
116
|
+
await pm.readPixelsToBuffer(buffer);
|
|
117
|
+
|
|
118
|
+
const opts: image.InitializationOptions = {
|
|
119
|
+
editable: true,
|
|
120
|
+
pixelFormat: image.PixelMapFormat.BGRA_8888,
|
|
121
|
+
size: { height: imageInfo.size.height, width: imageInfo.size.width }
|
|
122
|
+
};
|
|
123
|
+
return image.createPixelMap(buffer, opts);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -31,4 +31,8 @@ export class Constants {
|
|
|
31
31
|
static readonly IMAGE_FORMAT: string = '.jpg'
|
|
32
32
|
static readonly IMAGE_PREFIX: string = 'image';
|
|
33
33
|
static readonly ENCODE_FILE_PERMISSION: string = 'rw'
|
|
34
|
+
|
|
35
|
+
// 添加png支持
|
|
36
|
+
static readonly ENCODE_FORMAT_PNG: string = 'image/png'
|
|
37
|
+
static readonly IMAGE_FORMAT_PNG: string = '.png'
|
|
34
38
|
}
|
|
@@ -50,4 +50,25 @@ export async function encode(component: Object, pixelMap: ESObject) : Promise<st
|
|
|
50
50
|
fs.closeSync(newFile.fd);
|
|
51
51
|
imagePackerApi.release();
|
|
52
52
|
return imgPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// PNG编码函数
|
|
56
|
+
export async function encodeToPng(component: Object, pixelMap: ESObject): Promise<string> {
|
|
57
|
+
let imgPath: string = ''
|
|
58
|
+
const newPixelMap: ESObject = pixelMap
|
|
59
|
+
const imagePackerApi = image.createImagePacker()
|
|
60
|
+
const packOptions: image.PackingOption = {
|
|
61
|
+
format: Constants.ENCODE_FORMAT_PNG, // 使用PNG格式
|
|
62
|
+
quality: 100 // PNG质量参数可能无效,但保留
|
|
63
|
+
}
|
|
64
|
+
let packerData = await imagePackerApi.packing(newPixelMap, packOptions)
|
|
65
|
+
Logger.info(TAG, 'into PNG encode data size: ' + packerData.byteLength)
|
|
66
|
+
const context = getContext(component)
|
|
67
|
+
imgPath = context.tempDir + '/circle_image_' + util.generateRandomUUID(true) + Constants.IMAGE_FORMAT_PNG
|
|
68
|
+
let newFile = fs.openSync(imgPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE)
|
|
69
|
+
const number = fs.writeSync(newFile.fd, packerData)
|
|
70
|
+
Logger.info(TAG, 'PNG file saved: ' + imgPath + ', size: ' + number)
|
|
71
|
+
fs.closeSync(newFile.fd)
|
|
72
|
+
imagePackerApi.release()
|
|
73
|
+
return imgPath
|
|
53
74
|
}
|
|
Binary file
|