Skip to content

Instantly share code, notes, and snippets.

@stanaka
Created September 2, 2026 00:48
Show Gist options
  • Select an option

  • Save stanaka/7023521711e6a55a9b4577ddd9ceaefa to your computer and use it in GitHub Desktop.

Select an option

Save stanaka/7023521711e6a55a9b4577ddd9ceaefa to your computer and use it in GitHub Desktop.
One HTML file to generate a QR code without any dependencies
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="A private, dependency-free QR code generator that runs entirely in your browser.">
<title>Offline QR Code Generator</title>
<style>
:root {
color-scheme: light;
--ink: #17221d;
--muted: #647069;
--paper: #f4f0e7;
--card: #fffdf8;
--line: #d8d4c9;
--accent: #176b4d;
--accent-hover: #10543c;
--accent-soft: #e2f0e8;
--danger: #a43a35;
--shadow: 0 24px 60px rgba(31, 48, 39, 0.12);
}
* {
box-sizing: border-box;
}
html {
min-height: 100%;
background: var(--paper);
}
body {
min-height: 100vh;
margin: 0;
color: var(--ink);
background:
linear-gradient(rgba(23, 107, 77, 0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(23, 107, 77, 0.035) 1px, transparent 1px),
var(--paper);
background-size: 28px 28px;
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
}
button,
textarea {
font: inherit;
}
button {
-webkit-tap-highlight-color: transparent;
}
.page {
width: min(1080px, calc(100% - 32px));
margin: 0 auto;
padding: 48px 0;
}
.masthead {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
margin-bottom: 22px;
}
.eyebrow {
margin: 0 0 8px;
color: var(--accent);
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
}
h1 {
max-width: 680px;
margin: 0;
font-family: Georgia, "Times New Roman", serif;
font-size: clamp(2.25rem, 5vw, 4.35rem);
font-weight: 500;
letter-spacing: -0.045em;
line-height: 0.98;
}
.privacy-note {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
gap: 9px;
margin: 0 0 5px;
padding: 9px 13px;
border: 1px solid #bdd5c9;
border-radius: 999px;
color: #235e47;
background: rgba(255, 253, 248, 0.72);
font-size: 0.82rem;
font-weight: 700;
}
.privacy-note::before {
width: 8px;
height: 8px;
border-radius: 50%;
background: #2a9a6c;
box-shadow: 0 0 0 4px rgba(42, 154, 108, 0.14);
content: "";
}
.workspace {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.78fr);
min-height: 560px;
overflow: hidden;
border: 1px solid var(--line);
border-radius: 24px;
background: var(--card);
box-shadow: var(--shadow);
}
.editor,
.preview {
padding: clamp(26px, 4vw, 46px);
}
.editor {
display: flex;
flex-direction: column;
border-right: 1px solid var(--line);
}
.section-number {
display: inline-grid;
width: 32px;
height: 32px;
margin-bottom: 24px;
place-items: center;
border: 1px solid var(--line);
border-radius: 50%;
color: var(--muted);
font-size: 0.78rem;
font-weight: 800;
}
label {
display: block;
margin-bottom: 10px;
font-size: 1.05rem;
font-weight: 800;
}
.label-note {
display: block;
margin-top: 3px;
color: var(--muted);
font-size: 0.84rem;
font-weight: 500;
}
textarea {
width: 100%;
min-height: 230px;
resize: vertical;
padding: 18px;
border: 1px solid #c8c6bd;
border-radius: 14px;
outline: none;
color: var(--ink);
background: #fff;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.95rem;
line-height: 1.6;
transition: border-color 160ms ease, box-shadow 160ms ease;
}
textarea:hover {
border-color: #a8aca6;
}
textarea:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 4px rgba(23, 107, 77, 0.14);
}
textarea[aria-invalid="true"] {
border-color: var(--danger);
}
.input-meta {
display: flex;
min-height: 32px;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin: 10px 2px 20px;
color: var(--muted);
font-size: 0.78rem;
}
.status {
text-align: right;
}
.status.error {
color: var(--danger);
font-weight: 700;
}
.wifi-guide {
margin: 0 0 22px;
border: 1px solid #c8d8cf;
border-radius: 14px;
background: #f4f8f5;
}
.wifi-guide summary {
padding: 13px 16px;
cursor: pointer;
color: #235e47;
font-size: 0.86rem;
font-weight: 800;
list-style-position: inside;
}
.wifi-guide summary:focus-visible {
border-radius: 13px;
outline: 3px solid rgba(23, 107, 77, 0.22);
outline-offset: 2px;
}
.wifi-guide-body {
padding: 0 16px 16px;
color: var(--muted);
font-size: 0.8rem;
}
.wifi-guide-body p {
margin: 0;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
.wifi-builder {
min-width: 0;
margin: 13px 0 10px;
padding: 0;
border: 0;
}
.wifi-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 11px;
}
.wifi-field {
min-width: 0;
}
.wifi-field:last-child {
grid-column: 1 / -1;
}
.wifi-field label {
margin: 0 0 5px;
color: var(--ink);
font-size: 0.72rem;
font-weight: 800;
}
.wifi-field input,
.wifi-field select {
width: 100%;
min-height: 42px;
padding: 9px 11px;
border: 1px solid #bfcac3;
border-radius: 9px;
outline: none;
color: var(--ink);
background: #fff;
font-size: 0.82rem;
transition: border-color 160ms ease, box-shadow 160ms ease;
}
.wifi-field input:focus-visible,
.wifi-field select:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(23, 107, 77, 0.13);
}
.wifi-field input[aria-invalid="true"] {
border-color: var(--danger);
}
.wifi-field input:disabled {
cursor: not-allowed;
color: #8a938e;
background: #e9eeeb;
}
.wifi-options {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 12px;
}
.wifi-checkbox {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0;
color: var(--ink);
font-size: 0.78rem;
font-weight: 700;
}
.wifi-checkbox input {
width: 17px;
height: 17px;
margin: 0;
accent-color: var(--accent);
}
.wifi-build {
min-height: 42px;
padding: 9px 14px;
color: #fff;
background: var(--accent);
font-size: 0.78rem;
}
.wifi-build:hover {
background: var(--accent-hover);
}
.wifi-status {
min-height: 18px;
margin-top: 7px !important;
color: var(--muted);
font-size: 0.74rem;
}
.wifi-status.error {
color: var(--danger);
font-weight: 700;
}
.wifi-caution {
padding-top: 10px;
border-top: 1px solid #d6dfd9;
}
.wifi-guide a {
color: var(--accent);
font-weight: 700;
text-underline-offset: 2px;
}
.actions {
display: flex;
gap: 10px;
margin-top: auto;
}
button {
min-height: 46px;
padding: 11px 17px;
border: 1px solid transparent;
border-radius: 11px;
cursor: pointer;
font-weight: 800;
transition: background 160ms ease, border-color 160ms ease, color 160ms ease, transform 100ms ease;
}
button:active:not(:disabled) {
transform: translateY(1px);
}
button:focus-visible {
outline: 3px solid rgba(23, 107, 77, 0.28);
outline-offset: 2px;
}
.primary {
flex: 1;
color: #fff;
background: var(--accent);
}
.primary:hover {
background: var(--accent-hover);
}
.secondary {
color: var(--ink);
border-color: var(--line);
background: transparent;
}
.secondary:hover:not(:disabled) {
border-color: #a8aca6;
background: #f7f5ef;
}
button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #eef3ee;
}
.qr-frame {
display: grid;
width: min(100%, 390px);
aspect-ratio: 1;
place-items: center;
padding: clamp(16px, 4vw, 28px);
border: 1px solid #d4ddd7;
border-radius: 20px;
background: #fff;
box-shadow: 0 14px 35px rgba(36, 61, 48, 0.1);
}
canvas {
display: block;
width: 100%;
height: auto;
image-rendering: pixelated;
}
.output-meta {
min-height: 46px;
margin: 20px 0 16px;
color: var(--muted);
font-size: 0.82rem;
text-align: center;
}
.output-meta strong {
display: block;
margin-bottom: 2px;
color: var(--ink);
font-size: 0.9rem;
}
.download {
width: min(100%, 390px);
color: var(--accent);
border-color: #9fc5b3;
background: var(--accent-soft);
}
.download:hover:not(:disabled) {
border-color: #73a98e;
background: #d4e9de;
}
footer {
display: flex;
justify-content: space-between;
gap: 20px;
margin-top: 18px;
color: var(--muted);
font-size: 0.76rem;
}
footer p {
margin: 0;
}
@media (max-width: 760px) {
.page {
width: min(100% - 20px, 620px);
padding: 28px 0;
}
.masthead {
display: block;
}
.privacy-note {
margin-top: 18px;
}
.workspace {
grid-template-columns: 1fr;
}
.editor {
border-right: 0;
border-bottom: 1px solid var(--line);
}
.preview {
min-height: 480px;
}
footer {
display: block;
}
footer p + p {
margin-top: 4px;
}
}
@media (max-width: 430px) {
.editor,
.preview {
padding: 24px 18px;
}
h1 {
font-size: 2.45rem;
}
.actions {
flex-wrap: wrap;
}
.wifi-grid {
grid-template-columns: 1fr;
}
.wifi-field:last-child {
grid-column: auto;
}
.wifi-options {
align-items: stretch;
flex-direction: column;
}
.wifi-build {
width: 100%;
}
.primary {
flex-basis: 100%;
}
.secondary {
flex: 1;
}
.preview {
min-height: 420px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}
</style>
</head>
<body>
<main class="page">
<header class="masthead">
<div>
<p class="eyebrow">Private by design</p>
<h1>Make a QR code.<br>Keep your data.</h1>
</div>
<p class="privacy-note">Works entirely offline</p>
</header>
<section class="workspace" aria-label="QR code generator">
<form class="editor" id="qr-form" novalidate>
<span class="section-number" aria-hidden="true">01</span>
<label for="qr-input">
Text or URL
<span class="label-note">Paste a link, message, contact detail, or any UTF-8 text.</span>
</label>
<textarea id="qr-input" spellcheck="false" aria-describedby="input-help input-status">https://example.com/hello</textarea>
<div class="input-meta">
<span id="input-help">Press Ctrl/⌘ + Enter to generate</span>
<span class="status" id="input-status" role="status" aria-live="polite">25 bytes</span>
</div>
<details class="wifi-guide" open>
<summary>Making a Wi-Fi QR code?</summary>
<div class="wifi-guide-body">
<p>Enter the network settings and this page will build the Wi-Fi payload for you.</p>
<fieldset class="wifi-builder" id="wifi-builder">
<legend class="visually-hidden">Wi-Fi network settings</legend>
<div class="wifi-grid">
<div class="wifi-field">
<label for="wifi-ssid">Network name (SSID)</label>
<input id="wifi-ssid" type="text" autocomplete="off" placeholder="MyNetwork">
</div>
<div class="wifi-field">
<label for="wifi-security">Security</label>
<select id="wifi-security">
<option value="WPA">WPA / WPA2</option>
<option value="WEP">WEP</option>
<option value="nopass">No password</option>
</select>
</div>
<div class="wifi-field">
<label for="wifi-password">Password</label>
<input id="wifi-password" type="password" autocomplete="off" placeholder="Network password">
</div>
</div>
<div class="wifi-options">
<label class="wifi-checkbox" for="wifi-hidden">
<input id="wifi-hidden" type="checkbox">
Hidden network
</label>
<button class="wifi-build" id="wifi-build-button" type="button">Build &amp; Generate</button>
</div>
<p class="wifi-status" id="wifi-status" role="status" aria-live="polite"></p>
</fieldset>
<p>Inside the SSID or password, prefix <code>\</code>, <code>;</code>, <code>,</code>, <code>&quot;</code>, and <code>:</code> with a backslash. Anyone who scans the code can recover its password, so prefer a guest network.</p>
<p class="wifi-caution"><a href="https://dev.to/mycko22/the-technical-guide-to-wifi-qr-codes-implementation-security-and-best-practices-2f2n" target="_blank" rel="noreferrer">Read the Wi-Fi QR guide</a></p>
</div>
</details>
<div class="actions">
<button class="primary" type="submit">Generate QR Code</button>
<button class="secondary" id="clear-button" type="button">Clear</button>
</div>
</form>
<section class="preview" aria-label="Generated QR code">
<div class="qr-frame">
<canvas id="qr-canvas" width="348" height="348" role="img" aria-label="QR code for https://example.com/hello">
Your browser does not support canvas.
</canvas>
</div>
<p class="output-meta" id="output-meta" aria-live="polite"></p>
<button class="download" id="download-button" type="button" disabled>Download PNG</button>
</section>
</section>
<footer>
<p>No uploads. No tracking. No external libraries.</p>
<p>Error correction: Medium (M)</p>
</footer>
</main>
<script>
"use strict";
/*
* QR Code Model 2 encoder, adapted from Project Nayuki's QR Code generator.
* Copyright (c) Project Nayuki. Released under the MIT License.
* https://www.nayuki.io/page/qr-code-generator-library
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* This page uses byte mode and error correction level M.
*/
class BitBuffer {
constructor() {
this.bits = [];
}
append(value, length) {
if (length < 0 || value >>> length !== 0) {
throw new RangeError("Value does not fit in the requested bit length");
}
for (let i = length - 1; i >= 0; i--) {
this.bits.push((value >>> i) & 1);
}
}
get length() {
return this.bits.length;
}
}
class QrCode {
static ECC_CODEWORDS_PER_BLOCK = [
-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26,
30, 22, 22, 24, 24, 28, 28, 26, 26, 26,
28, 28, 28, 28, 28, 28, 28, 28, 28, 28,
28, 28, 28, 28, 28, 28, 28, 28, 28, 28
];
static NUM_ERROR_CORRECTION_BLOCKS = [
-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5,
5, 8, 9, 9, 10, 10, 11, 13, 14, 16,
17, 17, 18, 20, 21, 23, 25, 26, 28, 29,
31, 33, 35, 37, 38, 40, 43, 45, 47, 49
];
static encodeText(text) {
const data = Array.from(new TextEncoder().encode(text));
let version = 1;
for (; version <= 40; version++) {
const countBits = version <= 9 ? 8 : 16;
if (data.length >= 2 ** countBits) continue;
const usedBits = 4 + countBits + data.length * 8;
if (usedBits <= QrCode.getDataCodewords(version) * 8) break;
}
if (version > 40) {
throw new RangeError("That text is too long. Medium error correction supports up to 2,331 UTF-8 bytes.");
}
const dataCodewords = QrCode.makeDataCodewords(data, version);
const allCodewords = QrCode.addErrorCorrection(dataCodewords, version);
return new QrCode(version, allCodewords);
}
constructor(version, codewords) {
this.version = version;
this.size = version * 4 + 17;
this.modules = Array.from({ length: this.size }, () => Array(this.size).fill(false));
this.isFunction = Array.from({ length: this.size }, () => Array(this.size).fill(false));
this.drawFunctionPatterns();
this.drawCodewords(codewords);
let bestMask = 0;
let lowestPenalty = Infinity;
for (let mask = 0; mask < 8; mask++) {
this.applyMask(mask);
this.drawFormatBits(mask);
const penalty = this.getPenaltyScore();
if (penalty < lowestPenalty) {
bestMask = mask;
lowestPenalty = penalty;
}
this.applyMask(mask);
}
this.applyMask(bestMask);
this.drawFormatBits(bestMask);
this.mask = bestMask;
}
static makeDataCodewords(data, version) {
const capacity = QrCode.getDataCodewords(version) * 8;
const bits = new BitBuffer();
bits.append(0x4, 4); // Byte mode.
bits.append(data.length, version <= 9 ? 8 : 16);
for (const value of data) bits.append(value, 8);
bits.append(0, Math.min(4, capacity - bits.length));
bits.append(0, (8 - bits.length % 8) % 8);
const result = [];
for (let i = 0; i < bits.length; i += 8) {
let value = 0;
for (let j = 0; j < 8; j++) value = (value << 1) | bits.bits[i + j];
result.push(value);
}
for (let pad = 0xEC; result.length < capacity / 8; pad ^= 0xEC ^ 0x11) {
result.push(pad);
}
return result;
}
static addErrorCorrection(data, version) {
const blockCount = QrCode.NUM_ERROR_CORRECTION_BLOCKS[version];
const eccLength = QrCode.ECC_CODEWORDS_PER_BLOCK[version];
const rawCodewords = Math.floor(QrCode.getNumRawDataModules(version) / 8);
const shortBlockCount = blockCount - rawCodewords % blockCount;
const shortBlockLength = Math.floor(rawCodewords / blockCount);
const divisor = QrCode.reedSolomonDivisor(eccLength);
const blocks = [];
const eccBlocks = [];
let offset = 0;
for (let block = 0; block < blockCount; block++) {
const dataLength = shortBlockLength - eccLength + (block < shortBlockCount ? 0 : 1);
const part = data.slice(offset, offset + dataLength);
offset += dataLength;
blocks.push(part);
eccBlocks.push(QrCode.reedSolomonRemainder(part, divisor));
}
const result = [];
const longestDataBlock = shortBlockLength - eccLength + 1;
for (let i = 0; i < longestDataBlock; i++) {
for (const block of blocks) {
if (i < block.length) result.push(block[i]);
}
}
for (let i = 0; i < eccLength; i++) {
for (const block of eccBlocks) result.push(block[i]);
}
if (result.length !== rawCodewords) throw new Error("QR interleaving failed");
return result;
}
static reedSolomonDivisor(degree) {
const result = Array(degree).fill(0);
result[degree - 1] = 1;
let root = 1;
for (let i = 0; i < degree; i++) {
for (let j = 0; j < result.length; j++) {
result[j] = QrCode.galoisMultiply(result[j], root);
if (j + 1 < result.length) result[j] ^= result[j + 1];
}
root = QrCode.galoisMultiply(root, 0x02);
}
return result;
}
static reedSolomonRemainder(data, divisor) {
const result = Array(divisor.length).fill(0);
for (const value of data) {
const factor = value ^ result.shift();
result.push(0);
for (let i = 0; i < result.length; i++) {
result[i] ^= QrCode.galoisMultiply(divisor[i], factor);
}
}
return result;
}
static galoisMultiply(x, y) {
let result = 0;
for (let i = 7; i >= 0; i--) {
result = (result << 1) ^ ((result >>> 7) * 0x11D);
result ^= ((y >>> i) & 1) * x;
}
return result;
}
static getDataCodewords(version) {
return Math.floor(QrCode.getNumRawDataModules(version) / 8)
- QrCode.ECC_CODEWORDS_PER_BLOCK[version] * QrCode.NUM_ERROR_CORRECTION_BLOCKS[version];
}
static getNumRawDataModules(version) {
let result = (16 * version + 128) * version + 64;
if (version >= 2) {
const alignmentCount = Math.floor(version / 7) + 2;
result -= (25 * alignmentCount - 10) * alignmentCount - 55;
if (version >= 7) result -= 36;
}
return result;
}
drawFunctionPatterns() {
for (let i = 0; i < this.size; i++) {
this.setFunctionModule(6, i, i % 2 === 0);
this.setFunctionModule(i, 6, i % 2 === 0);
}
this.drawFinderPattern(3, 3);
this.drawFinderPattern(this.size - 4, 3);
this.drawFinderPattern(3, this.size - 4);
const positions = this.getAlignmentPatternPositions();
const last = positions.length - 1;
for (let i = 0; i < positions.length; i++) {
for (let j = 0; j < positions.length; j++) {
if ((i === 0 && j === 0) || (i === 0 && j === last) || (i === last && j === 0)) continue;
this.drawAlignmentPattern(positions[i], positions[j]);
}
}
this.drawFormatBits(0);
this.drawVersion();
}
drawFinderPattern(centerX, centerY) {
for (let dy = -4; dy <= 4; dy++) {
for (let dx = -4; dx <= 4; dx++) {
const x = centerX + dx;
const y = centerY + dy;
if (x < 0 || y < 0 || x >= this.size || y >= this.size) continue;
const distance = Math.max(Math.abs(dx), Math.abs(dy));
this.setFunctionModule(x, y, distance !== 2 && distance !== 4);
}
}
}
drawAlignmentPattern(centerX, centerY) {
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
this.setFunctionModule(centerX + dx, centerY + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
}
}
}
getAlignmentPatternPositions() {
if (this.version === 1) return [];
const count = Math.floor(this.version / 7) + 2;
const step = Math.floor((this.version * 8 + count * 3 + 5) / (count * 4 - 4)) * 2;
const result = [6];
for (let position = this.size - 7; result.length < count; position -= step) {
result.splice(1, 0, position);
}
return result;
}
drawFormatBits(mask) {
const data = mask; // Error correction M has format bits 00.
let remainder = data;
for (let i = 0; i < 10; i++) remainder = (remainder << 1) ^ ((remainder >>> 9) * 0x537);
const bits = ((data << 10) | remainder) ^ 0x5412;
const bit = index => ((bits >>> index) & 1) !== 0;
for (let i = 0; i <= 5; i++) this.setFunctionModule(8, i, bit(i));
this.setFunctionModule(8, 7, bit(6));
this.setFunctionModule(8, 8, bit(7));
this.setFunctionModule(7, 8, bit(8));
for (let i = 9; i < 15; i++) this.setFunctionModule(14 - i, 8, bit(i));
for (let i = 0; i < 8; i++) this.setFunctionModule(this.size - 1 - i, 8, bit(i));
for (let i = 8; i < 15; i++) this.setFunctionModule(8, this.size - 15 + i, bit(i));
this.setFunctionModule(8, this.size - 8, true);
}
drawVersion() {
if (this.version < 7) return;
let remainder = this.version;
for (let i = 0; i < 12; i++) remainder = (remainder << 1) ^ ((remainder >>> 11) * 0x1F25);
const bits = (this.version << 12) | remainder;
for (let i = 0; i < 18; i++) {
const value = ((bits >>> i) & 1) !== 0;
const a = this.size - 11 + i % 3;
const b = Math.floor(i / 3);
this.setFunctionModule(a, b, value);
this.setFunctionModule(b, a, value);
}
}
setFunctionModule(x, y, dark) {
this.modules[y][x] = dark;
this.isFunction[y][x] = true;
}
drawCodewords(codewords) {
let bitIndex = 0;
for (let right = this.size - 1; right >= 1; right -= 2) {
if (right === 6) right = 5;
for (let vertical = 0; vertical < this.size; vertical++) {
const upward = ((right + 1) & 2) === 0;
const y = upward ? this.size - 1 - vertical : vertical;
for (let column = 0; column < 2; column++) {
const x = right - column;
if (this.isFunction[y][x] || bitIndex >= codewords.length * 8) continue;
this.modules[y][x] = ((codewords[bitIndex >>> 3] >>> (7 - (bitIndex & 7))) & 1) !== 0;
bitIndex++;
}
}
}
if (bitIndex !== codewords.length * 8) throw new Error("QR data placement failed");
}
applyMask(mask) {
for (let y = 0; y < this.size; y++) {
for (let x = 0; x < this.size; x++) {
if (!this.isFunction[y][x] && QrCode.maskBit(mask, x, y)) {
this.modules[y][x] = !this.modules[y][x];
}
}
}
}
static maskBit(mask, x, y) {
switch (mask) {
case 0: return (x + y) % 2 === 0;
case 1: return y % 2 === 0;
case 2: return x % 3 === 0;
case 3: return (x + y) % 3 === 0;
case 4: return (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
case 5: return x * y % 2 + x * y % 3 === 0;
case 6: return (x * y % 2 + x * y % 3) % 2 === 0;
case 7: return ((x + y) % 2 + x * y % 3) % 2 === 0;
default: throw new RangeError("Invalid QR mask");
}
}
getPenaltyScore() {
let score = 0;
const scoreRuns = line => {
let total = 0;
let runColor = line[0];
let runLength = 1;
for (let i = 1; i <= line.length; i++) {
if (i < line.length && line[i] === runColor) {
runLength++;
} else {
if (runLength >= 5) total += 3 + runLength - 5;
if (i < line.length) {
runColor = line[i];
runLength = 1;
}
}
}
return total;
};
const finderA = "00001011101";
const finderB = "10111010000";
const scoreFinderPatterns = line => {
const text = line.map(value => value ? "1" : "0").join("");
let total = 0;
for (let i = 0; i <= text.length - 11; i++) {
const window = text.slice(i, i + 11);
if (window === finderA || window === finderB) total += 40;
}
return total;
};
for (let y = 0; y < this.size; y++) {
score += scoreRuns(this.modules[y]);
score += scoreFinderPatterns(this.modules[y]);
}
for (let x = 0; x < this.size; x++) {
const column = this.modules.map(row => row[x]);
score += scoreRuns(column);
score += scoreFinderPatterns(column);
}
for (let y = 0; y < this.size - 1; y++) {
for (let x = 0; x < this.size - 1; x++) {
const value = this.modules[y][x];
if (this.modules[y][x + 1] === value
&& this.modules[y + 1][x] === value
&& this.modules[y + 1][x + 1] === value) {
score += 3;
}
}
}
let dark = 0;
for (const row of this.modules) for (const value of row) dark += value ? 1 : 0;
const total = this.size * this.size;
score += (Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1) * 10;
return score;
}
}
const input = document.getElementById("qr-input");
const form = document.getElementById("qr-form");
const clearButton = document.getElementById("clear-button");
const downloadButton = document.getElementById("download-button");
const canvas = document.getElementById("qr-canvas");
const inputStatus = document.getElementById("input-status");
const outputMeta = document.getElementById("output-meta");
const wifiBuilder = document.getElementById("wifi-builder");
const wifiSsid = document.getElementById("wifi-ssid");
const wifiSecurity = document.getElementById("wifi-security");
const wifiPassword = document.getElementById("wifi-password");
const wifiHidden = document.getElementById("wifi-hidden");
const wifiBuildButton = document.getElementById("wifi-build-button");
const wifiStatus = document.getElementById("wifi-status");
const context = canvas.getContext("2d");
const MAX_BYTES = 2331;
let currentQr = null;
function getByteLength(text) {
return new TextEncoder().encode(text).length;
}
function updateInputStatus(message = "", isError = false) {
const bytes = getByteLength(input.value);
inputStatus.textContent = message || `${bytes.toLocaleString()} ${bytes === 1 ? "byte" : "bytes"}`;
inputStatus.classList.toggle("error", isError);
input.setAttribute("aria-invalid", String(isError));
}
function setWifiStatus(message = "", isError = false) {
wifiStatus.textContent = message;
wifiStatus.classList.toggle("error", isError);
}
function escapeWifiValue(value) {
return value.replace(/[\\;,":]/g, "\\$&");
}
function updateWifiPasswordState() {
const isOpen = wifiSecurity.value === "nopass";
wifiPassword.disabled = isOpen;
wifiPassword.placeholder = isOpen ? "Not needed for an open network" : "Network password";
wifiPassword.setAttribute("aria-invalid", "false");
setWifiStatus();
}
function buildWifiQr() {
const ssid = wifiSsid.value;
const security = wifiSecurity.value;
const isOpen = security === "nopass";
const password = wifiPassword.value;
wifiSsid.setAttribute("aria-invalid", String(ssid.length === 0));
wifiPassword.setAttribute("aria-invalid", String(!isOpen && password.length === 0));
if (ssid.length === 0) {
setWifiStatus("Enter the Wi-Fi network name.", true);
wifiSsid.focus();
return;
}
if (!isOpen && password.length === 0) {
setWifiStatus("Enter the Wi-Fi password, or choose No password.", true);
wifiPassword.focus();
return;
}
const fields = [`WIFI:T:${security}`, `S:${escapeWifiValue(ssid)}`];
if (!isOpen) fields.push(`P:${escapeWifiValue(password)}`);
fields.push(`H:${wifiHidden.checked}`);
input.value = `${fields.join(";")};;`;
setWifiStatus("Wi-Fi payload built and QR code generated.");
generate();
}
function clearCanvas() {
canvas.width = 348;
canvas.height = 348;
context.fillStyle = "#ffffff";
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = "#dfe7e2";
const cell = 12;
for (let y = 3; y < 26; y++) {
for (let x = 3; x < 26; x++) {
if ((x * 3 + y * 5) % 7 === 0) context.fillRect(x * cell, y * cell, cell, cell);
}
}
canvas.setAttribute("aria-label", "No QR code generated");
}
function renderQr(qr, label) {
const quietZone = 4;
const moduleCount = qr.size + quietZone * 2;
const scale = Math.max(4, Math.floor(1024 / moduleCount));
const imageSize = moduleCount * scale;
canvas.width = imageSize;
canvas.height = imageSize;
context.imageSmoothingEnabled = false;
context.fillStyle = "#ffffff";
context.fillRect(0, 0, imageSize, imageSize);
context.fillStyle = "#000000";
for (let y = 0; y < qr.size; y++) {
for (let x = 0; x < qr.size; x++) {
if (qr.modules[y][x]) {
context.fillRect((x + quietZone) * scale, (y + quietZone) * scale, scale, scale);
}
}
}
canvas.setAttribute("aria-label", `QR code for ${label}`);
}
function generate() {
const text = input.value;
if (text.length === 0) {
currentQr = null;
clearCanvas();
updateInputStatus("Enter some text to generate a QR code.", true);
outputMeta.innerHTML = "";
downloadButton.disabled = true;
return;
}
try {
const qr = QrCode.encodeText(text);
currentQr = qr;
const label = text.startsWith("WIFI:")
? "Wi-Fi network settings"
: text.length > 80 ? `${text.slice(0, 77)}…` : text;
renderQr(qr, label);
updateInputStatus();
outputMeta.innerHTML = `<strong>Ready to scan</strong>Version ${qr.version} · ${qr.size} × ${qr.size} modules · Mask ${qr.mask}`;
downloadButton.disabled = false;
} catch (error) {
currentQr = null;
clearCanvas();
updateInputStatus(error instanceof RangeError ? error.message : "Unable to generate this QR code.", true);
outputMeta.innerHTML = "";
downloadButton.disabled = true;
if (!(error instanceof RangeError)) console.error(error);
}
}
form.addEventListener("submit", event => {
event.preventDefault();
generate();
});
input.addEventListener("input", () => {
const bytes = getByteLength(input.value);
if (bytes > MAX_BYTES) {
updateInputStatus(`${bytes.toLocaleString()} bytes · exceeds the ${MAX_BYTES.toLocaleString()}-byte limit`, true);
} else {
updateInputStatus();
}
});
input.addEventListener("keydown", event => {
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
generate();
}
});
wifiBuildButton.addEventListener("click", buildWifiQr);
wifiBuilder.addEventListener("keydown", event => {
if (event.key === "Enter" && event.target.matches("input:not([type=checkbox]), select")) {
event.preventDefault();
buildWifiQr();
}
});
wifiSecurity.addEventListener("change", updateWifiPasswordState);
wifiSsid.addEventListener("input", () => {
wifiSsid.setAttribute("aria-invalid", "false");
setWifiStatus();
});
wifiPassword.addEventListener("input", () => {
wifiPassword.setAttribute("aria-invalid", "false");
setWifiStatus();
});
clearButton.addEventListener("click", () => {
input.value = "";
currentQr = null;
clearCanvas();
updateInputStatus();
outputMeta.innerHTML = "";
downloadButton.disabled = true;
wifiSsid.value = "";
wifiSecurity.value = "WPA";
wifiPassword.value = "";
wifiHidden.checked = false;
wifiSsid.setAttribute("aria-invalid", "false");
updateWifiPasswordState();
input.focus();
});
downloadButton.addEventListener("click", () => {
if (!currentQr) return;
const link = document.createElement("a");
link.download = "qr-code.png";
link.href = canvas.toDataURL("image/png");
link.click();
});
updateWifiPasswordState();
generate();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment