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
QR with 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