├── .editorconfig ├── .github └── workflows │ ├── deploy-to-github-pages.yml │ ├── preview-build.yml │ ├── preview-deploy.yml │ └── preview-start.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── README.zh-CN.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── public └── favicon.ico ├── src ├── app.css ├── app.js └── lib.rs ├── vite.config.js ├── vite.constant.js └── vite.helper.js /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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=$(<pr-id.txt) 73 | if ! [[ "$pr_id" =~ ^[0-9]+$ ]]; then 74 | echo "Error: pr-id.txt does not contain a valid numeric PR id. Please check." 75 | exit 1 76 | fi 77 | echo "id=$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 | <!-- AUTO_PREVIEW_HOOK --> 105 | body-include: <!-- AUTO_PREVIEW_HOOK --> 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 | <!-- AUTO_PREVIEW_HOOK --> 116 | body-include: <!-- AUTO_PREVIEW_HOOK --> 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 | -------------------------------------------------------------------------------- /.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 | <!-- AUTO_PREVIEW_HOOK --> 25 | body-include: <!-- AUTO_PREVIEW_HOOK --> 26 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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: <http://localhost:5173>) 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 | -------------------------------------------------------------------------------- /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. 在浏览器中打开项目 (默认为 <http://localhost:5173>) 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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <link rel="icon" type="image/svg+xml" href="/favicon.ico" /> 6 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 | <meta name="color-scheme" content="light" /> 8 | <title>Chromium Style QR Code Generator</title> 9 | <link rel="stylesheet" href="./src/app.css" /> 10 | <!-- Preload critical resources for better performance --> 11 | <link rel="preload" href="./src/qrcode_generator_with_wasm.js" as="script" /> 12 | <link rel="preload" href="./src/qrcode_generator_with_wasm_bg.wasm" as="fetch" type="application/wasm" crossorigin /> 13 | </head> 14 | <body> 15 | <div class="bubble-container"> 16 | <!-- Title matching Chromium's IDS_BROWSER_SHARING_QR_CODE_DIALOG_TITLE --> 17 | <h2>QR code</h2> 18 | 19 | <!-- QR code display area with border and background matching Chromium --> 20 | <div class="qr-code-area"> 21 | <canvas id="qrCanvas" width="240" height="240"></canvas> 22 | <div id="centerErrorLabel" class="error-label center-error hidden">Could not generate QR code. Please try again.</div> 23 | </div> 24 | 25 | <!-- Input field matching Chromium's textfield with accessibility --> 26 | <input 27 | type="text" 28 | id="urlInput" 29 | placeholder="Enter URL or text" 30 | aria-label="URL or text to encode" 31 | maxlength="2000" 32 | /> 33 | 34 | <!-- Bottom error for input too long, matching Chromium's behavior --> 35 | <div id="bottomErrorLabel" class="error-label bottom-error hidden">Input is too long. Please shorten the text to 2000 characters or less.</div> 36 | 37 | <!-- Button container with tooltip and buttons, matching Chromium's layout --> 38 | <div class="button-container"> 39 | <span class="tooltip" title="Generates a QR code for the entered text." 40 | >ⓘ</span 41 | > 42 | <div class="spacer"></div> 43 | <button id="copyButton" disabled>Copy</button> 44 | <button id="downloadButton" disabled>Download</button> 45 | </div> 46 | </div> 47 | <script type="module" src="./src/app.js"></script> 48 | </body> 49 | </html> 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - esbuild 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liuliangsir/chromium-style-qrcode-generator-with-wasm/553f3238b0978ccaf2b89fd2ea1d36a5dbe8a673/public/favicon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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<u8>, // 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<QrCodeResult, JsValue> { 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<QrCodeResult, JsValue> { 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vite.constant.js: -------------------------------------------------------------------------------- 1 | export const BASE = '/chromium-style-qrcode-generator-with-wasm/'; 2 | -------------------------------------------------------------------------------- /vite.helper.js: -------------------------------------------------------------------------------- 1 | export const isProd = (mode) => mode === 'production'; 2 | --------------------------------------------------------------------------------