---
title: "How to add a QR code overlay with Claude (tested prompt and script)"
description: "A white card with a scannable QR code and \"SCAN TO SHOP\" underneath slides up into the bottom-right corner and stays there for the last five seconds. Install one skill and ask Claude Code, or run the Python script we tested yourself."
url: https://www.recharm.com/effects/qr-code-overlay
---

# How to add a QR code overlay with Claude

A white card with a scannable QR code and "SCAN TO SHOP" underneath slides up into the bottom-right corner and stays there for the last five seconds.

Made by the script on this page, from our own test clip.

## Get this effect

1. ### Switch to Code in your [Claude Desktop App](https://claude.com/download)

2. ### Install the skill (once)

   ```
   Install the Recharm video effects skill: download https://recharm-growth-staging.s3.us-east-2.amazonaws.com/website/downloads/recharm-video-effects.zip and unzip it into ~/.claude/skills
   ```

3. ### Ask for the effect

   ```
   Add a QR code overlay to input.mp4
   Label: SCAN TO SHOP
   ```

## Several videos, several effects, one ask

Put your clips in the folder, mention them with @, and ask for everything at once. Claude runs one tested script after another on each clip, then joins them.

Add a qr code overlay to **@clip1.mp4**, put a sale badge on **@clip2.mp4**, give **@clip3.mp4** a VHS look, then join all three with crossfades.

## Settings you can change

Ask Claude to change any of these, or edit them at the top of the script.

| Setting  | Default               | What it does                                                                                   |
| -------- | --------------------- | ---------------------------------------------------------------------------------------------- |
| `LINK`   | `https://recharm.com` | What the code opens, up to 53 characters.                                                      |
| `LABEL`  | `SCAN TO SHOP`        | The line under the code.                                                                       |
| `START`  | `1.0`                 | Second the card appears. It stays to the end, so leave at least 5 seconds for viewers to scan. |
| `SLIDE`  | `0.4`                 | Seconds the card takes to slide up and fade in.                                                |
| `MODULE` | `5`                   | Pixels per QR square. Keep it a whole number so the squares stay crisp for phone cameras.      |
| `MARGIN` | `24, 60`              | Gap in pixels from the right and bottom edges of the frame.                                    |

## Without the skill

Don’t want to install the skill? Paste this prompt into Claude Code and Claude writes the script itself.

```
Add a QR code overlay to input.mp4. Encode "https://recharm.com" as a version 3 QR code (29x29 squares, low error correction) and draw each square as exactly 5x5 pixels, black on white, with a 4-square (20 pixel) white quiet zone all round, so the code is 185 pixels square. Put it on a pure white (255, 255, 255) card with 18 pixel rounded corners, 185 pixels wide and 229 pixels tall, with "SCAN TO SHOP" in black bold 18 pixel text centered under the code, and a soft dark drop shadow 6 pixels below it. Place the card so its right edge is 24 pixels from the right of the frame and its bottom edge 60 pixels from the bottom. At 1.0 seconds it slides up 40 pixels into place while fading in over 0.4 seconds (ease out), then stays until the end. Keep the rest of the frame untouched. If no QR library is installed, write the encoder in plain Python. Draw the card and text with Pillow (not ffmpeg's drawtext filter), composite it with ffmpeg, keep the audio and save it as output.mp4.
```

## Run it without Claude

This is the exact script we tested. Save it as `qr-code-overlay.py` and run:

```
python3 qr-code-overlay.py input.mp4 output.mp4
```

```
#!/usr/bin/env python3
"""QR code overlay: a white "SCAN TO SHOP" card with a scannable QR code slides up into the bottom-right corner.

Usage: python3 qr-code-overlay.py input.mp4 output.mp4
Needs ffmpeg, Pillow and numpy (pip install pillow numpy). The QR code is encoded right here, so no QR library.
"""
import json
import math
import subprocess
import sys
from pathlib import Path

import numpy as np
from PIL import Image, ImageDraw, ImageFilter, ImageFont

LINK = "https://recharm.com"   # what the code opens (up to 53 characters)
LABEL = "SCAN TO SHOP"         # line under the code
START = 1.0                    # second the card appears; it stays to the end
SLIDE = 0.4                    # seconds it takes to slide up and fade in
MODULE = 5                     # pixels per QR square; whole pixels keep the edges crisp for phone cameras
MARGIN = 24, 60                # gap from the right and bottom edges of the frame, in pixels
FONTS = ["/System/Library/Fonts/Supplemental/Arial Bold.ttf",     # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
         "C:/Windows/Fonts/arialbd.ttf"]                          # Windows
FONT_FILE = next((f for f in FONTS if Path(f).exists()), None) or sys.exit("No bold font found: add one to FONTS")

# --- QR encoder: version 3 (29x29 squares), low error correction, byte mode, mask 0 ---

def gf_mul(x, y):
    """Multiply in GF(256), the arithmetic QR's Reed-Solomon error correction uses."""
    z = 0
    for i in reversed(range(8)):
        z = (z << 1) ^ ((z >> 7) * 0x11D)
        z ^= ((y >> i) & 1) * x
    return z

def error_correction(data, n=15):
    divisor, root = [0] * (n - 1) + [1], 1
    for _ in range(n):
        for j in range(n):
            divisor[j] = gf_mul(divisor[j], root) ^ (divisor[j + 1] if j + 1 < n else 0)
        root = gf_mul(root, 2)
    rem = [0] * n
    for b in data:
        factor = b ^ rem.pop(0)
        rem.append(0)
        rem = [r ^ gf_mul(c, factor) for r, c in zip(rem, divisor)]
    return rem

def qr_matrix(text, size=29, capacity=55):
    payload = text.encode()
    bits = "0100" + f"{len(payload):08b}" + "".join(f"{b:08b}" for b in payload) + "0000"
    data = [int(bits[i:i + 8].ljust(8, "0"), 2) for i in range(0, len(bits), 8)]
    data += [0xEC, 0x11] * ((capacity - len(data)) // 2) + [0xEC] * ((capacity - len(data)) % 2)
    codewords = data + error_correction(data)

    grid = [[None] * size for _ in range(size)]   # grid[y][x]: True dark, False light, None free for data
    for i in range(size):                         # timing lines
        grid[6][i] = grid[i][6] = i % 2 == 0
    for cx, cy in [(3, 3), (size - 4, 3), (3, size - 4)]:   # finder squares with their light borders
        for dy in range(-4, 5):
            for dx in range(-4, 5):
                if 0 <= cx + dx < size and 0 <= cy + dy < size:
                    grid[cy + dy][cx + dx] = max(abs(dx), abs(dy)) not in (2, 4)
    for dy in range(-2, 3):                       # alignment square
        for dx in range(-2, 3):
            grid[22 + dy][22 + dx] = max(abs(dx), abs(dy)) != 1
    fmt = 0b01000                                 # level L, mask 0
    rem = fmt
    for _ in range(10):
        rem = (rem << 1) ^ ((rem >> 9) * 0x537)
    fmt_bits = [((fmt << 10 | rem) ^ 0x5412) >> i & 1 == 1 for i in range(15)]
    for i in range(15):                           # format info, twice
        x, y = [(8, 0), (8, 1), (8, 2), (8, 3), (8, 4), (8, 5), (8, 7), (8, 8), (7, 8),
                (5, 8), (4, 8), (3, 8), (2, 8), (1, 8), (0, 8)][i]
        grid[y][x] = fmt_bits[i]
        x, y = (size - 1 - i, 8) if i < 8 else (8, size - 15 + i)
        grid[y][x] = fmt_bits[i]
    grid[size - 8][8] = True                      # the one always-dark square

    stream = [(c >> (7 - k)) & 1 == 1 for c in codewords for k in range(8)]
    i = 0
    for right in range(size - 1, 0, -2):          # zigzag up and down two columns at a time
        right -= right <= 6
        for vert in range(size):
            y = size - 1 - vert if (right + 1) & 2 == 0 else vert
            for x in (right, right - 1):
                if grid[y][x] is None:
                    grid[y][x] = (i < len(stream) and stream[i]) != ((x + y) % 2 == 0)
                    i += 1
    return np.array(grid, bool)

def card():
    """White rounded card: the code with its quiet zone, the label underneath, and a soft shadow."""
    code = qr_matrix(LINK)
    quiet = 4 * MODULE
    side = code.shape[0] * MODULE + 2 * quiet
    label_h = round(side * 0.24)
    cw, ch = side, side + label_h
    pad = 16
    img = Image.new("RGBA", (cw + 2 * pad, ch + 2 * pad))
    shadow = Image.new("RGBA", img.size)
    ImageDraw.Draw(shadow).rounded_rectangle([pad, pad + 6, pad + cw, pad + ch + 6], 18, fill=(0, 0, 0, 110))
    img.alpha_composite(shadow.filter(ImageFilter.GaussianBlur(8)))
    draw = ImageDraw.Draw(img)
    draw.rounded_rectangle([pad, pad, pad + cw - 1, pad + ch - 1], 18, fill="white")
    squares = Image.fromarray(np.where(code, 0, 255).astype(np.uint8)).resize(
        (code.shape[0] * MODULE,) * 2, Image.NEAREST)
    img.paste(squares, (pad + quiet, pad + quiet))
    font = ImageFont.truetype(FONT_FILE, round(label_h * 0.42))
    draw.text((pad + cw / 2, pad + side + label_h * 0.32), LABEL, font=font, anchor="mm", fill="black")
    return img, pad

def overlay(t):
    img = Image.new("RGBA", (w, h))
    p = min(max((t - START) / SLIDE, 0), 1)
    if p > 0:
        ease = 1 - (1 - p) ** 3
        x = w - MARGIN[0] - full.width + pad
        y = h - MARGIN[1] - full.height + pad + round(40 * (1 - ease))
        layer = full.copy()
        layer.putalpha(layer.getchannel("A").point(lambda a: round(a * ease)))
        img.alpha_composite(layer, (x, y))
    return img

def probe(path):
    info = json.loads(subprocess.check_output(["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
                                               "stream=width,height,r_frame_rate:format=duration", "-of", "json", path]))
    s = info["streams"][0]
    num, den = s["r_frame_rate"].split("/")
    return s["width"], s["height"], s["r_frame_rate"], float(num) / float(den), float(info["format"]["duration"])

def render(src, out):
    """Draw overlay(t) on every frame of src and save to out, keeping the audio."""
    ff = subprocess.Popen(["ffmpeg", "-y", "-v", "error", "-i", src,
                           "-f", "rawvideo", "-pix_fmt", "rgba", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                           "-filter_complex", "[0:v][1:v]overlay=shortest=1,format=yuv420p[v]",
                           "-map", "[v]", "-map", "0:a?", "-c:v", "libx264", "-crf", "18", "-c:a", "copy", out],
                          stdin=subprocess.PIPE)
    for i in range(math.ceil(duration * fps)):
        ff.stdin.write(overlay(i / fps).tobytes())
    ff.stdin.close()
    if ff.wait():
        sys.exit("ffmpeg failed")

src, out = sys.argv[1:3]
w, h, rate, fps, duration = probe(src)
full, pad = card()
render(src, out)
```

## More product shots

- [Animated feature callouts **Animated feature callouts**](https://www.recharm.com/effects/animated-feature-callouts)
- [Before/after slider **Before/after slider**](https://www.recharm.com/effects/before-after-slider)
- [Best seller badge **Best seller badge**](https://www.recharm.com/effects/best-seller-badge)
- [Callout arrows and circles **Callout arrows and circles**](https://www.recharm.com/effects/callout-arrows-and-circles)
- [Carousel swipe **Carousel swipe**](https://www.recharm.com/effects/carousel-swipe)

## Find 3x _winning ads_

Recharm cuts your ad footage into tagged, searchable clips: hooks, testimonials, product shots. Find the shots that win, add the effect with Claude, and ship 3x more ad edits.

[Start 14-day free trial](https://www.recharm.com/get-started)
