Skip to content

Commit

Permalink
fix: webp detection (#63)
Browse files Browse the repository at this point in the history
* fix: webp detection

* fix: added check for full webp signature, added error throw if mimetype not found

* fix: added more tests

* fix: added txt module

* fix: fixed unimported module
  • Loading branch information
samuelOsborne authored Feb 20, 2024
1 parent 4a5a5f6 commit 94a9a93
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 78 deletions.
5 changes: 5 additions & 0 deletions .changeset/friendly-keys-brush.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dotlottie/dotlottie-js": patch
---

fixes webp detection
137 changes: 80 additions & 57 deletions packages/dotlottie-js/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export const MIME_TYPES: MimeTypes = {
gif: 'image/gif',
bmp: 'image/bmp',
svg: 'image/svg+xml',
svgxml: 'image/svg+xml',
webp: 'image/webp',
mpeg: 'audio/mpeg',
mp3: 'audio/mp3',
};

Expand All @@ -38,10 +38,12 @@ export const MIME_CODES: MimeCodes = {
png: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
gif: [0x47, 0x49, 0x46],
bmp: [0x42, 0x4d],
webp: [0x52, 0x49, 0x46, 0x46, 0x57, 0x45, 0x42, 0x50],
svg: [0x3c, 0x3f, 0x78],
mp3: [0x49, 0x44, 0x33, 0x3, 0x00, 0x00, 0x00, 0x00],
mpeg: [0x49, 0x44, 0x33, 0x3, 0x00, 0x00, 0x00, 0x00],
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 {
Expand All @@ -59,6 +61,49 @@ export const MIME_TO_EXTENSION: MimeToExtension = {
'audio/mp3': 'mp3',
};

export enum ErrorCodes {
ASSET_NOT_FOUND = 'ASSET_NOT_FOUND',
INVALID_DOTLOTTIE = 'INVALID_DOTLOTTIE',
INVALID_STATEMACHINE = 'INVALID_STATEMACHINE',
INVALID_URL = 'INVALID_URL',
}

export class DotLottieError extends Error {
public code: ErrorCodes | undefined;

public constructor(message: string, code?: ErrorCodes) {
super(message);
this.name = '[dotlottie-js]';
this.code = code;
}
}

/**
* Creates an Error object with the specified message.
*
* @remarks
* This function accepts a message string and constructs a new Error object prefixed with "[dotlottie-js]: ".
*
* @deprecated
* This function has been deprecated in favor of using the {@link DotLottieError} class directly.
*
* @param message - The error message to include in the Error object.
* @returns An Error object with the specified message, prefixed with "[dotlottie-js]: ".
*
* @example
* ```typescript
* const message = 'DotLottie not found';
* const error = createError(message);
* ```
*
* @public
*/
export const createError = (message: string): Error => {
const error = new Error(`[dotlottie-js]: ${message}`);

return error;
};

/**
* Converts a base64 string into a Uint8Array.
*
Expand Down Expand Up @@ -107,11 +152,16 @@ export const base64ToUint8Array = (base64String: string): Uint8Array => {
*
* @public
*/
export const getMimeTypeFromBase64 = (base64: string): string | null | undefined => {
export const getMimeTypeFromBase64 = (base64: string): string | undefined => {
let data: string | null = null;
let bytes: number[] = [];

if (!base64) return null;
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,
);
}

const withoutMeta = base64.substring(base64.indexOf(',') + 1);

Expand All @@ -127,16 +177,32 @@ export const getMimeTypeFromBase64 = (base64: string): string | null | undefined
bufData[i] = data.charCodeAt(i);
}

bytes = Array.from(bufData.subarray(0, 8));
for (const mimeType in MIME_CODES) {
const dataArr = MIME_CODES[mimeType];

if (dataArr && bytes.every((byte, index) => byte === dataArr[index])) {
return MIME_TYPES[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];
}
}
}

return null;
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,
);
};

/**
Expand All @@ -163,56 +229,13 @@ export const getExtensionTypeFromBase64 = (base64: string): string | null => {
const ext = base64.split(';')[0]?.split('/')[1];

if (ext) {
return MIME_TO_EXTENSION[ext] || 'png';
return MIME_TO_EXTENSION[ext] || null;
}

return 'png';
}

return MIME_TO_EXTENSION[mimeType] || 'png';
};

export enum ErrorCodes {
ASSET_NOT_FOUND = 'ASSET_NOT_FOUND',
INVALID_DOTLOTTIE = 'INVALID_DOTLOTTIE',
INVALID_STATEMACHINE = 'INVALID_STATEMACHINE',
INVALID_URL = 'INVALID_URL',
}

export class DotLottieError extends Error {
public code: ErrorCodes | undefined;

public constructor(message: string, code?: ErrorCodes) {
super(message);
this.name = '[dotlottie-js]';
this.code = code;
return null;
}
}

/**
* Creates an Error object with the specified message.
*
* @remarks
* This function accepts a message string and constructs a new Error object prefixed with "[dotlottie-js]: ".
*
* @deprecated
* This function has been deprecated in favor of using the {@link DotLottieError} class directly.
*
* @param message - The error message to include in the Error object.
* @returns An Error object with the specified message, prefixed with "[dotlottie-js]: ".
*
* @example
* ```typescript
* const message = 'DotLottie not found';
* const error = createError(message);
* ```
*
* @public
*/
export const createError = (message: string): Error => {
const error = new Error(`[dotlottie-js]: ${message}`);

return error;
return MIME_TO_EXTENSION[mimeType] || null;
};

/**
Expand Down
9 changes: 5 additions & 4 deletions packages/dotlottie-js/src/dotlottie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,16 +359,17 @@ export class DotLottie extends DotLottieCommon {
}
}
} catch (err) {
// throw error as it's invalid json
throw createError('Invalid manifest inside buffer!');
if (err instanceof Error) {
throw new DotLottieError(`Invalid manifest inside buffer! ${err.message}`);
}
}
} else {
// throw error as it's invalid buffer
throw createError('Invalid buffer');
throw new DotLottieError('Invalid buffer');
}
} catch (err) {
if (err instanceof Error) {
throw createError(err.message);
throw new DotLottieError(err.message);
}
}

Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Binary file not shown.
2 changes: 1 addition & 1 deletion packages/dotlottie-js/src/tests/dotlottie-js-node.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,7 @@ describe('DotLottie', () => {
unzippedBigMergedDotLottie[`images/${animation1?.imageAssets[0]?.fileName}`] as Uint8Array,
).toString('base64');

const expectedDataURL = `data:image/png;base64,${base64}`;
const expectedDataURL = `data:image/jpeg;base64,${base64}`;

expect(await animation1?.imageAssets[0]?.toDataURL()).toEqual(expectedDataURL);
});
Expand Down
86 changes: 78 additions & 8 deletions packages/dotlottie-js/src/tests/lottie-image-browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import type { Animation as AnimationType } from '@lottiefiles/lottie-types';

import { DotLottie, LottieImage } from '..';
import { DotLottie, LottieImage, getMimeTypeFromBase64 } from '..';

import BULL_DATA from './__fixtures__/image-asset-optimization/bull.json';
import IMAGE_ANIMATION_1_DATA from './__fixtures__/image-asset-optimization/image-animation-layer-1.json';
Expand All @@ -14,6 +14,9 @@ import IMAGE_ANIMATION_3_DATA from './__fixtures__/image-asset-optimization/imag
import IMAGE_ANIMATION_2_DATA from './__fixtures__/image-asset-optimization/image-animation-layer-2.json';
import DUPES_DATA from './__fixtures__/image-asset-optimization/lots-of-dupes.json';
import SIMPLE_IMAGE_ANIMATION from './__fixtures__/image-asset-optimization/simple-image-animation.json';
import AUDIO_TEST from './__fixtures__/mimetype-tests/mp-3-test.txt';
import SVG_XML_TEST from './__fixtures__/mimetype-tests/svg-xml-test.txt';
import VIDEO_DOTLOTTIE from './__fixtures__/simple/video-embedded.lottie';

describe('LottieImage', () => {
it('gets and sets the zipOptions', () => {
Expand Down Expand Up @@ -136,8 +139,8 @@ describe('LottieImage', () => {
expect(uniqueImages.length).toBe(4);

expect(uniqueImages.map((image) => image.fileName)).toEqual([
'image_0.png',
'image_1.png',
'image_0.jpeg',
'image_1.jpeg',
'image_3.png',
'image_4.png',
]);
Expand All @@ -163,9 +166,9 @@ describe('LottieImage', () => {
expect(uniqueImages.length).toBe(5);

expect(uniqueImages.map((image) => image.fileName)).toEqual([
'image_0.png',
'image_1.png',
'image_2.png',
'image_0.jpeg',
'image_1.jpeg',
'image_2.jpeg',
'image_3.png',
'image_4.png',
]);
Expand Down Expand Up @@ -211,13 +214,80 @@ describe('LottieImage', () => {
expect(uniqueImages.length).toBe(5);

expect(uniqueImages.map((image) => image.fileName)).toEqual([
'image_1.png',
'image_2.png',
'image_1.jpeg',
'image_2.jpeg',
'image_4.png',
'image_5.png',
'image_9.png',
]);
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(
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAZABkAAD/2wCEABQQEBkSGScXFycyJh8mMi4mJiYmLj41NTU1NT5EQUFBQUFBREREREREREREREREREREREREREREREREREREREQBFRkZIBwgJhgYJjYmICY2RDYrKzZERERCNUJERERERERERERERERERERERERERERERERERERERERERERERERERP/AABEIAAEAAQMBIgACEQEDEQH/xABMAAEBAAAAAAAAAAAAAAAAAAAABQEBAQAAAAAAAAAAAAAAAAAABQYQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AJQA9Yv/2Q==',
);

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

const pngFormat = 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=');

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

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

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

const webpFormat = 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(
// eslint-disable-next-line no-secrets/no-secrets
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InJlZCIvPjwvc3ZnPg==',
);

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

const svgXmlFormat = getMimeTypeFromBase64(SVG_XML_TEST);

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

const mp3Format = getMimeTypeFromBase64(AUDIO_TEST);

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

it('Throws an error when an unrecognized file mimetype is detected.', async () => {
try {
let videoDotLottie = new DotLottie();

videoDotLottie = await videoDotLottie.fromArrayBuffer(VIDEO_DOTLOTTIE);

const videoAnimation = videoDotLottie.getAnimations();

if (videoAnimation) {
videoAnimation.map(async (animation) => {
await animation[1].toJSON({
inlineAssets: true,
});
});
}
} catch (error) {
expect(error).toBeInstanceOf(Error);
}
});
});
Loading

0 comments on commit 94a9a93

Please sign in to comment.