layeris

2019, revived 2026

Photoshop-style blend modes, adjustments and curves for Python.

About

A Python library for Photoshop-style image editing: 14 blend modes, tone and color adjustments, and curves on separate RGB channels. Every method returns the image, so an edit reads as one chained pipeline.

It began in 2019 as Layer.is and was rebuilt in 2026 with tests, a lookup table that makes edits about 20× faster, and a new documentation site.

StackPython 3.10+, NumPy, Pillow and Requests
Award2nd place (Gold Prize, NIPA President’s Award), 2019 Open Source Software Developer Competition
LicenseMIT

Load a photo, chain the edits, save. clone() branches an edit without reading the file again.

The Layer.is mark: three translucent layers sharing one white-to-cyan gradient.
image = LayerImage.from_file("ginkgo.jpg")
ginkgo.jpg, original
Original
ginkgo.jpg after: image.grayscale()
Step 1
image.grayscale()
ginkgo.jpg after: image.brightness(0.1)
Step 2
image.brightness(0.1)
ginkgo.jpg after: image.multiply("#3fe28f", opacity=0.7)
Step 3
image.multiply("#3fe28f", opacity=0.7)
ginkgo.jpg after: image.curve("rgb", [0, 0.1, 0.4, 0.65, 0.9, 1])
Step 4
image.curve("rgb", [0, 0.1, 0.4, 0.65, 0.9, 1])
The quick-start chain, one line at a time. Hover a step to hold it.
The look switcher: four looks of one photo, each beside the code that made it.
Neon Nights: a blue curve and a magenta soft light, before and after.
han-river.jpg after Neon Nightshan-river.jpg, originalOriginalNeon Nights
Neon Nightshan-river.jpg
image = LayerImage.from_file("han-river.jpg")
(image.curve("b", [0.12, 0.45, 0.75, 1])
      .soft_light("#ff2fb3", opacity=0.6)
      .saturation(0.3)
      .contrast(1.1))
Neon Nights on the Han River at dawn. Drag the divider or use arrow keys.

24 complete looks, each with a before/after slider and code that runs as printed.

hanok-window.jpg, Original
00 Original
hanok-window.jpg, Teal & Orange
01 Teal & Orange
(image.curve("r", [0, 0.1, 0.5, 0.9, 1])
  .curve("b", [0.18, 0.38, 0.5, 0.6, 0.8])
  .contrast(1.1)
  .saturation(0.2))
hanok-window.jpg, Neon Nights
02 Neon Nights
(image.curve("b", [0.12, 0.45, 0.75, 1])
  .soft_light("#ff2fb3", opacity=0.6)
  .saturation(0.3)
  .contrast(1.1))
hanok-window.jpg, Cyanotype
03 Cyanotype
(image.grayscale()
  .curve("rgb", [0, 0.1, 0.55, 0.9, 1])
  .curve("r", [0.02, 0.85])
  .curve("g", [0.15, 0.95])
  .curve("b", [0.35, 1.0]))
hanok-window.jpg, Noir
04 Noir
(image.grayscale()
  .curve("rgb", [0, 0.04, 0.3, 0.75, 1])
  .contrast(1.2))
hanok-window.jpg, Faded film
05 Faded film
(image.curve("rgb", [0.15, 0.32, 0.55, 0.76, 0.9])
  .saturation(-0.25)
  .soft_light("#f0cf9c", opacity=0.7))
One hanok window, six looks, each with the code that made it.
Teal & Orange: opposing S-curves on the red and blue channels.
Cross-processed: three per-channel curves and nothing else.
jeju-wall.jpg after Teal & Orangejeju-wall.jpg, originalOriginalTeal & Orange
Teal & Orangejeju-wall.jpg
image = LayerImage.from_file("jeju-wall.jpg")
(image.curve("r", [0, 0.1, 0.5, 0.9, 1])
      .curve("b", [0.18, 0.38, 0.5, 0.6, 0.8])
      .contrast(1.1)
      .saturation(0.2))
Teal & Orange on a Jeju stone wall: teal shadows, orange evening light.
00Original
dancheong.jpg, Original
image = LayerImage.from_file("dancheong.jpg")
01B&W, red filter
dancheong.jpg, B&W, red filter
(image.multiply("#ff5a36")
      .grayscale()
      .curve("rgb", [0, 0.3, 0.85, 1])
      .contrast(1.2))
02Cross-processed
dancheong.jpg, Cross-processed
(image.curve("r", [0, 0.15, 0.55, 0.9, 1])
      .curve("g", [0, 0.2, 0.55, 0.85, 1])
      .curve("b", [0.22, 0.35, 0.48, 0.6, 0.72]))
Dancheong eaves three ways. The red filter turns the blue sky almost black.
Filtering the recipes by the methods they use.

14 modes in darken, lighten and contrast groups. The blend layer is a hex color or a NumPy array, with an optional opacity.

#e8743b
image = LayerImage.from_file("rice-tray.jpg")
image.<mode>("#e8743b")
rice-tray.jpg, original
00base
rice-tray.jpg with darken("#e8743b")
01darkenDarken
rice-tray.jpg with multiply("#e8743b")
02multiply
rice-tray.jpg with color_burn("#e8743b")
03color_burn
rice-tray.jpg with linear_burn("#e8743b")
04linear_burn
rice-tray.jpg with lighten("#e8743b")
05lightenLighten
rice-tray.jpg with screen("#e8743b")
06screen
rice-tray.jpg with color_dodge("#e8743b")
07color_dodge
rice-tray.jpg with linear_dodge("#e8743b")
08linear_dodge
rice-tray.jpg with overlay("#e8743b")
09overlayContrast
rice-tray.jpg with soft_light("#e8743b")
10soft_light
rice-tray.jpg with hard_light("#e8743b")
11hard_light
rice-tray.jpg with vivid_light("#e8743b")
12vivid_light
rice-tray.jpg with linear_light("#e8743b")
13linear_light
rice-tray.jpg with pin_light("#e8743b")
14pin_light
All 14 blend modes, one color, one rice bowl. Hover to see the base.
opacity
image.overlay("#ff4f8b", opacity=…)
gwanghwamun.jpg with overlay at opacity 0
0
gwanghwamun.jpg with overlay at opacity 0.25
0.25
gwanghwamun.jpg with overlay at opacity 0.5
0.5
gwanghwamun.jpg with overlay at opacity 0.75
0.75
gwanghwamun.jpg with overlay at opacity 1
1
Opacity mixes the blended result back with the original.
Hovering a result shows the photo underneath.
tea-field.jpg after Cross-processedtea-field.jpg, originalOriginalCross-processed
Cross-processedtea-field.jpg
image = LayerImage.from_file("tea-field.jpg")
(image.curve("r", [0, 0.15, 0.55, 0.9, 1])
      .curve("g", [0, 0.2, 0.55, 0.85, 1])
      .curve("b", [0.22, 0.35, 0.48, 0.6, 0.72]))
Cross-processed on a Jeju green-tea field: yellow-green highlights, deeper shadows.

Brightness, contrast, hue, saturation, lightness and grayscale are each one call. Curves remap any mix of the red, green and blue channels.

Each adjustment beside its one-line call.
namsan-fog.jpg after Winter bluesnamsan-fog.jpg, originalOriginalWinter blues
Winter bluesnamsan-fog.jpg
image = LayerImage.from_file("namsan-fog.jpg")
(image.soft_light("#1f5fd1", opacity=0.8)
      .curve("b", [0.1, 0.55, 1])
      .lightness(0.05))
Winter blues on Namsan in morning fog: blue soft light, lifted blue shadows.
image = LayerImage.from_file("subway.jpg")
subway.jpg, original
Original
subway.jpg after: image.grayscale().contrast(1.2)
Step 1
image.grayscale().contrast(1.2)
subway.jpg after: image.curve("r", [0.12, 1.0])
Step 2
image.curve("r", [0.12, 1.0])
subway.jpg after: image.curve("g", [0.08, 0.45])
Step 3
image.curve("g", [0.08, 0.45])
subway.jpg after: image.curve("b", [0.4, 0.42])
Step 4
image.curve("b", [0.4, 0.42])
A duotone, one channel at a time: grayscale, then curves on red, green, blue.
Warm Up Skin Tones: red and blue curves as a white balance.
Cyanotype: grayscale, then a curve on each channel.
celadon.jpg after Sepia printceladon.jpg, originalOriginalSepia print
Sepia printceladon.jpg
image = LayerImage.from_file("celadon.jpg")
(image.grayscale()
      .curve("r", [0.16, 0.62, 1.0])
      .curve("g", [0.1, 0.5, 0.92])
      .curve("b", [0.06, 0.36, 0.78]))
Sepia print on a celadon vase: three channel curves, black to brown, white to cream.

Ten studio portraits in Joseon dress, from a young King Sejong and his queen to a yangban, a bride and a market peddler, each given a light look in a few layeris calls.

01 HanjiKing
(image.curve("rgb", [0.05, 0.28, 0.53, 0.78, 0.97])
  .saturation(-0.08)
  .soft_light("#f3d9a8", opacity=0.3))
A young man as King Sejong in a red dragon robe and winged ikseongwan hat, the originalBefore
A young man as King Sejong in a red dragon robe and winged ikseongwan hat, in HanjiAfter
02 Gold LeafQueen
(image.overlay("#ffcf73", opacity=0.14)
  .curve("rgb", [0, 0.23, 0.5, 0.77, 1])
  .curve("b", [0.03, 0.48, 0.97]))
A young queen in a gold-patterned green dangui and a crown of braids, the originalBefore
A young queen in a gold-patterned green dangui and a crown of braids, in Gold LeafAfter
03 Clear MorningRoyal physician
(image.curve("rgb", [0.03, 0.3, 0.56, 0.8, 1])
  .curve("b", [0.02, 0.54, 1])
  .saturation(-0.1))
A young royal physician in a black garima headdress, holding a needle pouch, the originalBefore
A young royal physician in a black garima headdress, holding a needle pouch, in Clear MorningAfter
04 Old SilkYangban
(image.saturation(-0.4)
  .soft_light("#e5c99a", opacity=0.35)
  .curve("rgb", [0.06, 0.28, 0.52, 0.77, 0.96]))
A wealthy yangban in a jade silk dopo and black gat, holding a folding fan, the originalBefore
A wealthy yangban in a jade silk dopo and black gat, holding a folding fan, in Old SilkAfter
05 Spring HazeNoblewoman
(image.soft_light("#ffc9d6", opacity=0.4)
  .curve("rgb", [0.07, 0.3, 0.55, 0.79, 0.98]))
A noblewoman holding a green jang-ot cloak over her head like a hood, the originalBefore
A noblewoman holding a green jang-ot cloak over her head like a hood, in Spring HazeAfter
06 Silk PaintingBeauty
(image.saturation(-0.25)
  .soft_light("#e7c48f", opacity=0.4)
  .curve("rgb", [0.08, 0.3, 0.53, 0.77, 0.96]))
A Joseon beauty after Sin Yun-bok, in a jade jeogori and indigo chima, the originalBefore
A Joseon beauty after Sin Yun-bok, in a jade jeogori and indigo chima, in Silk PaintingAfter
07 FestiveBride
(image.saturation(0.12)
  .curve("r", [0, 0.53, 1])
  .curve("rgb", [0, 0.23, 0.5, 0.77, 1]))
A bride in a green wonsam and jokduri, red dots on her cheeks, hands in white hansam, the originalBefore
A bride in a green wonsam and jokduri, red dots on her cheeks, hands in white hansam, in FestiveAfter
08 SteelMilitary officer
(image.curve("rgb", [0, 0.22, 0.5, 0.78, 1])
  .curve("b", [0.06, 0.52, 0.96])
  .saturation(-0.15))
A military officer in a gugunbok and feathered jeollip hat, holding a sword, the originalBefore
A military officer in a gugunbok and feathered jeollip hat, holding a sword, in SteelAfter
09 Night WatchConstable
(image.overlay("#3d6fb5", opacity=0.18)
  .curve("rgb", [0.03, 0.25, 0.5, 0.76, 0.97]))
A constable in a black beonggeoji hat and a navy tabard marked 捕, the originalBefore
A constable in a black beonggeoji hat and a navy tabard marked 捕, in Night WatchAfter
10 HarvestPeddler
(image.overlay("#ffb870", opacity=0.22)
  .curve("rgb", [0.04, 0.27, 0.52, 0.77, 0.97])
  .saturation(0.05))
A bobusang peddler in hemp clothes and a straw hat with cotton tufts, the originalBefore
A bobusang peddler in hemp clothes and a straw hat with cotton tufts, in HarvestAfter
Each portrait before and after its look, beside the code that made it.

Per-channel edits on an 8-bit photo run on a 256-entry table instead of every pixel. A 12-megapixel edit takes about 0.2 s instead of 4.1 s, with identical output.

Install, quick start and the performance numbers.
One preset4 photos
onggi-snow.jpg, originalonggi-snow.jpg with warm-matte.json
onggi-snow.jpg
hanok-window.jpg, originalhanok-window.jpg with warm-matte.json
hanok-window.jpg
persimmon.jpg, originalpersimmon.jpg with warm-matte.json
persimmon.jpg
bojagi.jpg, originalbojagi.jpg with warm-matte.json
bojagi.jpg
warm-matte.json
{
  "operations": [
    {"type": "curve", "channels": "rgb", "curve_points": [0.08, 0.3, 0.55, 0.78, 0.94]},
    {"type": "saturation", "factor": -0.15},
    {"type": "soft_light", "hex": "#ffb36b", "opacity": 0.65},
    {"type": "lighten", "hex": "#1e1b2e"}
  ]
}
Python
image = LayerImage.from_file("onggi-snow.jpg")
image.apply_from_json("warm-matte.json")
One JSON preset applied to four photos.
A look saved as a JSON preset and applied to four photos.
bukchon-dusk.jpg after Duotonebukchon-dusk.jpg, originalOriginalDuotone
Duotonebukchon-dusk.jpg
image = LayerImage.from_file("bukchon-dusk.jpg")
image.grayscale().contrast(1.2)
image.curve("r", [0.12, 1.0])
image.curve("g", [0.08, 0.45])
image.curve("b", [0.4, 0.42])
Duotone on Bukchon rooftops at dusk: indigo shadows, coral highlights.

The docs site is generated by a script that runs every snippet verbatim to render its image.

The API reference: signatures, parameters, returns and errors.
A tour of the docs: looks, sliders, blend modes and the API.
The look switcher on a phone.
The Neon Nights slider on a phone.
The docs switched to the dark theme and back.
lake-pavilion.jpg after Golden glowlake-pavilion.jpg, originalOriginalGolden glow
Golden glowlake-pavilion.jpg
image = LayerImage.from_file("lake-pavilion.jpg")
(image.screen("#3a1800", opacity=0.7)
      .overlay("#ff9d3b", opacity=0.25)
      .curve("rgb", [0, 0.18, 0.5, 0.82, 1]))
Golden glow on a lake pavilion at dawn: screen, overlay and an S-curve.

Layer.is won 2nd place in the general division of the 2019 Open Source Software Developer Competition, among over 100 teams.