feat: same code to render from both client and server
This commit is contained in:
35
app.vue
35
app.vue
@@ -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>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<NuxtRouteAnnouncer />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2024-04-03",
|
compatibilityDate: "2024-04-03",
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
modules: ["@nuxt/eslint"],
|
modules: ["@nuxt/eslint", "@nuxt/ui"],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
"format": "pnpx prettier --cache --write ."
|
"format": "pnpx prettier --cache --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@iconify-json/heroicons": "^1.2.0",
|
||||||
"@nuxt/eslint": "^0.5.7",
|
"@nuxt/eslint": "^0.5.7",
|
||||||
|
"@nuxt/ui": "^2.18.6",
|
||||||
"canvas": "^2.11.2",
|
"canvas": "^2.11.2",
|
||||||
"nuxt": "^3.13.0",
|
"nuxt": "^3.13.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
@@ -23,6 +25,7 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.11.0",
|
"packageManager": "pnpm@9.11.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/qrcode": "^1.5.5"
|
"@types/qrcode": "^1.5.5",
|
||||||
|
"typescript": "5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1548
pnpm-lock.yaml
generated
1548
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,8 @@
|
|||||||
import { createCanvas, loadImage } from "canvas";
|
import { createCanvas } from "canvas";
|
||||||
import QRCode from "qrcode";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
import { CANVAS_SIZE, renderQRCodeToCanvas } from "~/utils/renderer";
|
||||||
const time = (label: string) => {
|
import { settingsSchema } from "~/utils/settings";
|
||||||
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),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const query = getQuery(event);
|
const query = getQuery(event);
|
||||||
@@ -36,66 +21,15 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
const { format, logo, content } = parsed.data;
|
const { format, logo, content } = parsed.data;
|
||||||
|
|
||||||
const tt = time("total");
|
const canvas = createCanvas(CANVAS_SIZE, CANVAS_SIZE);
|
||||||
let t;
|
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();
|
let image = canvas.toBuffer();
|
||||||
t();
|
|
||||||
|
|
||||||
if (format !== "png") {
|
if (format !== "png") {
|
||||||
t = time("convert");
|
|
||||||
image = await sharp(image).toFormat(format).toBuffer();
|
image = await sharp(image).toFormat(format).toBuffer();
|
||||||
t();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tt();
|
|
||||||
|
|
||||||
event.node.res.setHeader("Content-Type", `image/${format}`);
|
event.node.res.setHeader("Content-Type", `image/${format}`);
|
||||||
return image;
|
return image;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
45
utils/renderer.ts
Normal file
45
utils/renderer.ts
Normal 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
13
utils/settings.ts
Normal 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),
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user