QR code with a centred brand logo

QR codes carry built-in Reed-Solomon error correction. The 'H' level recovers ~30% of the symbol, which means we can punch a small hole out of the centre for a brand logo and the code still scans. Pipeline: generate the QR via the `qrcode` engine extension at ECL=H, resize the logo to ~22% of QR width (well under the 30% recovery limit), wrap it on a white puck so the logo reads as intentional, then composite onto the QR centre via Porter-Duff Over. Tweak LOGO_PCT to push closer to the recovery limit, or DARK / LIGHT for brand colours.

LOGO
LOGO — QR code with a centred brand logo
QR with logo
QR with logo — QR code with a centred brand logo
JavaScript
// QR code with a centred logo — link to mlcgo.eu
// demo_qrcode.js
//!INPUT: LOGO
//!OUTPUT: OUTPUT
//!PARAM: URL:string=https://mlcgo.eu
//!PARAM: SCALE:number=12,min=4,max=24
//!PARAM: ECL:string=H
//!PARAM: BORDER:integer=2,min=0,max=8
//!PARAM: LOGO_PCT:number=22,min=10,max=30
//!PARAM: LOGO_FRAME:integer=8,min=0,max=24
//!PARAM: DARK:string=#0f172a
//!PARAM: LIGHT:string=#ffffff

// QR codes carry built-in Reed-Solomon error correction. The "H" level
// recovers ~30% of the symbol, which means we can punch a small hole
// out of the centre — for a brand logo — and the code still scans.
//
// Pipeline:
//   1. Generate the QR code via the `qrcode` engine extension at
//      ECL=H. Choose colours and module-pixel size up front so the
//      rasterised result is final.
//   2. Resize the logo to LOGO_PCT% of QR width. Keep it well under
//      30% to leave a margin against the recovery limit; 22% is a
//      conservative sweet spot.
//   3. Build a white "puck" frame (logo + 2 × LOGO_FRAME px padding)
//      so the logo sits on a clean background and the eye reads it
//      as intentional, not as damage to the code.
//   4. Composite logo onto the puck via Porter-Duff "Over" (alpha-
//      aware), then composite the whole puck onto the QR centre.

const qr = Engine.qrcode(URL, {
  scale: SCALE,
  ecl: ECL,
  border: BORDER,
  light: LIGHT,
  dark: DARK,
});

// Logo: load, then resize to LOGO_PCT of QR width.
const logoSize = Math.round((qr.width * LOGO_PCT) / 100);
const logo = Engine.loadImage(LOGO).resize(logoSize, logoSize);

// White puck behind the logo. Engine.createImage returns a
// transparent-black canvas; adjustAlpha(1, Set) → fully opaque, then
// tint("#fff", 1.0) blends every pixel toward pure white.
const puckSize = logoSize + 2 * LOGO_FRAME;
const puck = Engine.createImage(puckSize, puckSize)
  .adjustAlpha(1.0, AlphaMode.Set)
  .tint("#ffffff", 1.0);

// Compose logo onto puck (Porter-Duff Over), then puck onto QR
// centre. blendAt takes a Px (= {x, y}) — use the global `px(x, y)`
// helper. Parallelises across rows since 1.29.11, so even big QR
// codes composite in a single frame.
puck.blendAt(logo, px(LOGO_FRAME, LOGO_FRAME), 1.0, BlendMode.Over);

const cx = Math.floor((qr.width - puckSize) / 2);
const cy = Math.floor((qr.height - puckSize) / 2);
qr.blendAt(puck, px(cx, cy), 1.0, BlendMode.Over);

qr.save(OUTPUT);

// © 2026 Michael Lechner · mlc OpticScript · https://mlcgo.eu · Elastic License 2.0