Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

separate components in different files #188

Open
wants to merge 1 commit into
base: trunk
Choose a base branch
from
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
158 changes: 158 additions & 0 deletions src/canvas.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
"use client"
/**

* @license qrcode.react
* Copyright (c) Paul O'Shannessy
* SPDX-License-Identifier: ISC
*/

import React, {useRef, useEffect} from 'react';
Copy link
Contributor

@kachkaev kachkaev May 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import React, {useRef, useEffect} from 'react';
import * as React from 'react';
import {useRef, useEffect} from 'react';

or

Suggested change
import React, {useRef, useEffect} from 'react';
import * as React from 'react';

if yu want to use React.useRef / React.useEffect.

Default export from react might be removed in v20 or a bit later. It’s not tree-shakeable.

import qrcodegen from './third-party/qrcodegen';
import {
QRProps,
DEFAULT_PROPS,
MARGIN_SIZE,
ERROR_LEVEL_MAP,
generatePath,
excavateModules,
getImageSettings,
} from './helpers';

// For canvas we're going to switch our drawing mode based on whether or not
// the environment supports Path2D. We only need the constructor to be
// supported, but Edge doesn't actually support the path (string) type
// argument. Luckily it also doesn't support the addPath() method. We can
// treat that as the same thing.
const SUPPORTS_PATH2D = (function () {
try {
new Path2D().addPath(new Path2D());
} catch (e) {
return false;
}
return true;
})();

export default function QRCodeCanvas(props: QRProps) {
const _canvas = useRef<HTMLCanvasElement>(null);
const _image = useRef<HTMLImageElement>(null);

function update() {
const {value, size, level, bgColor, fgColor, includeMargin} = props;

if (_canvas.current != null) {
const canvas = _canvas.current;

const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}

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(props, cells);

const image = _image.current;
const haveImageToRender =
calculatedImageSettings != null &&
image !== null &&
image.complete &&
image.naturalHeight !== 0 &&
image.naturalWidth !== 0;

if (haveImageToRender) {
if (calculatedImageSettings.excavation != null) {
cells = excavateModules(cells, calculatedImageSettings.excavation);
}
}

// We're going to scale this so that the number of drawable units
// matches the number of cells. This avoids rounding issues, but does
// result in some potentially unwanted single pixel issues between
// blocks, only in environments that don't support Path2D.
const pixelRatio = window.devicePixelRatio || 1;
canvas.height = canvas.width = size * pixelRatio;
const scale = (size / numCells) * pixelRatio;
ctx.scale(scale, scale);

// Draw solid background, only paint dark modules.
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, numCells, numCells);

ctx.fillStyle = fgColor;
if (SUPPORTS_PATH2D) {
// $FlowFixMe: Path2D c'tor doesn't support args yet.
ctx.fill(new Path2D(generatePath(cells, margin)));
} else {
cells.forEach(function (row, rdx) {
row.forEach(function (cell, cdx) {
if (cell) {
ctx.fillRect(cdx + margin, rdx + margin, 1, 1);
}
});
});
}

if (haveImageToRender) {
ctx.drawImage(
image,
calculatedImageSettings.x + margin,
calculatedImageSettings.y + margin,
calculatedImageSettings.w,
calculatedImageSettings.h
);
}
}
}

useEffect(() => {
// Always update the canvas. It's cheap enough and we want to be correct
// with the current state.
update();
});

const {
value,
size,
level,
bgColor,
fgColor,
style,
includeMargin,
imageSettings,
...otherProps
} = props;
const canvasStyle = {height: size, width: size, ...style};
let img = null;
let imgSrc = imageSettings?.src;
if (imgSrc != null) {
img = (
<img
src={imgSrc}
key={imgSrc}
style={{display: 'none'}}
onLoad={() => {
update();
}}
ref={_image}
/>
);
}
return (
<>
<canvas
style={canvasStyle}
height={size}
width={size}
ref={_canvas}
{...otherProps}
/>
{img}
</>
);
}

QRCodeCanvas.defaultProps = DEFAULT_PROPS;

154 changes: 154 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* @license qrcode.react
* Copyright (c) Paul O'Shannessy
* SPDX-License-Identifier: ISC
*/

import qrcodegen from './third-party/qrcodegen';
import type {CSSProperties} from 'react';

type Modules = ReturnType<qrcodegen.QrCode['getModules']>;
type Excavation = {x: number; y: number; w: number; h: number};

export const ERROR_LEVEL_MAP: {[index: string]: qrcodegen.QrCode.Ecc} = {
L: qrcodegen.QrCode.Ecc.LOW,
M: qrcodegen.QrCode.Ecc.MEDIUM,
Q: qrcodegen.QrCode.Ecc.QUARTILE,
H: qrcodegen.QrCode.Ecc.HIGH,
};

export type QRProps = {
value: string;
size: number;
// Should be a real enum, but doesn't seem to be compatible with real code.
level: string;
bgColor: string;
fgColor: string;
style?: CSSProperties;
includeMargin: boolean;
imageSettings?: {
src: string;
height: number;
width: number;
excavate: boolean;
x?: number;
y?: number;
};
};

export const DEFAULT_PROPS = {
size: 128,
level: 'L',
bgColor: '#FFFFFF',
fgColor: '#000000',
includeMargin: false,
};

export const MARGIN_SIZE = 4;

// This is *very* rough estimate of max amount of QRCode allowed to be covered.
// It is "wrong" in a lot of ways (area is a terrible way to estimate, it
// really should be number of modules covered), but if for some reason we don't
// get an explicit height or width, I'd rather default to something than throw.
const DEFAULT_IMG_SCALE = 0.1;

export function generatePath(modules: Modules, margin: number = 0): string {
const ops: Array<string> = [];
modules.forEach(function (row, y) {
let start: number | null = null;
row.forEach(function (cell, x) {
if (!cell && start !== null) {
// M0 0h7v1H0z injects the space with the move and drops the comma,
// saving a char per operation
ops.push(
`M${start + margin} ${y + margin}h${x - start}v1H${start + margin}z`
);
start = null;
return;
}

// end of row, clean up or skip
if (x === row.length - 1) {
if (!cell) {
// We would have closed the op above already so this can only mean
// 2+ light modules in a row.
return;
}
if (start === null) {
// Just a single dark module.
ops.push(`M${x + margin},${y + margin} h1v1H${x + margin}z`);
} else {
// Otherwise finish the current line.
ops.push(
`M${start + margin},${y + margin} h${x + 1 - start}v1H${
start + margin
}z`
);
}
return;
}

if (cell && start === null) {
start = x;
}
});
});
return ops.join('');
}

// We could just do this in generatePath, except that we want to support
// non-Path2D canvas, so we need to keep it an explicit step.
export function excavateModules(modules: Modules, excavation: Excavation): Modules {
return modules.slice().map((row, y) => {
if (y < excavation.y || y >= excavation.y + excavation.h) {
return row;
}
return row.map((cell, x) => {
if (x < excavation.x || x >= excavation.x + excavation.w) {
return cell;
}
return false;
});
});
}

export function getImageSettings(
props: QRProps,
cells: Modules
): null | {
x: number;
y: number;
h: number;
w: number;
excavation: Excavation | null;
} {
const {imageSettings, size, includeMargin} = props;
if (imageSettings == null) {
return null;
}
const margin = includeMargin ? MARGIN_SIZE : 0;
const numCells = cells.length + margin * 2;
const defaultSize = Math.floor(size * DEFAULT_IMG_SCALE);
const scale = numCells / size;
const w = (imageSettings.width || defaultSize) * scale;
const h = (imageSettings.height || defaultSize) * scale;
const x =
imageSettings.x == null
? cells.length / 2 - w / 2
: imageSettings.x * scale;
const y =
imageSettings.y == null
? cells.length / 2 - h / 2
: imageSettings.y * scale;

let excavation = null;
if (imageSettings.excavate) {
let floorX = Math.floor(x);
let floorY = Math.floor(y);
let ceilW = Math.ceil(w + x - floorX);
let ceilH = Math.ceil(h + y - floorY);
excavation = {x: floorX, y: floorY, w: ceilW, h: ceilH};
}

return {x, y, h, w, excavation};
}
Loading