diff --git a/.changeset/perfect-emus-unite.md b/.changeset/perfect-emus-unite.md new file mode 100644 index 0000000..e9b18b9 --- /dev/null +++ b/.changeset/perfect-emus-unite.md @@ -0,0 +1,5 @@ +--- +'@dotlottie/dotlottie-js': patch +--- + +image asset ids diff --git a/apps/vue/package.json b/apps/vue/package.json index 95bf7f8..419ffb4 100644 --- a/apps/vue/package.json +++ b/apps/vue/package.json @@ -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" } } diff --git a/packages/dotlottie-js/package.json b/packages/dotlottie-js/package.json index 309ad1c..34b4a81 100644 --- a/packages/dotlottie-js/package.json +++ b/packages/dotlottie-js/package.json @@ -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" diff --git a/packages/dotlottie-js/src/utils.ts b/packages/dotlottie-js/src/utils.ts index 9c17be3..402d279 100644 --- a/packages/dotlottie-js/src/utils.ts +++ b/packages/dotlottie-js/src/utils.ts @@ -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 { return uint8Array; }; +export const getMimeTypeFromUint8Data = async (file: Uint8Array): Promise => { + const data = await fileTypeFromBuffer(file); + + return data?.mime.toString(); +}; + /** * Determines the MIME type from a base64-encoded string. * @@ -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 => { + 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(); }; /** @@ -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 => { + 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(); }; /** @@ -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 { let base64: string; if (typeof window === 'undefined') { @@ -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}`; } @@ -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); } } @@ -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); } } diff --git a/packages/dotlottie-js/src/v1/__tests__/browser/image.spec.ts b/packages/dotlottie-js/src/v1/__tests__/browser/image.spec.ts index 0e3fb9c..c4cb0e7 100644 --- a/packages/dotlottie-js/src/v1/__tests__/browser/image.spec.ts +++ b/packages/dotlottie-js/src/v1/__tests__/browser/image.spec.ts @@ -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, @@ -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', + ]); }); }); @@ -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', ]); }); }); @@ -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']); }); }); @@ -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', + ]); }); }); @@ -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 () => { diff --git a/packages/dotlottie-js/src/v1/__tests__/node/image.spec.ts b/packages/dotlottie-js/src/v1/__tests__/node/image.spec.ts index b0b792e..4e1058e 100644 --- a/packages/dotlottie-js/src/v1/__tests__/node/image.spec.ts +++ b/packages/dotlottie-js/src/v1/__tests__/node/image.spec.ts @@ -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, @@ -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', + ]); }); }); @@ -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', ]); }); }); @@ -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']); }); }); @@ -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', + ]); }); }); @@ -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.png', '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 () => { diff --git a/packages/dotlottie-js/src/v1/browser/animation.ts b/packages/dotlottie-js/src/v1/browser/animation.ts index 6bf0ca2..7c03f6c 100644 --- a/packages/dotlottie-js/src/v1/browser/animation.ts +++ b/packages/dotlottie-js/src/v1/browser/animation.ts @@ -58,24 +58,28 @@ export class LottieAnimationV1 extends LottieAnimationCommonV1 { } let extType = null; - const fileType = getExtensionTypeFromBase64(asset.p); - - extType = fileType; - - const fileName = `${asset.id}.${extType}`; - - this._imageAssets.push( - new LottieImageV1({ - data: asset.p, - id: asset.id, - fileName, - parentAnimations: [this], - }), - ); - - asset.p = fileName; - asset.u = '/images/'; - asset.e = 0; + const fileType = await getExtensionTypeFromBase64(asset.p); + + // If we don't recognize the file type, we leave it inside the animation as is. + if (fileType) { + extType = fileType; + + const fileName = `${asset.id}.${extType}`; + + this._imageAssets.push( + new LottieImageV1({ + data: asset.p, + id: asset.id, + lottieAssetId: asset.id, + fileName, + parentAnimations: [this], + }), + ); + + asset.p = fileName; + asset.u = '/images/'; + asset.e = 0; + } } } @@ -105,7 +109,7 @@ export class LottieAnimationV1 extends LottieAnimationCommonV1 { } let extType = null; - const fileType = getExtensionTypeFromBase64(asset.p); + const fileType = await getExtensionTypeFromBase64(asset.p); extType = fileType; diff --git a/packages/dotlottie-js/src/v1/browser/dotlottie.ts b/packages/dotlottie-js/src/v1/browser/dotlottie.ts index 5380c18..d58ff47 100644 --- a/packages/dotlottie-js/src/v1/browser/dotlottie.ts +++ b/packages/dotlottie-js/src/v1/browser/dotlottie.ts @@ -248,13 +248,14 @@ export class DotLottieV1 extends DotLottieCommonV1 { let decodedImg = btoa(decodedStr); - const ext = getExtensionTypeFromBase64(decodedImg); + const ext = await getExtensionTypeFromBase64(decodedImg); // Push the images in to a temporary array decodedImg = `data:image/${ext};base64,${decodedImg}`; tmpImages.push( new LottieImageV1({ id: imageId, + lottieAssetId: imageId, data: decodedImg, fileName: key.split('/')[1] || '', }), @@ -269,7 +270,7 @@ export class DotLottieV1 extends DotLottieCommonV1 { let decodedAudio = btoa(decodedStr); - const ext = getExtensionTypeFromBase64(decodedAudio); + const ext = await getExtensionTypeFromBase64(decodedAudio); // Push the audio in to a temporary array decodedAudio = `data:audio/${ext};base64,${decodedAudio}`; @@ -292,7 +293,7 @@ export class DotLottieV1 extends DotLottieCommonV1 { if (animationAssets) { for (const asset of animationAssets) { if ('w' in asset && 'h' in asset) { - if (asset.p.includes(image.id)) { + if (asset.p === image.fileName) { image.parentAnimations.push(parentAnimation); parentAnimation.imageAssets.push(image); } diff --git a/packages/dotlottie-js/src/v1/common/animation.ts b/packages/dotlottie-js/src/v1/common/animation.ts index 9ee79c4..924a890 100644 --- a/packages/dotlottie-js/src/v1/common/animation.ts +++ b/packages/dotlottie-js/src/v1/common/animation.ts @@ -330,6 +330,7 @@ export class LottieAnimationCommonV1 { for (const asset of animationAssets) { if ('w' in asset && 'h' in asset && !('xt' in asset) && 'p' in asset) { for (const image of images) { + // If we only compare paths, we can leave the asset.id alone if (image.fileName === asset.p) { // encoded is true asset.e = 1; diff --git a/packages/dotlottie-js/src/v1/common/audio.ts b/packages/dotlottie-js/src/v1/common/audio.ts index 9fcd544..c3d1205 100644 --- a/packages/dotlottie-js/src/v1/common/audio.ts +++ b/packages/dotlottie-js/src/v1/common/audio.ts @@ -5,7 +5,7 @@ import type { ZipOptions } from 'fflate'; import type { AudioData } from '../../types'; -import { dataUrlFromU8, DotLottieError, ErrorCodes } from '../../utils'; +import { dataUrlFromU8, DotLottieError, ErrorCodes, getExtensionTypeFromBase64 } from '../../utils'; import type { LottieAnimationCommonV1 } from './animation'; @@ -114,18 +114,18 @@ export class LottieAudioCommonV1 { * Renames the id and fileName to newName. * @param newName - A new id and filename for the audio. */ - public renameAudio(newName: string): void { + public async renameAudio(newName: string): Promise { this.id = newName; - if (this.fileName) { - let fileExt = this.fileName.split('.').pop(); + const data = await this.toDataURL(); - if (!fileExt) { - fileExt = '.png'; - } - // Default to png if the file extension isn't available - this.fileName = `${newName}.${fileExt}`; + const ext = await getExtensionTypeFromBase64(data); + + if (!ext) { + throw new DotLottieError('File extension type could not be detected from asset file.'); } + + this.fileName = `${newName}.${ext}`; } public async toArrayBuffer(): Promise { diff --git a/packages/dotlottie-js/src/v1/common/dotlottie.ts b/packages/dotlottie-js/src/v1/common/dotlottie.ts index 25e7b22..0aa8a71 100644 --- a/packages/dotlottie-js/src/v1/common/dotlottie.ts +++ b/packages/dotlottie-js/src/v1/common/dotlottie.ts @@ -174,11 +174,17 @@ export class DotLottieCommonV1 { * @param newName - desired id and fileName, * @param imageId - The id of the LottieImageV1 to rename */ - private _renameImage(animation: LottieAnimationCommonV1, newName: string, imageId: string): void { - animation.imageAssets.forEach((imageAsset) => { - if (imageAsset.id === imageId) { + private async _renameImage( + animation: LottieAnimationCommonV1, + newLottieAssetId: string, + lottieAssetId: string, + ): Promise { + for (const imageAsset of animation.imageAssets) { + if (imageAsset.lottieAssetId === lottieAssetId) { // Rename the LottieImageV1 - imageAsset.renameImage(newName); + const oldPath = imageAsset.fileName; + + await imageAsset.renameImage(newLottieAssetId); if (!animation.data) throw new DotLottieError('No animation data available.'); @@ -189,38 +195,81 @@ export class DotLottieCommonV1 { // Find the image asset inside the animation data and rename its path for (const asset of animationAssets) { if ('w' in asset && 'h' in asset) { - if (asset.id === imageId) { + if (asset.p === oldPath) { asset.p = imageAsset.fileName; } } } } - }); + } } - private _renameImageAssets(): void { - const images: Map = new Map(); + /** + * Generates a map of duplicate image ids and their count. + * @returns Map of duplicate image ids and their count. + */ + private _generateMapOfOccurencesFromImageIds(): Map { + const dupeMap = new Map(); this.animations.forEach((animation) => { - images.set(animation.id, animation.imageAssets); + animation.imageAssets.forEach((imageAsset) => { + if (dupeMap.has(imageAsset.lottieAssetId)) { + const count = dupeMap.get(imageAsset.lottieAssetId) ?? 0; + + dupeMap.set(imageAsset.lottieAssetId, count + 1); + } else { + dupeMap.set(imageAsset.lottieAssetId, 1); + } + }); }); - let size = 0; + return dupeMap; + } - images.forEach((value) => { - size += value.length; - }); + /** + * Renames the image assets in all animations to avoid conflicts. + * + * Steps: + * - Generate how many times across all animations the same image id has been used. + * - Loop through every animation in reverse order + * - Every time an animation uses an image asset that is also used elsewhere, append the count to the image's asset id and then decrement. + * + * Result of renaming for every animation: + * + * - Inside the Lottie's data and it's Asset object: + * - The Asset id stays the same, meaning that every reference to the asset is still valid (refId) + * - The path is changed to the new asset id with the format \{assetId\}_\{count\} + * + * - On the dotLottie file system scope: + * - The image file name is changed to the new asset id \{assetId\}_\{count\}.\{ext\} + */ + private async _renameImageAssets(): Promise { + const occurenceMap = this._generateMapOfOccurencesFromImageIds(); + // Loop over every animation for (let i = this.animations.length - 1; i >= 0; i -= 1) { const animation = this.animations.at(i); if (animation) { + // Loop over every image asset of the animation for (let j = animation.imageAssets.length - 1; j >= 0; j -= 1) { const image = animation.imageAssets.at(j); if (image) { - this._renameImage(animation, `image_${size}`, image.id); - size -= 1; + // Get how many times the same image id has been used + let count = occurenceMap.get(image.lottieAssetId) ?? 0; + + if (count > 0) { + count -= 1; + } + + // Decrement the count + occurenceMap.set(image.lottieAssetId, count); + + if (count > 0) { + // Rename the with n-1 count + await this._renameImage(animation, `${image.lottieAssetId}_${count}`, image.lottieAssetId); + } } } } @@ -232,11 +281,11 @@ export class DotLottieCommonV1 { * @param newName - desired id and fileName, * @param audioId - The id of the LottieAudioV1 to rename */ - private _renameAudio(animation: LottieAnimationCommonV1, newName: string, audioId: string): void { - animation.audioAssets.forEach((audioAsset) => { + private async _renameAudio(animation: LottieAnimationCommonV1, newName: string, audioId: string): Promise { + for (const audioAsset of animation.audioAssets) { if (audioAsset.id === audioId) { // Rename the LottieImageV1 - audioAsset.renameAudio(newName); + await audioAsset.renameAudio(newName); if (!animation.data) throw new DotLottieError('No animation data available.'); @@ -253,10 +302,10 @@ export class DotLottieCommonV1 { } } } - }); + } } - private _renameAudioAssets(): void { + private async _renameAudioAssets(): Promise { const audio: Map = new Map(); this.animations.forEach((animation) => { @@ -277,7 +326,7 @@ export class DotLottieCommonV1 { const audioAsset = animation.audioAssets.at(j); if (audioAsset) { - this._renameAudio(animation, `audio_${size}`, audioAsset.id); + await this._renameAudio(animation, `audio_${size}`, audioAsset.id); size -= 1; } } @@ -430,8 +479,8 @@ export class DotLottieCommonV1 { if (this.animations.length > 1) { // Rename assets incrementally if there are multiple animations - this._renameImageAssets(); - this._renameAudioAssets(); + await this._renameImageAssets(); + await this._renameAudioAssets(); } const parallelPlugins = []; diff --git a/packages/dotlottie-js/src/v1/common/image.ts b/packages/dotlottie-js/src/v1/common/image.ts index 4386919..d124862 100644 --- a/packages/dotlottie-js/src/v1/common/image.ts +++ b/packages/dotlottie-js/src/v1/common/image.ts @@ -5,7 +5,7 @@ import type { ZipOptions } from 'fflate'; import type { ImageData } from '../../types'; -import { dataUrlFromU8, DotLottieError } from '../../utils'; +import { dataUrlFromU8, DotLottieError, getExtensionTypeFromBase64 } from '../../utils'; import type { LottieAnimationCommonV1 } from './animation'; @@ -13,6 +13,7 @@ export interface ImageOptionsV1 { data?: ImageData; fileName: string; id: string; + lottieAssetId: string; parentAnimations?: LottieAnimationCommonV1[]; zipOptions?: ZipOptions; } @@ -20,8 +21,16 @@ export interface ImageOptionsV1 { export class LottieImageCommonV1 { protected _data?: ImageData; + /** + * Unique id for the LottieImageCommon object. This is never modified. + */ protected _id: string = ''; + /** + * Asset id representing the image asset inside the Lottie animation. This can be modified. + */ + protected _lottieAssetId: string = ''; + protected _fileName: string = ''; protected _parentAnimations: LottieAnimationCommonV1[]; @@ -30,6 +39,7 @@ export class LottieImageCommonV1 { public constructor(options: ImageOptionsV1) { this._requireValidId(options.id); + this._requireValidLottieAssetId(options.lottieAssetId); this._requireValidFileName(options.fileName); this._zipOptions = options.zipOptions ?? {}; @@ -42,6 +52,10 @@ export class LottieImageCommonV1 { this._id = options.id; } + if (options.lottieAssetId) { + this._lottieAssetId = options.lottieAssetId; + } + if (options.fileName) { this._fileName = options.fileName; } @@ -67,6 +81,16 @@ export class LottieImageCommonV1 { if (!id) throw new DotLottieError('Invalid image id'); } + /** + * Ensure that the provided id is a valid string. + * The id must be a non-empty string, otherwise an error will be thrown. + * @param id - The id to validate. + * @throws Error - if the id is not a valid string. + */ + private _requireValidLottieAssetId(id: string | undefined): asserts id is string { + if (!id) throw new DotLottieError('Invalid Lottie Image Asset Id'); + } + /** * Ensure that the provided fileName is a valid string. * The fileName must be a non-empty string, otherwise an error will be thrown. @@ -97,6 +121,15 @@ export class LottieImageCommonV1 { this._id = id; } + public get lottieAssetId(): string { + return this._lottieAssetId; + } + + public set lottieAssetId(id: string) { + this._requireValidLottieAssetId(id); + this._lottieAssetId = id; + } + public get data(): ImageData | undefined { return this._data; } @@ -126,21 +159,21 @@ export class LottieImageCommonV1 { } /** - * Renames the id and fileName to newName. - * @param newName - A new id and filename for the image. + * Renames the lottieAssetId and fileName to newName. + * @param newName - A new lottieAssetId and filename for the image. */ - public renameImage(newName: string): void { - this.id = newName; + public async renameImage(newLottieAssetId: string): Promise { + this._lottieAssetId = newLottieAssetId; - if (this.fileName) { - let fileExt = this.fileName.split('.').pop(); + const data = await this.toDataURL(); - if (!fileExt) { - fileExt = '.png'; - } - // Default to png if the file extension isn't available - this.fileName = `${newName}.${fileExt}`; + const ext = await getExtensionTypeFromBase64(data); + + if (!ext) { + throw new DotLottieError('File extension type could not be detected from asset file.'); } + + this.fileName = `${newLottieAssetId}.${ext}`; } public async toArrayBuffer(): Promise { diff --git a/packages/dotlottie-js/src/v1/common/plugins/duplicate-image-detector.ts b/packages/dotlottie-js/src/v1/common/plugins/duplicate-image-detector.ts index 43cb854..71fd2b6 100644 --- a/packages/dotlottie-js/src/v1/common/plugins/duplicate-image-detector.ts +++ b/packages/dotlottie-js/src/v1/common/plugins/duplicate-image-detector.ts @@ -52,7 +52,7 @@ export class DuplicateImageDetectorCommon extends DotLottieV1Plugin { // Now that we have a single image of the image array, compare it to every other image in the arry for (const compareImage of images) { if ( - image.image.id !== compareImage.image.id && + image.image.lottieAssetId !== compareImage.image.lottieAssetId && !image.excludeFromExport && !compareImage.excludeFromExport && image.hash && @@ -67,7 +67,11 @@ export class DuplicateImageDetectorCommon extends DotLottieV1Plugin { recordOfDuplicates[image.image.fileName] = [compareImage.image]; } else if (recordOfDuplicates[compareImage.image.fileName]) { // Check for duplicates, otherwise push the duplicate image - if (!recordOfDuplicates[compareImage.image.fileName]?.find((item) => item.id === image.image.id)) { + if ( + !recordOfDuplicates[compareImage.image.fileName]?.find( + (item) => item.lottieAssetId === image.image.lottieAssetId, + ) + ) { image.excludeFromExport = true; recordOfDuplicates[compareImage.image.fileName]?.push(image.image); } @@ -139,6 +143,7 @@ export class DuplicateImageDetectorCommon extends DotLottieV1Plugin { clonedImages[key] = new LottieImageV1({ data: image.data, id: image.id, + lottieAssetId: image.lottieAssetId, fileName: image.fileName, }); } diff --git a/packages/dotlottie-js/src/v1/node/animation.ts b/packages/dotlottie-js/src/v1/node/animation.ts index 44f5e02..1c89a18 100644 --- a/packages/dotlottie-js/src/v1/node/animation.ts +++ b/packages/dotlottie-js/src/v1/node/animation.ts @@ -53,24 +53,27 @@ export class LottieAnimationV1 extends LottieAnimationCommonV1 { } let extType = null; - const fileType = getExtensionTypeFromBase64(asset.p); - - extType = fileType; - - const fileName = `${asset.id}.${extType}`; - - this._imageAssets.push( - new LottieImageV1({ - data: asset.p, - id: asset.id, - fileName, - parentAnimations: [this], - }), - ); - - asset.p = fileName; - asset.u = '/images/'; - asset.e = 0; + const fileType = await getExtensionTypeFromBase64(asset.p); + + if (fileType) { + extType = fileType; + + const fileName = `${asset.id}.${extType}`; + + this._imageAssets.push( + new LottieImageV1({ + data: asset.p, + id: asset.id, + lottieAssetId: asset.id, + fileName, + parentAnimations: [this], + }), + ); + + asset.p = fileName; + asset.u = '/images/'; + asset.e = 0; + } } } @@ -100,7 +103,7 @@ export class LottieAnimationV1 extends LottieAnimationCommonV1 { } let extType = null; - const fileType = getExtensionTypeFromBase64(asset.p); + const fileType = await getExtensionTypeFromBase64(asset.p); extType = fileType; diff --git a/packages/dotlottie-js/src/v1/node/dotlottie.ts b/packages/dotlottie-js/src/v1/node/dotlottie.ts index fffa31c..e126782 100644 --- a/packages/dotlottie-js/src/v1/node/dotlottie.ts +++ b/packages/dotlottie-js/src/v1/node/dotlottie.ts @@ -222,14 +222,18 @@ export class DotLottieV1 extends DotLottieCommonV1 { const base64 = Buffer.from(decompressedFile).toString('base64'); - const ext = getExtensionTypeFromBase64(base64); + const ext = await getExtensionTypeFromBase64(base64); + if (!ext) { + throw new DotLottieError('Unrecognized asset file format.'); + } // Push the images in to a temporary array const imgDataURL = `data:image/${ext};base64,${base64}`; tmpImages.push( new LottieImageV1({ id: imageId, + lottieAssetId: imageId, data: imgDataURL, fileName: key.split('/')[1] || '', }), @@ -245,7 +249,7 @@ export class DotLottieV1 extends DotLottieCommonV1 { const base64 = Buffer.from(decompressedFile).toString('base64'); - const ext = getExtensionTypeFromBase64(base64); + const ext = await getExtensionTypeFromBase64(base64); // Push the images in to a temporary array const audioDataURL = `data:audio/${ext};base64,${base64}`; @@ -269,7 +273,7 @@ export class DotLottieV1 extends DotLottieCommonV1 { if (animationAssets) { for (const asset of animationAssets) { if ('w' in asset && 'h' in asset) { - if (asset.p.includes(image.id)) { + if (asset.p === image.fileName) { image.parentAnimations.push(parentAnimation); parentAnimation.imageAssets.push(image); } diff --git a/packages/dotlottie-js/src/v2/__tests__/browser/image.spec.ts b/packages/dotlottie-js/src/v2/__tests__/browser/image.spec.ts index e2b98b5..9dbf82e 100644 --- a/packages/dotlottie-js/src/v2/__tests__/browser/image.spec.ts +++ b/packages/dotlottie-js/src/v2/__tests__/browser/image.spec.ts @@ -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, @@ -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', + ]); }); }); @@ -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', ]); }); }); @@ -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']); }); }); @@ -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', + ]); }); }); @@ -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.png', '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/svg+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 () => { diff --git a/packages/dotlottie-js/src/v2/__tests__/node/dotlottie.spec.ts b/packages/dotlottie-js/src/v2/__tests__/node/dotlottie.spec.ts index 3c32f3b..2bb73ce 100644 --- a/packages/dotlottie-js/src/v2/__tests__/node/dotlottie.spec.ts +++ b/packages/dotlottie-js/src/v2/__tests__/node/dotlottie.spec.ts @@ -868,11 +868,11 @@ describe('theming', () => { 'manifest.json', 'a/ball.json', 'a/bull.json', + 'i/image_0.png', 'i/image_1.png', 'i/image_2.png', 'i/image_3.png', 'i/image_4.png', - 'i/image_5.png', 't/light.json', 't/dark.json', ]); diff --git a/packages/dotlottie-js/src/v2/__tests__/node/image.spec.ts b/packages/dotlottie-js/src/v2/__tests__/node/image.spec.ts index 802230a..ccbc629 100644 --- a/packages/dotlottie-js/src/v2/__tests__/node/image.spec.ts +++ b/packages/dotlottie-js/src/v2/__tests__/node/image.spec.ts @@ -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, @@ -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', + ]); }); }); @@ -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', ]); }); }); @@ -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']); }); }); @@ -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', + ]); }); }); @@ -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.png', '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 () => { diff --git a/packages/dotlottie-js/src/v2/browser/animation.ts b/packages/dotlottie-js/src/v2/browser/animation.ts index 775b430..c56149f 100644 --- a/packages/dotlottie-js/src/v2/browser/animation.ts +++ b/packages/dotlottie-js/src/v2/browser/animation.ts @@ -58,24 +58,28 @@ export class LottieAnimation extends LottieAnimationCommon { } let extType = null; - const fileType = getExtensionTypeFromBase64(asset.p); - - extType = fileType; - - const fileName = `${asset.id}.${extType}`; - - this._imageAssets.push( - new LottieImage({ - data: asset.p, - id: asset.id, - fileName, - parentAnimations: [this], - }), - ); - - asset.p = fileName; - asset.u = '/i/'; - asset.e = 0; + const fileType = await getExtensionTypeFromBase64(asset.p); + + // If we don't recognize the file type, we leave it inside the animation as is. + if (fileType) { + extType = fileType; + + const fileName = `${asset.id}.${extType}`; + + this._imageAssets.push( + new LottieImage({ + data: asset.p, + id: asset.id, + lottieAssetId: asset.id, + fileName, + parentAnimations: [this], + }), + ); + + asset.p = fileName; + asset.u = '/i/'; + asset.e = 0; + } } } @@ -105,7 +109,7 @@ export class LottieAnimation extends LottieAnimationCommon { } let extType = null; - const fileType = getExtensionTypeFromBase64(asset.p); + const fileType = await getExtensionTypeFromBase64(asset.p); extType = fileType; diff --git a/packages/dotlottie-js/src/v2/browser/dotlottie.ts b/packages/dotlottie-js/src/v2/browser/dotlottie.ts index d9e66eb..d6570ec 100644 --- a/packages/dotlottie-js/src/v2/browser/dotlottie.ts +++ b/packages/dotlottie-js/src/v2/browser/dotlottie.ts @@ -241,13 +241,14 @@ export class DotLottie extends DotLottieCommon { let decodedImg = btoa(decodedStr); - const ext = getExtensionTypeFromBase64(decodedImg); + const ext = await getExtensionTypeFromBase64(decodedImg); // Push the images in to a temporary array decodedImg = `data:image/${ext};base64,${decodedImg}`; tmpImages.push( new LottieImage({ id: imageId, + lottieAssetId: imageId, data: decodedImg, fileName: key.split('/')[1] || '', }), @@ -262,7 +263,7 @@ export class DotLottie extends DotLottieCommon { let decodedAudio = btoa(decodedStr); - const ext = getExtensionTypeFromBase64(decodedAudio); + const ext = await getExtensionTypeFromBase64(decodedAudio); // Push the audio in to a temporary array decodedAudio = `data:audio/${ext};base64,${decodedAudio}`; @@ -319,7 +320,7 @@ export class DotLottie extends DotLottieCommon { if (animationAssets) { for (const asset of animationAssets) { if ('w' in asset && 'h' in asset) { - if (asset.p.includes(image.id)) { + if (asset.p === image.fileName) { image.parentAnimations.push(parentAnimation); parentAnimation.imageAssets.push(image); } diff --git a/packages/dotlottie-js/src/v2/common/audio.ts b/packages/dotlottie-js/src/v2/common/audio.ts index 49939c8..0c5b5e2 100644 --- a/packages/dotlottie-js/src/v2/common/audio.ts +++ b/packages/dotlottie-js/src/v2/common/audio.ts @@ -5,7 +5,7 @@ import type { ZipOptions } from 'fflate'; import type { AudioData } from '../../types'; -import { dataUrlFromU8, DotLottieError, ErrorCodes } from '../../utils'; +import { dataUrlFromU8, DotLottieError, ErrorCodes, getExtensionTypeFromBase64 } from '../../utils'; import type { LottieAnimationCommon } from './animation'; @@ -114,18 +114,18 @@ export class LottieAudioCommon { * Renames the id and fileName to newName. * @param newName - A new id and filename for the audio. */ - public renameAudio(newName: string): void { + public async renameAudio(newName: string): Promise { this.id = newName; - if (this.fileName) { - let fileExt = this.fileName.split('.').pop(); + const data = await this.toDataURL(); - if (!fileExt) { - fileExt = '.png'; - } - // Default to png if the file extension isn't available - this.fileName = `${newName}.${fileExt}`; + const ext = await getExtensionTypeFromBase64(data); + + if (!ext) { + throw new DotLottieError('File extension type could not be detected from asset file.'); } + + this.fileName = `${newName}.${ext}`; } public async toArrayBuffer(): Promise { diff --git a/packages/dotlottie-js/src/v2/common/dotlottie.ts b/packages/dotlottie-js/src/v2/common/dotlottie.ts index b46e8b1..4269ca6 100644 --- a/packages/dotlottie-js/src/v2/common/dotlottie.ts +++ b/packages/dotlottie-js/src/v2/common/dotlottie.ts @@ -109,11 +109,17 @@ export class DotLottieCommon { * @param newName - desired id and fileName, * @param imageId - The id of the LottieImage to rename */ - private _renameImage(animation: LottieAnimationCommon, newName: string, imageId: string): void { - animation.imageAssets.forEach((imageAsset) => { - if (imageAsset.id === imageId) { - // Rename the LottieImage - imageAsset.renameImage(newName); + private async _renameImage( + animation: LottieAnimationCommon, + newLottieAssetId: string, + lottieAssetId: string, + ): Promise { + for (const imageAsset of animation.imageAssets) { + if (imageAsset.lottieAssetId === lottieAssetId) { + const oldPath = imageAsset.fileName; + + // Rename image will change the fileName using the newLottieAssetId and append the detected extension + await imageAsset.renameImage(newLottieAssetId); if (!animation.data) throw new DotLottieError('No animation data available.'); @@ -124,38 +130,81 @@ export class DotLottieCommon { // Find the image asset inside the animation data and rename its path for (const asset of animationAssets) { if ('w' in asset && 'h' in asset) { - if (asset.id === imageId) { + if (asset.p === oldPath) { asset.p = imageAsset.fileName; } } } } - }); + } } - private _renameImageAssets(): void { - const images: Map = new Map(); + /** + * Generates a map of duplicate image ids and their count. + * @returns Map of duplicate image ids and their count. + */ + private _generateMapOfOccurencesFromImageIds(): Map { + const dupeMap = new Map(); this.animations.forEach((animation) => { - images.set(animation.id, animation.imageAssets); + animation.imageAssets.forEach((imageAsset) => { + if (dupeMap.has(imageAsset.lottieAssetId)) { + const count = dupeMap.get(imageAsset.lottieAssetId) ?? 0; + + dupeMap.set(imageAsset.lottieAssetId, count + 1); + } else { + dupeMap.set(imageAsset.lottieAssetId, 1); + } + }); }); - let size = 0; + return dupeMap; + } - images.forEach((value) => { - size += value.length; - }); + /** + * Renames the image assets in all animations to avoid conflicts. + * + * Steps: + * - Generate how many times across all animations the same image id has been used. + * - Loop through every animation in reverse order + * - Every time an animation uses an image asset that is also used elsewhere, append the count to the image's asset id and then decrement. + * + * Result of renaming for every animation: + * + * - Inside the Lottie's data and it's Asset object: + * - The Asset id stays the same, meaning that every reference to the asset is still valid (refId) + * - The path is changed to the new asset id with the format \{assetId\}_\{count\} + * + * - On the dotLottie file system scope: + * - The image file name is changed to the new asset id \{assetId\}_\{count\}.\{ext\} + */ + private async _renameImageAssets(): Promise { + const occurenceMap = this._generateMapOfOccurencesFromImageIds(); + // Loop over every animation for (let i = this.animations.length - 1; i >= 0; i -= 1) { const animation = this.animations.at(i); if (animation) { + // Loop over every image asset of the animation for (let j = animation.imageAssets.length - 1; j >= 0; j -= 1) { const image = animation.imageAssets.at(j); if (image) { - this._renameImage(animation, `image_${size}`, image.id); - size -= 1; + // Get how many times the same image id has been used + let count = occurenceMap.get(image.lottieAssetId) ?? 0; + + if (count > 0) { + count -= 1; + } + + // Decrement the count + occurenceMap.set(image.lottieAssetId, count); + + if (count > 0) { + // Rename the with n-1 count + await this._renameImage(animation, `${image.lottieAssetId}_${count}`, image.lottieAssetId); + } } } } @@ -167,11 +216,11 @@ export class DotLottieCommon { * @param newName - desired id and fileName, * @param audioId - The id of the LottieAudio to rename */ - private _renameAudio(animation: LottieAnimationCommon, newName: string, audioId: string): void { - animation.audioAssets.forEach((audioAsset) => { + private async _renameAudio(animation: LottieAnimationCommon, newName: string, audioId: string): Promise { + for (const audioAsset of animation.audioAssets) { if (audioAsset.id === audioId) { // Rename the LottieImage - audioAsset.renameAudio(newName); + await audioAsset.renameAudio(newName); if (!animation.data) throw new DotLottieError('No animation data available.'); @@ -188,10 +237,10 @@ export class DotLottieCommon { } } } - }); + } } - private _renameAudioAssets(): void { + private async _renameAudioAssets(): Promise { const audio: Map = new Map(); this.animations.forEach((animation) => { @@ -212,7 +261,7 @@ export class DotLottieCommon { const audioAsset = animation.audioAssets.at(j); if (audioAsset) { - this._renameAudio(animation, `audio_${size}`, audioAsset.id); + await this._renameAudio(animation, `audio_${size}`, audioAsset.id); size -= 1; } } @@ -392,8 +441,8 @@ export class DotLottieCommon { if (this.animations.length > 1) { // Rename assets incrementally if there are multiple animations - this._renameImageAssets(); - this._renameAudioAssets(); + await this._renameImageAssets(); + await this._renameAudioAssets(); } const parallelPlugins = []; diff --git a/packages/dotlottie-js/src/v2/common/image.ts b/packages/dotlottie-js/src/v2/common/image.ts index 04437c9..d1d29f3 100644 --- a/packages/dotlottie-js/src/v2/common/image.ts +++ b/packages/dotlottie-js/src/v2/common/image.ts @@ -5,7 +5,7 @@ import type { ZipOptions } from 'fflate'; import type { ImageData } from '../../types'; -import { dataUrlFromU8, DotLottieError } from '../../utils'; +import { dataUrlFromU8, DotLottieError, getExtensionTypeFromBase64 } from '../../utils'; import type { LottieAnimationCommon } from './animation'; @@ -13,6 +13,7 @@ export interface ImageOptions { data?: ImageData; fileName: string; id: string; + lottieAssetId: string; parentAnimations?: LottieAnimationCommon[]; zipOptions?: ZipOptions; } @@ -20,8 +21,16 @@ export interface ImageOptions { export class LottieImageCommon { protected _data?: ImageData; + /** + * Unique id for the LottieImageCommon object. This is never modified. + */ protected _id: string = ''; + /** + * Asset id representing the image asset inside the Lottie animation. This can be modified. + */ + protected _lottieAssetId: string = ''; + protected _fileName: string = ''; protected _parentAnimations: LottieAnimationCommon[]; @@ -30,6 +39,7 @@ export class LottieImageCommon { public constructor(options: ImageOptions) { this._requireValidId(options.id); + this._requireValidLottieAssetId(options.lottieAssetId); this._requireValidFileName(options.fileName); this._zipOptions = options.zipOptions ?? {}; @@ -42,6 +52,10 @@ export class LottieImageCommon { this._id = options.id; } + if (options.lottieAssetId) { + this._lottieAssetId = options.lottieAssetId; + } + if (options.fileName) { this._fileName = options.fileName; } @@ -67,6 +81,16 @@ export class LottieImageCommon { if (!id) throw new DotLottieError('Invalid image id'); } + /** + * Ensure that the provided id is a valid string. + * The id must be a non-empty string, otherwise an error will be thrown. + * @param id - The id to validate. + * @throws Error - if the id is not a valid string. + */ + private _requireValidLottieAssetId(id: string | undefined): asserts id is string { + if (!id) throw new DotLottieError('Invalid Lottie Image Asset Id'); + } + /** * Ensure that the provided fileName is a valid string. * The fileName must be a non-empty string, otherwise an error will be thrown. @@ -97,6 +121,15 @@ export class LottieImageCommon { this._id = id; } + public get lottieAssetId(): string { + return this._lottieAssetId; + } + + public set lottieAssetId(id: string) { + this._requireValidLottieAssetId(id); + this._lottieAssetId = id; + } + public get data(): ImageData | undefined { return this._data; } @@ -129,18 +162,18 @@ export class LottieImageCommon { * Renames the id and fileName to newName. * @param newName - A new id and filename for the image. */ - public renameImage(newName: string): void { - this.id = newName; + public async renameImage(newLottieAssetId: string): Promise { + this._lottieAssetId = newLottieAssetId; - if (this.fileName) { - let fileExt = this.fileName.split('.').pop(); + const data = await this.toDataURL(); - if (!fileExt) { - fileExt = '.png'; - } - // Default to png if the file extension isn't available - this.fileName = `${newName}.${fileExt}`; + const ext = await getExtensionTypeFromBase64(data); + + if (!ext) { + throw new DotLottieError('File extension type could not be detected from asset file.'); } + + this.fileName = `${newLottieAssetId}.${ext}`; } public async toArrayBuffer(): Promise { diff --git a/packages/dotlottie-js/src/v2/common/plugins/duplicate-image-detector.ts b/packages/dotlottie-js/src/v2/common/plugins/duplicate-image-detector.ts index 1d5b697..e2f2395 100644 --- a/packages/dotlottie-js/src/v2/common/plugins/duplicate-image-detector.ts +++ b/packages/dotlottie-js/src/v2/common/plugins/duplicate-image-detector.ts @@ -52,7 +52,7 @@ export class DuplicateImageDetectorCommon extends DotLottiePlugin { // Now that we have a single image of the image array, compare it to every other image in the arry for (const compareImage of images) { if ( - image.image.id !== compareImage.image.id && + image.image.lottieAssetId !== compareImage.image.lottieAssetId && !image.excludeFromExport && !compareImage.excludeFromExport && image.hash && @@ -67,7 +67,9 @@ export class DuplicateImageDetectorCommon extends DotLottiePlugin { recordOfDuplicates[image.image.fileName] = [compareImage.image]; } else if (recordOfDuplicates[compareImage.image.fileName]) { // Check for duplicates, otherwise push the duplicate image - if (!recordOfDuplicates[compareImage.image.fileName]?.find((item) => item.id === image.image.id)) { + if ( + !recordOfDuplicates[compareImage.image.fileName]?.find((item) => item.id === image.image.lottieAssetId) + ) { image.excludeFromExport = true; recordOfDuplicates[compareImage.image.fileName]?.push(image.image); } @@ -139,6 +141,7 @@ export class DuplicateImageDetectorCommon extends DotLottiePlugin { clonedImages[key] = new LottieImage({ data: image.data, id: image.id, + lottieAssetId: image.lottieAssetId, fileName: image.fileName, }); } diff --git a/packages/dotlottie-js/src/v2/node/animation.ts b/packages/dotlottie-js/src/v2/node/animation.ts index 1ff593b..82cbba5 100644 --- a/packages/dotlottie-js/src/v2/node/animation.ts +++ b/packages/dotlottie-js/src/v2/node/animation.ts @@ -53,24 +53,28 @@ export class LottieAnimation extends LottieAnimationCommon { } let extType = null; - const fileType = getExtensionTypeFromBase64(asset.p); - - extType = fileType; - - const fileName = `${asset.id}.${extType}`; - - this._imageAssets.push( - new LottieImage({ - data: asset.p, - id: asset.id, - fileName, - parentAnimations: [this], - }), - ); - - asset.p = fileName; - asset.u = '/i/'; - asset.e = 0; + const fileType = await getExtensionTypeFromBase64(asset.p); + + // If we don't recognize the file type, we leave it inside the animation as is. + if (fileType) { + extType = fileType; + + const fileName = `${asset.id}.${extType}`; + + this._imageAssets.push( + new LottieImage({ + data: asset.p, + id: asset.id, + lottieAssetId: asset.id, + fileName, + parentAnimations: [this], + }), + ); + + asset.p = fileName; + asset.u = '/i/'; + asset.e = 0; + } } } diff --git a/packages/dotlottie-js/src/v2/node/dotlottie.ts b/packages/dotlottie-js/src/v2/node/dotlottie.ts index a931ab6..c382147 100644 --- a/packages/dotlottie-js/src/v2/node/dotlottie.ts +++ b/packages/dotlottie-js/src/v2/node/dotlottie.ts @@ -217,14 +217,18 @@ export class DotLottie extends DotLottieCommon { const base64 = Buffer.from(decompressedFile).toString('base64'); - const ext = getExtensionTypeFromBase64(base64); + const ext = await getExtensionTypeFromBase64(base64); + if (!ext) { + throw new DotLottieError('Unrecognized asset file format.'); + } // Push the images in to a temporary array const imgDataURL = `data:image/${ext};base64,${base64}`; tmpImages.push( new LottieImage({ id: imageId, + lottieAssetId: imageId, data: imgDataURL, fileName: key.split('/')[1] || '', }), @@ -240,7 +244,7 @@ export class DotLottie extends DotLottieCommon { const base64 = Buffer.from(decompressedFile).toString('base64'); - const ext = getExtensionTypeFromBase64(base64); + const ext = await getExtensionTypeFromBase64(base64); // Push the images in to a temporary array const audioDataURL = `data:audio/${ext};base64,${base64}`; @@ -300,7 +304,7 @@ export class DotLottie extends DotLottieCommon { if (animationAssets) { for (const asset of animationAssets) { if ('w' in asset && 'h' in asset) { - if (asset.p.includes(image.id)) { + if (asset.p === image.fileName) { image.parentAnimations.push(parentAnimation); parentAnimation.imageAssets.push(image); } diff --git a/packages/dotlottie-js/tsup.config.cjs b/packages/dotlottie-js/tsup.config.cjs index 45a5e83..167435f 100644 --- a/packages/dotlottie-js/tsup.config.cjs +++ b/packages/dotlottie-js/tsup.config.cjs @@ -31,6 +31,6 @@ export default defineConfig([ outDir: './dist', platform: 'browser', target: ['es2020'], - noExternal: ['browser-image-hash'], + noExternal: ['browser-image-hash', 'file-type'], }, ]);