gimppy
A Python class library extracted from GIMP source code, providing programmatic image manipulation with an API modeled after GIMP's internal architecture.
Python >= 3.8 | numpy | Pillow (optional) | GPL-3.0
Table of Contents
Overview
gimppy brings GIMP's powerful image processing capabilities to Python. Every major subsystem — layers, selections, paths, painting, color adjustments, transforms, filters, undo — is implemented as a Python class that mirrors the corresponding GIMP C structure.
Key design principles:
- GIMP-faithful API — Class names, method names, and behavior follow GIMP's internal architecture (
gimpimage.c,gimplayer.c,gimpselection.c, etc.) - numpy-native — All pixel data is stored as
numpy.ndarraywithuint8dtype in RGBA (4-channel) format - Pure Python — Image processing algorithms implemented from scratch; no OpenCV dependency
- Non-destructive — Filter stacks and undo system allow reversible editing workflows
- Pillow integration — Optional Pillow dependency for TrueType text rendering and extended file format support (JPEG, BMP, TIFF, WebP, PSD, GIF)
Installation
pip install gimppy
For text rendering and additional image format support (JPEG, BMP, TIFF, WebP, PSD, GIF):
pip install gimppy[pillow]
For development (includes test dependencies):
pip install gimppy[dev]
Quick Start
from gimppy import GimpImage
from gimppy.enums import ImageType, FillType, LayerMode
# Create a new 800x600 RGB image
img = GimpImage(800, 600, ImageType.RGB, name="My Image")
# Add layers
bg = img.new_layer("Background", FillType.WHITE)
fg = img.new_layer("Foreground")
fg.set_opacity(0.7)
fg.set_mode(LayerMode.MULTIPLY)
# Apply color adjustments
img.brightness_contrast(brightness=0.1, contrast=0.2)
img.adjust_hue_saturation(hue_offset=30, saturation=0.3)
# Save in multiple formats
img.save("output.png")
img.save("output.jpg", quality=95)
Architecture
gimppy mirrors GIMP's internal object hierarchy:
GimpImage
├── layers[] → GimpLayer / GimpGroupLayer
│ ├── pixels → numpy ndarray (H x W x 4, RGBA uint8)
│ ├── mask → GimpLayerMask
│ ├── opacity → float (0.0 - 1.0)
│ └── mode → LayerMode enum
├── channels[] → GimpChannel
├── paths[] → GimpPath
│ └── strokes[] → Stroke
│ └── anchors[] → Anchor
├── selection → GimpSelection (extends GimpChannel)
├── guides[] → {orientation, position}
├── grid → {spacing, offset, style}
── undo_stack → UndoStack
│ ├── undo[] → UndoStep / UndoGroup
│ └── redo[] → UndoStep / UndoGroup
└── filter_stack → FilterStack
└── filters[] → GimpFilter
All pixel operations work on 4-channel RGBA arrays. When loading 3-channel RGB images (e.g., JPEG), an alpha channel is automatically added.
API Reference
GimpImage — Central Image Object
The GimpImage class is the entry point for all operations. It manages layers, channels, paths, selection, guides, grid, color profile, metadata, and the undo stack.
from gimppy import GimpImage
from gimppy.enums import ImageType, Precision
img = GimpImage(
width=800,
height=600,
image_type=ImageType.RGB, # RGB | GRAY | INDEXED
precision=Precision.U8, # U8 | U16 | U32 | HALF | FLOAT | DOUBLE
name="Untitled"
)
Properties:
| Property | Type | Description |
|---|---|---|
name |
str |
Image name |
width |
int |
Image width in pixels |
height |
int |
Image height in pixels |
image_type |
ImageType |
Color mode (RGB/GRAY/INDEXED) |
precision |
Precision |
Channel precision |
resolution_x |
float |
Horizontal DPI |
resolution_y |
float |
Vertical DPI |
unit |
str |
Distance unit |
active_layer |
GimpLayer |
Currently active layer |
active_channel |
GimpChannel |
Currently active channel |
active_path |
GimpPath |
Currently active path |
fg_color |
tuple |
Foreground color (R, G, B, A) |
bg_color |
tuple |
Background color (R, G, B, A) |
selection |
GimpSelection |
Active selection |
layers |
list |
All layers |
channels |
list |
All channels |
paths |
list |
All paths |
guides |
list |
Guide lines |
undo_stack |
UndoStack |
Undo history |
is_dirty |
bool |
Whether image has unsaved changes |
Layer Management:
# Create and add layers
layer = img.new_layer("Layer 1", fill_type=FillType.WHITE, opacity=1.0, mode=LayerMode.NORMAL)
group = img.new_group_layer("Group")
text_layer = img.new_text_layer("Hello", font="Sans-serif", size=24.0, color=(0, 0, 0))
visible_layer = img.new_layer_from_visible("Visible Composite")
img.add_layer(layer, position=-1) # -1 = top of stack
img.remove_layer(layer)
# Layer ordering
img.raise_item(layer)
img.lower_item(layer)
img.raise_item_to_top(layer)
img.lower_item_to_bottom(layer)
img.get_item_position(layer)
Image Operations:
# Duplicate / flatten / merge
dup = img.duplicate()
flat = img.flatten() # Returns flattened layer
merged = img.merge_visible_layers("Merged") # Merge all visible layers
img.merge_down(layer) # Merge layer with one below
# Resize / scale / crop
img.resize(1024, 768, offset_x=0, offset_y=0)
img.scale(1024, 768, interpolation=InterpolationType.CUBIC)
img.crop(10, 10, 200, 200)
img.autocrop()
img.resize_to_layers()
# Flip / rotate
from gimppy.enums import FlipType, RotationType
img.flip(FlipType.HORIZONTAL)
img.rotate(RotationType.ROTATE_90)
# Color mode conversion
img.convert_rgb()
img.convert_grayscale()
img.convert_indexed(max_colors=256, palette_type=ConvertPaletteType.GENERATE)
img.convert_precision(Precision.U16)
Color Adjustments (operate on active layer):
img.brightness_contrast(brightness=0.1, contrast=0.2)
img.adjust_levels(input_black=0.0, input_white=1.0, gamma=1.0)
img.auto_levels()
img.adjust_curves(curve_points=[(0.0, 0.0), (0.5, 0.7), (1.0, 1.0)])
img.adjust_hue_saturation(hue_offset=30, saturation=0.3, lightness=0.0)
img.adjust_color_balance(cyan_red=0.1, magenta_green=0.0, yellow_blue=-0.1)
img.desaturate(mode=DesaturateMode.LUMINANCE)
img.colorize(color=(0.6, 0.2, 0.8), lightness=0.5)
img.posterize(levels=4)
img.threshold(value=127)
img.invert()
img.equalize()
img.shadows_highlights(shadows=0.3, highlights=0.2)
Fill & Paint:
img.fill(FillType.FOREGROUND)
img.bucket_fill(x=100, y=100, fill_type=FillType.FOREGROUND, threshold=15)
img.gradient_fill(...)
img.stroke_selection(stroke_width=2.0, color=(255, 0, 0, 255))
img.offset(dx=10, dy=20, wrap=False)
Pixel Access:
color = img.pick_color(x=50, y=50, sample_merged=False)
projection = img.get_projection() # Composite of all visible layers
histogram = img.get_histogram(channel=HistogramChannel.VALUE)
Metadata, Guides, Grid:
img.set_color_profile(profile)
img.set_metadata("Author", "gimppy")
author = img.get_metadata("Author", default="Unknown")
guide = img.add_guide(GuideOrientation.HORIZONTAL, 50.0)
img.remove_guide(0)
img.set_grid_spacing(x=10.0, y=10.0)
img.set_grid_offset(x=0.0, y=0.0)
img.set_resolution(x=72.0, y=72.0)
Undo:
img.undo()
img.redo()
img.begin_undo_group("My Operation")
# ... operations ...
img.end_undo_group()
File I/O:
img.load("input.png")
img.save("output.png")
img.save("output.jpg", quality=95)
img.clean_all() # Clear undo history
Layers & Groups
GimpLayer
from gimppy import GimpLayer
from gimppy.enums import ImageType, FillType, LayerMode
layer = GimpLayer(
width=800,
height=600,
name="My Layer",
image_type=ImageType.RGB,
has_alpha=True,
fill_type=FillType.TRANSPARENT
)
Attributes:
| Attribute | Type | Description |
|---|---|---|
name |
str |
Layer name |
width / height |
int |
Layer dimensions |
pixels |
np.ndarray |
Pixel data (H x W x 4, RGBA uint8) |
opacity |
float |
Layer opacity (0.0 - 1.0) |
mode |
LayerMode |
Blend mode |
offset_x / offset_y |
int |
Layer offset from image origin |
visible |
bool |
Visibility flag |
lock_alpha |
bool |
Lock alpha channel |
lock_content |
bool |
Lock pixel content |
lock_position |
bool |
Lock position |
mask |
GimpLayerMask |
Attached layer mask |
Methods:
layer.set_offsets(x=10, y=20)
layer.set_opacity(0.8)
layer.set_mode(LayerMode.MULTIPLY)
# Layer mask
mask = layer.create_mask(from_color=False)
layer.add_mask(mask)
layer.remove_mask(MaskApplyMode.APPLY) # APPLY | DISCARD
# Alpha
layer.add_alpha()
layer.flatten()
# Transform
layer.scale(400, 300, interpolation="cubic")
layer.resize(1024, 768, offset_x=0, offset_y=0)
layer.resize_to_image_size(img.width, img.height)
# Pixel access
pixel = layer.get_pixel(x=50, y=50)
layer.set_pixel(x=50, y=50, color=(255, 0, 0, 255))
# Bounds
bounds = layer.bounds # (x1, y1, x2, y2)
# Copy
clone = layer.copy()
GimpGroupLayer
from gimppy import GimpGroupLayer
group = GimpGroupLayer(800, 600, name="My Group")
group.add_layer(child_layer, position=-1)
group.remove_layer(child_layer)
merged = group.merge()
all_layers = group.get_all_layers(recursive=True)
GimpLayerMask
from gimppy import GimpLayerMask
mask = GimpLayerMask(800, 600, name="Mask")
mask.fill(255) # Fill with value (0-255)
mask.invert()
mask.apply() # Apply mask to parent layer
mask.discard() # Remove without applying
mask.copy()
bounds = mask.bounds
Selections
GimpSelection extends GimpChannel with shape-based selection operations and boolean algebra.
from gimppy import GimpSelection
from gimppy.enums import SelectionOperation
sel = GimpSelection(800, 600)
# Shape selections
sel.select_all()
sel.select_none()
sel.select_invert()
sel.select_rectangle(x=10, y=10, width=100, height=100,
operation=SelectionOperation.REPLACE, antialias=True)
sel.select_ellipse(x=10, y=10, width=100, height=100,
operation=SelectionOperation.ADD, antialias=True)
sel.select_polygon(points=[(10,10), (100,10), (100,100), (10,100)],
operation=SelectionOperation.REPLACE, antialias=True)
sel.select_round_rectangle(x=10, y=10, width=100, height=100,
corner_radius=10, operation=SelectionOperation.REPLACE)
# Color-based selections
sel.select_by_color(image_pixels, x=50, y=50, threshold=15, antialias=True)
sel.select_contiguous_color(image_pixels, x=50, y=50, threshold=15)
# Modify selection
sel.feather(radius=5.0)
sel.sharpen()
sel.border(radius=3)
sel.grow(radius=5)
sel.shrink(radius=3)
sel.flood(x=50, y=50)
sel.translate(dx=10, dy=20)
# Suspend / resume (for batch operations)
sel.suspend()
# ... multiple operations ...
sel.resume()
# Query
bounds = sel.get_bounds()
boundary = sel.get_boundary() # List of contour point lists
sel.save_to_channel(channel) # Save selection to a channel
SelectionOperation values: REPLACE, ADD, SUBTRACT, INTERSECT
Channels
from gimppy import GimpChannel
ch = GimpChannel(800, 600, name="Alpha Copy", opacity=1.0, color=(255, 0, 0))
ch.fill(255)
ch.clear()
ch.invert()
ch.feather(radius=5.0)
ch.grow(radius=3)
ch.shrink(radius=3)
ch.combine(other_channel, operation="add") # "add" | "subtract" | "intersect"
ch.scale(400, 300)
ch.resize(1024, 768, offset_x=0, offset_y=0)
val = ch.get_value(x=50, y=50)
ch.set_value(x=50, y=50, value=200)
is_empty = ch.is_empty
bounds = ch.bounds
clone = ch.copy()
Color Adjustments
All color functions operate on numpy.ndarray pixel data and return a new array.
from gimppy import color
from gimppy.enums import DesaturateMode, HistogramChannel
# Brightness / Contrast (range: -1.0 to 1.0)
result = color.brightness_contrast(pixels, brightness=0.2, contrast=0.3)
# Levels
result = color.levels(pixels, input_black=0.0, input_white=1.0,
gamma=1.0, output_black=0.0, output_white=1.0, channel=-1)
# Auto levels (stretch histogram)
result = color.levels_stretch(pixels)
# Curves (list of (input, output) control points, 0.0-1.0)
result = color.curves(pixels, curve_points=[(0.0, 0.0), (0.5, 0.7), (1.0, 1.0)], channel=-1)
# Hue / Saturation (hue: -180 to 180, saturation/lightness: -1.0 to 1.0)
result = color.hue_saturation(pixels, hue_offset=90, saturation=0.5, lightness=0.0)
# Colorize (color as HSL tuple, lightness: 0.0 to 1.0)
result = color.colorize(pixels, color=(0.6, 0.8, 0.4), lightness=0.5)
# Desaturate
result = color.desaturate(pixels, mode=DesaturateMode.LUMINANCE)
# DesaturateMode: LIGHTNESS | LUMINANCE | VALUE
# Posterize (reduce to N levels)
result = color.posterize(pixels, levels=4)
# Threshold (binary at value, 0-255)
result = color.threshold(pixels, value=128)
# Invert
result = color.invert(pixels)
# Equalize (histogram equalization)
result = color.equalize(pixels)
# Color Balance (range: -1.0 to 1.0)
result = color.color_balance(pixels, cyan_red=0.2, magenta_green=0.0, yellow_blue=-0.1,
shadows=True, midtones=True, highlights=True)
# Shadows / Highlights
result = color.shadows_highlights(pixels, shadows=0.3, highlights=0.2,
white_point=0.0, radius=100.0, compress=0.5)
# Histogram (returns 256-element array)
hist = color.compute_histogram(pixels, channel=HistogramChannel.VALUE)
# HistogramChannel: VALUE | RED | GREEN | BLUE | ALPHA
Geometric Transforms
from gimppy.transform import (
transform_layer, flip_layer, rotate_layer, scale_layer,
translate_layer, shear_layer, perspective_layer
)
from gimppy.enums import FlipType, InterpolationType, TransformResize
from gimppy import Matrix3
# Affine transform via 3x3 matrix
matrix = Matrix3.translate(50, 30) @ Matrix3.scale(1.5, 1.5) @ Matrix3.rotate(0.785)
transform_layer(layer, matrix, interpolation=InterpolationType.CUBIC,
resize=TransformResize.ADJUST)
# Convenience transforms
flip_layer(layer, FlipType.HORIZONTAL)
rotate_layer(layer, angle_deg=45.0)
scale_layer(layer, new_width=400, new_height=300)
translate_layer(layer, dx=10, dy=20)
shear_layer(layer, shear_x=0.3, shear_y=0.0)
# Perspective transform (4 source corners -> 4 destination corners)
perspective_layer(layer,
src_corners=[(0,0), (100,0), (100,100), (0,100)],
dst_corners=[(10,10), (90,0), (100,90), (0,100)])
# Coordinate transform
new_x, new_y = transform_image_coords(matrix, x=50, y=50)
InterpolationType: NONE (nearest), LINEAR (bilinear), CUBIC (bicubic), NOHALO, LOHALO
TransformResize: ADJUST, CLIP, CROP, CROP_WITH_ASPECT
Paint Engine
Brush
from gimppy import Brush
from gimppy.enums import BrushShape
brush = Brush(
size=20.0,
hardness=0.8, # 0.0 (soft) to 1.0 (hard)
shape=BrushShape.CIRCLE, # CIRCLE | SQUARE | DIAMOND
angle=0.0, # Rotation angle in degrees
aspect_ratio=1.0, # Elliptical stretch
spacing=0.1 # Spacing as fraction of size
)
mask = brush.get_mask() # 2D float32 brush stamp
mask = brush.get_mask(size_override=30) # Override size
PaintOptions
from gimppy import PaintOptions
opts = PaintOptions()
opts.opacity = 0.8
opts.brush = Brush(size=15, hardness=0.7)
opts.flow = 1.0
opts.rate = 50
opts.pressure_opacity = True
opts.pressure_size = False
Stroke Functions
All stroke functions take a GimpLayer and a list of (x, y) control points.
from gimppy import paint
from gimppy.enums import ConvolveType, DodgeBurnType
points = [(10, 10), (100, 50), (200, 30), (300, 80)]
color_rgba = (255, 0, 0, 255)
# Paintbrush — soft-edged brush stroke
paint.paintbrush_stroke(layer, points, color=color_rgba, options=opts)
# Pencil — hard-edged, aliased stroke
paint.pencil_stroke(layer, points, color=color_rgba, options=opts)
# Eraser — erase to transparent
paint.eraser_stroke(layer, points, options=opts)
# Airbrush — pressure-sensitive spray
paint.airbrush_stroke(layer, points, color=color_rgba, options=opts, pressure=1.0)
# Clone — copy pixels from source
paint.clone_stroke(layer, source_pixels=src_layer.pixels, points=points,
source_offset=(0, 0), options=opts)
# Heal — clone with color blending
paint.heal_stroke(layer, source_pixels=src_layer.pixels, points=points,
source_offset=(0, 0), options=opts)
# Smudge — smear existing pixels
paint.smudge_stroke(layer, points, rate=0.5, options=opts)
# Convolve — blur or sharpen
paint.convolve_stroke(layer, points, convolve_type=ConvolveType.BLUR, options=opts)
# ConvolveType: BLUR | SHARPEN
# Dodge / Burn — lighten or darken
paint.dodge_burn_stroke(layer, points, db_type=DodgeBurnType.DODGE,
intensity=0.5, options=opts)
# DodgeBurnType: DODGE | BURN
Vector Paths
Stroke & Anchor
from gimppy import Stroke, Anchor
s = Stroke(closed=False)
# Add anchors
s.moveto(10, 10) # Move to starting point
s.lineto(100, 10) # Straight line
s.conicto(50, 50, 100, 100) # Quadratic Bezier
s.cubicto(30, 0, 70, 100, 100, 50) # Cubic Bezier
s.close() # Close the path
# Query
length = s.get_length(precision=100)
point = s.get_point_at_distance(dist=50.0)
interpolated = s.interpolate(precision=100) # List of (x, y)
bounds = s.get_bounds()
# Transforms
s.translate(dx=10, dy=20)
s.scale(sx=2.0, sy=1.5)
s.rotate(angle_rad=0.785)
s.flip(axis="horizontal", center=0.0)
s.reverse()
# Static constructors
s = Stroke.from_points([(10,10), (100,10), (100,100)], closed=True)
s = Stroke.ellipse(cx=100, cy=100, rx=50, ry=30)
GimpPath
from gimppy import GimpPath
path = GimpPath("My Path")
path.add_stroke(stroke)
path.remove_stroke(0)
stroke = path.get_stroke(0)
count = path.stroke_count
bounds = path.get_bounds()
# Transforms
path.translate(dx=10, dy=20)
path.scale(sx=2.0, sy=1.5)
path.rotate(angle_rad=0.785)
clone = path.copy()
# Export
mask = path.to_selection_mask(width=800, height=600) # 2D uint8 array
svg = path.to_svg() # SVG path string
Text Rendering
Text rendering uses Pillow's TrueType font engine with automatic font fallback.
from gimppy import GimpText, GimpTextLayer
from gimppy.enums import TextJustification, TextDirection
# Create text object
text = GimpText(
text="Hello, World!",
font="Sans-serif", # Font family name or file path
size=24.0, # Font size in points
color=(0, 0, 0) # RGB tuple
)
# Text properties
text.antialias = True
text.hint_style = "slight"
text.kerning = True
text.language = "en"
text.direction = TextDirection.LTR
text.justification = TextJustification.LEFT
text.indent = 0.0
text.line_spacing = 0.0
text.letter_spacing = 0.0
text.outline = False
text.outline_width = 1.0
text.outline_color = (0, 0, 0)
# Update text
text.set_text("New text")
text.set_markup("<b>Bold</b> text")
display = text.get_display_text() # Strips markup tags
# Create text layer
tl = GimpTextLayer(text, name="Title")
# Properties
w = tl.width # Actual rendered width
h = tl.height # Actual rendered height
pixels = tl.pixels # RGBA numpy array
dynamic = tl.is_dynamic
# Operations
tl.set_text("Updated")
tl.set_markup("<i>Italic</i>")
tl.resize(400, 100)
tl.rasterize() # Convert to static pixels
offsets = tl.get_layout_offsets()
clone = tl.copy()
Font resolution order: The specified font name is matched against system fonts in this order:
- Windows:
C:\Windows\Fonts\ - macOS:
/Library/Fonts,/System/Library/Fonts,~/Library/Fonts - Linux:
/usr/share/fonts,/usr/local/share/fonts,~/.fonts
Common font family names: "Sans-serif", "Serif", "Monospace"
Resources
GimpBrush
from gimppy import GimpBrush
from gimppy.enums import BrushShape
brush = GimpBrush(
name="My Brush",
shape=BrushShape.CIRCLE,
radius=10.0,
hardness=0.8,
spacing=0.1,
aspect_ratio=1.0,
angle=0.0
)
mask = brush.get_mask()
info = brush.get_info()
is_gen = brush.is_generated
GimpPattern
from gimppy import GimpPattern
import numpy as np
pattern_pixels = np.tile(np.array([[255, 0, 0, 255], [0, 255, 0, 255]], dtype=np.uint8), (5, 5, 1))
pattern = GimpPattern(name="Checker", pixels=pattern_pixels)
w = pattern.width
h = pattern.height
GimpGradient
from gimppy import GimpGradient
from gimppy.enums import GradientType
grad = GimpGradient("Sunset")
grad.add_segment(0.0, 0.5,
left_color=(255, 0, 0, 255),
right_color=(255, 255, 0, 255))
grad.add_segment(0.5, 1.0,
left_color=(255, 255, 0, 255),
right_color=(128, 0, 255, 255))
color = grad.get_color_at(0.3) # RGBA tuple at position 0.0-1.0
grad.replicate(count=3) # Repeat gradient N times
grad.reverse() # Reverse gradient direction
# Render gradient to image
rendered = grad.render(800, 100, gradient_type=GradientType.LINEAR,
x1=0, y1=0, x2=800, y2=0)
GradientType: LINEAR, BI_LINEAR, RADIAL, SQUARE, CONICAL_SYMMETRIC, CONICAL_ASYMMETRIC, SHAPEBURST_ANGULAR, SHAPEBURST_SPHERICAL, SHAPEBURST_DIMPLED, SPIRAL_CLOCKWISE, SPIRAL_COUNTERCLOCKWISE
GimpPalette
from gimppy import GimpPalette
pal = GimpPalette("Web Safe")
pal.add_color(255, 0, 0, "Red")
pal.add_color(0, 255, 0, "Green")
pal.add_color(0, 0, 255, "Blue")
count = pal.color_count
r, g, b, name = pal.get_color(0)
pal.remove_color(1)
index = pal.find_closest(200, 50, 50)
GimpDynamics
from gimppy import GimpDynamics
dyn = GimpDynamics("Pressure Sensitive")
dyn.pressure_opacity = 1.0
dyn.pressure_size = 0.8
dyn.pressure_hardness = 0.5
dyn.velocity_opacity = 0.0
dyn.velocity_size = 0.0
dyn.random_opacity = 0.0
dyn.random_size = 0.0
dyn.fade_opacity = 0.0
dyn.fade_length = 0
result = dyn.apply(pressure=0.7, velocity=10.0, distance=50.0)
# Returns {"opacity": float, "size": float, "hardness": float}
Filters
Non-destructive filter system with composable filter stacks.
from gimppy import GimpFilter, FilterStack
from gimppy import color
# Create individual filters
f1 = GimpFilter("Brighten", lambda p: color.brightness_contrast(p, brightness=0.2))
f2 = GimpFilter("Sharpen", lambda p: color.levels_stretch(p))
f3 = GimpFilter("Invert", color.invert)
# Build a filter stack
stack = FilterStack()
stack.add(f1)
stack.add(f2)
stack.add(f3)
# Apply all filters in sequence
result = stack.apply_all(pixels)
# Manage filters
f = stack.get(0)
stack.remove(f1)
stack.clear()
print(len(stack))
# Filter with region restriction
f1.set_region(x=10, y=10, width=100, height=100)
f1.set_param("brightness", 0.3)
val = f1.get_param("brightness", default=0.0)
# Convenience filter functions
from gimppy.filter import (
apply_brightness_contrast, apply_levels, apply_hue_saturation,
apply_color_balance, apply_desaturate, apply_posterize,
apply_threshold, apply_equalize, apply_invert,
apply_shadows_highlights, create_filter
)
result = apply_brightness_contrast(pixels, brightness=0.1, contrast=0.2)
result = apply_hue_saturation(pixels, hue_offset=30, saturation=0.3)
f = create_filter("MyFilter", lambda p: color.invert(p), brightness=0.5)
Undo System
Full undo/redo with grouping, freeze/thaw, and configurable depth.
from gimppy import UndoStack, UndoStep, UndoType
from gimppy import UndoGroup
# Create undo stack
stack = UndoStack(max_steps=50)
# Push a step
def undo_fn():
layer.pixels[:] = old_pixels
def redo_fn():
layer.pixels[:] = new_pixels
step = UndoStep(UndoType.LAYER_PIXELS, "Paint Stroke", undo_fn, redo_fn)
stack.push(step)
# Undo / Redo
name = stack.undo() # Returns step name
name = stack.redo()
# Query
can_undo = stack.can_undo
can_redo = stack.can_redo
print(stack.undo_count)
print(stack.redo_count)
print(stack.get_undo_names())
print(stack.get_redo_names())
# Group operations
stack.begin_group("Complex Operation")
stack.push(UndoStep(UndoType.LAYER_PIXELS, "Step 1", undo1, redo1))
stack.push(UndoStep(UndoType.LAYER_PIXELS, "Step 2", undo2, redo2))
stack.end_group()
# One undo() now undoes both steps
# Enable / Disable / Freeze / Thaw
stack.disable() # Pushes are ignored
stack.enable()
stack.freeze() # Pushes are queued but not executed
stack.thaw()
stack.clear() # Clear all history
UndoType values: LAYER_PIXELS, LAYER_PROPERTY, CHANNEL_PIXELS, CHANNEL_PROPERTY, IMAGE_PROPERTY, ITEM_ADD, ITEM_REMOVE, ITEM_REORDER, SELECTION, FILTER, GROUP
Math Utilities
Matrix3 — 3x3 Affine Transformation Matrix
from gimppy import Matrix3
# Identity
m = Matrix3.identity()
# Factory methods
m = Matrix3.translate(tx=50, ty=30)
m = Matrix3.scale(sx=2.0, sy=1.5)
m = Matrix3.rotate(angle_rad=0.785) # 45 degrees
m = Matrix3.shear(sx=0.3, sy=0.0)
# Perspective transform (4 source -> 4 destination corners)
m = Matrix3.perspective(
0, 0, 100, 0, 100, 100, 0, 100, # source corners
10, 10, 90, 0, 100, 90, 0, 100 # destination corners
)
# Compose transforms (matrix multiplication)
m = Matrix3.translate(50, 30) @ Matrix3.scale(1.5, 1.5) @ Matrix3.rotate(0.785)
# Transform a point
new_x, new_y = m.transform_point(x=100, y=100)
# Matrix properties
det = m.determinant()
inv = m.inverse()
val = m[0] # Access element by index (row-major)
Vector2 / Vector3
from gimppy import Vector2, Vector3
v2 = Vector2(3.0, 4.0)
print(v2.length()) # 5.0
v2n = v2.normalize()
dot = v2.dot(Vector2(1.0, 0.0))
cross = v2.cross(Vector2(0.0, 1.0))
dist = v2.distance(Vector2(6.0, 8.0))
v3 = Vector3(1.0, 2.0, 3.0)
v3n = v3.normalize()
cross3 = v3.cross(Vector3(0.0, 1.0, 0.0))
# Arithmetic
v = Vector2(1, 2) + Vector2(3, 4) # Vector2(4, 6)
v = Vector2(1, 2) - Vector2(3, 4) # Vector2(-2, -2)
v = Vector2(1, 2) * 3.0 # Vector2(3, 6)
File I/O
from gimppy.fileio import load_image, save_image, get_supported_formats
# Load an image
data = load_image("photo.png")
# Returns: {
# "width": int,
# "height": int,
# "image_type": ImageType,
# "precision": Precision,
# "layers": [{"pixels": ndarray, "name": str, "has_alpha": bool}, ...],
# "channels": [...],
# "source_format": str
# }
# Save an image
save_image("output.jpg", data, quality=95)
save_image("output.png", data, compress_level=6)
# Supported formats
formats = get_supported_formats()
# Always: .xcf, .bmp, .ico
# With Pillow: .png, .jpg, .jpeg, .tiff, .tif, .webp, .gif, .psd, .ppm, .pgm, .pbm
Enumerations
gimppy provides 30+ enumeration classes covering all GIMP concepts. Import from gimppy.enums:
from gimppy.enums import (
# Image
ImageType, # RGB | GRAY | INDEXED
Precision, # U8 | U16 | U32 | HALF | FLOAT | DOUBLE
# Layers
LayerMode, # NORMAL | MULTIPLY | SCREEN | OVERLAY | ... (40+ modes)
FillType, # FOREGROUND | BACKGROUND | WHITE | TRANSPARENT | PATTERN
CompositeSpace, # AUTO | RGB_LINEAR | RGB_PERCEPTUAL | LAB
CompositeMode, # AUTO | UNION | CLIP_TO_BACKDROP | CLIP_TO_LAYER | INTERSECTION
MaskApplyMode, # APPLY | DISCARD
# Selection
SelectionOperation, # REPLACE | ADD | SUBTRACT | INTERSECT
# Color
DesaturateMode, # LIGHTNESS | LUMINANCE | VALUE
HistogramChannel, # VALUE | RED | GREEN | BLUE | ALPHA
ConvertPaletteType, # GENERATE | MONO | CUSTOM | WEB
ConvertDitherType, # NONE | FS | FS_LOW_BLEED | POSITIONED
# Transforms
InterpolationType, # NONE | LINEAR | CUBIC | NOHALO | LOHALO
TransformResize, # ADJUST | CLIP | CROP | CROP_WITH_ASPECT
FlipType, # HORIZONTAL | VERTICAL
RotationType, # ROTATE_90 | ROTATE_180 | ROTATE_270
# Paint
BrushShape, # CIRCLE | SQUARE | DIAMOND
PaintMode, # NORMAL | BEHIND | ERASE
ConvolveType, # BLUR | SHARPEN
DodgeBurnType, # DODGE | BURN
# Paths
StrokeMethod, # LINE | PAINT_TOOL
LineCapStyle, # BUTT | ROUND | SQUARE
LineJoinStyle, # MITER | ROUND | BEVEL
# Gradients
GradientType, # LINEAR | RADIAL | CONICAL_SYMMETRIC | ... (11 types)
RepeatMode, # NONE | SAWTOOTH | TRIANGULAR
BlendColorSpace, # AUTO | RGB_LINEAR | RGB_PERCEPTUAL
# Guides / Grid
GuideOrientation, # HORIZONTAL | VERTICAL
GridStyle, # DOTS | INTERSECTIONS | ON_OFF_DASH | SOLID
# Text
TextJustification, # LEFT | CENTER | RIGHT | FILL
TextDirection, # LTR | RTL | TTB_RTL | ...
# Channels
ChannelType, # RED | GREEN | BLUE | ALPHA
# Misc
BucketFillArea, # SIMILAR_COLORS | BY_LINE_ART
OrientationType, # HORIZONTAL | VERTICAL
InkBlobType, # CIRCLE | SQUARE | DIAMOND
)
LayerMode includes 40+ blend modes: NORMAL, DISSOLVE, BEHIND, ERASE, ANTI_ERASE, MERGE, SPLIT, PASS_THROUGH, REPLACE, OVERWRITE, ADDITION, SUBTRACT, MULTIPLY, DIVIDE, SCREEN, OVERLAY, DARKEN_ONLY, LIGHTEN_ONLY, DIFFERENCE, EXCLUSION, SOFTLIGHT, GRAIN_EXTRACT, GRAIN_MERGE, COLOR_DODGE, COLOR_BURN, LINEAR_DODGE, LINEAR_BURN, VIVID_LIGHT, LINEAR_LIGHT, PIN_LIGHT, HARD_MIX, HSV_HUE, HSV_SATURATION, HSL_COLOR, HSV_VALUE, LCH_HUE, LCH_CHROMA, LCH_COLOR, LCH_LIGHTNESS, plus 15 LEGACY_* variants.
Exceptions
All exceptions inherit from GimpError:
from gimppy.exceptions import (
GimpError, # Base exception
ImageError, # Image operation errors
LayerError, # Layer operation errors
ChannelError, # Channel operation errors
SelectionError, # Selection operation errors
FileIOError, # File read/write errors
ColorError, # Color adjustment errors
TransformError, # Transform operation errors
PaintError, # Paint engine errors
FilterError, # Filter operation errors
PathError, # Path operation errors
TextError, # Text rendering errors
UndoError, # Undo system errors
ResourceError, # Resource operation errors
ValidationError, # Parameter validation errors
)
try:
img.undo()
except UndoError as e:
print(f"Cannot undo: {e}")
Examples
Batch Image Processing
from gimppy import GimpImage
from gimppy.fileio import load_image, save_image
import os
input_dir = "photos/"
output_dir = "processed/"
os.makedirs(output_dir, exist_ok=True)
for filename in os.listdir(input_dir):
if not filename.endswith((".jpg", ".png")):
continue
data = load_image(os.path.join(input_dir, filename))
img = GimpImage(data["width"], data["height"], data["image_type"])
for ld in data["layers"]:
from gimppy import GimpLayer
layer = GimpLayer(ld["pixels"].shape[1], ld["pixels"].shape[0],
ld.get("name", "Layer"), img.image_type)
layer.pixels = ld["pixels"]
img.add_layer(layer)
# Apply adjustments
img.brightness_contrast(brightness=0.1, contrast=0.15)
img.adjust_hue_saturation(saturation=0.2)
out_path = os.path.join(output_dir, filename.replace(".jpg", "_edited.png"))
img.save(out_path)
print(f"Processed: {filename}")
Layer Compositing with Blend Modes
from gimppy import GimpImage, GimpLayer
from gimppy.enums import ImageType, FillType, LayerMode
import numpy as np
img = GimpImage(512, 512, ImageType.RGB)
# Background: white
bg = img.new_layer("Background", FillType.WHITE)
# Red circle with Normal blend
red = img.new_layer("Red Circle")
red.set_mode(LayerMode.NORMAL)
red.set_opacity(0.8)
yy, xx = np.ogrid[:512, :512]
mask = (xx - 200)**2 + (yy - 200)**2 <= 150**2
red.pixels[mask, 0] = 255
red.pixels[mask, 3] = 204 # 80% opacity
# Blue circle with Multiply blend
blue = img.new_layer("Blue Circle")
blue.set_mode(LayerMode.MULTIPLY)
blue.set_opacity(0.6)
mask2 = (xx - 312)**2 + (yy - 312)**2 <= 150**2
blue.pixels[mask2, 2] = 255
blue.pixels[mask2, 3] = 153
img.save("composite.png")
Non-Destructive Filter Workflow
from gimppy import GimpImage, GimpFilter, FilterStack
from gimppy import color
from gimppy.fileio import load_image
data = load_image("photo.jpg")
img = GimpImage(data["width"], data["height"], data["image_type"])
from gimppy import GimpLayer
layer = GimpLayer(data["layers"][0]["pixels"].shape[1],
data["layers"][0]["pixels"].shape[0], "Photo", img.image_type)
layer.pixels = data["layers"][0]["pixels"]
img.add_layer(layer)
# Build a non-destructive filter stack
stack = FilterStack()
stack.add(GimpFilter("Warm", lambda p: color.color_balance(p, cyan_red=0.15)))
stack.add(GimpFilter("Contrast", lambda p: color.brightness_contrast(p, contrast=0.2)))
stack.add(GimpFilter("Vivid", lambda p: color.hue_saturation(p, saturation=0.3)))
# Apply
result = stack.apply_all(layer.pixels)
layer.pixels = result
img.save("filtered.png")
Undo-Aware Editing
from gimppy import GimpImage
from gimppy.enums import FillType
img = GimpImage(400, 400)
layer = img.new_layer("Canvas", FillType.WHITE)
# Group a series of operations as a single undo step
img.begin_undo_group("Artistic Effect")
img.invert()
img.brightness_contrast(brightness=0.2)
img.adjust_hue_saturation(hue_offset=45)
img.end_undo_group()
# One undo reverts the entire group
img.undo() # Back to white canvas
img.redo() # Re-apply the group
Path to Selection
from gimppy import GimpImage, GimpPath, Stroke
from gimppy.enums import FillType
img = GimpImage(400, 400)
layer = img.new_layer("Canvas", FillType.WHITE)
# Create a star-shaped path
path = GimpPath("Star")
s = Stroke(closed=True)
import math
for i in range(10):
angle = math.pi * 2 * i / 10 - math.pi / 2
r = 150 if i % 2 == 0 else 70
x = 200 + r * math.cos(angle)
y = 200 + r * math.sin(angle)
if i == 0:
s.moveto(x, y)
else:
s.lineto(x, y)
s.close()
path.add_stroke(s)
# Convert path to selection mask and fill
mask = path.to_selection_mask(400, 400)
layer.pixels[mask > 0] = [255, 200, 0, 255]
img.save("star.png")
Requirements
| Dependency | Version | Required | Purpose |
|---|---|---|---|
| Python | >= 3.8 | Yes | Runtime |
| numpy | >= 1.20 | Yes | Pixel data arrays |
| Pillow | >= 9.0 | Optional | Text rendering, JPEG/BMP/TIFF/WebP/PSD/GIF support |
License
GPL-3.0 — This library is derived from GIMP source code and is distributed under the GNU General Public License v3.0. See LICENSE for the full text.
Credits
- GIMP Team — Original source code and architecture (gimp.org)
- gimppy — Python extraction and adaptation
Powered by Qoder (Qwen 3.8)
Metadata
Release files for gimppy 1.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| gimppy-1.0.1.tar.gz | 98.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gimppy-1.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 168.6 kB
Release files / gimppy-1.0.1.tar.gz
| Download URL | gimppy-1.0.1.tar.gz |
|---|---|
| Size | 98.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eed7056ba578290c0f2e9d8be3df9ecce21407c1fbce2aa1277ec9ef8cf3e426
|
|
BLAKE2b-256 checksum How to use checksums |
d47544d1d020db1c77e4b6c257d6c6f40aba2882cd653ced97032b14fcc99e0d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logRelease files / gimppy-1.0.1-py3-none-any.whl
| Download URL | gimppy-1.0.1-py3-none-any.whl |
|---|---|
| Size | 70.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
af6481ba04ba7d7ccccc1e3680dc0e62ad1b034c8ec765d4b626328b8c1669de
|
|
BLAKE2b-256 checksum How to use checksums |
ce0d41fd1bb4329cf56fe6a12527c81382d8f9c2ea42c5ae695513ab32596ab7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logSecurity for gimppy 1.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for gimppy-1.0.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.