Skip to content

Commit

Permalink
feat(qr): export top-level headless helper, img now render base64 png…
Browse files Browse the repository at this point in the history
… (previously base64 svg)
  • Loading branch information
vnphanquang committed Apr 9, 2024
1 parent 7282584 commit 0c8dc62
Show file tree
Hide file tree
Showing 19 changed files with 306 additions and 65 deletions.
5 changes: 5 additions & 0 deletions .changeset/dull-suns-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@svelte-put/qr": minor
---

now render `img` as base64 png (previously base64 svg)
5 changes: 5 additions & 0 deletions .changeset/light-fireants-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@svelte-put/qr": minor
---

export top-level headless helpers `createQrSvgString` and `createQrSvgDataUrl` for svg (browser & server), and `createQrPngDataUrl` for png (browser only)
67 changes: 47 additions & 20 deletions packages/qr/src/img/QR.svelte
Original file line number Diff line number Diff line change
@@ -1,53 +1,80 @@
<script>
import { createEventDispatcher, onMount } from 'svelte';
import { createBase64Image } from '../qr/index.js';
import { createQrSvgDataUrl, createQrPngDataUrl } from '../qr/index.js';
import { DEFAULT_FILLS, toDataURL } from './index.js';
import { toDataURL } from './index.js';
$: ({ data, anchorInnerFill, anchorOuterFill, logo, logoRatio, margin, moduleFill, shape, errorCorrectionLevel, typeNumber, ...rest } = /** @type {import('../qr/types.js').QRConfig} */($$props));
// FIXME: svelte v5 for better props declaration & dependency tracking here
// FIXME: svelte v5 for better dependency tracking here
let logoData = logo;
$: fetchLogo(logo);
/** @type {string} */
export let data;
/** @type {number | undefined} */
export let margin = undefined;
/** @type {import('./QR.svelte.js').QRProps['shape'] | undefined} */
export let shape = undefined;
$: src = createBase64Image(
/** @type {import('./QR.svelte').QRProps} */ ({
...DEFAULT_FILLS,
/** @type {string | undefined} */
export let logo = undefined;
/** @type {number | undefined} */
export let logoRatio = undefined;
/** @type {string | undefined} */
export let moduleFill = undefined;
/** @type {string | undefined} */
export let anchorInnerFill = undefined;
/** @type {string | undefined} */
export let anchorOuterFill = undefined;
/** @type {string | undefined} */
export let backgroundFill = undefined;
/** @type {import('./QR.svelte.js').QRProps['typeNumber'] | undefined} */
export let typeNumber = undefined;
/** @type {import('./QR.svelte.js').QRProps['errorCorrectionLevel'] | undefined} */
export let errorCorrectionLevel = undefined;
function getConfig() {
return {
data,
anchorInnerFill,
anchorOuterFill,
logo: logoData,
logoRatio,
margin,
moduleFill,
shape,
typeNumber,
errorCorrectionLevel,
}),
);
};
}
let src = createQrSvgDataUrl(getConfig());
/** @type {SVGElement | HTMLImageElement}*/
let element;
/** @type {ReturnType<typeof createEventDispatcher<{ 'qr:init': typeof element, 'qr:logofetch': string }>>}*/
const dispatch = createEventDispatcher();
onMount(async () => {
if (element) dispatch('qr:init', element);
});
/**
* @param {string} logo
*/
async function fetchLogo(logo) {
/** @type {string | undefined}*/
let logoData = undefined;
if (logo?.startsWith('http')) {
logoData = await toDataURL(logo);
dispatch('qr:logofetch', logo);
}
}
const base64png = await createQrPngDataUrl({
...getConfig(),
backgroundFill,
width: $$restProps.width,
height: $$restProps.height,
logo: logoData,
});
src = base64png;
});
</script>
{#key src}
<slot {src}>
<img {src} alt={$$props.data ?? $$restProps.alt} {...rest} bind:this={element} />
<img {src} alt={$$props.data ?? $$restProps.alt} {...$$restProps} bind:this={element} />
</slot>
{/key}
4 changes: 3 additions & 1 deletion packages/qr/src/img/QR.svelte.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import type { HTMLImgAttributes } from 'svelte/elements';

import type { QRConfig } from '../qr/types';

export interface QRProps extends Omit<HTMLImgAttributes, 'src'>, QRConfig {}
export interface QRProps extends Omit<HTMLImgAttributes, 'src'>, QRConfig {
backgroundFill?: string;
}

export interface QREvents {
'qr:init': CustomEvent<HTMLImgAttributes>;
Expand Down
24 changes: 10 additions & 14 deletions packages/qr/src/img/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import { createBase64Image } from '../qr/index.js';

export const DEFAULT_FILLS = {
moduleFill: 'black',
anchorOuterFill: 'black',
anchorInnerFill: 'black',
};
import { createQrPngDataUrl } from '../qr';

/**
* Fetch a remote image and convert to base64 string
Expand Down Expand Up @@ -39,14 +33,16 @@ export function qr(node, param) {
logo = await toDataURL(logo);
}

node.src = createBase64Image(
/** @type {import('./QR.svelte').QRProps} */ ({
...DEFAULT_FILLS,
...param,
logo,
}),
);
/** @type {import('./types').ImgQRParameter} */
const rConfig = {
...param,
width: parseInt(node.getAttribute('width') || '') || param.width,
height: parseInt(node.getAttribute('height') || '') || param.height,
logo,
}
const pngBase64 = await createQrPngDataUrl(rConfig)

node.src = pngBase64;
node.dispatchEvent(new CustomEvent('qr:init', { detail: node }));
}

Expand Down
6 changes: 5 additions & 1 deletion packages/qr/src/img/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { ActionReturn, Action } from 'svelte/action';
import { SizeAttributes } from '../qr/types';

/** @public */
export type ImgQRParameter = import('../qr/types').QRConfig;
export type ImgQRParameter = import('../qr/types').QRConfig & SizeAttributes & {
/** background of the generated png. Default to transparent */
backgroundFill?: string;
};

/**
* Configurations available for
Expand Down
2 changes: 1 addition & 1 deletion packages/qr/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
* @packageDocumentation
*/

export { createSVG, createSVGParts, createBase64Image } from './qr/index.js';
export { createQrSvgString, createQrSvgParts, createQrSvgDataUrl, createQrPngDataUrl } from './qr/index.js';
86 changes: 76 additions & 10 deletions packages/qr/src/qr/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,17 @@ export function resolveConfig(config) {
anchorOuterFill: config.anchorOuterFill ?? 'currentcolor',
anchorInnerFill: config.anchorInnerFill ?? 'currentcolor',
typeNumber: config.typeNumber ?? 0,
errorCorrectionLevel: config.errorCorrectionLevel ?? 'H'
errorCorrectionLevel: config.errorCorrectionLevel ?? 'H',
});
}

/**
* create SVG parts that make up a QR. You should typically use {@link createSVG} instead
* create SVG parts that make up a QR. You should typically use {@link createQrSvgString} instead
* @public
* @param {import('./types').QRConfig} config
* @param {import('./types').QRCode} [qr]
*/
export function createSVGParts(config, qr) {
export function createQrSvgParts(config, qr) {
const { data, margin, shape, logo, logoRatio, anchorInnerFill, anchorOuterFill, moduleFill, typeNumber, errorCorrectionLevel } = resolveConfig(config);
if (!qr) {
qr = QR(typeNumber, errorCorrectionLevel);
Expand Down Expand Up @@ -102,22 +102,26 @@ export function createSVGParts(config, qr) {
/**
* create QR as an SVG string
* @public
* @param {import('./types').QRConfig} config
* @param {import('./types').QRConfig & Partial<import('./types').SizeAttributes>} config
*/
export function createSVG(config) {
const { anchors, attributes, logo, modules } = createSVGParts(config);
return `<svg ${Object.entries(attributes)
export function createQrSvgString(config) {
const { anchors, attributes, logo, modules } = createQrSvgParts(config);
/** @type {typeof attributes & Partial<import('./types').SizeAttributes>} */
const rAttributes = { ...attributes };
if (config.width) rAttributes.width = config.width;
if (config.height) rAttributes.height = config.height;
return `<svg ${Object.entries(rAttributes)
.map(([name, value]) => `${name}="${value}"`)
.join(' ')}>${anchors} ${modules} ${logo}</svg>`;
}

/**
* create QR as a base64 data URL (image/svg+xml)
* @public
* @param {import('./types').QRConfig} config
* @param {import('./types').QRConfig & Partial<import('./types').SizeAttributes>} config
*/
export function createBase64Image(config) {
const svg = createSVG(config);
export function createQrSvgDataUrl(config) {
const svg = createQrSvgString(config);
const svg64 = btoa(svg);
const b64start = `data:image/svg+xml;base64,`;
const image64 = b64start + svg64;
Expand Down Expand Up @@ -179,3 +183,65 @@ function calculateLogoSize(logoSize, logoRatio) {
height: logoSize,
};
}

const DEFAULT_PNG_FILLS = {
moduleFill: 'black',
anchorOuterFill: 'black',
anchorInnerFill: 'black',
}

/**
* @typedef {import('./types').QRConfig & import('./types').SizeAttributes & { backgroundFill?: string }} CreateQrPngDataUrlConfig
*/

/**
* @param {CreateQrPngDataUrlConfig} config
*/
export async function createQrPngDataUrl(config) {
if (typeof document === 'undefined') {
throw new Error('Cannot use createQrPngDataUrl in a non-browser environment');
}

const width = config.width || 1000;
const height = config.height || 1000;

/** @type {CreateQrPngDataUrlConfig} */
const rConfig = {
...DEFAULT_PNG_FILLS,
...config,
width,
height,
};
const base64 = createQrSvgDataUrl(rConfig);

const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Cannot get 2d context from canvas');
}
/** @type {(value: string) => void} */
let _resolve;
/** @type {Promise<string>} */
const promise = new Promise((resolve) => {
_resolve = resolve;
});

const img = new Image(width, height);
img.addEventListener('load', () => {
// background
if (rConfig.backgroundFill) ctx.fillStyle = rConfig.backgroundFill;
ctx.fillRect(0, 0, canvas.width, canvas.height);

// draw QR
ctx.drawImage(img, 0, 0);

const pngDataUrl = canvas.toDataURL('image/png');
img.remove();
_resolve(pngDataUrl);
});
img.src = base64;

return promise;
}
8 changes: 8 additions & 0 deletions packages/qr/src/qr/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,11 @@ export type QRConfig = {
* @internal
*/
export type ResolvedQRConfig = ReturnType<typeof resolveConfig>;

/**
* @internal
*/
export type SizeAttributes = {
width: number;
height: number;
};
4 changes: 2 additions & 2 deletions packages/qr/src/svg/QR.svelte
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script>
import { createEventDispatcher, onMount } from 'svelte';
import { createSVGParts } from '../qr/index.js';
import { createQrSvgParts } from '../qr/index.js';
$: ({ data, anchorInnerFill, anchorOuterFill, logo: logoURL, logoRatio, margin, moduleFill, shape, errorCorrectionLevel, typeNumber, ...rest } = /** @type {import('../qr/types.js').QRConfig} */($$props));
$: ({ anchors, attributes, logo, modules } = createSVGParts({
$: ({ anchors, attributes, logo, modules } = createQrSvgParts({
data,
anchorInnerFill,
anchorOuterFill,
Expand Down
4 changes: 2 additions & 2 deletions packages/qr/src/svg/QR.svelte.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { SvelteComponent } from 'svelte';
import type { SVGAttributes } from 'svelte/elements';

import { createSVGParts } from '../qr';
import { createQrSvgParts } from '../qr';
import type { QRConfig } from '../qr/types';

export interface QRProps extends Omit<SVGAttributes<any>, 'viewBox'>, QRConfig {}
Expand All @@ -13,7 +13,7 @@ export interface QREvents {

export interface QRSlots {
default: {
attributes: ReturnType<typeof createSVGParts>['attributes'];
attributes: ReturnType<typeof createQrSvgParts>['attributes'];
innerHTML: string;
};
}
Expand Down
4 changes: 2 additions & 2 deletions packages/qr/src/svg/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createSVGParts } from '../qr';
import { createQrSvgParts } from '../qr';

/**
* Svelte action for rendering a QR as innerHTML of this SVGElement
Expand All @@ -9,7 +9,7 @@ import { createSVGParts } from '../qr';
*/
export function qr(node, param) {
async function init() {
const { anchors, attributes, logo, modules } = createSVGParts(param);
const { anchors, attributes, logo, modules } = createQrSvgParts(param);
for (const [name, value] of Object.entries(attributes)) {
node.setAttribute(name, value);
}
Expand Down
Loading

0 comments on commit 0c8dc62

Please sign in to comment.