Skip to main content
Glama

photopea-api

A headless, read/write client for Photopea — plus an MCP server, so Claude Code (or any agent) can edit images on its own.

from photopea_api import Photopea

async with Photopea.session() as pp:
    await pp.open_path("photo.jpg")
    await pp.draw.gradient(["#e63946", "#1d3557"], angle=45)
    await pp.text.add("SALE", x=40, y=120, size=64, color="#ffffff")
    await pp.filter.gaussian_blur(2)
    await pp.export_to("out.psd")        # layers preserved

No visible browser window. No tab to keep open. No human in the loop.

On Photopea. This drives Photopea through Live Messaging, its own documented and sanctioned automation API. Nothing here bypasses ads, defeats a protection, or touches the paid AI features (Remove BG, Magic Cut) that cost its author money per call — Photopea is one developer's ad-funded product and those features should not be driven programmatically. It opens one editor per session and does local, deterministic editing. If you need volume, self-host with Photopea-Offline rather than pointing a fleet at photopea.com.

Why this exists

Photopea has no backend: it is a client-side WASM app, so there is nothing to scrape and no API to call. The only sanctioned automation surface is Live MessagingpostMessage a string of JavaScript into an iframe and read what comes back. That means automating Photopea always means running Photopea in a browser.

Two projects got halfway there. attalla1/photopea-mcp-server has the MCP layer but opens a visible browser tab the user must leave alone, and has been unmaintained since April 2026. yikuansun/HeadlessPhotopea runs headless but exposes no agent interface. Nobody combined the two.

This does, and fixes the failure modes the reference implementation ships with — see RECON.md for the measurements behind each of the following.

reference

here

browser

visible tab, must stay open

headless, owned by the process

call framing

counts "done" messages

unique sentinel per call

gradients

16 solid bands

true ramps (206 distinct colours over 300 px)

text

fails silently right after boot

blocks until fonts actually rasterise

colours

hexValue assignment → black

SolidColor components

send layer to front

move(doc, PLACEATBEGINNING) — a no-op

move(topLayer, PLACEBEFORE)

after a crashed script

every later call times out

health check + page reload

stealth

none

camoufox (patched Firefox) by default

Each row on the right is covered by a test that asserts on exported pixels, because every failure in the left column looks like success from the API.

Related MCP server: Photopea MCP Server

Install

cd ~/photopea-api
python3.12 -m venv .venv
./.venv/bin/pip install -r requirements.txt
./.venv/bin/python -m camoufox fetch        # one-off, 663 MB

Verify both engines really drive Photopea on your machine:

./.venv/bin/python scripts/gate_check.py
# [PASS] camoufox   boot 4635ms  png 126B
# [PASS] chromium   boot 1813ms  png 126B

Use as an MCP server

claude mcp add -s user photopea -- ~/photopea-api/.venv/bin/python -m mcp_server.server

The editor boots lazily on the first tool call and is reused for the rest of the session. Environment overrides: PHOTOPEA_ENGINE (camoufox | chromium | firefox, default camoufox) and PHOTOPEA_HEADLESS (0 to watch it work, useful when debugging).

Tools cover documents (create, open, resize, crop, rotate, flip, flatten, export), layers (add, select, rename, opacity, blend mode, visibility, move, scale, rotate, flip, reorder, group, delete, tree), drawing (gradients, rectangles, ellipses, image placement), text (point, paragraph, edit), colour adjustments, filters, selections, and run_script as the escape hatch.

Engine choice

camoufox is the default because it was asked for, and it works. But Photopea serves no anti-bot whatsoever — the recon found zero protection vendors and zero XHR endpoints — so stealth buys nothing functional here and costs ~2.8 s of extra boot. Use chromium when you control the environment:

from photopea_api.browser import BrowserConfig
async with Photopea.session(BrowserConfig(engine="chromium")) as pp:
    ...

Closing Photopea's gaps

Photopea implements a large slice of Photoshop, but it is missing channels, every layer effect, eight filters, four colour spaces, guides and AVIF — and because its Action Manager is a phantom there is no lower level to drop to.

So the architecture inverts: Photopea is the document engine, Python is the pixel engine. One primitive makes it work — :meth:PixelOps.export_layer isolates a layer and exports it with alpha, and place_image composites arbitrary RGBA back in. Everything missing is rebuilt on top of that pair.

gap

how it is closed

layer effects (0/1 native)

pp.fx.drop_shadow / outer_glow / inner_glow / stroke / color_overlay / gradient_overlay / bevel — rendered from the layer's alpha

8 missing filters

pp.pyfilter.radial_blur / smart_blur / spherize / zigzag / glass / lens_flare / deinterlace / clouds

channels + histograms (0/4)

pp.analyze.histogram / histograms / statistics / channel / palette — computed from exported pixels, exact

HSB / CMYK / Lab / Gray

script.hsb_to_hex, cmyk_to_hex, lab_to_hex, gray_to_hex

guides

pp.guides — tracked Python-side, render() draws them onto a layer

selection.store

pp.selections.save/load — replays the geometry we asked for

AVIF export

pp.analyze.export_avif() via Pillow

Live layer styles, not just raster

pp.fx.* gives raster effects — an effect arrives as an ordinary layer named title (drop_shadow). For a real, editable layer style use pp.fx.apply_live(), which writes an lfx2 block into the PSD and hands the file back:

await pp.fx.apply_live({"drop_shadow": {"color": "#000000", "size": 12}}, layer="title")

This works because Photopea does render PSD layer effects — verified by opening a Photoshop-authored file: drop shadow, both glows, both shadows, bevel/emboss, satin, all three overlays and stroke all render correctly. The style survives PSD export and stays editable in Photoshop.

All ten of Photoshop's layer styles are live: drop_shadow, inner_shadow, outer_glow, inner_glow, bevel, satin, gradient_overlay, pattern_overlay, stroke, color_overlay. The raster path in pp.fx.* remains as an alternative, not a fallback — it keeps undo history, which the live path discards when it re-opens the document.

pattern_overlay also takes a tile (PNG bytes): its pixels live in a global Patt block rather than in the effect descriptor, so the tile is written into the file and referenced by UUID.

Each raster operation costs one export plus one import — tens of milliseconds at typical sizes, not free.

One capability falls out of this for free and is worth knowing about: pp.analyze.drew_anything(layer) answers "did this layer actually rasterise?" It is the only reliable guard against Photopea's silent no-ops, and cheap enough to call after every text layer in a generated pipeline.

What Photopea can and cannot do

docs/API_MAP.md is a capability matrix built by calling every candidate member against a live editor. It exists because the API cannot be introspected — the objects are virtual, typeof lies, and an unsupported call does not raise, it aborts your whole script.

The headline result: Photopea implements a large slice of Photoshop's layer, filter and selection surface, but its Action Manager is a phantom. stringIDToTypeID() echoes its own argument back and executeAction() crashes the interpreter, so the usual "drop to the Action Manager for anything missing" escape hatch does not exist. Gradients are the visible casualty — hence photopea_api/raster.py, which renders them in Python and composites them in.

Tests

./.venv/bin/python -m pytest                # 70 tests, ~13 min
./.venv/bin/python -m pytest -m "not slow"  # skip the per-engine boots
./.venv/bin/python scripts/demo.py          # compose a poster end to end

Every visual assertion is made on exported pixels, never on layer state — because the failure mode that matters (text that renders nothing) leaves the layer looking perfectly correct.

Layout

photopea_api/
  browser.py    engine launcher (camoufox | chromium | firefox)
  bridge.py     Live Messaging transport, sentinel framing
  host.html     the page that hosts the Photopea iframe
  client.py     session, file I/O, export, font readiness
  script.py     JS generation for Photopea's restricted dialect
  raster.py     gradients rendered in Python
  ops/          document, layer, text, draw, adjust, filters, selection
mcp_server/     MCP server over the client
scripts/        recon, gate check, API mapper, targeted probes
docs/API_MAP.md the measured capability matrix
RECON.md        how Photopea works and what it forces on us

Etiquette

Photopea is one developer's ad-funded product and its AI features cost him money per call. This client drives local deterministic editing only, never touches showWindow("magiccut") or other paid surfaces, and opens one editor per session. For volume, self-host (Photopea-Offline) instead.

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • MCP server for NanoBanana AI image generation and editing

  • MCP server for Flux AI image generation

  • OCR, transcription, file extraction, and image generation for AI agents via MCP.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sportiz91/photopea-api'

If you have feedback or need assistance with the MCP directory API, please join our Discord server