The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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 | 


--------------------------------------------------------------------------------