fix: convert SVG avatar into PNG on save and serve (#20654)
This commit is contained in:
1 parent
2ab34004a2
commit
821bd37b37
6 files changed
+61
-9
No files matched your search
@@ -5,6 +5,7 @@ import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AVATAR_FALLBACK, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { convertSvgToPng } from "@calcom/lib/server/imageUtils";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const querySchema = z.object({
|
||||
@@ -44,7 +45,19 @@ async function handler(req: NextRequest, { params }: { params: Promise<Params> }
|
||||
data: true,
|
||||
},
|
||||
});
|
||||
img = data;
|
||||
|
||||
// Convert SVG to PNG if needed and update the database
|
||||
if (data.startsWith("data:image/svg+xml;base64,")) {
|
||||
const pngData = await convertSvgToPng(data);
|
||||
|
||||
await prisma.avatar.update({
|
||||
where: { objectKey },
|
||||
data: { data: pngData },
|
||||
});
|
||||
img = pngData;
|
||||
} else {
|
||||
img = data;
|
||||
}
|
||||
} catch (e) {
|
||||
// If anything goes wrong or avatar is not found, use default avatar
|
||||
const url = new URL(AVATAR_FALLBACK, WEBAPP_URL).toString();
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
"rrule": "^2.7.1",
|
||||
"sanitize-html": "^2.10.0",
|
||||
"schema-dts": "^1.1.0",
|
||||
"sharp": "0.33.5",
|
||||
"short-uuid": "^4.2.0",
|
||||
"sonner": "^1.7.4",
|
||||
"stripe": "^9.16.0",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"next-collect": "^0.2.1",
|
||||
"next-i18next": "^15.4.2",
|
||||
"rrule": "^2.7.1",
|
||||
"sharp": "0.33.5",
|
||||
"sonner": "^1.7.4",
|
||||
"tsdav": "2.0.3",
|
||||
"tslog": "^4.9.2",
|
||||
|
||||
@@ -2,8 +2,11 @@ import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { convertSvgToPng } from "./imageUtils";
|
||||
|
||||
export const uploadAvatar = async ({ userId, avatar: data }: { userId: number; avatar: string }) => {
|
||||
const objectKey = uuidv4();
|
||||
const processedData = await convertSvgToPng(data);
|
||||
|
||||
await prisma.avatar.upsert({
|
||||
where: {
|
||||
@@ -15,12 +18,12 @@ export const uploadAvatar = async ({ userId, avatar: data }: { userId: number; a
|
||||
},
|
||||
create: {
|
||||
userId: userId,
|
||||
data,
|
||||
data: processedData,
|
||||
objectKey,
|
||||
isBanner: false,
|
||||
},
|
||||
update: {
|
||||
data,
|
||||
data: processedData,
|
||||
objectKey,
|
||||
},
|
||||
});
|
||||
@@ -38,6 +41,7 @@ export const uploadLogo = async ({
|
||||
isBanner?: boolean;
|
||||
}): Promise<string> => {
|
||||
const objectKey = uuidv4();
|
||||
const processedData = await convertSvgToPng(data);
|
||||
|
||||
await prisma.avatar.upsert({
|
||||
where: {
|
||||
@@ -49,12 +53,12 @@ export const uploadLogo = async ({
|
||||
},
|
||||
create: {
|
||||
teamId,
|
||||
data,
|
||||
data: processedData,
|
||||
objectKey,
|
||||
isBanner,
|
||||
},
|
||||
update: {
|
||||
data,
|
||||
data: processedData,
|
||||
objectKey,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import sharp from "sharp";
|
||||
|
||||
// Maximum allowed size for SVG data (5MB)
|
||||
const MAX_SVG_SIZE = 5 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Converts an SVG image to PNG format
|
||||
* @param data Base64 encoded image data
|
||||
* @returns Base64 encoded PNG data or a placeholder image if conversion fails
|
||||
*/
|
||||
export const convertSvgToPng = async (data: string) => {
|
||||
if (data.startsWith("data:image/svg+xml;base64,")) {
|
||||
try {
|
||||
const base64Data = data.replace(/^data:image\/svg\+xml;base64,/, "");
|
||||
const buffer = Buffer.from(base64Data, "base64");
|
||||
|
||||
// Check if the SVG data exceeds the size limit
|
||||
if (buffer.length > MAX_SVG_SIZE) {
|
||||
throw new Error("SVG data exceeds maximum allowed size");
|
||||
}
|
||||
|
||||
const pngBuffer = await sharp(buffer).png().toBuffer();
|
||||
return `data:image/png;base64,${pngBuffer.toString("base64")}`;
|
||||
} catch (error) {
|
||||
console.error("Error converting SVG to PNG", error);
|
||||
// Return a 1x1 transparent PNG as placeholder
|
||||
return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
@@ -3165,6 +3165,7 @@ __metadata:
|
||||
next-collect: ^0.2.1
|
||||
next-i18next: ^15.4.2
|
||||
rrule: ^2.7.1
|
||||
sharp: 0.33.5
|
||||
sonner: ^1.7.4
|
||||
tsdav: 2.0.3
|
||||
tslog: ^4.9.2
|
||||
@@ -3951,6 +3952,7 @@ __metadata:
|
||||
rrule: ^2.7.1
|
||||
sanitize-html: ^2.10.0
|
||||
schema-dts: ^1.1.0
|
||||
sharp: 0.33.5
|
||||
short-uuid: ^4.2.0
|
||||
sonner: ^1.7.4
|
||||
stripe: ^9.16.0
|
||||
@@ -4480,11 +4482,11 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/runtime@npm:^1.2.0":
|
||||
version: 1.3.1
|
||||
resolution: "@emnapi/runtime@npm:1.3.1"
|
||||
version: 1.4.0
|
||||
resolution: "@emnapi/runtime@npm:1.4.0"
|
||||
dependencies:
|
||||
tslib: ^2.4.0
|
||||
checksum: 9a16ae7905a9c0e8956cf1854ef74e5087fbf36739abdba7aa6b308485aafdc993da07c19d7af104cd5f8e425121120852851bb3a0f78e2160e420a36d47f42f
|
||||
checksum: 90cca909ceb842e95cb5e16ab64e4c4d8925c6f4ed5527848e53faad8c4e23885e4a001a6099e26a76d099a67758032a9f208b13252176e2b68c860d19f5c1f9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -41502,7 +41504,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sharp@npm:^0.33.5":
|
||||
"sharp@npm:0.33.5, sharp@npm:^0.33.5":
|
||||
version: 0.33.5
|
||||
resolution: "sharp@npm:0.33.5"
|
||||
dependencies:
|
||||
|
||||
Reference in new issue
Block a user