├── pnpm-workspace.yaml
├── vite.helper.js
├── vite.constant.js
├── public
└── favicon.ico
├── vite.config.js
├── .editorconfig
├── Cargo.toml
├── package.json
├── .github
└── workflows
│ ├── preview-start.yml
│ ├── deploy-to-github-pages.yml
│ ├── preview-build.yml
│ └── preview-deploy.yml
├── LICENSE
├── index.html
├── README.zh-CN.md
├── .gitignore
├── README.md
├── Cargo.lock
├── src
├── lib.rs
├── app.css
└── app.js
└── pnpm-lock.yaml
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | onlyBuiltDependencies:
2 | - esbuild
3 |
--------------------------------------------------------------------------------
/vite.helper.js:
--------------------------------------------------------------------------------
1 | export const isProd = (mode) => mode === 'production';
2 |
--------------------------------------------------------------------------------
/vite.constant.js:
--------------------------------------------------------------------------------
1 | export const BASE = '/chromium-style-qrcode-generator-with-wasm/';
2 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liuliangsir/chromium-style-qrcode-generator-with-wasm/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 |
3 | import { BASE } from './vite.constant';
4 | import { isProd } from './vite.helper';
5 |
6 | // https://vitejs.dev/config/
7 | export default ({ mode }) =>
8 | defineConfig({ ...(isProd(mode) ? { base: BASE } : null) });
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: https://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 | quote_type = single
14 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "chromium-style-qrcode-generator-with-wasm"
3 | version = "1.0.0"
4 | edition = "2024"
5 |
6 | [lib]
7 | crate-type = ["cdylib"] # Critical for Wasm
8 |
9 | [dependencies]
10 | qr_code = "2.0.0"
11 | wasm-bindgen = "0.2"
12 | console_error_panic_hook = { version = "0.1.7", optional = true }
13 |
14 | [features]
15 | default = ["console_error_panic_hook"] # Enable panic hook by default
16 |
17 | [profile.release]
18 | lto = true # Enable Link Time Optimization for smaller code size
19 | opt-level = 's' # Optimize for size
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chromium-style-qrcode-generator-with-wasm",
3 | "version": "n/a",
4 | "description": "A Chromium Style QR Code Generator using Rust and WebAssembly",
5 | "main": "n/a",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "build:wasm": "wasm-pack build --target web --out-dir src"
10 | },
11 | "author": "liuliang@webfrontend.dev",
12 | "engines": {
13 | "node": ">=22.14.0",
14 | "pnpm": ">=10.8.0"
15 | },
16 | "license": "MIT",
17 | "packageManager": "pnpm@10.8.0",
18 | "devDependencies": {
19 | "vite": "^6.2.6"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.github/workflows/preview-start.yml:
--------------------------------------------------------------------------------
1 | name: Preview Start
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened]
6 |
7 | permissions:
8 | contents: read
9 |
10 | jobs:
11 | preview-start:
12 | permissions:
13 | issues: write
14 | pull-requests: write
15 | name: preview start
16 | runs-on: ubuntu-latest
17 | steps:
18 | - name: Update Status Comment
19 | uses: actions-cool/maintain-one-comment@v3
20 | with:
21 | token: ${{ secrets.GITHUB_TOKEN }}
22 | body: |
23 | [Prepare Preview](https://preview-${{ github.event.number }}-chromium-style-qrcode-generator-with-wasm.surge.sh)
24 |
25 | body-include:
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025-present Liang Liu.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-to-github-pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy To Github Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | deploy:
13 | name: Build WebAssembly Project
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout repository
17 | uses: actions/checkout@v4
18 |
19 | - name: Setup Node.js
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: '22'
23 |
24 | - name: Install pnpm
25 | uses: pnpm/action-setup@v3
26 | with:
27 | version: '10.8.0'
28 | run_install: false
29 |
30 | - name: Setup Rust toolchain
31 | uses: actions-rs/toolchain@v1
32 | with:
33 | toolchain: stable
34 | profile: minimal
35 | override: true
36 | target: wasm32-unknown-unknown
37 |
38 | - name: Install wasm-pack
39 | uses: jetli/wasm-pack-action@v0.4.0
40 | with:
41 | version: 'latest'
42 |
43 | - name: Cache pnpm dependencies
44 | uses: actions/cache@v3
45 | with:
46 | path: |
47 | ~/.pnpm-store
48 | node_modules
49 | ~/.cargo/registry
50 | ~/.cargo/git
51 | target
52 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/Cargo.lock') }}
53 | restore-keys: |
54 | ${{ runner.os }}-pnpm-
55 |
56 | - name: Install dependencies
57 | run: pnpm install
58 |
59 | - name: Build WebAssembly
60 | run: pnpm build:wasm
61 |
62 | - name: Build Assets
63 | run: pnpm build
64 |
65 | - name: Deploy to GitHub Pages
66 | uses: JamesIves/github-pages-deploy-action@v4
67 | with:
68 | clean: true
69 | folder: dist
70 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Chromium Style QR Code Generator
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
QR code
18 |
19 |
20 |
21 |
22 |
Could not generate QR code. Please try again.
23 |
24 |
25 |
26 |
33 |
34 |
35 |
Input is too long. Please shorten the text to 2000 characters or less.
36 |
37 |
38 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/.github/workflows/preview-build.yml:
--------------------------------------------------------------------------------
1 | name: Preview Build
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened]
6 |
7 | # Cancel prev CI if new commit come
8 | concurrency:
9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
10 | cancel-in-progress: true
11 |
12 | permissions:
13 | contents: read
14 |
15 | jobs:
16 | build-site:
17 | name: build site
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Checkout repository
21 | uses: actions/checkout@v4
22 |
23 | - name: Setup Node.js
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: '22'
27 |
28 | - name: Install pnpm
29 | uses: pnpm/action-setup@v3
30 | with:
31 | version: '10.8.0'
32 | run_install: false
33 |
34 | - name: Setup Rust toolchain
35 | uses: actions-rs/toolchain@v1
36 | with:
37 | toolchain: stable
38 | profile: minimal
39 | override: true
40 | target: wasm32-unknown-unknown
41 |
42 | - name: Install wasm-pack
43 | uses: jetli/wasm-pack-action@v0.4.0
44 | with:
45 | version: 'latest'
46 |
47 | - name: Cache pnpm dependencies
48 | uses: actions/cache@v3
49 | with:
50 | path: |
51 | ~/.pnpm-store
52 | node_modules
53 | ~/.cargo/registry
54 | ~/.cargo/git
55 | target
56 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/Cargo.lock') }}
57 | restore-keys: |
58 | ${{ runner.os }}-pnpm-
59 |
60 | - name: Install dependencies
61 | run: pnpm install
62 |
63 | - name: Build WebAssembly
64 | run: pnpm build:wasm
65 |
66 | - name: Build Assets
67 | run: pnpm build
68 |
69 | - name: Upload Site Artifact
70 | uses: actions/upload-artifact@v4
71 | with:
72 | name: site
73 | path: ./dist/
74 | retention-days: 5
75 |
76 | # Upload PR id for next workflow use
77 | - name: Save Pull Request number
78 | if: ${{ always() }}
79 | run: echo ${{ github.event.number }} > ./pr-id.txt
80 |
81 | - name: Upload Pull Request number
82 | if: ${{ always() }}
83 | uses: actions/upload-artifact@v4
84 | with:
85 | name: pr
86 | path: ./pr-id.txt
87 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # Chromium 风格 QR 码生成器 (WebAssembly 版)
2 |
3 | 这是一个使用 Rust 和 WebAssembly 技术开发的高性能 QR 码生成器。该项目将 Rust 的高效能与 WebAssembly 的跨平台特性相结合,为 Web 应用提供快速、高效的 QR 码生成功能。
4 |
5 | ## 功能特点
6 |
7 | - ⚡️ **高性能**:利用 Rust 和 WebAssembly 实现高速 QR 码生成
8 | - 🔄 **实时预览**:输入变化时即时更新 QR 码
9 | - 📋 **智能复制功能**:可直接复制 QR 码图像到剪贴板(支持文本降级)
10 | - 💾 **完美下载**:下载清晰的 450×450 像素 PNG QR 码图像
11 | - 🦖 **Chromium 风格恐龙**:支持带白色背景的恐龙中心图像
12 | - 📱 **响应式设计**:适配不同设备屏幕尺寸
13 | - ✨ **高 DPI 支持**:在 Retina 和高 DPI 显示器上清晰渲染
14 | - 🎯 **Chromium 兼容性**:像素级完美实现,匹配 Chrome 的 QR 生成器
15 |
16 | ## 质量改进
17 |
18 | ### 技术规范
19 |
20 | - **模块样式**:圆形点(`ModuleStyle::kCircles`)匹配 Chrome
21 | - **定位器样式**:圆角(`LocatorStyle::kRounded`)匹配 Chrome
22 | - **中心图像**:使用 Chromium 源码精确像素数据的恐龙
23 | - **画布尺寸**:240×240 像素(相当于 `GetQRCodeImageSize()`)
24 | - **模块大小**:每模块 10 像素(`kModuleSizePixels`)
25 | - **恐龙缩放**:每恐龙像素 4 像素(`kDinoTileSizePixels`)
26 |
27 | ## 技术栈
28 |
29 | - **Rust**:核心 QR 码生成逻辑
30 | - **WebAssembly**:将 Rust 编译为可在浏览器中运行的格式
31 | - **JavaScript**:前端交互和渲染
32 | - **HTML5/CSS**:用户界面
33 |
34 | ## 安装与使用
35 |
36 | ### 前置条件
37 |
38 | - [Rust](https://www.rust-lang.org/tools/install)
39 | - [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/)
40 | - [Node.js](https://nodejs.org/) (推荐使用 pnpm 包管理器)
41 |
42 | ### 构建步骤
43 |
44 | 1. 克隆仓库
45 |
46 | ```bash
47 | git clone https://github.com/liuliangsir/chromium-style-qrcode-generator-with-wasm.git
48 | cd chromium-style-qrcode-generator-with-wasm
49 | ```
50 |
51 | 2. 构建 WebAssembly 模块
52 |
53 | ```bash
54 | pnpm build:wasm
55 | ```
56 |
57 | 3. 安装前端依赖
58 |
59 | ```bash
60 | pnpm install
61 | ```
62 |
63 | 4. 启动开发服务器
64 |
65 | ```bash
66 | pnpm dev
67 | ```
68 |
69 | 5. 在浏览器中打开项目 (默认为 )
70 |
71 | ### 使用方法
72 |
73 | 1. 在输入框中输入任意文本、URL 或数据(最多 2000 个字符)
74 | 2. QR 码将自动生成并实时更新显示
75 | 3. 使用"复制"按钮将 QR 码图像直接复制到剪贴板
76 | 4. 使用"下载"按钮保存清晰的 450×450 PNG QR 码图像
77 |
78 | ## 项目结构
79 |
80 | ```text
81 | ├── src/ # 源代码目录
82 | │ ├── lib.rs # Rust WebAssembly 模块核心代码
83 | │ ├── app.js # 前端 JavaScript 逻辑
84 | │ └── app.css # 样式表
85 | ├── public/ # 静态资源
86 | ├── index.html # 主 HTML 页面
87 | ├── Cargo.toml # Rust 项目配置
88 | └── package.json # JavaScript 项目配置
89 | ```
90 |
91 | ## 原理介绍
92 |
93 | 该 QR 码生成器使用 Rust 的`qr_code`库生成 QR 码数据,并通过 WebAssembly 将其暴露给 JavaScript。生成过程包括:
94 |
95 | 1. 接收用户输入的文本数据
96 | 2. 在 Rust 中生成对应的 QR 码二维矩阵
97 | 3. 添加适当的安静区 (quiet zone)
98 | 4. 将二维矩阵数据传回 JavaScript
99 | 5. 使用 Canvas API 渲染 QR 码图像
100 |
101 | ## 开发
102 |
103 | ### 修改 Rust 代码
104 |
105 | 如果您修改了`lib.rs`或其他 Rust 代码,需要重新构建 WebAssembly 模块:
106 |
107 | ```bash
108 | pnpm build:wasm
109 | ```
110 |
111 | ### 修改前端代码
112 |
113 | 前端代码修改后会自动重新加载。
114 |
115 | ## 许可证
116 |
117 | [MIT](LICENSE)
118 |
119 | ## 贡献
120 |
121 | 欢迎提交问题和 PR!请确保您的代码符合项目的编码风格。
122 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | debug/
4 | target/
5 |
6 | # These are backup files generated by rustfmt
7 | **/*.rs.bk
8 |
9 | # MSVC Windows builds of rustc generate these, which store debugging information
10 | *.pdb
11 |
12 | # RustRover
13 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
14 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
15 | # and can be added to the global gitignore or merged into this file. For a more nuclear
16 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
17 | #.idea/
18 | # Logs
19 | logs
20 | *.log
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | lerna-debug.log*
25 | .pnpm-debug.log*
26 |
27 | # Diagnostic reports (https://nodejs.org/api/report.html)
28 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
29 |
30 | # Runtime data
31 | pids
32 | *.pid
33 | *.seed
34 | *.pid.lock
35 |
36 | # Directory for instrumented libs generated by jscoverage/JSCover
37 | lib-cov
38 |
39 | # Coverage directory used by tools like istanbul
40 | coverage
41 | *.lcov
42 |
43 | # nyc test coverage
44 | .nyc_output
45 |
46 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
47 | .grunt
48 |
49 | # Bower dependency directory (https://bower.io/)
50 | bower_components
51 |
52 | # node-waf configuration
53 | .lock-wscript
54 |
55 | # Compiled binary addons (https://nodejs.org/api/addons.html)
56 | build/Release
57 |
58 | # Dependency directories
59 | node_modules/
60 | jspm_packages/
61 |
62 | # Snowpack dependency directory (https://snowpack.dev/)
63 | web_modules/
64 |
65 | # TypeScript cache
66 | *.tsbuildinfo
67 |
68 | # Optional npm cache directory
69 | .npm
70 |
71 | # Optional eslint cache
72 | .eslintcache
73 |
74 | # Optional stylelint cache
75 | .stylelintcache
76 |
77 | # Microbundle cache
78 | .rpt2_cache/
79 | .rts2_cache_cjs/
80 | .rts2_cache_es/
81 | .rts2_cache_umd/
82 |
83 | # Optional REPL history
84 | .node_repl_history
85 |
86 | # Output of 'npm pack'
87 | *.tgz
88 |
89 | # Yarn Integrity file
90 | .yarn-integrity
91 |
92 | # dotenv environment variable files
93 | .env
94 | .env.development.local
95 | .env.test.local
96 | .env.production.local
97 | .env.local
98 |
99 | # parcel-bundler cache (https://parceljs.org/)
100 | .cache
101 | .parcel-cache
102 |
103 | # Next.js build output
104 | .next
105 | out
106 |
107 | # Nuxt.js build / generate output
108 | .nuxt
109 | dist
110 |
111 | # Gatsby files
112 | .cache/
113 | # Comment in the public line in if your project uses Gatsby and not Next.js
114 | # https://nextjs.org/blog/next-9-1#public-directory-support
115 | # public
116 |
117 | # vuepress build output
118 | .vuepress/dist
119 |
120 | # vuepress v2.x temp and cache directory
121 | .temp
122 | .cache
123 |
124 | # vitepress build output
125 | **/.vitepress/dist
126 |
127 | # vitepress cache directory
128 | **/.vitepress/cache
129 |
130 | # Docusaurus cache and generated files
131 | .docusaurus
132 |
133 | # Serverless directories
134 | .serverless/
135 |
136 | # FuseBox cache
137 | .fusebox/
138 |
139 | # DynamoDB Local files
140 | .dynamodb/
141 |
142 | # TernJS port file
143 | .tern-port
144 |
145 | # Stores VSCode versions used for testing VSCode extensions
146 | .vscode-test
147 |
148 | # yarn v2
149 | .yarn/cache
150 | .yarn/unplugged
151 | .yarn/build-state.yml
152 | .yarn/install-state.gz
153 | .pnp.*
154 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chromium Style QR Code Generator (WebAssembly Version)
2 |
3 | This is a high-performance QR code generator developed with Rust and WebAssembly technology. The project combines the efficiency of Rust with the cross-platform capabilities of WebAssembly to provide fast and efficient QR code generation for web applications.
4 |
5 | ## Features
6 |
7 | - ⚡️ **High Performance**: Utilizing Rust and WebAssembly for high-speed QR code generation
8 | - 🔄 **Real-time Preview**: Instantly updates QR codes as input changes
9 | - 📋 **Smart Copy Function**: Copies QR code images directly to clipboard (with text fallback)
10 | - 💾 **Perfect Downloads**: Downloads QR codes as crisp 450×450 pixel PNG images
11 | - 🦖 **Chromium-Style Dino**: Supports dinosaur center images with white backgrounds
12 | - 📱 **Responsive Design**: Adapts to different device screen sizes
13 | - ✨ **High DPI Support**: Crystal clear rendering on retina and high-DPI displays
14 | - 🎯 **Chromium Compliance**: Pixel-perfect implementation matching Chrome's QR generator
15 |
16 | ## Quality Improvements
17 |
18 | ### Technical Specifications
19 |
20 | - **Module Style**: Circular dots (`ModuleStyle::kCircles`) matching Chrome
21 | - **Locator Style**: Rounded corners (`LocatorStyle::kRounded`) matching Chrome
22 | - **Center Image**: Dino with exact pixel data from Chromium source code
23 | - **Canvas Size**: 240×240 pixels (`GetQRCodeImageSize()` equivalent)
24 | - **Module Size**: 10 pixels per module (`kModuleSizePixels`)
25 | - **Dino Scale**: 4 pixels per dino pixel (`kDinoTileSizePixels`)
26 |
27 | ## Technology Stack
28 |
29 | - **Rust**: Core QR code generation logic
30 | - **WebAssembly**: Compiles Rust into a format that can run in browsers
31 | - **JavaScript**: Front-end interaction and rendering
32 | - **HTML5/CSS**: User interface
33 |
34 | ## Installation and Usage
35 |
36 | ### Prerequisites
37 |
38 | - [Rust](https://www.rust-lang.org/tools/install)
39 | - [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/)
40 | - [Node.js](https://nodejs.org/) (pnpm package manager recommended)
41 |
42 | ### Build Steps
43 |
44 | 1. Clone the repository
45 |
46 | ```bash
47 | git clone https://github.com/liuliangsir/chromium-style-qrcode-generator-with-wasm.git
48 | cd chromium-style-qrcode-generator-with-wasm
49 | ```
50 |
51 | 2. Build the WebAssembly module
52 |
53 | ```bash
54 | pnpm build:wasm
55 | ```
56 |
57 | 3. Install frontend dependencies
58 |
59 | ```bash
60 | pnpm install
61 | ```
62 |
63 | 4. Start the development server
64 |
65 | ```bash
66 | pnpm dev
67 | ```
68 |
69 | 5. Open the project in your browser (default: )
70 |
71 | ### How to Use
72 |
73 | 1. Enter any text, URL, or data in the input field (up to 2000 characters)
74 | 2. The QR code will be automatically generated and displayed with real-time updates
75 | 3. Use the "Copy" button to copy the QR code image directly to clipboard
76 | 4. Use the "Download" button to save the QR code as a crisp 450×450 PNG image
77 |
78 | ## Project Structure
79 |
80 | ```text
81 | ├── src/ # Source code directory
82 | │ ├── lib.rs # Rust WebAssembly module core code
83 | │ ├── app.js # Frontend JavaScript logic
84 | │ └── app.css # Stylesheet
85 | ├── public/ # Static resources
86 | ├── index.html # Main HTML page
87 | ├── Cargo.toml # Rust project configuration
88 | └── package.json # JavaScript project configuration
89 | ```
90 |
91 | ## How It Works
92 |
93 | This QR code generator uses Rust's `qr_code` library to generate QR code data and exposes it to JavaScript via WebAssembly. The generation process includes:
94 |
95 | 1. Receiving text data input from users
96 | 2. Generating the corresponding QR code two-dimensional matrix in Rust
97 | 3. Adding appropriate quiet zones
98 | 4. Returning the two-dimensional matrix data to JavaScript
99 | 5. Rendering the QR code image using the Canvas API
100 |
101 | ## Development
102 |
103 | ### Modifying Rust Code
104 |
105 | If you modify `lib.rs` or other Rust code, you need to rebuild the WebAssembly module:
106 |
107 | ```bash
108 | pnpm build:wasm
109 | ```
110 |
111 | ### Modifying Frontend Code
112 |
113 | Frontend code modifications will automatically reload.
114 |
115 | ## License
116 |
117 | [MIT](LICENSE)
118 |
119 | ## Contribution
120 |
121 | Issues and PRs are welcome! Please ensure your code adheres to the project's coding style.
122 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "bumpalo"
7 | version = "3.17.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
10 |
11 | [[package]]
12 | name = "cfg-if"
13 | version = "1.0.0"
14 | source = "registry+https://github.com/rust-lang/crates.io-index"
15 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
16 |
17 | [[package]]
18 | name = "chromium-style-qrcode-generator-with-wasm"
19 | version = "1.0.0"
20 | dependencies = [
21 | "console_error_panic_hook",
22 | "qr_code",
23 | "wasm-bindgen",
24 | ]
25 |
26 | [[package]]
27 | name = "console_error_panic_hook"
28 | version = "0.1.7"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
31 | dependencies = [
32 | "cfg-if",
33 | "wasm-bindgen",
34 | ]
35 |
36 | [[package]]
37 | name = "log"
38 | version = "0.4.27"
39 | source = "registry+https://github.com/rust-lang/crates.io-index"
40 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
41 |
42 | [[package]]
43 | name = "once_cell"
44 | version = "1.21.3"
45 | source = "registry+https://github.com/rust-lang/crates.io-index"
46 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
47 |
48 | [[package]]
49 | name = "proc-macro2"
50 | version = "1.0.94"
51 | source = "registry+https://github.com/rust-lang/crates.io-index"
52 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
53 | dependencies = [
54 | "unicode-ident",
55 | ]
56 |
57 | [[package]]
58 | name = "qr_code"
59 | version = "2.0.0"
60 | source = "registry+https://github.com/rust-lang/crates.io-index"
61 | checksum = "43d2564aae5faaf3acb512b35b8bcb9a298d9d8c72d181c598691d800ee78a00"
62 |
63 | [[package]]
64 | name = "quote"
65 | version = "1.0.40"
66 | source = "registry+https://github.com/rust-lang/crates.io-index"
67 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
68 | dependencies = [
69 | "proc-macro2",
70 | ]
71 |
72 | [[package]]
73 | name = "rustversion"
74 | version = "1.0.20"
75 | source = "registry+https://github.com/rust-lang/crates.io-index"
76 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
77 |
78 | [[package]]
79 | name = "syn"
80 | version = "2.0.100"
81 | source = "registry+https://github.com/rust-lang/crates.io-index"
82 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
83 | dependencies = [
84 | "proc-macro2",
85 | "quote",
86 | "unicode-ident",
87 | ]
88 |
89 | [[package]]
90 | name = "unicode-ident"
91 | version = "1.0.18"
92 | source = "registry+https://github.com/rust-lang/crates.io-index"
93 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
94 |
95 | [[package]]
96 | name = "wasm-bindgen"
97 | version = "0.2.100"
98 | source = "registry+https://github.com/rust-lang/crates.io-index"
99 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
100 | dependencies = [
101 | "cfg-if",
102 | "once_cell",
103 | "rustversion",
104 | "wasm-bindgen-macro",
105 | ]
106 |
107 | [[package]]
108 | name = "wasm-bindgen-backend"
109 | version = "0.2.100"
110 | source = "registry+https://github.com/rust-lang/crates.io-index"
111 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
112 | dependencies = [
113 | "bumpalo",
114 | "log",
115 | "proc-macro2",
116 | "quote",
117 | "syn",
118 | "wasm-bindgen-shared",
119 | ]
120 |
121 | [[package]]
122 | name = "wasm-bindgen-macro"
123 | version = "0.2.100"
124 | source = "registry+https://github.com/rust-lang/crates.io-index"
125 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
126 | dependencies = [
127 | "quote",
128 | "wasm-bindgen-macro-support",
129 | ]
130 |
131 | [[package]]
132 | name = "wasm-bindgen-macro-support"
133 | version = "0.2.100"
134 | source = "registry+https://github.com/rust-lang/crates.io-index"
135 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
136 | dependencies = [
137 | "proc-macro2",
138 | "quote",
139 | "syn",
140 | "wasm-bindgen-backend",
141 | "wasm-bindgen-shared",
142 | ]
143 |
144 | [[package]]
145 | name = "wasm-bindgen-shared"
146 | version = "0.2.100"
147 | source = "registry+https://github.com/rust-lang/crates.io-index"
148 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
149 | dependencies = [
150 | "unicode-ident",
151 | ]
152 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | use wasm_bindgen::prelude::*;
2 | use qr_code::{QrCode, EcLevel};
3 | use qr_code::types::{QrError, Version, Color};
4 |
5 | // Enums matching Chromium's implementation exactly
6 | #[wasm_bindgen]
7 | #[derive(Clone, Copy, Debug, PartialEq)]
8 | pub enum ModuleStyle {
9 | Squares = 0,
10 | Circles = 1,
11 | }
12 |
13 | #[wasm_bindgen]
14 | #[derive(Clone, Copy, Debug, PartialEq)]
15 | pub enum LocatorStyle {
16 | Square = 0,
17 | Rounded = 1,
18 | }
19 |
20 | #[wasm_bindgen]
21 | #[derive(Clone, Copy, Debug, PartialEq)]
22 | pub enum CenterImage {
23 | NoCenterImage = 0,
24 | Dino = 1,
25 | // Passkey and ProductLogo would be here for non-iOS builds
26 | }
27 |
28 | #[wasm_bindgen]
29 | #[derive(Clone, Copy, Debug, PartialEq)]
30 | pub enum QuietZone {
31 | Included = 0,
32 | WillBeAddedByClient = 1,
33 | }
34 |
35 | // Structure to return data to JS - exactly matching Chromium's GeneratedCode
36 | #[wasm_bindgen]
37 | pub struct QrCodeResult {
38 | #[wasm_bindgen(getter_with_clone)]
39 | pub data: Vec, // Pixel data: least significant bit set if module should be "black"
40 | pub size: usize, // Width and height of the generated data, in modules
41 | pub original_size: usize, // Size without quiet zone for compatibility
42 | }
43 |
44 | #[wasm_bindgen]
45 | pub fn generate_qr_code_wasm(input_data: &str) -> Result {
46 | generate_qr_code_with_options(
47 | input_data,
48 | ModuleStyle::Circles,
49 | LocatorStyle::Rounded,
50 | CenterImage::Dino,
51 | QuietZone::WillBeAddedByClient, // Match Chromium Android implementation
52 | )
53 | }
54 |
55 | #[wasm_bindgen]
56 | pub fn generate_qr_code_with_options(
57 | input_data: &str,
58 | _module_style: ModuleStyle, // Module style is handled in frontend rendering
59 | _locator_style: LocatorStyle, // Locator style is handled in frontend rendering
60 | _center_image: CenterImage, // Center image is handled in frontend rendering
61 | quiet_zone: QuietZone,
62 | ) -> Result {
63 | // Initialize panic hook for better debugging
64 | #[cfg(feature = "console_error_panic_hook")]
65 | console_error_panic_hook::set_once();
66 |
67 | // The QR version (i.e. size) must be >= 5 because otherwise the dino
68 | // painted over the middle covers too much of the code to be decodable.
69 | // This matches Chromium's kMinimumQRVersion = 5
70 |
71 | // Generate QR code - try with minimum version 5 first
72 | let code = match QrCode::with_version(input_data.as_bytes(), Version::Normal(5), EcLevel::M) {
73 | Ok(code) => code,
74 | Err(_) => {
75 | // If version 5 doesn't work, let the library choose the version
76 | QrCode::new(input_data.as_bytes())
77 | .map_err(|e: QrError| {
78 | match e {
79 | QrError::DataTooLong => JsValue::from_str("Input string was too long"),
80 | _ => JsValue::from_str(&format!("QR Code generation error: {:?}", e)),
81 | }
82 | })?
83 | }
84 | };
85 |
86 | let qr_size = code.width() as usize;
87 |
88 | // Calculate final size based on quiet zone setting (matching Chromium)
89 | let margin_modules = match quiet_zone {
90 | QuietZone::Included => 4, // 4 modules quiet zone
91 | QuietZone::WillBeAddedByClient => 0,
92 | };
93 | let final_size = qr_size + 2 * margin_modules;
94 |
95 | // Initialize pixel data - following Chromium's approach
96 | let mut pixel_data = vec![0u8; final_size * final_size];
97 |
98 | // Get the module data - iterate over QR code modules
99 | for y in 0..qr_size {
100 | for x in 0..qr_size {
101 | let module_color = code[(x, y)]; // Use QrCode's indexing API
102 | let is_dark = module_color == Color::Dark;
103 |
104 | if is_dark {
105 | let final_x = x + margin_modules;
106 | let final_y = y + margin_modules;
107 | pixel_data[final_y * final_size + final_x] = 1; // Set to black (1)
108 | }
109 | }
110 | }
111 |
112 | // For each byte in data, keep only the least significant bit (exactly like Chromium)
113 | // The Chromium comment: "The least significant bit of each byte is set if that tile/module should be 'black'."
114 | for byte in pixel_data.iter_mut() {
115 | *byte &= 1;
116 | }
117 |
118 | Ok(QrCodeResult {
119 | data: pixel_data,
120 | size: final_size, // Size with quiet zone
121 | original_size: qr_size, // Original QR code size without quiet zone
122 | })
123 | }
124 |
--------------------------------------------------------------------------------
/.github/workflows/preview-deploy.yml:
--------------------------------------------------------------------------------
1 | name: Preview Deploy
2 |
3 | on:
4 | workflow_run:
5 | workflows: [Preview Build]
6 | types:
7 | - completed
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | upstream-workflow-summary:
14 | name: upstream workflow summary
15 | runs-on: ubuntu-latest
16 | if: github.event.workflow_run.event == 'pull_request'
17 | outputs:
18 | jobs: ${{ steps.prep-summary.outputs.result }}
19 | build-success: ${{ steps.prep-summary.outputs.build-success }}
20 | build-failure: ${{ steps.prep-summary.outputs.build-failure }}
21 | steps:
22 | - name: Summary Jobs Status
23 | uses: actions/github-script@v7
24 | id: prep-summary
25 | with:
26 | script: |
27 | const response = await github.rest.actions.listJobsForWorkflowRun({
28 | owner: context.repo.owner,
29 | repo: context.repo.repo,
30 | run_id: ${{ github.event.workflow_run.id }},
31 | });
32 |
33 | // { [name]: [conclusion] }, e.g. { 'build site': 'success' }
34 | const jobs = (response.data?.jobs ?? []).reduce((acc, job) => {
35 | if(job?.status === 'completed' && 'name' in job && 'conclusion' in job) {
36 | acc[job.name] = job.conclusion;
37 | }
38 | return acc;
39 | }, {});
40 |
41 | const total = Object.keys(jobs).length;
42 | if(total === 0) core.setFailed('no jobs found');
43 |
44 | // the name here must be the same as `jobs.xxx.{name}` in preview-build.yml
45 | // set output
46 | core.setOutput('build-success', jobs['build site'] === 'success');
47 | core.setOutput('build-failure', jobs['build site'] === 'failure');
48 | return jobs;
49 |
50 | deploy-preview:
51 | name: deploy preview
52 | permissions:
53 | actions: read
54 | issues: write
55 | pull-requests: write
56 | runs-on: ubuntu-latest
57 | needs: upstream-workflow-summary
58 | if: github.event.workflow_run.event == 'pull_request'
59 | steps:
60 | # We need get PR id first
61 | - name: Download Pull Request Artifact
62 | uses: dawidd6/action-download-artifact@v6
63 | with:
64 | workflow: ${{ github.event.workflow_run.workflow_id }}
65 | run_id: ${{ github.event.workflow_run.id }}
66 | name: pr
67 |
68 | # Save PR id to output
69 | - name: Save Pull Request Id
70 | id: pr
71 | run: |
72 | pr_id=$(> $GITHUB_OUTPUT
78 |
79 | # Download site artifact
80 | - name: Download Site Artifact
81 | if: ${{ fromJSON(needs.upstream-workflow-summary.outputs.build-success) }}
82 | uses: dawidd6/action-download-artifact@v6
83 | with:
84 | workflow: ${{ github.event.workflow_run.workflow_id }}
85 | run_id: ${{ github.event.workflow_run.id }}
86 | name: site
87 |
88 | - name: Upload Surge Service
89 | id: deploy
90 | continue-on-error: true
91 | env:
92 | PR_ID: ${{ steps.pr.outputs.id }}
93 | run: |
94 | export DEPLOY_DOMAIN=https://preview-${PR_ID}-chromium-style-qrcode-generator-with-wasm.surge.sh
95 | npx surge --project ./ --domain $DEPLOY_DOMAIN --token ${{ secrets.SURGE_TOKEN }}
96 |
97 | - name: Success Comment
98 | uses: actions-cool/maintain-one-comment@v3
99 | if: ${{ steps.deploy.outcome == 'success' }}
100 | with:
101 | token: ${{ secrets.GITHUB_TOKEN }}
102 | body: |
103 | [Preview Is Ready](https://preview-${{ steps.pr.outputs.id }}-chromium-style-qrcode-generator-with-wasm.surge.sh)
104 |
105 | body-include:
106 | number: ${{ steps.pr.outputs.id }}
107 |
108 | - name: Failed Comment
109 | if: ${{ fromJSON(needs.upstream-workflow-summary.outputs.build-failure) || steps.deploy.outcome == 'failure' || failure() }}
110 | uses: actions-cool/maintain-one-comment@v3
111 | with:
112 | token: ${{ secrets.GITHUB_TOKEN }}
113 | body: |
114 | [Preview Failed](https://preview-${{ steps.pr.outputs.id }}-chromium-style-qrcode-generator-with-wasm.surge.sh)
115 |
116 | body-include:
117 | number: ${{ steps.pr.outputs.id }}
118 |
119 | - name: Check Surge Deploy Result And Exit If Failed
120 | run: |
121 | if [ "${{ steps.deploy.outcome }}" != "success" ]; then
122 | echo "Surge Deploy failed."
123 | exit 1
124 | fi
125 |
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: -webkit-system-font, "Segoe UI", Roboto, sans-serif; /* Match Chromium's font stack */
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 | min-height: 100vh;
7 | background-color: #f0f0f0;
8 | margin: 0;
9 | color: #202124; /* Match Chromium's text color */
10 | }
11 |
12 | .bubble-container {
13 | background-color: white;
14 | padding: 20px;
15 | border-radius: 12px; /* Match Chromium's kHigh emphasis border radius */
16 | box-shadow: 0 1px 3px 0 rgba(60, 64, 67, 0.3), 0 4px 8px 3px rgba(60, 64, 67, 0.15); /* Match Chromium's elevation shadow */
17 | display: flex;
18 | flex-direction: column;
19 | align-items: center;
20 | min-width: 280px; /* Ensure consistent width similar to Chromium */
21 | width: auto;
22 | max-width: 400px;
23 | }
24 |
25 | h2 {
26 | margin-top: 0;
27 | margin-bottom: 16px; /* Match Chromium's DISTANCE_UNRELATED_CONTROL_VERTICAL_LARGE */
28 | font-size: 15px; /* Match Chromium's dialog title font size */
29 | color: #202124; /* Match Chromium's primary text color */
30 | text-align: center;
31 | font-weight: 500; /* Match Chromium's medium weight */
32 | line-height: 20px;
33 | }
34 |
35 | .qr-code-area {
36 | position: relative;
37 | display: flex;
38 | justify-content: center;
39 | align-items: center;
40 | margin-bottom: 16px; /* Match Chromium's spacing */
41 | border: 2px solid #dadce0; /* Match Chromium's kColorQrCodeBorder */
42 | border-radius: 12px; /* Match high emphasis border radius */
43 | background-color: #ffffff; /* Match Chromium's kColorQrCodeBackground */
44 | overflow: hidden; /* Ensure canvas stays within border */
45 | width: 252px; /* 240px QR code + 2*2px border + 2*4px padding */
46 | height: 252px;
47 | padding: 4px; /* Additional padding inside border */
48 | }
49 |
50 | #qrCanvas {
51 | display: block; /* Remove extra space below canvas */
52 | /* Enable crisp rendering on high DPI displays */
53 | image-rendering: -webkit-crisp-edges;
54 | image-rendering: -webkit-optimize-contrast;
55 | image-rendering: -moz-crisp-edges;
56 | image-rendering: crisp-edges;
57 | image-rendering: pixelated;
58 | }
59 |
60 | #urlInput {
61 | width: 100%;
62 | min-width: 240px; /* Match QR code width */
63 | padding: 8px 12px;
64 | margin-bottom: 8px; /* Space before potential bottom error */
65 | border: 1px solid #dadce0; /* Match Chromium's border color */
66 | border-radius: 4px;
67 | box-sizing: border-box; /* Include padding and border in width */
68 | font-size: 13px; /* Match Chromium's input font size */
69 | font-family: inherit;
70 | color: #202124; /* Match Chromium's text color */
71 | background-color: #ffffff;
72 | line-height: 20px;
73 | transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
74 | }
75 |
76 | #urlInput:focus {
77 | outline: none;
78 | border-color: #1a73e8; /* Match Chromium's focus color */
79 | box-shadow: 0 0 0 1px #1a73e8; /* Focus ring like Chromium */
80 | }
81 |
82 | .error-label {
83 | color: #d93025; /* Match Chromium's error color */
84 | font-size: 12px; /* Match Chromium's secondary text size */
85 | text-align: center;
86 | width: 100%;
87 | line-height: 16px;
88 | font-weight: 400;
89 | }
90 |
91 | .hidden {
92 | display: none;
93 | }
94 |
95 | .center-error {
96 | position: absolute;
97 | top: 0;
98 | left: 0;
99 | width: 100%;
100 | height: 100%;
101 | display: flex;
102 | justify-content: center;
103 | align-items: center;
104 | background-color: rgba(255, 255, 255, 0.9); /* Semi-transparent background */
105 | padding: 10px;
106 | box-sizing: border-box;
107 | }
108 |
109 | .center-error.hidden {
110 | display: none;
111 | }
112 |
113 | .bottom-error {
114 | margin-bottom: 10px; /* Space between error and buttons */
115 | min-height: 1.2em; /* Reserve space even when hidden */
116 | }
117 |
118 | .button-container {
119 | display: flex;
120 | align-items: center;
121 | gap: 8px; /* Match Chromium's DISTANCE_RELATED_BUTTON_HORIZONTAL */
122 | width: 100%;
123 | margin-top: 8px;
124 | }
125 |
126 | .tooltip {
127 | display: inline-flex;
128 | align-items: center;
129 | justify-content: center;
130 | width: 16px;
131 | height: 16px;
132 | border-radius: 50%;
133 | background-color: #dadce0; /* Match Chromium's neutral color */
134 | color: #5f6368; /* Match Chromium's secondary text */
135 | font-size: 11px;
136 | font-weight: bold;
137 | cursor: help;
138 | user-select: none;
139 | margin-right: 2px; /* Extra spacing like Chromium's kPaddingTooltipDownloadButtonPx */
140 | }
141 |
142 | .tooltip:hover {
143 | background-color: #c8c9ca;
144 | }
145 |
146 | .spacer {
147 | flex: 1; /* Takes up remaining space to push buttons to the right */
148 | }
149 |
150 | button {
151 | padding: 8px 16px;
152 | border: 1px solid #dadce0; /* Match Chromium's button border color */
153 | border-radius: 4px;
154 | background-color: #fff; /* White background like Chromium */
155 | cursor: pointer;
156 | margin-left: 8px; /* Similar to DISTANCE_RELATED_BUTTON_HORIZONTAL */
157 | font-size: 14px;
158 | color: #1a73e8; /* Blue text like Chromium buttons */
159 | min-width: 64px; /* Ensure buttons have minimum width */
160 | }
161 |
162 | button:disabled {
163 | cursor: not-allowed;
164 | opacity: 0.38; /* Match Chromium's disabled opacity */
165 | color: #5f6368; /* Gray text for disabled state */
166 | }
167 |
168 | button:hover:not(:disabled) {
169 | background-color: #f8f9fa; /* Light gray hover like Chromium */
170 | border-color: #dadce0;
171 | }
172 |
173 | button:active:not(:disabled) {
174 | background-color: #e8f0fe; /* Light blue active state */
175 | }
176 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | devDependencies:
11 | vite:
12 | specifier: ^6.2.6
13 | version: 6.2.6
14 |
15 | packages:
16 |
17 | '@esbuild/aix-ppc64@0.25.2':
18 | resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==}
19 | engines: {node: '>=18'}
20 | cpu: [ppc64]
21 | os: [aix]
22 |
23 | '@esbuild/android-arm64@0.25.2':
24 | resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==}
25 | engines: {node: '>=18'}
26 | cpu: [arm64]
27 | os: [android]
28 |
29 | '@esbuild/android-arm@0.25.2':
30 | resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==}
31 | engines: {node: '>=18'}
32 | cpu: [arm]
33 | os: [android]
34 |
35 | '@esbuild/android-x64@0.25.2':
36 | resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==}
37 | engines: {node: '>=18'}
38 | cpu: [x64]
39 | os: [android]
40 |
41 | '@esbuild/darwin-arm64@0.25.2':
42 | resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==}
43 | engines: {node: '>=18'}
44 | cpu: [arm64]
45 | os: [darwin]
46 |
47 | '@esbuild/darwin-x64@0.25.2':
48 | resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==}
49 | engines: {node: '>=18'}
50 | cpu: [x64]
51 | os: [darwin]
52 |
53 | '@esbuild/freebsd-arm64@0.25.2':
54 | resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==}
55 | engines: {node: '>=18'}
56 | cpu: [arm64]
57 | os: [freebsd]
58 |
59 | '@esbuild/freebsd-x64@0.25.2':
60 | resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==}
61 | engines: {node: '>=18'}
62 | cpu: [x64]
63 | os: [freebsd]
64 |
65 | '@esbuild/linux-arm64@0.25.2':
66 | resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==}
67 | engines: {node: '>=18'}
68 | cpu: [arm64]
69 | os: [linux]
70 |
71 | '@esbuild/linux-arm@0.25.2':
72 | resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==}
73 | engines: {node: '>=18'}
74 | cpu: [arm]
75 | os: [linux]
76 |
77 | '@esbuild/linux-ia32@0.25.2':
78 | resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==}
79 | engines: {node: '>=18'}
80 | cpu: [ia32]
81 | os: [linux]
82 |
83 | '@esbuild/linux-loong64@0.25.2':
84 | resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==}
85 | engines: {node: '>=18'}
86 | cpu: [loong64]
87 | os: [linux]
88 |
89 | '@esbuild/linux-mips64el@0.25.2':
90 | resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==}
91 | engines: {node: '>=18'}
92 | cpu: [mips64el]
93 | os: [linux]
94 |
95 | '@esbuild/linux-ppc64@0.25.2':
96 | resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==}
97 | engines: {node: '>=18'}
98 | cpu: [ppc64]
99 | os: [linux]
100 |
101 | '@esbuild/linux-riscv64@0.25.2':
102 | resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==}
103 | engines: {node: '>=18'}
104 | cpu: [riscv64]
105 | os: [linux]
106 |
107 | '@esbuild/linux-s390x@0.25.2':
108 | resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==}
109 | engines: {node: '>=18'}
110 | cpu: [s390x]
111 | os: [linux]
112 |
113 | '@esbuild/linux-x64@0.25.2':
114 | resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==}
115 | engines: {node: '>=18'}
116 | cpu: [x64]
117 | os: [linux]
118 |
119 | '@esbuild/netbsd-arm64@0.25.2':
120 | resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==}
121 | engines: {node: '>=18'}
122 | cpu: [arm64]
123 | os: [netbsd]
124 |
125 | '@esbuild/netbsd-x64@0.25.2':
126 | resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==}
127 | engines: {node: '>=18'}
128 | cpu: [x64]
129 | os: [netbsd]
130 |
131 | '@esbuild/openbsd-arm64@0.25.2':
132 | resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==}
133 | engines: {node: '>=18'}
134 | cpu: [arm64]
135 | os: [openbsd]
136 |
137 | '@esbuild/openbsd-x64@0.25.2':
138 | resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==}
139 | engines: {node: '>=18'}
140 | cpu: [x64]
141 | os: [openbsd]
142 |
143 | '@esbuild/sunos-x64@0.25.2':
144 | resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==}
145 | engines: {node: '>=18'}
146 | cpu: [x64]
147 | os: [sunos]
148 |
149 | '@esbuild/win32-arm64@0.25.2':
150 | resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==}
151 | engines: {node: '>=18'}
152 | cpu: [arm64]
153 | os: [win32]
154 |
155 | '@esbuild/win32-ia32@0.25.2':
156 | resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==}
157 | engines: {node: '>=18'}
158 | cpu: [ia32]
159 | os: [win32]
160 |
161 | '@esbuild/win32-x64@0.25.2':
162 | resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==}
163 | engines: {node: '>=18'}
164 | cpu: [x64]
165 | os: [win32]
166 |
167 | '@rollup/rollup-android-arm-eabi@4.40.0':
168 | resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==}
169 | cpu: [arm]
170 | os: [android]
171 |
172 | '@rollup/rollup-android-arm64@4.40.0':
173 | resolution: {integrity: sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==}
174 | cpu: [arm64]
175 | os: [android]
176 |
177 | '@rollup/rollup-darwin-arm64@4.40.0':
178 | resolution: {integrity: sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==}
179 | cpu: [arm64]
180 | os: [darwin]
181 |
182 | '@rollup/rollup-darwin-x64@4.40.0':
183 | resolution: {integrity: sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==}
184 | cpu: [x64]
185 | os: [darwin]
186 |
187 | '@rollup/rollup-freebsd-arm64@4.40.0':
188 | resolution: {integrity: sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==}
189 | cpu: [arm64]
190 | os: [freebsd]
191 |
192 | '@rollup/rollup-freebsd-x64@4.40.0':
193 | resolution: {integrity: sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==}
194 | cpu: [x64]
195 | os: [freebsd]
196 |
197 | '@rollup/rollup-linux-arm-gnueabihf@4.40.0':
198 | resolution: {integrity: sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==}
199 | cpu: [arm]
200 | os: [linux]
201 | libc: [glibc]
202 |
203 | '@rollup/rollup-linux-arm-musleabihf@4.40.0':
204 | resolution: {integrity: sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==}
205 | cpu: [arm]
206 | os: [linux]
207 | libc: [musl]
208 |
209 | '@rollup/rollup-linux-arm64-gnu@4.40.0':
210 | resolution: {integrity: sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==}
211 | cpu: [arm64]
212 | os: [linux]
213 | libc: [glibc]
214 |
215 | '@rollup/rollup-linux-arm64-musl@4.40.0':
216 | resolution: {integrity: sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==}
217 | cpu: [arm64]
218 | os: [linux]
219 | libc: [musl]
220 |
221 | '@rollup/rollup-linux-loongarch64-gnu@4.40.0':
222 | resolution: {integrity: sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==}
223 | cpu: [loong64]
224 | os: [linux]
225 | libc: [glibc]
226 |
227 | '@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
228 | resolution: {integrity: sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==}
229 | cpu: [ppc64]
230 | os: [linux]
231 | libc: [glibc]
232 |
233 | '@rollup/rollup-linux-riscv64-gnu@4.40.0':
234 | resolution: {integrity: sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==}
235 | cpu: [riscv64]
236 | os: [linux]
237 | libc: [glibc]
238 |
239 | '@rollup/rollup-linux-riscv64-musl@4.40.0':
240 | resolution: {integrity: sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==}
241 | cpu: [riscv64]
242 | os: [linux]
243 | libc: [musl]
244 |
245 | '@rollup/rollup-linux-s390x-gnu@4.40.0':
246 | resolution: {integrity: sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==}
247 | cpu: [s390x]
248 | os: [linux]
249 | libc: [glibc]
250 |
251 | '@rollup/rollup-linux-x64-gnu@4.40.0':
252 | resolution: {integrity: sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==}
253 | cpu: [x64]
254 | os: [linux]
255 | libc: [glibc]
256 |
257 | '@rollup/rollup-linux-x64-musl@4.40.0':
258 | resolution: {integrity: sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==}
259 | cpu: [x64]
260 | os: [linux]
261 | libc: [musl]
262 |
263 | '@rollup/rollup-win32-arm64-msvc@4.40.0':
264 | resolution: {integrity: sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==}
265 | cpu: [arm64]
266 | os: [win32]
267 |
268 | '@rollup/rollup-win32-ia32-msvc@4.40.0':
269 | resolution: {integrity: sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==}
270 | cpu: [ia32]
271 | os: [win32]
272 |
273 | '@rollup/rollup-win32-x64-msvc@4.40.0':
274 | resolution: {integrity: sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==}
275 | cpu: [x64]
276 | os: [win32]
277 |
278 | '@types/estree@1.0.7':
279 | resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==}
280 |
281 | esbuild@0.25.2:
282 | resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==}
283 | engines: {node: '>=18'}
284 | hasBin: true
285 |
286 | fsevents@2.3.3:
287 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
288 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
289 | os: [darwin]
290 |
291 | nanoid@3.3.11:
292 | resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
293 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
294 | hasBin: true
295 |
296 | picocolors@1.1.1:
297 | resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
298 |
299 | postcss@8.5.3:
300 | resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==}
301 | engines: {node: ^10 || ^12 || >=14}
302 |
303 | rollup@4.40.0:
304 | resolution: {integrity: sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==}
305 | engines: {node: '>=18.0.0', npm: '>=8.0.0'}
306 | hasBin: true
307 |
308 | source-map-js@1.2.1:
309 | resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
310 | engines: {node: '>=0.10.0'}
311 |
312 | vite@6.2.6:
313 | resolution: {integrity: sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==}
314 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
315 | hasBin: true
316 | peerDependencies:
317 | '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
318 | jiti: '>=1.21.0'
319 | less: '*'
320 | lightningcss: ^1.21.0
321 | sass: '*'
322 | sass-embedded: '*'
323 | stylus: '*'
324 | sugarss: '*'
325 | terser: ^5.16.0
326 | tsx: ^4.8.1
327 | yaml: ^2.4.2
328 | peerDependenciesMeta:
329 | '@types/node':
330 | optional: true
331 | jiti:
332 | optional: true
333 | less:
334 | optional: true
335 | lightningcss:
336 | optional: true
337 | sass:
338 | optional: true
339 | sass-embedded:
340 | optional: true
341 | stylus:
342 | optional: true
343 | sugarss:
344 | optional: true
345 | terser:
346 | optional: true
347 | tsx:
348 | optional: true
349 | yaml:
350 | optional: true
351 |
352 | snapshots:
353 |
354 | '@esbuild/aix-ppc64@0.25.2':
355 | optional: true
356 |
357 | '@esbuild/android-arm64@0.25.2':
358 | optional: true
359 |
360 | '@esbuild/android-arm@0.25.2':
361 | optional: true
362 |
363 | '@esbuild/android-x64@0.25.2':
364 | optional: true
365 |
366 | '@esbuild/darwin-arm64@0.25.2':
367 | optional: true
368 |
369 | '@esbuild/darwin-x64@0.25.2':
370 | optional: true
371 |
372 | '@esbuild/freebsd-arm64@0.25.2':
373 | optional: true
374 |
375 | '@esbuild/freebsd-x64@0.25.2':
376 | optional: true
377 |
378 | '@esbuild/linux-arm64@0.25.2':
379 | optional: true
380 |
381 | '@esbuild/linux-arm@0.25.2':
382 | optional: true
383 |
384 | '@esbuild/linux-ia32@0.25.2':
385 | optional: true
386 |
387 | '@esbuild/linux-loong64@0.25.2':
388 | optional: true
389 |
390 | '@esbuild/linux-mips64el@0.25.2':
391 | optional: true
392 |
393 | '@esbuild/linux-ppc64@0.25.2':
394 | optional: true
395 |
396 | '@esbuild/linux-riscv64@0.25.2':
397 | optional: true
398 |
399 | '@esbuild/linux-s390x@0.25.2':
400 | optional: true
401 |
402 | '@esbuild/linux-x64@0.25.2':
403 | optional: true
404 |
405 | '@esbuild/netbsd-arm64@0.25.2':
406 | optional: true
407 |
408 | '@esbuild/netbsd-x64@0.25.2':
409 | optional: true
410 |
411 | '@esbuild/openbsd-arm64@0.25.2':
412 | optional: true
413 |
414 | '@esbuild/openbsd-x64@0.25.2':
415 | optional: true
416 |
417 | '@esbuild/sunos-x64@0.25.2':
418 | optional: true
419 |
420 | '@esbuild/win32-arm64@0.25.2':
421 | optional: true
422 |
423 | '@esbuild/win32-ia32@0.25.2':
424 | optional: true
425 |
426 | '@esbuild/win32-x64@0.25.2':
427 | optional: true
428 |
429 | '@rollup/rollup-android-arm-eabi@4.40.0':
430 | optional: true
431 |
432 | '@rollup/rollup-android-arm64@4.40.0':
433 | optional: true
434 |
435 | '@rollup/rollup-darwin-arm64@4.40.0':
436 | optional: true
437 |
438 | '@rollup/rollup-darwin-x64@4.40.0':
439 | optional: true
440 |
441 | '@rollup/rollup-freebsd-arm64@4.40.0':
442 | optional: true
443 |
444 | '@rollup/rollup-freebsd-x64@4.40.0':
445 | optional: true
446 |
447 | '@rollup/rollup-linux-arm-gnueabihf@4.40.0':
448 | optional: true
449 |
450 | '@rollup/rollup-linux-arm-musleabihf@4.40.0':
451 | optional: true
452 |
453 | '@rollup/rollup-linux-arm64-gnu@4.40.0':
454 | optional: true
455 |
456 | '@rollup/rollup-linux-arm64-musl@4.40.0':
457 | optional: true
458 |
459 | '@rollup/rollup-linux-loongarch64-gnu@4.40.0':
460 | optional: true
461 |
462 | '@rollup/rollup-linux-powerpc64le-gnu@4.40.0':
463 | optional: true
464 |
465 | '@rollup/rollup-linux-riscv64-gnu@4.40.0':
466 | optional: true
467 |
468 | '@rollup/rollup-linux-riscv64-musl@4.40.0':
469 | optional: true
470 |
471 | '@rollup/rollup-linux-s390x-gnu@4.40.0':
472 | optional: true
473 |
474 | '@rollup/rollup-linux-x64-gnu@4.40.0':
475 | optional: true
476 |
477 | '@rollup/rollup-linux-x64-musl@4.40.0':
478 | optional: true
479 |
480 | '@rollup/rollup-win32-arm64-msvc@4.40.0':
481 | optional: true
482 |
483 | '@rollup/rollup-win32-ia32-msvc@4.40.0':
484 | optional: true
485 |
486 | '@rollup/rollup-win32-x64-msvc@4.40.0':
487 | optional: true
488 |
489 | '@types/estree@1.0.7': {}
490 |
491 | esbuild@0.25.2:
492 | optionalDependencies:
493 | '@esbuild/aix-ppc64': 0.25.2
494 | '@esbuild/android-arm': 0.25.2
495 | '@esbuild/android-arm64': 0.25.2
496 | '@esbuild/android-x64': 0.25.2
497 | '@esbuild/darwin-arm64': 0.25.2
498 | '@esbuild/darwin-x64': 0.25.2
499 | '@esbuild/freebsd-arm64': 0.25.2
500 | '@esbuild/freebsd-x64': 0.25.2
501 | '@esbuild/linux-arm': 0.25.2
502 | '@esbuild/linux-arm64': 0.25.2
503 | '@esbuild/linux-ia32': 0.25.2
504 | '@esbuild/linux-loong64': 0.25.2
505 | '@esbuild/linux-mips64el': 0.25.2
506 | '@esbuild/linux-ppc64': 0.25.2
507 | '@esbuild/linux-riscv64': 0.25.2
508 | '@esbuild/linux-s390x': 0.25.2
509 | '@esbuild/linux-x64': 0.25.2
510 | '@esbuild/netbsd-arm64': 0.25.2
511 | '@esbuild/netbsd-x64': 0.25.2
512 | '@esbuild/openbsd-arm64': 0.25.2
513 | '@esbuild/openbsd-x64': 0.25.2
514 | '@esbuild/sunos-x64': 0.25.2
515 | '@esbuild/win32-arm64': 0.25.2
516 | '@esbuild/win32-ia32': 0.25.2
517 | '@esbuild/win32-x64': 0.25.2
518 |
519 | fsevents@2.3.3:
520 | optional: true
521 |
522 | nanoid@3.3.11: {}
523 |
524 | picocolors@1.1.1: {}
525 |
526 | postcss@8.5.3:
527 | dependencies:
528 | nanoid: 3.3.11
529 | picocolors: 1.1.1
530 | source-map-js: 1.2.1
531 |
532 | rollup@4.40.0:
533 | dependencies:
534 | '@types/estree': 1.0.7
535 | optionalDependencies:
536 | '@rollup/rollup-android-arm-eabi': 4.40.0
537 | '@rollup/rollup-android-arm64': 4.40.0
538 | '@rollup/rollup-darwin-arm64': 4.40.0
539 | '@rollup/rollup-darwin-x64': 4.40.0
540 | '@rollup/rollup-freebsd-arm64': 4.40.0
541 | '@rollup/rollup-freebsd-x64': 4.40.0
542 | '@rollup/rollup-linux-arm-gnueabihf': 4.40.0
543 | '@rollup/rollup-linux-arm-musleabihf': 4.40.0
544 | '@rollup/rollup-linux-arm64-gnu': 4.40.0
545 | '@rollup/rollup-linux-arm64-musl': 4.40.0
546 | '@rollup/rollup-linux-loongarch64-gnu': 4.40.0
547 | '@rollup/rollup-linux-powerpc64le-gnu': 4.40.0
548 | '@rollup/rollup-linux-riscv64-gnu': 4.40.0
549 | '@rollup/rollup-linux-riscv64-musl': 4.40.0
550 | '@rollup/rollup-linux-s390x-gnu': 4.40.0
551 | '@rollup/rollup-linux-x64-gnu': 4.40.0
552 | '@rollup/rollup-linux-x64-musl': 4.40.0
553 | '@rollup/rollup-win32-arm64-msvc': 4.40.0
554 | '@rollup/rollup-win32-ia32-msvc': 4.40.0
555 | '@rollup/rollup-win32-x64-msvc': 4.40.0
556 | fsevents: 2.3.3
557 |
558 | source-map-js@1.2.1: {}
559 |
560 | vite@6.2.6:
561 | dependencies:
562 | esbuild: 0.25.2
563 | postcss: 8.5.3
564 | rollup: 4.40.0
565 | optionalDependencies:
566 | fsevents: 2.3.3
567 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | // Import the wasm-bindgen generated glue code
2 | import init, {
3 | QuietZone,
4 | CenterImage,
5 | ModuleStyle,
6 | LocatorStyle,
7 | generate_qr_code_with_options,
8 | } from './chromium_style_qrcode_generator_with_wasm.js';
9 |
10 | const urlInput = document.getElementById('urlInput');
11 | const qrCanvas = document.getElementById('qrCanvas');
12 | const centerErrorLabel = document.getElementById('centerErrorLabel');
13 | const bottomErrorLabel = document.getElementById('bottomErrorLabel');
14 | const copyButton = document.getElementById('copyButton');
15 | const downloadButton = document.getElementById('downloadButton');
16 |
17 | const ctx = qrCanvas.getContext('2d');
18 | const moduleColor = '#000000'; // Black
19 | const backgroundColor = '#FFFFFF'; // White
20 |
21 | // Constants matching Chromium implementation
22 | const MODULE_SIZE_PIXELS = 10;
23 | const DINO_TILE_SIZE_PIXELS = 4;
24 | const LOCATOR_SIZE_MODULES = 7;
25 | const QUIET_ZONE_SIZE_PIXELS = MODULE_SIZE_PIXELS * 4;
26 |
27 | // --- Polyfill for roundRect if not available ---
28 | if (!CanvasRenderingContext2D.prototype.roundRect) {
29 | CanvasRenderingContext2D.prototype.roundRect = function (
30 | x,
31 | y,
32 | width,
33 | height,
34 | radius
35 | ) {
36 | if (typeof radius === 'number') {
37 | radius = [radius, radius, radius, radius];
38 | } else if (radius.length === 1) {
39 | radius = [radius[0], radius[0], radius[0], radius[0]];
40 | } else if (radius.length === 2) {
41 | radius = [radius[0], radius[1], radius[0], radius[1]];
42 | }
43 |
44 | this.beginPath();
45 | this.moveTo(x + radius[0], y);
46 | this.arcTo(x + width, y, x + width, y + height, radius[1]);
47 | this.arcTo(x + width, y + height, x, y + height, radius[2]);
48 | this.arcTo(x, y + height, x, y, radius[3]);
49 | this.arcTo(x, y, x + width, y, radius[0]);
50 | this.closePath();
51 | return this;
52 | };
53 | }
54 |
55 | // --- Dino Data (EXACT copy from Chromium dino_image.h) ---
56 | const kDinoWidth = 20;
57 | const kDinoHeight = 22;
58 | const kDinoHeadHeight = 8;
59 | const kDinoBodyHeight = 14; // kDinoHeight - kDinoHeadHeight
60 | const kDinoWidthBytes = 3; // (kDinoWidth + 7) / 8
61 |
62 | // Pixel data for the dino's head, facing right - EXACT from Chromium
63 | const kDinoHeadRight = [
64 | 0b00000000, 0b00011111, 0b11100000, 0b00000000, 0b00111111, 0b11110000,
65 | 0b00000000, 0b00110111, 0b11110000, 0b00000000, 0b00111111, 0b11110000,
66 | 0b00000000, 0b00111111, 0b11110000, 0b00000000, 0b00111111, 0b11110000,
67 | 0b00000000, 0b00111110, 0b00000000, 0b00000000, 0b00111111, 0b11000000,
68 | ];
69 |
70 | // Pixel data for the dino's body - EXACT from Chromium
71 | const kDinoBody = [
72 | 0b10000000, 0b01111100, 0b00000000, 0b10000001, 0b11111100, 0b00000000,
73 | 0b11000011, 0b11111111, 0b00000000, 0b11100111, 0b11111101, 0b00000000,
74 | 0b11111111, 0b11111100, 0b00000000, 0b11111111, 0b11111100, 0b00000000,
75 | 0b01111111, 0b11111000, 0b00000000, 0b00111111, 0b11111000, 0b00000000,
76 | 0b00011111, 0b11110000, 0b00000000, 0b00001111, 0b11100000, 0b00000000,
77 | 0b00000111, 0b01100000, 0b00000000, 0b00000110, 0b00100000, 0b00000000,
78 | 0b00000100, 0b00100000, 0b00000000, 0b00000110, 0b00110000, 0b00000000,
79 | ];
80 | // --- End Dino Data ---
81 |
82 | let currentQrData = null;
83 | let currentQrSize = 0;
84 | let currentOriginalSize = 0; // Track original size without quiet zone
85 |
86 | // --- Error Handling ---
87 | const errorMessages = {
88 | // Based on C++ version - max input length is 2000 characters
89 | INPUT_TOO_LONG:
90 | 'Input is too long. Please shorten the text to 2000 characters or less.',
91 | UNKNOWN_ERROR: 'Could not generate QR code. Please try again.',
92 | };
93 |
94 | function displayError(errorType) {
95 | hideErrors(false); // Disable buttons
96 | qrCanvas.style.display = 'block'; // Keep canvas space
97 |
98 | if (errorType === 'INPUT_TOO_LONG') {
99 | centerErrorLabel.classList.add('hidden');
100 | bottomErrorLabel.textContent = errorMessages.INPUT_TOO_LONG;
101 | bottomErrorLabel.classList.remove('hidden');
102 | // Display placeholder (blank canvas)
103 | ctx.fillStyle = backgroundColor;
104 | ctx.fillRect(0, 0, qrCanvas.width, qrCanvas.height);
105 | } else {
106 | // Assuming UNKNOWN_ERROR or others
107 | bottomErrorLabel.classList.add('hidden');
108 | qrCanvas.style.display = 'none'; // Hide canvas
109 | centerErrorLabel.textContent = errorMessages.UNKNOWN_ERROR;
110 | centerErrorLabel.classList.remove('hidden'); // Show center error
111 | }
112 | }
113 |
114 | function hideErrors(enableButtons) {
115 | centerErrorLabel.classList.add('hidden');
116 | bottomErrorLabel.classList.add('hidden');
117 | qrCanvas.style.display = 'block'; // Ensure canvas is visible
118 | copyButton.disabled = !enableButtons;
119 | downloadButton.disabled = !enableButtons;
120 | }
121 |
122 | // --- QR Code Rendering (Chromium-exact implementation) ---
123 | function renderQRCodeChromiumStyle(pixelData, size, originalSize) {
124 | if (!pixelData || size === 0) {
125 | // Display placeholder if no data
126 | ctx.save(); // Save state before clearing
127 | ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
128 | ctx.fillStyle = backgroundColor;
129 | ctx.fillRect(0, 0, qrCanvas.width, qrCanvas.height);
130 | ctx.restore(); // Restore state
131 | hideErrors(false);
132 | currentQrData = null;
133 | currentQrSize = 0;
134 | currentOriginalSize = 0;
135 | return;
136 | }
137 |
138 | currentQrData = pixelData;
139 | currentQrSize = size;
140 | currentOriginalSize = originalSize;
141 |
142 | // Use high DPI canvas for crisp rendering (fix blur issue)
143 | const kQRImageSizePx = 240;
144 | const devicePixelRatio = window.devicePixelRatio || 1;
145 | const canvasSize = kQRImageSizePx * devicePixelRatio;
146 |
147 | // Set canvas size to high DPI for crisp rendering
148 | qrCanvas.width = canvasSize;
149 | qrCanvas.height = canvasSize;
150 | qrCanvas.style.width = '240px'; // CSS size remains 240px for display
151 | qrCanvas.style.height = '240px';
152 |
153 | // Scale the canvas context for high DPI
154 | ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
155 |
156 | // Clear canvas with white background (matching Chromium's eraseARGB(0xFF, 0xFF, 0xFF, 0xFF))
157 | ctx.fillStyle = backgroundColor;
158 | ctx.fillRect(0, 0, kQRImageSizePx, kQRImageSizePx);
159 |
160 | // Calculate scaling factor to fit QR code exactly in 240x240 canvas
161 | // The QR code should fill the entire canvas area with appropriate scaling
162 | const totalPixelsNeeded = kQRImageSizePx;
163 | const modulePixelSize = Math.floor(totalPixelsNeeded / originalSize);
164 | const margin = Math.floor(
165 | (totalPixelsNeeded - originalSize * modulePixelSize) / 2
166 | );
167 |
168 | // Enable anti-aliasing for smoother rendering
169 | ctx.imageSmoothingEnabled = true;
170 | ctx.imageSmoothingQuality = 'high';
171 |
172 | // Setup paint styles exactly like Chromium
173 | const paintBlack = { color: moduleColor }; // SK_ColorBLACK
174 | const paintWhite = { color: backgroundColor }; // SK_ColorWHITE
175 |
176 | // First pass: Draw data modules (exactly like Chromium's bitmap_generator.cc)
177 | // Note: pixelData might include quiet zone, handle it properly
178 | const hasQuietZone = size > originalSize;
179 | const quietZoneModules = hasQuietZone ? (size - originalSize) / 2 : 0;
180 |
181 | for (let y = 0; y < size; y++) {
182 | for (let x = 0; x < size; x++) {
183 | const dataIndex = y * size + x;
184 | if (pixelData[dataIndex] & 0x1) {
185 | // Check if module is dark (least significant bit)
186 | // Convert from data coordinates to original QR coordinates
187 | let originalX, originalY;
188 | if (hasQuietZone) {
189 | originalX = x - quietZoneModules;
190 | originalY = y - quietZoneModules;
191 |
192 | // Skip if outside original QR area
193 | if (
194 | originalX < 0 ||
195 | originalY < 0 ||
196 | originalX >= originalSize ||
197 | originalY >= originalSize
198 | ) {
199 | continue;
200 | }
201 | } else {
202 | originalX = x;
203 | originalY = y;
204 | }
205 |
206 | const isLocator = isLocatorModule(originalX, originalY, originalSize);
207 | if (isLocator) {
208 | continue; // Skip locators, draw them separately
209 | }
210 |
211 | // Draw data module with circles style (ModuleStyle::kCircles from Chromium)
212 | const centerX = margin + (originalX + 0.5) * modulePixelSize;
213 | const centerY = margin + (originalY + 0.5) * modulePixelSize;
214 | const radius = modulePixelSize / 2 - 1; // Exactly matching Chromium
215 |
216 | ctx.fillStyle = paintBlack.color;
217 | ctx.beginPath();
218 | ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
219 | ctx.fill();
220 | }
221 | }
222 | }
223 |
224 | // Second pass: Draw locators with rounded style (LocatorStyle::kRounded)
225 | drawLocators(
226 | ctx,
227 | { width: originalSize, height: originalSize },
228 | paintBlack,
229 | paintWhite,
230 | margin,
231 | modulePixelSize
232 | );
233 |
234 | // Third pass: Draw center image (CenterImage::kDino)
235 | const canvasBounds = {
236 | x: 0,
237 | y: 0,
238 | width: kQRImageSizePx,
239 | height: kQRImageSizePx,
240 | };
241 | drawCenterImage(ctx, canvasBounds, paintWhite, modulePixelSize);
242 |
243 | hideErrors(true); // Enable buttons on success
244 | }
245 |
246 | // Check if a module position is part of a locator pattern (matching Chromium logic exactly)
247 | function isLocatorModule(x, y, originalSize) {
248 | // Check the three locator positions (7x7 each)
249 | // Chromium logic: locators are at corners, each is LOCATOR_SIZE_MODULES x LOCATOR_SIZE_MODULES
250 |
251 | // Top-left locator
252 | if (x < LOCATOR_SIZE_MODULES && y < LOCATOR_SIZE_MODULES) {
253 | return true;
254 | }
255 |
256 | // Top-right locator
257 | if (x >= originalSize - LOCATOR_SIZE_MODULES && y < LOCATOR_SIZE_MODULES) {
258 | return true;
259 | }
260 |
261 | // Bottom-left locator
262 | if (x < LOCATOR_SIZE_MODULES && y >= originalSize - LOCATOR_SIZE_MODULES) {
263 | return true;
264 | }
265 |
266 | // No locator on bottom-right (as per Chromium comment)
267 | return false;
268 | }
269 |
270 | // Draw QR locators at three corners (EXACT Chromium DrawLocators implementation)
271 | function drawLocators(
272 | ctx,
273 | dataSize,
274 | paintForeground,
275 | paintBackground,
276 | margin,
277 | modulePixelSize
278 | ) {
279 | // Use exact Chromium radius calculation: LocatorStyle::kRounded = 10px
280 | // Scale the radius proportionally with module size for consistent appearance
281 | const chromiumModuleSize = 10; // Chromium's kModuleSizePixels
282 | const scaleFactor = modulePixelSize / chromiumModuleSize;
283 | const radius = 10 * scaleFactor; // Exact Chromium radius scaled proportionally
284 |
285 | // Draw a locator with upper left corner at {leftXModules, topYModules}
286 | function drawOneLocator(leftXModules, topYModules) {
287 | // Outermost square, 7x7 modules (exactly matching Chromium)
288 | let leftXPixels = leftXModules * modulePixelSize;
289 | let topYPixels = topYModules * modulePixelSize;
290 | let dimPixels = modulePixelSize * LOCATOR_SIZE_MODULES;
291 |
292 | drawRoundRect(
293 | ctx,
294 | margin + leftXPixels,
295 | margin + topYPixels,
296 | dimPixels,
297 | dimPixels,
298 | radius,
299 | paintForeground.color
300 | );
301 |
302 | // Middle square, one module smaller in all dimensions (5x5 - exactly matching Chromium)
303 | leftXPixels += modulePixelSize;
304 | topYPixels += modulePixelSize;
305 | dimPixels -= 2 * modulePixelSize;
306 |
307 | drawRoundRect(
308 | ctx,
309 | margin + leftXPixels,
310 | margin + topYPixels,
311 | dimPixels,
312 | dimPixels,
313 | radius,
314 | paintBackground.color
315 | );
316 |
317 | // Inner square, one additional module smaller in all dimensions (3x3 - exactly matching Chromium)
318 | leftXPixels += modulePixelSize;
319 | topYPixels += modulePixelSize;
320 | dimPixels -= 2 * modulePixelSize;
321 |
322 | drawRoundRect(
323 | ctx,
324 | margin + leftXPixels,
325 | margin + topYPixels,
326 | dimPixels,
327 | dimPixels,
328 | radius,
329 | paintForeground.color
330 | );
331 | }
332 |
333 | // Draw the three locators (exactly matching Chromium positions)
334 | drawOneLocator(0, 0); // Top-left
335 | drawOneLocator(dataSize.width - LOCATOR_SIZE_MODULES, 0); // Top-right
336 | drawOneLocator(0, dataSize.height - LOCATOR_SIZE_MODULES); // Bottom-left
337 | // No locator on bottom-right (as per Chromium)
338 | }
339 |
340 | // Helper function to draw rounded rectangles exactly matching Chromium
341 | function drawRoundRect(ctx, x, y, width, height, radius, fillStyle) {
342 | ctx.fillStyle = fillStyle;
343 |
344 | // Use exact Chromium rounding behavior
345 | ctx.beginPath();
346 | ctx.roundRect(x, y, width, height, radius);
347 | ctx.fill();
348 | }
349 |
350 | // Draw center image (dino implementation matching Chromium exactly)
351 | function drawCenterImage(ctx, canvasBounds, paintBackground, modulePixelSize) {
352 | // Calculate dino size exactly like Chromium's DrawDino function
353 | // In Chromium: DrawDino(&canvas, bitmap_bounds, kDinoTileSizePixels, 2, paint_black, paint_white);
354 | // But we need to scale these values based on our actual module size vs Chromium's 10px
355 | const chromiumModuleSize = 10; // Chromium's kModuleSizePixels
356 | const scaleFactor = modulePixelSize / chromiumModuleSize;
357 | const pixelsPerDinoTile = Math.round(DINO_TILE_SIZE_PIXELS * scaleFactor);
358 | const dinoWidthPx = pixelsPerDinoTile * kDinoWidth;
359 | const dinoHeightPx = pixelsPerDinoTile * kDinoHeight;
360 | const dinoBorderPx = Math.round(2 * scaleFactor); // Scale the border too
361 |
362 | paintCenterImage(
363 | ctx,
364 | canvasBounds,
365 | dinoWidthPx,
366 | dinoHeightPx,
367 | dinoBorderPx,
368 | paintBackground, // Pass white background color
369 | modulePixelSize
370 | );
371 | }
372 |
373 | // Paint center image exactly like Chromium's PaintCenterImage function
374 | function paintCenterImage(
375 | ctx,
376 | canvasBounds,
377 | widthPx,
378 | heightPx,
379 | borderPx,
380 | paintBackground,
381 | modulePixelSize = MODULE_SIZE_PIXELS
382 | ) {
383 | // Validation exactly like Chromium (asserts converted to early returns)
384 | if (
385 | canvasBounds.width / 2 < widthPx + borderPx ||
386 | canvasBounds.height / 2 < heightPx + borderPx
387 | ) {
388 | console.warn('Center image too large for canvas bounds');
389 | return;
390 | }
391 |
392 | // Assemble the target rect for the dino image data (exactly matching Chromium)
393 | let destX = (canvasBounds.width - widthPx) / 2;
394 | let destY = (canvasBounds.height - heightPx) / 2;
395 |
396 | // Clear out a little room for a border, snapped to some number of modules
397 | // Exactly matching Chromium's PaintCenterImage background calculation
398 | const backgroundLeft =
399 | Math.floor((destX - borderPx) / modulePixelSize) * modulePixelSize;
400 | const backgroundTop =
401 | Math.floor((destY - borderPx) / modulePixelSize) * modulePixelSize;
402 | const backgroundRight =
403 | Math.floor(
404 | (destX + widthPx + borderPx + modulePixelSize - 1) / modulePixelSize
405 | ) * modulePixelSize;
406 | const backgroundBottom =
407 | Math.floor(
408 | (destY + heightPx + borderPx + modulePixelSize - 1) / modulePixelSize
409 | ) * modulePixelSize;
410 |
411 | // Draw white background exactly like Chromium
412 | ctx.fillStyle = paintBackground.color; // Use white background from paint parameter
413 | ctx.fillRect(
414 | backgroundLeft,
415 | backgroundTop,
416 | backgroundRight - backgroundLeft,
417 | backgroundBottom - backgroundTop
418 | );
419 |
420 | // Center the image within the cleared space, and draw it
421 | // Exactly matching Chromium's centering logic with SkScalarRoundToScalar
422 | const deltaX = Math.round(
423 | (backgroundLeft + backgroundRight) / 2 - (destX + widthPx / 2)
424 | );
425 | const deltaY = Math.round(
426 | (backgroundTop + backgroundBottom) / 2 - (destY + heightPx / 2)
427 | );
428 | destX += deltaX;
429 | destY += deltaY;
430 |
431 | // Draw dino - only the black pixels, transparent background
432 | drawDinoPixelByPixel(ctx, destX, destY, widthPx, heightPx);
433 | }
434 |
435 | // Draw dino pixel by pixel to avoid any white background
436 | function drawDinoPixelByPixel(ctx, destX, destY, destWidth, destHeight) {
437 | const scaleX = destWidth / kDinoWidth;
438 | const scaleY = destHeight / kDinoHeight;
439 |
440 | ctx.fillStyle = moduleColor; // Black color for dino pixels
441 |
442 | // Helper function to draw pixel data
443 | function drawPixelData(srcArray, srcNumRows, startRow) {
444 | const bytesPerRow = kDinoWidthBytes;
445 |
446 | for (let row = 0; row < srcNumRows; row++) {
447 | let whichByte = row * bytesPerRow;
448 | let mask = 0b10000000;
449 |
450 | for (let col = 0; col < kDinoWidth; col++) {
451 | if (srcArray[whichByte] & mask) {
452 | // Calculate destination pixel position
453 | const pixelX = destX + col * scaleX;
454 | const pixelY = destY + (startRow + row) * scaleY;
455 |
456 | // Draw scaled pixel - only black pixels, no background
457 | ctx.fillRect(
458 | Math.floor(pixelX),
459 | Math.floor(pixelY),
460 | Math.ceil(scaleX),
461 | Math.ceil(scaleY)
462 | );
463 | }
464 | mask >>= 1;
465 | if (mask === 0) {
466 | mask = 0b10000000;
467 | whichByte++;
468 | }
469 | }
470 | }
471 | }
472 |
473 | // Draw dino head and body pixel by pixel
474 | drawPixelData(kDinoHeadRight, kDinoHeadHeight, 0);
475 | drawPixelData(kDinoBody, kDinoBodyHeight, kDinoHeadHeight);
476 | }
477 |
478 | // --- Actions ---
479 | async function generateQRCode() {
480 | const inputText = urlInput.value.trim();
481 | if (!inputText) {
482 | ctx.fillStyle = backgroundColor;
483 | ctx.fillRect(0, 0, qrCanvas.width, qrCanvas.height);
484 | hideErrors(false);
485 | return;
486 | }
487 |
488 | // Check input length limit (same as Chromium C++ version)
489 | const kMaxInputLength = 2000;
490 | if (inputText.length > kMaxInputLength) {
491 | displayError('INPUT_TOO_LONG');
492 | currentQrData = null;
493 | currentQrSize = 0;
494 | return;
495 | }
496 |
497 | try {
498 | // Use the Chromium-style options exactly matching the Android implementation
499 | const result = generate_qr_code_with_options(
500 | inputText,
501 | ModuleStyle.Circles, // Data modules as circles (kCircles)
502 | LocatorStyle.Rounded, // Rounded locators (kRounded)
503 | CenterImage.Dino, // Dino center image (kDino)
504 | QuietZone.WillBeAddedByClient // Match Android bridge layer behavior
505 | );
506 |
507 | if (!result || !result.data) {
508 | displayError('UNKNOWN_ERROR');
509 | currentQrData = null;
510 | currentQrSize = 0;
511 | return;
512 | }
513 |
514 | // Use the Chromium-exact rendering approach
515 | renderQRCodeChromiumStyle(result.data, result.size, result.original_size);
516 | } catch (error) {
517 | console.error('Wasm QR generation failed:', error);
518 |
519 | if (error && error.toString().includes('too long')) {
520 | displayError('INPUT_TOO_LONG');
521 | } else {
522 | displayError('UNKNOWN_ERROR');
523 | }
524 |
525 | currentQrData = null;
526 | currentQrSize = 0;
527 | }
528 | }
529 |
530 | function copyInputText() {
531 | if (!urlInput.value) return;
532 |
533 | // Copy QR code image to clipboard instead of just text
534 | if (currentQrData && currentQrSize > 0) {
535 | // Create a canvas for clipboard with Chromium-exact size
536 | const clipboardCanvas = document.createElement('canvas');
537 | const clipboardCtx = clipboardCanvas.getContext('2d');
538 |
539 | // Use same size as download - exact Chromium sizing
540 | const margin = QUIET_ZONE_SIZE_PIXELS; // 40 pixels (4 modules * 10 pixels)
541 | const chromiumSize = currentOriginalSize * MODULE_SIZE_PIXELS + margin * 2;
542 | clipboardCanvas.width = chromiumSize;
543 | clipboardCanvas.height = chromiumSize;
544 |
545 | clipboardCtx.imageSmoothingEnabled = false;
546 | clipboardCtx.imageSmoothingQuality = 'high';
547 |
548 | // Re-render QR code at exact size
549 | renderQRCodeAtSize(
550 | clipboardCtx,
551 | chromiumSize,
552 | currentQrData,
553 | currentQrSize
554 | );
555 |
556 | // Convert to blob and copy
557 | clipboardCanvas.toBlob((blob) => {
558 | const item = new ClipboardItem({ 'image/png': blob });
559 | navigator.clipboard
560 | .write([item])
561 | .then(() => {
562 | // Show feedback
563 | const originalText = copyButton.textContent;
564 | copyButton.textContent = 'Copied!';
565 | setTimeout(() => {
566 | copyButton.textContent = originalText;
567 | }, 1500);
568 | })
569 | .catch((err) => {
570 | console.error('Failed to copy image: ', err);
571 | // Fallback to copying text
572 | fallbackCopyText();
573 | });
574 | }, 'image/png');
575 | } else {
576 | fallbackCopyText();
577 | }
578 | }
579 |
580 | function fallbackCopyText() {
581 | navigator.clipboard
582 | .writeText(urlInput.value)
583 | .then(() => {
584 | const originalText = copyButton.textContent;
585 | copyButton.textContent = 'Copied!';
586 | setTimeout(() => {
587 | copyButton.textContent = originalText;
588 | }, 1500);
589 | })
590 | .catch((err) => {
591 | console.error('Failed to copy text: ', err);
592 | });
593 | }
594 |
595 | function getQRCodeFilenameForURL(urlStr) {
596 | try {
597 | const url = new URL(urlStr);
598 | if (url.hostname && !/^\d{1,3}(\.\d{1,3}){3}$/.test(url.hostname)) {
599 | // Check if hostname exists and is not an IP
600 | // Basic sanitization: replace non-alphanumeric with underscore
601 | const safeHostname = url.hostname.replace(/[^a-zA-Z0-9.-]/g, '_');
602 | return `qrcode_${safeHostname}.png`;
603 | }
604 | } catch (e) {
605 | // Ignore if not a valid URL
606 | }
607 | return 'qrcode_chrome.png'; // Default filename
608 | }
609 |
610 | function downloadQRCode() {
611 | if (!currentQrData || currentQrSize === 0) return;
612 |
613 | const filename = getQRCodeFilenameForURL(urlInput.value);
614 |
615 | // Create a temporary canvas with Chromium-exact sizing
616 | const downloadCanvas = document.createElement('canvas');
617 | const downloadCtx = downloadCanvas.getContext('2d');
618 |
619 | // Calculate exact size matching Chromium's RenderBitmap function
620 | // In Chromium: bitmap size = data_size.width() * kModuleSizePixels + margin * 2
621 | // where margin = kQuietZoneSizePixels = kModuleSizePixels * 4 = 40px
622 | const margin = QUIET_ZONE_SIZE_PIXELS; // 40 pixels (4 modules * 10 pixels)
623 | const chromiumSize = currentOriginalSize * MODULE_SIZE_PIXELS + margin * 2;
624 |
625 | // Set download canvas to exact Chromium size
626 | downloadCanvas.width = chromiumSize;
627 | downloadCanvas.height = chromiumSize;
628 |
629 | // Clear canvas with white background
630 | downloadCtx.fillStyle = backgroundColor;
631 | downloadCtx.fillRect(0, 0, chromiumSize, chromiumSize);
632 |
633 | // Enable high quality scaling
634 | downloadCtx.imageSmoothingEnabled = false; // Disable smoothing for exact pixel reproduction
635 | downloadCtx.imageSmoothingQuality = 'high';
636 |
637 | // Re-render QR code at exact download size using same rendering logic
638 | renderQRCodeAtSize(downloadCtx, chromiumSize, currentQrData, currentQrSize);
639 |
640 | // Create download link
641 | const link = document.createElement('a');
642 | link.download = filename;
643 | link.href = downloadCanvas.toDataURL('image/png');
644 | link.click();
645 | }
646 |
647 | // Render QR code at specific size (for download) - exactly matching Chromium
648 | function renderQRCodeAtSize(ctx, targetSize, pixelData, size) {
649 | // Clear canvas with white background
650 | ctx.fillStyle = backgroundColor;
651 | ctx.fillRect(0, 0, targetSize, targetSize);
652 |
653 | // Calculate margin and module size exactly like Chromium's RenderBitmap
654 | const margin = QUIET_ZONE_SIZE_PIXELS; // 40 pixels fixed margin
655 | const modulePixelSize = MODULE_SIZE_PIXELS; // 10 pixels per module
656 |
657 | // Setup paint styles exactly like Chromium
658 | const paintBlack = { color: moduleColor };
659 | const paintWhite = { color: backgroundColor };
660 |
661 | // Get original size without quiet zone (this is what Chromium calls data_size)
662 | const originalSize = currentOriginalSize;
663 |
664 | // Check if we have quiet zone in our data
665 | const hasQuietZone = size > originalSize;
666 | const quietZoneModules = hasQuietZone ? (size - originalSize) / 2 : 0;
667 |
668 | // First pass: Draw data modules (matching Chromium's loop exactly)
669 | for (let y = 0; y < size; y++) {
670 | for (let x = 0; x < size; x++) {
671 | const dataIndex = y * size + x;
672 | if (pixelData[dataIndex] & 0x1) {
673 | let originalX, originalY;
674 | if (hasQuietZone) {
675 | originalX = x - quietZoneModules;
676 | originalY = y - quietZoneModules;
677 | if (
678 | originalX < 0 ||
679 | originalY < 0 ||
680 | originalX >= originalSize ||
681 | originalY >= originalSize
682 | ) {
683 | continue;
684 | }
685 | } else {
686 | originalX = x;
687 | originalY = y;
688 | }
689 |
690 | // Skip locator modules - they will be drawn separately
691 | const isLocator = isLocatorModule(originalX, originalY, originalSize);
692 | if (isLocator) continue;
693 |
694 | // Draw circle module exactly like Chromium
695 | const centerX = margin + (originalX + 0.5) * modulePixelSize;
696 | const centerY = margin + (originalY + 0.5) * modulePixelSize;
697 | const radius = modulePixelSize / 2 - 1;
698 |
699 | ctx.fillStyle = paintBlack.color;
700 | ctx.beginPath();
701 | ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
702 | ctx.fill();
703 | }
704 | }
705 | }
706 |
707 | // Draw locators exactly like Chromium
708 | drawLocators(
709 | ctx,
710 | { width: originalSize, height: originalSize },
711 | paintBlack,
712 | paintWhite,
713 | margin,
714 | modulePixelSize
715 | );
716 |
717 | // Draw center image exactly like Chromium
718 | const canvasBounds = { x: 0, y: 0, width: targetSize, height: targetSize };
719 | drawCenterImage(ctx, canvasBounds, paintWhite, modulePixelSize);
720 | }
721 |
722 | // --- Initialization ---
723 | async function run() {
724 | // Initialize the Wasm module
725 | await init();
726 | console.log('Wasm module initialized.');
727 |
728 | // Add event listeners
729 | urlInput.addEventListener('input', generateQRCode);
730 | copyButton.addEventListener('click', copyInputText);
731 | downloadButton.addEventListener('click', downloadQRCode);
732 |
733 | // Set default URL for testing (matches qrcode.png)
734 | urlInput.value = 'https://avg.163.com';
735 |
736 | // Initial generation
737 | generateQRCode();
738 | }
739 |
740 | run();
741 |
--------------------------------------------------------------------------------