Skip to content

Commit

Permalink
merge: merged image asset id in to beta
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelOsborne committed Jan 20, 2025
2 parents 99cbfa0 + 889dd63 commit 6f7a7dd
Show file tree
Hide file tree
Showing 27 changed files with 600 additions and 376 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-emus-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dotlottie/dotlottie-js': patch
---

image asset ids
4 changes: 2 additions & 2 deletions apps/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"devDependencies": {
"@dotlottie/dotlottie-js": "workspace:*",
"@vitejs/plugin-vue": "^4.1.0",
"typescript": "^5.0.2",
"typescript": "^5.6.3",
"vite": "^4.3.0",
"vue-tsc": "^1.2.0"
"vue-tsc": "^2.1.10"
}
}
1 change: 1 addition & 0 deletions packages/dotlottie-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@lottie-animation-community/lottie-types": "^1.2.0",
"browser-image-hash": "^0.0.5",
"fflate": "^0.8.1",
"file-type": "^19.6.0",
"sharp": "^0.33.2",
"sharp-phash": "^2.1.0",
"valibot": "^0.13.1"
Expand Down
132 changes: 22 additions & 110 deletions packages/dotlottie-js/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,58 +7,12 @@
import type { Animation as AnimationData, Asset } from '@lottie-animation-community/lottie-types';
import type { UnzipFileFilter, Unzipped } from 'fflate';
import { unzip as fflateUnzip, strFromU8 } from 'fflate';
import { fileTypeFromBuffer } from 'file-type';

import type { ManifestV1 } from './v1/common/schemas/manifest';
import type { LottieStateMachine } from './v2/browser';
import type { Manifest as ManifestV2 } from './v2/common/schemas';

export interface MimeTypes {
[key: string]: string;
}

export interface MimeCodes {
[key: string]: number[];
}

export const MIME_TYPES: MimeTypes = {
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
bmp: 'image/bmp',
svg: 'image/svg+xml',
svgxml: 'image/svg+xml',
webp: 'image/webp',
mp3: 'audio/mp3',
};

export const MIME_CODES: MimeCodes = {
jpeg: [0xff, 0xd8, 0xff],
png: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
gif: [0x47, 0x49, 0x46],
bmp: [0x42, 0x4d],
webp: [0x52, 0x49, 0x46, 0x46, 0x3f, 0x3f, 0x3f, 0x3f, 0x57, 0x45, 0x42, 0x50],
// This covers <svg..
svg: [0x3c, 0x73, 0x76, 0x67],
// This covers <?xml..
svgxml: [0x3c, 0x3f, 0x78, 0x6d, 0x6c],
mp3: [0x49, 0x44, 0x33],
};

export interface MimeToExtension {
[key: string]: string;
}

export const MIME_TO_EXTENSION: MimeToExtension = {
'image/jpeg': 'jpeg',
'image/png': 'png',
'image/gif': 'gif',
'image/bmp': 'bmp',
'image/svg+xml': 'svg',
'image/webp': 'webp',
'audio/mpeg': 'mpeg',
'audio/mp3': 'mp3',
};

export enum ErrorCodes {
ASSET_NOT_FOUND = 'ASSET_NOT_FOUND',
INVALID_DOTLOTTIE = 'INVALID_DOTLOTTIE',
Expand Down Expand Up @@ -107,6 +61,12 @@ export const base64ToUint8Array = (base64String: string): Uint8Array => {
return uint8Array;
};

export const getMimeTypeFromUint8Data = async (file: Uint8Array): Promise<string | undefined> => {
const data = await fileTypeFromBuffer(file);

return data?.mime.toString();
};

/**
* Determines the MIME type from a base64-encoded string.
*
Expand All @@ -124,57 +84,12 @@ export const base64ToUint8Array = (base64String: string): Uint8Array => {
*
* @public
*/
export const getMimeTypeFromBase64 = (base64: string): string | undefined => {
let data: string | null = null;
let bytes: number[] = [];

if (!base64) {
throw new DotLottieError(
'Failed to determine the MIME type from the base64 asset string. Please check the input data. Supported asset types for dotLottie-js are: jpeg, png, gif, bmp, svg, webp, mp3',
ErrorCodes.INVALID_DOTLOTTIE,
);
}
export const getMimeTypeFromBase64 = async (base64: string): Promise<string | undefined> => {
const data = base64ToUint8Array(base64);

const withoutMeta = base64.substring(base64.indexOf(',') + 1);
const mime = await fileTypeFromBuffer(data);

if (typeof window === 'undefined') {
data = Buffer.from(withoutMeta, 'base64').toString('binary');
} else {
data = atob(withoutMeta);
}

const bufData = new Uint8Array(data.length);

for (let i = 0; i < data.length; i += 1) {
bufData[i] = data.charCodeAt(i);
}

for (const mimeType in MIME_CODES) {
const dataArr = MIME_CODES[mimeType];

if (mimeType === 'webp' && dataArr && bufData.length > dataArr.length) {
const riffHeader = Array.from(bufData.subarray(0, 4));
const webpFormatMarker = Array.from(bufData.subarray(8, 12));

if (
riffHeader.every((byte, index) => byte === dataArr[index]) &&
webpFormatMarker.every((byte, index) => byte === dataArr[index + 8])
) {
return MIME_TYPES[mimeType];
}
} else {
bytes = Array.from(bufData.subarray(0, dataArr?.length));

if (dataArr && bytes.every((byte, index) => byte === dataArr[index])) {
return MIME_TYPES[mimeType];
}
}
}

throw new DotLottieError(
'Failed to determine the MIME type from the base64 asset string. Please check the input data. Supported asset types for dotLottie-js are: jpeg, png, gif, bmp, svg, webp, mp3',
ErrorCodes.INVALID_DOTLOTTIE,
);
return mime?.mime.toString();
};

/**
Expand All @@ -194,20 +109,17 @@ export const getMimeTypeFromBase64 = (base64: string): string | undefined => {
*
* @public
*/
export const getExtensionTypeFromBase64 = (base64: string): string | null => {
const mimeType = getMimeTypeFromBase64(base64);
export const getExtensionTypeFromBase64 = async (base64: string): Promise<string | undefined> => {
const data = base64ToUint8Array(base64);

if (!mimeType) {
const ext = base64.split(';')[0]?.split('/')[1];

if (ext) {
return MIME_TO_EXTENSION[ext] || null;
}
const mime = await fileTypeFromBuffer(data);

return null;
// To keep mimetype(jpeg) and extension(jpg) consistent
if (mime?.ext.toString() === 'jpg') {
return 'jpeg';
}

return MIME_TO_EXTENSION[mimeType] || null;
return mime?.ext.toString();
};

/**
Expand Down Expand Up @@ -257,7 +169,7 @@ export const isValidURL = (url: string): boolean => {
* const dataUrl = dataUrlFromU8(uint8Data, fileExtension);
* ```
*/
export function dataUrlFromU8(uint8Data: Uint8Array): string {
export async function dataUrlFromU8(uint8Data: Uint8Array): Promise<string> {
let base64: string;

if (typeof window === 'undefined') {
Expand All @@ -270,7 +182,7 @@ export function dataUrlFromU8(uint8Data: Uint8Array): string {
base64 = window.btoa(binaryString);
}

const mimeType = getMimeTypeFromBase64(base64);
const mimeType = await getMimeTypeFromUint8Data(uint8Data);

return `data:${mimeType};base64,${base64}`;
}
Expand Down Expand Up @@ -628,7 +540,7 @@ export async function getAllAudio(dotLottie: Uint8Array, filter?: UnzipFileFilte
if (unzippedSingleAudio instanceof Uint8Array) {
const audioId = audioFilename.replace(audioPath, '');

audio[audioId] = dataUrlFromU8(unzippedSingleAudio);
audio[audioId] = await dataUrlFromU8(unzippedSingleAudio);
}
}

Expand Down Expand Up @@ -783,7 +695,7 @@ export async function getImages(dotLottie: Uint8Array, filter?: UnzipFileFilter)
if (unzippedImage instanceof Uint8Array) {
const imageId = imagePath.replace(imagePath, '');

images[imageId] = dataUrlFromU8(unzippedImage);
images[imageId] = await dataUrlFromU8(unzippedImage);
}
}

Expand Down
69 changes: 48 additions & 21 deletions packages/dotlottie-js/src/v1/__tests__/browser/image.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('LottieImage', () => {
it('gets and sets the zipOptions', () => {
const theme = new LottieImage({
id: 'image_1',
lottieAssetId: 'image_1',
fileName: 'image_1.png',
zipOptions: {
level: 9,
Expand Down Expand Up @@ -65,13 +66,19 @@ describe('LottieImage', () => {

expect(uniqueImages.length).toBe(5);
expect(uniqueImages.map((image) => image.fileName)).toEqual([
'image_0.png',
'image_1.png',
'image_2.png',
'image_3.png',
'image_4.png',
'image_5.png',
]);
expect(uniqueImages.map((image) => image.id)).toEqual(['image_1', 'image_2', 'image_3', 'image_4', 'image_5']);
expect(uniqueImages.map((image) => image.lottieAssetId)).toEqual([
'image_0',
'image_1',
'image_2',
'image_3',
'image_4',
]);
});
});

Expand Down Expand Up @@ -111,13 +118,19 @@ describe('LottieImage', () => {
);

expect(uniqueImages.length).toBe(5);
expect(uniqueImages.map((image) => image.id)).toEqual(['image_1', 'image_2', 'image_3', 'image_4', 'image_5']);
expect(uniqueImages.map((image) => image.lottieAssetId)).toEqual([
'image_0',
'image_1',
'image_2',
'image_3',
'image_4',
]);
expect(uniqueImages.map((image) => image.fileName)).toEqual([
'image_0.png',
'image_1.png',
'image_2.png',
'image_3.png',
'image_4.png',
'image_5.png',
]);
});
});
Expand Down Expand Up @@ -145,7 +158,7 @@ describe('LottieImage', () => {
'image_3.png',
'image_4.png',
]);
expect(uniqueImages.map((image) => image.id)).toEqual(['image_0', 'image_1', 'image_3', 'image_4']);
expect(uniqueImages.map((image) => image.lottieAssetId)).toEqual(['image_0', 'image_1', 'image_3', 'image_4']);
});
});

Expand Down Expand Up @@ -173,7 +186,13 @@ describe('LottieImage', () => {
'image_3.png',
'image_4.png',
]);
expect(uniqueImages.map((image) => image.id)).toEqual(['image_0', 'image_1', 'image_2', 'image_3', 'image_4']);
expect(uniqueImages.map((image) => image.lottieAssetId)).toEqual([
'image_0',
'image_1',
'image_2',
'image_3',
'image_4',
]);
});
});

Expand Down Expand Up @@ -215,61 +234,69 @@ describe('LottieImage', () => {
expect(uniqueImages.length).toBe(5);

expect(uniqueImages.map((image) => image.fileName)).toEqual([
'image_0.jpeg',
'image_1.jpeg',
'image_2.jpeg',
'image_3.jpeg',
'image_4.png',
'image_5.png',
'image_9.png',
'image_1_1.png',
]);
expect(uniqueImages.map((image) => image.lottieAssetId)).toEqual([
'image_0',
'image_1',
'image_3',
'image_4',
'image_1_1',
]);
expect(uniqueImages.map((image) => image.id)).toEqual(['image_1', 'image_2', 'image_4', 'image_5', 'image_9']);
});
});

it('getMimeTypeFromBase64 Properly detects mimetype of images.', async () => {
const jpegFormat = getMimeTypeFromBase64(
const jpegFormat = await getMimeTypeFromBase64(
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAZABkAAD/2wCEABQQEBkSGScXFycyJh8mMi4mJiYmLj41NTU1NT5EQUFBQUFBREREREREREREREREREREREREREREREREREREREQBFRkZIBwgJhgYJjYmICY2RDYrKzZERERCNUJERERERERERERERERERERERERERERERERERERERERERERERERERP/AABEIAAEAAQMBIgACEQEDEQH/xABMAAEBAAAAAAAAAAAAAAAAAAAABQEBAQAAAAAAAAAAAAAAAAAABQYQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AJQA9Yv/2Q==',
);

expect(jpegFormat).toEqual('image/jpeg');

const pngFormat = getMimeTypeFromBase64(
const pngFormat = await getMimeTypeFromBase64(
// eslint-disable-next-line no-secrets/no-secrets
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4z8AAAAMBAQD3A0FDAAAAAElFTkSuQmCC',
);

expect(pngFormat).toEqual('image/png');

const gifFormat = getMimeTypeFromBase64('data:image/gif;base64,R0lGODdhAQABAPAAAP8AAAAAACwAAAAAAQABAAACAkQBADs=');
const gifFormat = await getMimeTypeFromBase64(
'data:image/gif;base64,R0lGODdhAQABAPAAAP8AAAAAACwAAAAAAQABAAACAkQBADs=',
);

expect(gifFormat).toEqual('image/gif');

const bmpFormat = getMimeTypeFromBase64(
const bmpFormat = await getMimeTypeFromBase64(
'data:image/bmp;base64,Qk06AAAAAAAAADYAAAAoAAAAAQAAAAEAAAABABgAAAAAAAQAAADEDgAAxA4AAAAAAAAAAAAAAgD+AA==',
);

expect(bmpFormat).toEqual('image/bmp');

const webpFormat = getMimeTypeFromBase64(
const webpFormat = await getMimeTypeFromBase64(
// eslint-disable-next-line no-secrets/no-secrets
'data:image/webp;base64,UklGRkAAAABXRUJQVlA4IDQAAADwAQCdASoBAAEAAQAcJaACdLoB+AAETAAA/vW4f/6aR40jxpHxcP/ugT90CfugT/3NoAAA',
);

expect(webpFormat).toEqual('image/webp');

const svgFormat = getMimeTypeFromBase64(
const svgFormat = await getMimeTypeFromBase64(
// eslint-disable-next-line no-secrets/no-secrets
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InJlZCIvPjwvc3ZnPg==',
);

expect(svgFormat).toEqual('image/svg+xml');
expect(svgFormat).toEqual(undefined);

const svgXmlFormat = getMimeTypeFromBase64(SVG_XML_TEST);
const svgXmlFormat = await getMimeTypeFromBase64(SVG_XML_TEST);

expect(svgXmlFormat).toEqual('image/svg+xml');
expect(svgXmlFormat).toEqual('application/xml');

const mp3Format = getMimeTypeFromBase64(AUDIO_TEST);
const mp3Format = await getMimeTypeFromBase64(AUDIO_TEST);

expect(mp3Format).toEqual('audio/mp3');
expect(mp3Format).toEqual('audio/mpeg');
});

it('Throws an error when an unrecognized file mimetype is detected.', async () => {
Expand Down
Loading

0 comments on commit 6f7a7dd

Please sign in to comment.