feat: add QR code generator for zine CTAs
- generateQRCode() - create QR codes with punk-zine colors - generateQRCodeDataURL() - for embedding in HTML/canvas - generateZineQRCodes() - batch generate multiple QR codes - generateUndernetQRCodes() - preset for Undernet zine - CLI support: node src/qrcode.mjs <url> [options] - Default colors: black QR on green background 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6b848ccd03
commit
10d9459990
|
|
@ -9,6 +9,7 @@
|
|||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"qrcode": "^1.5.4",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -490,6 +491,77 @@
|
|||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
|
|
@ -499,6 +571,147 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qrcode": {
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dijkstrajs": "^1.0.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"qrcode": "bin/qrcode"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
|
|
@ -511,6 +724,12 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
|
|
@ -555,12 +774,99 @@
|
|||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
"author": "Jeff Emmett",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"qrcode": "^1.5.4",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@ export {
|
|||
getImagePrompt,
|
||||
getIdeationPrompt
|
||||
} from './prompts.mjs';
|
||||
export {
|
||||
generateQRCode,
|
||||
generateQRCodeDataURL,
|
||||
generateZineQRCodes,
|
||||
generateUndernetQRCodes
|
||||
} from './qrcode.mjs';
|
||||
|
||||
/**
|
||||
* Zine configuration defaults
|
||||
|
|
|
|||
|
|
@ -0,0 +1,253 @@
|
|||
/**
|
||||
* MycroZine QR Code Generator
|
||||
*
|
||||
* Generates QR codes for zine CTAs, URLs, and other content.
|
||||
* Designed to fit the punk-zine aesthetic with customizable colors.
|
||||
*/
|
||||
|
||||
import QRCode from 'qrcode';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* Default QR code options for punk-zine style
|
||||
*/
|
||||
const DEFAULT_OPTIONS = {
|
||||
width: 300,
|
||||
margin: 2,
|
||||
color: {
|
||||
dark: '#000000', // QR code color
|
||||
light: '#00ff00' // Background color (punk green)
|
||||
},
|
||||
errorCorrectionLevel: 'M'
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a QR code image from a URL or text
|
||||
*
|
||||
* @param {Object} options - QR code options
|
||||
* @param {string} options.data - URL or text to encode
|
||||
* @param {string} [options.outputPath] - Output file path (PNG)
|
||||
* @param {number} [options.width=300] - Width in pixels
|
||||
* @param {string} [options.darkColor='#000000'] - QR code color
|
||||
* @param {string} [options.lightColor='#00ff00'] - Background color
|
||||
* @param {string} [options.errorCorrectionLevel='M'] - L, M, Q, or H
|
||||
* @returns {Promise<string|Buffer>} - File path if outputPath provided, otherwise Buffer
|
||||
*/
|
||||
export async function generateQRCode(options) {
|
||||
const {
|
||||
data,
|
||||
outputPath,
|
||||
width = DEFAULT_OPTIONS.width,
|
||||
darkColor = DEFAULT_OPTIONS.color.dark,
|
||||
lightColor = DEFAULT_OPTIONS.color.light,
|
||||
errorCorrectionLevel = DEFAULT_OPTIONS.errorCorrectionLevel
|
||||
} = options;
|
||||
|
||||
if (!data) {
|
||||
throw new Error('QR code data is required');
|
||||
}
|
||||
|
||||
const qrOptions = {
|
||||
width,
|
||||
margin: DEFAULT_OPTIONS.margin,
|
||||
color: {
|
||||
dark: darkColor,
|
||||
light: lightColor
|
||||
},
|
||||
errorCorrectionLevel
|
||||
};
|
||||
|
||||
if (outputPath) {
|
||||
// Ensure output directory exists
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
|
||||
// Generate and save to file
|
||||
await QRCode.toFile(outputPath, data, qrOptions);
|
||||
console.log(`QR code generated: ${outputPath}`);
|
||||
return outputPath;
|
||||
} else {
|
||||
// Return as buffer
|
||||
return QRCode.toBuffer(data, qrOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a QR code as a data URL (for embedding in HTML/canvas)
|
||||
*
|
||||
* @param {string} data - URL or text to encode
|
||||
* @param {Object} [options] - QR code options
|
||||
* @returns {Promise<string>} - Data URL string
|
||||
*/
|
||||
export async function generateQRCodeDataURL(data, options = {}) {
|
||||
const {
|
||||
width = DEFAULT_OPTIONS.width,
|
||||
darkColor = DEFAULT_OPTIONS.color.dark,
|
||||
lightColor = DEFAULT_OPTIONS.color.light
|
||||
} = options;
|
||||
|
||||
return QRCode.toDataURL(data, {
|
||||
width,
|
||||
margin: DEFAULT_OPTIONS.margin,
|
||||
color: {
|
||||
dark: darkColor,
|
||||
light: lightColor
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate multiple QR codes for a zine's CTA page
|
||||
*
|
||||
* @param {Object[]} items - Array of QR code items
|
||||
* @param {string} items[].data - URL or text to encode
|
||||
* @param {string} items[].label - Label for the QR code
|
||||
* @param {string} items[].filename - Output filename (without extension)
|
||||
* @param {string} [outputDir] - Output directory
|
||||
* @returns {Promise<Object[]>} - Array of generated QR code info
|
||||
*/
|
||||
export async function generateZineQRCodes(items, outputDir = path.join(__dirname, '..', 'output', 'qrcodes')) {
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const item of items) {
|
||||
const outputPath = path.join(outputDir, `${item.filename}.png`);
|
||||
await generateQRCode({
|
||||
data: item.data,
|
||||
outputPath
|
||||
});
|
||||
results.push({
|
||||
label: item.label,
|
||||
data: item.data,
|
||||
path: outputPath
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Generated ${results.length} QR codes in ${outputDir}`);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate standard Undernet zine QR codes
|
||||
*/
|
||||
export async function generateUndernetQRCodes(outputDir) {
|
||||
const items = [
|
||||
{
|
||||
data: 'https://undernet.earth',
|
||||
label: 'UNDERNET.EARTH',
|
||||
filename: 'qr_undernet_earth'
|
||||
},
|
||||
{
|
||||
data: 'https://github.com/Jeff-Emmett/mycro-zine',
|
||||
label: 'PRINT MORE ZINES',
|
||||
filename: 'qr_print_zines'
|
||||
},
|
||||
{
|
||||
data: 'https://undernet.earth/template',
|
||||
label: 'BLANK TEMPLATE',
|
||||
filename: 'qr_blank_template'
|
||||
}
|
||||
];
|
||||
|
||||
return generateZineQRCodes(items, outputDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* CLI entry point
|
||||
* Usage: node qrcode.mjs <url> [--output <path>] [--color <hex>]
|
||||
*/
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0 || args.includes('--help')) {
|
||||
console.log(`
|
||||
MycroZine QR Code Generator
|
||||
|
||||
Usage:
|
||||
node qrcode.mjs <url> [options]
|
||||
node qrcode.mjs --undernet Generate standard Undernet zine QR codes
|
||||
|
||||
Options:
|
||||
--output, -o <path> Output file path (PNG)
|
||||
--color, -c <hex> QR code color (default: #000000)
|
||||
--bg <hex> Background color (default: #00ff00)
|
||||
--width, -w <pixels> Width in pixels (default: 300)
|
||||
|
||||
Examples:
|
||||
node qrcode.mjs https://undernet.earth -o qr_undernet.png
|
||||
node qrcode.mjs "Hello World" --color "#ff0000" --bg "#ffffff"
|
||||
node qrcode.mjs --undernet
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for --undernet flag
|
||||
if (args.includes('--undernet')) {
|
||||
await generateUndernetQRCodes();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
let data = null;
|
||||
let outputPath = null;
|
||||
let darkColor = '#000000';
|
||||
let lightColor = '#00ff00';
|
||||
let width = 300;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case '--output':
|
||||
case '-o':
|
||||
outputPath = args[++i];
|
||||
break;
|
||||
case '--color':
|
||||
case '-c':
|
||||
darkColor = args[++i];
|
||||
break;
|
||||
case '--bg':
|
||||
lightColor = args[++i];
|
||||
break;
|
||||
case '--width':
|
||||
case '-w':
|
||||
width = parseInt(args[++i], 10);
|
||||
break;
|
||||
default:
|
||||
if (!args[i].startsWith('-')) {
|
||||
data = args[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
console.error('Error: URL or text data required');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!outputPath) {
|
||||
outputPath = path.join(__dirname, '..', 'output', 'qrcode.png');
|
||||
}
|
||||
|
||||
await generateQRCode({
|
||||
data,
|
||||
outputPath,
|
||||
width,
|
||||
darkColor,
|
||||
lightColor
|
||||
});
|
||||
}
|
||||
|
||||
// Run CLI if executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch(console.error);
|
||||
}
|
||||
|
||||
export default {
|
||||
generateQRCode,
|
||||
generateQRCodeDataURL,
|
||||
generateZineQRCodes,
|
||||
generateUndernetQRCodes
|
||||
};
|
||||
Loading…
Reference in New Issue