Skip to content

Commit a5fe101

Browse files
fix: Banner uploader fix error handling and reduce image size jump (calcom#21903)
* improve uploader * better error handling * unnecessary check * more adjustments * remove logs
1 parent 7075384 commit a5fe101

5 files changed

Lines changed: 35 additions & 5 deletions

File tree

apps/web/public/static/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3253,5 +3253,6 @@
32533253
"booker_limit_exceeded_error_reschedule": "You already have a booking for this event on {{date}}. Would you like to reschedule to the new selected time?",
32543254
"duration_min_error": "Duration must be at least {{min}} minute(s)",
32553255
"duration_max_error": "Duration cannot exceed {{max}} minutes (24 hours)",
3256+
"converted_image_size_limit_exceed": "Image size limit exceeded, please use a smaller image preferably in JPEG format",
32563257
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
32573258
}

packages/features/ee/organizations/pages/settings/profile.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,11 @@ const OrgProfileForm = ({ defaultValues }: { defaultValues: FormValues }) => {
183183

184184
const mutation = trpc.viewer.organizations.update.useMutation({
185185
onError: (err) => {
186+
// Handle JSON parsing errors from body size limit exceeded
187+
if (err.message.includes("Unexpected token") && err.message.includes("Body excee")) {
188+
showToast(t("converted_image_size_limit_exceed"), "error");
189+
return;
190+
}
186191
showToast(err.message, "error");
187192
},
188193
onSuccess: async (res) => {

packages/trpc/server/routers/viewer/organizations/update.handler.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,12 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
161161
metadata: mergeMetadata({ ...input.metadata }),
162162
};
163163

164-
if (input.banner && input.banner.startsWith("data:image/png;base64,")) {
164+
if (
165+
input.banner &&
166+
(input.banner.startsWith("data:image/png;base64,") ||
167+
input.banner.startsWith("data:image/jpeg;base64,") ||
168+
input.banner.startsWith("data:image/jpg;base64,"))
169+
) {
165170
const banner = await resizeBase64Image(input.banner, { maxSize: 1500 });
166171
data.bannerUrl = await uploadLogo({
167172
logo: banner,
@@ -172,7 +177,12 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
172177
data.bannerUrl = null;
173178
}
174179

175-
if (input.logoUrl && input.logoUrl.startsWith("data:image/png;base64,")) {
180+
if (
181+
input.logoUrl &&
182+
(input.logoUrl.startsWith("data:image/png;base64,") ||
183+
input.logoUrl.startsWith("data:image/jpeg;base64,") ||
184+
input.logoUrl.startsWith("data:image/jpg;base64,"))
185+
) {
176186
data.logoUrl = await uploadLogo({
177187
logo: await resizeBase64Image(input.logoUrl),
178188
teamId: currentOrgId,

packages/ui/components/image-uploader/BannerUploader.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,12 @@ async function getCroppedImg(
209209
const ctx = canvas.getContext("2d");
210210
if (!ctx) throw new Error("Context is null, this should never happen.");
211211

212+
// Detect original image format from data URL
213+
const originalFormat =
214+
imageSrc.startsWith("data:image/jpeg") || imageSrc.startsWith("data:image/jpg")
215+
? "image/jpeg"
216+
: "image/png";
217+
212218
canvas.width = width;
213219
canvas.height = height;
214220

@@ -224,5 +230,6 @@ async function getCroppedImg(
224230
canvas.height
225231
);
226232

227-
return canvas.toDataURL("image/png");
233+
// Use original format with quality setting for JPEG
234+
return canvas.toDataURL(originalFormat, originalFormat === "image/jpeg" ? 0.6 : undefined);
228235
}

packages/ui/components/image-uploader/ImageUploader.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,12 @@ async function getCroppedImg(imageSrc: string, pixelCrop: Area): Promise<string>
192192
const ctx = canvas.getContext("2d");
193193
if (!ctx) throw new Error("Context is null, this should never happen.");
194194

195+
// Detect original image format from data URL
196+
const originalFormat =
197+
imageSrc.startsWith("data:image/jpeg") || imageSrc.startsWith("data:image/jpg")
198+
? "image/jpeg"
199+
: "image/png";
200+
195201
const maxSize = Math.max(image.naturalWidth, image.naturalHeight);
196202
const resizeRatio = MAX_IMAGE_SIZE / maxSize < 1 ? Math.max(MAX_IMAGE_SIZE / maxSize, 0.75) : 1;
197203

@@ -216,13 +222,14 @@ async function getCroppedImg(imageSrc: string, pixelCrop: Area): Promise<string>
216222
// on very low ratios, the quality of the resize becomes awful. For this reason the resizeRatio is limited to 0.75
217223
if (resizeRatio <= 0.75) {
218224
// With a smaller image, thus improved ratio. Keep doing this until the resizeRatio > 0.75.
219-
return getCroppedImg(canvas.toDataURL("image/png"), {
225+
return getCroppedImg(canvas.toDataURL(originalFormat), {
220226
width: canvas.width,
221227
height: canvas.height,
222228
x: 0,
223229
y: 0,
224230
});
225231
}
226232

227-
return canvas.toDataURL("image/png");
233+
// Use original format with quality setting for JPEG
234+
return canvas.toDataURL(originalFormat, originalFormat === "image/jpeg" ? 0.9 : undefined);
228235
}

0 commit comments

Comments
 (0)