88import type {
99 CameraDevice ,
1010 CameraDeviceFactory ,
11+ CameraOrientation ,
1112 FlashMode ,
1213 MirrorMode ,
1314 QualityPrioritization ,
@@ -18,6 +19,96 @@ import { CommonResolutions, VisionCamera } from 'react-native-vision-camera'
1819const sleep = ( ms : number ) =>
1920 new Promise < void > ( ( resolve ) => setTimeout ( resolve , ms ) )
2021
22+ function readJpegExifOrientation ( buffer : ArrayBuffer ) : number | undefined {
23+ const view = new DataView ( buffer )
24+ if ( view . byteLength < 4 || view . getUint16 ( 0 , false ) !== 0xffd8 ) {
25+ return undefined
26+ }
27+
28+ let offset = 2
29+ while ( offset + 4 <= view . byteLength ) {
30+ if ( view . getUint8 ( offset ) !== 0xff ) {
31+ return undefined
32+ }
33+ while ( offset < view . byteLength && view . getUint8 ( offset ) === 0xff ) {
34+ offset ++
35+ }
36+
37+ const marker = view . getUint8 ( offset )
38+ offset ++
39+ if ( marker === 0xda || marker === 0xd9 ) {
40+ return undefined
41+ }
42+ if ( offset + 2 > view . byteLength ) {
43+ return undefined
44+ }
45+
46+ const segmentLength = view . getUint16 ( offset , false )
47+ const segmentStart = offset + 2
48+ const segmentEnd = offset + segmentLength
49+ if ( segmentEnd > view . byteLength || segmentLength < 2 ) {
50+ return undefined
51+ }
52+
53+ const isExifSegment =
54+ marker === 0xe1 &&
55+ segmentStart + 6 <= segmentEnd &&
56+ view . getUint8 ( segmentStart ) === 0x45 &&
57+ view . getUint8 ( segmentStart + 1 ) === 0x78 &&
58+ view . getUint8 ( segmentStart + 2 ) === 0x69 &&
59+ view . getUint8 ( segmentStart + 3 ) === 0x66 &&
60+ view . getUint8 ( segmentStart + 4 ) === 0x00 &&
61+ view . getUint8 ( segmentStart + 5 ) === 0x00
62+ if ( isExifSegment ) {
63+ return readTiffOrientation ( view , segmentStart + 6 , segmentEnd )
64+ }
65+
66+ offset = segmentEnd
67+ }
68+
69+ return undefined
70+ }
71+
72+ function readTiffOrientation (
73+ view : DataView ,
74+ tiffStart : number ,
75+ tiffEnd : number ,
76+ ) : number | undefined {
77+ if ( tiffStart + 8 > tiffEnd ) {
78+ return undefined
79+ }
80+
81+ const byteOrder = view . getUint16 ( tiffStart , false )
82+ const littleEndian = byteOrder === 0x4949
83+ if ( ! littleEndian && byteOrder !== 0x4d4d ) {
84+ return undefined
85+ }
86+ if ( view . getUint16 ( tiffStart + 2 , littleEndian ) !== 42 ) {
87+ return undefined
88+ }
89+
90+ const firstIfdOffset = view . getUint32 ( tiffStart + 4 , littleEndian )
91+ const ifdStart = tiffStart + firstIfdOffset
92+ if ( ifdStart + 2 > tiffEnd ) {
93+ return undefined
94+ }
95+
96+ const entryCount = view . getUint16 ( ifdStart , littleEndian )
97+ for ( let index = 0 ; index < entryCount ; index ++ ) {
98+ const entryStart = ifdStart + 2 + index * 12
99+ if ( entryStart + 12 > tiffEnd ) {
100+ return undefined
101+ }
102+
103+ const tag = view . getUint16 ( entryStart , littleEndian )
104+ if ( tag === 0x0112 ) {
105+ return view . getUint16 ( entryStart + 8 , littleEndian )
106+ }
107+ }
108+
109+ return undefined
110+ }
111+
21112describe ( 'VisionCamera - Photo' , ( ) => {
22113 let factory : CameraDeviceFactory
23114 let backDevice : CameraDevice
@@ -154,6 +245,59 @@ describe('VisionCamera - Photo', () => {
154245 await session . stop ( )
155246 } )
156247
248+ it ( 'preserves JPEG EXIF orientation when saving an in-memory Photo to a file' , async ( ) => {
249+ const session = await VisionCamera . createCameraSession ( false )
250+ const photoOutput = VisionCamera . createPhotoOutput ( {
251+ targetResolution : CommonResolutions . HD_4_3 ,
252+ containerFormat : 'jpeg' ,
253+ quality : 0.9 ,
254+ qualityPrioritization : 'balanced' ,
255+ } )
256+ await session . configure ( [
257+ {
258+ input : backDevice ,
259+ outputs : [ { output : photoOutput , mirrorMode : 'off' } ] ,
260+ constraints : [ ] ,
261+ } ,
262+ ] )
263+ await session . start ( )
264+
265+ try {
266+ const outputOrientations : CameraOrientation [ ] = [
267+ 'up' ,
268+ 'right' ,
269+ 'down' ,
270+ 'left' ,
271+ ]
272+ for ( const outputOrientation of outputOrientations ) {
273+ photoOutput . outputOrientation = outputOrientation
274+ const photo = await photoOutput . capturePhoto (
275+ { flashMode : 'off' , enableShutterSound : false } ,
276+ { } ,
277+ )
278+ try {
279+ const inMemoryData = await photo . getFileDataAsync ( )
280+ const inMemoryOrientation = readJpegExifOrientation ( inMemoryData )
281+ expect ( inMemoryOrientation ) . toBeDefined ( )
282+
283+ const path = await photo . saveToTemporaryFileAsync ( )
284+ const response = await fetch ( `file://${ path } ` )
285+ const savedData = await response . arrayBuffer ( )
286+ const savedOrientation = readJpegExifOrientation ( savedData )
287+
288+ console . log (
289+ `outputOrientation=${ outputOrientation } photo.orientation=${ photo . orientation } in-memory EXIF=${ inMemoryOrientation } saved EXIF=${ savedOrientation } ` ,
290+ )
291+ expect ( savedOrientation ) . toBe ( inMemoryOrientation )
292+ } finally {
293+ photo . dispose ( )
294+ }
295+ }
296+ } finally {
297+ await session . stop ( )
298+ }
299+ } )
300+
157301 // TODO: Re-enable once VisionCamera exposes a way to query supported photo
158302 // container formats upfront (see the TODO in CameraPhotoOutput.nitro.ts
159303 // near `TargetPhotoContainerFormat`). Without that API there is no
0 commit comments