diff --git a/plugins/native-ocr/.gitignore b/plugins/native-ocr/.gitignore new file mode 100644 index 000000000..182e37266 --- /dev/null +++ b/plugins/native-ocr/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +releases/ +.DS_Store +*.log +.env +.env.* diff --git a/plugins/native-ocr/CHANGELOG.md b/plugins/native-ocr/CHANGELOG.md new file mode 100644 index 000000000..e91ea2bee --- /dev/null +++ b/plugins/native-ocr/CHANGELOG.md @@ -0,0 +1,50 @@ +# 更新日志 + +Native OCR 从官方 `wechat-ocr` 插件 fork 而来,作者署名 Samson。以下版本历史已按里程碑整合。 + +## v0.6.11(当前) + +- Windows 引擎排序调整:ONNX OCR(识别率最优)排第一 +- 切换引擎时自动用新引擎重新识别当前图片 + +## v0.6.7 ~ v0.6.10 + +- **微信 OCR 收缩为 macOS 专属**,Windows 引擎定稿为:ONNX OCR + Windows OCR(系统 WinRT) +- Windows OCR(WinRT)稳定性修复:补齐 `System.Runtime.WindowsRuntime` 程序集加载;输出统一 UTF-8 消除中文乱码;脚本带版本标记 +- Windows OCR 识别率优化:小图(最长边 < 1200px)解码阶段自动放大,提升小字识别率 +- 防御性回退:放大路径失败时自动退回原始路径,保证识别始终可用 + +## v0.6.0 ~ v0.6.6 + +- **三大引擎双端架构落地,全程零 Python**: + - ONNX OCR:PP-OCR v4 模型(约 60MB 一次性下载),Electron 自带 Node 子进程运行,双端通用,支持表格识别 + - 系统 OCR:macOS Vision(Swift)/ Windows OCR(WinRT PowerShell) + - 微信 OCR:macOS 原生运行时(npmmirror 按需下载) +- 运行时下载框架:npmmirror 源 + sha1 校验 + 原子替换 +- 修复 ONNX 引擎 "process is not defined"(改为 `ELECTRON_RUN_AS_NODE` 子进程常驻服务) + +## v0.5.0 ~ v0.5.3 + +- Windows 平台初步支持(过渡方案:RapidOCR / wechat-ocr Python 链路) +- Windows 引擎不可用时的自动降级与引导 + +## v0.4.0 + +- 双平台能力补齐:Windows 系统级 OCR 与历史管理适配 + +## v0.3.0 ~ v0.3.1 + +- 表格视图翻译修复 +- Windows 引擎错误处理与引导优化 + +## v0.2.0 ~ v0.2.1 + +- **表格识别**:行列聚类模型、分隔线拖拽/增删、单元格编辑、低置信度标记、TSV/CSV 导出 +- **OCR 后翻译**:文本/表格双视图翻译 +- **识别历史**:本地持久化、回放、单条删除、一键清空 + +## v0.1.0 ~ v0.1.1 + +- 从官方 `wechat-ocr` 插件 fork,改名 `native-ocr`(Native OCR) +- 新增 macOS Vision 引擎(系统内置,零依赖) +- 平台支持 darwin + win32 diff --git a/plugins/native-ocr/LICENSE b/plugins/native-ocr/LICENSE new file mode 100644 index 000000000..98437d8ee --- /dev/null +++ b/plugins/native-ocr/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 ztocwst + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/native-ocr/README.md b/plugins/native-ocr/README.md new file mode 100644 index 000000000..9615f03e2 --- /dev/null +++ b/plugins/native-ocr/README.md @@ -0,0 +1,53 @@ +# Native OCR + +ZTools 本地多引擎 OCR 插件。支持 macOS Vision / Windows OCR(系统 WinRT)/ ONNX OCR(PP-OCR v4)/ 微信 OCR(macOS)四类引擎,内置表格识别、OCR 后翻译、识别历史。 + +作者:Samson(fork 自官方 `wechat-ocr` 插件,原作者 zing,MIT 协议) + +## 引擎 + +| 引擎 | macOS | Windows | 说明 | +| --- | --- | --- | --- | +| ONNX OCR | ✅ | ✅(默认首选) | PP-OCR v4 模型,约 60MB 一次性下载,离线可用,支持表格识别,无需任何环境 | +| 系统 OCR | macOS Vision | Windows OCR | 系统内置能力,零依赖;小图自动放大提升识别率 | +| 微信 OCR | ✅ | — | 复用微信自带离线 OCR 运行时(按需从 npmmirror 下载) | + +Windows 平台默认展示 ONNX OCR,切换引擎后自动用新引擎重新识别当前图片。 + +## 功能 + +- **图片 OCR**:拖入/粘贴/截图,多引擎结果对比(切换引擎自动重识别) +- **表格识别**:行列聚类还原表格,分隔线可拖动/增删,单元格可编辑,导出 TSV/CSV +- **翻译**:识别结果一键翻译(文本/表格双视图) +- **识别历史**:本地持久化,可回放、删除、清空 +- **批量识别**:多图队列,合并复制 / 导出 TXT + +## 运行时下载 + +ONNX OCR 与微信 OCR 运行时不内置在插件包中,首次使用时从 npmmirror 下载并校验后缓存到本地: + +- ONNX OCR:`~/Library/Application Support/ZTools/native-ocr/onnx-runtime` +- 微信 OCR(macOS):`~/Library/Application Support/ZTools/native-ocr/ocr-runtime` + +## 开发 + +```bash +npm install +npm run dev +``` + +## 构建 + +```bash +npm run build +``` + +构建产物在 `dist/`,README/LICENSE/bin 会自动复制进产物目录。 + +## 更新日志 + +见 [CHANGELOG.md](./CHANGELOG.md)。 + +## 致谢 + +本项目基于 [ZToolsCenter 官方 wechat-ocr 插件](https://github.com/ZToolsCenter/ZTools-plugins/tree/main/plugins/wechat-ocr) fork 修改(原作者 zing,MIT 协议)。 diff --git a/plugins/native-ocr/bin/build-swift.sh b/plugins/native-ocr/bin/build-swift.sh new file mode 100755 index 000000000..b58394a8e --- /dev/null +++ b/plugins/native-ocr/bin/build-swift.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +swiftc -O bin/ocr-vision.swift -o bin/ocr-vision +chmod +x bin/ocr-vision diff --git a/plugins/native-ocr/bin/ocr-vision.swift b/plugins/native-ocr/bin/ocr-vision.swift new file mode 100644 index 000000000..4f5f5ba51 --- /dev/null +++ b/plugins/native-ocr/bin/ocr-vision.swift @@ -0,0 +1,60 @@ +import Foundation +import Vision +import AppKit + +guard CommandLine.arguments.count > 1 else { + FileHandle.standardError.write("Usage: ocr-vision \n".data(using: .utf8)!) + exit(2) +} + +let imagePath = CommandLine.arguments[1] +let url = URL(fileURLWithPath: imagePath) + +guard let image = NSImage(contentsOf: url), + let tiffData = image.tiffRepresentation, + let bitmap = NSBitmapImageRep(data: tiffData), + let cgImage = bitmap.cgImage else { + FileHandle.standardError.write("无法加载图片: \(imagePath)\n".data(using: .utf8)!) + exit(3) +} + +let request = VNRecognizeTextRequest { req, err in + if let err = err { + FileHandle.standardError.write("识别失败: \(err.localizedDescription)\n".data(using: .utf8)!) + exit(4) + } + guard let observations = req.results as? [VNRecognizedTextObservation] else { return } + + var items: [[String: Any]] = [] + for obs in observations { + guard let top = obs.topCandidates(1).first else { continue } + let box = obs.boundingBox + items.append([ + "text": top.string, + "box": [ + "x": Double(box.origin.x), + "y": Double(box.origin.y), + "w": Double(box.size.width), + "h": Double(box.size.height) + ] + ]) + } + + guard let data = try? JSONSerialization.data(withJSONObject: items, options: []), + let json = String(data: data, encoding: .utf8) else { + FileHandle.standardError.write("识别结果序列化失败\n".data(using: .utf8)!) + exit(4) + } + FileHandle.standardOutput.write((json + "\n").data(using: .utf8)!) +} +request.recognitionLevel = .accurate +request.recognitionLanguages = ["zh-Hans", "zh-Hant", "en-US"] +request.usesLanguageCorrection = true + +let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) +do { + try handler.perform([request]) +} catch { + FileHandle.standardError.write("执行失败: \(error.localizedDescription)\n".data(using: .utf8)!) + exit(5) +} diff --git a/plugins/native-ocr/bin/ocr-winrt.ps1 b/plugins/native-ocr/bin/ocr-winrt.ps1 new file mode 100644 index 000000000..7da844545 --- /dev/null +++ b/plugins/native-ocr/bin/ocr-winrt.ps1 @@ -0,0 +1,156 @@ +# Windows system OCR (WinRT Windows.Media.Ocr) for the native-ocr ZTools plugin. +# macOS equivalent of ocr-vision.swift. Runs on built-in PowerShell 5.1+ with +# zero dependencies. Output contract (stdout, single JSON line): +# { "width": int, "height": int, "lines": [ { "text": str, +# "box": { "x","y","w","h" } } ] } +# Box convention matches the macOS Vision engine: normalized to [0,1] with +# the origin at the image's lower-left corner (y measured from the bottom). +# Exit codes: 0 ok / 2 bad args / 4 engine unavailable / 5 ocr failed + +param( + [Parameter(Mandatory = $true)][string]$ImagePath +) + +$ErrorActionPreference = 'Stop' + +$scriptVersion = "0.6.10" + +# Emit stdout/stderr as UTF-8 so the Node side decodes Chinese messages correctly. +try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} + +# Required for [System.WindowsRuntimeSystemExtensions] (AsTask reflection below). +# Some systems do not auto-load it when resolving WinRT types. +try { Add-Type -AssemblyName System.Runtime.WindowsRuntime } catch {} + +$null = [Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime] +$null = [Windows.Graphics.Imaging.BitmapDecoder, Windows.Foundation, ContentType = WindowsRuntime] +$null = [Windows.Storage.StorageFile, Windows.Foundation, ContentType = WindowsRuntime] +$null = [Windows.Globalization.Language, Windows.Foundation, ContentType = WindowsRuntime] +$null = [Windows.Security.Cryptography.CryptographicBuffer, Windows.Foundation, ContentType = WindowsRuntime] + +function Await($WinRtTask, $ResultType) { + $asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | + Where-Object { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0] + if (-not $asTaskGeneric) { + throw 'WinRT AsTask helper not found' + } + $asTask = $asTaskGeneric.MakeGenericMethod($ResultType) + $netTask = $asTask.Invoke($null, @($WinRtTask)) + $netTask.Wait(-1) | Out-Null + return $netTask.Result +} + +function Fail($code, $message) { + [Console]::Error.WriteLine("[winrt-$scriptVersion] $message") + exit $code +} + +if (-not (Test-Path -LiteralPath $ImagePath)) { + Fail 3 "image not found: $ImagePath" +} + +# Prefer Chinese, then any user-profile language. +$engine = $null +try { + $available = @([Windows.Media.Ocr.OcrEngine]::AvailableRecognizerLanguages) + $preferred = $available | Where-Object { $_.LanguageTag -like 'zh*' } | Select-Object -First 1 + if ($preferred) { + $engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage($preferred) + } + if (-not $engine) { + $engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromUserProfileLanguages() + } +} catch { + Fail 4 "ocr engine init failed: $($_.Exception.Message)" +} +if (-not $engine) { + Fail 4 "ocr engine unavailable: install a language pack (Settings > Time & Language > Language)" +} + +try { + $file = Await ([Windows.Storage.StorageFile]::GetFileFromPathAsync((Resolve-Path -LiteralPath $ImagePath).Path)) ([Windows.Storage.StorageFile]) + $stream = Await ($file.OpenAsync([Windows.Storage.FileAccessMode]::Read)) ([Windows.Storage.Streams.IRandomAccessStream]) + $decoder = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder]) + + # Accuracy boost: small images / small text hurt WinRT OCR badly, so upscale + # at decode time (max edge < 1200px gets 2x, capped at 4000px). Boxes are + # normalized afterwards, so coordinates stay consistent with the source. + $srcW = [uint32]$decoder.PixelWidth + $srcH = [uint32]$decoder.PixelHeight + $outW = $srcW + $outH = $srcH + $transform = New-Object Windows.Graphics.Imaging.BitmapTransform + $maxEdge = [Math]::Max($srcW, $srcH) + if ($maxEdge -gt 0 -and $maxEdge -lt 1200) { + $scale = [Math]::Min(4.0, 1200.0 / [double]$maxEdge) + $outW = [uint32][Math]::Min(4000, [Math]::Round([double]$srcW * $scale)) + $outH = [uint32][Math]::Min(4000, [Math]::Round([double]$srcH * $scale)) + $transform.ScaledWidth = $outW + $transform.ScaledHeight = $outH + } + $pixelData = Await ($decoder.GetPixelDataAsync( + [Windows.Graphics.Imaging.BitmapPixelFormat]::Bgra8, + [Windows.Graphics.Imaging.BitmapAlphaMode]::Premultiplied, + $transform, + [Windows.Graphics.Imaging.ExifOrientationMode]::IgnoreExifOrientation, + [Windows.Graphics.Imaging.ColorManagementMode]::DoNotColorManage)) ([Windows.Graphics.Imaging.PixelDataProvider]) + # PowerShell 的 byte[] 不能直接匹配 IBuffer 重载,需转成 IBuffer。 + # 任何失败都回退到原始不放大路径,保证 OCR 始终可用。 + $bitmap = $null + try { + $pixels = $pixelData.DetachPixelData() + $buffer = [Windows.Security.Cryptography.CryptographicBuffer]::CreateFromByteArray($pixels) + $bitmap = [Windows.Graphics.Imaging.SoftwareBitmap]::CreateCopyFromBuffer( + $buffer, + [Windows.Graphics.Imaging.BitmapPixelFormat]::Bgra8, + [int]$outW, + [int]$outH) + } catch { + $bitmap = $null + } + if (-not $bitmap) { + $bitmap = Await ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap]) + } + $result = Await ($engine.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult]) + + $width = [double]$srcW + $height = [double]$srcH + $lines = @() + foreach ($line in $result.Lines) { + $text = $line.Text + if ([string]::IsNullOrWhiteSpace($text)) { continue } + $rect = $null + foreach ($word in $line.Words) { + $r = $word.BoundingRect + if ($null -eq $rect) { + $rect = @{ + left = $r.X; top = $r.Y + right = ($r.X + $r.Width); bottom = ($r.Y + $r.Height) + } + } else { + $rect.left = [Math]::Min($rect.left, $r.X) + $rect.top = [Math]::Min($rect.top, $r.Y) + $rect.right = [Math]::Max($rect.right, $r.X + $r.Width) + $rect.bottom = [Math]::Max($rect.bottom, $r.Y + $r.Height) + } + } + if ($null -eq $rect) { + $rect = @{ left = 0; top = 0; right = $width; bottom = $height } + } + $lines += @{ + text = $text + box = @{ + x = [Math]::Max(0.0, [Math]::Min(1.0, $rect.left / $width)) + y = [Math]::Max(0.0, [Math]::Min(1.0, 1.0 - $rect.bottom / $height)) + w = [Math]::Max(0.0, [Math]::Min(1.0, ($rect.right - $rect.left) / $width)) + h = [Math]::Max(0.0, [Math]::Min(1.0, ($rect.bottom - $rect.top) / $height)) + } + } + } + + $payload = @{ width = $width; height = $height; lines = $lines } + [Console]::Out.Write(($payload | ConvertTo-Json -Compress -Depth 5)) + exit 0 +} catch { + Fail 5 "ocr failed: $($_.Exception.Message)" +} diff --git a/plugins/native-ocr/bin/onnx_ocr_backend.mjs b/plugins/native-ocr/bin/onnx_ocr_backend.mjs new file mode 100644 index 000000000..f553f6ff9 --- /dev/null +++ b/plugins/native-ocr/bin/onnx_ocr_backend.mjs @@ -0,0 +1,143 @@ +// ONNX OCR backend for the native-ocr ZTools plugin. +// Loaded via dynamic import() from preload.js. Lives in the plugin runtime +// directory next to a flat node_modules/ (see preload ONNX_RUNTIME_MANIFEST). +// +// Zero native deps besides onnxruntime-node (prebuilt N-API binary): +// - images decoded with pngjs / jpeg-js (pure JS) +// - resize implemented here (bilinear), replacing sharp +// - @gutenye/ocr-common provides detection/recognition + opencv-js (WASM) + +import fs from 'node:fs/promises'; +import { PNG } from 'pngjs'; +import jpeg from 'jpeg-js'; +import { InferenceSession } from 'onnxruntime-node'; +import BaseOcr, { registerBackend } from '@gutenye/ocr-common'; +import { splitIntoLineImages } from '@gutenye/ocr-common/splitIntoLineImages'; + +class FileUtils { + static async read(path) { + return fs.readFile(path, 'utf8'); + } +} + +class ImageRaw { + data; + width; + height; + constructor({ data, width, height }) { + this.data = data; + this.width = width; + this.height = height; + } + + static decode(buf, path) { + const lower = String(path || '').toLowerCase(); + if (lower.endsWith('.png') || (buf[0] === 0x89 && buf[1] === 0x50)) { + const decoded = PNG.sync.read(buf); + return new ImageRaw({ data: new Uint8Array(decoded.data), width: decoded.width, height: decoded.height }); + } + if (lower.endsWith('.bmp')) { + return ImageRaw.decodeBmp(buf); + } + const decoded = jpeg.decode(buf, { useTArray: true, formatAsRGBA: true }); + return new ImageRaw({ data: new Uint8Array(decoded.data), width: decoded.width, height: decoded.height }); + } + + static decodeBmp(buf) { + // Uncompressed 24/32-bit BMP only (screenshots are png; bmp is a fallback). + if (buf[0] !== 0x42 || buf[1] !== 0x4d) throw new Error('not a bmp file'); + const dataOffset = buf.readUInt32LE(10); + const width = buf.readInt32LE(18); + const heightSigned = buf.readInt32LE(22); + const bpp = buf.readUInt16LE(28); + if (bpp !== 24 && bpp !== 32) throw new Error(`unsupported bmp bpp: ${bpp}`); + const bottomUp = heightSigned > 0; + const height = Math.abs(heightSigned); + const bytesPerPx = bpp / 8; + const rowSize = Math.ceil((width * bytesPerPx) / 4) * 4; + const data = new Uint8Array(width * height * 4); + for (let y = 0; y < height; y += 1) { + const srcY = bottomUp ? height - 1 - y : y; + for (let x = 0; x < width; x += 1) { + const si = dataOffset + srcY * rowSize + x * bytesPerPx; + const di = (y * width + x) * 4; + data[di] = buf[si + 2]; + data[di + 1] = buf[si + 1]; + data[di + 2] = buf[si]; + data[di + 3] = 255; + } + } + return new ImageRaw({ data, width, height }); + } + + static async open(path) { + const buf = await fs.readFile(path); + return ImageRaw.decode(buf, path); + } + + async resize({ width, height }) { + if (!width) width = Math.max(1, Math.round((this.width * height) / this.height)); + if (!height) height = Math.max(1, Math.round((this.height * width) / this.width)); + const src = this.data; + const dst = new Uint8Array(width * height * 4); + const sx = this.width / width; + const sy = this.height / height; + for (let y = 0; y < height; y += 1) { + const fy = Math.min(this.height - 1, Math.max(0, (y + 0.5) * sy - 0.5)); + const y0 = Math.floor(fy); + const y1 = Math.min(this.height - 1, y0 + 1); + const wy = fy - y0; + for (let x = 0; x < width; x += 1) { + const fx = Math.min(this.width - 1, Math.max(0, (x + 0.5) * sx - 0.5)); + const x0 = Math.floor(fx); + const x1 = Math.min(this.width - 1, x0 + 1); + const wx = fx - x0; + const di = (y * width + x) * 4; + for (let c = 0; c < 4; c += 1) { + const p00 = src[(y0 * this.width + x0) * 4 + c]; + const p01 = src[(y0 * this.width + x1) * 4 + c]; + const p10 = src[(y1 * this.width + x0) * 4 + c]; + const p11 = src[(y1 * this.width + x1) * 4 + c]; + dst[di + c] = (p00 * (1 - wx) + p01 * wx) * (1 - wy) + + (p10 * (1 - wx) + p11 * wx) * wy; + } + } + } + return new ImageRaw({ data: dst, width, height }); + } +} + +registerBackend({ + FileUtils, + ImageRaw, + InferenceSession, + splitIntoLineImages, + defaultModels: undefined, +}); + +let ocrInstance = null; +let ocrModels = null; + +export async function ensureOcr(models) { + const signature = JSON.stringify(models); + if (ocrInstance && ocrModels === signature) { + return ocrInstance; + } + ocrInstance = await BaseOcr.create({ models }); + ocrModels = signature; + return ocrInstance; +} + +export async function detectText(models, imagePath) { + const ocr = await ensureOcr(models); + const lines = await ocr.detect(imagePath); + const source = await ImageRaw.open(imagePath); + return { + width: source.width, + height: source.height, + lines: lines.map((line) => ({ + text: String(line.text || ''), + box: line.box || null, + })), + }; +} diff --git a/plugins/native-ocr/bin/onnx_ocr_server.mjs b/plugins/native-ocr/bin/onnx_ocr_server.mjs new file mode 100644 index 000000000..88f37db04 --- /dev/null +++ b/plugins/native-ocr/bin/onnx_ocr_server.mjs @@ -0,0 +1,189 @@ +// ONNX OCR persistent server for the native-ocr ZTools plugin. +// Runs under Electron's bundled Node via ELECTRON_RUN_AS_NODE=1 (no system +// Node required). Must live inside the ONNX runtime directory so that ESM +// imports resolve against its flat node_modules/. +// +// Protocol (newline-delimited JSON over stdin/stdout, same as the other +// plugin servers): +// <- {"id": 1, "imagePath": "/path/img.png"} +// -> {"event": "ready"} +// -> {"id": 1, "ok": true, "width": W, "height": H, "items": [ +// {"text": str, "box": [[x,y]x4]} ]} +// -> {"id": 1, "ok": false, "error": "..."} +// -> {"event": "fatal", "error": "..."} +// Boxes are pixel coordinates in the source image, top-left origin. + +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { PNG } from 'pngjs'; +import jpeg from 'jpeg-js'; +import { InferenceSession } from 'onnxruntime-node'; +import BaseOcr, { registerBackend } from '@gutenye/ocr-common'; +import { splitIntoLineImages } from '@gutenye/ocr-common/splitIntoLineImages'; + +const RUNTIME_DIR = path.dirname(fileURLToPath(import.meta.url)); +const IDLE_TIMEOUT = 15 * 60; + +class FileUtils { + static async read(filePath) { + return fs.readFile(filePath, 'utf8'); + } +} + +class ImageRaw { + data; + width; + height; + constructor({ data, width, height }) { + this.data = data; + this.width = width; + this.height = height; + } + + static decode(buf, filePath) { + const lower = String(filePath || '').toLowerCase(); + if (lower.endsWith('.png') || (buf[0] === 0x89 && buf[1] === 0x50)) { + const decoded = PNG.sync.read(buf); + return new ImageRaw({ data: new Uint8Array(decoded.data), width: decoded.width, height: decoded.height }); + } + if (lower.endsWith('.bmp')) { + return ImageRaw.decodeBmp(buf); + } + const decoded = jpeg.decode(buf, { useTArray: true, formatAsRGBA: true }); + return new ImageRaw({ data: new Uint8Array(decoded.data), width: decoded.width, height: decoded.height }); + } + + static decodeBmp(buf) { + if (buf[0] !== 0x42 || buf[1] !== 0x4d) throw new Error('not a bmp file'); + const dataOffset = buf.readUInt32LE(10); + const width = buf.readInt32LE(18); + const heightSigned = buf.readInt32LE(22); + const bpp = buf.readUInt16LE(28); + if (bpp !== 24 && bpp !== 32) throw new Error(`unsupported bmp bpp: ${bpp}`); + const bottomUp = heightSigned > 0; + const height = Math.abs(heightSigned); + const bytesPerPx = bpp / 8; + const rowSize = Math.ceil((width * bytesPerPx) / 4) * 4; + const data = new Uint8Array(width * height * 4); + for (let y = 0; y < height; y += 1) { + const srcY = bottomUp ? height - 1 - y : y; + for (let x = 0; x < width; x += 1) { + const si = dataOffset + srcY * rowSize + x * bytesPerPx; + const di = (y * width + x) * 4; + data[di] = buf[si + 2]; + data[di + 1] = buf[si + 1]; + data[di + 2] = buf[si]; + data[di + 3] = 255; + } + } + return new ImageRaw({ data, width, height }); + } + + static async open(filePath) { + const buf = await fs.readFile(filePath); + return ImageRaw.decode(buf, filePath); + } + + async resize({ width, height }) { + if (!width) width = Math.max(1, Math.round((this.width * height) / this.height)); + if (!height) height = Math.max(1, Math.round((this.height * width) / this.width)); + const src = this.data; + const dst = new Uint8Array(width * height * 4); + const sx = this.width / width; + const sy = this.height / height; + for (let y = 0; y < height; y += 1) { + const fy = Math.min(this.height - 1, Math.max(0, (y + 0.5) * sy - 0.5)); + const y0 = Math.floor(fy); + const y1 = Math.min(this.height - 1, y0 + 1); + const wy = fy - y0; + for (let x = 0; x < width; x += 1) { + const fx = Math.min(this.width - 1, Math.max(0, (x + 0.5) * sx - 0.5)); + const x0 = Math.floor(fx); + const x1 = Math.min(this.width - 1, x0 + 1); + const wx = fx - x0; + const di = (y * width + x) * 4; + for (let c = 0; c < 4; c += 1) { + const p00 = src[(y0 * this.width + x0) * 4 + c]; + const p01 = src[(y0 * this.width + x1) * 4 + c]; + const p10 = src[(y1 * this.width + x0) * 4 + c]; + const p11 = src[(y1 * this.width + x1) * 4 + c]; + dst[di + c] = (p00 * (1 - wx) + p01 * wx) * (1 - wy) + + (p10 * (1 - wx) + p11 * wx) * wy; + } + } + } + return new ImageRaw({ data: dst, width, height }); + } +} + +registerBackend({ + FileUtils, + ImageRaw, + InferenceSession, + splitIntoLineImages, + defaultModels: undefined, +}); + +const MODELS = { + detectionPath: path.join(RUNTIME_DIR, 'assets', 'ch_PP-OCRv4_det_infer.onnx'), + recognitionPath: path.join(RUNTIME_DIR, 'assets', 'ch_PP-OCRv4_rec_infer.onnx'), + dictionaryPath: path.join(RUNTIME_DIR, 'assets', 'ppocr_keys_v1.txt'), +}; + +async function main() { + for (const file of Object.values(MODELS)) { + await fs.access(file); + } + const ocr = await BaseOcr.create({ models: MODELS }); + + const timer = setTimeout(() => process.exit(0), IDLE_TIMEOUT * 1000); + timer.unref?.(); + + process.stdout.write(JSON.stringify({ event: 'ready' }) + '\n'); + + let buffer = ''; + for await (const chunk of process.stdin) { + buffer += chunk.toString('utf8'); + let newlineIndex; + while ((newlineIndex = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, newlineIndex).trim(); + buffer = buffer.slice(newlineIndex + 1); + if (!line) continue; + let request; + try { + request = JSON.parse(line); + } catch (_) { + continue; + } + const response = { id: request.id }; + try { + const imagePath = String(request.imagePath || ''); + if (!imagePath) throw new Error('imagePath is required'); + const source = await ImageRaw.open(imagePath); + const detected = await ocr.detect(imagePath); + response.ok = true; + response.width = source.width; + response.height = source.height; + response.items = detected.map((line) => ({ + text: String(line.text || ''), + box: line.box || null, + })); + } catch (error) { + response.ok = false; + response.error = error && error.message ? error.message : String(error); + } + process.stdout.write(JSON.stringify(response) + '\n'); + timer.refresh?.(); + } + } + process.exit(0); +} + +main().catch((error) => { + process.stdout.write(JSON.stringify({ + event: 'fatal', + error: error && error.message ? error.message : String(error), + }) + '\n'); + process.exit(1); +}); diff --git a/plugins/native-ocr/index.html b/plugins/native-ocr/index.html new file mode 100644 index 000000000..78b00a099 --- /dev/null +++ b/plugins/native-ocr/index.html @@ -0,0 +1,12 @@ + + + + + + Native OCR + + +
+ + + diff --git a/plugins/native-ocr/package-lock.json b/plugins/native-ocr/package-lock.json new file mode 100644 index 000000000..6d0da1731 --- /dev/null +++ b/plugins/native-ocr/package-lock.json @@ -0,0 +1,2855 @@ +{ + "name": "native-ocr", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "native-ocr", + "version": "0.1.0", + "dependencies": { + "pdfjs-dist": "^6.3.289", + "vue": "^3.5.22", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "vite": "^7.1.11", + "vite-plugin-vue-devtools": "^8.0.3" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", + "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.29.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", + "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-decorators": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/canvas": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-1.0.8.tgz", + "integrity": "sha512-/SaLcvlqGWdm0HSCWMgHu7cjJiQXfP8/mOY+6dUyV9flQz7sPBBZ+ed2zYtoukojPmxOaL7bm+d/G4GeWWoN7g==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "1.0.8", + "@napi-rs/canvas-darwin-arm64": "1.0.8", + "@napi-rs/canvas-darwin-x64": "1.0.8", + "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.8", + "@napi-rs/canvas-linux-arm64-gnu": "1.0.8", + "@napi-rs/canvas-linux-arm64-musl": "1.0.8", + "@napi-rs/canvas-linux-riscv64-gnu": "1.0.8", + "@napi-rs/canvas-linux-x64-gnu": "1.0.8", + "@napi-rs/canvas-linux-x64-musl": "1.0.8", + "@napi-rs/canvas-win32-arm64-msvc": "1.0.8", + "@napi-rs/canvas-win32-x64-msvc": "1.0.8" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.8.tgz", + "integrity": "sha512-5+nkh8i3gt6lqS/d2jTZ1xAn6tdgtB4Lf1mW6T0Qm5/rXNwBuV1sAEyLEWan5o9gJPU/GuvHR3rvSeZ+FaGrbw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.8.tgz", + "integrity": "sha512-7jQ47gi+fZ7KJmfc/5rNyy1CYw/cu4kZ0KPIYbo9UUgSdW0bKQJpt+WihEor6s4Lyp7+xc3a+3HeyXmAEbbnPg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.8.tgz", + "integrity": "sha512-rRjDMZs9pIRKGxgijwezplKc1RnJsqUokrA9h88bbTkqQ+7ePj0ZN4ZnZDy8Vu0tXs7KRlI2tQLaK4mx9QlxHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.8.tgz", + "integrity": "sha512-jGcCd+8ra6Q61xKqZeiItujTpp9a9eRLcQ0jW6qYNku+WpupqOPFPY0SrsuSnXFviJwkpKYT9p7QrB4lsf3LNQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.8.tgz", + "integrity": "sha512-od6I2Y7kU7i1SwZYG2EKW8rWz6JiedtPpko4WEe1DDsiikrfaotVBCRaUTM5/yeZKaZ92EatoAS+5xG+6uJlYA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.8.tgz", + "integrity": "sha512-yYkPbJDJiWj6N0gASA3CAvRypZmVpJnxU0DQg3aBhneLDQde9TPLKADsQkobNoJUtTT/lj46aWpzT48PDb3Qcg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.8.tgz", + "integrity": "sha512-PB00MSKAp4VwK/xwe6duKxRKmH8UH4GIl1pqHSbxng0jnU9Dr7FwaDypDiqwNFZ774N+8G7mJLGuLtg9NTcQsg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.8.tgz", + "integrity": "sha512-TWM2XWJoitLiIPCvgJh7SriC+L/T9qkYCVzC66AidsZy0QP1hkKzBzVwshCdcA3q6fIn3yE0ISbq4lMJSy8jFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.8.tgz", + "integrity": "sha512-hb20MxKXXb5IB7AAwN8UHz9WRsa2HmdZfjsDCzjElwJoeV1aotVEwFU4FrFQcYQVzsJQLeaCc/2Qdt/0Q72mMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.8.tgz", + "integrity": "sha512-WwPN08IXE4SkL+FhJyPz/iFnycMAUkbphFIT4cmKLlvbSU0Zfn1R7BGJ3Hqky1S89QUYc0Q4IOScXb/42Re9wQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.8.tgz", + "integrity": "sha512-XkrVqKb+pxyba7kjy2LJvABFVBTE0DNpEl7MrG4OYUmaWarrXH+t54z/Czj2YxCKtizYTV4mg6phNm3x24qjhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz", + "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", + "integrity": "sha512-u9HHgfrq3AjXlysn0eINFnWQOJQLO9WN6VprZ8FXl7A2bYisv3Hui9Ij+7QZ41F/WYWarHjwBbXtD7dKg3uxbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.13" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz", + "integrity": "sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.5.0.tgz", + "integrity": "sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@vue/babel-helper-vue-transform-on": "1.5.0", + "@vue/babel-plugin-resolve-type": "1.5.0", + "@vue/shared": "^3.5.18" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.5.0.tgz", + "integrity": "sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/parser": "^7.28.0", + "@vue/compiler-sfc": "^3.5.18" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/devtools-core": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-8.1.2.tgz", + "integrity": "sha512-ZGGyaSBP4/+bN2Nd9ZHNYAVDRIzMw1rv2RyXWtyZlo6mQal+IDmTvKY4V+DjAEBhaXt30mHmsgYp1yXJ/2tIWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^8.1.2", + "@vue/devtools-shared": "^8.1.2" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.2.tgz", + "integrity": "sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^8.1.2", + "birpc": "^2.6.1", + "hookable": "^5.5.3", + "perfect-debounce": "^2.0.0" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.2.tgz", + "integrity": "sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "license": "MIT" + }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.28.tgz", + "integrity": "sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", + "dev": true, + "license": "ISC" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pdfjs-dist": { + "version": "6.3.289", + "resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-6.3.289.tgz", + "integrity": "sha512-ZHjSVpDa3D6izMq8/04lvkhkATUmL9px6ChPaXc1k6nU2Mrhlg1/7F0bdUqCwUjw3NsPTfPZsMDUU6ZIcRaeQw==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.13.0 || >=24" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^1.0.0" + } + }, + "node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/unplugin-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz", + "integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-dev-rpc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vite-dev-rpc/-/vite-dev-rpc-1.1.0.tgz", + "integrity": "sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==", + "dev": true, + "license": "MIT", + "dependencies": { + "birpc": "^2.4.0", + "vite-hot-client": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0" + } + }, + "node_modules/vite-hot-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.2.0.tgz", + "integrity": "sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-11.3.3.tgz", + "integrity": "sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.1.0", + "debug": "^4.4.1", + "error-stack-parser-es": "^1.0.5", + "ohash": "^2.0.11", + "open": "^10.2.0", + "perfect-debounce": "^2.0.0", + "sirv": "^3.0.1", + "unplugin-utils": "^0.3.0", + "vite-dev-rpc": "^1.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-8.1.2.tgz", + "integrity": "sha512-gt5h1CNryR9Hy0tvhSbqY3j0F7aj0pGxBxWLa1lXSiZVkhdWDf0vbCOZyjh8ivFGE6FDHTGy3zkcZGlMZdVHig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^8.1.2", + "@vue/devtools-kit": "^8.1.2", + "@vue/devtools-shared": "^8.1.2", + "sirv": "^3.0.2", + "vite-plugin-inspect": "^11.3.3", + "vite-plugin-vue-inspector": "^6.0.0" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-6.0.0.tgz", + "integrity": "sha512-OpyITJLgZNibxlrik1EmRtvXHDjLRxNPsWkGFTERZs2LgMEdG4W0WoFt5GIgp3a3jRou+eJR8U1zOBk/XQgEbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/plugins/native-ocr/package.json b/plugins/native-ocr/package.json new file mode 100644 index 000000000..f9037e99c --- /dev/null +++ b/plugins/native-ocr/package.json @@ -0,0 +1,25 @@ +{ + "name": "native-ocr", + "version": "0.6.11", + "private": true, + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "pdfjs-dist": "^6.3.289", + "vue": "^3.5.22", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "vite": "^7.1.11", + "vite-plugin-vue-devtools": "^8.0.3" + }, + "author": "Samson" +} diff --git a/plugins/native-ocr/public/logo.svg b/plugins/native-ocr/public/logo.svg new file mode 100644 index 000000000..f68e17e4f --- /dev/null +++ b/plugins/native-ocr/public/logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/native-ocr/public/package.json b/plugins/native-ocr/public/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/plugins/native-ocr/public/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/plugins/native-ocr/public/plugin.json b/plugins/native-ocr/public/plugin.json new file mode 100644 index 000000000..6d2fe08d5 --- /dev/null +++ b/plugins/native-ocr/public/plugin.json @@ -0,0 +1,46 @@ +{ + "name": "native-ocr", + "title": "Native OCR", + "description": "本地多引擎 OCR(微信 OCR / macOS Vision)", + "version": "0.6.11", + "author": "Samson", + "platform": [ + "darwin", + "win32" + ], + "main": "index.html", + "logo": "logo.svg", + "preload": "preload.js", + "development": { + "main": "http://localhost:5179/" + }, + "features": [ + { + "code": "native-ocr", + "explain": "Native OCR 图片识别", + "cmds": [ + "ocr", + "native ocr", + "nativeocr", + "文字识别" + ] + }, + { + "code": "native-ocr-capture", + "explain": "截图并 OCR 识别", + "cmds": [ + "截图并OCR识别" + ] + }, + { + "code": "native-ocr-image", + "explain": "识别图片文字", + "cmds": [ + { + "type": "img", + "label": "Native OCR 识别图片" + } + ] + } + ] +} diff --git a/plugins/native-ocr/public/preload.js b/plugins/native-ocr/public/preload.js new file mode 100644 index 000000000..b66b1b923 --- /dev/null +++ b/plugins/native-ocr/public/preload.js @@ -0,0 +1,1238 @@ +const fs = require("node:fs"); +const https = require("node:https"); +const os = require("node:os"); +const path = require("node:path"); +const crypto = require("node:crypto"); +const zlib = require("node:zlib"); +const { clipboard } = require("electron"); +const { execFile, spawn, spawnSync } = require("node:child_process"); + +const OCR_IMAGE_STORAGE_KEY = "native_ocr_image"; +const OCR_IMAGE_EVENT = "native-ocr-image"; +const RUNTIME_REGISTRY_URL = "https://registry.npmmirror.com/@ztools-center/wechat-ocr-native"; +const RUNTIME_DIST_PREFIX = "package/dist/"; +const RUNTIME_CACHE_ROOT = path.join(os.homedir(), "Library", "Application Support", "ZTools", "native-ocr"); +const RUNTIME_DIR = path.join(RUNTIME_CACHE_ROOT, "ocr-runtime"); +const REQUIRED_RUNTIME_FILES = [ + "index.js", + "wcocr_native.node", + "vendor/wechat-ocr-mac/lib/libwxocr.dylib", + "vendor/wechat-ocr-mac/lib/libmmmojo.dylib", + "vendor/wechat-ocr-mac/models/text_det_fp16_v1.xnet", + "vendor/wechat-ocr-mac/models/text_rec_fp16_v2.xnet", + "vendor/wechat-ocr-mac/models/charset_zh10798.txt" +]; +const MAX_IMAGE_BYTES = 25 * 1024 * 1024; +const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff"]); +// 微信 OCR 仅支持 macOS:下载 @ztools-center/wechat-ocr-native 运行时(仅 mac 构建)。 +// Windows 平台提供 引擎:Windows OCR(系统 WinRT)+ ONNX OCR(PP-OCR v4),均零依赖。 +const IS_MAC = process.platform === "darwin"; +const WECHAT_RUNTIME_UNSUPPORTED_MESSAGE = "微信 OCR 运行时包仅提供 macOS 构建"; + + +let runtimeInstallPromise = null; +let ocrModule = null; +let ocrModulePath = ""; + +function emitProgress(onProgress, payload) { + if (typeof onProgress !== "function") return; + try { + onProgress(payload); + } catch (_) { + // Ignore renderer callback failures. + } +} + +function getResponse(url, redirectCount = 0) { + return new Promise((resolve, reject) => { + const req = https.get(url, { + headers: { + "User-Agent": "ZTools-Native-OCR/0.1.0", + Accept: "application/json, application/octet-stream" + } + }, (res) => { + const statusCode = res.statusCode || 0; + if (statusCode >= 300 && statusCode < 400 && res.headers.location) { + res.resume(); + if (redirectCount >= 5) { + reject(new Error("下载地址重定向次数过多")); + return; + } + resolve(getResponse(new URL(res.headers.location, url).toString(), redirectCount + 1)); + return; + } + if (statusCode < 200 || statusCode >= 300) { + res.resume(); + reject(new Error(`请求失败: HTTP ${statusCode}`)); + return; + } + resolve(res); + }); + req.setTimeout(30000, () => req.destroy(new Error("网络请求超时"))); + req.on("error", reject); + }); +} + +async function fetchJson(url) { + const res = await getResponse(url); + const chunks = []; + for await (const chunk of res) chunks.push(chunk); + return JSON.parse(Buffer.concat(chunks).toString("utf8")); +} + +async function downloadFile(url, destination, version, onProgress) { + const res = await getResponse(url); + const total = Number(res.headers["content-length"] || 0); + let downloaded = 0; + await new Promise((resolve, reject) => { + const output = fs.createWriteStream(destination); + res.on("data", (chunk) => { + downloaded += chunk.length; + emitProgress(onProgress, { + phase: "download", + version, + downloaded, + total, + percent: total ? Math.round((downloaded / total) * 100) : 0 + }); + }); + res.on("error", reject); + output.on("error", reject); + output.on("finish", resolve); + res.pipe(output); + }); +} + +async function fetchLatestRuntime() { + const metadata = await fetchJson(RUNTIME_REGISTRY_URL); + const version = metadata && metadata["dist-tags"] && metadata["dist-tags"].latest; + const latest = version && metadata.versions && metadata.versions[version]; + const tarball = latest && latest.dist && latest.dist.tarball; + if (!version || !tarball) { + throw new Error("无法解析 OCR 运行时最新版本"); + } + return { + version, + tarball, + shasum: latest.dist.shasum || "", + unpackedSize: latest.dist.unpackedSize || 0 + }; +} + +function readJsonFile(file) { + try { + return JSON.parse(fs.readFileSync(file, "utf8")); + } catch (_) { + return null; + } +} + +function isSafeRelativePath(relativePath) { + return Boolean(relativePath) + && !path.isAbsolute(relativePath) + && !relativePath.split(/[\\/]+/).includes(".."); +} + +function parseTarString(buffer, start, length) { + const raw = buffer.subarray(start, start + length); + const end = raw.indexOf(0); + return raw.subarray(0, end === -1 ? raw.length : end).toString("utf8"); +} + +function parseTarOctal(buffer, start, length) { + const value = parseTarString(buffer, start, length).trim(); + return value ? parseInt(value, 8) : 0; +} + +function extractRuntimeDist(tgzPath, destination, onProgress) { + emitProgress(onProgress, { phase: "extract", percent: 0 }); + const tarBuffer = zlib.gunzipSync(fs.readFileSync(tgzPath)); + let offset = 0; + while (offset + 512 <= tarBuffer.length) { + const name = parseTarString(tarBuffer, offset, 100); + if (!name) break; + const prefix = parseTarString(tarBuffer, offset + 345, 155); + const entryName = prefix ? `${prefix}/${name}` : name; + const mode = parseTarOctal(tarBuffer, offset + 100, 8); + const size = parseTarOctal(tarBuffer, offset + 124, 12); + const type = parseTarString(tarBuffer, offset + 156, 1) || "0"; + const bodyStart = offset + 512; + const bodyEnd = bodyStart + size; + + if (entryName.startsWith(RUNTIME_DIST_PREFIX)) { + const relativePath = entryName.slice(RUNTIME_DIST_PREFIX.length); + if (isSafeRelativePath(relativePath)) { + const target = path.join(destination, relativePath); + if (type === "5") { + fs.mkdirSync(target, { recursive: true }); + } else if (type === "0") { + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, tarBuffer.subarray(bodyStart, bodyEnd)); + if (mode) { + try { + fs.chmodSync(target, mode); + } catch (_) { + // Ignore chmod failures on filesystems that do not support it. + } + } + } + } + } + + offset = bodyStart + Math.ceil(size / 512) * 512; + emitProgress(onProgress, { + phase: "extract", + percent: Math.min(100, Math.round((offset / tarBuffer.length) * 100)) + }); + } +} + +function getLocalRuntimeInfo(runtimeDir = RUNTIME_DIR) { + const missing = REQUIRED_RUNTIME_FILES.filter((file) => !fs.existsSync(path.join(runtimeDir, file))); + const meta = readJsonFile(path.join(runtimeDir, ".ztools-runtime.json")); + const runtimePackage = readJsonFile(path.join(runtimeDir, "package.json")); + return { + installed: missing.length === 0, + version: (meta && meta.version) || (runtimePackage && runtimePackage.version) || "", + path: runtimeDir, + missing + }; +} + +function validateRuntimeDir(runtimeDir) { + const info = getLocalRuntimeInfo(runtimeDir); + if (!info.installed) { + throw new Error(`OCR 运行时文件不完整: ${info.missing.join(", ")}`); + } +} + +function clearOcrRequireCache() { + const runtimePrefix = RUNTIME_DIR + path.sep; + for (const cachedPath of Object.keys(require.cache)) { + if (cachedPath === ocrModulePath || cachedPath.startsWith(runtimePrefix)) { + try { + delete require.cache[cachedPath]; + } catch (_) { + // Ignore cache cleanup failures. + } + } + } + ocrModule = null; + ocrModulePath = ""; +} + +async function checkRuntime() { + if (!IS_MAC) { + return { + status: "native", + ready: true, + version: "win-local", + latestVersion: "", + path: "", + message: "Windows 使用本地微信 OCR 组件,无需下载运行时" + }; + } + const local = getLocalRuntimeInfo(); + try { + const latest = await fetchLatestRuntime(); + if (local.installed && local.version === latest.version) { + return { + status: "ready", + ready: true, + version: local.version, + latestVersion: latest.version, + path: local.path + }; + } + return { + status: local.installed ? "outdated" : "missing", + ready: false, + version: local.version, + latestVersion: latest.version, + path: local.path, + tarball: latest.tarball, + message: local.installed ? "发现 OCR 运行时新版本" : "需要下载 OCR 运行时" + }; + } catch (error) { + if (local.installed) { + return { + status: "ready", + ready: true, + version: local.version, + latestVersion: "", + path: local.path, + offline: true, + message: "无法检查最新版本,已使用本地 OCR 运行时" + }; + } + return { + status: "error", + ready: false, + version: "", + latestVersion: "", + path: local.path, + message: error && error.message ? error.message : "无法检查 OCR 运行时" + }; + } +} + +async function installRuntime(onProgress) { + if (!IS_MAC) { + throw new Error(WECHAT_RUNTIME_UNSUPPORTED_MESSAGE); + } + if (runtimeInstallPromise) return runtimeInstallPromise; + runtimeInstallPromise = (async () => { + fs.mkdirSync(RUNTIME_CACHE_ROOT, { recursive: true }); + emitProgress(onProgress, { phase: "metadata", percent: 0 }); + const latest = await fetchLatestRuntime(); + const current = getLocalRuntimeInfo(); + if (current.installed && current.version === latest.version) { + return checkRuntime(); + } + + const tmpTgz = path.join(RUNTIME_CACHE_ROOT, `native-ocr-runtime-${latest.version}-${Date.now()}.tgz`); + const tmpRuntimeDir = path.join(RUNTIME_CACHE_ROOT, `.ocr-runtime-${process.pid}-${Date.now()}`); + const backupDir = path.join(RUNTIME_CACHE_ROOT, `.ocr-runtime-backup-${Date.now()}`); + try { + await downloadFile(latest.tarball, tmpTgz, latest.version, onProgress); + if (latest.shasum) { + const shasum = crypto.createHash("sha1").update(fs.readFileSync(tmpTgz)).digest("hex"); + if (shasum !== latest.shasum) { + throw new Error("OCR 运行时下载校验失败"); + } + } + fs.rmSync(tmpRuntimeDir, { recursive: true, force: true }); + fs.mkdirSync(tmpRuntimeDir, { recursive: true }); + extractRuntimeDist(tmpTgz, tmpRuntimeDir, onProgress); + validateRuntimeDir(tmpRuntimeDir); + fs.writeFileSync(path.join(tmpRuntimeDir, ".ztools-runtime.json"), JSON.stringify({ + version: latest.version, + tarball: latest.tarball, + installedAt: new Date().toISOString() + }, null, 2)); + + clearOcrRequireCache(); + if (fs.existsSync(RUNTIME_DIR)) { + fs.renameSync(RUNTIME_DIR, backupDir); + } + fs.renameSync(tmpRuntimeDir, RUNTIME_DIR); + fs.rmSync(backupDir, { recursive: true, force: true }); + emitProgress(onProgress, { phase: "done", version: latest.version, percent: 100 }); + return checkRuntime(); + } catch (error) { + if (fs.existsSync(backupDir) && !fs.existsSync(RUNTIME_DIR)) { + try { + fs.renameSync(backupDir, RUNTIME_DIR); + } catch (_) { + // Keep the original error. + } + } + throw error; + } finally { + fs.rmSync(tmpTgz, { force: true }); + fs.rmSync(tmpRuntimeDir, { recursive: true, force: true }); + fs.rmSync(backupDir, { recursive: true, force: true }); + runtimeInstallPromise = null; + } + })(); + return runtimeInstallPromise; +} + +function loadOcrRuntime() { + if (!IS_MAC) { + throw new Error(WECHAT_RUNTIME_UNSUPPORTED_MESSAGE); + } + const local = getLocalRuntimeInfo(); + if (!local.installed) { + throw new Error("OCR 运行时未下载,请先下载后再识别"); + } + const entry = path.join(RUNTIME_DIR, "index.js"); + if (!ocrModule || ocrModulePath !== entry) { + ocrModulePath = entry; + ocrModule = require(entry); + } + return ocrModule; +} + +function assertImagePath(imagePath) { + if (!imagePath || typeof imagePath !== "string") { + throw new Error("图片路径不能为空"); + } + const resolved = path.resolve(imagePath); + if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) { + throw new Error(`图片不存在: ${resolved}`); + } + const ext = path.extname(resolved).toLowerCase(); + if (!IMAGE_EXTENSIONS.has(ext)) { + throw new Error(`不支持的图片格式: ${ext || "unknown"}`); + } + if (fs.statSync(resolved).size > MAX_IMAGE_BYTES) { + throw new Error("图片文件过大"); + } + return resolved; +} + +function dataUrlToTempFile(dataUrl) { + if (typeof dataUrl !== "string" || !dataUrl.startsWith("data:image/")) { + throw new Error("无效的图片数据"); + } + const match = /^data:image\/([a-zA-Z0-9.+-]+);base64,(.+)$/.exec(dataUrl); + if (!match) { + throw new Error("图片数据不是 base64 Data URL"); + } + const subtype = match[1].toLowerCase(); + const ext = subtype === "jpeg" ? ".jpg" : `.${subtype}`; + if (!IMAGE_EXTENSIONS.has(ext)) { + throw new Error(`不支持的图片格式: ${ext}`); + } + const buffer = Buffer.from(match[2], "base64"); + if (!buffer.length || buffer.length > MAX_IMAGE_BYTES) { + throw new Error("图片数据为空或过大"); + } + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ztools-native-ocr-")); + const file = path.join(dir, `image${ext}`); + fs.writeFileSync(file, buffer); + return file; +} + +function normalizeResult(result) { + const text = result && typeof result.text === "string" ? result.text : ""; + return { + engine: result && result.engine ? result.engine : "wechat-wevision", + text, + lines: Array.isArray(result && result.lines) ? result.lines : text ? [{ text }] : [], + raw: result + }; +} + +function firstExistingImage(value) { + if (!value) return ""; + if (typeof value === "string") { + if (value.startsWith("data:image/")) return value; + if (fs.existsSync(value)) return value; + return ""; + } + if (Array.isArray(value)) { + for (const item of value) { + const image = firstExistingImage(item); + if (image) return image; + } + return ""; + } + if (typeof value === "object") { + const candidates = [ + value.path, + value.filePath, + value.img, + value.image, + value.src, + value.url, + value.data, + value.pastedImage, + value.payload, + value.files, + value.items + ]; + for (const item of candidates) { + const image = firstExistingImage(item); + if (image) return image; + } + } + return ""; +} + +function getImageFromAction(action) { + if (!action) return ""; + return firstExistingImage([ + action.payload, + action.inputState && action.inputState.pastedImage, + action.inputState && action.inputState.files, + action + ]); +} + +function publishImage(image) { + if (!image) return; + window.__NATIVE_OCR_PENDING_IMAGE__ = image; + try { + window.ztools.dbStorage.setItem(OCR_IMAGE_STORAGE_KEY, image); + } catch (_) { + // Ignore unavailable storage. + } + const dispatch = () => { + window.dispatchEvent(new CustomEvent(OCR_IMAGE_EVENT, { detail: { source: image } })); + }; + if (document.readyState === "loading") { + window.addEventListener("DOMContentLoaded", dispatch, { once: true }); + } else { + setTimeout(dispatch, 0); + } +} + +async function recognize(source) { + let tempFile = ""; + try { + const ocr = loadOcrRuntime(); + const imagePath = typeof source === "string" && source.startsWith("data:image/") + ? (tempFile = dataUrlToTempFile(source)) + : assertImagePath(source); + return normalizeResult(ocr.ocr(imagePath)); + } finally { + if (tempFile) { + try { + fs.rmSync(path.dirname(tempFile), { recursive: true, force: true }); + } catch (_) { + // Ignore temp cleanup failures. + } + } + } +} + +const cachedRuntimeScripts = {}; + +function ensureRuntimeScript(scriptPath, targetName) { + const cached = cachedRuntimeScripts[targetName]; + if (cached && fs.existsSync(cached)) { + return cached; + } + const content = fs.readFileSync(scriptPath, "utf8"); + const target = path.join(os.tmpdir(), targetName); + fs.writeFileSync(target, content); + cachedRuntimeScripts[targetName] = target; + return target; +} + +function ensureVisionScript(scriptPath) { + return ensureRuntimeScript(scriptPath, "ztools-native-ocr-vision.swift"); +} + +let cachedVisionBinary = ""; + +function ensureVisionBinary(scriptPath) { + if (cachedVisionBinary && fs.existsSync(cachedVisionBinary)) { + return cachedVisionBinary; + } + const scriptReal = ensureVisionScript(scriptPath); + const target = `${scriptReal.replace(/\.swift$/, "")}-bin`; + const result = spawnSync("swiftc", ["-O", scriptReal, "-o", target], { timeout: 180000 }); + if (result.status === 0 && fs.existsSync(target)) { + cachedVisionBinary = target; + return target; + } + return ""; +} + +function runVisionScript(scriptPath, imagePath) { + return new Promise((resolve, reject) => { + let cmd = "swift"; + let args = [ensureVisionScript(scriptPath), imagePath]; + try { + const binary = ensureVisionBinary(scriptPath); + if (binary) { + cmd = binary; + args = [imagePath]; + } + } catch (_) { + // Fall back to the swift interpreter. + } + execFile(cmd, args, { timeout: 60000 }, (err, stdout, stderr) => { + if (err) { + reject(new Error(stderr || err.message || "Vision 识别失败")); + return; + } + resolve(String(stdout || "").trim()); + }); + }); +} + +function runWinRtOcrScript(scriptPath, imagePath) { + const scriptReal = ensureRuntimeScript(scriptPath, "ztools-native-ocr-winrt.ps1"); + return new Promise((resolve, reject) => { + execFile("powershell.exe", [ + "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", + "-File", scriptReal, "-ImagePath", imagePath + ], { timeout: 60000, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (err, stdout, stderr) => { + if (err) { + reject(new Error(String(stderr || err.message || "Windows OCR 识别失败"))); + return; + } + resolve(String(stdout || "").trim()); + }); + }); +} + +function parseWinRtOutput(raw) { + const output = String(raw || "").trim(); + if (!output) { + return { engine: "vision", text: "", lines: [] }; + } + const parsed = JSON.parse(output); + const lines = []; + for (const item of (parsed && parsed.lines) || []) { + if (!item || typeof item.text !== "string") continue; + const line = { text: item.text }; + const box = item.box; + if (box && Number.isFinite(Number(box.x))) { + line.box = { x: Number(box.x), y: Number(box.y), w: Number(box.w), h: Number(box.h) }; + } + lines.push(line); + } + return { engine: "vision", text: lines.map((line) => line.text).join("\n"), lines }; +} + +async function recognizeWinRtOcr(source) { + let tempFile = ""; + try { + const imagePath = typeof source === "string" && source.startsWith("data:image/") + ? (tempFile = dataUrlToTempFile(source)) + : assertImagePath(source); + const scriptPath = path.join(__dirname, "bin", "ocr-winrt.ps1"); + const stdout = await runWinRtOcrScript(scriptPath, imagePath); + return parseWinRtOutput(stdout); + } finally { + if (tempFile) { + try { + fs.rmSync(path.dirname(tempFile), { recursive: true, force: true }); + } catch (_) { + // Ignore temp cleanup failures. + } + } + } +} + +function parseVisionOutput(raw, engineName = "vision") { const output = String(raw || "").trim(); + if (!output) { + return { engine: engineName, text: "", lines: [] }; + } + try { + const parsed = JSON.parse(output); + if (parsed && typeof parsed.error === "string") { + throw new Error(parsed.error); + } + const items = Array.isArray(parsed) ? parsed : null; + if (items) { + const lines = []; + for (const item of items) { + if (!item || typeof item.text !== "string") continue; + const line = { text: item.text }; + const box = item.box; + if (box && typeof box === "object") { + const x = Number(box.x); + const y = Number(box.y); + const w = Number(box.w); + const h = Number(box.h); + if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(w) && Number.isFinite(h)) { + line.box = { x, y, w, h }; + } + } + const score = Number(item.score); + if (Number.isFinite(score)) { + line.score = score; + } + lines.push(line); + } + const text = lines.map((line) => line.text).join("\n"); + return { engine: engineName, text, lines }; + } + } catch (error) { + if (error instanceof Error && error.message && !/JSON/i.test(error.message)) { + throw error; + } + // Fall through to plain-text parsing below. + } + const lines = output.split("\n").filter(Boolean).map((line) => ({ text: line })); + return { + engine: engineName, + text: lines.map((line) => line.text).join("\n"), + lines + }; +} + +async function recognizeVision(source) { + if (process.platform === "win32") { + return recognizeWinRtOcr(source); + } + if (process.platform !== "darwin") { + throw new Error("系统 OCR 仅支持 macOS / Windows"); + } + let tempFile = ""; + try { + const imagePath = typeof source === "string" && source.startsWith("data:image/") + ? (tempFile = dataUrlToTempFile(source)) + : assertImagePath(source); + const scriptPath = path.join(__dirname, "bin", "ocr-vision.swift"); + const stdout = await runVisionScript(scriptPath, imagePath); + return parseVisionOutput(stdout); + } finally { + if (tempFile) { + try { + fs.rmSync(path.dirname(tempFile), { recursive: true, force: true }); + } catch (_) { + // Ignore temp cleanup failures. + } + } + } +} + +// --------------------------------------------------------------------------- +// 翻译引擎(腾讯交互翻译 transmart,免密钥、国内直连、auto 源语言检测) +// --------------------------------------------------------------------------- + +const TRANSLATE_API_URL = "https://transmart.qq.com/api/imt"; +const TRANSLATE_CLIENT_KEY = "browser-chromium-Win32-120.0.0.0-204747517"; +const TRANSLATE_MAX_CHUNK = 4500; + +function httpsRequestJson(url, { method = "POST", body = null, timeout = 30000 } = {}) { + return new Promise((resolve, reject) => { + const request = https.request(url, { + method, + headers: { + "Content-Type": "application/json", + "User-Agent": "ZTools-Native-OCR/0.4.0" + } + }, (response) => { + const statusCode = response.statusCode || 0; + const chunks = []; + response.on("data", (chunk) => chunks.push(chunk)); + response.on("end", () => { + const text = Buffer.concat(chunks).toString("utf8"); + if (statusCode < 200 || statusCode >= 300) { + reject(new Error(`翻译请求失败: HTTP ${statusCode}`)); + return; + } + resolve(text); + }); + }); + request.setTimeout(timeout, () => request.destroy(new Error("翻译请求超时"))); + request.on("error", reject); + if (body) request.write(body); + request.end(); + }); +} + +function chunkText(text) { + const source = String(text || ""); + if (source.length <= TRANSLATE_MAX_CHUNK) { + return [source]; + } + const chunks = []; + const lines = source.split("\n"); + let current = ""; + for (const line of lines) { + if ((current + line).length > TRANSLATE_MAX_CHUNK && current) { + chunks.push(current); + current = ""; + } + current += `${line}\n`; + } + if (current.trim()) chunks.push(current); + return chunks.length ? chunks : [source]; +} + +async function translateText(text, toLang) { + const chunks = chunkText(text); + const results = []; + for (const chunk of chunks) { + const payload = JSON.stringify({ + header: { + fn: "auto_translation", + session: "", + client_key: TRANSLATE_CLIENT_KEY + }, + type: "plain", + model_category: "normal", + text_domain: "general", + source: { type: "1", lang: "auto", text_list: [chunk] }, + target: { type: "2", lang: toLang } + }); + const raw = await httpsRequestJson(TRANSLATE_API_URL, { body: payload, timeout: 45000 }); + let data; + try { + data = JSON.parse(raw); + } catch (_) { + throw new Error("翻译响应解析失败"); + } + if (!data || data.header?.ret_code !== "succ" || !Array.isArray(data.auto_translation)) { + throw new Error((data && data.message) || "翻译服务返回异常"); + } + results.push(data.auto_translation.join("\n")); + } + return { text: results.join("") }; +} + +const TRANSLATE_SEGMENTS_PER_REQUEST = 40; + +async function translateSegments(segments, toLang) { + const list = Array.isArray(segments) ? segments.map((item) => String(item || "")) : []; + const translated = new Array(list.length).fill(""); + for (let start = 0; start < list.length; start += TRANSLATE_SEGMENTS_PER_REQUEST) { + const slice = list.slice(start, start + TRANSLATE_SEGMENTS_PER_REQUEST); + const payload = JSON.stringify({ + header: { + fn: "auto_translation", + session: "", + client_key: TRANSLATE_CLIENT_KEY + }, + type: "plain", + model_category: "normal", + text_domain: "general", + source: { type: "1", lang: "auto", text_list: slice }, + target: { type: "2", lang: toLang } + }); + const raw = await httpsRequestJson(TRANSLATE_API_URL, { body: payload, timeout: 60000 }); + let data; + try { + data = JSON.parse(raw); + } catch (_) { + throw new Error("翻译响应解析失败"); + } + if (!data || data.header?.ret_code !== "succ" || !Array.isArray(data.auto_translation)) { + throw new Error((data && data.message) || "翻译服务返回异常"); + } + for (let i = 0; i < slice.length; i += 1) { + translated[start + i] = String(data.auto_translation[i] ?? ""); + } + } + return translated; +} + +function isVisionAvailable() { + if (process.platform === "win32") { + // Windows 系统 OCR 走内置 PowerShell + WinRT,Windows 10+ 必有。 + try { + const probe = spawnSync("powershell.exe", ["-NoProfile", "-Command", "$PSVersionTable.PSVersion.Major"], { timeout: 15000 }); + return probe.status === 0; + } catch (_) { + return false; + } + } + if (process.platform !== "darwin") { + return false; + } + try { + const result = spawnSync("which", ["swift"], { encoding: "utf8", timeout: 5000 }); + return result.status === 0 + && typeof result.stdout === "string" + && result.stdout.trim() !== ""; + } catch (_) { + return false; + } +} + +// --------------------------------------------------------------------------- +// Python 探测(仅供 Windows 微信 OCR 过渡链路使用;ONNX OCR / Vision 均无 Python) +// --------------------------------------------------------------------------- + +function pythonCandidates() { + if (process.platform === "win32") { + return ["python", "python3", "py"]; + } + // GUI 应用不继承 shell 的 PATH,需补充常见绝对路径 + return [ + "/opt/homebrew/bin/python3", + "/usr/local/bin/python3", + "/usr/bin/python3", + "python3", + "python" + ]; +} + +// --------------------------------------------------------------------------- +// ONNX OCR(RapidOCR 引擎的去 Python 形态,三端通用) +// 运行时包 = npmmirror 上的公开 tarball,下载解压为平铺 node_modules + 模型 +// --------------------------------------------------------------------------- + +const ONNX_RUNTIME_VERSION = 1; +const ONNX_RUNTIME_DIR = path.join(RUNTIME_CACHE_ROOT, "onnx-runtime"); +const ONNX_PACKAGES = [ + { name: "onnxruntime-node", version: "1.17.3-rev.1", shasum: "4ca954232525074915a1f80e84d9933b829dd34c", trimPlatformBin: true }, + { name: "onnxruntime-common", version: "1.17.3", shasum: "aadc456477873a540ee3d611ae9cd4f3de7c43e5" }, + { name: "@gutenye/ocr-common", version: "1.4.8", shasum: "81ff93715896ebe45ab4c84a620d74ce58fa19e0" }, + { name: "@techstark/opencv-js", version: "4.9.0-release.3", shasum: "efcf5b33611a05f3f3eb5b180e6582a064f4b5d1" }, + { name: "js-clipper", version: "1.0.1", shasum: "4d6b07434a4408e7c129e32201ce66d21474ed21" }, + { name: "tiny-invariant", version: "1.3.3", shasum: "46680b7a873a0d5d10005995eb90a70d74d60127" }, + { name: "pngjs", version: "7.0.0", shasum: "a8b7446020ebbc6ac739db6c5415a65d17090e26" }, + { name: "jpeg-js", version: "0.4.4", shasum: "a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa" }, + { name: "@gutenye/ocr-models", version: "1.4.2", shasum: "06c61c34dcda863a0babe9b033721613b87457c8", assetsOnly: true } +]; +const ONNX_ASSETS = ["ch_PP-OCRv4_det_infer.onnx", "ch_PP-OCRv4_rec_infer.onnx", "ppocr_keys_v1.txt"]; +const ONNX_REGISTRY_BASE = "https://registry.npmmirror.com"; + +function onnxModelsPaths() { + return { + detectionPath: path.join(ONNX_RUNTIME_DIR, "assets", "ch_PP-OCRv4_det_infer.onnx"), + recognitionPath: path.join(ONNX_RUNTIME_DIR, "assets", "ch_PP-OCRv4_rec_infer.onnx"), + dictionaryPath: path.join(ONNX_RUNTIME_DIR, "assets", "ppocr_keys_v1.txt") + }; +} + +function checkOnnxRuntime() { + const meta = readJsonFile(path.join(ONNX_RUNTIME_DIR, ".native-ocr-onnx.json")); + const installed = Boolean( + meta && meta.version === ONNX_RUNTIME_VERSION + && fs.existsSync(path.join(ONNX_RUNTIME_DIR, "node_modules", "@gutenye", "ocr-common", "package.json")) + && fs.existsSync(path.join(ONNX_RUNTIME_DIR, "onnx_ocr_server.mjs")) + && ONNX_ASSETS.every((file) => fs.existsSync(path.join(ONNX_RUNTIME_DIR, "assets", file))) + ); + return { + installed, + ready: installed, + version: (meta && meta.version) || "", + latestVersion: ONNX_RUNTIME_VERSION, + path: ONNX_RUNTIME_DIR, + models: onnxModelsPaths() + }; +} + +function isOnnxOcrAvailable() { + return checkOnnxRuntime().ready; +} + +function extractTarBuffer(tarBuffer, destination, { entryFilter, mapPath } = {}) { + let offset = 0; + while (offset + 512 <= tarBuffer.length) { + const name = parseTarString(tarBuffer, offset, 100); + if (!name) break; + const prefix = parseTarString(tarBuffer, offset + 345, 155); + const entryName = prefix ? `${prefix}/${name}` : name; + const mode = parseTarOctal(tarBuffer, offset + 100, 8); + const size = parseTarOctal(tarBuffer, offset + 124, 12); + const type = parseTarString(tarBuffer, offset + 156, 1) || "0"; + const bodyStart = offset + 512; + const bodyEnd = bodyStart + size; + const rel = entryName.startsWith("package/") ? entryName.slice("package/".length) : entryName; + if (rel && type === "0" && !rel.endsWith("/") && (!entryFilter || entryFilter(rel))) { + const mapped = mapPath ? mapPath(rel) : rel; + if (mapped && isSafeRelativePath(mapped)) { + const target = path.join(destination, mapped); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, tarBuffer.subarray(bodyStart, bodyEnd)); + if (mode) { + try { + fs.chmodSync(target, mode); + } catch (_) { + // Ignore chmod failures on filesystems that do not support it. + } + } + } + } + offset = bodyStart + Math.ceil(size / 512) * 512; + } +} + +let onnxInstallPromise = null; + +async function installOnnxRuntime(onProgress) { + if (onnxInstallPromise) return onnxInstallPromise; + onnxInstallPromise = (async () => { + fs.mkdirSync(ONNX_RUNTIME_DIR, { recursive: true }); + const doneBytes = { value: 0 }; + for (let index = 0; index < ONNX_PACKAGES.length; index += 1) { + const pkg = ONNX_PACKAGES[index]; + emitProgress(onProgress, { + phase: "download", + message: `获取 ${pkg.name}`, + percent: Math.round((index / ONNX_PACKAGES.length) * 100) + }); + const metadata = await fetchJson(`${ONNX_REGISTRY_BASE}/${pkg.name}/${pkg.version}`); + const tarball = metadata && metadata.dist && metadata.dist.tarball; + if (!tarball) { + throw new Error(`无法解析 ${pkg.name} 的下载地址`); + } + const tmpTgz = path.join(RUNTIME_CACHE_ROOT, `onnx-${pkg.name.replace(/[^\w.-]/g, "_")}-${pkg.version}.tgz`); + const previousTotal = doneBytes.value; + await downloadFile(tarball, tmpTgz, pkg.name, (progress) => { + if (progress.total) { + doneBytes.value = previousTotal + (progress.downloaded || 0); + emitProgress(onProgress, { + phase: "download", + message: `下载 ${pkg.name}`, + percent: Math.round(((index + (progress.downloaded || 0) / progress.total) / ONNX_PACKAGES.length) * 100) + }); + } + }); + const shasum = crypto.createHash("sha1").update(fs.readFileSync(tmpTgz)).digest("hex"); + if (metadata.dist && metadata.dist.shasum && shasum !== metadata.dist.shasum) { + fs.rmSync(tmpTgz, { force: true }); + throw new Error(`${pkg.name} 下载校验失败`); + } + if (pkg.shasum && shasum !== pkg.shasum) { + fs.rmSync(tmpTgz, { force: true }); + throw new Error(`${pkg.name} 版本与清单不一致(sha1 校验失败),请更新插件`); + } + emitProgress(onProgress, { phase: "extract", message: `解包 ${pkg.name}`, percent: Math.round(((index + 0.5) / ONNX_PACKAGES.length) * 100) }); + const tarBuffer = zlib.gunzipSync(fs.readFileSync(tmpTgz)); + if (pkg.assetsOnly) { + for (const asset of ONNX_ASSETS) { + extractTarBuffer(tarBuffer, ONNX_RUNTIME_DIR, { + entryFilter: (rel) => rel === `assets/${asset}`, + mapPath: (rel) => `assets/${path.basename(rel)}` + }); + } + } else { + const platformBinPrefix = `bin/napi-v3/${process.platform}/${process.arch}`; + extractTarBuffer(tarBuffer, path.join(ONNX_RUNTIME_DIR, "node_modules", pkg.name), { + entryFilter: pkg.trimPlatformBin + ? (rel) => !rel.startsWith("bin/") || rel.startsWith(platformBinPrefix) + : undefined + }); + } + fs.rmSync(tmpTgz, { force: true }); + } + fs.copyFileSync( + path.join(__dirname, "bin", "onnx_ocr_backend.mjs"), + path.join(ONNX_RUNTIME_DIR, "onnx_ocr_backend.mjs") + ); + fs.copyFileSync( + path.join(__dirname, "bin", "onnx_ocr_server.mjs"), + path.join(ONNX_RUNTIME_DIR, "onnx_ocr_server.mjs") + ); + fs.writeFileSync(path.join(ONNX_RUNTIME_DIR, ".native-ocr-onnx.json"), JSON.stringify({ + version: ONNX_RUNTIME_VERSION, + installedAt: new Date().toISOString() + }, null, 2)); + emitProgress(onProgress, { phase: "done", percent: 100 }); + return checkOnnxRuntime(); + })(); + try { + return await onnxInstallPromise; + } finally { + onnxInstallPromise = null; + } +} + +let onnxServerState = null; + +function killOnnxServer() { + if (onnxServerState) { + try { + onnxServerState.proc.kill(); + } catch (_) { + // Ignore kill failures. + } + onnxServerState = null; + } +} + +// ONNX 依赖 ESM 原生模块,渲染进程 preload 的动态 import() 走浏览器解析 +// (报 process is not defined),因此放在 Electron 自带 Node 的子进程里跑: +// process.execPath + ELECTRON_RUN_AS_NODE=1,无需系统安装 Node。 +function ensureOnnxServer() { + if (onnxServerState && onnxServerState.proc.exitCode === null) { + return onnxServerState; + } + const scriptPath = path.join(ONNX_RUNTIME_DIR, "onnx_ocr_server.mjs"); + if (!fs.existsSync(scriptPath)) { + throw new Error("ONNX OCR 服务脚本缺失,请重新下载引擎"); + } + const proc = spawn(process.execPath, [scriptPath], { + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, + env: { + ...process.env, + ELECTRON_RUN_AS_NODE: "1" + } + }); + const state = { + proc, + pending: new Map(), + nextId: 1, + ready: false, + stdoutBuffer: "", + stderrTail: "" + }; + proc.stdout.setEncoding("utf8"); + proc.stdout.on("data", (chunk) => { + state.stdoutBuffer += chunk; + let newlineIndex; + while ((newlineIndex = state.stdoutBuffer.indexOf("\n")) >= 0) { + const line = state.stdoutBuffer.slice(0, newlineIndex).trim(); + state.stdoutBuffer = state.stdoutBuffer.slice(newlineIndex + 1); + if (!line) continue; + let message; + try { + message = JSON.parse(line); + } catch (_) { + continue; + } + if (message && message.event === "ready") { + state.ready = true; + continue; + } + if (message && message.event === "fatal") { + state.fatalError = message.error || "ONNX OCR 服务启动失败"; + continue; + } + if (message && state.pending.has(message.id)) { + const entry = state.pending.get(message.id); + state.pending.delete(message.id); + clearTimeout(entry.timer); + if (message.ok) entry.resolve(message); + else entry.reject(new Error(message.error || "ONNX OCR 识别失败")); + } + } + }); + proc.stderr.setEncoding("utf8"); + proc.stderr.on("data", (chunk) => { + state.stderrTail = (state.stderrTail + chunk).slice(-800); + }); + const failAll = (error) => { + for (const entry of state.pending.values()) { + clearTimeout(entry.timer); + entry.reject(error); + } + state.pending.clear(); + if (onnxServerState === state) { + onnxServerState = null; + } + }; + proc.on("exit", () => failAll(new Error(state.fatalError || "ONNX OCR 服务进程已退出"))); + proc.on("error", (error) => failAll(error)); + onnxServerState = state; + return state; +} + +function waitOnnxServerReady(state, timeoutMs = 90000) { + if (state.ready) return Promise.resolve(); + return new Promise((resolve, reject) => { + const started = Date.now(); + const poll = () => { + if (state.ready) { + resolve(); + return; + } + if (state.proc.exitCode !== null) { + reject(new Error(state.fatalError || "ONNX OCR 服务进程启动失败")); + return; + } + if (state.fatalError) { + reject(new Error(state.fatalError)); + return; + } + if (Date.now() - started > timeoutMs) { + reject(new Error("ONNX OCR 服务启动超时")); + return; + } + setTimeout(poll, 120); + }; + poll(); + }); +} + +function onnxServerRecognize(state, imagePath, timeoutMs = 120000) { + return new Promise((resolve, reject) => { + const id = state.nextId; + state.nextId += 1; + const timer = setTimeout(() => { + state.pending.delete(id); + reject(new Error("ONNX OCR 识别超时")); + }, timeoutMs); + state.pending.set(id, { resolve, reject, timer }); + state.proc.stdin.write(`${JSON.stringify({ id, imagePath })}\n`); + }); +} + +async function recognizeOnnxOcr(source) { + const check = checkOnnxRuntime(); + if (!check.ready) { + throw new Error("ONNX OCR 引擎未下载,请先下载引擎"); + } + let tempFile = ""; + try { + const imagePath = typeof source === "string" && source.startsWith("data:image/") + ? (tempFile = dataUrlToTempFile(source)) + : assertImagePath(source); + const state = ensureOnnxServer(); + await waitOnnxServerReady(state); + const message = await onnxServerRecognize(state, imagePath); + const width = Number(message.width) || 1; + const height = Number(message.height) || 1; + const items = Array.isArray(message.items) ? message.items : []; + return { + engine: "rapidocr", + text: items.map((item) => item.text).join("\n"), + lines: items.map((item) => { + const polygon = Array.isArray(item.box) ? item.box : []; + if (!polygon.length) { + return { text: item.text }; + } + const xs = polygon.map((point) => point[0]); + const ys = polygon.map((point) => point[1]); + const left = Math.min(...xs); + const right = Math.max(...xs); + const top = Math.min(...ys); + const bottom = Math.max(...ys); + return { + text: item.text, + box: { + x: left / width, + y: 1 - bottom / height, + w: (right - left) / width, + h: (bottom - top) / height + } + }; + }), + raw: message + }; + } finally { + if (tempFile) { + try { + fs.rmSync(path.dirname(tempFile), { recursive: true, force: true }); + } catch (_) { + // Ignore temp cleanup failures. + } + } + } +} + +// --------------------------------------------------------------------------- +// 微信 OCR(Windows):纯 Python 调用微信 PC 自带 OCR(社区 wechat-ocr 包) +// 依赖:Windows + Python + pip install wechat-ocr + 微信 PC 3.9.x +// 协议与 rapid_ocr_server 完全一致(JSON-line stdin/stdout) +// --------------------------------------------------------------------------- + +window.nativeOcr = { + checkRuntime, + installRuntime, + recognize, + recognizeVision, + isVisionAvailable, + recognizeOnnxOcr, + isOnnxOcrAvailable, + installOnnxRuntime, + getPlatform() { + return process.platform; + }, + translateText, + translateSegments, + getImageFromAction, + readImageDataUrl(imagePath) { + const resolved = assertImagePath(imagePath); + const ext = path.extname(resolved).toLowerCase(); + const mimeMap = { + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".webp": "image/webp", + ".bmp": "image/bmp", + ".tif": "image/tiff", + ".tiff": "image/tiff" + }; + return `data:${mimeMap[ext] || "application/octet-stream"};base64,${fs.readFileSync(resolved).toString("base64")}`; + }, + copyText(text) { + clipboard.writeText(String(text || "")); + return true; + } +}; + +try { + if (window.ztools && window.ztools.onPluginEnter) { + window.ztools.onPluginEnter((action) => { + const image = getImageFromAction(action); + if (image) publishImage(image); + }); + } +} catch (_) { + // Ignore host API failures. +} diff --git a/plugins/native-ocr/src/App.vue b/plugins/native-ocr/src/App.vue new file mode 100644 index 000000000..6e14c20d2 --- /dev/null +++ b/plugins/native-ocr/src/App.vue @@ -0,0 +1,2571 @@ + + + + + diff --git a/plugins/native-ocr/src/lib/tableModel.js b/plugins/native-ocr/src/lib/tableModel.js new file mode 100644 index 000000000..92f3565d4 --- /dev/null +++ b/plugins/native-ocr/src/lib/tableModel.js @@ -0,0 +1,148 @@ +// 表格聚类/网格构建纯函数模块 —— App.vue 与 tests/run.mjs 共用 +// 坐标约定:归一化 [0,1],行 y 为底部原点(与 Vision 输出一致),列 x 为左侧原点 + +export const LOW_CONFIDENCE_THRESHOLD = 0.85 + +export function median(values) { + if (!values || !values.length) return 0 + const sorted = [...values].sort((a, b) => a - b) + const mid = Math.floor(sorted.length / 2) + return sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2 +} + +export function extractCells(lines) { + const cells = [] + const list = Array.isArray(lines) ? lines : [] + for (let index = 0; index < list.length; index += 1) { + const line = list[index] + const box = line && line.box + const x = Number(box && box.x) + const y = Number(box && box.y) + const w = Number(box && box.w) + const h = Number(box && box.h) + if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(w) || !Number.isFinite(h)) continue + const cell = { + text: String(line.text || ''), + cx: x + w / 2, + cy: y + h / 2, + w, + h + } + const score = Number(line && line.score) + if (Number.isFinite(score)) cell.score = score + cells.push(cell) + } + return cells +} + +export function clusterRows(cells, threshold) { + const sorted = [...cells].sort((a, b) => b.cy - a.cy) + const rows = [] + for (const cell of sorted) { + const last = rows[rows.length - 1] + if (last && Math.abs(cell.cy - last.cy) <= threshold) { + last.cells.push(cell) + last.cy = last.cells.reduce((sum, item) => sum + item.cy, 0) / last.cells.length + } else { + rows.push({ cy: cell.cy, cells: [cell] }) + } + } + return rows +} + +export function clusterColumns(cells, threshold) { + const sorted = [...cells].sort((a, b) => a.cx - b.cx) + const cols = [] + for (const cell of sorted) { + const last = cols[cols.length - 1] + if (last && Math.abs(cell.cx - last.cx) <= threshold) { + last.cells.push(cell) + last.cx = last.cells.reduce((sum, item) => sum + item.cx, 0) / last.cells.length + } else { + cols.push({ cx: cell.cx, cells: [cell] }) + } + } + return cols +} + +export function clusterTable(lines, factor = 0.6) { + const cells = extractCells(lines) + if (!cells.length) { + return { grid: [], rowClusters: [], colClusters: [] } + } + const rowThreshold = median(cells.map((cell) => cell.h)) * factor + const colThreshold = median(cells.map((cell) => cell.w)) * factor + const rowClusters = clusterRows(cells, rowThreshold) + const colClusters = clusterColumns(cells, colThreshold) + + const rowCount = rowClusters.length + const colCount = colClusters.length + const grid = Array.from({ length: rowCount }, () => Array(colCount).fill('')) + + for (const cell of cells) { + let bestRow = 0 + let bestRowDist = Infinity + for (let r = 0; r < rowCount; r += 1) { + const dist = Math.abs(cell.cy - rowClusters[r].cy) + if (dist < bestRowDist) { + bestRowDist = dist + bestRow = r + } + } + let bestCol = 0 + let bestColDist = Infinity + for (let c = 0; c < colCount; c += 1) { + const dist = Math.abs(cell.cx - colClusters[c].cx) + if (dist < bestColDist) { + bestColDist = dist + bestCol = c + } + } + grid[bestRow][bestCol] = grid[bestRow][bestCol] ? `${grid[bestRow][bestCol]} ${cell.text}` : cell.text + } + + return { grid, rowClusters, colClusters } +} + +export function bandIndexFor(value, seps) { + let count = 0 + for (const sep of seps) { + if (sep < value) count += 1 + } + return count +} + +/** + * 按分隔线构建网格:单元格按带归属落格,行索引需底部原点反转。 + * edits: { "r-c": "text" } 手动编辑覆盖。 + * 返回 { grid, lowCells },lowCells 为置信度低于阈值的 "r-c" 集合。 + */ +export function buildGrid(cells, rowSeps, colSeps, edits = {}) { + const rows = rowSeps.length + 1 + const cols = colSeps.length + 1 + const grid = Array.from({ length: rows }, () => Array(cols).fill('')) + const minScore = {} + + for (const cell of cells) { + const r = rowSeps.length - bandIndexFor(cell.cy, rowSeps) + const c = bandIndexFor(cell.cx, colSeps) + grid[r][c] = grid[r][c] ? `${grid[r][c]} ${cell.text}` : cell.text + if (Number.isFinite(cell.score)) { + const key = `${r}-${c}` + minScore[key] = Math.min(minScore[key] ?? 1, cell.score) + } + } + + for (const key of Object.keys(edits || {})) { + const [r, c] = key.split('-').map(Number) + if (Number.isFinite(r) && Number.isFinite(c) && r >= 0 && r < rows && c >= 0 && c < cols) { + grid[r][c] = edits[key] + } + } + + const lowCells = new Set() + for (const [key, score] of Object.entries(minScore)) { + if (score < LOW_CONFIDENCE_THRESHOLD) lowCells.add(key) + } + return { grid, lowCells } +} diff --git a/plugins/native-ocr/src/main.js b/plugins/native-ocr/src/main.js new file mode 100644 index 000000000..a0140719b --- /dev/null +++ b/plugins/native-ocr/src/main.js @@ -0,0 +1,5 @@ +import { createApp } from 'vue' +import App from './App.vue' +import './styles/global.css' + +createApp(App).mount('#app') diff --git a/plugins/native-ocr/src/styles/global.css b/plugins/native-ocr/src/styles/global.css new file mode 100644 index 000000000..87541bb21 --- /dev/null +++ b/plugins/native-ocr/src/styles/global.css @@ -0,0 +1,49 @@ +@import './variables.css'; + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body { + margin: 0; + background: transparent; + color: var(--text-primary); +} + +#app { + width: 100%; + min-width: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; +} + +button, +textarea { + font: inherit; +} + +button { + border: 0; +} + +::-webkit-scrollbar { + width: 5px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} diff --git a/plugins/native-ocr/src/styles/variables.css b/plugins/native-ocr/src/styles/variables.css new file mode 100644 index 000000000..31c04b586 --- /dev/null +++ b/plugins/native-ocr/src/styles/variables.css @@ -0,0 +1,45 @@ +:root { + --bg-app: transparent; + --bg-surface: transparent; + --bg-panel: rgba(255, 255, 255, 0.28); + --bg-panel-muted: rgba(239, 244, 251, 0.58); + --bg-hover: rgba(245, 245, 245, 0.7); + --bg-hover-light: rgba(248, 248, 248, 0.5); + --bg-overlay: rgba(244, 246, 251, 0.78); + --primary-color: #1a73e8; + --primary-hover: #1557b0; + --primary-shadow: rgba(26, 115, 232, 0.32); + --text-primary: #333333; + --text-secondary: #666666; + --text-tertiary: #999999; + --text-white: white; + --border-color: rgba(0, 0, 0, 0.1); + --spinner-bg: rgba(0, 0, 0, 0.06); + --switch-bg: rgba(120, 132, 150, 0.5); + --toast-bg: rgba(36, 36, 36, 0.92); + --toast-text: #ffffff; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg-app: transparent; + --bg-surface: transparent; + --bg-panel: rgba(24, 28, 38, 0.22); + --bg-panel-muted: rgba(38, 44, 58, 0.42); + --bg-hover: rgba(50, 50, 70, 0.7); + --bg-hover-light: rgba(40, 40, 60, 0.5); + --bg-overlay: rgba(18, 22, 30, 0.74); + --primary-color: #4dabf7; + --primary-hover: #4299e1; + --primary-shadow: rgba(77, 171, 247, 0.28); + --text-primary: #e0e0e0; + --text-secondary: #a0a0a0; + --text-tertiary: #757575; + --text-white: white; + --border-color: rgba(255, 255, 255, 0.1); + --spinner-bg: rgba(255, 255, 255, 0.1); + --switch-bg: rgba(130, 140, 160, 0.46); + --toast-bg: rgba(245, 245, 245, 0.94); + --toast-text: #1f1f1f; + } +} diff --git a/plugins/native-ocr/tests/run.mjs b/plugins/native-ocr/tests/run.mjs new file mode 100644 index 000000000..a17eace19 --- /dev/null +++ b/plugins/native-ocr/tests/run.mjs @@ -0,0 +1,162 @@ +// native-ocr 表格模型回归测试 —— node tests/run.mjs +import assert from 'node:assert/strict' +import { + bandIndexFor, + buildGrid, + clusterTable, + extractCells, + LOW_CONFIDENCE_THRESHOLD, + median +} from '../src/lib/tableModel.js' + +let passed = 0 +let failed = 0 + +function test(name, fn) { + try { + fn() + passed += 1 + console.log(` ✓ ${name}`) + } catch (error) { + failed += 1 + console.error(` ✗ ${name}\n ${error.message}`) + } +} + +function makeLines(rows, cols, score = null) { + // 生成 rows×cols 的归一化坐标行(行距 0.35,列距 0.3,居中) + const lines = [] + for (let r = 0; r < rows; r += 1) { + for (let c = 0; c < cols; c += 1) { + const line = { + text: `R${r + 1}C${c + 1}`, + box: { + x: 0.2 + c * 0.3, + y: 1 - (0.25 + r * 0.35) - 0.12, + w: 0.12, + h: 0.12 + } + } + if (score !== null) line.score = score + lines.push(line) + } + } + return lines +} + +function autoSeps(lines) { + const cells = extractCells(lines) + const rowThreshold = median(cells.map((c) => c.h)) * 0.6 + const colThreshold = median(cells.map((c) => c.w)) * 0.6 + const { rowClusters, colClusters } = clusterTable(lines, 0.6) + const rowSeps = [] + for (let i = 0; i < rowClusters.length - 1; i += 1) { + rowSeps.push((rowClusters[i].cy + rowClusters[i + 1].cy) / 2) + } + const colSeps = [] + for (let i = 0; i < colClusters.length - 1; i += 1) { + colSeps.push((colClusters[i].cx + colClusters[i + 1].cx) / 2) + } + void rowThreshold + void colThreshold + return { + rowSeps: rowSeps.sort((a, b) => a - b), + colSeps: colSeps.sort((a, b) => a - b) + } +} + +console.log('[tableModel]') + +test('median 奇偶长度', () => { + assert.equal(median([3, 1, 2]), 2) + assert.equal(median([4, 1, 2, 3]), 2.5) +}) + +test('自动聚类等价分隔线模型(2×3)', () => { + const lines = makeLines(2, 3) + const auto = clusterTable(lines, 0.6) + const { rowSeps, colSeps } = autoSeps(lines) + const cells = extractCells(lines) + const model = buildGrid(cells, rowSeps, colSeps) + assert.deepEqual(model.grid, auto.grid) + assert.deepEqual(model.grid, [['R1C1', 'R1C2', 'R1C3'], ['R2C1', 'R2C2', 'R2C3']]) +}) + +test('拖动行分隔线重排(移到边缘产生空带)', () => { + const lines = makeLines(2, 3) + const auto = autoSeps(lines) + const dragged = [0.05] + const cells = extractCells(lines) + const { grid } = buildGrid(cells, dragged, auto.colSeps) + assert.equal(grid.length, 2) + assert.equal(grid[0].join('|'), 'R1C1 R2C1|R1C2 R2C2|R1C3 R2C3') + assert.ok(grid[1].every((cell) => cell === '')) +}) + +test('双击删线合并行列', () => { + const lines = makeLines(2, 3) + const auto = autoSeps(lines) + const cells = extractCells(lines) + const { grid } = buildGrid(cells, auto.rowSeps, auto.colSeps.slice(1)) + assert.equal(grid[0].length, 2) + assert.equal(grid[0][0], 'R1C1 R1C2') + assert.equal(grid[0][1], 'R1C3') + assert.equal(grid[1][0], 'R2C1 R2C2') +}) + +test('+列线在最大空隙插入产生空列', () => { + const lines = makeLines(2, 3) + const auto = autoSeps(lines) + const bounds = [0, ...auto.colSeps, 1] + let bestGap = -1 + let bestPos = 0.5 + for (let i = 0; i < bounds.length - 1; i += 1) { + const gap = bounds[i + 1] - bounds[i] + if (gap > bestGap) { + bestGap = gap + bestPos = (bounds[i] + bounds[i + 1]) / 2 + } + } + const cells = extractCells(lines) + const { grid } = buildGrid(cells, auto.rowSeps, [...auto.colSeps, bestPos].sort((a, b) => a - b)) + assert.equal(grid[0].length, 4) + const emptyIndex = grid[0].findIndex((cell) => cell === '') + assert.ok(emptyIndex >= 0, '应存在一个空列') + const filled = grid[0].filter((cell) => cell) + assert.deepEqual(filled, ['R1C1', 'R1C2', 'R1C3']) +}) + +test('单元格编辑覆盖', () => { + const lines = makeLines(2, 3) + const { rowSeps, colSeps } = autoSeps(lines) + const cells = extractCells(lines) + const { grid } = buildGrid(cells, rowSeps, colSeps, { '0-1': '校对后' }) + assert.equal(grid[0][1], '校对后') + assert.equal(grid[1][2], 'R2C3') +}) + +test('低置信度单元格标记', () => { + const low = makeLines(2, 3, 0.6) + const high = makeLines(2, 3, 0.95) + const { rowSeps, colSeps } = autoSeps(low) + const lowResult = buildGrid(extractCells(low), rowSeps, colSeps) + const highResult = buildGrid(extractCells(high), rowSeps, colSeps) + assert.equal(lowResult.lowCells.size, 6) + assert.equal(highResult.lowCells.size, 0) + assert.ok(LOW_CONFIDENCE_THRESHOLD === 0.85) +}) + +test('bandIndexFor 边界', () => { + assert.equal(bandIndexFor(0.5, []), 0) + assert.equal(bandIndexFor(0.2, [0.3, 0.6]), 0) + assert.equal(bandIndexFor(0.4, [0.3, 0.6]), 1) + assert.equal(bandIndexFor(0.9, [0.3, 0.6]), 2) +}) + +test('无 box 行不影响提取', () => { + const lines = [{ text: 'no box' }, ...makeLines(1, 1)] + assert.equal(extractCells(lines).length, 1) +}) + +console.log(`\n${passed} 通过 / ${failed} 失败`) +process.exit(failed ? 1 : 0) diff --git a/plugins/native-ocr/vite.config.js b/plugins/native-ocr/vite.config.js new file mode 100644 index 000000000..a1446df7b --- /dev/null +++ b/plugins/native-ocr/vite.config.js @@ -0,0 +1,41 @@ +import { fileURLToPath, URL } from 'node:url' +import { copyFileSync, cpSync, existsSync } from 'node:fs' +import { resolve } from 'node:path' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' + +function copyExtras() { + return { + name: 'copy-extras', + closeBundle() { + copyFileSync(resolve('README.md'), resolve('dist/README.md')) + if (existsSync(resolve('LICENSE'))) { + copyFileSync(resolve('LICENSE'), resolve('dist/LICENSE')) + } + const binSrc = resolve('bin') + const binDest = resolve('dist/bin') + if (existsSync(binSrc)) { + cpSync(binSrc, binDest, { recursive: true }) + } + } + } +} + +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + copyExtras() + ], + base: './', + server: { + port: 5179 + }, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } +})