@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.
- package/CHANGELOG.md +17 -0
- package/COMMITTERS.md +1 -5
- package/harmony/image_crop_picker/index.ets +3 -1
- package/harmony/image_crop_picker/oh-package.json5 +1 -1
- package/harmony/image_crop_picker/src/main/cpp/generated/RNOH/generated/BaseReactNativeImageCropPickerPackage.h +0 -1
- package/harmony/image_crop_picker/src/main/ets/{ImageCropPickerPackage.ts → ImageCropPickerPackage.ets} +2 -1
- package/harmony/image_crop_picker/src/main/ets/ImageCropPickerTurboModule.ts +45 -30
- package/harmony/image_crop_picker/src/main/ets/generated/components/ts.ts +1 -1
- package/harmony/image_crop_picker/src/main/ets/generated/turboModules/ImageCropPicker.ts +2 -0
- package/harmony/image_crop_picker/src/main/ets/pages/CircleImageInfo.ets +795 -0
- package/harmony/image_crop_picker/src/main/ets/pages/ImageEditInfo.ets +1073 -172
- 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 -4
- 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 +9 -3
- /package/harmony/image_crop_picker/{ts.ts → ts.ets} +0 -0
|
@@ -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
|
+
}
|