feat: same code to render from both client and server

This commit is contained in:
Pihkaal
2024-09-30 05:13:38 +02:00
parent 58d8342538
commit fafbc9253e
7 changed files with 1572 additions and 154 deletions

35
app.vue
View File

@@ -1,6 +1,39 @@
<script setup lang="ts">
import { renderQRCodeToCanvas } from "@/utils/renderer";
import { IMAGE_FORMATS, LOGOS } from "@/utils/settings";
const format = ref(IMAGE_FORMATS[0]);
const logo = ref(LOGOS[0]);
const content = ref("");
const canvas = ref(null);
const qrCode = ref("");
const updateQRCode = async () => {
await nextTick();
if (content.value.length === 0) return;
const logoUrl = `/${logo.value}.png`;
await renderQRCodeToCanvas(canvas.value, content.value, logoUrl);
qrCode.value = canvas.value.toDataURL(`image/${format.value}`);
};
</script>
<template>
<div>
<NuxtRouteAnnouncer />
<p>hey</p>
<USelectMenu
v-model="format"
:options="IMAGE_FORMATS"
@input="updateQRCode"
/>
<USelectMenu v-model="logo" :options="LOGOS" @input="updateQRCode" />
<UInput v-model="content" @input="updateQRCode" />
<canvas ref="canvas" class="hidden" />
<img :src="qrCode" />
</div>
</template>

View File

@@ -2,5 +2,5 @@
export default defineNuxtConfig({
compatibilityDate: "2024-04-03",
devtools: { enabled: true },
modules: ["@nuxt/eslint"],
modules: ["@nuxt/eslint", "@nuxt/ui"],
});

View File

@@ -12,7 +12,9 @@
"format": "pnpx prettier --cache --write ."
},
"dependencies": {
"@iconify-json/heroicons": "^1.2.0",
"@nuxt/eslint": "^0.5.7",
"@nuxt/ui": "^2.18.6",
"canvas": "^2.11.2",
"nuxt": "^3.13.0",
"qrcode": "^1.5.4",
@@ -23,6 +25,7 @@
},
"packageManager": "pnpm@9.11.0",
"devDependencies": {
"@types/qrcode": "^1.5.5"
"@types/qrcode": "^1.5.5",
"typescript": "5.5.4"
}
}

1548
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,8 @@
import { createCanvas, loadImage } from "canvas";
import QRCode from "qrcode";
import { z } from "zod";
import { createCanvas } from "canvas";
import { resolve } from "path";
import sharp from "sharp";
const time = (label: string) => {
const start = performance.now();
return () => {
console.log(`${label}\t${performance.now() - start}`);
};
};
const IMAGE_FORMATS = ["jpeg", "png", "webp"] as const;
const settingsSchema = z.object({
format: z.enum(IMAGE_FORMATS).default("png"),
logo: z.string().min(1),
content: z.string().min(1),
});
import { CANVAS_SIZE, renderQRCodeToCanvas } from "~/utils/renderer";
import { settingsSchema } from "~/utils/settings";
export default defineEventHandler(async (event) => {
const query = getQuery(event);
@@ -36,66 +21,15 @@ export default defineEventHandler(async (event) => {
const { format, logo, content } = parsed.data;
const tt = time("total");
let t;
const canvas = createCanvas(CANVAS_SIZE, CANVAS_SIZE);
const logoUrl = resolve("public", `${logo}.png`);
await renderQRCodeToCanvas(canvas, content, logoUrl);
const SIZE = 1000;
const LOGO_PADDING = 1;
t = time("render");
const canvas = createCanvas(SIZE, SIZE);
await QRCode.toCanvas(canvas, content, {
errorCorrectionLevel: "H",
width: SIZE,
margin: 1,
});
t();
t = time("count");
const qrCode = QRCode.create(content, { errorCorrectionLevel: "H" });
const moduleCount = qrCode.modules.size + 2;
t();
t = time("logo");
const logoImage = await loadImage(resolve("public", `${logo}.png`));
const moduleSize = SIZE / moduleCount;
let logoModules = Math.floor(moduleCount * 0.3);
if (logoModules % 2 !== moduleCount % 2) {
logoModules += 1;
}
const backgroundSize = logoModules * moduleSize + 1;
const backgroundPosition = (moduleSize * (moduleCount - logoModules)) / 2;
const logoSize = backgroundSize - LOGO_PADDING * 2;
const logoPosition = backgroundPosition + LOGO_PADDING;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "white";
ctx.fillRect(
backgroundPosition,
backgroundPosition,
backgroundSize,
backgroundSize,
);
ctx.drawImage(logoImage, logoPosition, logoPosition, logoSize, logoSize);
t();
t = time("buffer");
let image = canvas.toBuffer();
t();
if (format !== "png") {
t = time("convert");
image = await sharp(image).toFormat(format).toBuffer();
t();
}
tt();
event.node.res.setHeader("Content-Type", `image/${format}`);
return image;
});

45
utils/renderer.ts Normal file
View File

@@ -0,0 +1,45 @@
import { loadImage, type Canvas, type CanvasRenderingContext2D } from "canvas";
import QRCode from "qrcode";
export const CANVAS_SIZE = 1000;
export const LOGO_PADDING = 1;
export const renderQRCodeToCanvas = async (
canvas: HTMLCanvasElement | Canvas,
content: string,
logoUrl: string,
) => {
await QRCode.toCanvas(canvas, content, {
errorCorrectionLevel: "H",
width: CANVAS_SIZE,
margin: 1,
});
const qrCode = QRCode.create(content, { errorCorrectionLevel: "H" });
const moduleCount = qrCode.modules.size + 2;
const logoImage = await loadImage(logoUrl);
const moduleSize = CANVAS_SIZE / moduleCount;
let logoModules = Math.floor(moduleCount * 0.3);
if (logoModules % 2 !== moduleCount % 2) {
logoModules += 1;
}
const backgroundSize = logoModules * moduleSize + 1;
const backgroundPosition = (moduleSize * (moduleCount - logoModules)) / 2;
const logoSize = backgroundSize - LOGO_PADDING * 2;
const logoPosition = backgroundPosition + LOGO_PADDING;
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
ctx.fillStyle = "white";
ctx.fillRect(
backgroundPosition,
backgroundPosition,
backgroundSize,
backgroundSize,
);
ctx.drawImage(logoImage, logoPosition, logoPosition, logoSize, logoSize);
};

13
utils/settings.ts Normal file
View File

@@ -0,0 +1,13 @@
import { z } from "zod";
export const LOGOS = ["session", "instagram"] as const;
export const IMAGE_FORMATS = ["jpeg", "png", "webp"] as const;
export type ImageFormat = (typeof IMAGE_FORMATS)[number];
export const settingsSchema = z.object({
format: z.enum(IMAGE_FORMATS).default("png"),
logo: z.enum(LOGOS),
content: z.string().min(1),
});