feat: abandon webp

This commit is contained in:
trafficlunar 2026-03-13 22:07:10 +00:00
parent 44be36b501
commit 22fb3a2e30
18 changed files with 34 additions and 39 deletions

View file

@ -15,7 +15,7 @@ const nextConfig: NextConfig = {
pathname: "/tutorial/**", pathname: "/tutorial/**",
}, },
{ {
pathname: "/guest.webp", pathname: "/guest.png",
}, },
], ],
remotePatterns: [ remotePatterns: [

BIN
public/guest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -49,7 +49,7 @@ export async function PATCH(request: NextRequest) {
if (!image) { if (!image) {
await prisma.user.update({ await prisma.user.update({
where: { id: Number(session.user.id) }, where: { id: Number(session.user.id) },
data: { image: `/guest.webp`, imageUpdatedAt: new Date() }, data: { image: `/guest.png`, imageUpdatedAt: new Date() },
}); });
return rateLimit.sendResponse({ success: true }); return rateLimit.sendResponse({ success: true });
@ -64,10 +64,10 @@ export async function PATCH(request: NextRequest) {
try { try {
const buffer = Buffer.from(await image.arrayBuffer()); const buffer = Buffer.from(await image.arrayBuffer());
const webpBuffer = await sharp(buffer, { animated: true }).resize({ width: 128, height: 128 }).webp({ quality: 85 }).toBuffer(); const pngBuffer = await sharp(buffer, { animated: true }).resize({ width: 128, height: 128 }).png({ quality: 85 }).toBuffer();
const fileLocation = path.join(uploadsDirectory, `${session.user.id}.webp`); const fileLocation = path.join(uploadsDirectory, `${session.user.id}.png`);
await fs.writeFile(fileLocation, webpBuffer); await fs.writeFile(fileLocation, pngBuffer);
} catch (error) { } catch (error) {
console.error("Error uploading profile picture:", error); console.error("Error uploading profile picture:", error);
Sentry.captureException(error, { extra: { stage: "upload-profile-picture" } }); Sentry.captureException(error, { extra: { stage: "upload-profile-picture" } });

View file

@ -126,10 +126,10 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
await Promise.all( await Promise.all(
images.map(async (image, index) => { images.map(async (image, index) => {
const buffer = Buffer.from(await image.arrayBuffer()); const buffer = Buffer.from(await image.arrayBuffer());
const webpBuffer = await sharp(buffer).webp({ quality: 85 }).toBuffer(); const pngBuffer = await sharp(buffer).png({ quality: 85 }).toBuffer();
const fileLocation = path.join(miiUploadsDirectory, `image${index}.webp`); const fileLocation = path.join(miiUploadsDirectory, `image${index}.png`);
await fs.writeFile(fileLocation, webpBuffer); await fs.writeFile(fileLocation, pngBuffer);
}), }),
); );
} catch (error) { } catch (error) {

View file

@ -148,10 +148,10 @@ export async function POST(request: NextRequest) {
try { try {
// Compress and store // Compress and store
const studioWebpBuffer = await sharp(studioBuffer).webp({ quality: 85 }).toBuffer(); const studioPngBuffer = await sharp(studioBuffer).png({ quality: 85 }).toBuffer();
const studioFileLocation = path.join(miiUploadsDirectory, "mii.webp"); const studioFileLocation = path.join(miiUploadsDirectory, "mii.png");
await fs.writeFile(studioFileLocation, studioWebpBuffer); await fs.writeFile(studioFileLocation, studioPngBuffer);
// Generate a new QR code for aesthetic reasons // Generate a new QR code for aesthetic reasons
const byteString = String.fromCharCode(...qrBytes); const byteString = String.fromCharCode(...qrBytes);
@ -165,10 +165,10 @@ export async function POST(request: NextRequest) {
const codeBuffer = Buffer.from(codeBase64, "base64"); const codeBuffer = Buffer.from(codeBase64, "base64");
// Compress and store // Compress and store
const codeWebpBuffer = await sharp(codeBuffer).webp({ quality: 85 }).toBuffer(); const codePngBuffer = await sharp(codeBuffer).png({ quality: 85 }).toBuffer();
const codeFileLocation = path.join(miiUploadsDirectory, "qr-code.webp"); const codeFileLocation = path.join(miiUploadsDirectory, "qr-code.png");
await fs.writeFile(codeFileLocation, codeWebpBuffer); await fs.writeFile(codeFileLocation, codePngBuffer);
await generateMetadataImage(miiRecord, session.user.name!); await generateMetadataImage(miiRecord, session.user.name!);
} catch (error) { } catch (error) {
// Clean up if something went wrong // Clean up if something went wrong
@ -184,10 +184,10 @@ export async function POST(request: NextRequest) {
await Promise.all( await Promise.all(
images.map(async (image, index) => { images.map(async (image, index) => {
const buffer = Buffer.from(await image.arrayBuffer()); const buffer = Buffer.from(await image.arrayBuffer());
const webpBuffer = await sharp(buffer).webp({ quality: 85 }).toBuffer(); const pngBuffer = await sharp(buffer).png({ quality: 85 }).toBuffer();
const fileLocation = path.join(miiUploadsDirectory, `image${index}.webp`); const fileLocation = path.join(miiUploadsDirectory, `image${index}.png`);
await fs.writeFile(fileLocation, webpBuffer); await fs.writeFile(fileLocation, pngBuffer);
}), }),
); );

View file

@ -32,8 +32,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.issues[0].message }, 400); if (!searchParamsParsed.success) return rateLimit.sendResponse({ error: searchParamsParsed.error.issues[0].message }, 400);
const { type: imageType } = searchParamsParsed.data; const { type: imageType } = searchParamsParsed.data;
const fileExtension = imageType === "metadata" ? ".png" : ".webp"; const filePath = path.join(process.cwd(), "uploads", "mii", miiId.toString(), `${imageType}.png`);
const filePath = path.join(process.cwd(), "uploads", "mii", miiId.toString(), `${imageType}${fileExtension}`);
let buffer: Buffer | undefined; let buffer: Buffer | undefined;
// Only find Mii if image type is 'metadata' // Only find Mii if image type is 'metadata'
@ -109,7 +108,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
} }
return rateLimit.sendResponse(buffer, 200, { return rateLimit.sendResponse(buffer, 200, {
"Content-Type": "image/webp", "Content-Type": "image/png",
"X-Robots-Tag": "noindex, noimageindex, nofollow", "X-Robots-Tag": "noindex, noimageindex, nofollow",
"Cache-Control": "no-store", "Cache-Control": "no-store",
}); });

View file

@ -47,7 +47,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
type: "profile", type: "profile",
title: `${user.name} (@${user.username}) - TomodachiShare`, title: `${user.name} (@${user.username}) - TomodachiShare`,
description: `View ${user.name}'s profile on TomodachiShare. Creator of ${user._count.miis} Miis. Member since ${joinDate}.`, description: `View ${user.name}'s profile on TomodachiShare. Creator of ${user._count.miis} Miis. Member since ${joinDate}.`,
images: [user.image ?? "/guest.webp"], images: [user.image ?? "/guest.png"],
username: user.username, username: user.username,
firstName: user.name, firstName: user.name,
}, },
@ -55,7 +55,7 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
card: "summary", card: "summary",
title: `${user.name} (@${user.username}) - TomodachiShare`, title: `${user.name} (@${user.username}) - TomodachiShare`,
description: `View ${user.name}'s profile on TomodachiShare. Creator of ${user._count.miis} Miis. Member since ${joinDate}.`, description: `View ${user.name}'s profile on TomodachiShare. Creator of ${user._count.miis} Miis. Member since ${joinDate}.`,
images: [user.image ?? "/guest.webp"], images: [user.image ?? "/guest.png"],
creator: user.username!, creator: user.username!,
}, },
alternates: { alternates: {

View file

@ -16,7 +16,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
const userId = parsed.data; const userId = parsed.data;
const filePath = path.join(process.cwd(), "uploads", "user", `${userId}.webp`); const filePath = path.join(process.cwd(), "uploads", "user", `${userId}.png`);
try { try {
const buffer = await fs.readFile(filePath); const buffer = await fs.readFile(filePath);

View file

@ -63,7 +63,7 @@ export default function Description({ text, className }: Props) {
href={`/profile/${id}`} href={`/profile/${id}`}
className="inline-flex items-center align-bottom gap-1.5 pr-2 bg-orange-100 border border-orange-400 rounded-lg mx-1 text-orange-800 text-xs" className="inline-flex items-center align-bottom gap-1.5 pr-2 bg-orange-100 border border-orange-400 rounded-lg mx-1 text-orange-800 text-xs"
> >
<ProfilePicture src={linkedProfile.image || "/guest.webp"} width={24} height={24} className="bg-white rounded-lg border-r border-orange-400" /> <ProfilePicture src={linkedProfile.image || "/guest.png"} width={24} height={24} className="bg-white rounded-lg border-r border-orange-400" />
{linkedProfile.name} {linkedProfile.name}
</Link> </Link>
); );

View file

@ -22,7 +22,7 @@ export default function Dropzone({ onDrop, options, children }: Props) {
onDrop: handleDrop, onDrop: handleDrop,
maxFiles: 3, maxFiles: 3,
accept: { accept: {
"image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp", ".heic"], "image/*": [".png", ".jpg", ".jpeg", ".bmp", ".png", ".heic"],
}, },
...options, ...options,
}); });

View file

@ -30,7 +30,7 @@ export default async function ProfileInformation({ userId, page }: Props) {
<div className="flex w-full gap-4 overflow-x-scroll"> <div className="flex w-full gap-4 overflow-x-scroll">
{/* Profile picture */} {/* Profile picture */}
<Link href={`/profile/${user.id}`} className="size-28 aspect-square"> <Link href={`/profile/${user.id}`} className="size-28 aspect-square">
<ProfilePicture src={user.image ?? "/guest.webp"} className="rounded-full bg-white border-2 border-orange-400 shadow max-md:self-center" /> <ProfilePicture src={user.image ?? "/guest.png"} className="rounded-full bg-white border-2 border-orange-400 shadow max-md:self-center" />
</Link> </Link>
{/* User information */} {/* User information */}
<div className="flex flex-col w-full relative py-3"> <div className="flex flex-col w-full relative py-3">

View file

@ -9,7 +9,7 @@ export default async function ProfileOverview() {
<li title="Your profile"> <li title="Your profile">
<Link href={`/profile/${session?.user.id}`} aria-label="Go to profile" className="pill button gap-2! p-0! h-full max-w-64" data-tooltip="Your Profile"> <Link href={`/profile/${session?.user.id}`} aria-label="Go to profile" className="pill button gap-2! p-0! h-full max-w-64" data-tooltip="Your Profile">
<Image <Image
src={session?.user?.image ?? "/guest.webp"} src={session?.user?.image ?? "/guest.png"}
alt="profile picture" alt="profile picture"
width={40} width={40}
height={40} height={40}

View file

@ -7,5 +7,5 @@ export default function ProfilePicture(props: Partial<ImageProps>) {
const { src, ...rest } = props; const { src, ...rest } = props;
const [imgSrc, setImgSrc] = useState(src); const [imgSrc, setImgSrc] = useState(src);
return <Image width={128} height={128} {...rest} src={imgSrc || "/guest.webp"} alt={"profile picture"} onError={() => setImgSrc("/guest.webp")} />; return <Image width={128} height={128} {...rest} src={imgSrc || "/guest.png"} alt={"profile picture"} onError={() => setImgSrc("/guest.png")} />;
} }

View file

@ -59,7 +59,7 @@ export default function ProfilePictureSettings() {
</p> </p>
<Image <Image
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.webp"} src={newPicture ? URL.createObjectURL(newPicture) : "/guest.png"}
alt="new profile picture" alt="new profile picture"
width={96} width={96}
height={96} height={96}
@ -93,7 +93,7 @@ export default function ProfilePictureSettings() {
<div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1 flex items-center"> <div className="bg-orange-100 rounded-xl border-2 border-amber-500 mt-4 px-2 py-1 flex items-center">
<p className="font-semibold mb-2">New profile picture:</p> <p className="font-semibold mb-2">New profile picture:</p>
<Image <Image
src={newPicture ? URL.createObjectURL(newPicture) : "/guest.webp"} src={newPicture ? URL.createObjectURL(newPicture) : "/guest.png"}
alt="new profile picture" alt="new profile picture"
width={128} width={128}
height={128} height={128}

View file

@ -43,7 +43,7 @@ export default function ReportUserForm({ user }: Props) {
<hr className="border-zinc-300" /> <hr className="border-zinc-300" />
<div className="bg-orange-100 rounded-xl border-2 border-orange-400 flex p-4 gap-4"> <div className="bg-orange-100 rounded-xl border-2 border-orange-400 flex p-4 gap-4">
<ProfilePicture src={user.image ?? "/guest.webp"} width={96} height={96} className="aspect-square rounded-full border-2 border-orange-400" /> <ProfilePicture src={user.image ?? "/guest.png"} width={96} height={96} className="aspect-square rounded-full border-2 border-orange-400" />
<div className="flex flex-col justify-center"> <div className="flex flex-col justify-center">
<p className="text-xl font-bold overflow-hidden text-ellipsis">{user.name}</p> <p className="text-xl font-bold overflow-hidden text-ellipsis">{user.name}</p>
<p className="text-sm font-bold overflow-hidden text-ellipsis">@{user.username}</p> <p className="text-sm font-bold overflow-hidden text-ellipsis">@{user.username}</p>

View file

@ -90,7 +90,7 @@ export default function EditForm({ mii, likes }: Props) {
const response = await fetch(path); const response = await fetch(path);
const blob = await response.blob(); const blob = await response.blob();
return Object.assign(new File([blob], `image${index}.webp`, { type: "image/webp" }), { path }); return Object.assign(new File([blob], `image${index}.png`, { type: "image/png" }), { path });
}), }),
); );

View file

@ -130,16 +130,14 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise<{
// Load assets concurrently // Load assets concurrently
const [miiImage, qrCodeImage, fonts] = await Promise.all([ const [miiImage, qrCodeImage, fonts] = await Promise.all([
// Read and convert the .webp images to .png (because satori doesn't support it) // Read and convert the images to data URI
fs.readFile(path.join(miiUploadsDirectory, "mii.webp")).then((buffer) => fs.readFile(path.join(miiUploadsDirectory, "mii.png")).then((buffer) =>
sharp(buffer) sharp(buffer)
.png()
.toBuffer() .toBuffer()
.then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`), .then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`),
), ),
fs.readFile(path.join(miiUploadsDirectory, "qr-code.webp")).then((buffer) => fs.readFile(path.join(miiUploadsDirectory, "qr-code.png")).then((buffer) =>
sharp(buffer) sharp(buffer)
.png()
.toBuffer() .toBuffer()
.then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`), .then((pngBuffer) => `data:image/png;base64,${pngBuffer.toString("base64")}`),
), ),
@ -209,8 +207,6 @@ export async function generateMetadataImage(mii: Mii, author: string): Promise<{
// Store the file // Store the file
try { try {
// I tried using .webp here but the quality looked awful
// but it actually might be well-liked due to the hatred of .webp
const fileLocation = path.join(miiUploadsDirectory, "metadata.png"); const fileLocation = path.join(miiUploadsDirectory, "metadata.png");
await fs.writeFile(fileLocation, buffer); await fs.writeFile(fileLocation, buffer);
} catch (error) { } catch (error) {