Skip to content
Open
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
90 changes: 51 additions & 39 deletions src/app/(tools)/rounded-border/rounded-tool.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";
import { usePlausible } from "next-plausible";
import { useEffect, useMemo, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { UploadBox } from "@/components/shared/upload-box";
import { OptionSelector } from "@/components/shared/option-selector";
Expand All @@ -17,36 +17,51 @@ type BackgroundOption = "white" | "black" | "transparent";

function useImageConverter(props: {
canvas: HTMLCanvasElement | null;
imageContent: string;
imageBlobUrl: string;
radius: Radius;
background: BackgroundOption;
fileName?: string;
imageMetadata: { width: number; height: number; name: string };
}) {
const { width, height } = useMemo(() => {
return {
width: props.imageMetadata.width,
height: props.imageMetadata.height,
};
}, [props.imageMetadata]);
const { width, height } = props.imageMetadata;

// Store blob URL to clean it up later
const [usedCanvasBlobUrl, setUsedCanvasBlobUrl] = useState<string | null>(
null,
);
useEffect(
() => () => {
if (usedCanvasBlobUrl) URL.revokeObjectURL(usedCanvasBlobUrl);
},
[usedCanvasBlobUrl],
);

const convertToPng = async () => {
const ctx = props.canvas?.getContext("2d");
if (!ctx) throw new Error("Failed to get canvas context");

const saveImage = () => {
if (props.canvas) {
const dataURL = props.canvas.toDataURL("image/png");
const link = document.createElement("a");
link.href = dataURL;
const imageFileName = props.imageMetadata.name ?? "image_converted";
link.download = `${imageFileName.replace(/\..+$/, "")}.png`;
link.click();
}
const saveImage = async () => {
const canvasBlob = await new Promise<Blob>((resolve, reject) => {
if (props.canvas) {
props.canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else reject(new Error("Canvas blob could not be created"));
});
} else reject(new Error("Canvas not present"));
});
const canvasBlobUrl = URL.createObjectURL(canvasBlob);
setUsedCanvasBlobUrl(canvasBlobUrl);
const link = document.createElement("a");
link.href = canvasBlobUrl;
const imageFileName = props.imageMetadata.name ?? "image_converted";
link.download = `${imageFileName.replace(/\..+$/, "")}.png`;
link.click();
};

const img = new Image();
img.onload = () => {
ctx.save();
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = props.background;
ctx.fillRect(0, 0, width, height);
Expand All @@ -63,10 +78,11 @@ function useImageConverter(props: {
ctx.closePath();
ctx.clip();
ctx.drawImage(img, 0, 0, width, height);
saveImage();
ctx.restore();
void saveImage();
};

img.src = props.imageContent;
img.src = props.imageBlobUrl;
};

return {
Expand All @@ -76,58 +92,54 @@ function useImageConverter(props: {
}

interface ImageRendererProps {
imageContent: string;
imageBlobUrl: string;
radius: Radius;
background: BackgroundOption;
}

const ImageRenderer = ({
imageContent,
imageBlobUrl,
radius,
background,
}: ImageRendererProps) => {
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (containerRef.current) {
const imgElement = containerRef.current.querySelector("img");
if (imgElement) {
imgElement.style.borderRadius = `${radius}px`;
}
}
}, [imageContent, radius]);

return (
<div ref={containerRef} className="relative w-[500px]">
<div
className="absolute inset-0"
style={{ backgroundColor: background, borderRadius: 0 }}
/>
<img
src={imageContent}
src={imageBlobUrl}
alt="Preview"
className="relative rounded-lg"
style={{ width: "100%", height: "auto", objectFit: "contain" }}
style={{
width: "100%",
height: "auto",
objectFit: "contain",
borderRadius: `${radius}px`,
}}
/>
</div>
);
};

function SaveAsPngButton({
imageContent,
imageBlobUrl,
radius,
background,
imageMetadata,
}: {
imageContent: string;
imageBlobUrl: string;
radius: Radius;
background: BackgroundOption;
imageMetadata: { width: number; height: number; name: string };
}) {
const [canvasRef, setCanvasRef] = useState<HTMLCanvasElement | null>(null);
const { convertToPng, canvasProps } = useImageConverter({
canvas: canvasRef,
imageContent,
imageBlobUrl,
radius,
background,
imageMetadata,
Expand All @@ -152,7 +164,7 @@ function SaveAsPngButton({
}

function RoundedToolCore(props: { fileUploaderProps: FileUploaderResult }) {
const { imageContent, imageMetadata, handleFileUploadEvent, cancel } =
const { imageBlobUrl, imageMetadata, handleFileUploadEvent, cancel } =
props.fileUploaderProps;
const [radius, setRadius] = useLocalStorage<Radius>("roundedTool_radius", 2);
const [isCustomRadius, setIsCustomRadius] = useState(false);
Expand All @@ -170,7 +182,7 @@ function RoundedToolCore(props: { fileUploaderProps: FileUploaderResult }) {
}
};

if (!imageMetadata) {
if (!imageMetadata || !imageBlobUrl) {
return (
<UploadBox
title="Add rounded borders to your images. Quick and easy."
Expand All @@ -186,7 +198,7 @@ function RoundedToolCore(props: { fileUploaderProps: FileUploaderResult }) {
<div className="mx-auto flex max-w-2xl flex-col items-center justify-center gap-6 p-6">
<div className="flex w-full flex-col items-center gap-4 rounded-xl p-6">
<ImageRenderer
imageContent={imageContent}
imageBlobUrl={imageBlobUrl}
radius={radius}
background={background}
/>
Expand Down Expand Up @@ -229,7 +241,7 @@ function RoundedToolCore(props: { fileUploaderProps: FileUploaderResult }) {
Cancel
</button>
<SaveAsPngButton
imageContent={imageContent}
imageBlobUrl={imageBlobUrl}
radius={radius}
background={background}
imageMetadata={imageMetadata}
Expand Down
35 changes: 21 additions & 14 deletions src/app/(tools)/square-image/square-tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,28 @@ import {
import { useEffect, useState } from "react";

function SquareToolCore(props: { fileUploaderProps: FileUploaderResult }) {
const { imageContent, imageMetadata, handleFileUploadEvent, cancel } =
const { imageBlobUrl, imageMetadata, handleFileUploadEvent, cancel } =
props.fileUploaderProps;

const [backgroundColor, setBackgroundColor] = useLocalStorage<
"black" | "white"
>("squareTool_backgroundColor", "white");

const [squareImageContent, setSquareImageContent] = useState<string | null>(
// Store blob URL to clean it up later
const [usedCanvasBlobUrl, setUsedCanvasBlobUrl] = useState<string | null>(
null,
);
useEffect(
() => () => {
if (usedCanvasBlobUrl) URL.revokeObjectURL(usedCanvasBlobUrl);
},
[usedCanvasBlobUrl],
);

useEffect(() => {
if (imageContent && imageMetadata) {
const canvas = document.createElement("canvas");
if (imageBlobUrl && imageMetadata) {
const size = Math.max(imageMetadata.width, imageMetadata.height);
canvas.width = size;
canvas.height = size;
const canvas = new OffscreenCanvas(size, size);

const ctx = canvas.getContext("2d");
if (!ctx) return;
Expand All @@ -39,20 +44,22 @@ function SquareToolCore(props: { fileUploaderProps: FileUploaderResult }) {

// Load and center the image
const img = new Image();
img.onload = () => {
img.onload = async () => {
const x = (size - imageMetadata.width) / 2;
const y = (size - imageMetadata.height) / 2;
ctx.drawImage(img, x, y);
setSquareImageContent(canvas.toDataURL("image/png"));
const canvasBlob = await canvas.convertToBlob({ type: "image/png" });
const canvasBlobUrl = URL.createObjectURL(canvasBlob);
setUsedCanvasBlobUrl(canvasBlobUrl);
};
img.src = imageContent;
img.src = imageBlobUrl;
}
}, [imageContent, imageMetadata, backgroundColor]);
}, [imageBlobUrl, imageMetadata, backgroundColor]);

const handleSaveImage = () => {
if (squareImageContent && imageMetadata) {
if (usedCanvasBlobUrl && imageMetadata) {
const link = document.createElement("a");
link.href = squareImageContent;
link.href = usedCanvasBlobUrl;
const originalFileName = imageMetadata.name;
const fileNameWithoutExtension =
originalFileName.substring(0, originalFileName.lastIndexOf(".")) ||
Expand Down Expand Up @@ -81,8 +88,8 @@ function SquareToolCore(props: { fileUploaderProps: FileUploaderResult }) {
return (
<div className="mx-auto flex max-w-2xl flex-col items-center justify-center gap-6 p-6">
<div className="flex w-full flex-col items-center gap-4 rounded-xl p-6">
{squareImageContent && (
<img src={squareImageContent} alt="Preview" className="mb-4" />
{usedCanvasBlobUrl && (
<img src={usedCanvasBlobUrl} alt="Preview" className="mb-4" />
)}
<p className="text-lg font-medium text-white/80">
{imageMetadata.name}
Expand Down
Loading