Skip to content

subwaymatch/layer-is-python

Repository files navigation

Layer.is

A library that supports all blend modes in Photoshop, brightness, contrast, hue, saturation, lightness adjustments, and curve adjustments on separate RGB channels.

This project was a part of the 2019 Open Source Software Competition.

Why use Layer.is?

  • Supports all frequently used blend modes in Photoshop
  • Chain operations for concise, readable image pipelines
  • Straightforward API to manipulate images
  • Load a sequence of operations from JSON
  • Apply curve adjustments selectively to individual RGB channels
  • Lightweight: depends only on NumPy, Pillow, and Requests

Requirements

Python 3.10 or higher.

Installation

From PyPI

pip install layeris

From source

git clone https://github.com/subwaymatch/layer-is-python.git
cd layer-is-python
pip install -e .

To install with development dependencies (includes pytest):

pip install -e ".[dev]"
# or, using hatch:
hatch env create

Contributor note: The package uses a src/ layout — the importable package lives under src/layeris/. Run pip install -e . from the repo root for an editable install that picks up changes automatically. Tests live in tests/ and are discovered by pytest automatically.

Quick Start

Load an image

from layeris import LayerImage

# From a local file
image = LayerImage.from_file('/path/to/image.jpg')

# From a URL
image = LayerImage.from_url('https://example.com/photo.jpg')

# From a NumPy array (float, values in [0, 1], shape HxWx3 or HxWx4)
import numpy as np
data = np.random.random((100, 100, 3))
image = LayerImage.from_array(data)

Save an image

# Default quality is 95 for JPEG
image.save('output.jpg')

# Specify JPEG quality (0–100)
image.save('output.jpg', quality=85)

# Save as PNG (lossless)
image.save('output.png')

Apply operations

All operations mutate the image in place and return self, so calls can be chained:

result = (
    LayerImage.from_file('photo.jpg')
    .grayscale()
    .brightness(0.1)
    .multiply('#3fe28f', opacity=0.7)
    .curve('rgb', [0, 0.2, 0.8, 1])
    .save('output.jpg')
)

Use clone() to branch off an independent copy first:

copy = image.clone()
copy.darken('#000000')  # original is unaffected

Every available operation is shown with a before/after preview in the operation gallery below.

Operation gallery

Every image below is generated by scripts/generate_previews.py using layeris itself — the left half is the input, the right half is the result of the snippet above it. Regenerate them all with:

python scripts/generate_previews.py

Blend mode descriptions are adapted from Adobe's blending modes documentation.

Basic adjustments

Grayscale

Convert to grayscale using the ITU-R 601 luminance weights.

image.grayscale()

grayscale

Brightness

Scale pixel values: a factor above 0 brightens, below 0 darkens.

image.brightness(0.2)

brightness

Contrast

Expand or compress the range around mid-gray: a factor above 1 increases contrast, between 0 and 1 decreases it.

image.contrast(1.5)

contrast

Hue

Set every pixel's hue to the target value in [0, 1] while keeping saturation and value — a colorize-style adjustment.

image.hue(0.3)

hue

Saturation

Scale color intensity: a factor above 0 boosts saturation, below 0 mutes it (-1 fully desaturates).

image.saturation(-0.5)

saturation

Lightness

Blend toward white (factor above 0) or black (factor below 0).

image.lightness(0.4)

lightness

Curve (all channels)

Remap tones through control points spaced evenly across [0, 1] — here an S-curve that deepens shadows and lifts highlights.

image.curve('rgb', [0, 0.1, 0.4, 0.65, 0.9, 1])

curve_rgb

Curve (single channel)

The same curve machinery applied to the red channel only.

image.curve('r', [0, 0.2, 0.8, 1])

curve_red

Resize

Resize with high-quality Lanczos resampling.

image.resize(width=240, height=160)

resize

Blend modes — darken

Blend modes accept a hex color string or a NumPy array, plus an optional opacity (0.0–1.0). The darken-group examples are applied to a grayscale base (grayscale_image = image.clone().grayscale()), as in the original demo.

Darken

Looks at the color information in each channel and selects the base or blend color — whichever is darker — as the result color.

grayscale_image.darken('#3fe28f')

darken

Multiply

Multiplies the base color by the blend color. The result is always darker; multiplying with white leaves the color unchanged.

grayscale_image.multiply('#3fe28f')

multiply

Color Burn

Darkens the base color to reflect the blend color by increasing the contrast between the two. Blending with white produces no change.

grayscale_image.color_burn('#7fe3f8')

color_burn

Linear Burn

Darkens the base color to reflect the blend color by decreasing the brightness. Blending with white produces no change.

grayscale_image.linear_burn('#e1a8ff')

linear_burn

Blend modes — lighten

Lighten

Selects the base or blend color — whichever is lighter — as the result color.

image.lighten('#ff3ce1')

lighten

Screen

Multiplies the inverse of the blend and base colors. The result is always lighter — like projecting multiple slides on top of each other.

image.screen('#e633ba')

screen

Color Dodge

Brightens the base color to reflect the blend color by decreasing contrast between the two. Blending with black produces no change.

image.color_dodge('#490cc7')

color_dodge

Linear Dodge

Brightens the base color to reflect the blend color by increasing the brightness. Blending with black produces no change.

image.linear_dodge('#490cc7')

linear_dodge

Blend modes — contrast

Overlay

Multiplies or screens the colors depending on the base color, preserving its highlights and shadows.

image.overlay('#ffb956')

overlay

Soft Light

Darkens or lightens depending on the blend color — like shining a diffused spotlight on the image.

image.soft_light('#ff3cbc')

soft_light

Hard Light

Multiplies or screens depending on the blend color — like shining a harsh spotlight on the image.

image.hard_light('#df5dff')

hard_light

Vivid Light

Burns or dodges by increasing or decreasing the contrast, depending on the blend color.

image.vivid_light('#ac5b7f')

vivid_light

Linear Light

Burns or dodges by decreasing or increasing the brightness, depending on the blend color.

image.linear_light('#9fa500')

linear_light

Pin Light

Replaces colors depending on the blend color — useful for adding special effects to an image.

image.pin_light('#005546')

pin_light

Putting it together

Method chaining

Every operation returns the LayerImage itself, so a whole look can be built as one chained pipeline.

(image.grayscale()
      .brightness(0.1)
      .multiply('#3fe28f', opacity=0.7)
      .curve('rgb', [0, 0.1, 0.4, 0.65, 0.9, 1]))

chaining


More usage

Get image data as a NumPy array

arr = image.get_image_as_array()
# arr.shape  → (height, width, 3)  for RGB
#            → (height, width, 4)  for RGBA
# arr.dtype  → float64
# values in [0.0, 1.0]

Apply operations from a dictionary or JSON file

Operations can be described as a Python dict (or a JSON file) and applied in sequence:

ops = {
    "operations": [
        {"type": "grayscale"},
        {"type": "brightness", "factor": 0.15},
        {"type": "multiply", "hex": "#3fe28f", "opacity": 0.6},
        {"type": "curve", "channels": "rgb", "curve_points": [0, 0.1, 0.9, 1]},
        {"type": "resize", "width": 800, "height": 600},
    ]
}

image.apply_from_dict(ops)

# Or load directly from a JSON file
image.apply_from_json('pipeline.json')

Supported operation types: grayscale, resize, brightness, contrast, hue, saturation, lightness, curve, darken, multiply, color_burn, linear_burn, lighten, screen, color_dodge, linear_dodge, overlay, soft_light, hard_light, vivid_light, linear_light, pin_light.


Running tests

Install development dependencies and run the test suite with pytest:

# Using pip (editable install with dev extras)
pip install -e ".[dev]"
pytest

# Using hatch
hatch run test

# With coverage report
hatch run test-cov

Run a specific test file or test:

pytest tests/test_layer_image.py
pytest tests/test_utils.py
pytest tests/test_layer_image.py::TestBlendModes::test_multiply_by_white_no_change -v

Test images used by the suite are stored in tests/images/.


Blend mode reference

Group Method Formula (A = base, B = blend)
Darken darken min(A, B)
Darken multiply A × B
Darken color_burn 1 − (1 − A) / B
Darken linear_burn A + B − 1
Lighten lighten max(A, B)
Lighten screen 1 − (1 − A)(1 − B)
Lighten color_dodge A / (1 − B)
Lighten linear_dodge A + B
Contrast overlay 2AB if A ≤ 0.5 else 1 − 2(1−A)(1−B)
Contrast soft_light Pegtop formula
Contrast hard_light 2AB if B ≤ 0.5 else 1 − 2(1−A)(1−B)
Contrast vivid_light Color burn / dodge based on B
Contrast linear_light A + 2B − 1
Contrast pin_light Conditional replace

All results are clamped to [0, 1].


Docs site (GitHub Pages)

The preview generator also produces a standalone gallery page at docs/index.html. To publish it with GitHub Pages, enable Settings → Pages → Deploy from a branch and select the main branch with the /docs folder — no build step required. The page layout lives in scripts/generate_previews.py and can be customised freely.


Roadmap

  • Imitate Photoshop's auto brightness & auto contrast features
  • Add filter presets

About

Image processing library that supports Photoshop blend modes and image operations

Resources

License

Stars

24 stars

Watchers

2 watching

Forks

Contributors