Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,8 @@ export const downloadRemoteAssetHandler: ApiHandler<
assetType:
fileType.type === 'gif'
? 'gif'
: fileType.type === 'apng'
: fileType.type === 'apng' ||
(fileType.type === 'webp' && fileType.animated)
? 'animated-image'
: 'image',
src: assetPath,
Expand Down
33 changes: 31 additions & 2 deletions packages/studio-shared/src/detect-file-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,13 +287,41 @@ const getWebPDimensions = (bytes: Uint8Array): FileDimensions | null => {
return null;
};

const isAnimatedWebp = (data: Uint8Array): boolean => {
const animationChunk = new Uint8Array([0x41, 0x4e, 0x49, 0x4d]);
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
let offset = 12;

while (offset + 8 <= data.length) {
const chunkType = data.subarray(offset, offset + 4);
if (matchesPattern(animationChunk)(chunkType)) {
return true;
}

const chunkLength = view.getUint32(offset + 4, true);
const nextOffset = offset + 8 + chunkLength + (chunkLength % 2);
if (nextOffset <= offset) {
return false;
}

offset = nextOffset;
}

return false;
};

const isWebp = (data: Uint8Array): WebpType | null => {
const webpPattern = new Uint8Array([0x52, 0x49, 0x46, 0x46]);
const riffPattern = new Uint8Array([0x52, 0x49, 0x46, 0x46]);
const webpPattern = new Uint8Array([0x57, 0x45, 0x42, 0x50]);

if (matchesPattern(webpPattern)(data.subarray(0, 4))) {
if (
matchesPattern(riffPattern)(data.subarray(0, 4)) &&
matchesPattern(webpPattern)(data.subarray(8, 12))
) {
return {
type: 'webp',
dimensions: getWebPDimensions(data),
animated: isAnimatedWebp(data),
};
}

Expand Down Expand Up @@ -393,6 +421,7 @@ export type JpegType = {
export type WebpType = {
type: 'webp';
dimensions: FileDimensions | null;
animated: boolean;
};

export type BmpType = {
Expand Down
35 changes: 35 additions & 0 deletions packages/studio-shared/src/test/detect-file-type.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,41 @@ test('detects animated PNG dimensions', () => {
});
});

test('detects animated WebP files', () => {
const webp = new Uint8Array(44);
webp.set([0x52, 0x49, 0x46, 0x46, 36, 0, 0, 0], 0);
webp.set([0x57, 0x45, 0x42, 0x50], 8);
webp.set([0x56, 0x50, 0x38, 0x58, 10, 0, 0, 0], 12);
webp.set([0x02, 0, 0, 0, 0x3f, 0x01, 0, 0xb3, 0, 0], 20);
webp.set([0x41, 0x4e, 0x49, 0x4d, 6, 0, 0, 0], 30);

expect(detectFileType(webp)).toEqual({
type: 'webp',
animated: true,
dimensions: {
width: 320,
height: 180,
},
});
});

test('detects static WebP files', () => {
const webp = new Uint8Array(30);
webp.set([0x52, 0x49, 0x46, 0x46, 22, 0, 0, 0], 0);
webp.set([0x57, 0x45, 0x42, 0x50], 8);
webp.set([0x56, 0x50, 0x38, 0x58, 10, 0, 0, 0], 12);
webp.set([0, 0, 0, 0, 0x3f, 0x01, 0, 0xb3, 0, 0], 20);

expect(detectFileType(webp)).toEqual({
type: 'webp',
animated: false,
dimensions: {
width: 320,
height: 180,
},
});
});

test('detects GIF dimensions', () => {
const gif = new Uint8Array([
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x20, 0x03, 0x58, 0x02,
Expand Down
13 changes: 12 additions & 1 deletion packages/studio/src/components/import-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ export const getAssetElement = ({
fileType: FileType;
src: string;
}): InsertableAssetElement | null => {
if (fileType.type === 'webp' && fileType.animated) {
return {
type: 'asset',
assetType: 'animated-image',
src,
srcType: 'static',
dimensions: fileType.dimensions,
position: null,
};
}

if (
fileType.type === 'png' ||
fileType.type === 'jpeg' ||
Expand Down Expand Up @@ -435,7 +446,7 @@ const getStaticAssetFileType = async (
assetPath: string,
): Promise<FileType | null> => {
const extension = assetPath.split('.').pop()?.toLowerCase();
if (extension !== 'png' && extension !== 'apng') {
if (extension !== 'png' && extension !== 'apng' && extension !== 'webp') {
return null;
}

Expand Down
40 changes: 40 additions & 0 deletions packages/studio/src/test/import-assets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,46 @@ test('maps animated PNG file types to AnimatedImage assets', () => {
});
});

test('maps animated WebP file types to AnimatedImage assets', () => {
expect(
getAssetElement({
fileType: {
type: 'webp',
animated: true,
dimensions: {width: 480, height: 290},
},
src: 'animated.webp',
}),
).toEqual({
type: 'asset',
assetType: 'animated-image',
src: 'animated.webp',
srcType: 'static',
dimensions: {width: 480, height: 290},
position: null,
});
});

test('maps static WebP file types to Img assets', () => {
expect(
getAssetElement({
fileType: {
type: 'webp',
animated: false,
dimensions: {width: 480, height: 290},
},
src: 'static.webp',
}),
).toEqual({
type: 'asset',
assetType: 'image',
src: 'static.webp',
srcType: 'static',
dimensions: {width: 480, height: 290},
position: null,
});
});

test('maps existing static file paths to insertable assets', () => {
expect(getAssetElementFromPath('nested/photo.JPG')).toEqual({
type: 'asset',
Expand Down
Loading