code icon Code

Generate Styled QR Code

Generate a styled QR code with custom colors, shapes, gradients, and optional logo

Source Code

import fs from "fs";
import path from "path";
import QRCodeStyling from "qr-code-styling-node";
import { JSDOM } from "jsdom";

const [text, outputDir, optionsJson = "{}"] = process.argv.slice(2);

// Validation
if (!text) {
  console.error("Error: text is required");
  process.exit(1);
}

if (!outputDir) {
  console.error("Error: outputDir is required");
  process.exit(1);
}

// Parse options
let options;
try {
  options = JSON.parse(optionsJson);
} catch (e) {
  console.error("Error: options must be valid JSON");
  process.exit(1);
}

// Defaults
const config = {
  size: options.size || 300,
  format: options.format || "png", // png, svg, jpeg
  dotStyle: options.dotStyle || "square", // square, dots, rounded, extra-rounded, classy, classy-rounded
  dotColor: options.dotColor || "#000000",
  cornerSquareStyle: options.cornerStyle || "square", // square, dot, extra-rounded
  cornerSquareColor: options.cornerColor || options.dotColor || "#000000",
  cornerDotStyle: options.cornerDotStyle || "square", // square, dot
  cornerDotColor: options.cornerDotColor || options.cornerColor || options.dotColor || "#000000",
  backgroundColor: options.backgroundColor || "#FFFFFF",
  gradient: options.gradient || null, // { type: 'linear'|'radial', colors: ['#000', '#333'], rotation: 0 }
  logoPath: options.logoPath || null,
  logoSize: options.logoSize || 0.3, // Proportion of QR size (0.1 to 0.4)
  margin: options.margin ?? 10,
  errorCorrectionLevel: options.errorCorrectionLevel || "M", // L, M, Q, H
};

// Build gradient config if specified
function buildGradient(gradientConfig, fallbackColor) {
  if (!gradientConfig) return fallbackColor;

  return {
    type: gradientConfig.type || "linear", // linear or radial
    rotation: gradientConfig.rotation || 0,
    colorStops: gradientConfig.colors
      ? gradientConfig.colors.map((color, i, arr) => ({
          offset: i / (arr.length - 1),
          color: color,
        }))
      : [
          { offset: 0, color: fallbackColor },
          { offset: 1, color: fallbackColor },
        ],
  };
}

async function main() {
  console.log(`Generating styled QR code for: "${text.slice(0, 50)}${text.length > 50 ? "..." : ""}"`);
  console.log(`Size: ${config.size}px, Format: ${config.format}`);
  console.log(`Dot style: ${config.dotStyle}, Color: ${config.dotColor}`);
  if (config.gradient) {
    console.log(`Gradient: ${config.gradient.type} with ${config.gradient.colors?.length || 2} colors`);
  }
  if (config.logoPath) {
    console.log(`Logo: ${config.logoPath} (${Math.round(config.logoSize * 100)}% size)`);
  }

  // Build QR code styling options
  const qrOptions = {
    width: config.size,
    height: config.size,
    data: text,
    margin: config.margin,
    qrOptions: {
      errorCorrectionLevel: config.errorCorrectionLevel,
    },
    dotsOptions: {
      type: config.dotStyle,
      color: config.dotColor,
      ...(config.gradient && { gradient: buildGradient(config.gradient, config.dotColor) }),
    },
    cornersSquareOptions: {
      type: config.cornerSquareStyle,
      color: config.cornerSquareColor,
    },
    cornersDotOptions: {
      type: config.cornerDotStyle,
      color: config.cornerDotColor,
    },
    backgroundOptions: {
      color: config.backgroundColor,
    },
  };

  // Add logo if specified
  if (config.logoPath) {
    // Check if logo exists
    if (!fs.existsSync(config.logoPath)) {
      console.error(`Error: Logo file not found: ${config.logoPath}`);
      process.exit(1);
    }

    qrOptions.image = config.logoPath;
    qrOptions.imageOptions = {
      crossOrigin: "anonymous",
      margin: 5,
      imageSize: config.logoSize,
      hideBackgroundDots: true,
    };
    // Bump error correction for logo QR codes
    qrOptions.qrOptions.errorCorrectionLevel = config.errorCorrectionLevel === "L" ? "M" : config.errorCorrectionLevel;
  }

  // Generate filename from text content
  const slug = text
    .toLowerCase()
    .replace(/^https?:\/\//, "")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "")
    .slice(0, 40);

  const filename = `qr-${slug || "code"}.${config.format}`;

  // Ensure output directory exists
  fs.mkdirSync(outputDir, { recursive: true });
  const outputPath = path.join(outputDir, filename);

  // Create QR code
  const qrCode = new QRCodeStyling({
    jsdom: JSDOM,
    nodeCanvas: await import("canvas"),
    ...qrOptions,
  });

  // Generate and save
  const buffer = await qrCode.getRawData(config.format);
  fs.writeFileSync(outputPath, buffer);

  console.log(`✓ Saved: ${outputPath}`);
  console.log(
    JSON.stringify({
      success: true,
      path: outputPath,
      text: text,
      filename: filename,
      format: config.format,
      size: config.size,
      style: {
        dotStyle: config.dotStyle,
        dotColor: config.dotColor,
        cornerStyle: config.cornerSquareStyle,
        backgroundColor: config.backgroundColor,
        hasGradient: !!config.gradient,
        hasLogo: !!config.logoPath,
      },
    })
  );
}

main().catch((err) => {
  console.error("Failed:", err.message);
  process.exit(1);
});