mycro-zine/src/qrcode.mjs

254 lines
6.2 KiB
JavaScript

/**
* 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
};