惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
罗磊的独立博客
量子位
Microsoft Azure Blog
Microsoft Azure Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
J
Java Code Geeks
M
MIT News - Artificial intelligence
月光博客
月光博客
IT之家
IT之家
D
DataBreaches.Net
A
About on SuperTechFans
博客园 - 三生石上(FineUI控件)
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Last Week in AI
Last Week in AI
D
Docker
The GitHub Blog
The GitHub Blog
B
Blog
V
Visual Studio Blog
博客园 - Franky
N
Netflix TechBlog - Medium
博客园 - 【当耐特】
Martin Fowler
Martin Fowler
博客园 - 聂微东
U
Unit 42

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Watermarks, overlays, and blend modes in a few lines — im...
Aissam Irhir · 2026-06-23 · via DEV Community
Cover image for Watermarks, overlays, and blend modes in a few lines — imgkit now has composite()

Aissam Irhir

If you ship anything with user-generated or product imagery, you eventually need to put one image on top of another: a logo in the corner, a "SAMPLE" stamp across a preview, a badge on a product shot, a gradient blended over a hero photo.

This usually means reaching for sharp and stitching together its composite API, or dropping down to node-canvas and doing the math by hand. The latest release of imgkit (v2.3.0) adds a dedicated composite() for exactly this — Rust-backed, async off the main thread, and the same on Node.js and Bun.

Install

bun add imgkit
# or
npm install imgkit

Prebuilt binaries ship for the common platforms, so there's no build step on install.

The hello-world: a corner watermark

The most common case — a semi-transparent logo anchored to the bottom-right corner, nudged in from the edges:

import { composite, resize } from 'imgkit';

const photo = Buffer.from(await Bun.file('photo.jpg').arrayBuffer());
const logoRaw = Buffer.from(await Bun.file('logo.png').arrayBuffer());

// Scale the logo first, then composite
const logo = await resize(logoRaw, { width: 180 });

const watermarked = await composite(photo, {
  layers: [
    { input: logo, gravity: 'southEast', opacity: 0.7, offsetX: -32, offsetY: -32 },
  ],
  output: { format: 'jpeg', jpeg: { quality: 90 } },
});

await Bun.write('watermarked.jpg', watermarked);

That's the whole API surface for the simple case. Everything else is just more layers and more options.

The mental model

composite() paints an array of layers onto a base image. The rules are small enough to keep in your head:

  • Layers paint in array order — the first layer is the bottom-most overlay, the last sits on top.
  • Placement is either gravity or absolute. Use gravity (center, north, southEast, etc.) to anchor a layer to a region, with optional offsetX / offsetY nudges. Or set left / top for pixel-precise placement — those override gravity, and off-canvas/negative values are clipped for you.
  • opacity (0.0–1.0) fades a layer; resize scales it before compositing; tile: true repeats it across the whole base.
  • blend picks the blend mode — over (default), multiply, screen, overlay, darken, lighten, or add, following the standard W3C separable blend formulas.

Recipes

Tiled "SAMPLE" / "DRAFT" stamp across the whole image:

const tiled = await composite(photo, {
  layers: [{ input: stamp, tile: true, opacity: 0.15 }],
});

Blend a gradient or texture over a photo:

const blended = await composite(photo, {
  layers: [{ input: gradient, blend: 'multiply', opacity: 0.6 }],
});

Stack several layers — a product on a background, a badge top-right, a faded logo bottom-left:

const out = await composite(background, {
  layers: [
    { input: product, gravity: 'center' },
    { input: badge,   gravity: 'northEast', resize: { width: 80 } },
    { input: logo,    gravity: 'southWest', opacity: 0.8 },
  ],
  output: { format: 'webp', webp: { quality: 90 } },
});

A couple of things worth knowing

  • The base is decoded to RGBA, so the result preserves transparency by defaultcomposite() returns PNG unless you set output. Choosing a JPEG output flattens any alpha (it's dropped, not matted), which is usually what you want for a final watermarked photo.
  • For lots of layers or large canvases, the async composite() runs off the main thread and can be cancelled with an AbortSignal or a timeoutMs via AsyncOptions. There's a compositeSync() if you'd rather stay synchronous.
  • Fully transparent source pixels and fully opaque over paints take fast paths, so the common watermark case stays cheap.

Why I built it this way

imgkit's whole point is to keep the fast path in Rust (via napi-rs) while exposing an API that feels native to JS — one function, plain options objects, Buffers in and out, identical behavior on Node and Bun. composite() follows that: no canvas, no manual pixel loops, no separate paths for "watermark" vs "blend" vs "tile." They're all just layers.

If you want the full option reference and more recipes, the docs are here:

If you try it, I'd love to hear what you're compositing — and issues/stars on GitHub are always welcome.