Skip to content

Commit

Permalink
Merge pull request #1321 from BharathxD/fix-qr
Browse files Browse the repository at this point in the history
fix: add logo support to QR code API using <img> tag as a workaround
  • Loading branch information
steven-tey authored Oct 4, 2024
2 parents 4ba6bb6 + d547743 commit b8140b7
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 28 deletions.
17 changes: 8 additions & 9 deletions apps/web/app/api/qr/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +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,
Expand All @@ -26,12 +24,13 @@ export async function GET(req: NextRequest) {
includeMargin,
fgColor,
bgColor,
// imageSettings: {
// src: logo,
// height: size / 4,
// width: size / 4,
// excavate: true,
// },
imageSettings: {
src: logo,
height: size / 4,
width: size / 4,
excavate: true,
},
isOGContext: true,
}),
{
width: size,
Expand Down
87 changes: 87 additions & 0 deletions apps/web/lib/qr/api.tsx
Original file line number Diff line number Diff line change
@@ -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 = (
<image
href={base64Image}
height={calculatedImageSettings.h}
width={calculatedImageSettings.w}
x={calculatedImageSettings.x + margin}
y={calculatedImageSettings.y + margin}
preserveAspectRatio="none"
/>
);
}

// 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 (
<svg
height={size}
width={size}
viewBox={`0 0 ${numCells} ${numCells}`}
xmlns="http://www.w3.org/2000/svg"
{...otherProps}
>
<path
fill={bgColor}
d={`M0,0 h${numCells}v${numCells}H0z`}
shapeRendering="crispEdges"
/>
<path fill={fgColor} d={fgPath} shapeRendering="crispEdges" />
{image}
</svg>
);
}
1 change: 1 addition & 0 deletions apps/web/lib/qr/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export type QRProps = {
style?: CSSProperties;
includeMargin?: boolean;
imageSettings?: ImageSettings;
isOGContext?: boolean;
};
export type QRPropsCanvas = QRProps &
React.CanvasHTMLAttributes<HTMLCanvasElement>;
Expand Down
74 changes: 63 additions & 11 deletions apps/web/lib/qr/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -124,13 +149,21 @@ export function QRCodeSVG(props: QRPropsSVG) {
bgColor = DEFAULT_BGCOLOR,
fgColor = DEFAULT_FGCOLOR,
includeMargin = DEFAULT_INCLUDEMARGIN,
isOGContext = false,
imageSettings,
...otherProps
} = props;

const shouldUseHigherErrorLevel =
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.
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;
Expand All @@ -148,16 +181,35 @@ export function QRCodeSVG(props: QRPropsSVG) {
cells = excavateModules(cells, calculatedImageSettings.excavation);
}

image = (
<image
href={imageSettings.src}
height={calculatedImageSettings.h}
width={calculatedImageSettings.w}
x={calculatedImageSettings.x + margin}
y={calculatedImageSettings.y + margin}
preserveAspectRatio="none"
/>
);
if (isOGContext) {
const { imgWidth, imgHeight, imgLeft, imgTop } =
convertImageSettingsToPixels(calculatedImageSettings, size, numCells);

image = (
<img
src={imageSettings.src}
alt="Logo"
style={{
position: "absolute",
left: `${imgLeft}px`,
top: `${imgTop}px`,
width: `${imgWidth}px`,
height: `${imgHeight}px`,
}}
/>
);
} else {
image = (
<image
href={imageSettings.src}
height={calculatedImageSettings.h}
width={calculatedImageSettings.w}
x={calculatedImageSettings.x + margin}
y={calculatedImageSettings.y + margin}
preserveAspectRatio="none"
/>
);
}
}

// Drawing strategy: instead of a rect per module, we're going to create a
Expand Down
7 changes: 7 additions & 0 deletions apps/web/lib/zod/schemas/qr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"@upstash/redis": "^1.25.1",
"@vercel/edge-config": "^0.4.1",
"@vercel/functions": "^1.4.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",
Expand Down
6 changes: 3 additions & 3 deletions apps/web/ui/modals/link-qr-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit b8140b7

Please sign in to comment.