diff --git a/web/apps/cast/src/services/detect-type.ts b/web/apps/cast/src/services/detect-type.ts index 4ff8801a4..187e19df8 100644 --- a/web/apps/cast/src/services/detect-type.ts +++ b/web/apps/cast/src/services/detect-type.ts @@ -24,5 +24,5 @@ export const detectMediaMIMEType = async (file: File): Promise => { const ext = lowercaseExtension(file.name); if (!ext) return undefined; - return KnownFileTypeInfos.find((f) => f.exactType == ext)?.mimeType; + return KnownFileTypeInfos.find((f) => f.extension == ext)?.mimeType; }; diff --git a/web/apps/cast/src/types/upload.ts b/web/apps/cast/src/types/upload.ts index b74621fad..27a47aa3b 100644 --- a/web/apps/cast/src/types/upload.ts +++ b/web/apps/cast/src/types/upload.ts @@ -15,11 +15,3 @@ export interface Metadata { version?: number; deviceFolder?: string; } - -export interface FileTypeInfo { - fileType: FILE_TYPE; - exactType: string; - mimeType?: string; - imageType?: string; - videoType?: string; -} diff --git a/web/apps/photos/src/services/exif.ts b/web/apps/photos/src/services/exif.ts index aea44dedc..af3552ab8 100644 --- a/web/apps/photos/src/services/exif.ts +++ b/web/apps/photos/src/services/exif.ts @@ -36,16 +36,15 @@ type RawEXIFData = Record & export async function getParsedExifData( receivedFile: File, - { exactType }: FileTypeInfo, + { extension }: FileTypeInfo, tags?: string[], ): Promise { const exifLessFormats = ["gif", "bmp"]; const exifrUnsupportedFileFormatMessage = "Unknown file format"; try { - if (exifLessFormats.includes(exactType)) { - return null; - } + if (exifLessFormats.includes(extension)) return null; + const exifData: RawEXIFData = await exifr.parse(receivedFile, { reviveValues: false, tiff: true, @@ -68,10 +67,10 @@ export async function getParsedExifData( return parseExifData(filteredExifData); } catch (e) { if (e.message == exifrUnsupportedFileFormatMessage) { - log.error(`EXIFR does not support format ${exactType}`, e); + log.error(`EXIFR does not support ${extension} files`, e); return undefined; } else { - log.error(`Failed to parse EXIF data of ${exactType} file`, e); + log.error(`Failed to parse EXIF data for a ${extension} file`, e); throw e; } } diff --git a/web/apps/photos/src/services/typeDetectionService.ts b/web/apps/photos/src/services/typeDetectionService.ts index 9644e98cf..b4eaeede8 100644 --- a/web/apps/photos/src/services/typeDetectionService.ts +++ b/web/apps/photos/src/services/typeDetectionService.ts @@ -59,12 +59,12 @@ export const detectFileTypeInfo = async ( } return { fileType, - exactType: typeResult.ext, + extension: typeResult.ext, mimeType: typeResult.mime, }; } catch (e) { const extension = lowercaseExtension(fileOrPath.name); - const known = KnownFileTypeInfos.find((f) => f.exactType == extension); + const known = KnownFileTypeInfos.find((f) => f.extension == extension); if (known) return known; if (KnownNonMediaFileExtensions.includes(extension)) @@ -91,8 +91,8 @@ async function extractElectronFileType(file: ElectronFile) { async function getFileTypeFromBuffer(buffer: Uint8Array) { const result = await FileType.fromBuffer(buffer); - if (!result?.mime) { - throw Error(`Could not deduce MIME type from buffer`); + if (!result?.ext || !result?.mime) { + throw Error(`Could not deduce file type from buffer`); } return result; } diff --git a/web/apps/photos/src/services/upload/metadata.ts b/web/apps/photos/src/services/upload/metadata.ts index b5716e4de..f0e0c0f75 100644 --- a/web/apps/photos/src/services/upload/metadata.ts +++ b/web/apps/photos/src/services/upload/metadata.ts @@ -246,7 +246,7 @@ async function extractLivePhotoMetadata( ): Promise { const imageFileTypeInfo: FileTypeInfo = { fileType: FILE_TYPE.IMAGE, - exactType: fileTypeInfo.imageType, + extension: fileTypeInfo.imageType, }; const { metadata: imageMetadata, diff --git a/web/apps/photos/src/services/upload/thumbnail.ts b/web/apps/photos/src/services/upload/thumbnail.ts index b89bc2f79..4552d11b3 100644 --- a/web/apps/photos/src/services/upload/thumbnail.ts +++ b/web/apps/photos/src/services/upload/thumbnail.ts @@ -5,7 +5,6 @@ import { withTimeout } from "@ente/shared/utils"; import { BLACK_THUMBNAIL_BASE64 } from "constants/upload"; import * as ffmpeg from "services/ffmpeg"; import { heicToJPEG } from "services/heic-convert"; -import { isFileHEIC } from "utils/file"; /** Maximum width or height of the generated thumbnail */ const maxThumbnailDimension = 720; @@ -36,9 +35,9 @@ export const generateThumbnailWeb = async ( const generateImageThumbnailUsingCanvas = async ( blob: Blob, - fileTypeInfo: FileTypeInfo, + { extension }: FileTypeInfo, ) => { - if (isFileHEIC(fileTypeInfo.exactType)) { + if (extension == "heic" || extension == "heif") { log.debug(() => `Pre-converting HEIC to JPEG for thumbnail generation`); blob = await heicToJPEG(blob); } diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 8bd91ad34..bc245bc87 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -341,9 +341,9 @@ const getLivePhotoFileType = async ( const videoFileTypeInfo = await detectFileTypeInfo(livePhotoAssets.video); return { fileType: FILE_TYPE.LIVE_PHOTO, - exactType: `${imageFileTypeInfo.exactType}+${videoFileTypeInfo.exactType}`, - imageType: imageFileTypeInfo.exactType, - videoType: videoFileTypeInfo.exactType, + extension: `${imageFileTypeInfo.extension}+${videoFileTypeInfo.extension}`, + imageType: imageFileTypeInfo.extension, + videoType: videoFileTypeInfo.extension, }; }; @@ -588,7 +588,7 @@ const readLivePhoto = async ( } = await withThumbnail( livePhotoAssets.image, { - exactType: fileTypeInfo.imageType, + extension: fileTypeInfo.imageType, fileType: FILE_TYPE.IMAGE, }, readImage.dataOrStream, diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 5026162da..c8bb8431c 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -1,4 +1,4 @@ -import { FILE_TYPE, type FileTypeInfo } from "@/media/file-type"; +import { FILE_TYPE } from "@/media/file-type"; import { decodeLivePhoto } from "@/media/live-photo"; import { lowercaseExtension } from "@/next/file"; import log from "@/next/log"; @@ -40,9 +40,6 @@ import { isArchivedFile, updateMagicMetadata } from "utils/magicMetadata"; import { safeFileName } from "utils/native-fs"; import { writeStream } from "utils/native-stream"; -const TYPE_HEIC = "heic"; -const TYPE_HEIF = "heif"; - const RAW_FORMATS = [ "heic", "rw2", @@ -287,23 +284,22 @@ export function generateStreamFromArrayBuffer(data: Uint8Array) { } export const getRenderableImage = async (fileName: string, imageBlob: Blob) => { - let fileTypeInfo: FileTypeInfo; try { const tempFile = new File([imageBlob], fileName); - fileTypeInfo = await detectFileTypeInfo(tempFile); + const fileTypeInfo = await detectFileTypeInfo(tempFile); log.debug( - () => - `Obtaining renderable image for ${JSON.stringify(fileTypeInfo)}`, + () => `Need renderable image for ${JSON.stringify(fileTypeInfo)}`, ); - const { exactType } = fileTypeInfo; + const { extension } = fileTypeInfo; - if (!isRawFile(exactType)) { - // Not something we know how to handle yet, give back the original. + if (!isRawFile(extension)) { + // Either it is not something we know how to handle yet, or + // something that the browser already knows how to render. return imageBlob; } const available = !moduleState.isNativeJPEGConversionNotAvailable; - if (isElectron() && available && isSupportedRawFormat(exactType)) { + if (isElectron() && available && isSupportedRawFormat(extension)) { // If we're running in our desktop app, see if our Node.js layer can // convert this into a JPEG using native tools for us. try { @@ -317,17 +313,14 @@ export const getRenderableImage = async (fileName: string, imageBlob: Blob) => { } } - if (isFileHEIC(exactType)) { - // If it is an HEIC file, use our web HEIC converter. + if (extension == "heic" || extension == "heif") { + // For HEIC/HEIF files we can use our web HEIC converter. return await heicToJPEG(imageBlob); } return undefined; } catch (e) { - log.error( - `Failed to get renderable image for ${JSON.stringify(fileTypeInfo ?? fileName)}`, - e, - ); + log.error(`Failed to get renderable image for ${fileName}`, e); return undefined; } }; @@ -346,13 +339,6 @@ const nativeConvertToJPEG = async (imageBlob: Blob) => { return new Blob([jpegData]); }; -export function isFileHEIC(exactType: string) { - return ( - exactType.toLowerCase().endsWith(TYPE_HEIC) || - exactType.toLowerCase().endsWith(TYPE_HEIF) - ); -} - export function isRawFile(exactType: string) { return RAW_FORMATS.includes(exactType.toLowerCase()); } diff --git a/web/packages/media/file-type.ts b/web/packages/media/file-type.ts index e7eb90275..586c75f06 100644 --- a/web/packages/media/file-type.ts +++ b/web/packages/media/file-type.ts @@ -7,7 +7,15 @@ export enum FILE_TYPE { export interface FileTypeInfo { fileType: FILE_TYPE; - exactType: string; + /** + * A lowercased, standardized extension for files of the current type. + * + * TODO(MR): This in not valid for LIVE_PHOTO. + * + * See https://github.com/sindresorhus/file-type/blob/main/core.d.ts for the + * full list of values this property can have. + */ + extension: string; mimeType?: string; imageType?: string; videoType?: string; @@ -15,42 +23,42 @@ export interface FileTypeInfo { // list of format that were missed by type-detection for some files. export const KnownFileTypeInfos: FileTypeInfo[] = [ - { fileType: FILE_TYPE.IMAGE, exactType: "jpeg", mimeType: "image/jpeg" }, - { fileType: FILE_TYPE.IMAGE, exactType: "jpg", mimeType: "image/jpeg" }, - { fileType: FILE_TYPE.VIDEO, exactType: "webm", mimeType: "video/webm" }, - { fileType: FILE_TYPE.VIDEO, exactType: "mod", mimeType: "video/mpeg" }, - { fileType: FILE_TYPE.VIDEO, exactType: "mp4", mimeType: "video/mp4" }, - { fileType: FILE_TYPE.IMAGE, exactType: "gif", mimeType: "image/gif" }, - { fileType: FILE_TYPE.VIDEO, exactType: "dv", mimeType: "video/x-dv" }, + { fileType: FILE_TYPE.IMAGE, extension: "jpeg", mimeType: "image/jpeg" }, + { fileType: FILE_TYPE.IMAGE, extension: "jpg", mimeType: "image/jpeg" }, + { fileType: FILE_TYPE.VIDEO, extension: "webm", mimeType: "video/webm" }, + { fileType: FILE_TYPE.VIDEO, extension: "mod", mimeType: "video/mpeg" }, + { fileType: FILE_TYPE.VIDEO, extension: "mp4", mimeType: "video/mp4" }, + { fileType: FILE_TYPE.IMAGE, extension: "gif", mimeType: "image/gif" }, + { fileType: FILE_TYPE.VIDEO, extension: "dv", mimeType: "video/x-dv" }, { fileType: FILE_TYPE.VIDEO, - exactType: "wmv", + extension: "wmv", mimeType: "video/x-ms-asf", }, { fileType: FILE_TYPE.VIDEO, - exactType: "hevc", + extension: "hevc", mimeType: "video/hevc", }, { fileType: FILE_TYPE.IMAGE, - exactType: "raf", + extension: "raf", mimeType: "image/x-fuji-raf", }, { fileType: FILE_TYPE.IMAGE, - exactType: "orf", + extension: "orf", mimeType: "image/x-olympus-orf", }, { fileType: FILE_TYPE.IMAGE, - exactType: "crw", + extension: "crw", mimeType: "image/x-canon-crw", }, { fileType: FILE_TYPE.VIDEO, - exactType: "mov", + extension: "mov", mimeType: "video/quicktime", }, ];