Created
March 10, 2026 19:31
-
-
Save ariankordi/a8e24dcc17fd4fd78c32771725f0efe4 to your computer and use it in GitHub Desktop.
MiiImportController wip version and esbuild bundled 03/10/2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| var __create = Object.create; | |
| var __defProp = Object.defineProperty; | |
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | |
| var __getOwnPropNames = Object.getOwnPropertyNames; | |
| var __getProtoOf = Object.getPrototypeOf; | |
| var __hasOwnProp = Object.prototype.hasOwnProperty; | |
| var __commonJS = (cb, mod) => function __require() { | |
| return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; | |
| }; | |
| var __copyProps = (to, from, except, desc) => { | |
| if (from && typeof from === "object" || typeof from === "function") { | |
| for (let key of __getOwnPropNames(from)) | |
| if (!__hasOwnProp.call(to, key) && key !== except) | |
| __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | |
| } | |
| return to; | |
| }; | |
| var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( | |
| // If the importer is in node compatibility mode or this is not an ESM | |
| // file that has been converted to a CommonJS file using a Babel- | |
| // compatible transform (i.e. "__esModule" has not been set), then set | |
| // "default" to the CommonJS "module.exports" for node compatibility. | |
| isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, | |
| mod | |
| )); | |
| // ../../../build/qr-scanner/qr-scanner.legacy.min.js | |
| var require_qr_scanner_legacy_min = __commonJS({ | |
| "../../../build/qr-scanner/qr-scanner.legacy.min.js"(exports, module) { | |
| "use strict"; | |
| (function(global, factory) { | |
| typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.QrScanner = factory()); | |
| })(exports, function() { | |
| function __awaiter(thisArg, _arguments, P, generator) { | |
| function adopt(value) { | |
| return value instanceof P ? value : new P(function(resolve) { | |
| resolve(value); | |
| }); | |
| } | |
| return new (P || (P = Promise))(function(resolve, reject) { | |
| function fulfilled(value) { | |
| try { | |
| step(generator.next(value)); | |
| } catch (e) { | |
| reject(e); | |
| } | |
| } | |
| function rejected(value) { | |
| try { | |
| step(generator["throw"](value)); | |
| } catch (e) { | |
| reject(e); | |
| } | |
| } | |
| function step(result) { | |
| result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); | |
| } | |
| step((generator = generator.apply(thisArg, _arguments || [])).next()); | |
| }); | |
| } | |
| typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) { | |
| var e = new Error(message); | |
| return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; | |
| }; | |
| class QrScanner2 { | |
| static hasCamera() { | |
| return __awaiter( | |
| this, | |
| void 0, | |
| void 0, | |
| function* () { | |
| try { | |
| return !!(yield QrScanner2.listCameras(false)).length; | |
| } catch (e) { | |
| return false; | |
| } | |
| } | |
| ); | |
| } | |
| static setBarcodeDetectorDisabled() { | |
| this._disableBarcodeDetector = true; | |
| } | |
| static listCameras(requestLabels = false) { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| if (!navigator.mediaDevices) return []; | |
| let enumerateCameras = () => __awaiter(this, void 0, void 0, function* () { | |
| return (yield navigator.mediaDevices.enumerateDevices()).filter((device) => device.kind === "videoinput"); | |
| }); | |
| let openedStream; | |
| try { | |
| if (requestLabels && (yield enumerateCameras()).every((camera) => !camera.label)) openedStream = yield navigator.mediaDevices.getUserMedia({ audio: false, video: true }); | |
| } catch (e) { | |
| } | |
| try { | |
| return (yield enumerateCameras()).map((camera, i) => ({ id: camera.deviceId, label: camera.label || (i === 0 ? "Default Camera" : `Camera ${i + 1}`) })); | |
| } finally { | |
| if (openedStream) { | |
| console.warn("Call listCameras after successfully starting a QR scanner to avoid creating a temporary video stream"); | |
| QrScanner2._stopVideoStream(openedStream); | |
| } | |
| } | |
| }); | |
| } | |
| constructor(video, onDecode, options) { | |
| this._legacyCanvasSize = QrScanner2.DEFAULT_CANVAS_SIZE; | |
| this._preferredCamera = "environment"; | |
| this._maxScansPerSecond = 25; | |
| this._lastScanTimestamp = -1; | |
| this._active = false; | |
| this._paused = false; | |
| this._flashOn = false; | |
| this._destroyed = false; | |
| this.$video = video; | |
| this.$canvas = document.createElement("canvas"); | |
| this._onDecode = onDecode; | |
| options = typeof options === "object" ? options : {}; | |
| this._onDecodeError = options.onDecodeError || this._onDecodeError; | |
| this._calculateScanRegion = options.calculateScanRegion || this._calculateScanRegion; | |
| this._preferredCamera = options.preferredCamera || this._preferredCamera; | |
| this._maxScansPerSecond = options.maxScansPerSecond || this._maxScansPerSecond; | |
| this._onPlay = this._onPlay.bind(this); | |
| this._onLoadedMetaData = this._onLoadedMetaData.bind(this); | |
| this._onVisibilityChange = this._onVisibilityChange.bind(this); | |
| this._updateOverlay = this._updateOverlay.bind(this); | |
| video.disablePictureInPicture = true; | |
| video.playsInline = true; | |
| video.muted = true; | |
| let shouldHideVideo = false; | |
| if (video.hidden) { | |
| video.hidden = false; | |
| shouldHideVideo = true; | |
| } | |
| if (!document.body.contains(video)) { | |
| document.body.append(video); | |
| shouldHideVideo = true; | |
| } | |
| let videoContainer = video.parentElement; | |
| if (options.highlightScanRegion || options.highlightCodeOutline) { | |
| let gotExternalOverlay = !!options.overlay; | |
| this.$overlay = options.overlay || document.createElement("div"); | |
| let overlayStyle = this.$overlay.style; | |
| overlayStyle.position = "absolute"; | |
| overlayStyle.display = "none"; | |
| overlayStyle.pointerEvents = "none"; | |
| this.$overlay.classList.add("scan-region-highlight"); | |
| if (!gotExternalOverlay && options.highlightScanRegion) { | |
| this.$overlay.innerHTML = '<svg class="scan-region-highlight-svg" viewBox="0 0 238 238" preserveAspectRatio="none" style="position:absolute;width:100%;height:100%;left:0;top:0;fill:none;stroke:#e9b213;stroke-width:4;stroke-linecap:round;stroke-linejoin:round"><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 0H10a8 8 0 0 1-8-8v-21"/></svg>'; | |
| try { | |
| this.$overlay.firstElementChild.animate({ transform: ["scale(.98)", "scale(1.01)"] }, { duration: 400, iterations: Infinity, direction: "alternate", easing: "ease-in-out" }); | |
| } catch (e) { | |
| } | |
| videoContainer.insertBefore( | |
| this.$overlay, | |
| this.$video.nextSibling | |
| ); | |
| } | |
| if (options.highlightCodeOutline) { | |
| this.$overlay.insertAdjacentHTML("beforeend", '<svg class="code-outline-highlight" preserveAspectRatio="none" style="display:none;width:100%;height:100%;fill:none;stroke:#e9b213;stroke-width:5;stroke-dasharray:25;stroke-linecap:round;stroke-linejoin:round"><polygon/></svg>'); | |
| this.$codeOutlineHighlight = this.$overlay.lastElementChild; | |
| } | |
| } | |
| this._scanRegion = this._calculateScanRegion(video); | |
| requestAnimationFrame(() => { | |
| let videoStyle = globalThis.getComputedStyle(video); | |
| if (videoStyle.display === "none") { | |
| video.style.setProperty("display", "block", "important"); | |
| shouldHideVideo = true; | |
| } | |
| if (videoStyle.visibility !== "visible") { | |
| video.style.setProperty("visibility", "visible", "important"); | |
| shouldHideVideo = true; | |
| } | |
| if (shouldHideVideo) { | |
| console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."); | |
| video.style.opacity = "0"; | |
| video.style.width = "0"; | |
| video.style.height = "0"; | |
| if (this.$overlay && this.$overlay.parentElement) this.$overlay.remove(); | |
| delete this.$overlay; | |
| delete this.$codeOutlineHighlight; | |
| } | |
| if (this.$overlay) this._updateOverlay(); | |
| }); | |
| video.addEventListener("play", this._onPlay); | |
| video.addEventListener("loadedmetadata", this._onLoadedMetaData); | |
| document.addEventListener("visibilitychange", this._onVisibilityChange); | |
| window.addEventListener("resize", this._updateOverlay); | |
| this._qrEnginePromise = QrScanner2.createQrEngine(); | |
| } | |
| hasFlash() { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| let stream; | |
| try { | |
| if (this.$video.srcObject) { | |
| if (!(this.$video.srcObject instanceof MediaStream)) return false; | |
| stream = this.$video.srcObject; | |
| } else stream = (yield this._getCameraStream()).stream; | |
| return "torch" in stream.getVideoTracks()[0].getSettings(); | |
| } catch (e) { | |
| return false; | |
| } finally { | |
| if (stream && stream !== this.$video.srcObject) { | |
| console.warn("Call hasFlash after successfully starting the scanner to avoid creating a temporary video stream"); | |
| QrScanner2._stopVideoStream(stream); | |
| } | |
| } | |
| }); | |
| } | |
| isFlashOn() { | |
| return this._flashOn; | |
| } | |
| toggleFlash() { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| yield this._flashOn ? this.turnFlashOff() : this.turnFlashOn(); | |
| }); | |
| } | |
| turnFlashOn() { | |
| return __awaiter( | |
| this, | |
| void 0, | |
| void 0, | |
| function* () { | |
| if (this._flashOn || this._destroyed) return; | |
| this._flashOn = true; | |
| if (!this._active || this._paused) return; | |
| try { | |
| if (!(yield this.hasFlash())) throw new Error("No flash available"); | |
| yield this.$video.srcObject.getVideoTracks()[0].applyConstraints({ advanced: [{ torch: true }] }); | |
| } catch (e) { | |
| this._flashOn = false; | |
| throw e; | |
| } | |
| } | |
| ); | |
| } | |
| turnFlashOff() { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| if (!this._flashOn) return; | |
| this._flashOn = false; | |
| yield this._restartVideoStream(); | |
| }); | |
| } | |
| destroy() { | |
| this.$video.removeEventListener( | |
| "loadedmetadata", | |
| this._onLoadedMetaData | |
| ); | |
| this.$video.removeEventListener("play", this._onPlay); | |
| document.removeEventListener("visibilitychange", this._onVisibilityChange); | |
| window.removeEventListener("resize", this._updateOverlay); | |
| this._destroyed = true; | |
| this._flashOn = false; | |
| this.stop(); | |
| QrScanner2._postWorkerMessage(this._qrEnginePromise, "close"); | |
| } | |
| start() { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| if (this._destroyed) throw new Error("The QR scanner can not be started as it had been destroyed."); | |
| if (this._active && !this._paused) return; | |
| if (globalThis.location.protocol !== "https:") console.warn("The camera stream is only accessible if the page is transferred via https."); | |
| this._active = true; | |
| if (document.hidden) return; | |
| this._paused = false; | |
| if (this.$video.srcObject) { | |
| yield this.$video.play(); | |
| return; | |
| } | |
| try { | |
| let { stream, facingMode } = yield this._getCameraStream(); | |
| if (!this._active || this._paused) { | |
| QrScanner2._stopVideoStream(stream); | |
| return; | |
| } | |
| this._setVideoMirror(facingMode); | |
| this.$video.srcObject = stream; | |
| yield this.$video.play(); | |
| if (this._flashOn) { | |
| this._flashOn = false; | |
| this.turnFlashOn().catch(() => { | |
| }); | |
| } | |
| } catch (e) { | |
| if (this._paused) return; | |
| this._active = false; | |
| throw e; | |
| } | |
| }); | |
| } | |
| stop() { | |
| this.pause(); | |
| this._active = false; | |
| } | |
| pause(stopStreamImmediately = false) { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| this._paused = true; | |
| if (!this._active) return true; | |
| this.$video.pause(); | |
| if (this.$overlay) this.$overlay.style.display = "none"; | |
| let stopStream = () => { | |
| if (this.$video.srcObject instanceof MediaStream) { | |
| QrScanner2._stopVideoStream(this.$video.srcObject); | |
| this.$video.srcObject = null; | |
| } | |
| }; | |
| if (stopStreamImmediately) { | |
| stopStream(); | |
| return true; | |
| } | |
| yield new Promise((resolve) => setTimeout(resolve, 300)); | |
| if (!this._paused) return false; | |
| stopStream(); | |
| return true; | |
| }); | |
| } | |
| setCamera(facingModeOrDeviceId) { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| if (facingModeOrDeviceId === this._preferredCamera) return; | |
| this._preferredCamera = facingModeOrDeviceId; | |
| yield this._restartVideoStream(); | |
| }); | |
| } | |
| static scanImage(imageOrFileOrBlobOrUrl, options) { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| let scanRegion = options.scanRegion || void 0; | |
| let qrEngine; | |
| let canvas = options.canvas || document.createElement("canvas"); | |
| let disallowCanvasResizing = false; | |
| let alsoTryWithoutScanRegion = false; | |
| options = typeof options === "object" ? options : {}; | |
| qrEngine = options.qrEngine || qrEngine; | |
| disallowCanvasResizing = options.disallowCanvasResizing || false; | |
| alsoTryWithoutScanRegion = options.alsoTryWithoutScanRegion || false; | |
| let gotExternalEngine = !!qrEngine; | |
| try { | |
| let image; | |
| [qrEngine, image] = yield Promise.all([qrEngine || QrScanner2.createQrEngine(), QrScanner2._loadImage(imageOrFileOrBlobOrUrl)]); | |
| let canvasContext = QrScanner2._drawToCanvas( | |
| image, | |
| canvas, | |
| scanRegion, | |
| disallowCanvasResizing | |
| ); | |
| let detailedScanResult; | |
| if (qrEngine instanceof Worker) { | |
| let qrEngineWorker = qrEngine; | |
| if (!gotExternalEngine) QrScanner2._postWorkerMessageSync(qrEngineWorker, "inversionMode", "both"); | |
| detailedScanResult = yield new Promise((resolve, reject) => { | |
| let timeout; | |
| let onError; | |
| let onMessage; | |
| let expectedResponseId = -1; | |
| onMessage = (event) => { | |
| let data = event.data; | |
| if (data["id"] !== expectedResponseId) return; | |
| qrEngineWorker.removeEventListener("message", onMessage); | |
| qrEngineWorker.removeEventListener( | |
| "error", | |
| onError | |
| ); | |
| clearTimeout(timeout); | |
| if (data["data"] === null) reject(QrScanner2.NO_QR_CODE_FOUND); | |
| else resolve({ data: data["data"], binaryData: data["binaryData"], cornerPoints: QrScanner2._convertPoints(data["cornerPoints"], scanRegion) }); | |
| }; | |
| onError = (error) => { | |
| qrEngineWorker.removeEventListener("message", onMessage); | |
| qrEngineWorker.removeEventListener("error", onError); | |
| clearTimeout(timeout); | |
| let errorMessage = error ? error.message || error : "Unknown Error"; | |
| reject("Scanner error: " + errorMessage); | |
| }; | |
| qrEngineWorker.addEventListener( | |
| "message", | |
| onMessage | |
| ); | |
| qrEngineWorker.addEventListener("error", onError); | |
| timeout = setTimeout(() => onError("timeout"), 1e4); | |
| let imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height); | |
| expectedResponseId = QrScanner2._postWorkerMessageSync(qrEngineWorker, "decode", imageData, [imageData.data.buffer]); | |
| }); | |
| } else detailedScanResult = yield Promise.race([new Promise((resolve, reject) => globalThis.setTimeout(() => reject("Scanner error: timeout"), 1e4)), (() => __awaiter(this, void 0, void 0, function* () { | |
| try { | |
| let [scanResult] = yield qrEngine.detect(canvas); | |
| if (!scanResult) throw QrScanner2.NO_QR_CODE_FOUND; | |
| return { data: scanResult.rawValue, cornerPoints: QrScanner2._convertPoints(scanResult.cornerPoints, scanRegion) }; | |
| } catch (e) { | |
| let errorMessage = e.message || e; | |
| if (/not implemented|service unavailable/.test(errorMessage)) { | |
| QrScanner2._disableBarcodeDetector = true; | |
| return QrScanner2.scanImage(imageOrFileOrBlobOrUrl, { scanRegion, canvas, disallowCanvasResizing, alsoTryWithoutScanRegion }); | |
| } | |
| throw `Scanner error: ${errorMessage}`; | |
| } | |
| }))()]); | |
| return detailedScanResult; | |
| } catch (e) { | |
| if (!scanRegion || !alsoTryWithoutScanRegion) throw e; | |
| let detailedScanResult = yield QrScanner2.scanImage(imageOrFileOrBlobOrUrl, { qrEngine, canvas, disallowCanvasResizing }); | |
| return detailedScanResult; | |
| } finally { | |
| if (!gotExternalEngine) QrScanner2._postWorkerMessage(qrEngine, "close"); | |
| } | |
| }); | |
| } | |
| setGrayscaleWeights(red, green, blue, useIntegerApproximation = true) { | |
| QrScanner2._postWorkerMessage(this._qrEnginePromise, "grayscaleWeights", { red, green, blue, useIntegerApproximation }); | |
| } | |
| setInversionMode(inversionMode) { | |
| QrScanner2._postWorkerMessage( | |
| this._qrEnginePromise, | |
| "inversionMode", | |
| inversionMode | |
| ); | |
| } | |
| static createQrEngine() { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| let createWorker2 = () => Promise.resolve().then(function() { | |
| return qrScannerWorker_min; | |
| }).then((module2) => module2.createWorker()); | |
| let useBarcodeDetector = !QrScanner2._disableBarcodeDetector && "BarcodeDetector" in globalThis && BarcodeDetector.getSupportedFormats && (yield BarcodeDetector.getSupportedFormats()).indexOf("qr_code") !== -1; | |
| if (!useBarcodeDetector) return createWorker2(); | |
| let userAgentData = navigator.userAgentData; | |
| let isChromiumOnMacWithArmVentura = userAgentData && userAgentData.brands.some(({ brand, version }) => /Chromium/i.test(brand) && Number.parseInt(version) < 113) && /mac ?OS/i.test(userAgentData.platform) && (yield userAgentData.getHighEntropyValues(["architecture", "platformVersion"]).then(({ architecture, platformVersion }) => /arm/i.test(architecture || "arm") && Number.parseInt(platformVersion || "13") >= 13).catch(() => true)); | |
| if (isChromiumOnMacWithArmVentura) return createWorker2(); | |
| return new BarcodeDetector({ formats: ["qr_code"] }); | |
| }); | |
| } | |
| _onPlay() { | |
| this._scanRegion = this._calculateScanRegion(this.$video); | |
| this._updateOverlay(); | |
| if (this.$overlay) this.$overlay.style.display = ""; | |
| this._scanFrame(); | |
| } | |
| _onLoadedMetaData() { | |
| this._scanRegion = this._calculateScanRegion(this.$video); | |
| this._updateOverlay(); | |
| } | |
| _onVisibilityChange() { | |
| if (document.hidden) this.pause(); | |
| else if (this._active) this.start(); | |
| } | |
| _calculateScanRegion(video) { | |
| let smallestDimension = Math.min(video.videoWidth, video.videoHeight); | |
| let scanRegionSize = Math.round(2 / 3 * smallestDimension); | |
| return { x: Math.round((video.videoWidth - scanRegionSize) / 2), y: Math.round((video.videoHeight - scanRegionSize) / 2), width: scanRegionSize, height: scanRegionSize, downScaledWidth: this._legacyCanvasSize, downScaledHeight: this._legacyCanvasSize }; | |
| } | |
| _updateOverlay() { | |
| requestAnimationFrame(() => { | |
| if (!this.$overlay) return; | |
| let video = this.$video; | |
| let videoWidth = video.videoWidth; | |
| let videoHeight = video.videoHeight; | |
| let elementWidth = video.offsetWidth; | |
| let elementHeight = video.offsetHeight; | |
| let elementX = video.offsetLeft; | |
| let elementY = video.offsetTop; | |
| let videoStyle = globalThis.getComputedStyle(video); | |
| let videoObjectFit = videoStyle.objectFit; | |
| let videoAspectRatio = videoWidth / videoHeight; | |
| let elementAspectRatio = elementWidth / elementHeight; | |
| let videoScaledWidth; | |
| let videoScaledHeight; | |
| switch (videoObjectFit) { | |
| case "none": | |
| videoScaledWidth = videoWidth; | |
| videoScaledHeight = videoHeight; | |
| break; | |
| case "fill": | |
| videoScaledWidth = elementWidth; | |
| videoScaledHeight = elementHeight; | |
| break; | |
| default: | |
| if (videoObjectFit === "cover" ? videoAspectRatio > elementAspectRatio : videoAspectRatio < elementAspectRatio) { | |
| videoScaledHeight = elementHeight; | |
| videoScaledWidth = videoScaledHeight * videoAspectRatio; | |
| } else { | |
| videoScaledWidth = elementWidth; | |
| videoScaledHeight = videoScaledWidth / videoAspectRatio; | |
| } | |
| if (videoObjectFit === "scale-down") { | |
| videoScaledWidth = Math.min(videoScaledWidth, videoWidth); | |
| videoScaledHeight = Math.min(videoScaledHeight, videoHeight); | |
| } | |
| } | |
| let [videoX, videoY] = videoStyle.objectPosition.split(" ").map((length, i) => { | |
| const lengthValue = Number.parseFloat(length); | |
| return length.endsWith("%") ? (i ? elementHeight - videoScaledHeight : elementWidth - videoScaledWidth) * lengthValue / 100 : lengthValue; | |
| }); | |
| let regionWidth = this._scanRegion.width || videoWidth; | |
| let regionHeight = this._scanRegion.height || videoHeight; | |
| let regionX = this._scanRegion.x || 0; | |
| let regionY = this._scanRegion.y || 0; | |
| let overlayStyle = this.$overlay.style; | |
| overlayStyle.width = `${regionWidth / videoWidth * videoScaledWidth}px`; | |
| overlayStyle.height = `${regionHeight / videoHeight * videoScaledHeight}px`; | |
| overlayStyle.top = `${elementY + videoY + regionY / videoHeight * videoScaledHeight}px`; | |
| let isVideoMirrored = /scaleX\(-1\)/.test(video.style.transform); | |
| overlayStyle.left = `${elementX + (isVideoMirrored ? elementWidth - videoX - videoScaledWidth : videoX) + (isVideoMirrored ? videoWidth - regionX - regionWidth : regionX) / videoWidth * videoScaledWidth}px`; | |
| overlayStyle.transform = video.style.transform; | |
| }); | |
| } | |
| static _convertPoints(points, scanRegion) { | |
| if (!scanRegion) return points; | |
| let offsetX = scanRegion.x || 0; | |
| let offsetY = scanRegion.y || 0; | |
| let scaleFactorX = scanRegion.width && scanRegion.downScaledWidth ? scanRegion.width / scanRegion.downScaledWidth : 1; | |
| let scaleFactorY = scanRegion.height && scanRegion.downScaledHeight ? scanRegion.height / scanRegion.downScaledHeight : 1; | |
| for (let point of points) { | |
| point.x = point.x * scaleFactorX + offsetX; | |
| point.y = point.y * scaleFactorY + offsetY; | |
| } | |
| return points; | |
| } | |
| _scanFrame() { | |
| if (!this._active || this.$video.paused || this.$video.ended) return; | |
| let requestFrame = "requestVideoFrameCallback" in this.$video ? this.$video.requestVideoFrameCallback.bind(this.$video) : requestAnimationFrame; | |
| requestFrame(() => __awaiter(this, void 0, void 0, function* () { | |
| if (this.$video.readyState <= 1) { | |
| this._scanFrame(); | |
| return; | |
| } | |
| let timeSinceLastScan = Date.now() - this._lastScanTimestamp; | |
| let minimumTimeBetweenScans = 1e3 / this._maxScansPerSecond; | |
| if (timeSinceLastScan < minimumTimeBetweenScans) yield new Promise((resolve) => setTimeout(resolve, minimumTimeBetweenScans - timeSinceLastScan)); | |
| this._lastScanTimestamp = Date.now(); | |
| let result; | |
| try { | |
| result = yield QrScanner2.scanImage(this.$video, { scanRegion: this._scanRegion, qrEngine: this._qrEnginePromise, canvas: this.$canvas }); | |
| } catch (error) { | |
| if (!this._active) return; | |
| this._onDecodeError(error); | |
| } | |
| if (QrScanner2._disableBarcodeDetector && !((yield this._qrEnginePromise) instanceof Worker)) this._qrEnginePromise = QrScanner2.createQrEngine(); | |
| if (result) { | |
| if (this._onDecode) this._onDecode(result); | |
| if (this.$codeOutlineHighlight) { | |
| clearTimeout(this._codeOutlineHighlightRemovalTimeout); | |
| this._codeOutlineHighlightRemovalTimeout = void 0; | |
| this.$codeOutlineHighlight.setAttribute("viewBox", `${this._scanRegion.x || 0} ${this._scanRegion.y || 0} ${this._scanRegion.width || this.$video.videoWidth} ${this._scanRegion.height || this.$video.videoHeight}`); | |
| let polygon = this.$codeOutlineHighlight.firstElementChild; | |
| console.assert(!!polygon); | |
| polygon.setAttribute("points", result.cornerPoints.map(({ x, y }) => `${x},${y}`).join(" ")); | |
| this.$codeOutlineHighlight.style.display = ""; | |
| } | |
| } else if (!this._codeOutlineHighlightRemovalTimeout) this._codeOutlineHighlightRemovalTimeout = setTimeout(() => { | |
| if (this.$codeOutlineHighlight) this.$codeOutlineHighlight.style.display = "none"; | |
| }, 100); | |
| this._scanFrame(); | |
| })); | |
| } | |
| _onDecodeError(error) { | |
| if (error === QrScanner2.NO_QR_CODE_FOUND) return; | |
| console.log(error); | |
| } | |
| _getCameraStream() { | |
| return __awaiter( | |
| this, | |
| void 0, | |
| void 0, | |
| function* () { | |
| if (!navigator.mediaDevices) throw new Error("Camera not found."); | |
| let preferenceType = /^(environment|user)$/.test(this._preferredCamera) ? "facingMode" : "deviceId"; | |
| let constraintsWithoutCamera = [{ width: { min: 1024 } }, { width: { min: 768 } }, {}]; | |
| let constraintsWithCamera = constraintsWithoutCamera.map((constraint) => Object.assign({}, constraint, { [preferenceType]: { exact: this._preferredCamera } })); | |
| for (let constraints of [...constraintsWithCamera, ...constraintsWithoutCamera]) try { | |
| let stream = yield navigator.mediaDevices.getUserMedia({ | |
| video: constraints, | |
| audio: false | |
| }); | |
| let facingMode = QrScanner2._getFacingMode(stream) || (constraints.facingMode ? this._preferredCamera : this._preferredCamera === "environment" ? "user" : "environment"); | |
| return { stream, facingMode }; | |
| } catch (e) { | |
| } | |
| throw new Error("Camera not found."); | |
| } | |
| ); | |
| } | |
| _restartVideoStream() { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| let wasPaused = this._paused; | |
| let paused = yield this.pause(true); | |
| if (!paused || wasPaused || !this._active) return; | |
| yield this.start(); | |
| }); | |
| } | |
| static _stopVideoStream(stream) { | |
| for (let track of stream.getTracks()) { | |
| track.stop(); | |
| stream.removeTrack(track); | |
| } | |
| } | |
| _setVideoMirror(facingMode) { | |
| let scaleFactor = facingMode === "user" ? -1 : 1; | |
| this.$video.style.transform = "scaleX(" + scaleFactor + ")"; | |
| } | |
| static _getFacingMode(videoStream) { | |
| let videoTrack = videoStream.getVideoTracks()[0]; | |
| if (!videoTrack) return null; | |
| return /rear|back|environment/i.test(videoTrack.label) ? "environment" : /front|user|face/i.test(videoTrack.label) ? "user" : null; | |
| } | |
| static _drawToCanvas(image, canvas, scanRegion, disallowCanvasResizing = false) { | |
| let scanRegionX = scanRegion && scanRegion.x ? scanRegion.x : 0; | |
| let scanRegionY = scanRegion && scanRegion.y ? scanRegion.y : 0; | |
| let scanRegionWidth = scanRegion && scanRegion.width ? scanRegion.width : image.videoWidth || image.width; | |
| let scanRegionHeight = scanRegion && scanRegion.height ? scanRegion.height : image.videoHeight || image.height; | |
| if (!disallowCanvasResizing) { | |
| let canvasWidth = scanRegion && scanRegion.downScaledWidth ? scanRegion.downScaledWidth : scanRegionWidth; | |
| let canvasHeight = scanRegion && scanRegion.downScaledHeight ? scanRegion.downScaledHeight : scanRegionHeight; | |
| if (canvas.width !== canvasWidth) canvas.width = canvasWidth; | |
| if (canvas.height !== canvasHeight) canvas.height = canvasHeight; | |
| } | |
| let context = canvas.getContext("2d", { alpha: false, willReadFrequently: true }); | |
| if (!context) throw new Error("Could not get canvas rendering context."); | |
| context.imageSmoothingEnabled = false; | |
| context.drawImage(image, scanRegionX, scanRegionY, scanRegionWidth, scanRegionHeight, 0, 0, canvas.width, canvas.height); | |
| return context; | |
| } | |
| static _loadImage(resource) { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| if (resource instanceof Image) { | |
| yield QrScanner2._awaitImageLoad(resource); | |
| return resource; | |
| } else if (resource instanceof HTMLVideoElement || resource instanceof HTMLCanvasElement || resource instanceof SVGImageElement || "OffscreenCanvas" in globalThis && resource instanceof OffscreenCanvas || "ImageBitmap" in globalThis && resource instanceof ImageBitmap) return resource; | |
| else if (resource instanceof File || resource instanceof Blob || resource instanceof URL || typeof resource === "string") { | |
| let image = new Image(); | |
| image.src = resource instanceof File || resource instanceof Blob ? URL.createObjectURL(resource) : resource.toString(); | |
| try { | |
| yield QrScanner2._awaitImageLoad(image); | |
| return image; | |
| } finally { | |
| if (resource instanceof File || resource instanceof Blob) URL.revokeObjectURL(image.src); | |
| } | |
| } else throw new TypeError("Unsupported image type."); | |
| }); | |
| } | |
| static _awaitImageLoad(image) { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| if (image.complete && image.naturalWidth !== 0) return; | |
| yield new Promise((resolve, reject) => { | |
| let listener = (event) => { | |
| image.removeEventListener("load", listener); | |
| image.removeEventListener("error", listener); | |
| if (event instanceof ErrorEvent) reject("Image load error"); | |
| else resolve(); | |
| }; | |
| image.addEventListener("load", listener); | |
| image.addEventListener("error", listener); | |
| }); | |
| }); | |
| } | |
| static _postWorkerMessage(qrEngineOrQrEnginePromise, type, data, transfer) { | |
| return __awaiter(this, void 0, void 0, function* () { | |
| return QrScanner2._postWorkerMessageSync(yield qrEngineOrQrEnginePromise, type, data, transfer); | |
| }); | |
| } | |
| static _postWorkerMessageSync(qrEngine, type, data, transfer) { | |
| if (!(qrEngine instanceof Worker)) return -1; | |
| let id = QrScanner2._workerMessageId++; | |
| qrEngine.postMessage({ id, type, data }, { transfer }); | |
| return id; | |
| } | |
| } | |
| QrScanner2.DEFAULT_CANVAS_SIZE = 400; | |
| QrScanner2.NO_QR_CODE_FOUND = "No QR code found"; | |
| QrScanner2._disableBarcodeDetector = false; | |
| QrScanner2._workerMessageId = 0; | |
| let createWorker = () => new Worker(URL.createObjectURL(new Blob([`var w=class a{static ja(b,c){return new a(new Uint8ClampedArray(b*c),b)}constructor(b,c){this.width=c;this.height=b.length/c;this.data=b}get(b,c){return 0>b||b>=this.width||0>c||c>=this.height?!1:!!this.data[c*this.width+b]}set(b,c,d){this.data[c*this.width+b]=d?1:0}Z(b,c,d,e){for(let f=c;f<c+e;f++)for(let g=b;g<b+d;g++)this.set(g,f,!0)}},A=class{constructor(a,b,c){this.width=a;a*=b;if(c&&c.length!==a)throw Error("Wrong buffer size");this.data=c||new Uint8ClampedArray(a)}get(a,b){return this.data[b* | |
| this.width+a]}set(a,b,c){this.data[b*this.width+a]=c}};function B(a){return 8*(a.H.length-a.byteOffset)-a.W}function C(a,b){if(1>b||32<b||b>B(a))throw Error("Cannot read "+b.toString()+" bits");var c=0;if(0<a.W){c=8-a.W;var d=b<c?b:c;c-=d;c=(a.H[a.byteOffset]&255>>8-d<<c)>>c;b-=d;a.W+=d;8===a.W&&(a.W=0,a.byteOffset++)}if(0<b){for(;8<=b;)c=c<<8|a.H[a.byteOffset]&255,a.byteOffset++,b-=8;0<b&&(d=8-b,c=c<<b|(a.H[a.byteOffset]&255>>d<<d)>>d,a.W+=b)}return c} | |
| var aa=class{constructor(a){this.W=this.byteOffset=0;this.H=a}},D="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".split("");function ba(a,b){let c=[],d="";b=C(a,[8,16,16][b]);for(let e=0;e<b;e++){let f=C(a,8);c.push(f)}try{d+=decodeURIComponent(c.map(e=>"%"+("0"+e.toString(16)).substr(-2)).join(""))}catch(e){}return{H:c,text:d}} | |
| function ca(a,b){a=new aa(a);let c=9>=b?0:26>=b?1:2;for(b={text:"",H:[],U:[],version:b};4<=B(a);){var d=C(a,4);if(0===d)return b;if(7===d)0===C(a,1)?b.U.push({type:"eci",ma:C(a,7)}):0===C(a,1)?b.U.push({type:"eci",ma:C(a,14)}):0===C(a,1)?b.U.push({type:"eci",ma:C(a,21)}):b.U.push({type:"eci",ma:-1});else if(1===d){var e=a,f=[];d="";for(var g=C(e,[10,12,14][c]);3<=g;){var h=C(e,10);if(1E3<=h)throw Error("Invalid numeric value above 999");var k=Math.floor(h/100),l=Math.floor(h/10)%10;h%=10;f.push(48+ | |
| k,48+l,48+h);d+=k.toString()+l.toString()+h.toString();g-=3}if(2===g){g=C(e,7);if(100<=g)throw Error("Invalid numeric value above 99");e=Math.floor(g/10);g%=10;f.push(48+e,48+g);d+=e.toString()+g.toString()}else if(1===g){e=C(e,4);if(10<=e)throw Error("Invalid numeric value above 9");f.push(48+e);d+=e.toString()}b.text+=d;b.H.push(...f);b.U.push({type:"numeric",text:d})}else if(2===d){e=a;f=[];d="";for(g=C(e,[9,11,13][c]);2<=g;)l=C(e,11),k=Math.floor(l/45),l%=45,f.push(D[k].charCodeAt(0),D[l].charCodeAt(0)), | |
| d+=D[k]+D[l],g-=2;1===g&&(e=C(e,6),f.push(D[e].charCodeAt(0)),d+=D[e]);b.text+=d;b.H.push(...f);b.U.push({type:"alphanumeric",text:d})}else if(4===d)d=ba(a,c),b.text+=d.text,b.H.push(...d.H),b.U.push({type:"byte",H:d.H,text:d.text});else if(8===d){f=a;d=[];e=C(f,[8,10,12][c]);for(g=0;g<e;g++)k=C(f,13),k=Math.floor(k/192)<<8|k%192,k=7936>k?k+33088:k+49472,d.push(k>>8,k&255);f=(new TextDecoder("shift-jis")).decode(Uint8Array.from(d));b.text+=f;b.H.push(...d);b.U.push({type:"kanji",H:d,text:f})}else 3=== | |
| d&&b.U.push({type:"structuredappend",Ja:C(a,4),Na:C(a,4),Ka:C(a,8)})}if(0===B(a)||0===C(a,B(a)))return b} | |
| var F=class a{constructor(b,c){if(0===c.length)throw Error("No coefficients.");this.V=b;let d=c.length;if(1<d&&0===c[0]){let e=1;for(;e<d&&0===c[e];)e++;if(e===d)this.C=b.$.C;else for(this.C=new Uint8ClampedArray(d-e),b=0;b<this.C.length;b++)this.C[b]=c[e+b]}else this.C=c}T(){return this.C.length-1}ba(){return 0===this.C[0]}fa(b){return this.C[this.C.length-1-b]}la(b){if(this.ba())return b;if(b.ba())return this;let c=this.C;b=b.C;c.length>b.length&&([c,b]=[b,c]);let d=new Uint8ClampedArray(b.length), | |
| e=b.length-c.length;for(var f=0;f<e;f++)d[f]=b[f];for(f=e;f<b.length;f++)d[f]=c[f-e]^b[f];return new a(this.V,d)}multiply(b){if(0===b)return this.V.$;if(1===b)return this;let c=this.C.length,d=new Uint8ClampedArray(c);for(let e=0;e<c;e++)d[e]=this.V.multiply(this.C[e],b);return new a(this.V,d)}Ba(b){if(this.ba()||b.ba())return this.V.$;let c=this.C,d=c.length;b=b.C;let e=b.length,f=new Uint8ClampedArray(d+e-1);for(let g=0;g<d;g++){let h=c[g];for(let k=0;k<e;k++)f[g+k]=E(f[g+k],this.V.multiply(h,b[k]))}return new a(this.V, | |
| f)}Aa(b,c){if(0>b)throw Error("Invalid degree less than 0");if(0===c)return this.V.$;let d=this.C.length;b=new Uint8ClampedArray(d+b);for(let e=0;e<d;e++)b[e]=this.V.multiply(this.C[e],c);return new a(this.V,b)}na(b){let c=0;if(0===b)return this.fa(0);let d=this.C.length;if(1===b)return this.C.forEach(e=>{c^=e}),c;c=this.C[0];for(let e=1;e<d;e++)c=E(this.V.multiply(b,c),this.C[e]);return c}};function E(a,b){return a^b} | |
| function G(a,b,c){if(0>b)throw Error("Invalid monomial degree less than 0");if(0===c)return a.$;b=new Uint8ClampedArray(b+1);b[0]=c;return new F(a,b)} | |
| var da=class{constructor(){this.Ea=285;this.size=256;this.sa=0;this.ea=Array(this.size);this.ga=Array(this.size);var a=1;for(let b=0;b<this.size;b++)this.ea[b]=a,a*=2,a>=this.size&&(a=(a^this.Ea)&this.size-1);for(a=0;a<this.size-1;a++)this.ga[this.ea[a]]=a;this.$=new F(this,Uint8ClampedArray.from([0]));this.Ca=new F(this,Uint8ClampedArray.from([1]))}multiply(a,b){return 0===a||0===b?0:this.ea[(this.ga[a]+this.ga[b])%(this.size-1)]}inverse(a){if(0===a)throw Error("Can't invert 0");return this.ea[this.size- | |
| this.ga[a]-1]}log(a){if(0===a)throw Error("Can't take log(0)");return this.ga[a]}exp(a){return this.ea[a]}}; | |
| function ea(a,b,c,d){b.T()<c.T()&&([b,c]=[c,b]);let e=a.$;for(var f=a.Ca;c.T()>=d/2;){var g=b;let h=e;b=c;e=f;if(b.ba())return null;c=g;f=a.$;for(g=a.inverse(b.fa(b.T()));c.T()>=b.T()&&!c.ba();){let k=c.T()-b.T(),l=a.multiply(c.fa(c.T()),g);f=f.la(G(a,k,l));c=c.la(b.Aa(k,l))}f=f.Ba(e).la(h);if(c.T()>=b.T())return null}d=f.fa(0);if(0===d)return null;a=a.inverse(d);return[f.multiply(a),c.multiply(a)]} | |
| function fa(a,b){let c=new Uint8ClampedArray(a.length);c.set(a);a=new da;var d=new F(a,c),e=new Uint8ClampedArray(b),f=!1;for(var g=0;g<b;g++){var h=d.na(a.exp(g+a.sa));e[e.length-1-g]=h;0!==h&&(f=!0)}if(!f)return c;d=new F(a,e);d=ea(a,G(a,b,1),d,b);if(null===d)return null;b=d[0];g=b.T();if(1===g)b=[b.fa(1)];else{e=Array(g);f=0;for(h=1;h<a.size&&f<g;h++)0===b.na(h)&&(e[f]=a.inverse(h),f++);b=f!==g?null:e}if(null==b)return null;e=d[1];f=b.length;d=Array(f);for(g=0;g<f;g++){h=a.inverse(b[g]);let k= | |
| 1;for(let l=0;l<f;l++)g!==l&&(k=a.multiply(k,E(1,a.multiply(b[l],h))));d[g]=a.multiply(e.na(h),a.inverse(k));0!==a.sa&&(d[g]=a.multiply(d[g],h))}for(e=0;e<b.length;e++){f=c.length-1-a.log(b[e]);if(0>f)return null;c[f]^=d[e]}return c} | |
| var H=[{l:null,m:1,o:[],s:[{j:7,i:[{h:1,g:19}]},{j:10,i:[{h:1,g:16}]},{j:13,i:[{h:1,g:13}]},{j:17,i:[{h:1,g:9}]}]},{l:null,m:2,o:[6,18],s:[{j:10,i:[{h:1,g:34}]},{j:16,i:[{h:1,g:28}]},{j:22,i:[{h:1,g:22}]},{j:28,i:[{h:1,g:16}]}]},{l:null,m:3,o:[6,22],s:[{j:15,i:[{h:1,g:55}]},{j:26,i:[{h:1,g:44}]},{j:18,i:[{h:2,g:17}]},{j:22,i:[{h:2,g:13}]}]},{l:null,m:4,o:[6,26],s:[{j:20,i:[{h:1,g:80}]},{j:18,i:[{h:2,g:32}]},{j:26,i:[{h:2,g:24}]},{j:16,i:[{h:4,g:9}]}]},{l:null,m:5,o:[6,30],s:[{j:26,i:[{h:1,g:108}]}, | |
| {j:24,i:[{h:2,g:43}]},{j:18,i:[{h:2,g:15},{h:2,g:16}]},{j:22,i:[{h:2,g:11},{h:2,g:12}]}]},{l:null,m:6,o:[6,34],s:[{j:18,i:[{h:2,g:68}]},{j:16,i:[{h:4,g:27}]},{j:24,i:[{h:4,g:19}]},{j:28,i:[{h:4,g:15}]}]},{l:31892,m:7,o:[6,22,38],s:[{j:20,i:[{h:2,g:78}]},{j:18,i:[{h:4,g:31}]},{j:18,i:[{h:2,g:14},{h:4,g:15}]},{j:26,i:[{h:4,g:13},{h:1,g:14}]}]},{l:34236,m:8,o:[6,24,42],s:[{j:24,i:[{h:2,g:97}]},{j:22,i:[{h:2,g:38},{h:2,g:39}]},{j:22,i:[{h:4,g:18},{h:2,g:19}]},{j:26,i:[{h:4,g:14},{h:2,g:15}]}]},{l:39577, | |
| m:9,o:[6,26,46],s:[{j:30,i:[{h:2,g:116}]},{j:22,i:[{h:3,g:36},{h:2,g:37}]},{j:20,i:[{h:4,g:16},{h:4,g:17}]},{j:24,i:[{h:4,g:12},{h:4,g:13}]}]},{l:42195,m:10,o:[6,28,50],s:[{j:18,i:[{h:2,g:68},{h:2,g:69}]},{j:26,i:[{h:4,g:43},{h:1,g:44}]},{j:24,i:[{h:6,g:19},{h:2,g:20}]},{j:28,i:[{h:6,g:15},{h:2,g:16}]}]},{l:48118,m:11,o:[6,30,54],s:[{j:20,i:[{h:4,g:81}]},{j:30,i:[{h:1,g:50},{h:4,g:51}]},{j:28,i:[{h:4,g:22},{h:4,g:23}]},{j:24,i:[{h:3,g:12},{h:8,g:13}]}]},{l:51042,m:12,o:[6,32,58],s:[{j:24,i:[{h:2, | |
| g:92},{h:2,g:93}]},{j:22,i:[{h:6,g:36},{h:2,g:37}]},{j:26,i:[{h:4,g:20},{h:6,g:21}]},{j:28,i:[{h:7,g:14},{h:4,g:15}]}]},{l:55367,m:13,o:[6,34,62],s:[{j:26,i:[{h:4,g:107}]},{j:22,i:[{h:8,g:37},{h:1,g:38}]},{j:24,i:[{h:8,g:20},{h:4,g:21}]},{j:22,i:[{h:12,g:11},{h:4,g:12}]}]},{l:58893,m:14,o:[6,26,46,66],s:[{j:30,i:[{h:3,g:115},{h:1,g:116}]},{j:24,i:[{h:4,g:40},{h:5,g:41}]},{j:20,i:[{h:11,g:16},{h:5,g:17}]},{j:24,i:[{h:11,g:12},{h:5,g:13}]}]},{l:63784,m:15,o:[6,26,48,70],s:[{j:22,i:[{h:5,g:87},{h:1, | |
| g:88}]},{j:24,i:[{h:5,g:41},{h:5,g:42}]},{j:30,i:[{h:5,g:24},{h:7,g:25}]},{j:24,i:[{h:11,g:12},{h:7,g:13}]}]},{l:68472,m:16,o:[6,26,50,74],s:[{j:24,i:[{h:5,g:98},{h:1,g:99}]},{j:28,i:[{h:7,g:45},{h:3,g:46}]},{j:24,i:[{h:15,g:19},{h:2,g:20}]},{j:30,i:[{h:3,g:15},{h:13,g:16}]}]},{l:70749,m:17,o:[6,30,54,78],s:[{j:28,i:[{h:1,g:107},{h:5,g:108}]},{j:28,i:[{h:10,g:46},{h:1,g:47}]},{j:28,i:[{h:1,g:22},{h:15,g:23}]},{j:28,i:[{h:2,g:14},{h:17,g:15}]}]},{l:76311,m:18,o:[6,30,56,82],s:[{j:30,i:[{h:5,g:120}, | |
| {h:1,g:121}]},{j:26,i:[{h:9,g:43},{h:4,g:44}]},{j:28,i:[{h:17,g:22},{h:1,g:23}]},{j:28,i:[{h:2,g:14},{h:19,g:15}]}]},{l:79154,m:19,o:[6,30,58,86],s:[{j:28,i:[{h:3,g:113},{h:4,g:114}]},{j:26,i:[{h:3,g:44},{h:11,g:45}]},{j:26,i:[{h:17,g:21},{h:4,g:22}]},{j:26,i:[{h:9,g:13},{h:16,g:14}]}]},{l:84390,m:20,o:[6,34,62,90],s:[{j:28,i:[{h:3,g:107},{h:5,g:108}]},{j:26,i:[{h:3,g:41},{h:13,g:42}]},{j:30,i:[{h:15,g:24},{h:5,g:25}]},{j:28,i:[{h:15,g:15},{h:10,g:16}]}]},{l:87683,m:21,o:[6,28,50,72,94],s:[{j:28, | |
| i:[{h:4,g:116},{h:4,g:117}]},{j:26,i:[{h:17,g:42}]},{j:28,i:[{h:17,g:22},{h:6,g:23}]},{j:30,i:[{h:19,g:16},{h:6,g:17}]}]},{l:92361,m:22,o:[6,26,50,74,98],s:[{j:28,i:[{h:2,g:111},{h:7,g:112}]},{j:28,i:[{h:17,g:46}]},{j:30,i:[{h:7,g:24},{h:16,g:25}]},{j:24,i:[{h:34,g:13}]}]},{l:96236,m:23,o:[6,30,54,74,102],s:[{j:30,i:[{h:4,g:121},{h:5,g:122}]},{j:28,i:[{h:4,g:47},{h:14,g:48}]},{j:30,i:[{h:11,g:24},{h:14,g:25}]},{j:30,i:[{h:16,g:15},{h:14,g:16}]}]},{l:102084,m:24,o:[6,28,54,80,106],s:[{j:30,i:[{h:6, | |
| g:117},{h:4,g:118}]},{j:28,i:[{h:6,g:45},{h:14,g:46}]},{j:30,i:[{h:11,g:24},{h:16,g:25}]},{j:30,i:[{h:30,g:16},{h:2,g:17}]}]},{l:102881,m:25,o:[6,32,58,84,110],s:[{j:26,i:[{h:8,g:106},{h:4,g:107}]},{j:28,i:[{h:8,g:47},{h:13,g:48}]},{j:30,i:[{h:7,g:24},{h:22,g:25}]},{j:30,i:[{h:22,g:15},{h:13,g:16}]}]},{l:110507,m:26,o:[6,30,58,86,114],s:[{j:28,i:[{h:10,g:114},{h:2,g:115}]},{j:28,i:[{h:19,g:46},{h:4,g:47}]},{j:28,i:[{h:28,g:22},{h:6,g:23}]},{j:30,i:[{h:33,g:16},{h:4,g:17}]}]},{l:110734,m:27,o:[6,34, | |
| 62,90,118],s:[{j:30,i:[{h:8,g:122},{h:4,g:123}]},{j:28,i:[{h:22,g:45},{h:3,g:46}]},{j:30,i:[{h:8,g:23},{h:26,g:24}]},{j:30,i:[{h:12,g:15},{h:28,g:16}]}]},{l:117786,m:28,o:[6,26,50,74,98,122],s:[{j:30,i:[{h:3,g:117},{h:10,g:118}]},{j:28,i:[{h:3,g:45},{h:23,g:46}]},{j:30,i:[{h:4,g:24},{h:31,g:25}]},{j:30,i:[{h:11,g:15},{h:31,g:16}]}]},{l:119615,m:29,o:[6,30,54,78,102,126],s:[{j:30,i:[{h:7,g:116},{h:7,g:117}]},{j:28,i:[{h:21,g:45},{h:7,g:46}]},{j:30,i:[{h:1,g:23},{h:37,g:24}]},{j:30,i:[{h:19,g:15},{h:26, | |
| g:16}]}]},{l:126325,m:30,o:[6,26,52,78,104,130],s:[{j:30,i:[{h:5,g:115},{h:10,g:116}]},{j:28,i:[{h:19,g:47},{h:10,g:48}]},{j:30,i:[{h:15,g:24},{h:25,g:25}]},{j:30,i:[{h:23,g:15},{h:25,g:16}]}]},{l:127568,m:31,o:[6,30,56,82,108,134],s:[{j:30,i:[{h:13,g:115},{h:3,g:116}]},{j:28,i:[{h:2,g:46},{h:29,g:47}]},{j:30,i:[{h:42,g:24},{h:1,g:25}]},{j:30,i:[{h:23,g:15},{h:28,g:16}]}]},{l:133589,m:32,o:[6,34,60,86,112,138],s:[{j:30,i:[{h:17,g:115}]},{j:28,i:[{h:10,g:46},{h:23,g:47}]},{j:30,i:[{h:10,g:24},{h:35, | |
| g:25}]},{j:30,i:[{h:19,g:15},{h:35,g:16}]}]},{l:136944,m:33,o:[6,30,58,86,114,142],s:[{j:30,i:[{h:17,g:115},{h:1,g:116}]},{j:28,i:[{h:14,g:46},{h:21,g:47}]},{j:30,i:[{h:29,g:24},{h:19,g:25}]},{j:30,i:[{h:11,g:15},{h:46,g:16}]}]},{l:141498,m:34,o:[6,34,62,90,118,146],s:[{j:30,i:[{h:13,g:115},{h:6,g:116}]},{j:28,i:[{h:14,g:46},{h:23,g:47}]},{j:30,i:[{h:44,g:24},{h:7,g:25}]},{j:30,i:[{h:59,g:16},{h:1,g:17}]}]},{l:145311,m:35,o:[6,30,54,78,102,126,150],s:[{j:30,i:[{h:12,g:121},{h:7,g:122}]},{j:28,i:[{h:12, | |
| g:47},{h:26,g:48}]},{j:30,i:[{h:39,g:24},{h:14,g:25}]},{j:30,i:[{h:22,g:15},{h:41,g:16}]}]},{l:150283,m:36,o:[6,24,50,76,102,128,154],s:[{j:30,i:[{h:6,g:121},{h:14,g:122}]},{j:28,i:[{h:6,g:47},{h:34,g:48}]},{j:30,i:[{h:46,g:24},{h:10,g:25}]},{j:30,i:[{h:2,g:15},{h:64,g:16}]}]},{l:152622,m:37,o:[6,28,54,80,106,132,158],s:[{j:30,i:[{h:17,g:122},{h:4,g:123}]},{j:28,i:[{h:29,g:46},{h:14,g:47}]},{j:30,i:[{h:49,g:24},{h:10,g:25}]},{j:30,i:[{h:24,g:15},{h:46,g:16}]}]},{l:158308,m:38,o:[6,32,58,84,110,136, | |
| 162],s:[{j:30,i:[{h:4,g:122},{h:18,g:123}]},{j:28,i:[{h:13,g:46},{h:32,g:47}]},{j:30,i:[{h:48,g:24},{h:14,g:25}]},{j:30,i:[{h:42,g:15},{h:32,g:16}]}]},{l:161089,m:39,o:[6,26,54,82,110,138,166],s:[{j:30,i:[{h:20,g:117},{h:4,g:118}]},{j:28,i:[{h:40,g:47},{h:7,g:48}]},{j:30,i:[{h:43,g:24},{h:22,g:25}]},{j:30,i:[{h:10,g:15},{h:67,g:16}]}]},{l:167017,m:40,o:[6,30,58,86,114,142,170],s:[{j:30,i:[{h:19,g:118},{h:6,g:119}]},{j:28,i:[{h:18,g:47},{h:31,g:48}]},{j:30,i:[{h:34,g:24},{h:34,g:25}]},{j:30,i:[{h:20, | |
| g:15},{h:61,g:16}]}]}];function I(a,b){a^=b;for(b=0;a;)b++,a&=a-1;return b}function J(a,b){return b<<1|a} | |
| var ha=[{u:21522,B:{A:1,v:0}},{u:20773,B:{A:1,v:1}},{u:24188,B:{A:1,v:2}},{u:23371,B:{A:1,v:3}},{u:17913,B:{A:1,v:4}},{u:16590,B:{A:1,v:5}},{u:20375,B:{A:1,v:6}},{u:19104,B:{A:1,v:7}},{u:30660,B:{A:0,v:0}},{u:29427,B:{A:0,v:1}},{u:32170,B:{A:0,v:2}},{u:30877,B:{A:0,v:3}},{u:26159,B:{A:0,v:4}},{u:25368,B:{A:0,v:5}},{u:27713,B:{A:0,v:6}},{u:26998,B:{A:0,v:7}},{u:5769,B:{A:3,v:0}},{u:5054,B:{A:3,v:1}},{u:7399,B:{A:3,v:2}},{u:6608,B:{A:3,v:3}},{u:1890,B:{A:3,v:4}},{u:597,B:{A:3,v:5}},{u:3340,B:{A:3,v:6}}, | |
| {u:2107,B:{A:3,v:7}},{u:13663,B:{A:2,v:0}},{u:12392,B:{A:2,v:1}},{u:16177,B:{A:2,v:2}},{u:14854,B:{A:2,v:3}},{u:9396,B:{A:2,v:4}},{u:8579,B:{A:2,v:5}},{u:11994,B:{A:2,v:6}},{u:11245,B:{A:2,v:7}}],ia=[a=>0===(a.y+a.x)%2,a=>0===a.y%2,a=>0===a.x%3,a=>0===(a.y+a.x)%3,a=>0===(Math.floor(a.y/2)+Math.floor(a.x/3))%2,a=>0===a.x*a.y%2+a.x*a.y%3,a=>0===(a.y*a.x%2+a.y*a.x%3)%2,a=>0===((a.y+a.x)%2+a.y*a.x%3)%2]; | |
| function ja(a,b,c){c=ia[c.v];let d=a.height;var e=17+4*b.m;let f=w.ja(e,e);f.Z(0,0,9,9);f.Z(e-8,0,8,9);f.Z(0,e-8,9,8);for(var g of b.o)for(var h of b.o)6===g&&6===h||6===g&&h===e-7||g===e-7&&6===h||f.Z(g-2,h-2,5,5);f.Z(6,9,1,e-17);f.Z(9,6,e-17,1);6<b.m&&(f.Z(e-11,0,3,6),f.Z(0,e-11,6,3));b=[];h=g=0;e=!0;for(let k=d-1;0<k;k-=2){6===k&&k--;for(let l=0;l<d;l++){let m=e?d-1-l:l;for(let n=0;2>n;n++){let q=k-n;if(!f.get(q,m)){h++;let t=a.get(q,m);c({y:m,x:q})&&(t=!t);g=g<<1|t;8===h&&(b.push(g),g=h=0)}}}e= | |
| !e}return b}function ka(a){var b=a.height,c=Math.floor((b-17)/4);if(6>=c)return H[c-1];c=0;for(var d=5;0<=d;d--)for(var e=b-9;e>=b-11;e--)c=J(a.get(e,d),c);d=0;for(e=5;0<=e;e--)for(let g=b-9;g>=b-11;g--)d=J(a.get(e,g),d);a=Infinity;let f;for(let g of H){if(g.l===c||g.l===d)return g;b=I(c,g.l);b<a&&(f=g,a=b);b=I(d,g.l);b<a&&(f=g,a=b)}if(3>=a)return f} | |
| function la(a){let b=0;for(var c=0;8>=c;c++)6!==c&&(b=J(a.get(c,8),b));for(c=7;0<=c;c--)6!==c&&(b=J(a.get(8,c),b));var d=a.height;c=0;for(var e=d-1;e>=d-7;e--)c=J(a.get(8,e),c);for(e=d-8;e<d;e++)c=J(a.get(e,8),c);a=Infinity;d=null;for(let {u:f,B:g}of ha){if(f===b||f===c)return g;e=I(b,f);e<a&&(d=g,a=e);b!==c&&(e=I(c,f),e<a&&(d=g,a=e))}return 3>=a?d:null} | |
| function ma(a,b,c){let d=b.s[c],e=[],f=0;d.i.forEach(h=>{for(let k=0;k<h.h;k++)e.push({pa:h.g,da:[]}),f+=h.g+d.j});if(a.length<f)return null;a=a.slice(0,f);b=d.i[0].g;for(c=0;c<b;c++)for(var g of e)g.da.push(a.shift());if(1<d.i.length)for(g=d.i[0].h,b=d.i[1].h,c=0;c<b;c++)e[g+c].da.push(a.shift());for(;0<a.length;)for(let h of e)h.da.push(a.shift());return e} | |
| function K(a){let b=ka(a);if(!b)return null;var c=la(a);if(!c)return null;a=ja(a,b,c);var d=ma(a,b,c.A);if(!d)return null;c=d.reduce((e,f)=>e+f.pa,0);c=new Uint8ClampedArray(c);a=0;for(let e of d){d=fa(e.da,e.da.length-e.pa);if(!d)return null;for(let f=0;f<e.pa;f++)c[a++]=d[f]}try{return ca(c,b.m)}catch(e){return null}} | |
| function L(a,b,c,d){var e=a.x-b.x+c.x-d.x;let f=a.y-b.y+c.y-d.y;if(0===e&&0===f)return{J:b.x-a.x,K:b.y-a.y,L:0,M:c.x-b.x,N:c.y-b.y,O:0,P:a.x,R:a.y,S:1};let g=b.x-c.x;var h=d.x-c.x;let k=b.y-c.y,l=d.y-c.y;c=g*l-h*k;h=(e*l-h*f)/c;e=(g*f-e*k)/c;return{J:b.x-a.x+h*b.x,K:b.y-a.y+h*b.y,L:h,M:d.x-a.x+e*d.x,N:d.y-a.y+e*d.y,O:e,P:a.x,R:a.y,S:1}} | |
| function na(a,b,c){a=L({x:3.5,y:3.5},a,b,c);return{J:a.N*a.S-a.O*a.R,K:a.L*a.R-a.K*a.S,L:a.K*a.O-a.L*a.N,M:a.O*a.P-a.M*a.S,N:a.J*a.S-a.L*a.P,O:a.L*a.M-a.J*a.O,P:a.M*a.R-a.N*a.P,R:a.K*a.P-a.J*a.R,S:a.J*a.N-a.K*a.M}}function oa(a,b){return{J:a.J*b.J+a.M*b.K+a.P*b.L,K:a.K*b.J+a.N*b.K+a.R*b.L,L:a.L*b.J+a.O*b.K+a.S*b.L,M:a.J*b.M+a.M*b.N+a.P*b.O,N:a.K*b.M+a.N*b.N+a.R*b.O,O:a.L*b.M+a.O*b.N+a.S*b.O,P:a.J*b.P+a.M*b.R+a.P*b.S,R:a.K*b.P+a.N*b.R+a.R*b.S,S:a.L*b.P+a.O*b.R+a.S*b.S}} | |
| function pa(a,b){let c=oa(L(b.ha,b.ia,b.X,b.ca),na({x:b.D-3.5,y:3.5},{x:b.D-6.5,y:b.D-6.5},{x:3.5,y:b.D-3.5})),d=w.ja(b.D,b.D),e=(f,g)=>{const h=c.L*f+c.O*g+c.S;return{x:(c.J*f+c.M*g+c.P)/h,y:(c.K*f+c.N*g+c.R)/h}};for(let f=0;f<b.D;f++)for(let g=0;g<b.D;g++){let h=e(g+.5,f+.5);d.set(g,f,a.get(Math.floor(h.x),Math.floor(h.y)))}return{oa:d,ka:e}}var M=(a,b)=>Math.sqrt(Math.pow(b.x-a.x,2)+Math.pow(b.y-a.y,2));function N(a){return a.reduce((b,c)=>b+c)} | |
| function qa(a,b,c){let d=M(a,b),e=M(b,c),f=M(a,c),g,h,k;e>=d&&e>=f?[g,h,k]=[b,a,c]:f>=e&&f>=d?[g,h,k]=[a,b,c]:[g,h,k]=[a,c,b];0>(k.x-h.x)*(g.y-h.y)-(k.y-h.y)*(g.x-h.x)&&([g,k]=[k,g]);return{ca:g,ha:h,ia:k}}function ra(a,b,c,d){d=(N(O(a,c,d,5))/7+N(O(a,b,d,5))/7+N(O(c,a,d,5))/7+N(O(b,a,d,5))/7)/4;if(1>d)throw Error("Invalid module size");a=Math.floor((Math.round(M(a,b)/d)+Math.round(M(a,c)/d))/2)+7;switch(a%4){case 0:a++;break;case 2:a--}return{D:a,za:d}} | |
| function P(a,b,c,d){let e=[{x:Math.floor(a.x),y:Math.floor(a.y)}];var f=Math.abs(b.y-a.y)>Math.abs(b.x-a.x);if(f){var g=Math.floor(a.y);var h=Math.floor(a.x);a=Math.floor(b.y);b=Math.floor(b.x)}else g=Math.floor(a.x),h=Math.floor(a.y),a=Math.floor(b.x),b=Math.floor(b.y);let k=Math.abs(a-g),l=Math.abs(b-h),m=Math.floor(-k/2),n=g<a?1:-1,q=h<b?1:-1,t=!0;for(let x=g,r=h;x!==a+n;x+=n){g=f?r:x;h=f?x:r;if(c.get(g,h)!==t&&(t=!t,e.push({x:g,y:h}),e.length===d+1))break;m+=l;if(0<m){if(r===b)break;r+=q;m-=k}}c= | |
| [];for(f=0;f<d;f++)e[f]&&e[f+1]?c.push(M(e[f],e[f+1])):c.push(0);return c}function O(a,b,c,d){let e=b.y-a.y,f=b.x-a.x;b=P(a,b,c,Math.ceil(d/2));a=P(a,{x:a.x-f,y:a.y-e},c,Math.ceil(d/2));c=b.shift()+a.shift()-1;return a.concat(c).concat(...b)}function Q(a,b){let c=N(a)/N(b),d=0;b.forEach((e,f)=>{d+=Math.pow(a[f]-e*c,2)});return{Y:c,error:d}} | |
| function R(a,b,c){try{let d=O(a,{x:-1,y:a.y},c,b.length),e=O(a,{x:a.x,y:-1},c,b.length),f=O(a,{x:Math.max(0,a.x-a.y)-1,y:Math.max(0,a.y-a.x)-1},c,b.length),g=O(a,{x:Math.min(c.width,a.x+a.y)+1,y:Math.min(c.height,a.y+a.x)+1},c,b.length),h=Q(d,b),k=Q(e,b),l=Q(f,b),m=Q(g,b),n=(h.Y+k.Y+l.Y+m.Y)/4;return Math.sqrt(h.error*h.error+k.error*k.error+l.error*l.error+m.error*m.error)+Math.pow(Math.pow(h.Y-n,2)+Math.pow(k.Y-n,2)+Math.pow(l.Y-n,2)+(m.Y-n),2)/n}catch(d){return Infinity}} | |
| function S(a,b){for(var c=Math.round(b.x);a.get(c,Math.round(b.y));)c--;for(var d=Math.round(b.x);a.get(d,Math.round(b.y));)d++;c=(c+d)/2;for(d=Math.round(b.y);a.get(Math.round(c),d);)d--;for(b=Math.round(b.y);a.get(Math.round(c),b);)b++;return{x:c,y:(d+b)/2}} | |
| function sa(a){var b=[],c=[];let d=[];var e=[];for(let r=0;r<=a.height;r++){var f=0,g=!1;let p=[0,0,0,0,0];for(let u=-1;u<=a.width;u++){var h=a.get(u,r);if(h===g)f++;else{p=[p[1],p[2],p[3],p[4],f];f=1;g=h;var k=N(p)/7;k=Math.abs(p[0]-k)<k&&Math.abs(p[1]-k)<k&&Math.abs(p[2]-3*k)<3*k&&Math.abs(p[3]-k)<k&&Math.abs(p[4]-k)<k&&!h;var l=N(p.slice(-3))/3;h=Math.abs(p[2]-l)<l&&Math.abs(p[3]-l)<l&&Math.abs(p[4]-l)<l&&h;if(k){let z=u-p[3]-p[4],y=z-p[2];k={G:y,F:z,y:r};l=c.filter(v=>y>=v.bottom.G&&y<=v.bottom.F|| | |
| z>=v.bottom.G&&y<=v.bottom.F||y<=v.bottom.G&&z>=v.bottom.F&&1.5>p[2]/(v.bottom.F-v.bottom.G)&&.5<p[2]/(v.bottom.F-v.bottom.G));0<l.length?l[0].bottom=k:c.push({top:k,bottom:k})}if(h){let z=u-p[4],y=z-p[3];h={G:y,y:r,F:z};k=e.filter(v=>y>=v.bottom.G&&y<=v.bottom.F||z>=v.bottom.G&&y<=v.bottom.F||y<=v.bottom.G&&z>=v.bottom.F&&1.5>p[2]/(v.bottom.F-v.bottom.G)&&.5<p[2]/(v.bottom.F-v.bottom.G));0<k.length?k[0].bottom=h:e.push({top:h,bottom:h})}}}b.push(...c.filter(u=>u.bottom.y!==r&&2<=u.bottom.y-u.top.y)); | |
| c=c.filter(u=>u.bottom.y===r);d.push(...e.filter(u=>u.bottom.y!==r));e=e.filter(u=>u.bottom.y===r)}b.push(...c.filter(r=>2<=r.bottom.y-r.top.y));d.push(...e);c=[];for(var m of b)2>m.bottom.y-m.top.y||(b=(m.top.G+m.top.F+m.bottom.G+m.bottom.F)/4,e=(m.top.y+m.bottom.y+1)/2,a.get(Math.round(b),Math.round(e))&&(f=[m.top.F-m.top.G,m.bottom.F-m.bottom.G,m.bottom.y-m.top.y+1],f=N(f)/f.length,g=R({x:Math.round(b),y:Math.round(e)},[1,1,3,1,1],a),c.push({I:g,x:b,y:e,size:f})));if(3>c.length)return null;c.sort((r, | |
| p)=>r.I-p.I);m=[];for(b=0;b<Math.min(c.length,5);++b){e=c[b];f=[];for(var n of c)n!==e&&f.push({x:n.x,y:n.y,size:n.size,I:n.I+Math.pow(n.size-e.size,2)/e.size});f.sort((r,p)=>r.I-p.I);m.push({Da:[e,f[0],f[1]],I:e.I+f[0].I+f[1].I})}m.sort((r,p)=>r.I-p.I);let {ia:q,ha:t,ca:x}=qa(...m[0].Da);m=T(a,d,q,t,x);n=[];m&&n.push({X:{x:m.X.x,y:m.X.y},ca:{x:x.x,y:x.y},D:m.D,ha:{x:t.x,y:t.y},ia:{x:q.x,y:q.y}});m=S(a,q);b=S(a,t);c=S(a,x);(a=T(a,d,m,b,c))&&n.push({X:{x:a.X.x,y:a.X.y},ca:{x:c.x,y:c.y},ha:{x:b.x,y:b.y}, | |
| ia:{x:m.x,y:m.y},D:a.D});return 0===n.length?null:n} | |
| function T(a,b,c,d,e){let f;try{({D:f,za:g}=ra(d,c,e,a))}catch(l){return null}var g=(M(d,e)+M(d,c))/2/g;let h=1-3/g,k={x:d.x+h*(c.x-d.x+e.x-d.x),y:d.y+h*(c.y-d.y+e.y-d.y)};b=b.map(l=>{const m=(l.top.G+l.top.F+l.bottom.G+l.bottom.F)/4;l=(l.top.y+l.bottom.y+1)/2;if(a.get(Math.floor(m),Math.floor(l))){var n=R({x:Math.floor(m),y:Math.floor(l)},[1,1,1],a)+M({x:m,y:l},k);return{x:m,y:l,I:n}}}).filter(l=>!!l).sort((l,m)=>l.I-m.I);return{X:15<=g&&b.length?b[0]:k,D:f}} | |
| function U(a){var b=sa(a);if(!b)return null;for(let e of b){b=pa(a,e);var c=b.oa;if(null==c)c=null;else{var d=K(c);if(d)c=d;else{for(d=0;d<c.width;d++)for(let f=d+1;f<c.height;f++)c.get(d,f)!==c.get(f,d)&&(c.set(d,f,!c.get(d,f)),c.set(f,d,!c.get(f,d)));c=K(c)}}if(c)return{ua:c.H,data:c.text,U:c.U,version:c.version,location:{Ga:b.ka(e.D,0),Fa:b.ka(0,0),wa:b.ka(e.D,e.D),va:b.ka(0,e.D),Ma:e.ia,La:e.ha,Ha:e.ca,Ia:e.X},oa:b.oa}}return null} | |
| var ta={aa:"attemptBoth",ta:{red:.2126,green:.7152,blue:.0722,qa:!1},xa:!0};function V(a,b){Object.keys(b).forEach(c=>{a[c]=b[c]})} | |
| function W(a,b,c,d={}){let e=Object.create(null);V(e,ta);V(e,d);d="onlyInvert"===e.aa||"invertFirst"===e.aa;var f="attemptBoth"===e.aa||d,g=e.ta,h=e.xa,k=b*c;if(a.length!==4*k)throw Error("Malformed data passed to binarizer.");var l=0;if(h){var m=new Uint8ClampedArray(a.buffer,l,k);l+=k}m=new A(b,c,m);if(g.qa)for(var n=0;n<c;n++)for(var q=0;q<b;q++){var t=4*(n*b+q);m.set(q,n,g.red*a[t]+g.green*a[t+1]+g.blue*a[t+2]+128>>8)}else for(n=0;n<c;n++)for(q=0;q<b;q++)t=4*(n*b+q),m.set(q,n,g.red*a[t]+g.green* | |
| a[t+1]+g.blue*a[t+2]);g=Math.ceil(b/8);n=Math.ceil(c/8);q=g*n;if(h){var x=new Uint8ClampedArray(a.buffer,l,q);l+=q}x=new A(g,n,x);for(q=0;q<n;q++)for(t=0;t<g;t++){var r=Infinity,p=0;for(var u=0;8>u;u++)for(let v=0;8>v;v++){let Z=m.get(8*t+v,8*q+u);r=Math.min(r,Z);p=Math.max(p,Z)}u=(r+p)/2;u=Math.min(255,1.11*u);24>=p-r&&(u=r/2,0<q&&0<t&&(p=(x.get(t,q-1)+2*x.get(t-1,q)+x.get(t-1,q-1))/4,r<p&&(u=p)));x.set(t,q,u)}h?(q=new Uint8ClampedArray(a.buffer,l,k),l+=k,q=new w(q,b)):q=w.ja(b,c);t=null;f&&(h?(a= | |
| new Uint8ClampedArray(a.buffer,l,k),t=new w(a,b)):t=w.ja(b,c));for(b=0;b<n;b++)for(a=0;a<g;a++){c=g-3;c=2>a?2:a>c?c:a;h=n-3;h=2>b?2:b>h?h:b;k=0;for(l=-2;2>=l;l++)for(r=-2;2>=r;r++)k+=x.get(c+l,h+r);c=k/25;for(h=0;8>h;h++)for(k=0;8>k;k++)l=8*a+h,r=8*b+k,p=m.get(l,r),q.set(l,r,p<=c),f&&t.set(l,r,!(p<=c))}let {ra:z,ya:y}=f?{ra:q,ya:t}:{ra:q};(f=U(d?y:z))||"attemptBoth"!==e.aa&&"invertFirst"!==e.aa||(f=U(d?z:y));return f}W.default=W;let X="dontInvert",Y={red:77,green:150,blue:29,qa:!0}; | |
| self.onmessage=a=>{let b=a.data.id;var c=a.data.data;switch(a.data.type){case "decode":a=W(c.data,c.width,c.height,{aa:X,ta:Y});c={};c.id=b;c.type="qrResult";a?(c.data=a.data,c.binaryData=Uint8Array.from(a.ua),c.cornerPoints=[a.location.Fa,a.location.Ga,a.location.wa,a.location.va]):c.data=null;self.postMessage(c);break;case "grayscaleWeights":Y.red=c.red;Y.green=c.green;Y.blue=c.blue;Y.qa=c.useIntegerApproximation;break;case "inversionMode":switch(c){case "original":X="dontInvert";break;case "invert":X= | |
| "onlyInvert";break;case "both":X="attemptBoth";break;default:throw Error("Invalid inversion mode");}break;case "close":self.close()}} | |
| `]), { type: "application/javascript" })); | |
| var qrScannerWorker_min = Object.freeze({ __proto__: null, createWorker }); | |
| return QrScanner2; | |
| }); | |
| } | |
| }); | |
| // MiiImportController.js | |
| var import_qr_scanner = __toESM(require_qr_scanner_legacy_min()); | |
| // ../MiiDecoderCli.js | |
| var MiiDecoder = class _MiiDecoder { | |
| /** | |
| * @private | |
| */ | |
| constructor() { | |
| } | |
| /** | |
| * Size of Wii format Mii data (RFLCharData). | |
| * @public | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static SIZE_WII_DATA = 74; | |
| /** | |
| * Base size of 3DS/Wii U format Mii data. | |
| * Usually 96 bytes long (FFLStoreData, Ver3StoreData). | |
| * @public | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static SIZE3DS_WIIU_DATA = 72; | |
| /** | |
| * Size of Switch "nn::mii::CharInfo" format. | |
| * @public | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static SIZE_NX_CHAR_INFO = 88; | |
| /** | |
| * @public | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static SIZE_STUDIO_RAW_DATA = 46; | |
| /** | |
| * @public | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static SIZE_STUDIO_URL_DATA = 47; | |
| /** | |
| * @public | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static SIZE_NX_STORE_DATA = 68; | |
| /** | |
| * @public | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static SIZE_NX_CORE_DATA = 48; | |
| /** | |
| * Base URL of /miis/image.png API that returns a Mii image. | |
| * This can also be replaced by mii-unsecure.ariankordi.net. | |
| * @public | |
| * @readonly | |
| * @type {string} | |
| */ | |
| static URL_BASE_IMAGE_PNG = "https://truenas.local:8443/miis/image.png?shaderType=miitomo&data="; | |
| /** | |
| * Gets the Mii image URL using the Mii data converted in this instance. | |
| * | |
| * <p>Decodes the input Mii data, where the type is automatically detected. | |
| * Returns null if the format is not supported. | |
| * Formats (3DS/Wii U): | |
| * <ul> | |
| * <li>96 byte FFLStoreData/nn::mii::Ver3StoreData (has CRC)</li> | |
| * <li>92 byte FFLiMiiDataOfficial (in database, or kazuki-4ys ".3dsmii")</li> | |
| * <li>72 byte FFLiMiiDataCore (no creator name or CRC) | |
| * Formats (Wii):</li> | |
| * <li>76 byte RFLStoreData (has CRC)</li> | |
| * <li>74 byte RFLCharData (no CRC) | |
| * Formats (Switch, Studio):</li> | |
| * <li>88 byte nn::mii::CharInfo</li> | |
| * <li>47 byte obfuscated studio URL data</li> | |
| * <li>46 byte un-obfuscated raw "Studio Code"</li> | |
| * </ul> | |
| * @public | |
| * @param {Uint8Array} dst | |
| * @param {number} dataSize Size of the input data used to detect the type. | |
| * @param {Uint8Array} src Input Mii data to be converted. | |
| */ | |
| static fromAnyMiiData(dst, dataSize, src) { | |
| switch (dataSize) { | |
| case 96: | |
| case 92: | |
| case 72: | |
| _MiiDecoder.from3dsWiiuData(dst, src); | |
| return true; | |
| case 76: | |
| case 74: | |
| _MiiDecoder.fromWiiData(dst, src); | |
| return true; | |
| case 46: | |
| dst.set(src.subarray(0, 46)); | |
| return true; | |
| case 47: | |
| _MiiDecoder._deobfuscateStudioUrl(dst, src); | |
| return true; | |
| case 88: | |
| _MiiDecoder.fromNxCharInfo(dst, src); | |
| return true; | |
| case 68: | |
| case 48: | |
| _MiiDecoder.fromNxCoreData(dst, src); | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| /** | |
| * @public | |
| * @param {Uint8Array} dst | |
| * @param {MiiExtraInfo} dstExtra | |
| * @param {number} dataSize | |
| * @param {Uint8Array} src | |
| */ | |
| static fromAnyMiiDataExtra(dst, dstExtra, dataSize, src) { | |
| switch (dataSize) { | |
| case 96: | |
| case 92: | |
| case 72: | |
| _MiiDecoder.from3dsWiiuData(dst, src); | |
| _MiiDecoder.setExtraFromStudioData(dstExtra, dst); | |
| _MiiDecoder.setExtraFrom3dsWiiuData(dstExtra, src); | |
| return true; | |
| case 76: | |
| case 74: | |
| _MiiDecoder.fromWiiData(dst, src); | |
| _MiiDecoder.setExtraFromStudioData(dstExtra, dst); | |
| _MiiDecoder.setExtraFromWiiData(dstExtra, src); | |
| return true; | |
| case 46: | |
| dst.set(src.subarray(0, 46)); | |
| _MiiDecoder.setExtraFromStudioData(dstExtra, dst); | |
| return true; | |
| case 47: | |
| _MiiDecoder._deobfuscateStudioUrl(dst, src); | |
| _MiiDecoder.setExtraFromStudioData(dstExtra, dst); | |
| return true; | |
| case 88: | |
| _MiiDecoder.fromNxCharInfo(dst, src); | |
| _MiiDecoder.setExtraFromStudioData(dstExtra, dst); | |
| _MiiDecoder.setExtraFromNxCharInfo(dstExtra, src); | |
| return true; | |
| case 68: | |
| case 48: | |
| _MiiDecoder.fromNxCoreData(dst, src); | |
| _MiiDecoder.setExtraFromStudioData(dstExtra, dst); | |
| _MiiDecoder.setExtraFromNxCoreData(dstExtra, src); | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| /** | |
| * Verifies if the converted data in this instance is considered valid. | |
| * Derived from nn::mii::detail::CharInfoRaw::IsValid, | |
| * but does not return the specific reason. | |
| * @public | |
| * @param {Uint8Array} buf | |
| */ | |
| static isValid(buf) { | |
| return buf[0] < 100 && buf[1] < 6 && 128 > buf[2] && buf[3] < 7 && buf[4] < 100 && buf[5] < 8 && buf[6] < 8 && buf[7] < 60 && buf[8] < 13 && buf[9] < 19 && buf[10] < 7 && buf[11] < 100 && buf[12] < 12 && buf[13] < 9 && buf[14] < 24 && buf[15] < 13 && buf[16] >= 3 && buf[16] <= 18 && buf[17] < 10 && buf[18] < 12 && buf[19] < 12 && buf[20] < 12 && buf[21] < 12 && buf[22] < 2 && buf[23] < 100 && buf[24] < 8 && buf[25] < 20 && buf[26] < 21 && buf[27] < 100 && buf[28] < 2 && buf[29] < 132 && 128 > buf[30] && buf[31] < 9 && buf[32] < 2 && buf[33] < 17 && buf[34] < 31 && buf[35] < 7 && buf[36] < 100 && buf[37] < 9 && buf[38] < 36 && buf[39] < 19 && buf[40] < 9 && buf[41] < 6 && buf[42] < 17 && buf[43] < 9 && buf[44] < 18 && buf[45] < 19; | |
| } | |
| /** | |
| * Decodes the input 3DS/Wii U Mii data (FFLStoreData/nn::mii::Ver3StoreData). | |
| * @public | |
| * @param {Uint8Array} dst | |
| * @param {Uint8Array} src | |
| */ | |
| static from3dsWiiuData(dst, src) { | |
| dst[0] = src[66] >> 3 & 7; | |
| dst[1] = src[66] & 7; | |
| dst[2] = src[47]; | |
| dst[3] = src[53] >> 5; | |
| dst[4] = (src[53] & 1) << 2 | src[52] >> 6; | |
| dst[5] = src[54] & 31; | |
| dst[6] = src[53] >> 1 & 15; | |
| dst[7] = src[52] & 63; | |
| dst[8] = (src[55] & 1) << 3 | src[54] >> 5; | |
| dst[9] = src[55] >> 1 & 31; | |
| dst[10] = src[57] >> 4 & 7; | |
| dst[11] = src[56] >> 5; | |
| dst[12] = src[58] & 31; | |
| dst[13] = src[57] & 15; | |
| dst[14] = src[56] & 31; | |
| dst[15] = (src[59] & 1) << 3 | src[58] >> 5; | |
| dst[16] = src[59] >> 1 & 31; | |
| dst[17] = src[48] >> 5; | |
| dst[18] = src[49] >> 4; | |
| dst[19] = src[48] >> 1 & 15; | |
| dst[20] = src[49] & 15; | |
| dst[21] = src[25] >> 2 & 15; | |
| dst[22] = src[24] & 1; | |
| dst[23] = src[68] >> 4 & 7; | |
| dst[24] = (src[69] & 7) * 2 | src[68] >> 7; | |
| dst[25] = src[68] & 15; | |
| dst[26] = src[69] >> 3; | |
| dst[27] = src[51] & 7; | |
| dst[28] = src[51] >> 3 & 1; | |
| dst[29] = src[50]; | |
| dst[30] = src[46]; | |
| dst[31] = src[70] >> 1 & 15; | |
| dst[32] = src[70] & 1; | |
| dst[33] = (src[71] & 3) << 3 | src[70] >> 5; | |
| dst[34] = src[71] >> 2 & 31; | |
| dst[35] = src[63] >> 5; | |
| dst[36] = (src[63] & 1) << 2 | src[62] >> 6; | |
| dst[37] = src[63] >> 1 & 15; | |
| dst[38] = src[62] & 63; | |
| dst[39] = src[64] & 31; | |
| dst[40] = (src[67] & 3) << 2 | src[66] >> 6; | |
| dst[41] = src[64] >> 5; | |
| dst[42] = src[67] >> 2 & 31; | |
| dst[43] = (src[61] & 1) << 3 | src[60] >> 5; | |
| dst[44] = src[60] & 31; | |
| dst[45] = src[61] >> 1 & 31; | |
| _MiiDecoder._convertFieldsVer3ToNx(dst); | |
| return dst; | |
| } | |
| /** | |
| * Decodes the input Wii Mii data (RFLCharData, RFLStoreData). | |
| * @public | |
| * @param {Uint8Array} dst | |
| * @param {Uint8Array} src | |
| */ | |
| static fromWiiData(dst, src) { | |
| dst[0] = src[50] >> 1 & 7; | |
| dst[1] = src[50] >> 4 & 3; | |
| dst[2] = src[23]; | |
| dst[3] = 3; | |
| dst[4] = src[42] >> 5; | |
| dst[5] = src[41] >> 5 | (src[40] & 3) << 3; | |
| dst[6] = src[42] >> 1 & 15; | |
| dst[7] = src[40] >> 2; | |
| dst[8] = src[43] >> 5 | (src[42] & 1) << 3; | |
| dst[9] = src[41] & 31; | |
| dst[10] = 3; | |
| dst[11] = src[38] >> 5; | |
| dst[12] = src[37] >> 6 | (src[36] & 7) << 2; | |
| dst[13] = src[38] >> 1 & 15; | |
| dst[14] = src[36] >> 3; | |
| dst[15] = src[39] & 15; | |
| dst[16] = src[39] >> 4 | (src[38] & 1) << 4; | |
| dst[17] = src[32] >> 2 & 7; | |
| let faceTex = src[33] >> 6 | (src[32] & 3) << 2; | |
| dst[18] = _MiiDecoder._FROM_WII_DATA_FACE_TEX_TABLE[faceTex * 2 + 1]; | |
| dst[19] = src[32] >> 5; | |
| dst[20] = _MiiDecoder._FROM_WII_DATA_FACE_TEX_TABLE[faceTex * 2]; | |
| dst[21] = src[1] >> 1 & 15; | |
| dst[22] = src[0] >> 6 & 1; | |
| dst[23] = src[48] >> 1 & 7; | |
| dst[24] = src[49] >> 5 | (src[48] & 1) << 3; | |
| dst[25] = src[48] >> 4; | |
| dst[26] = src[49] & 31; | |
| dst[27] = src[35] >> 6 | (src[34] & 1) << 2; | |
| dst[28] = src[35] >> 5 & 1; | |
| dst[29] = src[34] >> 1; | |
| dst[30] = src[22]; | |
| dst[31] = src[52] >> 3 & 15; | |
| dst[32] = src[52] >> 7; | |
| dst[33] = src[53] >> 1 & 31; | |
| dst[34] = src[53] >> 6 | (src[52] & 7) << 2; | |
| dst[35] = 3; | |
| dst[36] = src[46] >> 1 & 3; | |
| dst[37] = src[47] >> 5 | (src[46] & 1) << 3; | |
| dst[38] = src[46] >> 3; | |
| dst[39] = src[47] & 31; | |
| dst[40] = src[51] >> 5 | (src[50] & 1) << 3; | |
| dst[41] = src[50] >> 6; | |
| dst[42] = src[51] & 31; | |
| dst[43] = src[44] & 15; | |
| dst[44] = src[44] >> 4; | |
| dst[45] = src[45] >> 3; | |
| _MiiDecoder._convertFieldsVer3ToNx(dst); | |
| return dst; | |
| } | |
| /** | |
| * Decodes the input Switch nn::mii::CharInfo format. | |
| * @public | |
| * @param {Uint8Array} dst | |
| * @param {Uint8Array} src | |
| */ | |
| static fromNxCharInfo(dst, src) { | |
| dst[0] = src[74]; | |
| dst[1] = src[75]; | |
| dst[2] = src[42]; | |
| dst[3] = src[55]; | |
| dst[4] = src[53]; | |
| dst[5] = src[56]; | |
| dst[6] = src[54]; | |
| dst[7] = src[52]; | |
| dst[8] = src[57]; | |
| dst[9] = src[58]; | |
| dst[10] = src[62]; | |
| dst[11] = src[60]; | |
| dst[12] = src[63]; | |
| dst[13] = src[61]; | |
| dst[14] = src[59]; | |
| dst[15] = src[64]; | |
| dst[16] = src[65]; | |
| dst[17] = src[46]; | |
| dst[18] = src[48]; | |
| dst[19] = src[45]; | |
| dst[20] = src[47]; | |
| dst[21] = src[39]; | |
| dst[22] = src[40]; | |
| dst[23] = src[80]; | |
| dst[24] = src[81]; | |
| dst[25] = src[79]; | |
| dst[26] = src[82]; | |
| dst[27] = src[50]; | |
| dst[28] = src[51]; | |
| dst[29] = src[49]; | |
| dst[30] = src[41]; | |
| dst[31] = src[84]; | |
| dst[32] = src[83]; | |
| dst[33] = src[85]; | |
| dst[34] = src[86]; | |
| dst[35] = src[72]; | |
| dst[36] = src[70]; | |
| dst[37] = src[71]; | |
| dst[38] = src[69]; | |
| dst[39] = src[73]; | |
| dst[40] = src[77]; | |
| dst[41] = src[76]; | |
| dst[42] = src[78]; | |
| dst[43] = src[67]; | |
| dst[44] = src[66]; | |
| dst[45] = src[68]; | |
| return dst; | |
| } | |
| /** | |
| * Decodes the input Switch nn::mii::CoreData (database) format. | |
| * @public | |
| * @param {Uint8Array} dst | |
| * @param {Uint8Array} src | |
| */ | |
| static fromNxCoreData(dst, src) { | |
| dst[0] = src[7] & 127; | |
| dst[1] = src[13] >> 5; | |
| dst[2] = src[2] & 127; | |
| dst[3] = src[17] >> 5; | |
| dst[4] = src[4] & 127; | |
| dst[5] = src[16] >> 5; | |
| dst[6] = src[18] >> 5; | |
| dst[7] = src[9] & 63; | |
| dst[8] = src[23] >> 4; | |
| dst[9] = src[11] & 31; | |
| dst[10] = src[15] >> 5; | |
| dst[11] = src[5] & 127; | |
| dst[12] = src[24] >> 4; | |
| dst[13] = src[24] & 15; | |
| dst[14] = src[12] & 31; | |
| dst[15] = src[25] & 15; | |
| dst[16] = (src[25] >> 4) + 3; | |
| dst[17] = src[22] & 15; | |
| dst[18] = src[23] & 15; | |
| dst[19] = src[21] >> 4; | |
| dst[20] = src[22] >> 4; | |
| dst[21] = src[21] & 15; | |
| dst[22] = src[4] >> 7; | |
| dst[23] = src[8] & 127; | |
| dst[24] = src[11] >> 5; | |
| dst[25] = src[20] & 31; | |
| dst[26] = src[17] & 31; | |
| dst[27] = src[3] & 127; | |
| dst[28] = src[2] >> 7; | |
| dst[29] = src[0]; | |
| dst[30] = src[1] & 127; | |
| dst[31] = src[27] >> 4; | |
| dst[32] = src[1] >> 7; | |
| dst[33] = src[18] & 31; | |
| dst[34] = src[19] & 31; | |
| dst[35] = src[14] >> 5; | |
| dst[36] = src[6] & 127; | |
| dst[37] = src[26] >> 4; | |
| dst[38] = src[10] & 63; | |
| dst[39] = src[15] & 31; | |
| dst[40] = src[27] & 15; | |
| dst[41] = src[12] >> 5; | |
| dst[42] = src[16] & 31; | |
| dst[43] = src[26] & 15; | |
| dst[44] = src[13] & 31; | |
| dst[45] = src[14] & 31; | |
| return dst; | |
| } | |
| /** | |
| * Common method to convert colors and other fields from | |
| * 3DS/Wii U and Wii data to Switch/Studio equivalents. | |
| * @param {Uint8Array} buf | |
| */ | |
| static _convertFieldsVer3ToNx(buf) { | |
| if (buf[27] == 0) | |
| buf[27] = 8; | |
| if (buf[0] == 0) | |
| buf[0] = 8; | |
| if (buf[11] == 0) | |
| buf[11] = 8; | |
| buf[36] += 19; | |
| buf[4] += 8; | |
| if (buf[23] == 0) | |
| buf[23] = 8; | |
| else if (buf[23] < 6) | |
| buf[23] += 13; | |
| if (127 < buf[2]) | |
| buf[2] = 127; | |
| if (127 < buf[30]) | |
| buf[30] = 127; | |
| } | |
| /** | |
| * Obfuscates Studio data to be used in the URL. | |
| * @public | |
| * @param {Uint8Array} dst | |
| * @param {Uint8Array} src | |
| * @param {number} [seed=0] The random value to use for the obfuscation. Best left as 0. | |
| */ | |
| static obfuscateStudioUrl(dst, src, seed = 0) { | |
| dst[0] = seed; | |
| for (let i = 0; i < 46; i++) { | |
| let val = src[i] ^ dst[i]; | |
| dst[i + 1] = (7 + val) % 256; | |
| } | |
| } | |
| /** | |
| * Deobfuscates Studio URL data to raw decodable data. | |
| * @param {Uint8Array} dst | |
| * @param {Uint8Array} src | |
| */ | |
| static _deobfuscateStudioUrl(dst, src) { | |
| for (let i = 0; i < 46; i++) { | |
| let val = (src[i + 1] - 7) % 256; | |
| dst[i] = val ^ src[i]; | |
| } | |
| } | |
| /** | |
| * @public | |
| * @param {MiiExtraInfo} dst | |
| * @param {Uint8Array} src | |
| */ | |
| static setExtraFromStudioData(dst, src) { | |
| dst.isNx = true; | |
| dst.height = src[30]; | |
| dst.build = src[2]; | |
| dst.skinColor = src[17]; | |
| dst.gender = src[22]; | |
| dst.favoriteColor = src[21]; | |
| dst.fontRegion = 0; | |
| dst.birthMonth = 0; | |
| dst.birthDay = 0; | |
| dst.favorite = false; | |
| dst.copyable = false; | |
| dst.ngWord = false; | |
| dst.localonly = false; | |
| dst.regionMove = 0; | |
| dst.nicknameSize = dst.creatorNameSize = 0; | |
| } | |
| /** | |
| * @public | |
| * @param {MiiExtraInfo} dst | |
| * @param {Uint8Array} src | |
| */ | |
| static setExtraFrom3dsWiiuData(dst, src) { | |
| dst.isNx = false; | |
| dst.fontRegion = src[1] >> 4 & 3; | |
| dst.birthMonth = src[24] >> 1 & 15; | |
| dst.birthDay = (src[25] & 3) << 3 | src[24] >> 5; | |
| dst.favorite = (src[25] >> 6 & 1) == 1; | |
| dst.copyable = (src[1] & 1) == 1; | |
| dst.ngWord = (src[1] >> 1 & 1) == 1; | |
| dst.localonly = (src[48] & 1) == 1; | |
| dst.regionMove = src[1] >> 2 & 3; | |
| dst.authorId.set(src.subarray(4, 12)); | |
| dst.createId.set(src.subarray(12, 22)); | |
| const name = new Uint16Array(10); | |
| for (let i = 0; i < 10; i++) | |
| name[i] = src[26 + i * 2] | src[26 + i * 2 + 1] << 8; | |
| const creator = new Uint16Array(10); | |
| for (let i = 0; i < 10; i++) | |
| creator[i] = src[72 + i * 2] | src[72 + i * 2 + 1] << 8; | |
| dst.nicknameSize = _MiiDecoder._char16ToUtf8(dst.nicknameData, name, 10); | |
| dst.creatorNameSize = _MiiDecoder._char16ToUtf8(dst.creatorNameData, creator, 10); | |
| } | |
| /** | |
| * @public | |
| * @param {MiiExtraInfo} dst | |
| * @param {Uint8Array} src | |
| */ | |
| static setExtraFromWiiData(dst, src) { | |
| dst.isNx = false; | |
| dst.fontRegion = 0; | |
| dst.birthMonth = src[0] >> 2 & 15; | |
| dst.birthDay = src[1] >> 5 | (src[0] & 3) << 3; | |
| dst.favorite = (src[1] & 1) == 1; | |
| dst.copyable = false; | |
| dst.ngWord = false; | |
| dst.localonly = (src[33] >> 2 & 1) == 1; | |
| dst.regionMove = 0; | |
| dst.createId.set(src.subarray(24, 32)); | |
| const name = new Uint16Array(10); | |
| for (let i = 0; i < 10; i++) | |
| name[i] = src[2 + i * 2] << 8 | src[2 + i * 2 + 1]; | |
| const creator = new Uint16Array(10); | |
| for (let i = 0; i < 10; i++) | |
| creator[i] = src[54 + i * 2] << 8 | src[54 + i * 2 + 1]; | |
| dst.nicknameSize = _MiiDecoder._char16ToUtf8(dst.nicknameData, name, 10); | |
| dst.creatorNameSize = _MiiDecoder._char16ToUtf8(dst.creatorNameData, creator, 10); | |
| } | |
| /** | |
| * @public | |
| * @param {MiiExtraInfo} dst | |
| * @param {Uint8Array} src | |
| */ | |
| static setExtraFromNxCharInfo(dst, src) { | |
| dst.isNx = true; | |
| dst.fontRegion = src[38]; | |
| dst.createId.set(src.subarray(0, 16)); | |
| const name = new Uint16Array(10); | |
| for (let i = 0; i < 10; i++) | |
| name[i] = src[16 + i * 2] | src[16 + i * 2 + 1] << 8; | |
| dst.nicknameSize = _MiiDecoder._char16ToUtf8(dst.nicknameData, name, 10); | |
| } | |
| /** | |
| * @public | |
| * @param {MiiExtraInfo} dst | |
| * @param {Uint8Array} src | |
| */ | |
| static setExtraFromNxCoreData(dst, src) { | |
| dst.isNx = true; | |
| dst.fontRegion = src[10] >> 6; | |
| const name = new Uint16Array(10); | |
| for (let i = 0; i < 10; i++) | |
| name[i] = src[28 + i * 2] | src[28 + i * 2 + 1] << 8; | |
| dst.nicknameSize = _MiiDecoder._char16ToUtf8(dst.nicknameData, name, 10); | |
| } | |
| /** | |
| * Writes UTF-8 text from a buffer of 16-bit wide characters | |
| * that are within the Unicode BMP, also known as UCS-2 encoded text. | |
| * This does not support decoding UTF-16 surrogate pairs for | |
| * characters beyond the Basic Multilingual Plane (emoji, etc.) | |
| * @param {Uint8Array} dst Destination UTF-8 bytes to write to. Size MUST be 3 * characterCount. | |
| * @param {Uint16Array} src Source array of 16-bit code points. | |
| * @param {number} characterCount Amount of characters from the original string to process. | |
| */ | |
| static _char16ToUtf8(dst, src, characterCount) { | |
| let iDst = 0; | |
| for (let i = 0; i < characterCount && src[i] != 0; i++) { | |
| let chr = src[i]; | |
| if (chr <= 127) | |
| dst[iDst++] = chr; | |
| else if (chr <= 2047) { | |
| dst[iDst + 0] = 192 | chr >> 6; | |
| dst[iDst + 1] = 128 | chr & 63; | |
| iDst += 2; | |
| } else { | |
| if (chr >= 55296 && chr <= 57343) | |
| chr = 65533; | |
| dst[iDst + 0] = 224 | chr >> 12; | |
| dst[iDst + 1] = 128 | chr >> 6 & 63; | |
| dst[iDst + 2] = 128 | chr & 63; | |
| iDst += 3; | |
| } | |
| } | |
| return iDst; | |
| } | |
| /** | |
| * @param {Uint16Array} dst | |
| * @param {Uint8Array} src | |
| * @param {number} srcSize | |
| */ | |
| static _utf8ToChar16(dst, src, srcSize) { | |
| let iDst = 0; | |
| for (let i = 0; i < srcSize && src[i] != 0; iDst++) { | |
| let chr = 0; | |
| if ((src[i] & 224) == 224 && (src[i] & 16) == 0) { | |
| chr = (src[i + 0] & 15) << 12 | (src[i + 1] & 63) << 6 | (src[i + 2] & 63) << 0; | |
| i += 3; | |
| } else if ((src[i] & 192) == 192 && (src[i] & 32) == 0) { | |
| chr = (src[i + 0] & 31) << 6 | (src[i + 1] & 63) << 0; | |
| i += 2; | |
| } else | |
| chr = src[i++] & 127; | |
| dst[iDst] = chr; | |
| } | |
| return iDst; | |
| } | |
| /** | |
| * @readonly | |
| * @type {Uint8Array} | |
| */ | |
| static _FROM_WII_DATA_FACE_TEX_TABLE = new Uint8Array([ | |
| 0, | |
| 0, | |
| 0, | |
| 1, | |
| 0, | |
| 6, | |
| 0, | |
| 9, | |
| 5, | |
| 0, | |
| 2, | |
| 0, | |
| 3, | |
| 0, | |
| 7, | |
| 0, | |
| 8, | |
| 0, | |
| 0, | |
| 10, | |
| 9, | |
| 0, | |
| 11, | |
| 0 | |
| ]); | |
| }; | |
| var MiiExtraInfo = class { | |
| /** | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static _NAME_LENGTH = 10; | |
| /** | |
| * True if the info is from NX Mii data. | |
| * @public | |
| * @type {boolean} | |
| */ | |
| isNx; | |
| /** | |
| * Mii's nickname as UTF-8. | |
| * @public | |
| * @readonly | |
| * @type {Uint8Array} | |
| */ | |
| nicknameData = new Uint8Array(30); | |
| /** | |
| * Mii's creator name as UTF-8. | |
| * Only present if IsNx = false. | |
| * @public | |
| * @readonly | |
| * @type {Uint8Array} | |
| */ | |
| creatorNameData = new Uint8Array(30); | |
| /** | |
| * Size of buffer for nickname in bytes. | |
| * @public | |
| * @type {number} | |
| */ | |
| nicknameSize; | |
| /** | |
| * Size of buffer for creator name in bytes. | |
| * @public | |
| * @type {number} | |
| */ | |
| creatorNameSize; | |
| /** | |
| * @public | |
| * @type {number} | |
| */ | |
| height; | |
| /** | |
| * @public | |
| * @type {number} | |
| */ | |
| build; | |
| /** | |
| * @public | |
| * @type {number} | |
| */ | |
| skinColor; | |
| /** | |
| * @public | |
| * @type {number} | |
| */ | |
| gender; | |
| /** | |
| * @public | |
| * @type {number} | |
| */ | |
| favoriteColor; | |
| /** | |
| * @public | |
| * @type {number} | |
| */ | |
| fontRegion; | |
| /** | |
| * Only present if IsNx = false. | |
| * @public | |
| * @type {number} | |
| */ | |
| birthMonth; | |
| /** | |
| * Only present if IsNx = false. | |
| * @public | |
| * @type {number} | |
| */ | |
| birthDay; | |
| /** | |
| * Only present if IsNx = false. | |
| * @public | |
| * @type {boolean} | |
| */ | |
| favorite; | |
| /** | |
| * Only present if IsNx = false. | |
| * @public | |
| * @type {boolean} | |
| */ | |
| copyable; | |
| /** | |
| * Only present if IsNx = false. | |
| * @public | |
| * @type {boolean} | |
| */ | |
| ngWord; | |
| /** | |
| * Only present if IsNx = false. | |
| * @public | |
| * @type {boolean} | |
| */ | |
| localonly; | |
| /** | |
| * Only present if IsNx = false. | |
| * @public | |
| * @type {number} | |
| */ | |
| regionMove; | |
| /** | |
| * Mii unique ID. | |
| * When IsNx = true, this is a 16-byte UUIDv4, | |
| * otherwise 10 bytes (3DS/Wii U) or 8 bytes (Wii). | |
| * @public | |
| * @readonly | |
| * @type {Uint8Array} | |
| */ | |
| createId = new Uint8Array(16); | |
| /** | |
| * Mii console ID only for 3DS/Wii U data. | |
| * Only present if IsNx = false. | |
| * This is equal to the Transferable ID (not permanent). | |
| * @public | |
| * @readonly | |
| * @type {Uint8Array} | |
| */ | |
| authorId = new Uint8Array(8); | |
| /** | |
| * @public | |
| */ | |
| getNickname() { | |
| return new TextDecoder().decode(this.nicknameData.subarray(0, this.nicknameSize)); | |
| } | |
| /** | |
| * @public | |
| */ | |
| getCreatorName() { | |
| return new TextDecoder().decode(this.creatorNameData.subarray(0, this.creatorNameSize)); | |
| } | |
| }; | |
| // ../../WrappedStoreData/WrappedStoreDataTest.js | |
| var Aes128 = class _Aes128 { | |
| /** | |
| * @readonly | |
| * @type {Int32Array} | |
| */ | |
| _encKey = new Int32Array(44); | |
| /** | |
| * @readonly | |
| * @type {Int32Array} | |
| */ | |
| _encTable0 = new Int32Array(256); | |
| /** | |
| * @readonly | |
| * @type {Int32Array} | |
| */ | |
| _encTable1 = new Int32Array(256); | |
| /** | |
| * @readonly | |
| * @type {Int32Array} | |
| */ | |
| _encTable2 = new Int32Array(256); | |
| /** | |
| * @readonly | |
| * @type {Int32Array} | |
| */ | |
| _encTable3 = new Int32Array(256); | |
| /** | |
| * @readonly | |
| * @type {Int32Array} | |
| */ | |
| _sbox = new Int32Array(256); | |
| /** | |
| * Initializes AES-128 with the given 16-byte key. | |
| * Must be called before <code>EncryptBlock</code>. | |
| * Runs table pre-computation and key schedule. | |
| * Ported from sjcl.cipher.aes constructor and _precompute: | |
| * https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/aes.js_L25 | |
| * @public | |
| * @param {Readonly<Uint8Array>} key The 16-byte AES key. | |
| * @param {number} keyOffset Offset into <code>key</code>. | |
| */ | |
| initialize(key, keyOffset) { | |
| this._precompute(); | |
| this._scheduleKey(key, keyOffset); | |
| } | |
| /** | |
| * Shifts a byte value (0-255) into the top byte of a 32-bit signed int: result = b << 24. | |
| * Avoids signed-integer overflow (which is undefined behaviour in C and not guaranteed | |
| * to wrap in Fusion/Python) by splitting around the sign bit: | |
| * (b & 0x7F) << 24 is at most 0x7F000000 = 2130706432, which fits in int. | |
| * If bit 7 of b is set we subtract 2147483648 (INT_MIN) to set the sign bit. | |
| * @param {number} b | |
| */ | |
| static _byteToTop(b) { | |
| let hi = (b & 127) << 24; | |
| if ((b & 128) != 0) | |
| hi = Number(hi - 2147483648); | |
| return hi; | |
| } | |
| /** | |
| * Rotates a 32-bit integer left by 8 bits (= right by 24): [b3,b2,b1,b0] -> [b2,b1,b0,b3]. | |
| * Equivalent to <code>x << 24 ^ x >> 8 & 0xFFFFFF</code>, but calls ByteToTop to avoid overflow. | |
| * @param {number} x | |
| */ | |
| static _rot8Left(x) { | |
| return _Aes128._byteToTop(x & 255) ^ x >> 8 & 16777215; | |
| } | |
| /** | |
| * Loads a big-endian 32-bit word from <code>buf[o..o+4)</code>. | |
| * @param {Readonly<Uint8Array>} buf | |
| * @param {number} o | |
| */ | |
| static _loadWord(buf, o) { | |
| return _Aes128._byteToTop(buf[o] & 255) | (buf[o + 1] & 255) << 16 | (buf[o + 2] & 255) << 8 | buf[o + 3] & 255; | |
| } | |
| /** | |
| * Stores a big-endian 32-bit word into <code>buf[o..o+4)</code>. | |
| * @param {Uint8Array} buf | |
| * @param {number} o | |
| * @param {number} w | |
| */ | |
| static _storeWord(buf, o, w) { | |
| buf[o] = w >> 24 & 255; | |
| buf[o + 1] = w >> 16 & 255; | |
| buf[o + 2] = w >> 8 & 255; | |
| buf[o + 3] = w & 255; | |
| } | |
| /** | |
| * Pre-computes the AES S-box and MixColumns tables. | |
| * | |
| * <p>Ported from sjcl.cipher.aes._precompute: | |
| * https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/aes.js_L114 | |
| * <p>The loop mirrors sjcl's <code>for (x = xInv = 0; !sbox[x]; x ^= x2 || 1, xInv = th[xInv] || 1)</code>. | |
| * It processes all 256 GF(2^8) elements and stops when sbox[x] is already populated. | |
| */ | |
| _precompute() { | |
| this._sbox.fill(0); | |
| const d = new Int32Array(256); | |
| const th = new Int32Array(256); | |
| for (let i = 0; i < 256; i++) { | |
| d[i] = i << 1 ^ (i >> 7) * 283; | |
| th[d[i] ^ i] = i; | |
| } | |
| let x = 0; | |
| let xInv = 0; | |
| for (; ; ) { | |
| if (this._sbox[x] != 0) | |
| break; | |
| let s = xInv ^ xInv << 1 ^ xInv << 2 ^ xInv << 3 ^ xInv << 4; | |
| s = s >> 8 ^ s & 255 ^ 99; | |
| this._sbox[x] = s; | |
| let x2 = d[x]; | |
| let x4 = d[x2]; | |
| let x8 = d[x4]; | |
| let tEnc = d[s] * 257 ^ s * 16843008; | |
| tEnc = _Aes128._rot8Left(tEnc); | |
| this._encTable0[x] = tEnc; | |
| tEnc = _Aes128._rot8Left(tEnc); | |
| this._encTable1[x] = tEnc; | |
| tEnc = _Aes128._rot8Left(tEnc); | |
| this._encTable2[x] = tEnc; | |
| tEnc = _Aes128._rot8Left(tEnc); | |
| this._encTable3[x] = tEnc; | |
| let x2or1 = x2; | |
| if (x2or1 == 0) | |
| x2or1 = 1; | |
| x ^= x2or1; | |
| let nextXInv = th[xInv]; | |
| if (nextXInv == 0) | |
| nextXInv = 1; | |
| xInv = nextXInv; | |
| } | |
| } | |
| /** | |
| * Expands the 16-byte AES-128 key into the 44-word encryption key schedule. | |
| * Ported from the key expansion loop in sjcl.cipher.aes constructor: | |
| * https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/aes.js_L42 | |
| * @param {Readonly<Uint8Array>} key | |
| * @param {number} keyOffset | |
| */ | |
| _scheduleKey(key, keyOffset) { | |
| for (let i = 0; i < 4; i++) { | |
| this._encKey[i] = _Aes128._loadWord(key, keyOffset + i * 4); | |
| } | |
| let rcon = 1; | |
| for (let i = 4; i < 44; i++) { | |
| let tmp = this._encKey[i - 1]; | |
| if (i % 4 == 0) { | |
| tmp = _Aes128._byteToTop(this._sbox[tmp >> 16 & 255]) ^ this._sbox[tmp >> 8 & 255] << 16 ^ this._sbox[tmp & 255] << 8 ^ this._sbox[tmp >> 24 & 255] ^ _Aes128._byteToTop(rcon); | |
| rcon = rcon << 1 ^ (rcon >> 7) * 283; | |
| } | |
| this._encKey[i] = this._encKey[i - 4] ^ tmp; | |
| } | |
| } | |
| /** | |
| * Encrypts one 16-byte block in-place (big-endian word order). | |
| * Ported from sjcl.cipher.aes._crypt (encrypt direction, dir=0): | |
| * https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/aes.js_L156 | |
| * @public | |
| * @param {Uint8Array} block 16-byte block buffer. Encrypted in-place. | |
| * @param {number} offset Offset into <code>block</code>. | |
| */ | |
| encryptBlock(block, offset) { | |
| let a = _Aes128._loadWord(block, offset) ^ this._encKey[0]; | |
| let b = _Aes128._loadWord(block, offset + 4) ^ this._encKey[1]; | |
| let c = _Aes128._loadWord(block, offset + 8) ^ this._encKey[2]; | |
| let d = _Aes128._loadWord(block, offset + 12) ^ this._encKey[3]; | |
| let a2; | |
| let b2; | |
| let c2; | |
| let kIndex = 4; | |
| for (let i = 0; i < 9; i++) { | |
| a2 = this._encTable0[a >> 24 & 255] ^ this._encTable1[b >> 16 & 255] ^ this._encTable2[c >> 8 & 255] ^ this._encTable3[d & 255] ^ this._encKey[kIndex]; | |
| b2 = this._encTable0[b >> 24 & 255] ^ this._encTable1[c >> 16 & 255] ^ this._encTable2[d >> 8 & 255] ^ this._encTable3[a & 255] ^ this._encKey[kIndex + 1]; | |
| c2 = this._encTable0[c >> 24 & 255] ^ this._encTable1[d >> 16 & 255] ^ this._encTable2[a >> 8 & 255] ^ this._encTable3[b & 255] ^ this._encKey[kIndex + 2]; | |
| d = this._encTable0[d >> 24 & 255] ^ this._encTable1[a >> 16 & 255] ^ this._encTable2[b >> 8 & 255] ^ this._encTable3[c & 255] ^ this._encKey[kIndex + 3]; | |
| kIndex += 4; | |
| a = a2; | |
| b = b2; | |
| c = c2; | |
| } | |
| let out0 = _Aes128._byteToTop(this._sbox[a >> 24 & 255]) ^ this._sbox[b >> 16 & 255] << 16 ^ this._sbox[c >> 8 & 255] << 8 ^ this._sbox[d & 255] ^ this._encKey[kIndex]; | |
| let out1 = _Aes128._byteToTop(this._sbox[b >> 24 & 255]) ^ this._sbox[c >> 16 & 255] << 16 ^ this._sbox[d >> 8 & 255] << 8 ^ this._sbox[a & 255] ^ this._encKey[kIndex + 1]; | |
| let out2 = _Aes128._byteToTop(this._sbox[c >> 24 & 255]) ^ this._sbox[d >> 16 & 255] << 16 ^ this._sbox[a >> 8 & 255] << 8 ^ this._sbox[b & 255] ^ this._encKey[kIndex + 2]; | |
| let out3 = _Aes128._byteToTop(this._sbox[d >> 24 & 255]) ^ this._sbox[a >> 16 & 255] << 16 ^ this._sbox[b >> 8 & 255] << 8 ^ this._sbox[c & 255] ^ this._encKey[kIndex + 3]; | |
| _Aes128._storeWord(block, offset, out0); | |
| _Aes128._storeWord(block, offset + 4, out1); | |
| _Aes128._storeWord(block, offset + 8, out2); | |
| _Aes128._storeWord(block, offset + 12, out3); | |
| } | |
| }; | |
| var AesCcmCtr = class { | |
| /** | |
| * @private | |
| */ | |
| constructor() { | |
| } | |
| /** | |
| * Runs AES-CCM CTR mode on <code>data</code>, XORing each 16-byte block with | |
| * AES(key, counter), where the counter is initialized from the nonce | |
| * and incremented for each block. | |
| * | |
| * <p>This is used for both encryption and decryption (CTR mode is symmetric). | |
| * Tag verification is intentionally skipped — see the 3DS AES CCM errata: | |
| * https://www.3dbrew.org/wiki/AES_Registers_CCM_mode_pitfall | |
| * <p>Ported from sjcl.mode.ccm._ctrMode: | |
| * https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/ccm.js_L194 | |
| * <p>Counter block layout (16 bytes): | |
| * [0] = L - 1 (flags byte; L=3 for 3DS so this is 0x02) | |
| * [1..12) = nonce (11 bytes, nonce is clamped to 8*(15-L) = 96 bits) | |
| * [12..16) = 0x00000000 (counter, starts at 0 for tag, 1 for data) | |
| * <p>The data counter starts at 1 (counter 0 is reserved for the tag). | |
| * @public | |
| * @param {Aes128} aes Aes128 instance initialized with the key. | |
| * @param {Uint8Array} data Data buffer to XOR in-place (plaintext for encrypt, ciphertext for decrypt). | |
| * @param {number} dataOffset Offset into <code>data</code>. | |
| * @param {number} dataLength Number of bytes to process. | |
| * @param {Readonly<Uint8Array>} nonce Nonce buffer. Must be at least <code>nonceLength</code> bytes at <code>nonceOffset</code>. | |
| * @param {number} nonceOffset Offset into <code>nonce</code>. | |
| * @param {number} nonceLength Length of the nonce in bytes. For 3DS CCM this is 12, but only | |
| * 11 bytes are used after clamping to 8*(15-L) bits with L=3. | |
| */ | |
| static ctrXor(aes, data, dataOffset, dataLength, nonce, nonceOffset, nonceLength) { | |
| const ctr = new Uint8Array(16); | |
| ctr[0] = 2; | |
| let nonceBytes = nonceLength; | |
| if (nonceBytes > 11) | |
| nonceBytes = 11; | |
| ctr.set(nonce.subarray(nonceOffset, nonceOffset + nonceBytes), 1); | |
| ctr[15] = 1; | |
| const block = new Uint8Array(16); | |
| let processed = 0; | |
| for (; ; ) { | |
| if (processed >= dataLength) | |
| break; | |
| block.set(ctr); | |
| aes.encryptBlock(block, 0); | |
| let blockLen = dataLength - processed; | |
| if (blockLen > 16) | |
| blockLen = 16; | |
| for (let j = 0; j < blockLen; j++) { | |
| data[dataOffset + processed + j] ^= block[j]; | |
| } | |
| processed += blockLen; | |
| ctr[15]++; | |
| if (ctr[15] == 0) { | |
| ctr[14]++; | |
| if (ctr[14] == 0) { | |
| ctr[13]++; | |
| if (ctr[13] == 0) { | |
| ctr[12]++; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /** | |
| * Computes the AES-CCM CBC-MAC authentication tag over <code>data</code>. | |
| * | |
| * <p>Ported from sjcl.mode.ccm._computeTag and _macAdditionalData (no adata): | |
| * https://github.com/bitwiseshiftleft/sjcl/blob/85caa53c/core/ccm.js_L156 | |
| * <p>The tag is then encrypted with CTR counter 0 (before the data blocks). | |
| * The result is written into <code>tagOut</code>. | |
| * @public | |
| * @param {Aes128} aes Aes128 instance initialized with the key. | |
| * @param {Readonly<Uint8Array>} data Plaintext data to MAC. | |
| * @param {number} dataOffset Offset into <code>data</code>. | |
| * @param {number} dataLength Length of data. | |
| * @param {Readonly<Uint8Array>} nonce Nonce buffer. | |
| * @param {number} nonceOffset Offset into <code>nonce</code>. | |
| * @param {number} nonceLength Length of nonce in bytes. | |
| * @param {number} tagLength Desired tag length in bytes (must be even, 4..16). | |
| * @param {Uint8Array} tagOut Output buffer for the encrypted tag. Must be at least <code>tagLength</code> bytes. | |
| * @param {number} tagOutOffset Offset into <code>tagOut</code>. | |
| */ | |
| static computeTag(aes, data, dataOffset, dataLength, nonce, nonceOffset, nonceLength, tagLength, tagOut, tagOutOffset) { | |
| let l = 3; | |
| let flags = (tagLength / 2 | 0) - 1 << 3 | l - 1; | |
| const mac = new Uint8Array(16); | |
| mac[0] = flags; | |
| let nonceBytes = nonceLength; | |
| if (nonceBytes > 11) | |
| nonceBytes = 11; | |
| mac.set(nonce.subarray(nonceOffset, nonceOffset + nonceBytes), 1); | |
| mac[13] = dataLength >> 16 & 255; | |
| mac[14] = dataLength >> 8 & 255; | |
| mac[15] = dataLength & 255; | |
| aes.encryptBlock(mac, 0); | |
| const block = new Uint8Array(16); | |
| let processed = 0; | |
| for (; ; ) { | |
| if (processed >= dataLength) | |
| break; | |
| let blockLen = dataLength - processed; | |
| if (blockLen > 16) | |
| blockLen = 16; | |
| block.fill(0); | |
| block.set(data.subarray(dataOffset + processed, dataOffset + processed + blockLen)); | |
| for (let j = 0; j < 16; j++) { | |
| mac[j] ^= block[j]; | |
| } | |
| aes.encryptBlock(mac, 0); | |
| processed += blockLen; | |
| } | |
| const ctr0 = new Uint8Array(16); | |
| ctr0[0] = 2; | |
| let nb = nonceLength; | |
| if (nb > 11) | |
| nb = 11; | |
| ctr0.set(nonce.subarray(nonceOffset, nonceOffset + nb), 1); | |
| aes.encryptBlock(ctr0, 0); | |
| for (let i = 0; i < tagLength; i++) { | |
| tagOut[tagOutOffset + i] = mac[i] ^ ctr0[i]; | |
| } | |
| } | |
| }; | |
| var AesCcmCipher = class { | |
| }; | |
| var NativeAesCcmCipher = class extends AesCcmCipher { | |
| /** | |
| * @readonly | |
| * @type {Aes128} | |
| */ | |
| _aes = new Aes128(); | |
| /** | |
| * Initializes the cipher with the given 16-byte key. | |
| * @public | |
| * @param {Readonly<Uint8Array>} key | |
| * @param {number} keyOffset | |
| */ | |
| initialize(key, keyOffset) { | |
| this._aes.initialize(key, keyOffset); | |
| } | |
| /** | |
| * @public | |
| * @param {Uint8Array} dst | |
| * @param {number} dstOffset | |
| * @param {Readonly<Uint8Array>} content | |
| * @param {number} contentOffset | |
| * @param {number} contentLength | |
| * @param {Readonly<Uint8Array>} nonce | |
| * @param {number} nonceOffset | |
| * @param {number} nonceLength | |
| * @param {number} tagLength | |
| */ | |
| encrypt(dst, dstOffset, content, contentOffset, contentLength, nonce, nonceOffset, nonceLength, tagLength) { | |
| dst.set(content.subarray(contentOffset, contentOffset + contentLength), dstOffset); | |
| AesCcmCtr.computeTag(this._aes, dst, dstOffset, contentLength, nonce, nonceOffset, nonceLength, tagLength, dst, dstOffset + contentLength); | |
| AesCcmCtr.ctrXor(this._aes, dst, dstOffset, contentLength, nonce, nonceOffset, nonceLength); | |
| } | |
| /** | |
| * @public | |
| * @param {Uint8Array} dst | |
| * @param {number} dstOffset | |
| * @param {Readonly<Uint8Array>} ciphertext | |
| * @param {number} ciphertextOffset | |
| * @param {number} ciphertextLength | |
| * @param {Readonly<Uint8Array>} nonce | |
| * @param {number} nonceOffset | |
| * @param {number} nonceLength | |
| * @param {number} tagLength | |
| */ | |
| decryptSkipTag(dst, dstOffset, ciphertext, ciphertextOffset, ciphertextLength, nonce, nonceOffset, nonceLength, tagLength) { | |
| dst.set(ciphertext.subarray(ciphertextOffset, ciphertextOffset + ciphertextLength), dstOffset); | |
| AesCcmCtr.ctrXor(this._aes, dst, dstOffset, ciphertextLength, nonce, nonceOffset, nonceLength); | |
| } | |
| }; | |
| var WrappedStoreData = class { | |
| /** | |
| * @private | |
| */ | |
| constructor() { | |
| } | |
| /** | |
| * Size of encrypted Mii data found in QR codes (CFLiWrappedMiiData, FFLiWrappedStoreData). | |
| * @public | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static LENGTH = 112; | |
| /** | |
| * Size of 3DS/Wii U format Mii data (FFLStoreData, CFLiMiiDataPacket, nn::mii::Ver3StoreData). | |
| * @public | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static STORE_DATA_LENGTH = 96; | |
| /** | |
| * Size of the AES-CCM nonce (IV) within wrapped data. | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static _NONCE_LENGTH = 12; | |
| /** | |
| * Size of the AES-CCM authentication tag (MAC) within wrapped data. | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static _TAG_LENGTH = 16; | |
| /** | |
| * Offset within StoreData of the ID field used to form the nonce (CreateID). | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static _ID_OFFSET = 12; | |
| /** | |
| * Number of bytes of the ID copied into the nonce. | |
| * The CreateID is 10 bytes, but it is truncated to be a multiple of 4 | |
| * and no larger than the nonce, so it is 8 bytes. | |
| * @readonly | |
| * @type {number} | |
| */ | |
| static _ID_LENGTH = 8; | |
| /** | |
| * Gets 96-byte 3DS/Wii U format Mii data from wrapped QR code data. | |
| * | |
| * <p>Decrypts AES-CCM encrypted data (CFLiWrappedMiiData) using the provided cipher. | |
| * <p>The default AES-CCM tag verification fails due to the following errata: | |
| * https://www.3dbrew.org/wiki/AES_Registers_CCM_mode_pitfall | |
| * Therefore tag verification is skipped. You MUST verify the CRC-16 of | |
| * the output to ensure the data is valid. | |
| * <p>Layout of <code>encryptedData</code> (112 bytes total): | |
| * <ul> | |
| * <li>[0..8) ID / nonce prefix (8 bytes, copied verbatim into output at [12..20))</li> | |
| * <li>[8..96) ciphertext (88 bytes = StoreDataLength - IdLength)</li> | |
| * <li>[96..112) authentication tag (16 bytes, not verified)</li> | |
| * </ul> | |
| * @public | |
| * @param {Uint8Array} dst Destination for decrypted StoreData. Must be <code>StoreDataLength</code> (96) bytes. | |
| * @param {number} dstOffset Offset into <code>dst</code>. | |
| * @param {Readonly<Uint8Array>} encryptedData Encrypted wrapped Mii QR code data (CFLiWrappedMiiData). Must be at least 112 bytes. | |
| * @param {number} encryptedOffset Offset into <code>encryptedData</code>. | |
| * @param {AesCcmCipher} cipher AES-CCM cipher implementation to use for decryption. | |
| */ | |
| static decrypt(dst, dstOffset, encryptedData, encryptedOffset, cipher) { | |
| const nonce = new Uint8Array(12); | |
| nonce.set(encryptedData.subarray(encryptedOffset, encryptedOffset + 8)); | |
| let ciphertextLength = 88; | |
| const decrypted = new Uint8Array(88); | |
| cipher.decryptSkipTag(decrypted, 0, encryptedData, encryptedOffset + 8, ciphertextLength, nonce, 0, 12, 16); | |
| dst.set(decrypted.subarray(0, 12), dstOffset); | |
| dst.set(nonce.subarray(0, 8), dstOffset + 12); | |
| dst.set(decrypted.subarray(12, 12 + ciphertextLength - 12), dstOffset + 12 + 8); | |
| } | |
| /** | |
| * Encrypts 3DS/Wii U Mii StoreData into the wrapped format (CFLiWrappedMiiData) | |
| * for use in a Mii QR code. | |
| * | |
| * <p>Layout of output <code>dst</code> (112 bytes total): | |
| * <ul> | |
| * <li>[0..8) ID / nonce prefix (from storeData[12..20))</li> | |
| * <li>[8..96) ciphertext (88 bytes)</li> | |
| * <li>[96..112) authentication tag (16 bytes)</li> | |
| * </ul> | |
| * @public | |
| * @param {Uint8Array} dst Destination for encrypted wrapped data. Must be 112 bytes. | |
| * @param {number} dstOffset Offset into <code>dst</code>. | |
| * @param {Readonly<Uint8Array>} storeData Input 3DS/Wii U StoreData. Must be <code>StoreDataLength</code> (96) bytes starting at <code>storeDataOffset</code>. | |
| * @param {number} storeDataOffset Offset into <code>storeData</code>. | |
| * @param {AesCcmCipher} cipher AES-CCM cipher implementation to use for encryption. | |
| */ | |
| static encrypt(dst, dstOffset, storeData, storeDataOffset, cipher) { | |
| let idEndOffset = 20; | |
| const nonce = new Uint8Array(12); | |
| nonce.set(storeData.subarray(storeDataOffset + 12, storeDataOffset + 12 + 8)); | |
| const content = new Uint8Array(96); | |
| content.set(storeData.subarray(storeDataOffset, storeDataOffset + 12)); | |
| content.set(storeData.subarray(storeDataOffset + idEndOffset, storeDataOffset + idEndOffset + 96 - idEndOffset), 12); | |
| const encryptedBytes = new Uint8Array(112); | |
| cipher.encrypt(encryptedBytes, 0, content, 0, 96, nonce, 0, 12, 16); | |
| let correctEncryptedContentLength = 88; | |
| dst.set(nonce.subarray(0, 8), dstOffset); | |
| dst.set(encryptedBytes.subarray(0, correctEncryptedContentLength), dstOffset + 8); | |
| dst.set(encryptedBytes.subarray(96, 112), dstOffset + 96); | |
| } | |
| }; | |
| var WrappedStoreDataTest = class _WrappedStoreDataTest { | |
| /** | |
| * @readonly | |
| * @type {Readonly<Uint8Array>} | |
| */ | |
| static _WRAP_KEY_DEVELOPMENT = new Uint8Array([18, 223, 146, 182, 255, 212, 56, 171, 41, 28, 79, 212, 215, 206, 37, 109]); | |
| /** | |
| * @readonly | |
| * @type {Readonly<Uint8Array>} | |
| */ | |
| static _WRAP_TEST_DATA = new Uint8Array([ | |
| 3, | |
| 0, | |
| 0, | |
| 48, | |
| 223, | |
| 154, | |
| 52, | |
| 2, | |
| 131, | |
| 165, | |
| 234, | |
| 189, | |
| 144, | |
| 241, | |
| 7, | |
| 220, | |
| 120, | |
| 162, | |
| 160, | |
| 53, | |
| 216, | |
| 164, | |
| 0, | |
| 0, | |
| 1, | |
| 0, | |
| 81, | |
| 48, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 64, | |
| 64, | |
| 0, | |
| 0, | |
| 12, | |
| 1, | |
| 4, | |
| 104, | |
| 67, | |
| 24, | |
| 32, | |
| 52, | |
| 70, | |
| 20, | |
| 129, | |
| 18, | |
| 23, | |
| 104, | |
| 13, | |
| 0, | |
| 0, | |
| 41, | |
| 0, | |
| 82, | |
| 72, | |
| 80, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 0, | |
| 65, | |
| 200 | |
| ]); | |
| /** | |
| * @readonly | |
| * @type {Readonly<Uint8Array>} | |
| */ | |
| static _WRAP_EXPECTED = new Uint8Array([ | |
| 144, | |
| 241, | |
| 7, | |
| 220, | |
| 120, | |
| 162, | |
| 160, | |
| 53, | |
| 255, | |
| 197, | |
| 89, | |
| 28, | |
| 47, | |
| 51, | |
| 192, | |
| 18, | |
| 5, | |
| 139, | |
| 52, | |
| 86, | |
| 184, | |
| 185, | |
| 164, | |
| 113, | |
| 113, | |
| 109, | |
| 56, | |
| 161, | |
| 6, | |
| 122, | |
| 20, | |
| 145, | |
| 35, | |
| 23, | |
| 132, | |
| 182, | |
| 82, | |
| 5, | |
| 169, | |
| 255, | |
| 169, | |
| 40, | |
| 21, | |
| 59, | |
| 107, | |
| 168, | |
| 156, | |
| 139, | |
| 163, | |
| 255, | |
| 179, | |
| 59, | |
| 117, | |
| 11, | |
| 201, | |
| 3, | |
| 234, | |
| 37, | |
| 99, | |
| 164, | |
| 228, | |
| 14, | |
| 87, | |
| 168, | |
| 161, | |
| 221, | |
| 210, | |
| 52, | |
| 194, | |
| 214, | |
| 103, | |
| 27, | |
| 133, | |
| 26, | |
| 208, | |
| 25, | |
| 47, | |
| 196, | |
| 121, | |
| 213, | |
| 187, | |
| 121, | |
| 250, | |
| 69, | |
| 226, | |
| 12, | |
| 1, | |
| 234, | |
| 154, | |
| 68, | |
| 54, | |
| 41, | |
| 243, | |
| 203, | |
| 24, | |
| 163, | |
| 248, | |
| 17, | |
| 248, | |
| 142, | |
| 190, | |
| 95, | |
| 25, | |
| 38, | |
| 162, | |
| 103, | |
| 177, | |
| 151, | |
| 240, | |
| 122, | |
| 13, | |
| 167 | |
| ]); | |
| /** | |
| * @param {Readonly<Uint8Array>} a | |
| * @param {Readonly<Uint8Array>} b | |
| * @param {number} size | |
| */ | |
| static _compareByteArraysForTest(a, b, size) { | |
| for (let i = 0; i < size; i++) { | |
| if (a[i] != b[i]) { | |
| console.log("MISMATCH! Array A:"); | |
| let hexA = ""; | |
| let hexB = ""; | |
| for (let i2 = 0; i2 < size; i2++) { | |
| hexA += `${a[i2].toString(16).toUpperCase().padStart(2, "0")} `; | |
| hexB += `${b[i2].toString(16).toUpperCase().padStart(2, "0")} `; | |
| } | |
| console.log(hexA); | |
| console.log(`vs. Array B (mismatching at ${i}):`); | |
| console.log(hexB); | |
| throw new Error(`Bytes are mismatching at position ${i}.`); | |
| } | |
| } | |
| } | |
| /** | |
| * @public | |
| * @param {AesCcmCipher} cipher | |
| */ | |
| static testEncrypt(cipher) { | |
| const wrapped = new Uint8Array(112); | |
| WrappedStoreData.encrypt(wrapped, 0, _WrappedStoreDataTest._WRAP_TEST_DATA, 0, cipher); | |
| _WrappedStoreDataTest._compareByteArraysForTest(wrapped, _WrappedStoreDataTest._WRAP_EXPECTED, 112); | |
| } | |
| /** | |
| * @public | |
| * @param {AesCcmCipher} cipher | |
| */ | |
| static testDecrypt(cipher) { | |
| const unwrapped = new Uint8Array(96); | |
| WrappedStoreData.decrypt(unwrapped, 0, _WrappedStoreDataTest._WRAP_EXPECTED, 0, cipher); | |
| _WrappedStoreDataTest._compareByteArraysForTest(unwrapped, _WrappedStoreDataTest._WRAP_TEST_DATA, 96); | |
| } | |
| /** | |
| * @public | |
| */ | |
| static main() { | |
| const cipher = new NativeAesCcmCipher(); | |
| cipher.initialize(_WrappedStoreDataTest._WRAP_KEY_DEVELOPMENT, 0); | |
| _WrappedStoreDataTest.testEncrypt(cipher); | |
| _WrappedStoreDataTest.testDecrypt(cipher); | |
| console.log("Encrypt & decrypt tests passed."); | |
| return 0; | |
| } | |
| }; | |
| // MiiImportController.js | |
| var base64ToBytes = (base64) => Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); | |
| var hexToBytes = (hex) => Uint8Array.from( | |
| { length: hex.length >>> 1 }, | |
| (_, i) => Number.parseInt(hex.slice(i << 1, (i << 1) + 2), 16) | |
| ); | |
| var bytesToHex = (bytes) => Array.prototype.map.call( | |
| bytes, | |
| (x) => x.toString(16).padStart(2, "0") | |
| ).join(""); | |
| var base64ExToBytes = (base64) => { | |
| base64 = base64.replace(/-/g, "+").replace(/_/g, "/"); | |
| while (base64.length % 4 !== 0) { | |
| base64 += "="; | |
| } | |
| return base64ToBytes(base64); | |
| }; | |
| var parseHexOrBase64ExToBytes = (text) => { | |
| text = text.replace(/\s+/g, ""); | |
| return /^[0-9a-fA-F]+$/.test(text) ? hexToBytes(text) : base64ExToBytes(text); | |
| }; | |
| var MiiErrorReason = { | |
| /** Data size does not match any known Mii format. */ | |
| UnsupportedSize: 0, | |
| /** Converted studio data failed MiiDecoder.isValid(). */ | |
| ValidationFailed: 1, | |
| /** QR code was found but contained no binary payload. */ | |
| QrNoBinary: 2, | |
| /** QR code scanner failed while scanning. */ | |
| QrScanError: 3, | |
| /** No QR code detected in uploaded image. */ | |
| QrNotFound: 4, | |
| /** File could not be read from disk. */ | |
| FileReadError: 5 | |
| }; | |
| var ErrorMessageTable = /* @__PURE__ */ new Map([ | |
| [MiiErrorReason.UnsupportedSize, "Data size does not match any known Mii format."], | |
| [MiiErrorReason.ValidationFailed, "Decoded data failed validation check."], | |
| [MiiErrorReason.QrNoBinary, "QR code found but contains no binary data."], | |
| [MiiErrorReason.QrScanError, "Unexpected QR code scanner error. (Camera not present? Permissions not granted?)"], | |
| [MiiErrorReason.QrNotFound, "No QR code detected in uploaded image."], | |
| [MiiErrorReason.FileReadError, "Failed to read the file."] | |
| ]); | |
| var ImportSourceType = { | |
| Text: 0, | |
| File: 1, | |
| Qr: 2, | |
| Custom: 3 | |
| }; | |
| var ImportParsedEvent = class { | |
| /** | |
| * @param {symbol} sourceId - Which source produced this data. | |
| * @param {Uint8Array} rawData - Original input bytes before conversion. | |
| * @param {Uint8Array} studioData - 46-byte converted Studio data. | |
| * @param {string} studioHexUrl - Obfuscated hex string for the Studio image URL. | |
| * @param {MiiExtraInfo} extraInfo - Extra metadata extracted from the source format. | |
| */ | |
| constructor(sourceId, rawData, studioData, studioHexUrl, extraInfo) { | |
| this.sourceId = sourceId; | |
| this.rawData = rawData; | |
| this.studioData = studioData; | |
| this.studioHexUrl = studioHexUrl; | |
| this.extraInfo = extraInfo; | |
| } | |
| /** @returns {string} Full URL to the rendered Mii image. */ | |
| getImageUrl() { | |
| return MiiDecoder.URL_BASE_IMAGE_PNG + this.studioHexUrl; | |
| } | |
| }; | |
| var ImportErrorEvent = class { | |
| /** | |
| * @param {symbol} sourceId - Which source produced the error. | |
| * @param {MiiErrorReason} reason - Error reason enum value. | |
| * @param {Error} [cause] - Optional underlying error for debugging. | |
| */ | |
| constructor(sourceId, reason, cause) { | |
| this.sourceId = sourceId; | |
| this.reason = reason; | |
| this.cause = cause || null; | |
| } | |
| /** | |
| * Get a default English friendly message for this error. | |
| * Consumers who need localization should use the reason enum | |
| * to look up their own strings instead. | |
| * @returns {string} | |
| */ | |
| getFriendlyMessage() { | |
| return ErrorMessageTable.get(this.reason) || "Unknown error."; | |
| } | |
| }; | |
| var ImportSourceChangedEvent = class { | |
| /** | |
| * @param {symbol} activeSourceId - Newly active source. | |
| * @param {symbol | null} previousSourceId - Previously active source, or null. | |
| */ | |
| constructor(activeSourceId, previousSourceId) { | |
| this.activeSourceId = activeSourceId; | |
| this.previousSourceId = previousSourceId; | |
| } | |
| }; | |
| var ImportCamerasLoadedEvent = class { | |
| /** @param {Array<{id: string, label: string}>} cameras */ | |
| constructor(cameras) { | |
| this.cameras = cameras; | |
| } | |
| }; | |
| function buildStudioHexUrl(studioData) { | |
| const obfuscated = new Uint8Array(MiiDecoder.SIZE_STUDIO_URL_DATA); | |
| MiiDecoder.obfuscateStudioUrl(obfuscated, studioData, 0); | |
| return bytesToHex(obfuscated); | |
| } | |
| function decryptIfWrappedStoreData(data) { | |
| if (data.length < WrappedStoreData.LENGTH) { | |
| return data; | |
| } | |
| const cipher = new NativeAesCcmCipher(); | |
| cipher.initialize(/* @__PURE__ */ new Uint8Array([ | |
| 89, | |
| 252, | |
| 129, | |
| 126, | |
| 100, | |
| 70, | |
| 234, | |
| 97, | |
| 144, | |
| 52, | |
| 123, | |
| 32, | |
| 233, | |
| 189, | |
| 206, | |
| 82 | |
| ]), 0); | |
| const unwrapped = new Uint8Array(WrappedStoreData.STORE_DATA_LENGTH); | |
| WrappedStoreData.decrypt(unwrapped, 0, data, 0, cipher); | |
| return unwrapped; | |
| } | |
| var ListenerRecord = class { | |
| /** | |
| * @param {EventTarget} target | |
| * @param {string} event | |
| * @param {EventListener} handler | |
| * @param {AddEventListenerOptions} [options] | |
| */ | |
| constructor(target, event, handler, options) { | |
| this.target = target; | |
| this.event = event; | |
| this.handler = handler; | |
| this.options = options; | |
| } | |
| /** Remove this listener from its target. */ | |
| remove() { | |
| this.target.removeEventListener(this.event, this.handler, this.options); | |
| } | |
| }; | |
| var SourceRecord = class { | |
| /** | |
| * @param {symbol} id | |
| * @param {ImportSourceType} type | |
| */ | |
| constructor(id, type) { | |
| this.id = id; | |
| this.type = type; | |
| this.lastRawData = null; | |
| this.lastStudioData = null; | |
| this.lastExtraInfo = null; | |
| this.listeners = []; | |
| this.extra = null; | |
| this._disposeExtra = null; | |
| } | |
| /** Dispose of this source: remove listeners and run type-specific cleanup. */ | |
| dispose() { | |
| for (const record of this.listeners) { | |
| record.remove(); | |
| } | |
| this.listeners.length = 0; | |
| if (this._disposeExtra) { | |
| this._disposeExtra(); | |
| this._disposeExtra = null; | |
| } | |
| } | |
| }; | |
| function trackListener(source, target, event, handler, options) { | |
| target.addEventListener(event, handler, options); | |
| source.listeners.push(new ListenerRecord(target, event, handler, options)); | |
| } | |
| var MiiImportController = class { | |
| /** | |
| * @type {Map<symbol, SourceRecord>} | |
| * @private | |
| */ | |
| _sources = /* @__PURE__ */ new Map(); | |
| /** | |
| * @type {symbol | null} | |
| * @private | |
| */ | |
| _activeSourceId = null; | |
| /** | |
| * @type {Map<string, Set<Function>>} | |
| * @private | |
| */ | |
| _eventHandlers = /* @__PURE__ */ new Map(); | |
| /** | |
| * @type {boolean} | |
| * @private | |
| */ | |
| _disposed = false; | |
| // // ------------------------------------------------------- | |
| // // Event emitter | |
| // // ------------------------------------------------------- | |
| /** | |
| * a | |
| * @typedef {'parsed' | 'error' | 'sourceChanged' | 'scanning' | | |
| * 'scannerStopped' | 'camerasLoaded'} ImportEventType | |
| */ | |
| /** | |
| * s | |
| * @typedef {ImportParsedHandler | ImportErrorHandler | ImportSourceChangedHandler | | |
| * ImportScanningHandler | ImportScannerStoppedHandler} ImportEventHandler | |
| */ | |
| /** | |
| * Register an event handler. | |
| * Supported events: | |
| * - `'parsed'`: {@link ImportParsedHandler} | |
| * - `'error'`: {@link ImportErrorHandler} | |
| * - `'sourceChanged'`: {@link ImportSourceChangedHandler} | |
| * - `'scanning'`: {@link ImportScanningHandler} | |
| * - `'scannerStopped'`: {@link ImportScannerStoppedHandler} | |
| * - `'camerasLoaded'`: {@link ImportCamerasLoadedHandler} | |
| * @param {ImportEventType} event | |
| * @param {ImportEventHandler} handler | |
| * @returns {this} For chaining. | |
| */ | |
| on(event, handler) { | |
| let set = this._eventHandlers.get(event); | |
| if (!set) { | |
| set = /* @__PURE__ */ new Set(); | |
| this._eventHandlers.set(event, set); | |
| } | |
| set.add(handler); | |
| return this; | |
| } | |
| /** | |
| * Remove an event handler. | |
| * @param {ImportEventType} event | |
| * @param {ImportEventHandler} handler | |
| * @returns {this} | |
| */ | |
| off(event, handler) { | |
| const handlers = this._eventHandlers.get(event); | |
| if (handlers) { | |
| handlers.delete(handler); | |
| } | |
| return this; | |
| } | |
| /** | |
| * Emit an event to all registered handlers. | |
| * @param {string} event | |
| * @param {Object} [data] | |
| * @private | |
| */ | |
| _emit(event, data) { | |
| const handlers = this._eventHandlers.get(event); | |
| if (!handlers) { | |
| return; | |
| } | |
| for (const handler of handlers) { | |
| try { | |
| handler(data); | |
| } catch (err) { | |
| console.error(`[MiiImportController] Error in '${event}' handler:`, err); | |
| } | |
| } | |
| } | |
| // // ------------------------------------------------------- | |
| // // Active source management | |
| // // ------------------------------------------------------- | |
| /** @returns {symbol | null} Currently active source ID. */ | |
| get activeSourceId() { | |
| return this._activeSourceId; | |
| } | |
| /** | |
| * Switch the active source. | |
| * @param {symbol} id - Source ID returned from an add method. | |
| * @throws {Error} If the source ID is not registered. | |
| */ | |
| setActiveSource(id) { | |
| if (!this._sources.has(id)) { | |
| throw new Error("Unknown source ID"); | |
| } | |
| this._switchActive(id); | |
| } | |
| /** | |
| * Get the source record for an ID. | |
| * @param {symbol} id | |
| * @returns {SourceRecord | undefined} | |
| */ | |
| getSource(id) { | |
| return this._sources.get(id); | |
| } | |
| /** | |
| * Internal active-source switching with event emission. | |
| * @param {symbol} newId | |
| * @private | |
| */ | |
| _switchActive(newId) { | |
| const previousId = this._activeSourceId; | |
| if (previousId === newId) { | |
| return; | |
| } | |
| this._activeSourceId = newId; | |
| this._emit("sourceChanged", new ImportSourceChangedEvent(newId, previousId)); | |
| } | |
| // // ------------------------------------------------------- | |
| // // Source registration: text input | |
| // // ------------------------------------------------------- | |
| /** | |
| * Register a text input that accepts hex, base64, or base64url Mii data. | |
| * Automatically parses on input with configurable debounce. | |
| * @param {HTMLInputElement} element - The text input element. | |
| * @param {Object} [opts] | |
| * @param {number} [opts.debounceMs] - Debounce delay in milliseconds. Default 500. | |
| * @returns {symbol} Source ID for this input. | |
| */ | |
| addTextInput(element, opts) { | |
| const debounceMs = opts && opts.debounceMs ? opts.debounceMs : 500; | |
| const id = Symbol("import-source-text"); | |
| const source = new SourceRecord(id, ImportSourceType.Text); | |
| source.extra = { debounceTimer: 0 }; | |
| source._disposeExtra = () => { | |
| clearTimeout(source.extra.debounceTimer); | |
| }; | |
| const onInput = () => { | |
| clearTimeout(source.extra.debounceTimer); | |
| source.extra.debounceTimer = setTimeout(() => { | |
| const value = element.value.trim(); | |
| if (value.length === 0) { | |
| return; | |
| } | |
| this._switchActive(id); | |
| const data = parseHexOrBase64ExToBytes(value); | |
| this.submitData(id, data); | |
| }, debounceMs); | |
| }; | |
| trackListener(source, element, "input", onInput); | |
| this._sources.set(id, source); | |
| return id; | |
| } | |
| // // ------------------------------------------------------- | |
| // // Source registration: file input | |
| // // ------------------------------------------------------- | |
| /** | |
| * Register a file input that reads Mii data files. | |
| * Supports any binary file whose size matches a known Mii format. | |
| * @param {HTMLInputElement} element - File input element (type="file"). | |
| * @returns {symbol} Source ID for this input. | |
| */ | |
| addFileInput(element) { | |
| const id = Symbol("import-source-file"); | |
| const source = new SourceRecord(id, ImportSourceType.File); | |
| const onChange = async () => { | |
| if (!element.files || !element.files.length) { | |
| return; | |
| } | |
| const file = element.files[0]; | |
| this._switchActive(id); | |
| const reader = new FileReader(); | |
| reader.onload = () => { | |
| const data = new Uint8Array( | |
| /** @type {ArrayBuffer} */ | |
| reader.result | |
| ); | |
| this.submitData(id, data); | |
| }; | |
| reader.onerror = () => { | |
| const e = reader.error; | |
| this._emit("error", new ImportErrorEvent( | |
| id, | |
| MiiErrorReason.FileReadError, | |
| e instanceof Error ? e : new Error(String(e)) | |
| )); | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| }; | |
| trackListener(source, element, "change", onChange); | |
| this._sources.set(id, source); | |
| return id; | |
| } | |
| // // ------------------------------------------------------- | |
| // // Source registration: QR scanner | |
| // // ------------------------------------------------------- | |
| /** | |
| * Register a QR code scanner source. | |
| * The controller creates the QrScanner instance from the imported module. | |
| * @param {Object} opts | |
| * @param {HTMLVideoElement} opts.videoElement - Video element for camera feed. | |
| * @param {HTMLElement} opts.startButton - Element that starts scanning on click. | |
| * @param {HTMLElement} [opts.stopButton] - Element that stops scanning on click. | |
| * @param {HTMLElement} opts.containerElement - Toggled visible/hidden by the controller. | |
| * @param {HTMLInputElement} [opts.fileInput] - Optional file input for QR image scanning. | |
| * @param {HTMLSelectElement} [opts.cameraSelect] - Select element the controller populates with available cameras. | |
| * @param {Object} [opts.scannerOptions] - Extra options forwarded to QrScanner constructor. | |
| * @returns {symbol} Source ID for this scanner. | |
| */ | |
| addQrScanner(opts) { | |
| const { | |
| videoElement, | |
| startButton, | |
| stopButton, | |
| containerElement, | |
| fileInput, | |
| cameraSelect, | |
| scannerOptions | |
| } = opts; | |
| import_qr_scanner.default.setBarcodeDetectorDisabled !== void 0 && import_qr_scanner.default.setBarcodeDetectorDisabled(); | |
| const id = Symbol("import-source-qr"); | |
| const source = new SourceRecord(id, ImportSourceType.Qr); | |
| source.extra = { scanner: null, scanning: false }; | |
| const onDecode = (result) => { | |
| const bytes = result.binaryData; | |
| if (!bytes || bytes.length <= 0) { | |
| this._emit("error", new ImportErrorEvent(id, MiiErrorReason.QrNoBinary)); | |
| return; | |
| } | |
| this._switchActive(id); | |
| this.submitData(id, new Uint8Array(bytes)); | |
| stopScanning(); | |
| }; | |
| const scanner = new import_qr_scanner.default(videoElement, onDecode, { | |
| // @ts-ignore -- This property may have been removed. | |
| returnDetailedScanResult: true, | |
| highlightScanRegion: true, | |
| highlightCodeOutline: true, | |
| ...scannerOptions, | |
| onDecodeError: function(err) { | |
| if (typeof err === "string" && err.indexOf("No QR code found") !== -1) { | |
| return; | |
| } | |
| scannerOptions?.onDecodeError?.(err); | |
| } | |
| }); | |
| source.extra.scanner = scanner; | |
| const startScanning = () => { | |
| containerElement.style.display = ""; | |
| source.extra.scanning = true; | |
| this._emit("scanning"); | |
| scanner.start().then(() => { | |
| import_qr_scanner.default.listCameras(true).then((cameras) => { | |
| if (cameraSelect) { | |
| const existing = cameraSelect.getElementsByClassName("device-camera"); | |
| for (const el of existing) { | |
| el.remove(); | |
| } | |
| for (const camera of cameras) { | |
| const option = document.createElement("option"); | |
| option.value = camera.id; | |
| option.text = camera.label; | |
| option.className = "device-camera"; | |
| cameraSelect.add(option); | |
| } | |
| } | |
| this._emit("camerasLoaded", new ImportCamerasLoadedEvent(cameras)); | |
| }); | |
| }).catch((error) => { | |
| this._emit("error", new ImportErrorEvent( | |
| id, | |
| MiiErrorReason.QrScanError, | |
| error instanceof Error ? error : new Error(String(error)) | |
| )); | |
| }); | |
| }; | |
| const stopScanning = () => { | |
| scanner.stop(); | |
| containerElement.style.display = "none"; | |
| source.extra.scanning = false; | |
| this._emit("scannerStopped"); | |
| }; | |
| if (cameraSelect) { | |
| trackListener(source, cameraSelect, "change", () => { | |
| scanner.setCamera(cameraSelect.value); | |
| }); | |
| } | |
| trackListener(source, startButton, "click", startScanning); | |
| if (stopButton) { | |
| trackListener(source, stopButton, "click", stopScanning); | |
| } | |
| if (fileInput) { | |
| const onFileChange = () => { | |
| if (!fileInput.files || !fileInput.files.length) { | |
| return; | |
| } | |
| const file = fileInput.files[0]; | |
| this._switchActive(id); | |
| import_qr_scanner.default.scanImage( | |
| file, | |
| // @ts-ignore -- This property may have been removed. | |
| { returnDetailedScanResult: true } | |
| ).then(onDecode).catch(() => { | |
| this._emit("error", new ImportErrorEvent(id, MiiErrorReason.QrNotFound)); | |
| }); | |
| }; | |
| trackListener(source, fileInput, "change", onFileChange); | |
| } | |
| containerElement.style.display = "none"; | |
| source._disposeExtra = () => { | |
| try { | |
| scanner.stop(); | |
| scanner.destroy(); | |
| } catch (e) { | |
| } | |
| }; | |
| this._sources.set(id, source); | |
| return id; | |
| } | |
| // // ------------------------------------------------------- | |
| // // Source registration: custom source | |
| // // ------------------------------------------------------- | |
| /** | |
| * Register a custom source that participates in active-source switching. | |
| * The consumer is responsible for obtaining data and calling | |
| * {@link submitData} when ready. | |
| * @returns {symbol} Source ID. | |
| */ | |
| addCustomSource() { | |
| const id = Symbol("import-source-custom"); | |
| const source = new SourceRecord(id, ImportSourceType.Custom); | |
| this._sources.set(id, source); | |
| return id; | |
| } | |
| // // ------------------------------------------------------- | |
| // // Universal data submission | |
| // // ------------------------------------------------------- | |
| /** | |
| * Submit raw Mii data from any source for decoding. | |
| * Built-in sources call this internally; custom sources call it directly. | |
| * On success, fires a 'parsed' event. On failure, fires an 'error' event. | |
| * @param {symbol} sourceId - Source ID that produced this data. | |
| * @param {Uint8Array} data - Raw Mii data bytes. | |
| * @throws {Error} Throws if the source symbol ID does not exist. | |
| */ | |
| submitData(sourceId, data) { | |
| if (this._disposed) { | |
| return; | |
| } | |
| const source = this._sources.get(sourceId); | |
| if (!source) { | |
| throw new Error("Unknown source ID"); | |
| } | |
| data = decryptIfWrappedStoreData(data); | |
| const studioData = new Uint8Array(MiiDecoder.SIZE_STUDIO_RAW_DATA); | |
| const extraInfo = new MiiExtraInfo(); | |
| const decoded = MiiDecoder.fromAnyMiiDataExtra( | |
| studioData, | |
| extraInfo, | |
| data.length, | |
| data | |
| ); | |
| if (!decoded) { | |
| source.lastRawData = null; | |
| source.lastStudioData = null; | |
| source.lastExtraInfo = null; | |
| this._emit("error", new ImportErrorEvent( | |
| sourceId, | |
| MiiErrorReason.UnsupportedSize | |
| )); | |
| return; | |
| } | |
| if (!MiiDecoder.isValid(studioData)) { | |
| source.lastRawData = data; | |
| source.lastStudioData = null; | |
| source.lastExtraInfo = null; | |
| this._emit("error", new ImportErrorEvent( | |
| sourceId, | |
| MiiErrorReason.ValidationFailed | |
| )); | |
| return; | |
| } | |
| const studioHexUrl = buildStudioHexUrl(studioData); | |
| source.lastRawData = data; | |
| source.lastStudioData = studioData; | |
| source.lastExtraInfo = extraInfo; | |
| this._emit("parsed", new ImportParsedEvent( | |
| sourceId, | |
| data, | |
| studioData, | |
| studioHexUrl, | |
| extraInfo | |
| )); | |
| } | |
| // // ------------------------------------------------------- | |
| // // Cleanup | |
| // // ------------------------------------------------------- | |
| /** | |
| * Dispose of the controller. | |
| * Delegates cleanup to each source record's own dispose method. | |
| * The controller is unusable after this call. | |
| */ | |
| dispose() { | |
| this._disposed = true; | |
| for (const source of this._sources.values()) { | |
| source.dispose(); | |
| } | |
| this._sources.clear(); | |
| this._eventHandlers.clear(); | |
| this._activeSourceId = null; | |
| } | |
| }; | |
| export { | |
| ImportCamerasLoadedEvent, | |
| ImportErrorEvent, | |
| ImportParsedEvent, | |
| ImportSourceChangedEvent, | |
| ImportSourceType, | |
| MiiErrorReason, | |
| MiiImportController | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MiiImportController — Test</title> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: system-ui, sans-serif; | |
| background: #1a1a2e; | |
| color: #eee; | |
| padding: 1.5rem; | |
| max-width: 640px; | |
| margin: 0 auto; | |
| } | |
| h1 { font-size: 1.1rem; margin-bottom: 1rem; } | |
| /* Source rows — the entire row is clickable. */ | |
| .source-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| padding: 0.5rem 0.75rem; | |
| margin-bottom: 0.35rem; | |
| background: #16213e; | |
| border: 2px solid #333; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| opacity: 0.5; | |
| outline: none; | |
| } | |
| .source-row:hover, .source-row:focus-visible { opacity: 0.75; } | |
| .source-row.active { opacity: 1; } | |
| /* Border state colors — only visible when active. */ | |
| .source-row.active.state-loaded { border-color: #53d769; } | |
| .source-row.active.state-error { border-color: #ff6b6b; } | |
| .source-row.active.state-idle { border-color: #666; } | |
| .source-row label { | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| color: #888; | |
| min-width: 2.5rem; | |
| cursor: pointer; | |
| } | |
| .source-row .source-input { flex: 1; min-width: 0; } | |
| .source-row input[type="text"], | |
| .source-row input[type="file"] { | |
| width: 100%; | |
| padding: 0.3rem 0.5rem; | |
| background: #0f3460; | |
| border: 1px solid #333; | |
| border-radius: 3px; | |
| color: #eee; | |
| font-size: 0.85rem; | |
| font-family: monospace; | |
| } | |
| .source-row input:disabled { opacity: 0.3; cursor: default; } | |
| .source-row input[type="radio"] { accent-color: #53d769; } | |
| .loaded-tag { | |
| display: none; | |
| font-size: 0.7rem; | |
| white-space: nowrap; | |
| } | |
| .state-loaded .loaded-tag { color: #53d769; } | |
| .state-error .loaded-tag { color: #ff6b6b; } | |
| /* QR buttons and select. */ | |
| .qr-controls { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; } | |
| #qr-start-btn, #qr-stop-btn { | |
| padding: 0.3rem 0.7rem; | |
| border: 1px solid #333; | |
| border-radius: 3px; | |
| cursor: pointer; | |
| font-size: 0.85rem; | |
| } | |
| #qr-start-btn { background: #0f3460; color: #eee; } | |
| #qr-stop-btn { background: #e94560; color: #fff; border: none; } | |
| #cam-list { | |
| padding: 0.2rem; | |
| background: #0f3460; | |
| color: #eee; | |
| border: 1px solid #333; | |
| border-radius: 3px; | |
| font-size: 0.85rem; | |
| max-width: 120px; | |
| } | |
| .qr-upload-label { font-size: 0.8rem; color: #888; } | |
| #qr-file-input { font-size: 0.8rem; max-width: 160px; } | |
| /* QR container — no CSS display rule; JS controls visibility. */ | |
| #qr-container { | |
| margin: 0.5rem 0; | |
| padding: 0.75rem; | |
| background: #16213e; | |
| border-radius: 6px; | |
| text-align: center; | |
| } | |
| #qr-container .qr-title { | |
| font-size: 0.8rem; | |
| color: #888; | |
| margin-bottom: 0.4rem; | |
| } | |
| #qr-container video { | |
| width: 100%; | |
| max-width: 320px; | |
| border-radius: 3px; | |
| display: block; | |
| margin: 0 auto; | |
| } | |
| /* Status. */ | |
| #status-bar { margin-top: 0.5rem; font-size: 0.8rem; min-height: 1.2em; } | |
| .status-error { color: #ff6b6b; } | |
| .status-ok { color: #53d769; } | |
| .status-info { color: #5bc0de; } | |
| /* Result panel. */ | |
| #result-panel { | |
| margin-top: 1rem; | |
| padding: 0.75rem; | |
| background: #16213e; | |
| border-radius: 6px; | |
| } | |
| .result-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; } | |
| #result-image { width: 96px; height: 96px; border-radius: 6px; background: #fff; flex-shrink: 0; } | |
| .result-name { font-size: 1.2rem; font-weight: 700; } | |
| .result-source { font-size: 0.7rem; color: #888; } | |
| .result-props { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); | |
| gap: 0.2rem 0.75rem; | |
| font-size: 0.8rem; | |
| } | |
| .prop-row { display: flex; gap: 0.2rem; } | |
| .prop-row dt { color: #888; } | |
| .prop-row dd { font-weight: 600; } | |
| .hex-preview { | |
| margin-top: 0.5rem; | |
| padding: 0.4rem; | |
| background: #0f3460; | |
| border-radius: 3px; | |
| font-family: monospace; | |
| font-size: 0.7rem; | |
| word-break: break-all; | |
| color: #888; | |
| max-height: 3rem; | |
| overflow-y: auto; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>MiiImportController Test</h1> | |
| <!-- Source: Text --> | |
| <div class="source-row state-idle" id="row-text" tabindex="0"> | |
| <input type="radio" name="active-source" id="radio-text" tabindex="-1"> | |
| <label for="radio-text">Text</label> | |
| <div class="source-input"> | |
| <input type="text" id="text-input" | |
| placeholder="Hex or Base64 Mii data" | |
| value="AwBgMIJUICvpzY4vnWYVrXy7ikd01AAAWR1KAGEAcwBtAGkAbgBlAAAAAAAAABw3EhB7ASFuQxwNZMcYAAgegg0AMEGzW4JtAAAAAAAAAAAAAAAAAAAAAAAAAAAAAML0"> | |
| </div> | |
| <span class="loaded-tag" id="loaded-text"></span> | |
| </div> | |
| <!-- Source: File --> | |
| <div class="source-row state-idle" id="row-file" tabindex="0"> | |
| <input type="radio" name="active-source" id="radio-file" tabindex="-1"> | |
| <label for="radio-file">File</label> | |
| <div class="source-input"> | |
| <input type="file" id="file-input" accept=".cfsd,.ffsd,.3dsmii,.rcd,.miigx,.mii,.mae,.rsd,.charinfo,.nfcd,.nfsd,.mnms,*/*"> | |
| </div> | |
| <span class="loaded-tag" id="loaded-file"></span> | |
| </div> | |
| <!-- Source: QR --> | |
| <div class="source-row state-idle" id="row-qr" tabindex="0"> | |
| <input type="radio" name="active-source" id="radio-qr" tabindex="-1"> | |
| <label for="radio-qr">QR</label> | |
| <div class="source-input"> | |
| <div class="qr-controls"> | |
| <button id="qr-start-btn" type="button">Scan</button> | |
| <button id="qr-stop-btn" type="button">Stop</button> | |
| <select id="cam-list"> | |
| <option value="environment" selected>Back Camera</option> | |
| <option value="user">Front Camera</option> | |
| </select> | |
| <span class="qr-upload-label">or upload QR image:</span> | |
| <input type="file" id="qr-file-input" accept="image/*"> | |
| </div> | |
| </div> | |
| <span class="loaded-tag" id="loaded-qr"></span> | |
| </div> | |
| <!-- QR camera container. --> | |
| <div id="qr-container"> | |
| <div class="qr-title">QR Code Scanner</div> | |
| <video id="qr-video" muted playsinline></video> | |
| </div> | |
| <div id="status-bar"></div> | |
| <!-- Result panel. --> | |
| <div id="result-panel" style="display: none;"> | |
| <div class="result-header"> | |
| <img id="result-image" src="" alt=""> | |
| <div> | |
| <div class="result-name" id="result-name"></div> | |
| <div class="result-source" id="result-source"></div> | |
| </div> | |
| </div> | |
| <div class="result-props" id="result-props"></div> | |
| <div class="hex-preview" id="hex-preview"></div> | |
| </div> | |
| <!-- | |
| <script type="importmap"> | |
| { | |
| "imports": { | |
| "qr-scanner": "https://esm.sh/@getify-as-is/qr-scanner@0.20251225.0" | |
| } | |
| } | |
| </script> | |
| --> | |
| <script type="importmap">{"imports": { | |
| "./MiiImportController.js": "./MiiImportController.esm.js" | |
| }}</script> | |
| <script type="module" src="MiiImportTest.js"></script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // @ts-check | |
| import { | |
| MiiImportController, | |
| ImportParsedEvent, | |
| ImportErrorEvent, | |
| ImportSourceChangedEvent | |
| } from './MiiImportController.js'; | |
| // ---- Helpers ---- | |
| const elementByIdOrThrow = (/** @type {string} */ id) => { | |
| const el = document.getElementById(id); | |
| if (!el) { | |
| throw new Error('Element not found: ' + id); | |
| } | |
| return el; | |
| }; | |
| // ---- DOM references ---- | |
| const textInput = /** @type {HTMLInputElement} */ (elementByIdOrThrow('text-input')); | |
| const fileInput = /** @type {HTMLInputElement} */ (elementByIdOrThrow('file-input')); | |
| const qrStartBtn = elementByIdOrThrow('qr-start-btn'); | |
| const qrStopBtn = elementByIdOrThrow('qr-stop-btn'); | |
| const qrVideo = /** @type {HTMLVideoElement} */ (elementByIdOrThrow('qr-video')); | |
| const qrContainer = elementByIdOrThrow('qr-container'); | |
| const qrFileInput = /** @type {HTMLInputElement} */ (elementByIdOrThrow('qr-file-input')); | |
| const camList = /** @type {HTMLSelectElement} */ (elementByIdOrThrow('cam-list')); | |
| const statusBar = elementByIdOrThrow('status-bar'); | |
| const resultPanel = elementByIdOrThrow('result-panel'); | |
| const resultImage = /** @type {HTMLImageElement} */ (elementByIdOrThrow('result-image')); | |
| const resultName = elementByIdOrThrow('result-name'); | |
| const resultSource = elementByIdOrThrow('result-source'); | |
| const resultProps = elementByIdOrThrow('result-props'); | |
| const hexPreview = elementByIdOrThrow('hex-preview'); | |
| const radioText = /** @type {HTMLInputElement} */ (elementByIdOrThrow('radio-text')); | |
| const radioFile = /** @type {HTMLInputElement} */ (elementByIdOrThrow('radio-file')); | |
| const radioQr = /** @type {HTMLInputElement} */ (elementByIdOrThrow('radio-qr')); | |
| const rowText = elementByIdOrThrow('row-text'); | |
| const rowFile = elementByIdOrThrow('row-file'); | |
| const rowQr = elementByIdOrThrow('row-qr'); | |
| const loadedText = elementByIdOrThrow('loaded-text'); | |
| const loadedFile = elementByIdOrThrow('loaded-file'); | |
| const loadedQr = elementByIdOrThrow('loaded-qr'); | |
| // ---- Controller setup ---- | |
| const controller = new MiiImportController(); | |
| const textSourceId = controller.addTextInput(textInput, { debounceMs: 400 }); | |
| const fileSourceId = controller.addFileInput(fileInput); | |
| const qrSourceId = controller.addQrScanner({ | |
| videoElement: qrVideo, | |
| startButton: qrStartBtn, | |
| stopButton: qrStopBtn, | |
| containerElement: qrContainer, | |
| fileInput: qrFileInput, | |
| cameraSelect: camList | |
| }); | |
| // ---- Per-source UI config ---- | |
| /** | |
| * @typedef {Object} SourceUiEntry | |
| * @property {HTMLElement} row | |
| * @property {HTMLInputElement} radio | |
| * @property {HTMLInputElement | null} input - The main input to disable/enable. | |
| * @property {HTMLElement} loadedIndicator | |
| * @property {string} label - Human-readable source name. | |
| */ | |
| /** @type {Map<symbol, SourceUiEntry>} */ | |
| const sourceUiMap = new Map([ | |
| [textSourceId, { row: rowText, radio: radioText, input: textInput, loadedIndicator: loadedText, label: 'Text Input' }], | |
| [fileSourceId, { row: rowFile, radio: radioFile, input: fileInput, loadedIndicator: loadedFile, label: 'File Input' }], | |
| [qrSourceId, { row: rowQr, radio: radioQr, input: null, loadedIndicator: loadedQr, label: 'QR Scanner' }] | |
| ]); | |
| /** @type {Map<symbol, ImportParsedEvent>} Per-source last successful parse. */ | |
| const sourceResults = new Map(); | |
| /** | |
| * Per-source state for border coloring. | |
| * @type {Map<symbol, 'idle' | 'loaded' | 'error'>} | |
| */ | |
| const sourceState = new Map(); | |
| for (const id of sourceUiMap.keys()) { | |
| sourceState.set(id, 'idle'); | |
| } | |
| // ---- Row click activates source ---- | |
| // Clicking anywhere on the row (not just the radio) switches source. | |
| for (const [id, ui] of sourceUiMap) { | |
| // Click anywhere on the row to activate this source. | |
| ui.row.addEventListener('click', (e) => { | |
| const target = /** @type {HTMLElement} */ (e.target); | |
| // Don't hijack clicks on functional inputs (text box, file picker, buttons). | |
| if (target === ui.input || target.tagName === 'BUTTON') { | |
| return; | |
| } | |
| controller.setActiveSource(id); | |
| }); | |
| // Keyboard activation: Enter or Space on focused row. | |
| ui.row.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| controller.setActiveSource(id); | |
| } | |
| }); | |
| } | |
| // Radio change handlers (for native radio keyboard nav). | |
| radioText.addEventListener('change', () => controller.setActiveSource(textSourceId)); | |
| radioFile.addEventListener('change', () => controller.setActiveSource(fileSourceId)); | |
| radioQr.addEventListener('change', () => controller.setActiveSource(qrSourceId)); | |
| // ---- Source changed: update row states ---- | |
| controller.on('sourceChanged', (/** @type {ImportSourceChangedEvent} */ evt) => { | |
| for (const [id, ui] of sourceUiMap) { | |
| const isActive = id === evt.activeSourceId; | |
| ui.row.classList.toggle('active', isActive); | |
| ui.radio.checked = isActive; | |
| // Enable/disable the main input. | |
| if (ui.input) { | |
| ui.input.disabled = !isActive; | |
| } | |
| // Update border color class based on state. | |
| updateRowBorder(id); | |
| } | |
| // Switch displayed result to the newly active source if it has one. | |
| const storedResult = sourceResults.get(evt.activeSourceId); | |
| if (storedResult) { | |
| displayResult(storedResult); | |
| } else { | |
| // No result for this source — clear the display. | |
| resultPanel.style.display = 'none'; | |
| // if (sourceState.get(evt.activeSourceId) === 'idle') { | |
| // clearStatus(); | |
| // } | |
| } | |
| // Always clear the status, good or bad, after switching. | |
| clearStatus(); | |
| }); | |
| // ---- Parsed: store and show result ---- | |
| controller.on('parsed', (/** @type {ImportParsedEvent} */ evt) => { | |
| // Store per-source. | |
| sourceResults.set(evt.sourceId, evt); | |
| sourceState.set(evt.sourceId, 'loaded'); | |
| // Update loaded indicator on the source row. | |
| const ui = sourceUiMap.get(evt.sourceId); | |
| if (ui) { | |
| const name = evt.extraInfo.getNickname(); | |
| ui.loadedIndicator.textContent = '\u2714 ' + (name || evt.rawData.length + ' bytes'); | |
| ui.loadedIndicator.style.display = ''; | |
| updateRowBorder(evt.sourceId); | |
| } | |
| // Display result if this is the active source. | |
| if (evt.sourceId === controller.activeSourceId) { | |
| setStatus('ok', 'Decoded ' + evt.rawData.length + ' bytes.'); | |
| displayResult(evt); | |
| } | |
| }); | |
| /** Render a parsed result into the result panel. */ | |
| function displayResult(/** @type {ImportParsedEvent} */ evt) { | |
| resultImage.src = evt.getImageUrl() + '&type=face&width=128&bgColor=FFFFFFFF'; | |
| resultImage.alt = 'Mii face render'; | |
| const nickname = evt.extraInfo.getNickname(); | |
| resultName.textContent = nickname || '(no name)'; | |
| // Identify the source for display. | |
| const sourceUi = sourceUiMap.get(evt.sourceId); | |
| const sourceName = (sourceUi ? sourceUi.label : '?'); | |
| resultSource.textContent = 'Source: ' + sourceName + | |
| ' \u00B7 ' + evt.rawData.length + ' bytes'; | |
| // Build property rows. | |
| const extra = evt.extraInfo; | |
| /** @type {Array<Array<string>>} */ | |
| const props = []; | |
| const creatorName = extra.getCreatorName(); | |
| if (nickname) { | |
| props.push(['Nickname', nickname]); | |
| } | |
| if (creatorName) { | |
| props.push(['Creator', creatorName]); | |
| } | |
| if (extra.height !== undefined) { | |
| props.push(['Height', String(extra.height)]); | |
| } | |
| if (extra.build !== undefined) { | |
| props.push(['Build', String(extra.build)]); | |
| } | |
| if (extra.skinColor !== undefined) { | |
| props.push(['Skin Color', String(extra.skinColor)]); | |
| } | |
| if (extra.gender !== undefined) { | |
| props.push(['Gender', extra.gender === 0 ? 'Male' : 'Female']); | |
| } | |
| if (extra.favoriteColor !== undefined) { | |
| props.push(['Favorite Color', String(extra.favoriteColor)]); | |
| } | |
| if (extra.isNx !== undefined) { | |
| props.push(['Is NX', String(extra.isNx)]); | |
| } | |
| if (extra.birthMonth) { | |
| props.push(['Birth Month', String(extra.birthMonth)]); | |
| } | |
| if (extra.birthDay) { | |
| props.push(['Birth Day', String(extra.birthDay)]); | |
| } | |
| if (extra.favorite !== undefined) { | |
| props.push(['Favorite', String(extra.favorite)]); | |
| } | |
| if (extra.copyable !== undefined) { | |
| props.push(['Copyable', String(extra.copyable)]); | |
| } | |
| resultProps.innerHTML = props.map(([key, val]) => | |
| `<div class="prop-row"><dt>${key}:</dt><dd>${val}</dd></div>` | |
| ).join(''); | |
| // Hex preview of raw data. | |
| const rawHex = Array.from(evt.rawData) | |
| .map(b => b.toString(16).padStart(2, '0')) | |
| .join(' '); | |
| hexPreview.textContent = rawHex; | |
| resultPanel.style.display = ''; | |
| } | |
| // ---- Error: update state and status ---- | |
| controller.on('error', (/** @type {ImportErrorEvent} */ evt) => { | |
| sourceState.set(evt.sourceId, 'error'); | |
| const ui = sourceUiMap.get(evt.sourceId); | |
| if (ui) { | |
| ui.loadedIndicator.textContent = '\u2718'; | |
| ui.loadedIndicator.style.display = ''; | |
| updateRowBorder(evt.sourceId); | |
| } | |
| if (evt.sourceId === controller.activeSourceId) { | |
| // Map reason to a display string. This is the test harness — | |
| // a real consumer would use their own localized elements. | |
| setStatus('error', evt.getFriendlyMessage()); | |
| } | |
| }); | |
| // ---- Scanning state ---- | |
| controller.on('scanning', () => { | |
| setStatus('info', 'QR scanner active — point camera at a Mii QR code.'); | |
| // Show stop, hide start, show camera list. | |
| qrStopBtn.style.display = ''; | |
| qrStartBtn.style.display = 'none'; | |
| camList.style.display = ''; | |
| }); | |
| controller.on('scannerStopped', () => { | |
| // Restore start button, hide camera selection and camera list. | |
| qrStopBtn.style.display = 'none'; | |
| qrStartBtn.style.display = ''; | |
| camList.style.display = 'none'; | |
| // Clear the scanning status if it was still showing. | |
| if (statusBar.classList.contains('status-info')) { | |
| clearStatus(); | |
| } | |
| }); | |
| // ---- Utilities ---- | |
| /** | |
| * Update the border color class on a source row. | |
| * @param {symbol} id | |
| */ | |
| function updateRowBorder(id) { | |
| const ui = sourceUiMap.get(id); | |
| if (!ui) { | |
| return; | |
| } | |
| const state = sourceState.get(id); | |
| ui.row.classList.remove('state-loaded', 'state-error', 'state-idle'); | |
| if (state === 'loaded') { | |
| ui.row.classList.add('state-loaded'); | |
| } else if (state === 'error') { | |
| ui.row.classList.add('state-error'); | |
| } else { | |
| ui.row.classList.add('state-idle'); | |
| } | |
| } | |
| /** | |
| * Set the status bar text and style class. | |
| * @param {'ok' | 'error' | 'info'} type | |
| * @param {string} message | |
| */ | |
| function setStatus(type, message) { | |
| statusBar.className = 'status-' + type; | |
| statusBar.textContent = message; | |
| } | |
| /** Clear the status bar. */ | |
| function clearStatus() { | |
| statusBar.className = ''; | |
| statusBar.textContent = ''; | |
| } | |
| // ---- Initial state ---- | |
| // Hide camera list and stop button initially. | |
| camList.style.display = 'none'; | |
| qrStopBtn.style.display = 'none'; | |
| // If the text input has a default value, fire input to kick off the debounced parse. | |
| if (textInput.value.trim().length > 0) { | |
| textInput.dispatchEvent(new Event('input')); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment