diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index c2b0d85da9..f8b043adce 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -15,7 +15,6 @@ "bs58": "^5.0.0", "chrono-node": "^2.2.6", "debounce": "^2.0.0", - "exifr": "^7.1.3", "exifreader": "^4", "fast-srp-hap": "^2.0.4", "ffmpeg-wasm": "file:./thirdparty/ffmpeg-wasm", diff --git a/web/apps/photos/src/services/locationSearchService.ts b/web/apps/photos/src/services/locationSearchService.ts index d28e8190da..30a36e5e04 100644 --- a/web/apps/photos/src/services/locationSearchService.ts +++ b/web/apps/photos/src/services/locationSearchService.ts @@ -1,6 +1,5 @@ import log from "@/base/log"; -import type { Location } from "@/new/photos/types/metadata"; -import type { LocationTagData } from "types/entity"; +import type { Location, LocationTagData } from "types/entity"; export interface City { city: string; diff --git a/web/apps/photos/src/services/upload/date.ts b/web/apps/photos/src/services/upload/date.ts index d70e00b5ef..d18aeeb5eb 100644 --- a/web/apps/photos/src/services/upload/date.ts +++ b/web/apps/photos/src/services/upload/date.ts @@ -1,5 +1,4 @@ import log from "@/base/log"; -import { validateAndGetCreationUnixTimeInMicroSeconds } from "@ente/shared/time"; /** * Try to extract a date (as epoch microseconds) from a file name by matching it @@ -41,6 +40,21 @@ export const tryParseEpochMicrosecondsFromFileName = ( } }; +export function validateAndGetCreationUnixTimeInMicroSeconds(dateTime: Date) { + if (!dateTime || isNaN(dateTime.getTime())) { + return null; + } + const unixTime = dateTime.getTime() * 1000; + //ignoring dateTimeString = "0000:00:00 00:00:00" + if (unixTime === Date.UTC(0, 0, 0, 0, 0, 0, 0) || unixTime === 0) { + return null; + } else if (unixTime > Date.now() * 1000) { + return null; + } else { + return unixTime; + } +} + interface DateComponent { year: T; month: T; diff --git a/web/apps/photos/src/types/entity.ts b/web/apps/photos/src/types/entity.ts index 0f22973d21..4371562cc8 100644 --- a/web/apps/photos/src/types/entity.ts +++ b/web/apps/photos/src/types/entity.ts @@ -1,5 +1,3 @@ -import { Location } from "@/new/photos/types/metadata"; - export enum EntityType { LOCATION_TAG = "location", } @@ -27,6 +25,11 @@ export interface EncryptedEntity { userID: number; } +export interface Location { + latitude: number | null; + longitude: number | null; +} + export interface LocationTagData { name: string; radius: number; diff --git a/web/packages/new/photos/services/exif.ts b/web/packages/new/photos/services/exif.ts index d559b9750a..5e7a7ea8ec 100644 --- a/web/packages/new/photos/services/exif.ts +++ b/web/packages/new/photos/services/exif.ts @@ -1,66 +1,10 @@ -import { nameAndExtension } from "@/base/file"; import log from "@/base/log"; import { parseMetadataDate, type ParsedMetadata, type ParsedMetadataDate, } from "@/media/file-metadata"; -import { FileType } from "@/media/file-type"; -import { parseImageMetadata } from "@ente/shared/utils/exif-old"; import ExifReader from "exifreader"; -import type { EnteFile } from "../types/file"; -import type { ParsedExtractedMetadata } from "../types/metadata"; - -const cmpTsEq = (a: number | undefined | null, b: number | undefined) => { - if (!a && !b) return true; - if (!a || !b) return false; - if (a == b) return true; - if (Math.floor(a / 1e6) == Math.floor(b / 1e6)) return true; - return false; -}; - -export const cmpNewLib = ( - oldLib: ParsedExtractedMetadata, - newLib: ParsedMetadata, -) => { - const logM = (r: string) => - log.info("[exif]", r, JSON.stringify({ old: oldLib, new: newLib })); - if ( - cmpTsEq(oldLib.creationTime, newLib.creationDate?.timestamp) && - oldLib.location.latitude == newLib.location?.latitude && - oldLib.location.longitude == newLib.location?.longitude - ) { - if ( - oldLib.width == newLib.width && - oldLib.height == newLib.height && - oldLib.creationTime == newLib.creationDate?.timestamp - ) - logM("exact match"); - else logM("enhanced match"); - log.debug(() => ["exif/cmp", { oldLib, newLib }]); - } else { - logM("potential mismatch ❗️🚩"); - } -}; - -export const cmpNewLib2 = async ( - enteFile: EnteFile, - blob: Blob, - _exif: unknown, -) => { - const [, ext] = nameAndExtension(enteFile.metadata.title); - const oldLib = await parseImageMetadata( - new File([blob], enteFile.metadata.title), - { - fileType: FileType.image, - extension: ext ?? "", - }, - ); - // cast is fine here, this is just temporary debugging code. - const rawExif = _exif as RawExifTags; - const newLib = parseExif(rawExif); - cmpNewLib(oldLib, newLib); -}; /** * Extract Exif and other metadata from the given file. diff --git a/web/packages/new/photos/services/ml/worker.ts b/web/packages/new/photos/services/ml/worker.ts index e012ebab16..2fe408f9bb 100644 --- a/web/packages/new/photos/services/ml/worker.ts +++ b/web/packages/new/photos/services/ml/worker.ts @@ -11,7 +11,7 @@ import { wait } from "@/utils/promise"; import { DOMParser } from "@xmldom/xmldom"; import { expose, wrap } from "comlink"; import downloadManager from "../download"; -import { cmpNewLib2, extractRawExif, type RawExifTags } from "../exif"; +import { extractRawExif, type RawExifTags } from "../exif"; import { getAllLocalFiles, getLocalTrashedFiles } from "../files"; import type { UploadItem } from "../upload/types"; import { @@ -518,13 +518,6 @@ const index = async ( throw e; } - try { - if (originalImageBlob && exif) - await cmpNewLib2(enteFile, originalImageBlob, exif); - } catch (e) { - log.warn(`Skipping exif cmp for ${f}`, e); - } - log.debug(() => { const ms = Date.now() - startTime; const msg = []; diff --git a/web/packages/new/photos/services/upload/types.ts b/web/packages/new/photos/services/upload/types.ts index d8d8517803..094e6704eb 100644 --- a/web/packages/new/photos/services/upload/types.ts +++ b/web/packages/new/photos/services/upload/types.ts @@ -1,5 +1,4 @@ import type { ZipItem } from "@/base/types/ipc"; -import type { Location } from "../../types/metadata"; /** * An item to upload is one of the following: @@ -59,8 +58,6 @@ export const toDataOrPathOrZipEntry = (desktopUploadItem: DesktopUploadItem) => export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random(); -export const NULL_LOCATION: Location = { latitude: null, longitude: null }; - export enum UPLOAD_STAGES { START, READING_GOOGLE_METADATA_FILES, diff --git a/web/packages/new/photos/types/metadata.ts b/web/packages/new/photos/types/metadata.ts deleted file mode 100644 index 8c7ee8088e..0000000000 --- a/web/packages/new/photos/types/metadata.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface Location { - latitude: number | null; - longitude: number | null; -} - -export interface ParsedExtractedMetadata { - location: Location; - creationTime: number | null; - width: number | null; - height: number | null; -} diff --git a/web/packages/shared/time/index.ts b/web/packages/shared/time/index.ts index 87e1d9648b..52c9c499bd 100644 --- a/web/packages/shared/time/index.ts +++ b/web/packages/shared/time/index.ts @@ -22,21 +22,6 @@ export function getUnixTimeInMicroSecondsWithDelta(delta: TimeDelta): number { return currentDate.getTime() * 1000; } -export function validateAndGetCreationUnixTimeInMicroSeconds(dateTime: Date) { - if (!dateTime || isNaN(dateTime.getTime())) { - return null; - } - const unixTime = dateTime.getTime() * 1000; - //ignoring dateTimeString = "0000:00:00 00:00:00" - if (unixTime === Date.UTC(0, 0, 0, 0, 0, 0, 0) || unixTime === 0) { - return null; - } else if (unixTime > Date.now() * 1000) { - return null; - } else { - return unixTime; - } -} - function _addDays(date: Date, days: number): Date { const result = new Date(date); result.setDate(date.getDate() + days); diff --git a/web/packages/shared/utils/exif-old.ts b/web/packages/shared/utils/exif-old.ts deleted file mode 100644 index b0041edc43..0000000000 --- a/web/packages/shared/utils/exif-old.ts +++ /dev/null @@ -1,340 +0,0 @@ -// The code in this file is deprecated and meant to be deleted. -// -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-nocheck - -import log from "@/base/log"; -import { type FileTypeInfo } from "@/media/file-type"; -import { NULL_LOCATION } from "@/new/photos/services/upload/types"; -import type { - Location, - ParsedExtractedMetadata, -} from "@/new/photos/types/metadata"; -import { validateAndGetCreationUnixTimeInMicroSeconds } from "@ente/shared/time"; -import exifr from "exifr"; - -type ParsedEXIFData = Record & - Partial<{ - DateTimeOriginal: Date; - CreateDate: Date; - ModifyDate: Date; - DateCreated: Date; - MetadataDate: Date; - latitude: number; - longitude: number; - imageWidth: number; - imageHeight: number; - }>; - -type RawEXIFData = Record & - Partial<{ - DateTimeOriginal: string; - CreateDate: string; - ModifyDate: string; - DateCreated: string; - MetadataDate: string; - GPSLatitude: number[]; - GPSLongitude: number[]; - GPSLatitudeRef: string; - GPSLongitudeRef: string; - ImageWidth: number; - ImageHeight: number; - }>; - -const exifTagsNeededForParsingImageMetadata = [ - "DateTimeOriginal", - "CreateDate", - "ModifyDate", - "GPSLatitude", - "GPSLongitude", - "GPSLatitudeRef", - "GPSLongitudeRef", - "DateCreated", - "ExifImageWidth", - "ExifImageHeight", - "ImageWidth", - "ImageHeight", - "PixelXDimension", - "PixelYDimension", - "MetadataDate", -]; - -/** - * Read Exif data from an image {@link file} and use that to construct and - * return an {@link ParsedExtractedMetadata}. - * - * This function is tailored for use when we upload files. - */ -export const parseImageMetadata = async ( - file: File, - fileTypeInfo: FileTypeInfo, -): Promise => { - const exifData = await getParsedExifData( - file, - fileTypeInfo, - exifTagsNeededForParsingImageMetadata, - ); - - // TODO: Exif- remove me. - log.debug(() => ["exif/old", exifData]); - return { - location: getEXIFLocation(exifData), - creationTime: getEXIFTime(exifData), - width: exifData?.imageWidth ?? null, - height: exifData?.imageHeight ?? null, - }; -}; - -export async function getParsedExifData( - receivedFile: File, - { extension }: FileTypeInfo, - tags?: string[], -): Promise { - const exifLessFormats = ["gif", "bmp"]; - const exifrUnsupportedFileFormatMessage = "Unknown file format"; - - try { - if (exifLessFormats.includes(extension)) return null; - - const exifData: RawEXIFData = await exifr.parse(receivedFile, { - reviveValues: false, - tiff: true, - xmp: true, - icc: true, - iptc: true, - jfif: true, - ihdr: true, - }); - if (!exifData) { - return null; - } - const filteredExifData = tags - ? Object.fromEntries( - Object.entries(exifData).filter(([key]) => - tags.includes(key), - ), - ) - : exifData; - return parseExifData(filteredExifData); - } catch (e) { - if (e.message == exifrUnsupportedFileFormatMessage) { - log.error(`EXIFR does not support ${extension} files`, e); - return undefined; - } else { - log.error(`Failed to parse Exif data for a ${extension} file`, e); - throw e; - } - } -} - -function parseExifData(exifData: RawEXIFData): ParsedEXIFData { - if (!exifData) { - return null; - } - const { - DateTimeOriginal, - CreateDate, - ModifyDate, - DateCreated, - ImageHeight, - ImageWidth, - ExifImageHeight, - ExifImageWidth, - PixelXDimension, - PixelYDimension, - MetadataDate, - ...rest - } = exifData; - const parsedExif: ParsedEXIFData = { ...rest }; - if (DateTimeOriginal) { - parsedExif.DateTimeOriginal = parseEXIFDate(exifData.DateTimeOriginal); - } - if (CreateDate) { - parsedExif.CreateDate = parseEXIFDate(exifData.CreateDate); - } - if (ModifyDate) { - parsedExif.ModifyDate = parseEXIFDate(exifData.ModifyDate); - } - if (DateCreated) { - parsedExif.DateCreated = parseEXIFDate(exifData.DateCreated); - } - if (MetadataDate) { - parsedExif.MetadataDate = parseEXIFDate(exifData.MetadataDate); - } - if (exifData.GPSLatitude && exifData.GPSLongitude) { - const parsedLocation = parseEXIFLocation( - exifData.GPSLatitude, - exifData.GPSLatitudeRef, - exifData.GPSLongitude, - exifData.GPSLongitudeRef, - ); - parsedExif.latitude = parsedLocation.latitude; - parsedExif.longitude = parsedLocation.longitude; - } - if (ImageWidth && ImageHeight) { - if (typeof ImageWidth === "number" && typeof ImageHeight === "number") { - parsedExif.imageWidth = ImageWidth; - parsedExif.imageHeight = ImageHeight; - } else { - log.warn("Exif: Ignoring non-numeric ImageWidth or ImageHeight"); - } - } else if (ExifImageWidth && ExifImageHeight) { - if ( - typeof ExifImageWidth === "number" && - typeof ExifImageHeight === "number" - ) { - parsedExif.imageWidth = ExifImageWidth; - parsedExif.imageHeight = ExifImageHeight; - } else { - log.warn( - "Exif: Ignoring non-numeric ExifImageWidth or ExifImageHeight", - ); - } - } else if (PixelXDimension && PixelYDimension) { - if ( - typeof PixelXDimension === "number" && - typeof PixelYDimension === "number" - ) { - parsedExif.imageWidth = PixelXDimension; - parsedExif.imageHeight = PixelYDimension; - } else { - log.warn( - "Exif: Ignoring non-numeric PixelXDimension or PixelYDimension", - ); - } - } - return parsedExif; -} - -function parseEXIFDate(dateTimeString: string) { - try { - if (typeof dateTimeString !== "string" || dateTimeString === "") { - throw new Error("Invalid date string"); - } - - // Check and parse date in the format YYYYMMDD - if (dateTimeString.length === 8) { - const year = Number(dateTimeString.slice(0, 4)); - const month = Number(dateTimeString.slice(4, 6)); - const day = Number(dateTimeString.slice(6, 8)); - if ( - !Number.isNaN(year) && - !Number.isNaN(month) && - !Number.isNaN(day) - ) { - const date = new Date(year, month - 1, day); - if (!Number.isNaN(+date)) { - return date; - } - } - } - const [year, month, day, hour, minute, second] = dateTimeString - .match(/\d+/g) - .map(Number); - - if ( - typeof year === "undefined" || - Number.isNaN(year) || - typeof month === "undefined" || - Number.isNaN(month) || - typeof day === "undefined" || - Number.isNaN(day) - ) { - throw new Error("Invalid date"); - } - let date: Date; - if ( - typeof hour === "undefined" || - Number.isNaN(hour) || - typeof minute === "undefined" || - Number.isNaN(minute) || - typeof second === "undefined" || - Number.isNaN(second) - ) { - date = new Date(year, month - 1, day); - } else { - date = new Date(year, month - 1, day, hour, minute, second); - } - if (Number.isNaN(+date)) { - throw new Error("Invalid date"); - } - return date; - } catch (e) { - log.error(`Failed to parseEXIFDate ${dateTimeString}`, e); - return null; - } -} - -export function parseEXIFLocation( - gpsLatitude: number[], - gpsLatitudeRef: string, - gpsLongitude: number[], - gpsLongitudeRef: string, -) { - try { - if ( - !Array.isArray(gpsLatitude) || - !Array.isArray(gpsLongitude) || - gpsLatitude.length !== 3 || - gpsLongitude.length !== 3 - ) { - throw new Error("Invalid Exif location"); - } - const latitude = convertDMSToDD( - gpsLatitude[0], - gpsLatitude[1], - gpsLatitude[2], - gpsLatitudeRef, - ); - const longitude = convertDMSToDD( - gpsLongitude[0], - gpsLongitude[1], - gpsLongitude[2], - gpsLongitudeRef, - ); - return { latitude, longitude }; - } catch (e) { - const p = { - gpsLatitude, - gpsLatitudeRef, - gpsLongitude, - gpsLongitudeRef, - }; - log.error(`Failed to parse Exif location ${JSON.stringify(p)}`, e); - return { ...NULL_LOCATION }; - } -} - -function convertDMSToDD( - degrees: number, - minutes: number, - seconds: number, - direction: string, -) { - let dd = degrees + minutes / 60 + seconds / (60 * 60); - if (direction === "S" || direction === "W") dd *= -1; - return dd; -} - -export function getEXIFLocation(exifData: ParsedEXIFData): Location { - if (!exifData || (!exifData.latitude && exifData.latitude !== 0)) { - return { ...NULL_LOCATION }; - } - return { latitude: exifData.latitude, longitude: exifData.longitude }; -} - -export function getEXIFTime(exifData: ParsedEXIFData): number { - if (!exifData) { - return null; - } - const dateTime = - exifData.DateTimeOriginal ?? - exifData.DateCreated ?? - exifData.CreateDate ?? - exifData.MetadataDate ?? - exifData.ModifyDate; - if (!dateTime) { - return null; - } - return validateAndGetCreationUnixTimeInMicroSeconds(dateTime); -} diff --git a/web/yarn.lock b/web/yarn.lock index cb006b8edd..caf41595f2 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2486,11 +2486,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -exifr@^7.1.3: - version "7.1.3" - resolved "https://registry.yarnpkg.com/exifr/-/exifr-7.1.3.tgz#f6218012c36dbb7d843222011b27f065fddbab6f" - integrity sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw== - exifreader@^4: version "4.23.3" resolved "https://registry.yarnpkg.com/exifreader/-/exifreader-4.23.3.tgz#3389c2dab3ab2501562ebdef4115ea34ab9d9aa4"