From 7b7570d50e15ddba2a9cbd2bb6fe3823b73c49c0 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Fri, 20 Sep 2024 10:49:03 -0700 Subject: [PATCH 1/5] Add logo support to QR API --- apps/web/app/api/qr/route.tsx | 44 +++++++++--------- apps/web/lib/qr/api.tsx | 87 +++++++++++++++++++++++++++++++++++ apps/web/package.json | 2 +- pnpm-lock.yaml | 8 ++-- 4 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 apps/web/lib/qr/api.tsx diff --git a/apps/web/app/api/qr/route.tsx b/apps/web/app/api/qr/route.tsx index 37138df19f..9c65c09220 100644 --- a/apps/web/app/api/qr/route.tsx +++ b/apps/web/app/api/qr/route.tsx @@ -1,9 +1,9 @@ import { handleAndReturnErrorResponse } from "@/lib/api/errors"; import { ratelimitOrThrow } from "@/lib/api/utils"; -import { QRCodeSVG } from "@/lib/qr/utils"; +import { getQRAsSVG } from "@/lib/qr/api"; import { getQRCodeQuerySchema } from "@/lib/zod/schemas/qr"; import { getSearchParams } from "@dub/utils"; -import { ImageResponse } from "next/og"; +import { ImageResponse } from "@vercel/og"; import { NextRequest } from "next/server"; export const runtime = "edge"; @@ -16,28 +16,28 @@ export async function GET(req: NextRequest) { const { url, size, level, fgColor, bgColor, includeMargin } = getQRCodeQuerySchema.parse(params); - // const logo = req.nextUrl.searchParams.get("logo") || "https://assets.dub.co/logo.png"; + const logo = + req.nextUrl.searchParams.get("logo") || "https://assets.dub.co/logo.png"; - return new ImageResponse( - QRCodeSVG({ - value: url, - size, - level, - includeMargin, - fgColor, - bgColor, - // imageSettings: { - // src: logo, - // height: size / 4, - // width: size / 4, - // excavate: true, - // }, - }), - { - width: size, - height: size, + const svg = await getQRAsSVG({ + value: url, + size, + level, + includeMargin, + fgColor, + bgColor, + imageSettings: { + src: logo, + height: size / 4, + width: size / 4, + excavate: true, }, - ); + }); + + return new ImageResponse(svg, { + width: size, + height: size, + }); } catch (error) { return handleAndReturnErrorResponse(error); } diff --git a/apps/web/lib/qr/api.tsx b/apps/web/lib/qr/api.tsx new file mode 100644 index 0000000000..ac88b82001 --- /dev/null +++ b/apps/web/lib/qr/api.tsx @@ -0,0 +1,87 @@ +import qrcodegen from "./codegen"; +import { + DEFAULT_BGCOLOR, + DEFAULT_FGCOLOR, + DEFAULT_INCLUDEMARGIN, + DEFAULT_LEVEL, + DEFAULT_SIZE, + ERROR_LEVEL_MAP, + MARGIN_SIZE, +} from "./constants"; +import { QRPropsSVG } from "./types"; +import { excavateModules, generatePath, getImageSettings } from "./utils"; + +export async function getQRAsSVG(props: QRPropsSVG) { + const { + value, + size = DEFAULT_SIZE, + level = DEFAULT_LEVEL, + bgColor = DEFAULT_BGCOLOR, + fgColor = DEFAULT_FGCOLOR, + includeMargin = DEFAULT_INCLUDEMARGIN, + imageSettings, + ...otherProps + } = props; + + let cells = qrcodegen.QrCode.encodeText( + value, + ERROR_LEVEL_MAP[level], + ).getModules(); + + const margin = includeMargin ? MARGIN_SIZE : 0; + const numCells = cells.length + margin * 2; + const calculatedImageSettings = getImageSettings( + cells, + size, + includeMargin, + imageSettings, + ); + + let image = <>; + if (imageSettings != null && calculatedImageSettings != null) { + if (calculatedImageSettings.excavation != null) { + cells = excavateModules(cells, calculatedImageSettings.excavation); + } + + const base64Image = await fetch( + `https://wsrv.nl/?url=${imageSettings.src}&w=100&h=100&encoding=base64`, + ).then((res) => res.text()); + + image = ( + + ); + } + + // Drawing strategy: instead of a rect per module, we're going to create a + // single path for the dark modules and layer that on top of a light rect, + // for a total of 2 DOM nodes. We pay a bit more in string concat but that's + // way faster than DOM ops. + // For level 1, 441 nodes -> 2 + // For level 40, 31329 -> 2 + const fgPath = generatePath(cells, margin); + + return ( + + + + {image} + + ); +} diff --git a/apps/web/package.json b/apps/web/package.json index 35c8296511..2c8a07ad15 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -41,7 +41,7 @@ "@vercel/edge": "^0.3.1", "@vercel/edge-config": "^0.4.1", "@vercel/functions": "^1.0.1", - "@vercel/og": "^0.6.2", + "@vercel/og": "^0.6.3", "@visx/axis": "^2.14.0", "@visx/curve": "^3.3.0", "@visx/event": "^2.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9225292bf0..bed330dafb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,8 +120,8 @@ importers: specifier: ^1.0.1 version: 1.0.1 '@vercel/og': - specifier: ^0.6.2 - version: 0.6.2 + specifier: ^0.6.3 + version: 0.6.3 '@visx/axis': specifier: ^2.14.0 version: 2.14.0(react@18.2.0) @@ -9586,8 +9586,8 @@ packages: engines: {node: '>= 16'} dev: false - /@vercel/og@0.6.2: - resolution: {integrity: sha512-OTe0KE37F5Y2eTys6eMnfopC+P4qr2ooXUTFyFPTplYSPwowmFk/HLD1FXtbKLjqsIH0SgekcJWad+C5uX4nkg==} + /@vercel/og@0.6.3: + resolution: {integrity: sha512-aoCrC9FqkeA+WEEb9CwSmjD0rGlFeNqbUsI41JPmKWR9Hx6FFn86tvH96O5HZMF6VAXTGHxa3nPH3BokROpdgA==} engines: {node: '>=16'} dependencies: '@resvg/resvg-wasm': 2.4.0 From 9afc7a45f84af41133ce6472328961d0fb3c000b Mon Sep 17 00:00:00 2001 From: Bharath Lakshman Kumar <108531789+BharathxD@users.noreply.github.com> Date: Fri, 4 Oct 2024 21:38:09 +0530 Subject: [PATCH 2/5] fix: add logo support to QR code API using tag as a workaround --- apps/web/app/api/qr/route.tsx | 41 +++++++++++++------------- apps/web/lib/qr/utils.tsx | 54 +++++++++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/apps/web/app/api/qr/route.tsx b/apps/web/app/api/qr/route.tsx index 9c65c09220..a4fc751b43 100644 --- a/apps/web/app/api/qr/route.tsx +++ b/apps/web/app/api/qr/route.tsx @@ -1,9 +1,9 @@ import { handleAndReturnErrorResponse } from "@/lib/api/errors"; import { ratelimitOrThrow } from "@/lib/api/utils"; -import { getQRAsSVG } from "@/lib/qr/api"; +import { QRCodeSVG } from "@/lib/qr/utils"; import { getQRCodeQuerySchema } from "@/lib/zod/schemas/qr"; import { getSearchParams } from "@dub/utils"; -import { ImageResponse } from "@vercel/og"; +import { ImageResponse } from "next/og"; import { NextRequest } from "next/server"; export const runtime = "edge"; @@ -19,25 +19,26 @@ export async function GET(req: NextRequest) { const logo = req.nextUrl.searchParams.get("logo") || "https://assets.dub.co/logo.png"; - const svg = await getQRAsSVG({ - value: url, - size, - level, - includeMargin, - fgColor, - bgColor, - imageSettings: { - src: logo, - height: size / 4, - width: size / 4, - excavate: true, + return new ImageResponse( + QRCodeSVG({ + value: url, + size, + level, + includeMargin, + fgColor, + bgColor, + imageSettings: { + src: logo, + height: size / 4, + width: size / 4, + excavate: true, + }, + }), + { + width: size, + height: size, }, - }); - - return new ImageResponse(svg, { - width: size, - height: size, - }); + ); } catch (error) { return handleAndReturnErrorResponse(error); } diff --git a/apps/web/lib/qr/utils.tsx b/apps/web/lib/qr/utils.tsx index 54a43f75df..8ec8756393 100644 --- a/apps/web/lib/qr/utils.tsx +++ b/apps/web/lib/qr/utils.tsx @@ -116,6 +116,31 @@ export function getImageSettings( return { x, y, h, w, excavation }; } +export function convertImageSettingsToPixels( + calculatedImageSettings: { + x: number; + y: number; + w: number; + h: number; + excavation: Excavation | null; + }, + size: number, + numCells: number, +): { + imgWidth: number; + imgHeight: number; + imgLeft: number; + imgTop: number; +} { + const pixelRatio = size / numCells; + const imgWidth = calculatedImageSettings.w * pixelRatio; + const imgHeight = calculatedImageSettings.h * pixelRatio; + const imgLeft = calculatedImageSettings.x * pixelRatio; + const imgTop = calculatedImageSettings.y * pixelRatio; + + return { imgWidth, imgHeight, imgLeft, imgTop }; +} + export function QRCodeSVG(props: QRPropsSVG) { const { value, @@ -128,9 +153,16 @@ export function QRCodeSVG(props: QRPropsSVG) { ...otherProps } = props; + const shouldUseHigherErrorLevel = + imageSettings?.excavate && (level === "L" || level === "M"); + + // Use a higher error correction level 'Q' when excavation is enabled + // to ensure the QR code remains scannable despite the removed modules. + const effectiveLevel = shouldUseHigherErrorLevel ? "Q" : level; + let cells = qrcodegen.QrCode.encodeText( value, - ERROR_LEVEL_MAP[level], + ERROR_LEVEL_MAP[effectiveLevel], ).getModules(); const margin = includeMargin ? MARGIN_SIZE : 0; @@ -148,14 +180,20 @@ export function QRCodeSVG(props: QRPropsSVG) { cells = excavateModules(cells, calculatedImageSettings.excavation); } + const { imgWidth, imgHeight, imgLeft, imgTop } = + convertImageSettingsToPixels(calculatedImageSettings, size, numCells); + image = ( - Logo ); } From 1b91086cd8012362d88df1f4b0d651aca24a8676 Mon Sep 17 00:00:00 2001 From: Bharath Lakshman Kumar <108531789+BharathxD@users.noreply.github.com> Date: Fri, 4 Oct 2024 22:40:05 +0530 Subject: [PATCH 3/5] fix(qr-code): ensure compatibility with React components and OG context --- apps/web/app/api/qr/route.tsx | 1 + apps/web/lib/qr/types.ts | 1 + apps/web/lib/qr/utils.tsx | 48 ++++++++++++++++++++++------------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/apps/web/app/api/qr/route.tsx b/apps/web/app/api/qr/route.tsx index a4fc751b43..5202160ae8 100644 --- a/apps/web/app/api/qr/route.tsx +++ b/apps/web/app/api/qr/route.tsx @@ -33,6 +33,7 @@ export async function GET(req: NextRequest) { width: size / 4, excavate: true, }, + isOGContext: true, }), { width: size, diff --git a/apps/web/lib/qr/types.ts b/apps/web/lib/qr/types.ts index 921e75d99e..cbac647afe 100644 --- a/apps/web/lib/qr/types.ts +++ b/apps/web/lib/qr/types.ts @@ -22,6 +22,7 @@ export type QRProps = { style?: CSSProperties; includeMargin?: boolean; imageSettings?: ImageSettings; + isOGContext?: boolean; }; export type QRPropsCanvas = QRProps & React.CanvasHTMLAttributes; diff --git a/apps/web/lib/qr/utils.tsx b/apps/web/lib/qr/utils.tsx index 8ec8756393..a48e203efd 100644 --- a/apps/web/lib/qr/utils.tsx +++ b/apps/web/lib/qr/utils.tsx @@ -149,12 +149,13 @@ export function QRCodeSVG(props: QRPropsSVG) { bgColor = DEFAULT_BGCOLOR, fgColor = DEFAULT_FGCOLOR, includeMargin = DEFAULT_INCLUDEMARGIN, + isOGContext = false, imageSettings, ...otherProps } = props; const shouldUseHigherErrorLevel = - imageSettings?.excavate && (level === "L" || level === "M"); + isOGContext && imageSettings?.excavate && (level === "L" || level === "M"); // Use a higher error correction level 'Q' when excavation is enabled // to ensure the QR code remains scannable despite the removed modules. @@ -180,22 +181,35 @@ export function QRCodeSVG(props: QRPropsSVG) { cells = excavateModules(cells, calculatedImageSettings.excavation); } - const { imgWidth, imgHeight, imgLeft, imgTop } = - convertImageSettingsToPixels(calculatedImageSettings, size, numCells); - - image = ( - Logo - ); + if (isOGContext) { + const { imgWidth, imgHeight, imgLeft, imgTop } = + convertImageSettingsToPixels(calculatedImageSettings, size, numCells); + + image = ( + Logo + ); + } else { + image = ( + + ); + } } // Drawing strategy: instead of a rect per module, we're going to create a From 0890cfa1df04b03128bbd3c59f17e4715eaf1bc3 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Fri, 4 Oct 2024 11:48:07 -0700 Subject: [PATCH 4/5] stash --- apps/web/ui/modals/link-qr-modal.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/ui/modals/link-qr-modal.tsx b/apps/web/ui/modals/link-qr-modal.tsx index 54be7ff901..58afa60a56 100644 --- a/apps/web/ui/modals/link-qr-modal.tsx +++ b/apps/web/ui/modals/link-qr-modal.tsx @@ -25,7 +25,7 @@ import { Hyperlink, Photo, } from "@dub/ui/src/icons"; -import { cn, linkConstructor } from "@dub/utils"; +import { API_DOMAIN, cn, linkConstructor } from "@dub/utils"; import { AnimatePresence, motion } from "framer-motion"; import { Dispatch, @@ -489,13 +489,13 @@ function CopyPopover({ type="button" onClick={() => { navigator.clipboard.writeText( - `https://api.dub.co/qr?url=${linkConstructor({ + `${API_DOMAIN}/qr?url=${linkConstructor({ key: props.key, domain: props.domain, searchParams: { qr: "1", }, - })}`, + })}${qrData.imageSettings?.src ? `&logo=${qrData.imageSettings.src}` : ""}`, ); toast.success("Copied QR code URL to clipboard!"); setCopiedURL(true); From d547743bc7f461f53cd39dc44370c53e44067e6d Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Fri, 4 Oct 2024 12:04:19 -0700 Subject: [PATCH 5/5] add `logo` to openapi --- apps/web/app/api/qr/route.tsx | 5 +---- apps/web/lib/zod/schemas/qr.ts | 7 +++++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/web/app/api/qr/route.tsx b/apps/web/app/api/qr/route.tsx index 5202160ae8..68e9ec4253 100644 --- a/apps/web/app/api/qr/route.tsx +++ b/apps/web/app/api/qr/route.tsx @@ -13,12 +13,9 @@ export async function GET(req: NextRequest) { await ratelimitOrThrow(req, "qr"); const params = getSearchParams(req.url); - const { url, size, level, fgColor, bgColor, includeMargin } = + const { url, logo, size, level, fgColor, bgColor, includeMargin } = getQRCodeQuerySchema.parse(params); - const logo = - req.nextUrl.searchParams.get("logo") || "https://assets.dub.co/logo.png"; - return new ImageResponse( QRCodeSVG({ value: url, diff --git a/apps/web/lib/zod/schemas/qr.ts b/apps/web/lib/zod/schemas/qr.ts index 0209a21924..8462af07d8 100644 --- a/apps/web/lib/zod/schemas/qr.ts +++ b/apps/web/lib/zod/schemas/qr.ts @@ -10,6 +10,13 @@ import { parseUrlSchema } from "./utils"; export const getQRCodeQuerySchema = z.object({ url: parseUrlSchema.describe("The URL to generate a QR code for."), + logo: z + .string() + .optional() + .default("https://assets.dub.co/logo.png") + .describe( + "The logo to include in the QR code. Defaults to `https://assets.dub.co/logo.png` if not provided.", + ), size: z.coerce .number() .optional()