Skip to content

Commit 628f111

Browse files
authored
fix: Fix ByteBuffer not being rewinded for ExifInterface save (#3846)
* fix: Fix `ByteBuffer` not being rewinded for `ExifInterface` save * Add test for photo temp file saving
1 parent 3beb275 commit 628f111

2 files changed

Lines changed: 49 additions & 1 deletion

File tree

apps/simple-camera/__tests__/visioncamera.photo.harness.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,49 @@ describe('VisionCamera - Photo', () => {
6363
await session.stop()
6464
})
6565

66+
it('saves a JPEG Photo to a temporary file after converting it to an Image', async () => {
67+
// Regression: on Android, `toImageAsync()` goes through CameraX's
68+
// `jpegImageToJpegByteArray`, which advances the JPEG plane's `ByteBuffer`
69+
// position to `capacity`. The plane buffer is shared across reads (Android's
70+
// `ImageReader` caches the same `ByteBuffer` instance), so a subsequent
71+
// `saveToTemporaryFileAsync()` that reads `buffer.remaining()` would write a
72+
// 0-byte file, and `ExifInterface.saveAttributes()` would then throw
73+
// "ExifInterface only supports saving attributes for JPEG, PNG, and WebP
74+
// formats" because it cannot sniff the MIME type of an empty file.
75+
// See `HybridPhoto.kt#saveToFile`.
76+
const session = await VisionCamera.createCameraSession(false)
77+
const photoOutput = VisionCamera.createPhotoOutput({
78+
targetResolution: CommonResolutions.HD_4_3,
79+
containerFormat: 'jpeg',
80+
quality: 0.9,
81+
qualityPrioritization: 'balanced',
82+
})
83+
await session.configure([
84+
{
85+
input: backDevice,
86+
outputs: [{ output: photoOutput, mirrorMode: 'auto' }],
87+
constraints: [],
88+
},
89+
])
90+
await session.start()
91+
92+
const photo = await photoOutput.capturePhoto(
93+
{ flashMode: 'off', enableShutterSound: false },
94+
{},
95+
)
96+
97+
const image = await photo.toImageAsync()
98+
expect(image.width).toBeGreaterThan(0)
99+
expect(image.height).toBeGreaterThan(0)
100+
image.dispose()
101+
102+
const path = await photo.saveToTemporaryFileAsync()
103+
expect(path.length).toBeGreaterThan(0)
104+
photo.dispose()
105+
106+
await session.stop()
107+
})
108+
66109
// TODO: Re-enable once VisionCamera exposes a way to query supported photo
67110
// container formats upfront (see the TODO in CameraPhotoOutput.nitro.ts
68111
// near `TargetPhotoContainerFormat`). Without that API there is no

packages/react-native-vision-camera/android/src/main/java/com/margelo/nitro/camera/hybrids/instances/HybridPhoto.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,12 @@ class HybridPhoto(
9191
// JPEG Images have a single plane of image data.
9292
val plane = image.planes.single()
9393
val buffer = plane.buffer
94-
val bytes = ByteArray(buffer.remaining()).also { bytes -> buffer.get(bytes) }
94+
// The ByteBuffer is shared and cached by Android's ImageReader, so other readers
95+
// (e.g. `image.toBitmap()` via `toImage()`) may have advanced its position to the end.
96+
// Rewind so we read all bytes from the start instead of writing an empty file.
97+
buffer.rewind()
98+
val bytes = ByteArray(buffer.remaining())
99+
buffer.get(bytes)
95100
FileOutputStream(file).use { stream ->
96101
stream.write(bytes)
97102
}

0 commit comments

Comments
 (0)