145 lines
4.1 KiB
TypeScript
145 lines
4.1 KiB
TypeScript
import {
|
|
fileNameFromComponents,
|
|
lowercaseExtension,
|
|
nameAndExtension,
|
|
} from "@/next/file";
|
|
import JSZip from "jszip";
|
|
import { FILE_TYPE } from "./file-type";
|
|
|
|
const potentialImageExtensions = [
|
|
"heic",
|
|
"heif",
|
|
"jpeg",
|
|
"jpg",
|
|
"png",
|
|
"gif",
|
|
"bmp",
|
|
"tiff",
|
|
"webp",
|
|
];
|
|
|
|
const potentialVideoExtensions = [
|
|
"mov",
|
|
"mp4",
|
|
"m4v",
|
|
"avi",
|
|
"wmv",
|
|
"flv",
|
|
"mkv",
|
|
"webm",
|
|
"3gp",
|
|
"3g2",
|
|
"avi",
|
|
"ogv",
|
|
"mpg",
|
|
"mp",
|
|
];
|
|
|
|
/**
|
|
* Use the file extension of the given {@link fileName} to deduce if is is
|
|
* potentially the image or the video part of a Live Photo.
|
|
*/
|
|
export const potentialFileTypeFromExtension = (
|
|
fileName: string,
|
|
): FILE_TYPE | undefined => {
|
|
const ext = lowercaseExtension(fileName);
|
|
if (!ext) return undefined;
|
|
|
|
if (potentialImageExtensions.includes(ext)) return FILE_TYPE.IMAGE;
|
|
else if (potentialVideoExtensions.includes(ext)) return FILE_TYPE.VIDEO;
|
|
else return undefined;
|
|
};
|
|
|
|
/**
|
|
* An in-memory representation of a live photo.
|
|
*/
|
|
interface LivePhoto {
|
|
imageFileName: string;
|
|
imageData: Uint8Array;
|
|
videoFileName: string;
|
|
videoData: Uint8Array;
|
|
}
|
|
|
|
/**
|
|
* Convert a binary serialized representation of a live photo to an in-memory
|
|
* {@link LivePhoto}.
|
|
*
|
|
* A live photo is a zip file containing two files - an image and a video. This
|
|
* functions reads that zip file (blob), and return separate bytes (and
|
|
* filenames) for the image and video parts.
|
|
*
|
|
* @param fileName The name of the overall live photo. Both the image and video
|
|
* parts of the decompressed live photo use this as their name, combined with
|
|
* their original extensions.
|
|
*
|
|
* @param zipBlob A blob contained the zipped data (i.e. the binary serialized
|
|
* live photo).
|
|
*/
|
|
export const decodeLivePhoto = async (
|
|
fileName: string,
|
|
zipBlob: Blob,
|
|
): Promise<LivePhoto> => {
|
|
let imageFileName, videoFileName: string | undefined;
|
|
let imageData, videoData: Uint8Array | undefined;
|
|
|
|
const [name] = nameAndExtension(fileName);
|
|
const zip = await JSZip.loadAsync(zipBlob, { createFolders: true });
|
|
|
|
for (const zipFileName in zip.files) {
|
|
if (zipFileName.startsWith("image")) {
|
|
const [, imageExt] = nameAndExtension(zipFileName);
|
|
imageFileName = fileNameFromComponents([name, imageExt]);
|
|
imageData = await zip.files[zipFileName]?.async("uint8array");
|
|
} else if (zipFileName.startsWith("video")) {
|
|
const [, videoExt] = nameAndExtension(zipFileName);
|
|
videoFileName = fileNameFromComponents([name, videoExt]);
|
|
videoData = await zip.files[zipFileName]?.async("uint8array");
|
|
}
|
|
}
|
|
|
|
if (!imageFileName || !imageData)
|
|
throw new Error(
|
|
`Decoded live photo ${fileName} does not have an image`,
|
|
);
|
|
|
|
if (!videoFileName || !videoData)
|
|
throw new Error(
|
|
`Decoded live photo ${fileName} does not have an image`,
|
|
);
|
|
|
|
return { imageFileName, imageData, videoFileName, videoData };
|
|
};
|
|
|
|
/** Variant of {@link LivePhoto}, but one that allows files and data. */
|
|
interface EncodeLivePhotoInput {
|
|
imageFileName: string;
|
|
imageFileOrData: File | Uint8Array;
|
|
videoFileName: string;
|
|
videoFileOrData: File | Uint8Array;
|
|
}
|
|
|
|
/**
|
|
* Return a binary serialized representation of a live photo.
|
|
*
|
|
* This function takes the (in-memory) image and video data from the
|
|
* {@link livePhoto} object, writes them to a zip file (using the respective
|
|
* filenames), and returns the {@link Uint8Array} that represent the bytes of
|
|
* this zip file.
|
|
*
|
|
* @param livePhoto The in-mem photo to serialized.
|
|
*/
|
|
export const encodeLivePhoto = async ({
|
|
imageFileName,
|
|
imageFileOrData,
|
|
videoFileName,
|
|
videoFileOrData,
|
|
}: EncodeLivePhotoInput) => {
|
|
const [, imageExt] = nameAndExtension(imageFileName);
|
|
const [, videoExt] = nameAndExtension(videoFileName);
|
|
|
|
const zip = new JSZip();
|
|
zip.file(fileNameFromComponents(["image", imageExt]), imageFileOrData);
|
|
zip.file(fileNameFromComponents(["video", videoExt]), videoFileOrData);
|
|
return await zip.generateAsync({ type: "uint8array" });
|
|
};
|