@unif/react-native-hms-scan 0.6.1 → 1.0.0
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/README.md +24 -72
- package/android/src/main/AndroidManifest.xml +0 -9
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanImage.kt +135 -0
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanModule.kt +163 -147
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanResultMapper.kt +17 -9
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanView.kt +57 -19
- package/android/src/main/java/com/unif/reactnativehmsscan/HmsScanViewManager.kt +1 -1
- package/ios/HmsScanModule.mm +13 -37
- package/ios/HmsScanResultMapper.h +4 -3
- package/ios/HmsScanResultMapper.mm +31 -19
- package/ios/HmsScanView.h +1 -1
- package/ios/HmsScanView.mm +140 -25
- package/lib/module/HmsScanView/HmsScanView.js +66 -0
- package/lib/module/HmsScanView/HmsScanView.js.map +1 -0
- package/lib/module/HmsScanView/HmsScanView.web.js +26 -0
- package/lib/module/HmsScanView/HmsScanView.web.js.map +1 -0
- package/lib/module/HmsScanView/types.js +4 -0
- package/lib/module/HmsScanView/types.js.map +1 -0
- package/lib/module/HmsScanViewNativeComponent.ts +7 -5
- package/lib/module/NativeHmsScan.js +1 -1
- package/lib/module/NativeHmsScan.js.map +1 -1
- package/lib/module/ScanError.js +12 -0
- package/lib/module/ScanError.js.map +1 -0
- package/lib/module/Scanner/DeniedOverlay.js +1 -1
- package/lib/module/Scanner/DeniedOverlay.js.map +1 -1
- package/lib/module/Scanner/ResultFail.js +3 -2
- package/lib/module/Scanner/ResultFail.js.map +1 -1
- package/lib/module/Scanner/ResultFocus/ResultFocus.js +56 -0
- package/lib/module/Scanner/ResultFocus/ResultFocus.js.map +1 -0
- package/lib/module/Scanner/ResultFocus/styles.js +39 -0
- package/lib/module/Scanner/ResultFocus/styles.js.map +1 -0
- package/lib/module/Scanner/ResultFocus/types.js +4 -0
- package/lib/module/Scanner/ResultFocus/types.js.map +1 -0
- package/lib/module/Scanner/ScanHint.js +6 -3
- package/lib/module/Scanner/ScanHint.js.map +1 -1
- package/lib/module/Scanner/ScanToolbar.js +6 -3
- package/lib/module/Scanner/ScanToolbar.js.map +1 -1
- package/lib/module/Scanner/ScanTopBar.js +6 -3
- package/lib/module/Scanner/ScanTopBar.js.map +1 -1
- package/lib/module/Scanner/Scanner.js +9 -311
- package/lib/module/Scanner/Scanner.js.map +1 -1
- package/lib/module/Scanner/Scanner.web.js +29 -0
- package/lib/module/Scanner/Scanner.web.js.map +1 -0
- package/lib/module/Scanner/ScannerInner.js +310 -0
- package/lib/module/Scanner/ScannerInner.js.map +1 -0
- package/lib/module/Scanner/styles.js +11 -0
- package/lib/module/Scanner/styles.js.map +1 -0
- package/lib/module/Scanner/types.js +4 -0
- package/lib/module/Scanner/types.js.map +1 -0
- package/lib/module/constants.js +20 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/decodeImage.js +29 -24
- package/lib/module/decodeImage.js.map +1 -1
- package/lib/module/decodeImage.web.js +7 -0
- package/lib/module/decodeImage.web.js.map +1 -0
- package/lib/module/errors.js +24 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/format.js +53 -47
- package/lib/module/format.js.map +1 -1
- package/lib/module/index.js +5 -19
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +8 -0
- package/lib/module/index.web.js.map +1 -0
- package/lib/module/mock.js +2 -23
- package/lib/module/mock.js.map +1 -1
- package/lib/module/permissions.js +24 -20
- package/lib/module/permissions.js.map +1 -1
- package/lib/module/permissions.web.js +10 -0
- package/lib/module/permissions.web.js.map +1 -0
- package/lib/module/types.js +1 -45
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/src/HmsScanView/HmsScanView.d.ts +3 -0
- package/lib/typescript/src/HmsScanView/HmsScanView.d.ts.map +1 -0
- package/lib/typescript/src/HmsScanView/HmsScanView.web.d.ts +3 -0
- package/lib/typescript/src/HmsScanView/HmsScanView.web.d.ts.map +1 -0
- package/lib/typescript/src/HmsScanView/types.d.ts +12 -0
- package/lib/typescript/src/HmsScanView/types.d.ts.map +1 -0
- package/lib/typescript/src/HmsScanViewNativeComponent.d.ts +6 -4
- package/lib/typescript/src/HmsScanViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/NativeHmsScan.d.ts +3 -5
- package/lib/typescript/src/NativeHmsScan.d.ts.map +1 -1
- package/lib/typescript/src/ScanError.d.ts +7 -0
- package/lib/typescript/src/ScanError.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/DeniedOverlay.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/ResultFail.d.ts +2 -1
- package/lib/typescript/src/Scanner/ResultFail.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/ResultFocus/ResultFocus.d.ts +4 -0
- package/lib/typescript/src/Scanner/ResultFocus/ResultFocus.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ResultFocus/styles.d.ts +36 -0
- package/lib/typescript/src/Scanner/ResultFocus/styles.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ResultFocus/types.d.ts +8 -0
- package/lib/typescript/src/Scanner/ResultFocus/types.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ScanHint.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/ScanToolbar.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/ScanTopBar.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/Scanner.d.ts +2 -45
- package/lib/typescript/src/Scanner/Scanner.d.ts.map +1 -1
- package/lib/typescript/src/Scanner/Scanner.web.d.ts +3 -0
- package/lib/typescript/src/Scanner/Scanner.web.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/ScannerInner.d.ts +3 -0
- package/lib/typescript/src/Scanner/ScannerInner.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/styles.d.ts +7 -0
- package/lib/typescript/src/Scanner/styles.d.ts.map +1 -0
- package/lib/typescript/src/Scanner/types.d.ts +20 -0
- package/lib/typescript/src/Scanner/types.d.ts.map +1 -0
- package/lib/typescript/src/constants.d.ts +8 -0
- package/lib/typescript/src/constants.d.ts.map +1 -0
- package/lib/typescript/src/decodeImage.d.ts +2 -12
- package/lib/typescript/src/decodeImage.d.ts.map +1 -1
- package/lib/typescript/src/decodeImage.web.d.ts +3 -0
- package/lib/typescript/src/decodeImage.web.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +6 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/format.d.ts +15 -12
- package/lib/typescript/src/format.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +6 -7
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +9 -0
- package/lib/typescript/src/index.web.d.ts.map +1 -0
- package/lib/typescript/src/mock.d.ts +9 -8
- package/lib/typescript/src/mock.d.ts.map +1 -1
- package/lib/typescript/src/permissions.d.ts +4 -8
- package/lib/typescript/src/permissions.d.ts.map +1 -1
- package/lib/typescript/src/permissions.web.d.ts +4 -0
- package/lib/typescript/src/permissions.web.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +31 -56
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +9 -2
- package/src/HmsScanView/HmsScanView.tsx +67 -0
- package/src/HmsScanView/HmsScanView.web.tsx +22 -0
- package/src/HmsScanView/types.ts +17 -0
- package/src/HmsScanViewNativeComponent.ts +7 -5
- package/src/NativeHmsScan.ts +3 -5
- package/src/ScanError.ts +13 -0
- package/src/Scanner/DeniedOverlay.tsx +4 -2
- package/src/Scanner/ResultFail.tsx +23 -5
- package/src/Scanner/ResultFocus/ResultFocus.tsx +43 -0
- package/src/Scanner/ResultFocus/styles.ts +25 -0
- package/src/Scanner/ResultFocus/types.ts +8 -0
- package/src/Scanner/ScanHint.tsx +16 -11
- package/src/Scanner/ScanToolbar.tsx +35 -13
- package/src/Scanner/ScanTopBar.tsx +20 -12
- package/src/Scanner/Scanner.tsx +13 -436
- package/src/Scanner/Scanner.web.tsx +26 -0
- package/src/Scanner/ScannerInner.tsx +391 -0
- package/src/Scanner/styles.ts +6 -0
- package/src/Scanner/types.ts +36 -0
- package/src/constants.ts +55 -0
- package/src/decodeImage.ts +35 -29
- package/src/decodeImage.web.ts +8 -0
- package/src/errors.ts +26 -0
- package/src/format.ts +77 -76
- package/src/index.tsx +17 -32
- package/src/index.web.tsx +22 -0
- package/src/mock.ts +21 -29
- package/src/permissions.ts +33 -20
- package/src/permissions.web.ts +9 -0
- package/src/types.ts +38 -126
- package/lib/module/HmsScanView.js +0 -43
- package/lib/module/HmsScanView.js.map +0 -1
- package/lib/module/Scanner/ResultFocus.js +0 -171
- package/lib/module/Scanner/ResultFocus.js.map +0 -1
- package/lib/typescript/src/HmsScanView.d.ts +0 -33
- package/lib/typescript/src/HmsScanView.d.ts.map +0 -1
- package/lib/typescript/src/Scanner/ResultFocus.d.ts +0 -14
- package/lib/typescript/src/Scanner/ResultFocus.d.ts.map +0 -1
- package/src/HmsScanView.tsx +0 -87
- package/src/Scanner/ResultFocus.tsx +0 -122
package/README.md
CHANGED
|
@@ -1,91 +1,43 @@
|
|
|
1
1
|
# @unif/react-native-hms-scan
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://github.com/unif-design/react-native-hms-scan/actions/workflows/ci.yml)
|
|
5
|
-
[](LICENSE)
|
|
6
|
-
[](https://unif-design.github.io/react-native-hms-scan/)
|
|
3
|
+
基于 HUAWEI Scan Kit 的 React Native 扫码库,支持实时扫码和本地图片识别。
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
[文档站](https://unif-design.github.io/react-native-hms-scan/) · [npm](https://www.npmjs.com/package/@unif/react-native-hms-scan) · [示例](example/README.md)
|
|
9
6
|
|
|
10
|
-
##
|
|
7
|
+
## 提供什么
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- 仅支持 **RN 新架构**;`@unif/react-native-design` 风格统一。
|
|
9
|
+
| 入口 | 用途 |
|
|
10
|
+
| ------------- | -------------------------------------- |
|
|
11
|
+
| `Scanner` | 带权限流程、取景和结果确认的扫一扫页面 |
|
|
12
|
+
| `HmsScanView` | 仅提供预览与事件,由应用组合界面 |
|
|
13
|
+
| `decodeImage` | 识别本地图片中的二维码或条码 |
|
|
18
14
|
|
|
19
15
|
## 安装
|
|
20
16
|
|
|
21
17
|
```sh
|
|
22
|
-
yarn add @unif/react-native-hms-scan
|
|
23
|
-
cd ios && pod install
|
|
18
|
+
yarn add @unif/react-native-hms-scan
|
|
24
19
|
```
|
|
25
20
|
|
|
26
|
-
|
|
21
|
+
按[安装指南](website/docs/getting-started/installation.md)补齐 peer 依赖、相机权限和原生配置:Android 需要 Huawei Maven;iOS 使用官方 ScanKit CocoaPod。
|
|
27
22
|
|
|
28
|
-
Android
|
|
23
|
+
Android 内置识别引擎,不要求华为手机或 HMS Core。两端均不需要 AppGallery Connect 或 API Key。当前 iOS SDK 仅支持真机,React Native 需开启新架构。
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
allprojects {
|
|
32
|
-
repositories {
|
|
33
|
-
maven { url 'https://developer.huawei.com/repo/' }
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
`react-native-svg` 是 peer 依赖(`<Scanner>` 图标用它绘制);`<Scanner>` 还依赖 peer `@unif/react-native-design`。宿主需开启新架构。完整 peer dependencies、Android 权限、iOS `NSCameraUsageDescription` 等见[文档站 · 安装](https://unif-design.github.io/react-native-hms-scan/docs/getting-started/installation)。
|
|
39
|
-
|
|
40
|
-
## 快速开始
|
|
25
|
+
## 最小用法
|
|
41
26
|
|
|
42
|
-
|
|
27
|
+
```ts
|
|
28
|
+
import { decodeImage } from '@unif/react-native-hms-scan';
|
|
43
29
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<Scanner
|
|
50
|
-
title="扫一扫"
|
|
51
|
-
onClose={() => navigation.goBack()}
|
|
52
|
-
resolveProduct={async (r: ScanResult): Promise<ScanProduct | null> => {
|
|
53
|
-
const p = await api.lookupByBarcode(r.value); // 你来解析;返回 null = 未识别
|
|
54
|
-
return p ? { name: p.name, brand: p.brand, price: `¥${p.price}`, barcode: r.value } : null;
|
|
55
|
-
}}
|
|
56
|
-
onConfirm={(product, result) => navigation.navigate('Order', { product, barcode: result.value })}
|
|
57
|
-
/>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
30
|
+
const results = await decodeImage({
|
|
31
|
+
uri: 'file:///path/to/photo.jpg',
|
|
32
|
+
formats: ['QR_CODE'],
|
|
33
|
+
});
|
|
60
34
|
```
|
|
61
35
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
## Example 能力展厅
|
|
65
|
-
|
|
66
|
-
仓库内的 [`example/`](example/) 是可直接运行的三能力展示应用,而非 library API 的一部分:
|
|
67
|
-
|
|
68
|
-
- **Scanner 成品页** — 配置码制、`autoConfirm`、相册选图与商品确认流。
|
|
69
|
-
- **HmsScanView 自定义页** — 展示由宿主管理的权限、`paused` / `continuous` / `torch`、结果和错误状态。
|
|
70
|
-
- **decodeImage 图片识别** — 从设备上的本地图片选择并离线解码,展示成功、空数组和 `HmsScanError`。
|
|
71
|
-
|
|
72
|
-
运行、原生配置、平台边界和测试矩阵见 [example/README.md](example/README.md)。示例的 image-picker 与本地商品表仅用于演示,接入应用应替换为自己的选择器和业务数据。
|
|
73
|
-
|
|
74
|
-
## 文档
|
|
75
|
-
|
|
76
|
-
- **完整文档**(安装 · 平台配置 · API · 平台差异 · 故障排查):https://unif-design.github.io/react-native-hms-scan/
|
|
77
|
-
- **AI / Agent**(按需 fetch,别凭记忆猜 API):[llms.txt](https://unif-design.github.io/react-native-hms-scan/llms.txt) · [llms-full.txt](https://unif-design.github.io/react-native-hms-scan/llms-full.txt)
|
|
78
|
-
- **Agent Skill** `hms-scan`(`unif` plugin):`/plugin marketplace add unif-design/skills` → `/plugin install unif@skills`
|
|
79
|
-
|
|
80
|
-
## 兼容性
|
|
81
|
-
|
|
82
|
-
| 平台 | 支持 |
|
|
83
|
-
| --- | --- |
|
|
84
|
-
| React Native | 新架构(Fabric + TurboModule)**必须开启**;在 RN 0.86.3 / React 19.2.3 上开发与验证 |
|
|
85
|
-
| Android | ✅ minSdkVersion ≥ 24(Android 7.0) |
|
|
86
|
-
| iOS | ✅ 官方 CocoaPod `ScanKitFrameWork 1.1.2.305` + 真机 |
|
|
87
|
-
| iOS Simulator | ❌ 不支持;无硬件逻辑测试使用随包 Jest mock |
|
|
36
|
+
正常识别但没有发现码时返回 `[]`;读取或解码失败抛出 `ScanError`。函数不下载网络图片,应用需先准备本地文件。
|
|
88
37
|
|
|
89
|
-
##
|
|
38
|
+
## 文档与开发
|
|
90
39
|
|
|
91
|
-
|
|
40
|
+
- [Scanner](website/docs/api/scanner.md) · [HmsScanView](website/docs/api/hms-scan-view.md) · [函数 API](website/docs/api/functions.md)
|
|
41
|
+
- [运行示例](example/README.md) · [开发资料与新版本契约](docs/DEVELOPMENT.md)
|
|
42
|
+
- [AI 文档索引](https://unif-design.github.io/react-native-hms-scan/llms.txt)
|
|
43
|
+
- [研发技能](https://github.com/unif-skill/unif-portal-dev-skills) · [MIT 许可](LICENSE)
|
|
@@ -3,15 +3,6 @@
|
|
|
3
3
|
<!-- Camera preview / scanning (RemoteView). -->
|
|
4
4
|
<uses-permission android:name="android.permission.CAMERA" />
|
|
5
5
|
|
|
6
|
-
<!-- Reading a local image for decodeImage().
|
|
7
|
-
Android 13+ (API 33) uses the scoped media permission; older devices fall
|
|
8
|
-
back to READ_EXTERNAL_STORAGE (capped at API 32 so the merger doesn't
|
|
9
|
-
force it on newer OS versions). -->
|
|
10
|
-
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
|
11
|
-
<uses-permission
|
|
12
|
-
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
|
13
|
-
android:maxSdkVersion="32" />
|
|
14
|
-
|
|
15
6
|
<!-- Camera is used but not strictly required (gallery decode works without it). -->
|
|
16
7
|
<uses-feature
|
|
17
8
|
android:name="android.hardware.camera"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
package com.unif.reactnativehmsscan
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.BitmapFactory
|
|
5
|
+
import android.graphics.Matrix
|
|
6
|
+
import android.media.ExifInterface
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import java.io.Closeable
|
|
9
|
+
import java.io.File
|
|
10
|
+
import java.io.FileInputStream
|
|
11
|
+
import java.io.IOException
|
|
12
|
+
|
|
13
|
+
internal class ScanImageFailure(
|
|
14
|
+
val code: String,
|
|
15
|
+
message: String,
|
|
16
|
+
) : IOException(message)
|
|
17
|
+
|
|
18
|
+
/** Owns one upright bitmap; points are returned in the upright original image. */
|
|
19
|
+
internal class HmsScanImage private constructor(
|
|
20
|
+
val bitmap: Bitmap,
|
|
21
|
+
val scaleX: Double,
|
|
22
|
+
val scaleY: Double,
|
|
23
|
+
) : Closeable {
|
|
24
|
+
override fun close() {
|
|
25
|
+
if (!bitmap.isRecycled) bitmap.recycle()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
companion object {
|
|
29
|
+
// At most 32 MiB per ARGB bitmap, temporarily 64 MiB during EXIF rotation.
|
|
30
|
+
// The vendor has additional working memory. Only one image job runs at once.
|
|
31
|
+
private const val MAX_PIXELS = 8_388_608L
|
|
32
|
+
private const val MAX_SOURCE_PIXELS = 268_435_456L
|
|
33
|
+
private const val MAX_SOURCE_EDGE = 32_768
|
|
34
|
+
private const val MAX_FILE_BYTES = 128L * 1024 * 1024
|
|
35
|
+
|
|
36
|
+
fun read(uri: String): HmsScanImage {
|
|
37
|
+
val parsed = Uri.parse(uri)
|
|
38
|
+
val path = parsed.path
|
|
39
|
+
if (!parsed.scheme.equals("file", ignoreCase = true) || path.isNullOrEmpty()) {
|
|
40
|
+
throw ScanImageFailure("E_INVALID_INPUT", "需要可读的 file URI")
|
|
41
|
+
}
|
|
42
|
+
val file = File(path)
|
|
43
|
+
if (!file.isFile || !file.canRead()) {
|
|
44
|
+
throw ScanImageFailure("E_IMAGE_LOAD_FAILED", "无法读取本地图片")
|
|
45
|
+
}
|
|
46
|
+
if (file.length() > MAX_FILE_BYTES) {
|
|
47
|
+
throw ScanImageFailure("E_INVALID_INPUT", "图片文件超过 128 MiB")
|
|
48
|
+
}
|
|
49
|
+
val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
50
|
+
BitmapFactory.decodeFile(path, bounds)
|
|
51
|
+
val width = bounds.outWidth
|
|
52
|
+
val height = bounds.outHeight
|
|
53
|
+
if (width <= 0 || height <= 0) {
|
|
54
|
+
throw ScanImageFailure("E_IMAGE_LOAD_FAILED", "无法读取图片尺寸")
|
|
55
|
+
}
|
|
56
|
+
if (width > MAX_SOURCE_EDGE || height > MAX_SOURCE_EDGE || width.toLong() * height > MAX_SOURCE_PIXELS) {
|
|
57
|
+
throw ScanImageFailure("E_INVALID_INPUT", "图片尺寸超过安全解码范围")
|
|
58
|
+
}
|
|
59
|
+
var sample = 1
|
|
60
|
+
while (((width + sample - 1) / sample).toLong() * ((height + sample - 1) / sample) > MAX_PIXELS) {
|
|
61
|
+
sample *= 2
|
|
62
|
+
}
|
|
63
|
+
val options =
|
|
64
|
+
BitmapFactory.Options().apply {
|
|
65
|
+
inSampleSize = sample
|
|
66
|
+
inPreferredConfig = Bitmap.Config.ARGB_8888
|
|
67
|
+
}
|
|
68
|
+
var bitmap =
|
|
69
|
+
BitmapFactory.decodeFile(path, options)
|
|
70
|
+
?: throw ScanImageFailure("E_IMAGE_LOAD_FAILED", "无法解码本地图片")
|
|
71
|
+
try {
|
|
72
|
+
if (bitmap.width.toLong() * bitmap.height > MAX_PIXELS || bitmap.allocationByteCount.toLong() > MAX_PIXELS * 4) {
|
|
73
|
+
throw ScanImageFailure("E_INVALID_INPUT", "图片解码超过内存预算")
|
|
74
|
+
}
|
|
75
|
+
// Unsupported/missing optional EXIF does not discard an otherwise valid image.
|
|
76
|
+
val orientation =
|
|
77
|
+
try {
|
|
78
|
+
FileInputStream(file).use {
|
|
79
|
+
ExifInterface(it).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
|
|
80
|
+
}
|
|
81
|
+
} catch (_: IOException) {
|
|
82
|
+
ExifInterface.ORIENTATION_NORMAL
|
|
83
|
+
}
|
|
84
|
+
val matrix = Matrix()
|
|
85
|
+
when (orientation) {
|
|
86
|
+
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> {
|
|
87
|
+
matrix.setScale(-1f, 1f)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ExifInterface.ORIENTATION_ROTATE_180 -> {
|
|
91
|
+
matrix.setRotate(180f)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
|
|
95
|
+
matrix.setScale(1f, -1f)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
ExifInterface.ORIENTATION_TRANSPOSE -> {
|
|
99
|
+
matrix.setRotate(90f)
|
|
100
|
+
matrix.postScale(-1f, 1f)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ExifInterface.ORIENTATION_ROTATE_90 -> {
|
|
104
|
+
matrix.setRotate(90f)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
ExifInterface.ORIENTATION_TRANSVERSE -> {
|
|
108
|
+
matrix.setRotate(270f)
|
|
109
|
+
matrix.postScale(-1f, 1f)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ExifInterface.ORIENTATION_ROTATE_270 -> {
|
|
113
|
+
matrix.setRotate(270f)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (orientation in ExifInterface.ORIENTATION_FLIP_HORIZONTAL..ExifInterface.ORIENTATION_ROTATE_270) {
|
|
117
|
+
val upright = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
|
|
118
|
+
if (upright !== bitmap) {
|
|
119
|
+
bitmap.recycle()
|
|
120
|
+
bitmap = upright
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
val swapsAxes = orientation in ExifInterface.ORIENTATION_TRANSPOSE..ExifInterface.ORIENTATION_ROTATE_270
|
|
124
|
+
return HmsScanImage(
|
|
125
|
+
bitmap,
|
|
126
|
+
(if (swapsAxes) height else width).toDouble() / bitmap.width,
|
|
127
|
+
(if (swapsAxes) width else height).toDouble() / bitmap.height,
|
|
128
|
+
)
|
|
129
|
+
} catch (error: Throwable) {
|
|
130
|
+
if (!bitmap.isRecycled) bitmap.recycle()
|
|
131
|
+
throw error
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -3,200 +3,216 @@ package com.unif.reactnativehmsscan
|
|
|
3
3
|
import android.Manifest
|
|
4
4
|
import android.app.Activity
|
|
5
5
|
import android.content.pm.PackageManager
|
|
6
|
-
import android.graphics.Bitmap
|
|
7
|
-
import android.graphics.BitmapFactory
|
|
8
|
-
import android.net.Uri
|
|
9
6
|
import androidx.core.app.ActivityCompat
|
|
10
7
|
import androidx.core.content.ContextCompat
|
|
8
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
11
9
|
import com.facebook.react.bridge.Promise
|
|
12
10
|
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
13
12
|
import com.facebook.react.modules.core.PermissionAwareActivity
|
|
14
13
|
import com.facebook.react.modules.core.PermissionListener
|
|
15
14
|
import com.huawei.hms.hmsscankit.ScanUtil
|
|
16
15
|
import com.huawei.hms.ml.scan.HmsScan
|
|
17
16
|
import com.huawei.hms.ml.scan.HmsScanAnalyzerOptions
|
|
18
|
-
import java.
|
|
19
|
-
import java.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* - getCameraPermissionStatus() / requestCameraPermission(): camera permission.
|
|
25
|
-
*/
|
|
17
|
+
import java.util.concurrent.ArrayBlockingQueue
|
|
18
|
+
import java.util.concurrent.RejectedExecutionException
|
|
19
|
+
import java.util.concurrent.ThreadPoolExecutor
|
|
20
|
+
import java.util.concurrent.TimeUnit
|
|
21
|
+
|
|
22
|
+
/** Independent image decoding and camera permission boundaries. */
|
|
26
23
|
class HmsScanModule(
|
|
27
24
|
reactContext: ReactApplicationContext,
|
|
28
|
-
) : NativeHmsScanSpec(reactContext)
|
|
29
|
-
|
|
25
|
+
) : NativeHmsScanSpec(reactContext),
|
|
26
|
+
LifecycleEventListener {
|
|
27
|
+
// One allocated image at a time, with at most four waiting file borrowers.
|
|
28
|
+
private val imageQueue =
|
|
29
|
+
ThreadPoolExecutor(1, 1, 0, TimeUnit.MILLISECONDS, ArrayBlockingQueue(4)) { work ->
|
|
30
|
+
Thread(work, "HmsScan-image").apply { isDaemon = true }
|
|
31
|
+
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
@Volatile private var invalidated = false
|
|
34
|
+
|
|
35
|
+
// Permission requests and host callbacks are serialized on the UI thread.
|
|
36
|
+
private class PermissionBatch(
|
|
37
|
+
val activity: Activity,
|
|
38
|
+
val promises: MutableList<Promise>,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
private var permissionBatch: PermissionBatch? = null
|
|
42
|
+
|
|
43
|
+
override fun getName(): String = NAME
|
|
32
44
|
|
|
33
|
-
/**
|
|
34
|
-
* Decode barcodes/QR codes from a local image. Loads the bitmap from a
|
|
35
|
-
* file:// / content:// URI or an absolute path, runs ScanUtil.decodeWithBitmap
|
|
36
|
-
* (photo mode), and resolves the ScanResult[] JSON. Never rejects on "no code
|
|
37
|
-
* found" — it resolves an empty array, matching the JS contract.
|
|
38
|
-
*/
|
|
39
45
|
override fun decodeImage(
|
|
40
46
|
uri: String,
|
|
41
47
|
formatsCsv: String,
|
|
42
48
|
promise: Promise,
|
|
43
49
|
) {
|
|
44
|
-
val
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
val work = ImageWork(uri, formatsCsv, promise)
|
|
51
|
+
synchronized(imageQueue) {
|
|
52
|
+
if (invalidated) {
|
|
53
|
+
work.rejectUnavailable()
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
imageQueue.execute(work)
|
|
58
|
+
} catch (_: RejectedExecutionException) {
|
|
59
|
+
work.rejectUnavailable()
|
|
60
|
+
}
|
|
48
61
|
}
|
|
62
|
+
}
|
|
49
63
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
64
|
+
private inner class ImageWork(
|
|
65
|
+
val uri: String,
|
|
66
|
+
val formatsCsv: String,
|
|
67
|
+
val promise: Promise,
|
|
68
|
+
) : Runnable {
|
|
69
|
+
fun rejectUnavailable() {
|
|
70
|
+
promise.reject("E_UNAVAILABLE", "图片识别不可用或等待队列已满")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override fun run() {
|
|
74
|
+
if (invalidated) {
|
|
75
|
+
rejectUnavailable()
|
|
76
|
+
return
|
|
59
77
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
try {
|
|
79
|
+
// use closes the bitmap before the promise settles and the caller releases its file.
|
|
80
|
+
val json =
|
|
81
|
+
HmsScanImage.read(uri).use { image ->
|
|
82
|
+
val types = HmsScanResultMapper.parseFormatsCsv(formatsCsv)
|
|
83
|
+
val creator = HmsScanAnalyzerOptions.Creator().setPhotoMode(true)
|
|
84
|
+
if (types != null && types.isNotEmpty()) {
|
|
85
|
+
creator.setHmsScanTypes(types.first(), *types.drop(1).toIntArray())
|
|
86
|
+
} else {
|
|
87
|
+
creator.setHmsScanTypes(HmsScan.ALL_SCAN_TYPE)
|
|
88
|
+
}
|
|
89
|
+
val scans = ScanUtil.decodeWithBitmap(reactApplicationContext, image.bitmap, creator.create())
|
|
90
|
+
HmsScanResultMapper.toJson(scans, image.scaleX, image.scaleY)
|
|
91
|
+
}
|
|
92
|
+
promise.resolve(json)
|
|
93
|
+
} catch (error: ScanImageFailure) {
|
|
94
|
+
promise.reject(error.code, error.message, error)
|
|
95
|
+
} catch (error: InvalidScanResponse) {
|
|
96
|
+
promise.reject("E_INVALID_RESPONSE", error.message, error)
|
|
97
|
+
} catch (error: Exception) {
|
|
98
|
+
promise.reject("E_DECODE_FAILED", error.message ?: "图片识别失败", error)
|
|
99
|
+
} catch (error: OutOfMemoryError) {
|
|
100
|
+
promise.reject("E_DECODE_FAILED", "图片识别内存不足", error)
|
|
69
101
|
}
|
|
70
102
|
}
|
|
71
103
|
}
|
|
72
104
|
|
|
73
|
-
/**
|
|
74
|
-
|
|
105
|
+
/** Android does not distinguish never asked from blocked in a read-only query. */
|
|
106
|
+
override fun getCameraPermissionStatus(promise: Promise) {
|
|
75
107
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
var stream: InputStream? = null
|
|
82
|
-
try {
|
|
83
|
-
stream = resolver.openInputStream(parsed)
|
|
84
|
-
if (stream == null) null else BitmapFactory.decodeStream(stream)
|
|
85
|
-
} finally {
|
|
86
|
-
stream?.close()
|
|
87
|
-
}
|
|
88
|
-
}
|
|
108
|
+
promise.resolve(if (hasCameraPermission()) GRANTED else DENIED)
|
|
109
|
+
} catch (error: Exception) {
|
|
110
|
+
promise.reject("E_UNAVAILABLE", "读取相机权限失败", error)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
89
113
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
114
|
+
override fun requestCameraPermission(promise: Promise) {
|
|
115
|
+
UiThreadUtil.runOnUiThread {
|
|
116
|
+
if (invalidated) {
|
|
117
|
+
promise.reject("E_UNAVAILABLE", "扫码模块已释放")
|
|
118
|
+
return@runOnUiThread
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
if (hasCameraPermission()) {
|
|
122
|
+
promise.resolve(GRANTED)
|
|
123
|
+
return@runOnUiThread
|
|
94
124
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
125
|
+
val activity = reactApplicationContext.currentActivity
|
|
126
|
+
if (activity !is PermissionAwareActivity) {
|
|
127
|
+
promise.reject("E_NO_ACTIVITY", "没有可申请相机权限的界面")
|
|
128
|
+
return@runOnUiThread
|
|
129
|
+
}
|
|
130
|
+
permissionBatch?.let { pending ->
|
|
131
|
+
if (pending.activity === activity) {
|
|
132
|
+
pending.promises.add(promise)
|
|
133
|
+
return@runOnUiThread
|
|
134
|
+
}
|
|
135
|
+
rejectPermissions(pending, "E_NO_ACTIVITY", "相机权限所属界面已结束")
|
|
136
|
+
}
|
|
137
|
+
val batch = PermissionBatch(activity, mutableListOf(promise))
|
|
138
|
+
permissionBatch = batch
|
|
139
|
+
reactApplicationContext.addLifecycleEventListener(this)
|
|
140
|
+
val listener =
|
|
141
|
+
PermissionListener { requestCode, _, grantResults ->
|
|
142
|
+
if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
|
|
143
|
+
return@PermissionListener false
|
|
144
|
+
}
|
|
145
|
+
if (permissionBatch !== batch) return@PermissionListener true
|
|
146
|
+
if (grantResults.isEmpty()) {
|
|
147
|
+
rejectPermissions(batch, "E_UNAVAILABLE", "相机权限申请未完成")
|
|
148
|
+
} else {
|
|
149
|
+
val status =
|
|
150
|
+
when {
|
|
151
|
+
grantResults[0] == PackageManager.PERMISSION_GRANTED -> GRANTED
|
|
152
|
+
ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.CAMERA) -> DENIED
|
|
153
|
+
else -> BLOCKED
|
|
154
|
+
}
|
|
155
|
+
finishPermissions(batch).forEach { it.resolve(status) }
|
|
156
|
+
}
|
|
157
|
+
true
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
activity.requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE, listener)
|
|
161
|
+
} catch (error: Exception) {
|
|
162
|
+
rejectPermissions(batch, "E_UNAVAILABLE", error.message ?: "申请相机权限失败")
|
|
99
163
|
}
|
|
164
|
+
} catch (error: Exception) {
|
|
165
|
+
promise.reject("E_UNAVAILABLE", "申请相机权限失败", error)
|
|
100
166
|
}
|
|
101
|
-
} catch (e: Throwable) {
|
|
102
|
-
null
|
|
103
167
|
}
|
|
168
|
+
}
|
|
104
169
|
|
|
105
|
-
private fun
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
170
|
+
private fun finishPermissions(batch: PermissionBatch): List<Promise> {
|
|
171
|
+
if (permissionBatch !== batch) return emptyList()
|
|
172
|
+
permissionBatch = null
|
|
173
|
+
reactApplicationContext.removeLifecycleEventListener(this)
|
|
174
|
+
return batch.promises.toList().also { batch.promises.clear() }
|
|
109
175
|
}
|
|
110
176
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
* "blocked" / "undetermined" from a plain denial (shouldShowRequestPermissionRationale
|
|
118
|
-
* is false for both the never-asked and permanently-denied states), so a
|
|
119
|
-
* not-granted result is reported as "denied". The blocked/undetermined nuance is
|
|
120
|
-
* resolved by requestCameraPermission() via the post-request rationale check.
|
|
121
|
-
*/
|
|
122
|
-
override fun getCameraPermissionStatus(promise: Promise) {
|
|
123
|
-
promise.resolve(if (hasCameraPermission()) GRANTED else DENIED)
|
|
177
|
+
private fun rejectPermissions(
|
|
178
|
+
batch: PermissionBatch,
|
|
179
|
+
code: String,
|
|
180
|
+
message: String,
|
|
181
|
+
) {
|
|
182
|
+
finishPermissions(batch).forEach { it.reject(code, message) }
|
|
124
183
|
}
|
|
125
184
|
|
|
126
|
-
|
|
127
|
-
* Request the camera permission (system dialog if needed) and resolve the
|
|
128
|
-
* resulting status:
|
|
129
|
-
* - granted: permission held.
|
|
130
|
-
* - denied: rejected but the app may ask again (rationale should be shown).
|
|
131
|
-
* - blocked: rejected with "don't ask again" (rationale will not be shown).
|
|
132
|
-
*/
|
|
133
|
-
override fun requestCameraPermission(promise: Promise) {
|
|
134
|
-
if (hasCameraPermission()) {
|
|
135
|
-
promise.resolve(GRANTED)
|
|
136
|
-
return
|
|
137
|
-
}
|
|
185
|
+
override fun onHostResume() = Unit
|
|
138
186
|
|
|
139
|
-
|
|
140
|
-
if (activity == null || activity !is PermissionAwareActivity) {
|
|
141
|
-
// Can't surface a system dialog without a PermissionAwareActivity; report the
|
|
142
|
-
// current (not-granted) state rather than hanging the promise.
|
|
143
|
-
promise.reject(
|
|
144
|
-
E_NO_ACTIVITY,
|
|
145
|
-
"Cannot request camera permission: no current PermissionAwareActivity",
|
|
146
|
-
)
|
|
147
|
-
return
|
|
148
|
-
}
|
|
187
|
+
override fun onHostPause() = Unit
|
|
149
188
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (requestCode != CAMERA_PERMISSION_REQUEST_CODE) {
|
|
154
|
-
return@PermissionListener false
|
|
155
|
-
}
|
|
156
|
-
val granted =
|
|
157
|
-
grantResults.isNotEmpty() &&
|
|
158
|
-
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
|
159
|
-
val status =
|
|
160
|
-
when {
|
|
161
|
-
granted -> GRANTED
|
|
162
|
-
|
|
163
|
-
// After a denial: rationale==true means the user can be asked again;
|
|
164
|
-
// rationale==false means "don't ask again" (blocked).
|
|
165
|
-
ActivityCompat.shouldShowRequestPermissionRationale(
|
|
166
|
-
activity,
|
|
167
|
-
Manifest.permission.CAMERA,
|
|
168
|
-
) -> DENIED
|
|
169
|
-
|
|
170
|
-
else -> BLOCKED
|
|
171
|
-
}
|
|
172
|
-
promise.resolve(status)
|
|
173
|
-
true
|
|
174
|
-
}
|
|
189
|
+
override fun onHostDestroy() {
|
|
190
|
+
permissionBatch?.let { rejectPermissions(it, "E_NO_ACTIVITY", "相机权限所属界面已结束") }
|
|
191
|
+
}
|
|
175
192
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
override fun invalidate() {
|
|
194
|
+
val waiting = mutableListOf<Runnable>()
|
|
195
|
+
synchronized(imageQueue) {
|
|
196
|
+
invalidated = true
|
|
197
|
+
imageQueue.queue.drainTo(waiting)
|
|
198
|
+
// Do not interrupt an active vendor read or return its file before it finishes.
|
|
199
|
+
imageQueue.shutdown()
|
|
200
|
+
}
|
|
201
|
+
waiting.forEach { (it as ImageWork).rejectUnavailable() }
|
|
202
|
+
UiThreadUtil.runOnUiThread {
|
|
203
|
+
permissionBatch?.let { rejectPermissions(it, "E_UNAVAILABLE", "扫码模块已释放") }
|
|
204
|
+
}
|
|
205
|
+
super.invalidate()
|
|
181
206
|
}
|
|
182
207
|
|
|
183
208
|
private fun hasCameraPermission(): Boolean =
|
|
184
|
-
ContextCompat.checkSelfPermission(
|
|
185
|
-
reactApplicationContext,
|
|
186
|
-
Manifest.permission.CAMERA,
|
|
187
|
-
) == PackageManager.PERMISSION_GRANTED
|
|
209
|
+
ContextCompat.checkSelfPermission(reactApplicationContext, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
|
188
210
|
|
|
189
211
|
companion object {
|
|
190
212
|
const val NAME = NativeHmsScanSpec.NAME
|
|
191
|
-
|
|
192
213
|
private const val GRANTED = "granted"
|
|
193
214
|
private const val DENIED = "denied"
|
|
194
215
|
private const val BLOCKED = "blocked"
|
|
195
|
-
|
|
196
|
-
private const val E_IMAGE_LOAD_FAILED = "E_IMAGE_LOAD_FAILED"
|
|
197
|
-
private const val E_DECODE_FAILED = "E_DECODE_FAILED"
|
|
198
|
-
private const val E_NO_ACTIVITY = "E_NO_ACTIVITY"
|
|
199
|
-
|
|
200
|
-
private const val CAMERA_PERMISSION_REQUEST_CODE = 0x48_4D // "HM"
|
|
216
|
+
private const val CAMERA_PERMISSION_REQUEST_CODE = 0x48_4D
|
|
201
217
|
}
|
|
202
218
|
}
|