@mentra/bluetooth-sdk 0.1.9 → 0.1.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.
Files changed (147) hide show
  1. package/README.md +116 -18
  2. package/android/build.gradle +289 -16
  3. package/android/gradle.properties.example +9 -0
  4. package/android/lc3Lib/build.gradle +95 -6
  5. package/android/settings.gradle +3 -0
  6. package/android/silero/LICENSE.md +17 -0
  7. package/android/silero/build.gradle +42 -0
  8. package/android/silero/proguard-rules.pro +24 -0
  9. package/android/silero/src/androidTest/assets/hello.wav +0 -0
  10. package/android/silero/src/androidTest/java/com/konovalov/vad/silero/VadSileroTest.kt +79 -0
  11. package/android/silero/src/main/assets/LICENSE +21 -0
  12. package/android/silero/src/main/assets/silero_vad.onnx +0 -0
  13. package/android/silero/src/main/java/com/konovalov/vad/silero/Vad.kt +160 -0
  14. package/android/silero/src/main/java/com/konovalov/vad/silero/VadSilero.kt +449 -0
  15. package/android/silero/src/main/java/com/konovalov/vad/silero/config/FrameSize.kt +15 -0
  16. package/android/silero/src/main/java/com/konovalov/vad/silero/config/Mode.kt +14 -0
  17. package/android/silero/src/main/java/com/konovalov/vad/silero/config/SampleRate.kt +12 -0
  18. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/AudioUtils.kt +47 -0
  19. package/android/silero/src/main/java/com/konovalov/vad/silero/utils/TensorMap.kt +41 -0
  20. package/android/silero/src/test/java/com/konovalov/vad/silero/utils/AudioUtilsTest.kt +44 -0
  21. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkDefaults.kt +6 -0
  22. package/android/src/main/java/com/mentra/bluetoothsdk/BluetoothSdkModule.kt +195 -34
  23. package/android/src/main/java/com/mentra/bluetoothsdk/Bridge.kt +103 -21
  24. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceManager.kt +210 -18
  25. package/android/src/main/java/com/mentra/bluetoothsdk/DeviceStore.kt +30 -2
  26. package/android/src/main/java/com/mentra/bluetoothsdk/MentraBluetoothSdk.kt +787 -85
  27. package/android/src/main/java/com/mentra/bluetoothsdk/audio/AudioModels.kt +6 -2
  28. package/android/src/main/java/com/mentra/bluetoothsdk/camera/CameraModels.kt +125 -11
  29. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/ControllerManager.kt +12 -2
  30. package/android/src/main/java/com/mentra/bluetoothsdk/controllers/R1.kt +8 -2
  31. package/android/src/main/java/com/mentra/bluetoothsdk/debug/BleTraceLogger.kt +33 -17
  32. package/android/src/main/java/com/mentra/bluetoothsdk/events/BluetoothEvents.kt +101 -0
  33. package/android/src/main/java/com/mentra/bluetoothsdk/internal/MapParsing.kt +8 -0
  34. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistry.java +108 -0
  35. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoUploadServer.kt +399 -74
  36. package/android/src/main/java/com/mentra/bluetoothsdk/photoreceiver/MentraPhotoReceiverModule.kt +100 -17
  37. package/android/src/main/java/com/mentra/bluetoothsdk/requests/DisplayRequests.kt +17 -0
  38. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G1.java +2 -2
  39. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/G2.kt +1293 -758
  40. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Mach1.java +2 -2
  41. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraLive.java +271 -66
  42. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraNex.kt +197 -149
  43. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/MentraosBle.java +12753 -27036
  44. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/SGCManager.kt +64 -2
  45. package/android/src/main/java/com/mentra/bluetoothsdk/sgcs/Simulated.kt +16 -4
  46. package/android/src/main/java/com/mentra/bluetoothsdk/status/DeviceStatus.kt +60 -3
  47. package/android/src/main/java/com/mentra/bluetoothsdk/streaming/StreamModels.kt +12 -3
  48. package/android/src/main/java/com/mentra/bluetoothsdk/stt/STTTools.kt +87 -36
  49. package/android/src/main/java/com/mentra/bluetoothsdk/stt/SherpaOnnxTranscriber.kt +13 -4
  50. package/android/src/main/java/com/mentra/bluetoothsdk/stt/VadGateSpeechPolicy.kt +229 -0
  51. package/android/src/main/java/com/mentra/bluetoothsdk/tts/TTSTools.kt +180 -0
  52. package/android/src/main/java/com/mentra/bluetoothsdk/utils/AvifExifStripper.java +573 -0
  53. package/android/src/main/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadService.java +488 -50
  54. package/android/src/main/java/com/mentra/bluetoothsdk/utils/HeifExifTagReader.java +199 -0
  55. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunkReassembler.java +18 -1
  56. package/android/src/main/java/com/mentra/bluetoothsdk/utils/MessageChunker.java +64 -18
  57. package/android/src/main/java/com/mentra/bluetoothsdk/utils/NexSGCUtils.kt +48 -39
  58. package/android/src/test/java/com/mentra/bluetoothsdk/camera/PhotoRequestTest.kt +23 -0
  59. package/android/src/test/java/com/mentra/bluetoothsdk/photoreceiver/LocalPhotoReceiverRegistryTest.java +75 -0
  60. package/android/src/test/java/com/mentra/bluetoothsdk/utils/AvifExifStripperTest.java +30 -0
  61. package/android/src/test/java/com/mentra/bluetoothsdk/utils/BlePhotoUploadServiceTest.java +98 -0
  62. package/android/src/test/resources/avif_with_exif.avif +0 -0
  63. package/build/BluetoothSdk.types.d.ts +253 -44
  64. package/build/BluetoothSdk.types.d.ts.map +1 -1
  65. package/build/BluetoothSdk.types.js +11 -10
  66. package/build/BluetoothSdk.types.js.map +1 -1
  67. package/build/_private/BluetoothSdkModule.d.ts +37 -23
  68. package/build/_private/BluetoothSdkModule.d.ts.map +1 -1
  69. package/build/_private/BluetoothSdkModule.js +43 -52
  70. package/build/_private/BluetoothSdkModule.js.map +1 -1
  71. package/build/_private/photoRequestPayload.d.ts +4 -0
  72. package/build/_private/photoRequestPayload.d.ts.map +1 -0
  73. package/build/_private/photoRequestPayload.js +25 -0
  74. package/build/_private/photoRequestPayload.js.map +1 -0
  75. package/build/index.d.ts +1 -1
  76. package/build/index.d.ts.map +1 -1
  77. package/build/index.js +25 -0
  78. package/build/index.js.map +1 -1
  79. package/build/react/useMentraBluetooth.d.ts +2 -2
  80. package/build/react/useMentraBluetooth.d.ts.map +1 -1
  81. package/build/react/useMentraBluetooth.js +3 -3
  82. package/build/react/useMentraBluetooth.js.map +1 -1
  83. package/ios/BluetoothSdkModule.swift +177 -70
  84. package/ios/LocalPhotoUploadServer.swift +370 -105
  85. package/ios/MentraBluetoothSDK.podspec +3 -1
  86. package/ios/MentraPhotoReceiverModule.swift +62 -10
  87. package/ios/Packages/CoreObjC/include/PcmConverter.h +22 -0
  88. package/ios/Packages/SherpaOnnx/SherpaOnnx.swift +668 -26
  89. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Info.plist +13 -9
  90. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
  91. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
  92. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/libsherpa-onnx.a +0 -0
  93. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/c-api.h +4551 -0
  94. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/Headers/sherpa-onnx/c-api/cxx-api.h +1745 -0
  95. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/libsherpa-onnx.a +0 -0
  96. package/ios/Source/BluetoothSdkDefaults.swift +6 -0
  97. package/ios/Source/Bridge.swift +68 -19
  98. package/ios/Source/DeviceManager.swift +167 -32
  99. package/ios/Source/DeviceStore.swift +24 -8
  100. package/ios/Source/MentraBluetoothSDK.swift +803 -47
  101. package/ios/Source/ObservableStore.swift +6 -0
  102. package/ios/Source/{Audio → audio}/AudioModels.swift +9 -7
  103. package/ios/Source/{Camera → camera}/CameraModels.swift +226 -16
  104. package/ios/Source/controllers/ControllerManager.swift +4 -3
  105. package/ios/Source/controllers/R1.swift +3 -3
  106. package/ios/Source/{Events → events}/BluetoothEvents.swift +183 -1
  107. package/ios/Source/{Internal → internal}/ValueParsing.swift +10 -1
  108. package/ios/Source/{Requests → requests}/DisplayRequests.swift +28 -4
  109. package/ios/Source/services/PhoneMic.swift +1 -0
  110. package/ios/Source/sgcs/G1.swift +5 -5
  111. package/ios/Source/sgcs/G2.swift +1494 -870
  112. package/ios/Source/sgcs/Mach1.swift +4 -4
  113. package/ios/Source/sgcs/MentraLive.swift +814 -321
  114. package/ios/Source/sgcs/MentraNex.swift +604 -244
  115. package/ios/Source/sgcs/SGCManager.swift +68 -5
  116. package/ios/Source/sgcs/Simulated.swift +9 -5
  117. package/ios/Source/sgcs/mentraos_ble.pb.swift +3073 -3134
  118. package/ios/Source/status/DeviceStatus.swift +974 -0
  119. package/ios/Source/{Status → status}/WifiHotspotStatus.swift +4 -4
  120. package/ios/Source/{Streaming → streaming}/StreamModels.swift +24 -9
  121. package/ios/Source/stt/STTTools.swift +27 -12
  122. package/ios/Source/stt/SherpaOnnxTranscriber.swift +31 -8
  123. package/ios/Source/tts/TTSTools.swift +164 -0
  124. package/ios/Source/utils/JSCExperiment.swift +7 -7
  125. package/ios/Source/utils/MessageChunkReassembler.swift +20 -1
  126. package/ios/Source/utils/MessageChunker.swift +78 -21
  127. package/ios/Source/utils/TarBz2Extractor.swift +53 -2
  128. package/package.json +1 -1
  129. package/plugin/build/withAndroid.js +68 -4
  130. package/src/BluetoothSdk.types.ts +344 -60
  131. package/src/_private/BluetoothSdkModule.ts +132 -102
  132. package/src/_private/photoRequestPayload.ts +28 -0
  133. package/src/index.ts +61 -15
  134. package/src/react/useMentraBluetooth.ts +6 -4
  135. package/android/.project +0 -28
  136. package/android/lc3Lib/.project +0 -28
  137. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/cargs.h +0 -162
  138. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/c-api.h +0 -1852
  139. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/Headers/sherpa-onnx/c-api/cxx-api.h +0 -674
  140. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64/sherpa-onnx.a +0 -0
  141. package/ios/Packages/SherpaOnnx/sherpa-onnx.xcframework/ios-arm64_x86_64-simulator/sherpa-onnx.a +0 -0
  142. package/ios/Source/Status/DeviceStatus.swift +0 -471
  143. /package/ios/Source/{Connection → connection}/ScanSession.swift +0 -0
  144. /package/ios/Source/{Errors → errors}/BluetoothError.swift +0 -0
  145. /package/ios/Source/{Internal → internal}/BluetoothAvailability.swift +0 -0
  146. /package/ios/Source/{Status → status}/RuntimeState.swift +0 -0
  147. /package/ios/Source/{Types → types}/DeviceModels.swift +0 -0
@@ -0,0 +1,573 @@
1
+ package com.mentra.bluetoothsdk.utils;
2
+
3
+ import java.io.ByteArrayOutputStream;
4
+ import java.io.IOException;
5
+ import java.nio.ByteBuffer;
6
+ import java.nio.ByteOrder;
7
+ import java.nio.charset.StandardCharsets;
8
+ import java.util.ArrayList;
9
+ import java.util.Arrays;
10
+ import java.util.List;
11
+
12
+ /**
13
+ * Removes the Exif metadata item from glasses BLE AVIF so {@link com.radzivon.bartoshyk.avif.coder.HeifCoder}
14
+ * can decode the image. EXIF is appended to {@code mdat} by the glasses encoder; libavif then fails if that
15
+ * trailer is present. IMU JSON must be read from the original bytes before stripping.
16
+ */
17
+ final class AvifExifStripper {
18
+ private AvifExifStripper() {}
19
+
20
+ static byte[] stripForDecode(byte[] avif) throws IOException {
21
+ if (avif.length < 12) {
22
+ return avif;
23
+ }
24
+ // Primary path: parse meta tables, remove Exif item from iloc/iinf/iref, truncate mdat.
25
+ try {
26
+ byte[] cleaned = stripUsingMetaTables(avif);
27
+ if (cleaned.length < avif.length) {
28
+ return cleaned;
29
+ }
30
+ } catch (IOException | RuntimeException ignored) {
31
+ // Malformed BMFF can throw runtime parse errors (buffer/index); fall through to the
32
+ // marker-based fallback rather than letting the decode crash.
33
+ }
34
+ // Fallback: truncate mdat at the Exif\0\0 marker AND purge Exif from meta tables.
35
+ int exifMarker = lastExifMarkerOffset(avif);
36
+ if (exifMarker < 0) {
37
+ return avif;
38
+ }
39
+ return stripMdatTailAndMeta(avif, exifMarker);
40
+ }
41
+
42
+ /**
43
+ * Truncates mdat at the {@code Exif\0\0} byte position, then removes the Exif item from
44
+ * iloc/iinf/iref so HeifCoder does not try to seek to the now-absent data.
45
+ */
46
+ private static byte[] stripMdatTailAndMeta(byte[] avif, int exifMarker) throws IOException {
47
+ List<BmffBox> top = parseTopLevel(avif);
48
+ BmffBox ftyp = require(top, "ftyp");
49
+ BmffBox meta = require(top, "meta");
50
+ BmffBox mdat = require(top, "mdat");
51
+
52
+ int fileOffset = 0;
53
+ for (BmffBox box : top) {
54
+ if ("mdat".equals(box.type)) {
55
+ int payloadStart = fileOffset + 8;
56
+ if (exifMarker >= payloadStart && exifMarker < payloadStart + box.payload.length) {
57
+ int newPayloadLen = exifMarker - payloadStart;
58
+ if (newPayloadLen <= 0) {
59
+ return avif;
60
+ }
61
+ byte[] newMdatPayload = Arrays.copyOf(box.payload, newPayloadLen);
62
+ byte[] metaPayload = normalizeMetaPayload(meta.payload);
63
+ int oldMetaBoxSize = meta.encodedSize();
64
+ // Best-effort: clean up Exif item refs from meta tables.
65
+ try {
66
+ MetaTables tables = parseMetaTables(metaPayload);
67
+ int exifItemId = findExifItemId(tables.iinfPayload);
68
+ if (exifItemId >= 0) {
69
+ byte[] newIloc = removeIlocEntry(tables.ilocPayload, exifItemId);
70
+ byte[] newIinf = removeIinfEntry(tables.iinfPayload, exifItemId);
71
+ byte[] newIref = removeCdscRefs(tables.irefPayload, exifItemId);
72
+ byte[] tempMetaPayload =
73
+ rebuildMeta(metaPayload, newIloc, newIinf, newIref);
74
+ int metaDelta =
75
+ new BmffBox("meta", tempMetaPayload).encodedSize()
76
+ - oldMetaBoxSize;
77
+ newIloc = shiftIlocExtentOffsets(newIloc, metaDelta);
78
+ metaPayload = rebuildMeta(metaPayload, newIloc, newIinf, newIref);
79
+ }
80
+ } catch (IOException ignored) {
81
+ }
82
+ ByteArrayOutputStream out = new ByteArrayOutputStream(avif.length);
83
+ ftyp.writeTo(out);
84
+ new BmffBox("meta", metaPayload).writeTo(out);
85
+ for (BmffBox other : top) {
86
+ if (!"ftyp".equals(other.type)
87
+ && !"meta".equals(other.type)
88
+ && !"mdat".equals(other.type)) {
89
+ other.writeTo(out);
90
+ }
91
+ }
92
+ new BmffBox("mdat", newMdatPayload).writeTo(out);
93
+ return out.toByteArray();
94
+ }
95
+ return avif;
96
+ }
97
+ fileOffset += 8 + box.payload.length;
98
+ }
99
+ return avif;
100
+ }
101
+
102
+ private static byte[] stripUsingMetaTables(byte[] avif) throws IOException {
103
+ List<BmffBox> top = parseTopLevel(avif);
104
+ BmffBox ftyp = require(top, "ftyp");
105
+ BmffBox meta = require(top, "meta");
106
+ BmffBox mdat = require(top, "mdat");
107
+
108
+ byte[] metaPayload = normalizeMetaPayload(meta.payload);
109
+ MetaTables tables = parseMetaTables(metaPayload);
110
+ int exifItemId = findExifItemId(tables.iinfPayload);
111
+ if (exifItemId < 0) {
112
+ return avif;
113
+ }
114
+
115
+ int exifLength = ilocItemLength(tables.ilocPayload, exifItemId);
116
+ if (exifLength <= 0 || mdat.payload.length < exifLength) {
117
+ return avif;
118
+ }
119
+
120
+ int mdatPayloadStart = locateMdatPayloadStart(avif);
121
+ int exifFileOffset = ilocItemOffset(tables.ilocPayload, exifItemId);
122
+ int exifOffsetInMdat = exifFileOffset - mdatPayloadStart;
123
+ int newMdatPayloadLength;
124
+ if (exifOffsetInMdat >= 0 && exifOffsetInMdat + exifLength == mdat.payload.length) {
125
+ newMdatPayloadLength = exifOffsetInMdat;
126
+ } else if (mdat.payload.length >= exifLength) {
127
+ newMdatPayloadLength = mdat.payload.length - exifLength;
128
+ } else {
129
+ return avif;
130
+ }
131
+ byte[] newMdatPayload = Arrays.copyOf(mdat.payload, newMdatPayloadLength);
132
+
133
+ byte[] newIloc = removeIlocEntry(tables.ilocPayload, exifItemId);
134
+ byte[] newIinf = removeIinfEntry(tables.iinfPayload, exifItemId);
135
+ byte[] newIref = removeCdscRefs(tables.irefPayload, exifItemId);
136
+ byte[] tempMetaPayload = rebuildMeta(metaPayload, newIloc, newIinf, newIref);
137
+ int metaDelta = new BmffBox("meta", tempMetaPayload).encodedSize() - meta.encodedSize();
138
+ newIloc = shiftIlocExtentOffsets(newIloc, metaDelta);
139
+ byte[] newMetaPayload = rebuildMeta(metaPayload, newIloc, newIinf, newIref);
140
+
141
+ ByteArrayOutputStream out = new ByteArrayOutputStream(avif.length);
142
+ ftyp.writeTo(out);
143
+ new BmffBox("meta", newMetaPayload).writeTo(out);
144
+ for (BmffBox box : top) {
145
+ if (!"ftyp".equals(box.type) && !"meta".equals(box.type) && !"mdat".equals(box.type)) {
146
+ box.writeTo(out);
147
+ }
148
+ }
149
+ new BmffBox("mdat", newMdatPayload).writeTo(out);
150
+ return out.toByteArray();
151
+ }
152
+
153
+ private static int lastExifMarkerOffset(byte[] data) {
154
+ byte[] marker = new byte[] {'E', 'x', 'i', 'f', 0, 0};
155
+ int last = -1;
156
+ outer:
157
+ for (int i = 0; i <= data.length - marker.length; i++) {
158
+ for (int j = 0; j < marker.length; j++) {
159
+ if (data[i + j] != marker[j]) {
160
+ continue outer;
161
+ }
162
+ }
163
+ last = i;
164
+ }
165
+ return last;
166
+ }
167
+
168
+ /** Older injector builds accidentally nested a full meta box inside meta payload. */
169
+ private static byte[] normalizeMetaPayload(byte[] metaPayload) {
170
+ if (metaPayload.length >= 8) {
171
+ String type = new String(metaPayload, 4, 4, StandardCharsets.ISO_8859_1);
172
+ if ("meta".equals(type)) {
173
+ int innerSize = u32(metaPayload, 0);
174
+ if (innerSize >= 8 && innerSize <= metaPayload.length) {
175
+ return Arrays.copyOfRange(metaPayload, 8, innerSize);
176
+ }
177
+ }
178
+ }
179
+ return metaPayload;
180
+ }
181
+
182
+ private static int findExifItemId(byte[] iinfPayload) {
183
+ int offset = 6;
184
+ while (offset + 8 <= iinfPayload.length) {
185
+ int size = u32(iinfPayload, offset);
186
+ if (size < 8 || offset + size > iinfPayload.length) {
187
+ break;
188
+ }
189
+ String type = new String(iinfPayload, offset + 4, 4, StandardCharsets.ISO_8859_1);
190
+ if ("infe".equals(type) && size >= 21) {
191
+ int version = iinfPayload[offset + 8] & 0xFF;
192
+ int id =
193
+ version >= 3
194
+ ? ByteBuffer.wrap(iinfPayload, offset + 12, 4)
195
+ .order(ByteOrder.BIG_ENDIAN)
196
+ .getInt()
197
+ : u16(iinfPayload, offset + 12);
198
+ if (offset + 16 + 4 <= iinfPayload.length) {
199
+ String itemType =
200
+ new String(iinfPayload, offset + 16, 4, StandardCharsets.ISO_8859_1);
201
+ if ("Exif".equals(itemType)) {
202
+ return id;
203
+ }
204
+ }
205
+ }
206
+ offset += size;
207
+ }
208
+ return -1;
209
+ }
210
+
211
+ private static int locateMdatPayloadStart(byte[] file) throws IOException {
212
+ int offset = 0;
213
+ for (BmffBox box : parseTopLevel(file)) {
214
+ if ("mdat".equals(box.type)) {
215
+ return offset + 8;
216
+ }
217
+ offset += 8 + box.payload.length;
218
+ }
219
+ throw new IOException("mdat missing");
220
+ }
221
+
222
+ private static int ilocItemOffset(byte[] ilocPayload, int itemId) throws IOException {
223
+ IlocLayout layout = parseIlocLayout(ilocPayload);
224
+ int itemCount = u16(ilocPayload, 6);
225
+ ByteBuffer buf = ByteBuffer.wrap(ilocPayload).order(ByteOrder.BIG_ENDIAN);
226
+ buf.position(8);
227
+ for (int i = 0; i < itemCount; i++) {
228
+ int id = buf.getShort() & 0xFFFF;
229
+ buf.getShort();
230
+ skipSized(buf, layout.baseOffsetSize);
231
+ int extentCount = buf.getShort() & 0xFFFF;
232
+ if (extentCount < 1) {
233
+ continue;
234
+ }
235
+ int offset = readSized(buf, layout.offsetSize);
236
+ skipSized(buf, layout.lengthSize);
237
+ for (int e = 1; e < extentCount; e++) {
238
+ skipSized(buf, layout.offsetSize);
239
+ skipSized(buf, layout.lengthSize);
240
+ }
241
+ if (id == itemId) {
242
+ return offset;
243
+ }
244
+ }
245
+ return -1;
246
+ }
247
+
248
+ private static byte[] shiftIlocExtentOffsets(byte[] ilocPayload, int delta) throws IOException {
249
+ if (delta == 0) {
250
+ return ilocPayload;
251
+ }
252
+ byte[] copy = Arrays.copyOf(ilocPayload, ilocPayload.length);
253
+ IlocLayout layout = parseIlocLayout(copy);
254
+ ByteBuffer buf = ByteBuffer.wrap(copy).order(ByteOrder.BIG_ENDIAN);
255
+ int itemCount = u16(copy, 6);
256
+ buf.position(8);
257
+ for (int i = 0; i < itemCount; i++) {
258
+ buf.getShort();
259
+ buf.getShort();
260
+ skipSized(buf, layout.baseOffsetSize);
261
+ int extentCount = buf.getShort() & 0xFFFF;
262
+ for (int e = 0; e < extentCount; e++) {
263
+ int offset = readSized(buf, layout.offsetSize);
264
+ int length = readSized(buf, layout.lengthSize);
265
+ buf.position(buf.position() - layout.offsetSize - layout.lengthSize);
266
+ writeSized(buf, offset + delta, layout.offsetSize);
267
+ writeSized(buf, length, layout.lengthSize);
268
+ }
269
+ }
270
+ return copy;
271
+ }
272
+
273
+ private static void writeSized(ByteBuffer buf, int value, int bytes) throws IOException {
274
+ switch (bytes) {
275
+ case 0:
276
+ break;
277
+ case 4:
278
+ buf.putInt(value);
279
+ break;
280
+ default:
281
+ throw new IOException("unsupported iloc field size " + bytes);
282
+ }
283
+ }
284
+
285
+ private static int ilocItemLength(byte[] ilocPayload, int itemId) throws IOException {
286
+ IlocLayout layout = parseIlocLayout(ilocPayload);
287
+ int itemCount = u16(ilocPayload, 6);
288
+ ByteBuffer buf = ByteBuffer.wrap(ilocPayload).order(ByteOrder.BIG_ENDIAN);
289
+ buf.position(8);
290
+ for (int i = 0; i < itemCount; i++) {
291
+ int id = buf.getShort() & 0xFFFF;
292
+ buf.getShort();
293
+ skipSized(buf, layout.baseOffsetSize);
294
+ int extentCount = buf.getShort() & 0xFFFF;
295
+ int totalLength = 0;
296
+ for (int e = 0; e < extentCount; e++) {
297
+ skipSized(buf, layout.offsetSize);
298
+ totalLength += readSized(buf, layout.lengthSize);
299
+ }
300
+ if (id == itemId) {
301
+ return totalLength;
302
+ }
303
+ }
304
+ return -1;
305
+ }
306
+
307
+ private static byte[] removeIlocEntry(byte[] ilocPayload, int removeId) throws IOException {
308
+ IlocLayout layout = parseIlocLayout(ilocPayload);
309
+ int itemCount = u16(ilocPayload, 6);
310
+ ByteBuffer buf = ByteBuffer.wrap(ilocPayload).order(ByteOrder.BIG_ENDIAN);
311
+ buf.position(8);
312
+ ByteArrayOutputStream kept = new ByteArrayOutputStream(ilocPayload.length);
313
+ int keptCount = 0;
314
+ for (int i = 0; i < itemCount; i++) {
315
+ int start = buf.position();
316
+ int id = buf.getShort() & 0xFFFF;
317
+ buf.getShort();
318
+ skipSized(buf, layout.baseOffsetSize);
319
+ int extentCount = buf.getShort() & 0xFFFF;
320
+ for (int e = 0; e < extentCount; e++) {
321
+ skipSized(buf, layout.offsetSize);
322
+ skipSized(buf, layout.lengthSize);
323
+ }
324
+ int end = buf.position();
325
+ if (id != removeId) {
326
+ kept.write(ilocPayload, start, end - start);
327
+ keptCount++;
328
+ }
329
+ }
330
+ ByteArrayOutputStream out = new ByteArrayOutputStream(ilocPayload.length);
331
+ out.write(ilocPayload, 0, 6);
332
+ writeU16(out, keptCount);
333
+ kept.writeTo(out);
334
+ return out.toByteArray();
335
+ }
336
+
337
+ private static byte[] removeIinfEntry(byte[] iinfPayload, int removeId) throws IOException {
338
+ ByteArrayOutputStream entries = new ByteArrayOutputStream(iinfPayload.length);
339
+ int kept = 0;
340
+ int offset = 6;
341
+ while (offset + 8 <= iinfPayload.length) {
342
+ int size = u32(iinfPayload, offset);
343
+ if (size < 8 || offset + size > iinfPayload.length) {
344
+ break;
345
+ }
346
+ boolean drop = false;
347
+ if ("infe".equals(new String(iinfPayload, offset + 4, 4, StandardCharsets.ISO_8859_1))
348
+ && size >= 20) {
349
+ int version = iinfPayload[offset + 8] & 0xFF;
350
+ int id =
351
+ version >= 3
352
+ ? ByteBuffer.wrap(iinfPayload, offset + 12, 4)
353
+ .order(ByteOrder.BIG_ENDIAN)
354
+ .getInt()
355
+ : u16(iinfPayload, offset + 12);
356
+ if (id == removeId) {
357
+ drop = true;
358
+ }
359
+ }
360
+ if (!drop) {
361
+ entries.write(iinfPayload, offset, size);
362
+ kept++;
363
+ }
364
+ offset += size;
365
+ }
366
+ ByteArrayOutputStream out = new ByteArrayOutputStream(iinfPayload.length);
367
+ out.write(iinfPayload, 0, 4);
368
+ writeU16(out, kept);
369
+ entries.writeTo(out);
370
+ return out.toByteArray();
371
+ }
372
+
373
+ private static byte[] removeCdscRefs(byte[] irefPayload, int exifItemId) throws IOException {
374
+ ByteArrayOutputStream body = new ByteArrayOutputStream(irefPayload.length);
375
+ body.write(irefPayload, 0, 4);
376
+ int offset = 4;
377
+ while (offset + 8 <= irefPayload.length) {
378
+ int size = u32(irefPayload, offset);
379
+ if (size < 8 || offset + size > irefPayload.length) {
380
+ break;
381
+ }
382
+ String type = new String(irefPayload, offset + 4, 4, StandardCharsets.ISO_8859_1);
383
+ if ("cdsc".equals(type) && size >= 14) {
384
+ int fromId = u16(irefPayload, offset + 8);
385
+ if (fromId != exifItemId) {
386
+ body.write(irefPayload, offset, size);
387
+ }
388
+ } else {
389
+ body.write(irefPayload, offset, size);
390
+ }
391
+ offset += size;
392
+ }
393
+ return body.toByteArray();
394
+ }
395
+
396
+ private static byte[] rebuildMeta(byte[] metaPayload, byte[] iloc, byte[] iinf, byte[] iref)
397
+ throws IOException {
398
+ ByteArrayOutputStream result = new ByteArrayOutputStream(metaPayload.length + 64);
399
+ result.write(metaPayload, 0, Math.min(4, metaPayload.length));
400
+ for (BmffBox child : parseMetaChildren(metaPayload)) {
401
+ byte[] payload;
402
+ switch (child.type) {
403
+ case "iloc":
404
+ payload = iloc;
405
+ break;
406
+ case "iinf":
407
+ payload = iinf;
408
+ break;
409
+ case "iref":
410
+ payload = iref;
411
+ break;
412
+ default:
413
+ payload = child.payload;
414
+ break;
415
+ }
416
+ new BmffBox(child.type, payload).writeTo(result);
417
+ }
418
+ return result.toByteArray();
419
+ }
420
+
421
+ private static MetaTables parseMetaTables(byte[] metaPayload) throws IOException {
422
+ MetaTables tables = new MetaTables();
423
+ for (BmffBox child : parseMetaChildren(metaPayload)) {
424
+ switch (child.type) {
425
+ case "iloc":
426
+ tables.ilocPayload = child.payload;
427
+ break;
428
+ case "iinf":
429
+ tables.iinfPayload = child.payload;
430
+ break;
431
+ case "iref":
432
+ tables.irefPayload = child.payload;
433
+ break;
434
+ default:
435
+ break;
436
+ }
437
+ }
438
+ if (tables.ilocPayload == null || tables.iinfPayload == null || tables.irefPayload == null) {
439
+ throw new IOException("meta missing iloc/iinf/iref");
440
+ }
441
+ return tables;
442
+ }
443
+
444
+ private static IlocLayout parseIlocLayout(byte[] ilocPayload) throws IOException {
445
+ if (ilocPayload.length < 10) {
446
+ throw new IOException("iloc too small");
447
+ }
448
+ IlocLayout layout = new IlocLayout();
449
+ layout.offsetSize = (ilocPayload[4] >> 4) & 0xF;
450
+ layout.lengthSize = ilocPayload[4] & 0xF;
451
+ layout.baseOffsetSize = (ilocPayload[5] >> 4) & 0xF;
452
+ if (layout.offsetSize != 4 || layout.lengthSize != 4) {
453
+ throw new IOException("Unexpected iloc field sizes");
454
+ }
455
+ return layout;
456
+ }
457
+
458
+ private static void skipSized(ByteBuffer buf, int bytes) {
459
+ buf.position(buf.position() + bytes);
460
+ }
461
+
462
+ private static int readSized(ByteBuffer buf, int bytes) throws IOException {
463
+ switch (bytes) {
464
+ case 0:
465
+ return 0;
466
+ case 4:
467
+ return buf.getInt();
468
+ default:
469
+ throw new IOException("unsupported iloc field size " + bytes);
470
+ }
471
+ }
472
+
473
+ private static List<BmffBox> parseMetaChildren(byte[] metaPayload) {
474
+ if (metaPayload.length < 4) {
475
+ return List.of();
476
+ }
477
+ try {
478
+ return parseChildBoxes(metaPayload, 4, metaPayload.length);
479
+ } catch (IOException e) {
480
+ return List.of();
481
+ }
482
+ }
483
+
484
+ private static List<BmffBox> parseTopLevel(byte[] data) throws IOException {
485
+ return parseChildBoxes(data, 0, data.length);
486
+ }
487
+
488
+ private static List<BmffBox> parseChildBoxes(byte[] data, int start, int end) throws IOException {
489
+ List<BmffBox> boxes = new ArrayList<>();
490
+ int offset = start;
491
+ while (offset + 8 <= end) {
492
+ int size = u32(data, offset);
493
+ String type = new String(data, offset + 4, 4, StandardCharsets.ISO_8859_1);
494
+ int header = 8;
495
+ if (size == 1) {
496
+ if (offset + 16 > end) {
497
+ break;
498
+ }
499
+ size = (int) ByteBuffer.wrap(data, offset + 8, 8).order(ByteOrder.BIG_ENDIAN).getLong();
500
+ header = 16;
501
+ } else if (size == 0) {
502
+ size = end - offset;
503
+ }
504
+ if (size < header || offset + size > end) {
505
+ throw new IOException("Invalid box " + type + " size=" + size + " @" + offset);
506
+ }
507
+ byte[] payload = Arrays.copyOfRange(data, offset + header, offset + size);
508
+ boxes.add(new BmffBox(type, payload));
509
+ offset += size;
510
+ }
511
+ return boxes;
512
+ }
513
+
514
+ private static BmffBox require(List<BmffBox> boxes, String type) throws IOException {
515
+ for (BmffBox box : boxes) {
516
+ if (type.equals(box.type)) {
517
+ return box;
518
+ }
519
+ }
520
+ throw new IOException("Missing box " + type);
521
+ }
522
+
523
+ private static int u16(byte[] data, int offset) {
524
+ return ((data[offset] & 0xFF) << 8) | (data[offset + 1] & 0xFF);
525
+ }
526
+
527
+ private static int u32(byte[] data, int offset) {
528
+ return ((data[offset] & 0xFF) << 24)
529
+ | ((data[offset + 1] & 0xFF) << 16)
530
+ | ((data[offset + 2] & 0xFF) << 8)
531
+ | (data[offset + 3] & 0xFF);
532
+ }
533
+
534
+ private static void writeU16(ByteArrayOutputStream out, int value) throws IOException {
535
+ out.write((value >> 8) & 0xFF);
536
+ out.write(value & 0xFF);
537
+ }
538
+
539
+ private static final class IlocLayout {
540
+ int offsetSize;
541
+ int lengthSize;
542
+ int baseOffsetSize;
543
+ }
544
+
545
+ private static final class MetaTables {
546
+ byte[] ilocPayload;
547
+ byte[] iinfPayload;
548
+ byte[] irefPayload;
549
+ }
550
+
551
+ private static final class BmffBox {
552
+ final String type;
553
+ final byte[] payload;
554
+
555
+ BmffBox(String type, byte[] payload) {
556
+ this.type = type;
557
+ this.payload = payload;
558
+ }
559
+
560
+ int encodedSize() {
561
+ return 8 + payload.length;
562
+ }
563
+
564
+ void writeTo(ByteArrayOutputStream out) throws IOException {
565
+ int total = 8 + payload.length;
566
+ ByteBuffer hdr = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN);
567
+ hdr.putInt(total);
568
+ hdr.put(type.getBytes(StandardCharsets.ISO_8859_1), 0, 4);
569
+ out.write(hdr.array());
570
+ out.write(payload);
571
+ }
572
+ }
573
+ }