Skip to content

Commit e505eac

Browse files
authored
feat: expose more photo metadata (#1203)
* improve SavedPhoto type * WIP * wip: accounting for potentially updated schema * cleanup * remove unused exif helper function * update displayed storage size * update expo-image dep declaration * changes based on feedback * fix type errors * remove todo comment
1 parent d64b946 commit e505eac

15 files changed

Lines changed: 989 additions & 285 deletions

File tree

messages/en.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,13 +1278,25 @@
12781278
"screens.OnboardingPrivacyPolicy.shareDiagnostics": {
12791279
"message": "Share Diagnostic Information"
12801280
},
1281-
"screens.PhotoPreviewModal.DeletePhoto.headerButtonText": {
1281+
"screens.PhotoPreviewModal.headerButtonText": {
12821282
"message": "Delete Photo"
12831283
},
1284-
"screens.PhotoPreviewModal.DeletePhoto.navTitle": {
1284+
"screens.PhotoPreviewModal.imageStorageSize": {
1285+
"description": "Image size in megabytes",
1286+
"message": "{value} MB"
1287+
},
1288+
"screens.PhotoPreviewModal.landscape": {
1289+
"description": "Describes image layout when taking photo horizontally",
1290+
"message": "Landscape"
1291+
},
1292+
"screens.PhotoPreviewModal.navTitle": {
12851293
"message": "Photo Info"
12861294
},
1287-
"screens.PhotoPreviewModal.DeletePhoto.validatedByCoMapeo": {
1295+
"screens.PhotoPreviewModal.portrait": {
1296+
"description": "Describes image layout when taking photo vertically",
1297+
"message": "Portrait"
1298+
},
1299+
"screens.PhotoPreviewModal.validatedByCoMapeo": {
12881300
"message": "Validated by CoMapeo"
12891301
},
12901302
"screens.PrivacyPolicy.aboutAwana": {

package-lock.json

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"expo-file-system": "18.0.12",
6767
"expo-font": "13.0.4",
6868
"expo-image-manipulator": "13.0.6",
69+
"expo-image": "2.0.7",
6970
"expo-localization": "16.0.1",
7071
"expo-location": "18.0.10",
7172
"expo-secure-store": "14.0.1",

src/frontend/contexts/PhotoPromiseContext/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ export const THUMBNAIL_QUALITY = 30;
66
export const PREVIEW_SIZE = 1200;
77
export const PREVIEW_QUALITY = 30;
88

9-
export type SavedPhoto = Omit<Attachment, 'type'> & {
10-
type: 'photo';
9+
export type SavedPhoto = Extract<Attachment, {type: 'photo'}> & {
1110
deleted?: boolean;
1211
};
1312

src/frontend/lib/exif.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import * as v from 'valibot';
22

33
import type {PhotoEXIF} from '../sharedTypes';
44

5+
export type PhotoLayout = 'horizontal' | 'vertical';
6+
57
export const PhotoEXIFSchema = v.object({
68
ApertureValue: v.optional(v.number()),
79
ExposureTime: v.optional(v.number()),
810
Flash: v.optional(v.number()),
9-
FocalLength: v.optional(v.number()),
1011
FNumber: v.optional(v.number()),
12+
FocalLength: v.optional(v.number()),
1113
ISOSpeedRatings: v.optional(v.number()),
1214
ImageLength: v.optional(v.number()),
1315
ImageWidth: v.optional(v.number()),
@@ -18,3 +20,32 @@ export const PhotoEXIFSchema = v.object({
1820
} satisfies Required<{
1921
[field in keyof PhotoEXIF]: v.GenericSchema<PhotoEXIF[field]>;
2022
}>) satisfies v.GenericSchema<PhotoEXIF>;
23+
24+
/**
25+
* Describes the layout of the photo based on its EXIF orientation tag value.
26+
*
27+
* @param exifOrientation Value of EXIF orientation tag
28+
* @returns The layout
29+
*/
30+
export function getPhotoLayout(exifOrientation: number): PhotoLayout {
31+
// See "Orientation" tag in https://exiftool.org/TagNames/EXIF.html
32+
// Helpful explainer: https://www.ameto.de/blog/exif-orientation-primer/
33+
34+
switch (exifOrientation) {
35+
case 1:
36+
case 2:
37+
case 3:
38+
case 4: {
39+
return 'horizontal';
40+
}
41+
case 5:
42+
case 6:
43+
case 7:
44+
case 8: {
45+
return 'vertical';
46+
}
47+
default: {
48+
throw new Error(`Invalid orientation value: ${exifOrientation}`);
49+
}
50+
}
51+
}

src/frontend/lib/file-system.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as FileSystem from 'expo-file-system';
22
import * as DocumentPicker from 'expo-document-picker';
3+
import {Image as ExpoImage} from 'expo-image';
34

45
// @ts-expect-error Only null when on web https://github.com/expo/expo/issues/5558
56
export const DOCUMENT_DIRECTORY: string = FileSystem.documentDirectory;
@@ -53,3 +54,37 @@ export async function selectFile({
5354

5455
return asset;
5556
}
57+
58+
/**
59+
* Returns the storage size of an image rendered using `expo-image`.
60+
* This is specific to Expo because their `Image` component uses the Expo file system abstraction for caching.
61+
*
62+
* @param imageURL The URL of the image.
63+
*
64+
* @returns The storage size of the image, in bytes.
65+
*/
66+
export async function getExpoImageStorageSize(
67+
imageURL: string,
68+
): Promise<number> {
69+
let fileInfo: FileSystem.FileInfo;
70+
71+
if (imageURL.startsWith('file://')) {
72+
fileInfo = await FileSystem.getInfoAsync(imageURL, {size: true});
73+
} else {
74+
const cachePath = await ExpoImage.getCachePathAsync(imageURL);
75+
76+
if (!cachePath) {
77+
throw new Error(`Could not get size for image at ${imageURL}`);
78+
}
79+
80+
fileInfo = await FileSystem.getInfoAsync(`file://${cachePath}`, {
81+
size: true,
82+
});
83+
}
84+
85+
if (!fileInfo.exists) {
86+
throw new Error(`Could not get size for image at ${imageURL}`);
87+
}
88+
89+
return fileInfo.size;
90+
}

0 commit comments

Comments
 (0)