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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | 5x 24x 22x 22x 22x 2x 1x 1x 1x 1x 1x 1x 1x 31x 2x 29x 24x 24x 24x 28x 28x 28x 28x 91x 58x 24x 14x 10x 2x 8x 8x 58x 31x 7x 24x 24x 7x 7x 7x 7x 7x | import { QRUnsupportedError } from "../errors";
import type { ImageSource } from "../types";
/**
* Normalizing every accepted input into `ImageData`.
*
* This is the only file in the decode path that touches the DOM, which is what
* keeps everything below it usable in a worker, in a test, and on a server.
* The rest of the pipeline never learns whether its pixels came from a camera
* frame, a dropped screenshot or a hand-built array.
*/
/** A canvas that can be reused across frames rather than reallocated. */
interface ScratchCanvas {
readonly canvas: HTMLCanvasElement | OffscreenCanvas;
readonly context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
}
let scratch: ScratchCanvas | null = null;
function createCanvas(width: number, height: number): ScratchCanvas {
if (typeof OffscreenCanvas !== "undefined") {
const canvas = new OffscreenCanvas(width, height);
const context = canvas.getContext("2d", { willReadFrequently: true });
Eif (context) return { canvas, context };
}
if (typeof document === "undefined") {
throw new QRUnsupportedError(
"Reading pixels needs a canvas: pass an ImageData directly, or run where " +
"OffscreenCanvas or the DOM is available.",
);
}
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d", { willReadFrequently: true });
/* c8 ignore next 6 -- a 2D context is unavailable only when canvas support is
disabled outright; jsdom without a canvas package hits the OffscreenCanvas
branch above or throws there first. */
Iif (!context) {
throw new QRUnsupportedError(
"This browser refused a 2D canvas context, so pixels cannot be read from " +
"the supplied source.",
);
}
return { canvas, context };
}
/**
* Draw a drawable source and read its pixels back.
*
* The scratch canvas is module-level and reused: at 12 frames a second, a new
* canvas per frame is 12 GPU allocations a second, and browsers punish that
* with visible stutter.
*/
function readPixels(
source: CanvasImageSource,
width: number,
height: number,
): ImageData {
if (width < 1 || height < 1) {
throw new QRUnsupportedError(
"The image source has no pixels yet — a video that has not loaded a frame, " +
"or an image that has not decoded.",
);
}
if (!scratch || scratch.canvas.width !== width || scratch.canvas.height !== height) {
scratch = createCanvas(width, height);
scratch.canvas.width = width;
scratch.canvas.height = height;
}
const { context } = scratch;
context.clearRect(0, 0, width, height);
context.drawImage(source as CanvasImageSource, 0, 0, width, height);
return context.getImageData(0, 0, width, height);
}
/** Release the shared scratch canvas — for tests, and for a scanner shutting down. */
export function releaseScratchCanvas(): void {
scratch = null;
}
function isImageData(source: ImageSource): source is ImageData {
return (
typeof source === "object" &&
source !== null &&
"data" in source &&
"width" in source &&
"height" in source
);
}
/**
* The pixel dimensions of a source, preferring its *intrinsic* size.
*
* A `<video>` element's CSS size has nothing to do with the frame it is
* showing, and reading pixels at the CSS size would silently downscale (or
* upscale) every frame — a `videoWidth` of 1280 rendered into a 320-wide
* canvas throws away exactly the detail a scanner needs.
*/
function dimensionsOf(source: ImageSource): { width: number; height: number } {
if (typeof HTMLVideoElement !== "undefined" && source instanceof HTMLVideoElement) {
return { width: source.videoWidth, height: source.videoHeight };
}
if (typeof HTMLImageElement !== "undefined" && source instanceof HTMLImageElement) {
return { width: source.naturalWidth || source.width, height: source.naturalHeight || source.height };
}
const sized = source as { width: number; height: number };
return { width: sized.width, height: sized.height };
}
/**
* Turn any accepted source into `ImageData`.
*
* @throws {QRUnsupportedError} when the environment cannot read pixels from the
* source, or the source has no pixels yet.
*/
export async function toImageData(source: ImageSource): Promise<ImageData> {
if (isImageData(source)) return source;
if (typeof Blob !== "undefined" && source instanceof Blob) {
return fromBlob(source);
}
const { width, height } = dimensionsOf(source);
return readPixels(source as CanvasImageSource, width, height);
}
/**
* Decode a blob (a `File` from an `<input>`, a paste, or a drop) into pixels.
*
* `createImageBitmap` is preferred — it decodes off the main thread and works
* in a worker. The `<img>` fallback covers Safari versions that reject blobs
* there, and is the reason this function is async at all.
*/
async function fromBlob(blob: Blob): Promise<ImageData> {
Eif (typeof createImageBitmap === "function") {
const bitmap = await createImageBitmap(blob);
try {
return readPixels(bitmap, bitmap.width, bitmap.height);
} finally {
// Bitmaps hold GPU memory until closed; a scanner reading a burst of
// dropped files would otherwise pile up several megabytes each.
bitmap.close();
}
}
/* c8 ignore start -- the `<img>` fallback needs a real image decoder, which
jsdom does not have; it is exercised in the browser QA pass instead. */
if (typeof document === "undefined" || typeof URL === "undefined") {
throw new QRUnsupportedError("This environment cannot decode an image blob.");
}
const url = URL.createObjectURL(blob);
try {
const image = await new Promise<HTMLImageElement>((resolve, reject) => {
const element = new Image();
element.onload = () => resolve(element);
element.onerror = () =>
reject(new QRUnsupportedError("The supplied file is not an image this browser can decode."));
element.src = url;
});
return readPixels(image, image.naturalWidth, image.naturalHeight);
} finally {
URL.revokeObjectURL(url);
}
/* c8 ignore stop */
}
|