Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | 3x 3x 3x 3x 3x 3x 19x 3x 12x 3x 9x 9x 9x 12x 1x 9x 9x 9x 8x 8x 8x 1x 7x 7x 2x 2x 5x 7x | import type { QRMatrix } from "../types";
import { clampInt } from "../types";
import { QRExportError } from "../errors";
import { type CanvasRenderOptions, drawToCanvas } from "./canvas";
import { loadImage } from "./logo";
import { resolveStyle } from "./style";
/** Options for {@link toPNG}. */
export interface PNGExportOptions extends CanvasRenderOptions {
/** Output width and height in pixels. @default 512 */
pixelSize?: number;
}
/** A rasterized QR code, as both a data URI and a `Blob`. */
export interface PNGExport {
dataURL: string;
blob: Blob;
}
function dataURLToBlob(dataURL: string): Blob {
const comma = dataURL.indexOf(",");
const header = dataURL.slice(0, comma);
const body = dataURL.slice(comma + 1);
const mime = /:(.*?);/.exec(header)?.[1] ?? "image/png";
const binary = atob(body);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return new Blob([bytes], { type: mime });
}
/**
* Render a matrix to a PNG.
*
* Loads the logo (if any) first, draws through the shared canvas renderer, and
* returns both a data URI and a `Blob` so the result is equally usable in an
* `<img>`, an upload, and a download link.
*
* A **cross-origin logo taints the canvas** and makes the export impossible.
* Rather than returning a blank image — the classic silent failure of this
* feature — that case throws a {@link QRExportError} naming the fix: serve the
* logo with CORS headers and set `logo.crossOrigin`, or inline it as a data
* URI.
*
* @example
* ```ts
* const { dataURL, blob } = await toPNG(encodeQR("https://usefy.dev"), { pixelSize: 1024 });
* ```
*/
export async function toPNG(
matrix: QRMatrix,
options: PNGExportOptions = {},
): Promise<PNGExport> {
if (typeof document === "undefined") {
throw new QRExportError("toPNG() requires a browser environment — use toSVG() on the server");
}
const style = resolveStyle(options);
// Runs through the same guard as every other numeric option, so a NaN
// exports at the documented default instead of silently at some other size.
const pixelSize = clampInt(options.pixelSize, 512, 16, 8192);
let logoImage: CanvasImageSource | null = options.logoImage ?? null;
if (!logoImage && style.logo) {
logoImage = await loadImage(style.logo.src, style.logo.crossOrigin);
}
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
if (!ctx) throw new QRExportError("Could not acquire a 2D canvas context");
drawToCanvas(ctx, matrix, { ...options, size: pixelSize, dpr: 1, logoImage });
let dataURL: string;
try {
dataURL = canvas.toDataURL("image/png");
} catch (cause) {
throw new QRExportError(
"Could not export the PNG: the canvas is tainted, which happens when the logo is " +
"loaded cross-origin. Serve it with CORS headers and set logo.crossOrigin, or " +
"embed the logo as a data URI.",
cause,
);
}
const blob = await new Promise<Blob | null>((resolve) => {
if (typeof canvas.toBlob !== "function") {
resolve(null);
return;
}
canvas.toBlob((result) => resolve(result), "image/png");
});
return { dataURL, blob: blob ?? dataURLToBlob(dataURL) };
}
|