forked from exetico/qr-scanner
-
Notifications
You must be signed in to change notification settings - Fork 0
/
qr-scanner.legacy.min.js.map
1 lines (1 loc) · 53.2 KB
/
qr-scanner.legacy.min.js.map
1
{"version":3,"file":"qr-scanner.legacy.min.js","sources":["src/qr-scanner.ts"],"sourcesContent":["export class QrScanner {\r\n static readonly DEFAULT_CANVAS_SIZE = 400;\r\n static readonly NO_QR_CODE_FOUND = 'No QR code found';\r\n private static _disableBarcodeDetector = false;\r\n private static _workerMessageId = 0;\r\n\r\n /** @deprecated */\r\n static set WORKER_PATH(workerPath: string) {\r\n console.warn('Setting QrScanner.WORKER_PATH is not required and not supported anymore. '\r\n + 'Have a look at the README for new setup instructions.');\r\n }\r\n\r\n static async hasCamera(): Promise<boolean> {\r\n try {\r\n return !!(await QrScanner.listCameras(false)).length;\r\n } catch (e) {\r\n return false;\r\n }\r\n }\r\n\r\n static async listCameras(requestLabels = false): Promise<Array<Camera>> {\r\n if (!navigator.mediaDevices) return [];\r\n\r\n const enumerateCameras = async (): Promise<Array<MediaDeviceInfo>> =>\r\n (await navigator.mediaDevices.enumerateDevices()).filter((device) => device.kind === 'videoinput');\r\n\r\n // Note that enumerateDevices can always be called and does not prompt the user for permission.\r\n // However, enumerateDevices only includes device labels if served via https and an active media stream exists\r\n // or permission to access the camera was given. Therefore, if we're not getting labels but labels are requested\r\n // ask for camera permission by opening a stream.\r\n let openedStream: MediaStream | undefined;\r\n try {\r\n if (requestLabels && (await enumerateCameras()).every((camera) => !camera.label)) {\r\n openedStream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true });\r\n }\r\n } catch (e) {\r\n // Fail gracefully, especially if the device has no camera or on mobile when the camera is already in use\r\n // and some browsers disallow a second stream.\r\n }\r\n\r\n try {\r\n return (await enumerateCameras()).map((camera, i) => ({\r\n id: camera.deviceId,\r\n label: camera.label || (i === 0 ? 'Default Camera' : `Camera ${i + 1}`),\r\n }));\r\n } finally {\r\n // close the stream we just opened for getting camera access for listing the device labels\r\n if (openedStream) {\r\n console.warn('Call listCameras after successfully starting a QR scanner to avoid creating '\r\n + 'a temporary video stream');\r\n QrScanner._stopVideoStream(openedStream);\r\n }\r\n }\r\n }\r\n\r\n readonly $video: HTMLVideoElement;\r\n readonly $canvas: HTMLCanvasElement;\r\n readonly $overlay?: HTMLDivElement;\r\n private readonly $codeOutlineHighlight?: SVGSVGElement;\r\n private readonly _onDecode?: (result: ScanResult) => void;\r\n private readonly _legacyOnDecode?: (result: string) => void;\r\n private readonly _legacyCanvasSize: number = QrScanner.DEFAULT_CANVAS_SIZE;\r\n private _preferredCamera: FacingMode | DeviceId = 'environment';\r\n private readonly _maxScansPerSecond: number = 25;\r\n private _lastScanTimestamp: number = -1;\r\n private _scanRegion: ScanRegion;\r\n private _codeOutlineHighlightRemovalTimeout?: number;\r\n private _qrEnginePromise: Promise<Worker | BarcodeDetector>\r\n private _active: boolean = false;\r\n private _paused: boolean = false;\r\n private _flashOn: boolean = false;\r\n private _destroyed: boolean = false;\r\n\r\n constructor(\r\n video: HTMLVideoElement,\r\n onDecode: (result: ScanResult) => void,\r\n options: {\r\n onDecodeError?: (error: Error | string) => void,\r\n calculateScanRegion?: (video: HTMLVideoElement) => ScanRegion,\r\n preferredCamera?: FacingMode | DeviceId,\r\n maxScansPerSecond?: number;\r\n highlightScanRegion?: boolean,\r\n highlightCodeOutline?: boolean,\r\n overlay?: HTMLDivElement,\r\n /** just a temporary flag until we switch entirely to the new api */\r\n returnDetailedScanResult?: boolean,\r\n domTarget?: HTMLDivElement | ShadowRoot | null,\r\n },\r\n ) {\r\n this.$video = video;\r\n this.$canvas = document.createElement('canvas');\r\n this._onDecode = onDecode;\r\n if (options.onDecodeError) this._onDecodeError = options.onDecodeError;\r\n if (options.calculateScanRegion) this._calculateScanRegion = options.calculateScanRegion;\r\n this._preferredCamera = options.preferredCamera || this._preferredCamera; \r\n this._maxScansPerSecond = options.maxScansPerSecond || this._maxScansPerSecond;\r\n\r\n this._onPlay = this._onPlay.bind(this);\r\n this._onLoadedMetaData = this._onLoadedMetaData.bind(this);\r\n this._onVisibilityChange = this._onVisibilityChange.bind(this);\r\n this._updateOverlay = this._updateOverlay.bind(this);\r\n\r\n // @ts-ignore\r\n this.$video.disablePictureInPicture = true;\r\n // Allow inline playback on iPhone instead of requiring full screen playback,\r\n // see https://webkit.org/blog/6784/new-video-policies-for-ios/\r\n // @ts-ignore\r\n this.$video.playsInline = true;\r\n // Allow play() on iPhone without requiring a user gesture. Should not really be needed as camera stream\r\n // includes no audio, but just to be safe.\r\n this.$video.muted = true;\r\n\r\n // Avoid Safari stopping the video stream on a hidden video.\r\n // See https://github.com/cozmo/jsQR/issues/185\r\n let shouldHideVideo = false;\r\n if (this.$video.hidden) {\r\n this.$video.hidden = false;\r\n shouldHideVideo = true;\r\n }\r\n\r\n const domTarget = options.domTarget || document.body;\r\n if (!domTarget.contains(this.$video)) {\r\n domTarget.appendChild(this.$video);\r\n shouldHideVideo = true;\r\n }\r\n const videoContainer = this.$video.parentElement!;\r\n\r\n if (options.highlightScanRegion || options.highlightCodeOutline) {\r\n const gotExternalOverlay = !!options.overlay;\r\n this.$overlay = options.overlay || document.createElement('div');\r\n const overlayStyle = this.$overlay.style;\r\n overlayStyle.position = 'absolute';\r\n overlayStyle.display = 'none';\r\n overlayStyle.pointerEvents = 'none';\r\n this.$overlay.classList.add('scan-region-highlight');\r\n if (!gotExternalOverlay && options.highlightScanRegion) {\r\n // default style; can be overwritten via css, e.g. by changing the svg's stroke color, hiding the\r\n // .scan-region-highlight-svg, setting a border, outline, background, etc.\r\n this.$overlay.innerHTML = '<svg class=\"scan-region-highlight-svg\" viewBox=\"0 0 238 238\" '\r\n + 'preserveAspectRatio=\"none\" style=\"position:absolute;width:100%;height:100%;left:0;top:0;'\r\n + 'fill:none;stroke:#e9b213;stroke-width:4;stroke-linecap:round;stroke-linejoin:round\">'\r\n + '<path d=\"M31 2H10a8 8 0 0 0-8 8v21M207 2h21a8 8 0 0 1 8 8v21m0 176v21a8 8 0 0 1-8 8h-21m-176 '\r\n + '0H10a8 8 0 0 1-8-8v-21\"/></svg>';\r\n try {\r\n this.$overlay.firstElementChild!.animate({ transform: ['scale(.98)', 'scale(1.01)'] }, {\r\n duration: 400,\r\n iterations: Infinity,\r\n direction: 'alternate',\r\n easing: 'ease-in-out',\r\n });\r\n } catch (e) {}\r\n videoContainer.insertBefore(this.$overlay, this.$video.nextSibling);\r\n //this.$video.appendChild(this.$overlay);\r\n }\r\n if (options.highlightCodeOutline) {\r\n // default style; can be overwritten via css\r\n this.$overlay.insertAdjacentHTML(\r\n 'beforeend',\r\n '<svg class=\"code-outline-highlight\" preserveAspectRatio=\"none\" style=\"display:none;width:100%;'\r\n + 'height:100%;fill:none;stroke:#e9b213;stroke-width:5;stroke-dasharray:25;'\r\n + 'stroke-linecap:round;stroke-linejoin:round\"><polygon/></svg>',\r\n );\r\n this.$codeOutlineHighlight = this.$overlay.lastElementChild as SVGSVGElement;\r\n }\r\n }\r\n this._scanRegion = this._calculateScanRegion(video);\r\n\r\n requestAnimationFrame(() => {\r\n // Checking in requestAnimationFrame which should avoid a potential additional re-flow for getComputedStyle.\r\n const videoStyle = window.getComputedStyle(this.$video); \r\n //const videoStyle = video.style; \r\n if (videoStyle.display === 'none') {\r\n this.$video.style.setProperty('display', 'block', 'important');\r\n shouldHideVideo = true;\r\n }\r\n \r\n if (videoStyle.visibility !== 'visible') {\r\n this.$video.style.setProperty('visibility', 'visible', 'important');\r\n shouldHideVideo = true;\r\n }\r\n if (shouldHideVideo) {\r\n // Hide the video in a way that doesn't cause Safari to stop the playback.\r\n console.warn('QrScanner has overwritten the video hiding style to avoid Safari stopping the playback.');\r\n this.$video.style.opacity = '0';\r\n this.$video.style.width = '0';\r\n this.$video.style.height = '0';\r\n if (this.$overlay && this.$overlay.parentElement) {\r\n this.$overlay.parentElement.removeChild(this.$overlay);\r\n }\r\n // @ts-ignore\r\n delete this.$overlay!;\r\n // @ts-ignore\r\n delete this.$codeOutlineHighlight!;\r\n }\r\n\r\n if (this.$overlay) {\r\n this._updateOverlay();\r\n }\r\n });\r\n\r\n video.addEventListener('play', this._onPlay);\r\n video.addEventListener('loadedmetadata', this._onLoadedMetaData);\r\n document.addEventListener('visibilitychange', this._onVisibilityChange);\r\n window.addEventListener('resize', this._updateOverlay);\r\n\r\n this._qrEnginePromise = QrScanner.createQrEngine();\r\n }\r\n\r\n async hasFlash(): Promise<boolean> {\r\n let stream: MediaStream | undefined;\r\n try {\r\n if (this.$video.srcObject) {\r\n if (!(this.$video.srcObject instanceof MediaStream)) return false; // srcObject is not a camera stream\r\n stream = this.$video.srcObject;\r\n } else {\r\n stream = (await this._getCameraStream()).stream;\r\n }\r\n return 'torch' in stream.getVideoTracks()[0].getSettings();\r\n } catch (e) {\r\n return false;\r\n } finally {\r\n // close the stream we just opened for detecting whether it supports flash\r\n if (stream && stream !== this.$video.srcObject) {\r\n console.warn('Call hasFlash after successfully starting the scanner to avoid creating '\r\n + 'a temporary video stream');\r\n QrScanner._stopVideoStream(stream);\r\n }\r\n }\r\n }\r\n\r\n isFlashOn(): boolean {\r\n return this._flashOn;\r\n }\r\n\r\n async toggleFlash(): Promise<void> {\r\n if (this._flashOn) {\r\n await this.turnFlashOff();\r\n } else {\r\n await this.turnFlashOn();\r\n }\r\n }\r\n\r\n async turnFlashOn(): Promise<void> {\r\n if (this._flashOn || this._destroyed) return;\r\n this._flashOn = true;\r\n if (!this._active || this._paused) return; // flash will be turned on later on .start()\r\n try {\r\n if (!await this.hasFlash()) throw 'No flash available';\r\n // Note that the video track is guaranteed to exist and to be a MediaStream due to the check in hasFlash\r\n await (this.$video.srcObject as MediaStream).getVideoTracks()[0].applyConstraints({\r\n // @ts-ignore: constraint 'torch' is unknown to ts\r\n advanced: [{ torch: true }],\r\n });\r\n } catch (e) {\r\n this._flashOn = false;\r\n throw e;\r\n }\r\n }\r\n\r\n async turnFlashOff(): Promise<void> {\r\n if (!this._flashOn) return;\r\n // applyConstraints with torch: false does not work to turn the flashlight off, as a stream's torch stays\r\n // continuously on, see https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#torch. Therefore,\r\n // we have to stop the stream to turn the flashlight off.\r\n this._flashOn = false;\r\n await this._restartVideoStream();\r\n }\r\n\r\n destroy(): void {\r\n this.$video.removeEventListener('loadedmetadata', this._onLoadedMetaData);\r\n this.$video.removeEventListener('play', this._onPlay);\r\n document.removeEventListener('visibilitychange', this._onVisibilityChange);\r\n window.removeEventListener('resize', this._updateOverlay);\r\n\r\n this._destroyed = true;\r\n this._flashOn = false;\r\n this.stop(); // sets this._paused = true and this._active = false\r\n QrScanner._postWorkerMessage(this._qrEnginePromise, 'close');\r\n }\r\n\r\n async start(): Promise<void> {\r\n if (this._destroyed) throw new Error('The QR scanner can not be started as it had been destroyed.');\r\n if (this._active && !this._paused) return;\r\n\r\n if (window.location.protocol !== 'https:') {\r\n // warn but try starting the camera anyways\r\n console.warn('The camera stream is only accessible if the page is transferred via https.');\r\n }\r\n\r\n this._active = true;\r\n if (document.hidden) return; // camera will be started as soon as tab is in foreground\r\n this._paused = false;\r\n if (this.$video.srcObject) {\r\n // camera stream already/still set\r\n await this.$video.play();\r\n return;\r\n }\r\n\r\n try {\r\n const { stream, facingMode } = await this._getCameraStream();\r\n if (!this._active || this._paused) {\r\n // was stopped in the meantime\r\n QrScanner._stopVideoStream(stream);\r\n return;\r\n }\r\n this._setVideoMirror(facingMode);\r\n this.$video.srcObject = stream;\r\n await this.$video.play();\r\n\r\n // Restart the flash if it was previously on\r\n if (this._flashOn) {\r\n this._flashOn = false; // force turnFlashOn to restart the flash\r\n this.turnFlashOn().catch(() => {});\r\n }\r\n } catch (e) {\r\n if (this._paused) return;\r\n this._active = false;\r\n throw e;\r\n }\r\n }\r\n\r\n stop(): void {\r\n this.pause();\r\n this._active = false;\r\n }\r\n\r\n async pause(stopStreamImmediately = false): Promise<boolean> {\r\n this._paused = true;\r\n if (!this._active) return true;\r\n this.$video.pause();\r\n\r\n if (this.$overlay) {\r\n this.$overlay.style.display = 'none';\r\n }\r\n\r\n const stopStream = () => {\r\n if (this.$video.srcObject instanceof MediaStream) {\r\n // revoke srcObject only if it's a stream which was likely set by us\r\n QrScanner._stopVideoStream(this.$video.srcObject);\r\n this.$video.srcObject = null;\r\n }\r\n };\r\n\r\n if (stopStreamImmediately) {\r\n stopStream();\r\n return true;\r\n }\r\n\r\n await new Promise((resolve) => setTimeout(resolve, 300));\r\n if (!this._paused) return false;\r\n stopStream();\r\n return true;\r\n }\r\n\r\n async setCamera(facingModeOrDeviceId: FacingMode | DeviceId): Promise<void> {\r\n if (facingModeOrDeviceId === this._preferredCamera) return;\r\n this._preferredCamera = facingModeOrDeviceId;\r\n // Restart the scanner with the new camera which will also update the video mirror and the scan region.\r\n await this._restartVideoStream();\r\n }\r\n\r\n static async scanImage(\r\n imageOrFileOrBlobOrUrl: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\r\n | SVGImageElement | File | Blob | URL | String,\r\n options: {\r\n scanRegion?: ScanRegion | null,\r\n qrEngine?: Worker | BarcodeDetector | Promise<Worker | BarcodeDetector> | null,\r\n canvas?: HTMLCanvasElement | null,\r\n disallowCanvasResizing?: boolean,\r\n alsoTryWithoutScanRegion?: boolean,\r\n /** just a temporary flag until we switch entirely to the new api */\r\n returnDetailedScanResult?: boolean,\r\n },\r\n ): Promise<ScanResult>;\r\n /** @deprecated */\r\n static async scanImage(\r\n imageOrFileOrBlobOrUrl: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\r\n | SVGImageElement | File | Blob | URL | String,\r\n scanRegion?: ScanRegion | null,\r\n qrEngine?: Worker | BarcodeDetector | Promise<Worker | BarcodeDetector> | null,\r\n canvas?: HTMLCanvasElement | null,\r\n disallowCanvasResizing?: boolean,\r\n alsoTryWithoutScanRegion?: boolean,\r\n ): Promise<string>;\r\n static async scanImage(\r\n imageOrFileOrBlobOrUrl: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\r\n | SVGImageElement | File | Blob | URL | String,\r\n scanRegionOrOptions?: ScanRegion | {\r\n scanRegion?: ScanRegion | null,\r\n qrEngine?: Worker | BarcodeDetector | Promise<Worker | BarcodeDetector> | null,\r\n canvas?: HTMLCanvasElement | null,\r\n disallowCanvasResizing?: boolean,\r\n alsoTryWithoutScanRegion?: boolean,\r\n /** just a temporary flag until we switch entirely to the new api */\r\n returnDetailedScanResult?: boolean,\r\n } | null,\r\n qrEngine?: Worker | BarcodeDetector | Promise<Worker | BarcodeDetector> | null,\r\n canvas?: HTMLCanvasElement | null,\r\n disallowCanvasResizing: boolean = false,\r\n alsoTryWithoutScanRegion: boolean = false,\r\n ): Promise<string | ScanResult> {\r\n let scanRegion: ScanRegion | null | undefined;\r\n let returnDetailedScanResult = false;\r\n if (scanRegionOrOptions && (\r\n 'scanRegion' in scanRegionOrOptions\r\n || 'qrEngine' in scanRegionOrOptions\r\n || 'canvas' in scanRegionOrOptions\r\n || 'disallowCanvasResizing' in scanRegionOrOptions\r\n || 'alsoTryWithoutScanRegion' in scanRegionOrOptions\r\n || 'returnDetailedScanResult' in scanRegionOrOptions\r\n )) {\r\n // we got an options object using the new api\r\n scanRegion = scanRegionOrOptions.scanRegion;\r\n qrEngine = scanRegionOrOptions.qrEngine;\r\n canvas = scanRegionOrOptions.canvas;\r\n disallowCanvasResizing = scanRegionOrOptions.disallowCanvasResizing || false;\r\n alsoTryWithoutScanRegion = scanRegionOrOptions.alsoTryWithoutScanRegion || false;\r\n returnDetailedScanResult = true;\r\n } else if (scanRegionOrOptions || qrEngine || canvas || disallowCanvasResizing || alsoTryWithoutScanRegion) {\r\n console.warn('You\\'re using a deprecated api for scanImage which will be removed in the future.');\r\n } else {\r\n // Only imageOrFileOrBlobOrUrl was specified and we can't distinguish between new or old api usage. For\r\n // backwards compatibility we have to assume the old api for now. The options object is marked as non-\r\n // optional in the parameter list above to make clear that ScanResult instead of string is only returned if\r\n // an options object was provided. However, in the future once legacy support is removed, the options object\r\n // should become optional.\r\n console.warn('Note that the return type of scanImage will change in the future. To already switch to the '\r\n + 'new api today, you can pass returnDetailedScanResult: true.');\r\n }\r\n\r\n const gotExternalEngine = !!qrEngine;\r\n\r\n try {\r\n let image: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\r\n | SVGImageElement;\r\n let canvasContext: CanvasRenderingContext2D;\r\n [qrEngine, image] = await Promise.all([\r\n qrEngine || QrScanner.createQrEngine(),\r\n QrScanner._loadImage(imageOrFileOrBlobOrUrl),\r\n ]);\r\n [canvas, canvasContext] = QrScanner._drawToCanvas(image, scanRegion, canvas, disallowCanvasResizing);\r\n let detailedScanResult: ScanResult;\r\n\r\n if (qrEngine instanceof Worker) {\r\n const qrEngineWorker = qrEngine; // for ts to know that it's still a worker later in the event listeners\r\n if (!gotExternalEngine) {\r\n // Enable scanning of inverted color qr codes.\r\n QrScanner._postWorkerMessageSync(qrEngineWorker, 'inversionMode', [], 'both');\r\n }\r\n detailedScanResult = await new Promise((resolve, reject) => {\r\n let timeout: number;\r\n let onMessage: (event: MessageEvent) => void;\r\n let onError: (error: ErrorEvent | string) => void;\r\n let expectedResponseId = -1;\r\n onMessage = (event: MessageEvent) => {\r\n if (event.data.id !== expectedResponseId) {\r\n return;\r\n }\r\n qrEngineWorker.removeEventListener('message', onMessage);\r\n qrEngineWorker.removeEventListener('error', onError);\r\n clearTimeout(timeout);\r\n if (event.data.data !== null) {\r\n resolve({\r\n data: event.data.data,\r\n cornerPoints: QrScanner._convertPoints(event.data.cornerPoints, scanRegion),\r\n });\r\n } else {\r\n reject(QrScanner.NO_QR_CODE_FOUND);\r\n }\r\n };\r\n onError = (error: ErrorEvent | string) => {\r\n qrEngineWorker.removeEventListener('message', onMessage);\r\n qrEngineWorker.removeEventListener('error', onError);\r\n clearTimeout(timeout);\r\n const errorMessage = !error ? 'Unknown Error' : ((error as ErrorEvent).message || error);\r\n reject('Scanner error: ' + errorMessage);\r\n };\r\n qrEngineWorker.addEventListener('message', onMessage);\r\n qrEngineWorker.addEventListener('error', onError);\r\n timeout = setTimeout(() => onError('timeout'), 10000);\r\n const imageData = canvasContext.getImageData(0, 0, canvas!.width, canvas!.height);\r\n expectedResponseId = QrScanner._postWorkerMessageSync(\r\n qrEngineWorker,\r\n 'decode',\r\n [imageData.data.buffer],\r\n imageData, \r\n );\r\n });\r\n } else {\r\n detailedScanResult = await Promise.race([\r\n new Promise<ScanResult>((resolve, reject) => window.setTimeout(\r\n () => reject('Scanner error: timeout'),\r\n 10000,\r\n )),\r\n (async (): Promise<ScanResult> => {\r\n try {\r\n const [scanResult] = await qrEngine.detect(canvas!);\r\n if (!scanResult) throw QrScanner.NO_QR_CODE_FOUND;\r\n return {\r\n data: scanResult.rawValue,\r\n cornerPoints: QrScanner._convertPoints(scanResult.cornerPoints, scanRegion),\r\n };\r\n } catch (e) {\r\n const errorMessage = (e as Error).message || e as string;\r\n if (/not implemented|service unavailable/.test(errorMessage)) {\r\n // Not implemented can apparently for some reason happen even though getSupportedFormats\r\n // in createQrScanner reported that it's supported, see issue #98.\r\n // Service unavailable can happen after some time when the BarcodeDetector crashed and\r\n // can theoretically be recovered from by creating a new BarcodeDetector. However, in\r\n // newer browsers this issue does not seem to be present anymore and therefore we do not\r\n // apply this optimization anymore but just set _disableBarcodeDetector in both cases.\r\n // Also note that if we got an external qrEngine that crashed, we should possibly notify\r\n // the caller about it, but we also don't do this here, as it's such an unlikely case.\r\n QrScanner._disableBarcodeDetector = true;\r\n // retry without passing the broken BarcodeScanner instance\r\n return QrScanner.scanImage(imageOrFileOrBlobOrUrl, {\r\n scanRegion,\r\n canvas,\r\n disallowCanvasResizing,\r\n alsoTryWithoutScanRegion,\r\n });\r\n }\r\n throw `Scanner error: ${errorMessage}`;\r\n }\r\n })(),\r\n ]);\r\n }\r\n return returnDetailedScanResult ? detailedScanResult : detailedScanResult.data;\r\n } catch (e) {\r\n if (!scanRegion || !alsoTryWithoutScanRegion) throw e;\r\n const detailedScanResult = await QrScanner.scanImage(\r\n imageOrFileOrBlobOrUrl,\r\n { qrEngine, canvas, disallowCanvasResizing },\r\n );\r\n return returnDetailedScanResult ? detailedScanResult : detailedScanResult.data;\r\n } finally {\r\n if (!gotExternalEngine) {\r\n QrScanner._postWorkerMessage(qrEngine!, 'close');\r\n }\r\n }\r\n }\r\n\r\n setGrayscaleWeights(red: number, green: number, blue: number, useIntegerApproximation: boolean = true): void {\r\n // Note that for the native BarcodeDecoder or if the worker was destroyed, this is a no-op. However, the native\r\n // implementations work also well with colored qr codes.\r\n QrScanner._postWorkerMessage(\r\n this._qrEnginePromise,\r\n 'grayscaleWeights',\r\n { red, green, blue, useIntegerApproximation }\r\n );\r\n }\r\n\r\n setInversionMode(inversionMode: InversionMode): void {\r\n // Note that for the native BarcodeDecoder or if the worker was destroyed, this is a no-op. However, the native\r\n // implementations scan normal and inverted qr codes by default\r\n QrScanner._postWorkerMessage(this._qrEnginePromise, 'inversionMode', inversionMode);\r\n }\r\n\r\n static async createQrEngine(): Promise<Worker | BarcodeDetector>;\r\n /** @deprecated */\r\n static async createQrEngine(workerPath: string): Promise<Worker | BarcodeDetector>;\r\n static async createQrEngine(workerPath?: string): Promise<Worker | BarcodeDetector> {\r\n if (workerPath) {\r\n console.warn('Specifying a worker path is not required and not supported anymore.');\r\n }\r\n\r\n // @ts-ignore no types defined for import\r\n const createWorker = () => (import('./qr-scanner-worker.min.js') as Promise<{ createWorker: () => Worker }>)\r\n .then((module) => module.createWorker());\r\n\r\n const useBarcodeDetector = !QrScanner._disableBarcodeDetector\r\n && 'BarcodeDetector' in window\r\n && BarcodeDetector.getSupportedFormats\r\n && (await BarcodeDetector.getSupportedFormats()).includes('qr_code');\r\n\r\n if (!useBarcodeDetector) return createWorker();\r\n\r\n // On Macs with an M1/M2 processor and macOS Ventura (macOS version 13), the BarcodeDetector is broken in\r\n // Chromium based browsers, regardless of the version. For that constellation, the BarcodeDetector does not\r\n // error but does not detect QR codes. Macs without an M1/M2 or before Ventura are fine.\r\n // See issue #209 and https://bugs.chromium.org/p/chromium/issues/detail?id=1382442\r\n // TODO update this once the issue in macOS is fixed\r\n const userAgentData = navigator.userAgentData;\r\n const isChromiumOnMacWithArmVentura = userAgentData // all Chromium browsers support userAgentData\r\n && userAgentData.brands.some(({ brand }) => /Chromium/i.test(brand))\r\n && /mac ?OS/i.test(userAgentData.platform)\r\n // Does it have an ARM chip (e.g. M1/M2) and Ventura? Check this last as getHighEntropyValues can\r\n // theoretically trigger a browser prompt, although no browser currently does seem to show one.\r\n // If browser or user refused to return the requested values, assume broken ARM Ventura, to be safe.\r\n && await userAgentData.getHighEntropyValues(['architecture', 'platformVersion'])\r\n .then(({ architecture, platformVersion }) =>\r\n /arm/i.test(architecture || 'arm') && parseInt(platformVersion || '13') >= /* Ventura */ 13)\r\n .catch(() => true);\r\n if (isChromiumOnMacWithArmVentura) return createWorker();\r\n\r\n return new BarcodeDetector({ formats: ['qr_code'] });\r\n }\r\n\r\n private _onPlay(): void {\r\n this._scanRegion = this._calculateScanRegion(this.$video);\r\n this._updateOverlay();\r\n if (this.$overlay) {\r\n this.$overlay.style.display = '';\r\n }\r\n this._scanFrame();\r\n }\r\n\r\n private _onLoadedMetaData(): void {\r\n this._scanRegion = this._calculateScanRegion(this.$video);\r\n this._updateOverlay();\r\n }\r\n\r\n private _onVisibilityChange(): void {\r\n if (document.hidden) {\r\n this.pause();\r\n } else if (this._active) {\r\n this.start();\r\n }\r\n }\r\n\r\n private _calculateScanRegion(video: HTMLVideoElement): ScanRegion {\r\n // Default scan region calculation. Note that this can be overwritten in the constructor.\r\n const smallestDimension = Math.min(video.videoWidth, video.videoHeight);\r\n const scanRegionSize = Math.round(2 / 3 * smallestDimension);\r\n return {\r\n x: Math.round((video.videoWidth - scanRegionSize) / 2),\r\n y: Math.round((video.videoHeight - scanRegionSize) / 2),\r\n width: scanRegionSize,\r\n height: scanRegionSize,\r\n downScaledWidth: this._legacyCanvasSize,\r\n downScaledHeight: this._legacyCanvasSize,\r\n };\r\n }\r\n\r\n private _updateOverlay(): void {\r\n requestAnimationFrame(() => {\r\n // Running in requestAnimationFrame which should avoid a potential additional re-flow for getComputedStyle\r\n // and offsetWidth, offsetHeight, offsetLeft, offsetTop.\r\n if (!this.$overlay) return;\r\n const video = this.$video;\r\n const videoWidth = video.videoWidth;\r\n const videoHeight = video.videoHeight;\r\n const elementWidth = video.offsetWidth;\r\n const elementHeight = video.offsetHeight;\r\n const elementX = video.offsetLeft;\r\n const elementY = video.offsetTop;\r\n\r\n const videoStyle = window.getComputedStyle(this.$video); \r\n //const videoStyle = video.style;\r\n const videoObjectFit = videoStyle.objectFit;\r\n const videoAspectRatio = videoWidth / videoHeight;\r\n const elementAspectRatio = elementWidth / elementHeight;\r\n let videoScaledWidth: number;\r\n let videoScaledHeight: number;\r\n switch (videoObjectFit) {\r\n case 'none':\r\n videoScaledWidth = videoWidth;\r\n videoScaledHeight = videoHeight;\r\n break;\r\n case 'fill':\r\n videoScaledWidth = elementWidth;\r\n videoScaledHeight = elementHeight;\r\n break;\r\n default: // 'cover', 'contains', 'scale-down'\r\n if (videoObjectFit === 'cover'\r\n ? videoAspectRatio > elementAspectRatio\r\n : videoAspectRatio < elementAspectRatio) {\r\n // The scaled height is the element height\r\n // - for 'cover' if the video aspect ratio is wider than the element aspect ratio\r\n // (scaled height matches element height and scaled width overflows element width)\r\n // - for 'contains'/'scale-down' if element aspect ratio is wider than the video aspect ratio\r\n // (scaled height matched element height and element width overflows scaled width)\r\n videoScaledHeight = elementHeight;\r\n videoScaledWidth = videoScaledHeight * videoAspectRatio;\r\n } else {\r\n videoScaledWidth = elementWidth;\r\n videoScaledHeight = videoScaledWidth / videoAspectRatio;\r\n }\r\n if (videoObjectFit === 'scale-down') {\r\n // for 'scale-down' the dimensions are the minimum of 'contains' and 'none'\r\n videoScaledWidth = Math.min(videoScaledWidth, videoWidth);\r\n videoScaledHeight = Math.min(videoScaledHeight, videoHeight);\r\n }\r\n }\r\n\r\n // getComputedStyle is so nice to convert keywords (left, center, right, top, bottom) to percent and makes\r\n // sure to set the default of 50% if only one or no component was provided, therefore we can be sure that\r\n // both components are set. Additionally, it converts units other than px (e.g. rem) to px.\r\n const [videoX, videoY] = videoStyle.objectPosition.split(' ').map((length, i) => {\r\n const lengthValue = parseFloat(length);\r\n return length.endsWith('%')\r\n ? (!i ? elementWidth - videoScaledWidth : elementHeight - videoScaledHeight) * lengthValue / 100\r\n : lengthValue;\r\n });\r\n\r\n const regionWidth = this._scanRegion.width || videoWidth;\r\n const regionHeight = this._scanRegion.height || videoHeight;\r\n const regionX = this._scanRegion.x || 0;\r\n const regionY = this._scanRegion.y || 0;\r\n\r\n const overlayStyle = this.$overlay.style;\r\n overlayStyle.width = `${regionWidth / videoWidth * videoScaledWidth}px`;\r\n overlayStyle.height = `${regionHeight / videoHeight * videoScaledHeight}px`;\r\n overlayStyle.top = `${elementY + videoY + regionY / videoHeight * videoScaledHeight}px`;\r\n const isVideoMirrored = /scaleX\\(-1\\)/.test(video.style.transform!);\r\n overlayStyle.left = `${elementX\r\n + (isVideoMirrored ? elementWidth - videoX - videoScaledWidth : videoX)\r\n + (isVideoMirrored ? videoWidth - regionX - regionWidth : regionX) / videoWidth * videoScaledWidth}px`;\r\n // apply same mirror as on video\r\n overlayStyle.transform = video.style.transform;\r\n });\r\n }\r\n\r\n private static _convertPoints(\r\n points: Point[],\r\n scanRegion?: ScanRegion | null,\r\n ): Point[] {\r\n if (!scanRegion) return points;\r\n const offsetX = scanRegion.x || 0;\r\n const offsetY = scanRegion.y || 0;\r\n const scaleFactorX = scanRegion.width && scanRegion.downScaledWidth\r\n ? scanRegion.width / scanRegion.downScaledWidth\r\n : 1;\r\n const scaleFactorY = scanRegion.height && scanRegion.downScaledHeight\r\n ? scanRegion.height / scanRegion.downScaledHeight\r\n : 1;\r\n for (const point of points) {\r\n point.x = point.x * scaleFactorX + offsetX;\r\n point.y = point.y * scaleFactorY + offsetY;\r\n }\r\n return points;\r\n }\r\n\r\n private _scanFrame(): void {\r\n if (!this._active || this.$video.paused || this.$video.ended) return;\r\n // If requestVideoFrameCallback is available use that to avoid unnecessary scans on the same frame as the\r\n // camera's framerate can be lower than the screen refresh rate and this._maxScansPerSecond, especially in dark\r\n // settings where the exposure time is longer. Both, requestVideoFrameCallback and requestAnimationFrame are not\r\n // being fired if the tab is in the background, which is what we want.\r\n const requestFrame = 'requestVideoFrameCallback' in this.$video\r\n // @ts-ignore\r\n ? this.$video.requestVideoFrameCallback.bind(this.$video)\r\n : requestAnimationFrame;\r\n requestFrame(async () => {\r\n if (this.$video.readyState <= 1) {\r\n // Skip scans until the video is ready as drawImage() only works correctly on a video with readyState\r\n // > 1, see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage#Notes.\r\n // This also avoids false positives for videos paused after a successful scan which remains visible on\r\n // the canvas until the video is started again and ready.\r\n this._scanFrame();\r\n return;\r\n }\r\n\r\n const timeSinceLastScan = Date.now() - this._lastScanTimestamp;\r\n const minimumTimeBetweenScans = 1000 / this._maxScansPerSecond;\r\n if (timeSinceLastScan < minimumTimeBetweenScans) {\r\n await new Promise((resolve) => setTimeout(resolve, minimumTimeBetweenScans - timeSinceLastScan));\r\n }\r\n // console.log('Scan rate:', Math.round(1000 / (Date.now() - this._lastScanTimestamp)));\r\n this._lastScanTimestamp = Date.now();\r\n\r\n let result: ScanResult | undefined;\r\n try {\r\n result = await QrScanner.scanImage(this.$video, {\r\n scanRegion: this._scanRegion,\r\n qrEngine: this._qrEnginePromise,\r\n canvas: this.$canvas,\r\n });\r\n console.log(result);\r\n } catch (error) {\r\n if (!this._active) return;\r\n this._onDecodeError(error as Error | string);\r\n }\r\n\r\n if (QrScanner._disableBarcodeDetector && !(await this._qrEnginePromise instanceof Worker)) {\r\n // replace the disabled BarcodeDetector\r\n this._qrEnginePromise = QrScanner.createQrEngine();\r\n }\r\n\r\n if (result) {\r\n if (this._onDecode) {\r\n this._onDecode(result);\r\n } else if (this._legacyOnDecode) {\r\n this._legacyOnDecode(result.data);\r\n }\r\n\r\n if (this.$codeOutlineHighlight) {\r\n clearTimeout(this._codeOutlineHighlightRemovalTimeout);\r\n this._codeOutlineHighlightRemovalTimeout = undefined;\r\n this.$codeOutlineHighlight.setAttribute(\r\n 'viewBox',\r\n `${this._scanRegion.x || 0} `\r\n + `${this._scanRegion.y || 0} `\r\n + `${this._scanRegion.width || this.$video.videoWidth} `\r\n + `${this._scanRegion.height || this.$video.videoHeight}`,\r\n );\r\n const polygon = this.$codeOutlineHighlight.firstElementChild!;\r\n polygon.setAttribute('points', result.cornerPoints.map(({x, y}) => `${x},${y}`).join(' '));\r\n this.$codeOutlineHighlight.style.display = '';\r\n }\r\n } else if (this.$codeOutlineHighlight && !this._codeOutlineHighlightRemovalTimeout) {\r\n // hide after timeout to make it flash less when on some frames the QR code is detected and on some not\r\n this._codeOutlineHighlightRemovalTimeout = setTimeout(\r\n () => this.$codeOutlineHighlight!.style.display = 'none',\r\n 100,\r\n );\r\n }\r\n\r\n this._scanFrame();\r\n });\r\n }\r\n\r\n private _onDecodeError(error: Error | string): void {\r\n // default error handler; can be overwritten in the constructor\r\n if (error === QrScanner.NO_QR_CODE_FOUND) return;\r\n console.log(error);\r\n }\r\n\r\n private async _getCameraStream(): Promise<{ stream: MediaStream, facingMode: FacingMode }> {\r\n if (!navigator.mediaDevices) throw 'Camera not found.';\r\n\r\n const preferenceType = /^(environment|user)$/.test(this._preferredCamera)\r\n ? 'facingMode'\r\n : 'deviceId';\r\n const constraintsWithoutCamera: Array<MediaTrackConstraints> = [{\r\n width: { min: 1024 }\r\n }, {\r\n width: { min: 768 }\r\n }, {}];\r\n const constraintsWithCamera = constraintsWithoutCamera.map((constraint) => Object.assign({}, constraint, {\r\n [preferenceType]: { exact: this._preferredCamera },\r\n }));\r\n\r\n for (const constraints of [...constraintsWithCamera, ...constraintsWithoutCamera]) {\r\n try {\r\n const stream = await navigator.mediaDevices.getUserMedia({ video: constraints, audio: false });\r\n // Try to determine the facing mode from the stream, otherwise use a guess or 'environment' as\r\n // default. Note that the guess is not always accurate as Safari returns cameras of different facing\r\n // mode, even for exact facingMode constraints.\r\n const facingMode = this._getFacingMode(stream)\r\n || (constraints.facingMode\r\n ? this._preferredCamera as FacingMode // a facing mode we were able to fulfill\r\n : (this._preferredCamera === 'environment'\r\n ? 'user' // switch as _preferredCamera was environment but we are not able to fulfill it\r\n : 'environment' // switch from unfulfilled user facingMode or default to environment\r\n )\r\n );\r\n return { stream, facingMode };\r\n } catch (e) {}\r\n }\r\n\r\n throw 'Camera not found.';\r\n }\r\n\r\n private async _restartVideoStream(): Promise<void> {\r\n // Note that we always pause the stream and not only if !this._paused as even if this._paused === true, the\r\n // stream might still be running, as it's by default only stopped after a delay of 300ms.\r\n const wasPaused = this._paused;\r\n const paused = await this.pause(true);\r\n if (!paused || wasPaused || !this._active) return;\r\n await this.start();\r\n }\r\n\r\n private static _stopVideoStream(stream : MediaStream): void {\r\n for (const track of stream.getTracks()) {\r\n track.stop(); // note that this will also automatically turn the flashlight off\r\n stream.removeTrack(track);\r\n }\r\n }\r\n\r\n private _setVideoMirror(facingMode: FacingMode): void {\r\n // in user facing mode mirror the video to make it easier for the user to position the QR code\r\n const scaleFactor = facingMode === 'user'? -1 : 1;\r\n this.$video.style.transform = 'scaleX(' + scaleFactor + ')';\r\n }\r\n\r\n private _getFacingMode(videoStream: MediaStream): FacingMode | null {\r\n const videoTrack = videoStream.getVideoTracks()[0];\r\n if (!videoTrack) return null; // unknown\r\n // inspired by https://github.com/JodusNodus/react-qr-reader/blob/master/src/getDeviceId.js#L13\r\n return /rear|back|environment/i.test(videoTrack.label)\r\n ? 'environment'\r\n : /front|user|face/i.test(videoTrack.label)\r\n ? 'user'\r\n : null; // unknown\r\n }\r\n\r\n private static _drawToCanvas(\r\n image: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\r\n | SVGImageElement,\r\n scanRegion?: ScanRegion | null,\r\n canvas?: HTMLCanvasElement | null,\r\n disallowCanvasResizing= false,\r\n ): [HTMLCanvasElement, CanvasRenderingContext2D] {\r\n canvas = canvas || document.createElement('canvas');\r\n const scanRegionX = scanRegion && scanRegion.x ? scanRegion.x : 0;\r\n const scanRegionY = scanRegion && scanRegion.y ? scanRegion.y : 0;\r\n const scanRegionWidth = scanRegion && scanRegion.width\r\n ? scanRegion.width\r\n : (image as HTMLVideoElement).videoWidth || image.width as number;\r\n const scanRegionHeight = scanRegion && scanRegion.height\r\n ? scanRegion.height\r\n : (image as HTMLVideoElement).videoHeight || image.height as number;\r\n\r\n if (!disallowCanvasResizing) {\r\n const canvasWidth = scanRegion && scanRegion.downScaledWidth\r\n ? scanRegion.downScaledWidth\r\n : scanRegionWidth;\r\n const canvasHeight = scanRegion && scanRegion.downScaledHeight\r\n ? scanRegion.downScaledHeight\r\n : scanRegionHeight;\r\n // Setting the canvas width or height clears the canvas, even if the values didn't change, therefore only\r\n // set them if they actually changed.\r\n if (canvas.width !== canvasWidth) {\r\n canvas.width = canvasWidth;\r\n }\r\n if (canvas.height !== canvasHeight) {\r\n canvas.height = canvasHeight;\r\n }\r\n }\r\n\r\n const context = canvas.getContext('2d', { alpha: false })!;\r\n context.imageSmoothingEnabled = false; // gives less blurry images\r\n context.drawImage(\r\n image,\r\n scanRegionX, scanRegionY, scanRegionWidth, scanRegionHeight,\r\n 0, 0, canvas.width, canvas.height,\r\n );\r\n return [canvas, context];\r\n }\r\n\r\n private static async _loadImage(\r\n imageOrFileOrBlobOrUrl: HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\r\n | SVGImageElement | File | Blob | URL | String,\r\n ): Promise<HTMLImageElement | HTMLVideoElement | HTMLCanvasElement | OffscreenCanvas | ImageBitmap\r\n | SVGImageElement > {\r\n if (imageOrFileOrBlobOrUrl instanceof Image) {\r\n await QrScanner._awaitImageLoad(imageOrFileOrBlobOrUrl);\r\n return imageOrFileOrBlobOrUrl;\r\n } else if (imageOrFileOrBlobOrUrl instanceof HTMLVideoElement\r\n || imageOrFileOrBlobOrUrl instanceof HTMLCanvasElement\r\n || imageOrFileOrBlobOrUrl instanceof SVGImageElement\r\n || 'OffscreenCanvas' in window && imageOrFileOrBlobOrUrl instanceof OffscreenCanvas\r\n || 'ImageBitmap' in window && imageOrFileOrBlobOrUrl instanceof ImageBitmap) {\r\n return imageOrFileOrBlobOrUrl;\r\n } else if (imageOrFileOrBlobOrUrl instanceof File || imageOrFileOrBlobOrUrl instanceof Blob\r\n || imageOrFileOrBlobOrUrl instanceof URL || typeof imageOrFileOrBlobOrUrl === 'string') {\r\n const image = new Image();\r\n if (imageOrFileOrBlobOrUrl instanceof File || imageOrFileOrBlobOrUrl instanceof Blob) {\r\n image.src = URL.createObjectURL(imageOrFileOrBlobOrUrl);\r\n } else {\r\n image.src = imageOrFileOrBlobOrUrl.toString();\r\n }\r\n try {\r\n await QrScanner._awaitImageLoad(image);\r\n return image;\r\n } finally {\r\n if (imageOrFileOrBlobOrUrl instanceof File || imageOrFileOrBlobOrUrl instanceof Blob) {\r\n URL.revokeObjectURL(image.src);\r\n }\r\n }\r\n } else {\r\n throw 'Unsupported image type.';\r\n }\r\n }\r\n\r\n private static async _awaitImageLoad(image: HTMLImageElement): Promise<void> {\r\n if (image.complete && image.naturalWidth !== 0) return; // already loaded\r\n await new Promise<void>((resolve, reject) => {\r\n const listener = (event: ErrorEvent | Event) => {\r\n image.removeEventListener('load', listener);\r\n image.removeEventListener('error', listener);\r\n if (event instanceof ErrorEvent) {\r\n reject('Image load error');\r\n } else {\r\n resolve();\r\n }\r\n };\r\n image.addEventListener('load', listener);\r\n image.addEventListener('error', listener);\r\n });\r\n }\r\n\r\n private static async _postWorkerMessage(\r\n qrEngineOrQrEnginePromise: Worker | BarcodeDetector | Promise<Worker | BarcodeDetector>,\r\n type: string,\r\n data?: any,\r\n transfer?: Transferable[],\r\n ): Promise<number> {\r\n return QrScanner._postWorkerMessageSync(await qrEngineOrQrEnginePromise, type, data, transfer);\r\n }\r\n\r\n // sync version of _postWorkerMessage without performance overhead of async functions\r\n private static _postWorkerMessageSync(\r\n qrEngine: Worker | BarcodeDetector,\r\n type: string,\r\n transfer: Transferable[],\r\n data?: any \r\n ): number {\r\n if (!(qrEngine instanceof Worker)) return -1;\r\n const id = QrScanner._workerMessageId++;\r\n qrEngine.postMessage({\r\n id,\r\n type,\r\n data,\r\n }, transfer);\r\n return id;\r\n }\r\n}\r\n\r\n//declare namespace QrScanner {\r\n export interface ScanRegion {\r\n x?: number;\r\n y?: number;\r\n width?: number;\r\n height?: number;\r\n downScaledWidth?: number;\r\n downScaledHeight?: number;\r\n }\r\n\r\n export type FacingMode = 'environment' | 'user';\r\n export type DeviceId = string;\r\n\r\n export interface Camera {\r\n id: DeviceId;\r\n label: string;\r\n }\r\n\r\n export type InversionMode = 'original' | 'invert' | 'both';\r\n\r\n export interface Point {\r\n x: number;\r\n y: number;\r\n }\r\n\r\n export interface ScanResult {\r\n data: string;\r\n // In clockwise order, starting at top left, but this might not be guaranteed in the future.\r\n cornerPoints: Point[];\r\n }\r\n//}\r\n\r\n// simplified from https://wicg.github.io/shape-detection-api/#barcode-detection-api\r\ndeclare class BarcodeDetector {\r\n constructor(options?: { formats: string[] });\r\n static getSupportedFormats(): Promise<string[]>;\r\n detect(image: ImageBitmapSource): Promise<Array<{ rawValue: string, cornerPoints: Point[] }>>;\r\n}\r\n\r\n// simplified from https://github.com/lukewarlow/user-agent-data-types/blob/master/index.d.ts\r\ndeclare global {\r\n interface Navigator {\r\n readonly userAgentData?: {\r\n readonly platform: string;\r\n readonly brands: Array<{\r\n readonly brand: string;\r\n readonly version: string;\r\n }>;\r\n getHighEntropyValues(hints: string[]): Promise<{\r\n readonly architecture?: string;\r\n readonly platformVersion?: string;\r\n }>;\r\n };\r\n }\r\n}\r\n\r\n//export default QrScanner;\r\n"],"names":[],"mappings":"mDAkGY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}