How to add a rubber stamp text with Claude

A red "APPROVED" stamp with a double border and patchy, worn-out ink slams down above the product at a tilt, knocking the picture with a small jolt.

Get this effect

  1. Switch to Code in your Claude Desktop App

  2. Install the skill (once)

    Show the prompt
    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 rubber stamp text to input.mp4
    Text: APPROVED

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 rubber stamp text 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.

SettingDefaultWhat it does
TEXTAPPROVEDThe word on the stamp.
LAND0.8Second the stamp hits the frame.
DROP0.12Seconds it takes to come down onto the frame.
START_SCALE2.5How big the stamp starts its drop, relative to its final size.
WIDTH0.74Stamp width, border included, as a share of the frame width.
X, Y0.5, 0.13Stamp center (0-1 across and down the frame).
INK(205, 30, 40)Ink color.
OPACITY0.92Ink opacity, so a little of the picture shows through.
TILT12Rotation in degrees, counterclockwise.
WEAR0.3Share of the ink knocked out by the worn, patchy print.
JOLT10How far the frame jumps on impact, in pixels.
JOLT_LEN0.3Seconds until the jolt settles.

Without the skill

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

Show the prompt
Add a rubber stamp to input.mp4: the word "APPROVED" in a heavy font (Arial Black) in red ink (205, 30, 40) at 92% opacity, inside a rounded rectangle border, with a thick outer line (14% of the letter size) and a thin inner line (5% of the letter size). The stamp, border included, is 74% of the frame width wide and 1.7 times the letter size tall, with the word 80% of the stamp's width; tilt it 12 degrees counterclockwise and center it 50% across and 13% down. Make the ink look worn: mix soft blotchy noise with fine speckle (numpy is fine) and leave out the weakest 30% of the ink so the print is patchy. The stamp comes down over 0.12 seconds and hits the frame at 0.8 seconds: it starts at 250% size and fully transparent, shrinks to 100% while fading in, speeding up as it drops, then stays to the end. On impact, jolt the whole frame (stamp included) about 10 pixels in changing directions with an extra 4% zoom so no edge shows, both dying down to still over 0.3 seconds. Nothing changes before 0.68 seconds. Draw the stamp with Python and Pillow (don't rely on ffmpeg's drawtext filter), keep the size and audio, and save it as output.mp4.

Run it without Claude

This is the exact script we tested. Save it as rubber-stamp-text.py and run:

python3 rubber-stamp-text.py input.mp4 output.mp4
Show the script
#!/usr/bin/env python3
"""Rubber stamp text: a worn red "APPROVED" stamp slams down onto the frame, tilted, and the picture jolts.

Usage: python3 rubber-stamp-text.py input.mp4 output.mp4
Needs ffmpeg, Pillow and numpy (pip install pillow numpy).
"""
import json
import math
import subprocess
import sys
from pathlib import Path

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

TEXT = "APPROVED"
LAND = 0.8                # second the stamp hits the frame
DROP = 0.12               # seconds it takes to come down onto the frame
START_SCALE = 2.5         # how big the stamp starts its drop, relative to its final size
WIDTH = 0.74              # stamp width (border included) as a share of the frame width
X, Y = 0.5, 0.13          # stamp center (0-1 across and down the frame)
INK = (205, 30, 40)       # ink color
OPACITY = 0.92            # ink opacity, so a little of the picture shows through
TILT = 12                 # degrees, counterclockwise
WEAR = 0.3                # share of the ink knocked out by the worn, patchy print
JOLT = 10                 # how far the frame jumps on impact, in pixels
JOLT_LEN = 0.3            # seconds until the jolt settles
FONTS = ["/System/Library/Fonts/Supplemental/Arial Black.ttf",    # macOS
         "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",  # Linux
         "C:/Windows/Fonts/ariblk.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")


def stamp():
    """The stamp drawn once: the word inside a thick and a thin border, worn patchy, tilted."""
    box_w = round(w * WIDTH)
    probe_font = ImageFont.truetype(FONT_FILE, 100)
    x0, y0, x1, y1 = probe_font.getbbox(TEXT)
    font = ImageFont.truetype(FONT_FILE, round(100 * box_w * 0.8 / (x1 - x0)))
    thick, thin = round(font.size * 0.14), round(font.size * 0.05)
    box_h = round(font.size * 1.7)

    ink = Image.new("L", (box_w, box_h))
    draw = ImageDraw.Draw(ink)
    draw.rounded_rectangle([0, 0, box_w - 1, box_h - 1], radius=thick * 2, outline=255, width=thick)
    gap = thick + thin * 2
    draw.rounded_rectangle([gap, gap, box_w - 1 - gap, box_h - 1 - gap], radius=thick, outline=255, width=thin)
    draw.text((box_w / 2, box_h / 2), TEXT, font=font, anchor="mm", fill=255)

    # wear: soft blotches plus fine speckle, and the weakest WEAR share of the ink doesn't print
    rng = np.random.default_rng(7)
    blotches = Image.fromarray((rng.random((box_h // 14 + 2, box_w // 14 + 2)) * 255).astype(np.uint8))
    blotches = np.asarray(blotches.resize((box_w, box_h), Image.BICUBIC), np.float32) / 255
    worn = 0.6 * blotches + 0.4 * rng.random((box_h, box_w))
    printed = worn > np.quantile(worn[np.asarray(ink) > 0], WEAR)
    alpha = (np.asarray(ink, np.float32) * printed * OPACITY).astype(np.uint8)

    img = Image.new("RGBA", (box_w, box_h), INK)
    img.putalpha(Image.fromarray(alpha))
    return img.rotate(TILT, resample=Image.BICUBIC, expand=True)


def add_stamp(img, t):
    if t < LAND - DROP:
        return img
    p = min((t - (LAND - DROP)) / DROP, 1)
    scale = START_SCALE + (1 - START_SCALE) * p * p   # speeds up as it comes down, like a hand pressing
    s = full.resize((round(full.width * scale), round(full.height * scale)), Image.LANCZOS)
    if p < 1:
        s.putalpha(s.getchannel("A").point(lambda a: a * p))
    img.alpha_composite(s, (round(w * X - s.width / 2), round(h * Y - s.height / 2)))
    return img


def jolt(img, t):
    """Knock the whole frame sideways and down on impact, settling over JOLT_LEN (a slight zoom hides the edges)."""
    s = t - LAND
    if s < 0 or s >= JOLT_LEN:
        return img
    k = 1 - s / JOLT_LEN
    zoom = 1 + 0.04 * k
    dx, dy = JOLT * k * math.sin(t * 83), JOLT * k * math.cos(t * 61)
    cw, ch = w / zoom, h / zoom
    x0, y0 = (w - cw) / 2 + dx / zoom, (h - ch) / 2 + dy / zoom
    return img.resize((w, h), Image.BICUBIC, box=(x0, y0, x0 + cw, y0 + ch))


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):
    """Read every frame of src, stamp and jolt it, and save to out with the audio."""
    reader = subprocess.Popen(["ffmpeg", "-v", "error", "-i", src, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
                              stdout=subprocess.PIPE)
    writer = subprocess.Popen(["ffmpeg", "-y", "-v", "error",
                               "-f", "rawvideo", "-pix_fmt", "rgb24", "-s", f"{w}x{h}", "-r", rate, "-i", "-",
                               "-i", src, "-map", "0:v", "-map", "1:a?", "-c:v", "libx264", "-crf", "18",
                               "-pix_fmt", "yuv420p", "-c:a", "copy", "-shortest", out], stdin=subprocess.PIPE)
    for i in range(math.ceil(duration * fps)):
        raw = reader.stdout.read(w * h * 3)
        if len(raw) < w * h * 3:
            break
        t = i / fps
        img = add_stamp(Image.frombuffer("RGB", (w, h), raw).convert("RGBA"), t).convert("RGB")
        writer.stdin.write(jolt(img, t).tobytes())
    writer.stdin.close()
    reader.wait()
    if writer.wait():
        sys.exit("ffmpeg failed")


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

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