Skip to content

NightHawkHSI/Layered

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

54 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation


Layered logo

LAYERED

Modern Python image & game-asset editor

Real-time canvas Β· Non-destructive layers Β· Plugin-powered workflow


Release Python PyQt6 License


Download Bug Report Feature Request Plugin Docs Brush Docs


Stars Forks Downloads Issues Last Commit Repo Size Views


Preview



πŸ“– Table of Contents

πŸ–Ό What is Layered? 🧩 Blend Modes
✨ Features πŸͺ΅ Logging & Crash Reports
πŸš€ Quick Start πŸ“¦ Building a Standalone EXE
πŸ”Œ Bundled Plugins 🀝 Contributing
πŸ—‚ Project Structure πŸ“„ License
✍️ Writing a Plugin πŸ–Œ Brush Presets & Custom Tools

πŸ–Ό What is Layered?

Layered is an open-source image and game-asset editor built in Python with PyQt6, inspired by Paint.NET. It delivers a familiar non-destructive workflow β€” draw, stack layers, blend, export β€” without ever leaving your Python toolchain.

Built for game developers. Export every layer as its own PNG alongside a manifest.json carrying offsets, blend modes, and visibility β€” so your engine can reassemble the scene at runtime.


✨ Features

🎨 Drawing Toolkit

Brush Β· Eraser Β· Fill Bucket Β· Line Β· Rectangle Β· Ellipse Β· Color Picker Β· Text

Paint assets from scratch or retouch imports with a full suite of drawing primitives.

πŸ—‚ Non-Destructive Layers
  • Per-layer opacity and visibility toggle
  • 12 blend modes β€” Normal, Multiply, Screen, Overlay, Soft Light, Darken, Lighten, Add, Subtract, Difference, Color, Saturation
  • Reorder, rename, duplicate, and group layers
  • Original pixel data is never destroyed β€” every operation is fully reversible
β†Ά Full Undo / History

Every brush stroke, filter, and layer operation is tracked. Browse the history panel and jump to any prior state instantly.

πŸ“¦ Export Formats
Format Description
PNG / JPEG / WEBP Flattened composite export
Per-layer PNG + manifest.json Offsets, blend modes, visibility, opacity β€” game-engine ready
Multi-tab Projects Work on several files simultaneously
πŸ”Œ Plugin System

Drop a .py file into Plugins/ and it's live. Plugins can register tools, filters, or menu actions, declare typed settings (auto-generated dialog), and run fully sandboxed β€” a crashing plugin gets logged and isolated while the editor keeps running.

πŸ“‹ Logging & Diagnostics
  • logs/layered.log β€” full session activity
  • logs/errors/ β€” per-crash reports with stack trace + context
  • In-app Console panel mirrors log output live

πŸš€ Quick Start

# 1. Clone the repo
git clone https://github.com/NightHawkHSI/Layered.git
cd Layered

# 2. Install dependencies
pip install -r requirements.txt

# 3. Launch
python main.py

Requirements: Python 3.9+ Β· PyQt6 >= 6.6 Β· Pillow >= 10.0 Β· numpy >= 1.26

πŸ’‘ No Python? Grab the prebuilt Windows binary from the Releases page β€” no setup needed.


πŸ”Œ Bundled Plugins

Layered ships with 17+ working plugins in Plugins/ β€” ready to use or read as templates.

Plugin Type Description
grayscale πŸ”΅ Filter Desaturate to grayscale
invert πŸ”΅ Filter Invert RGB / per-channel
brightness_contrast πŸ”΅ Filter Brightness + contrast sliders
sharpen πŸ”΅ Filter Unsharp mask sharpening
posterize πŸ”΅ Filter Reduce color levels
gradient_map πŸ”΅ Filter Remap luminance to a gradient
color_replace πŸ”΅ Filter Swap one color for another
outline_filter πŸ”΅ Filter Edge outline effect
glow_filter πŸ”΅ Filter Soft outer glow
drop_shadow πŸ”΅ Filter Drop shadow with offset & blur
normal_map πŸ”΅ Filter Generate normal map from height
background_remove πŸ”΅ Filter Knock out flat / chroma background
tile_fix πŸ”΅ Filter Make textures seamless
pixel_art_resize πŸ”΅ Filter Nearest-neighbor upscale
crop_tool 🟠 Action Crop canvas to selection
flip_tool 🟠 Action Flip horizontal / vertical
grid_overlay 🟠 Action Toggle grid overlay

πŸ—‚ Project Structure

Layered/
β”œβ”€β”€ πŸ“„ main.py                    # Entry point
β”œβ”€β”€ πŸ“„ requirements.txt
β”œβ”€β”€ πŸ“„ build.bat                  # PyInstaller one-file build (Windows)
β”œβ”€β”€ πŸ–Ό Icon.png / Icon.ico
β”‚
β”œβ”€β”€ πŸ“ app/
β”‚   β”œβ”€β”€ main_window.py            # Menus, docks, plugin wiring
β”‚   β”œβ”€β”€ πŸ“ core/                  # Domain model
β”‚   β”‚   β”œβ”€β”€ layer.py              #   Layer + LayerStack
β”‚   β”‚   β”œβ”€β”€ project.py            #   .layered project document
β”‚   β”‚   β”œβ”€β”€ history.py            #   Undo / redo stack
β”‚   β”‚   β”œβ”€β”€ blending.py           #   Blend-mode math (NumPy / numba)
β”‚   β”‚   β”œβ”€β”€ image_ops.py          #   Pixel ops (fill, transforms, etc.)
β”‚   β”‚   └── adjustments.py        #   Adjustment-layer math
β”‚   β”œβ”€β”€ πŸ“ render/                # Compositing surfaces
β”‚   β”‚   β”œβ”€β”€ canvas.py             #   Interactive canvas widget
β”‚   β”‚   β”œβ”€β”€ gpu_renderer.py       #   moderngl GPU compositor (opt-in)
β”‚   β”‚   └── tile_renderer.py      #   Tiled CPU compositor
β”‚   β”œβ”€β”€ πŸ“ io/                    # Persistence
β”‚   β”‚   β”œβ”€β”€ export.py             #   Composite + per-layer export
β”‚   β”‚   β”œβ”€β”€ project_io.py         #   .layered file save / load
β”‚   β”‚   β”œβ”€β”€ session.py            #   Multi-document session state
β”‚   β”‚   └── brush_loader.py       #   Brush-preset discovery from Brushes/
β”‚   β”œβ”€β”€ πŸ“ plugins/               # Plugin system
β”‚   β”‚   β”œβ”€β”€ plugin_api.py         #   Public plugin API
β”‚   β”‚   β”œβ”€β”€ plugin_loader.py      #   Plugin discovery + sandbox
β”‚   β”‚   β”œβ”€β”€ tool_loader.py        #   Tool discovery from Plugins/Brushes/
β”‚   β”‚   └── tools.py              #   Tool base class + ToolContext + helpers
β”‚   β”œβ”€β”€ πŸ“ app_ui/                # App-shell support
β”‚   β”‚   β”œβ”€β”€ theme.py              #   Dark / light theme engine
β”‚   β”‚   β”œβ”€β”€ preferences.py        #   User preferences (prefs.json)
β”‚   β”‚   └── logger.py             #   Logging + crash reporter
β”‚   β”œβ”€β”€ πŸ“ controllers/           # History / paste / selection controllers
β”‚   └── πŸ“ ui/                    # Qt panels (layers, tools, color, history,
β”‚                                 #   text, console, project tabs, dialogs)
β”‚
β”œβ”€β”€ πŸ“ Plugins/                   # ← Drop your plugins here
β”‚   β”œβ”€β”€ Brushes/                  #   Tool plugins, grouped by folder
β”‚   β”‚   β”œβ”€β”€ _shared.py            #   One-stop import for every brush
β”‚   β”‚   └── <Category>/<Tool>/    #   Each tool is a folder
β”‚   β”‚       β”œβ”€β”€ tool.py           #   Required β€” defines TOOL_CLASS = MyTool
β”‚   β”‚       └── tool.json         #   Optional β€” display name, id, icon, category override
β”‚   └── *.py                      #   Filter / action plugins (flat .py files)
β”‚
β”œβ”€β”€ πŸ“ Brushes/                   # ← Brush presets (size/hardness/opacity/...)
β”œβ”€β”€ πŸ“ docs/
β”‚   β”œβ”€β”€ PLUGIN_API.md             # Full plugin API reference
β”‚   └── build_brush.md            # How to build a brush (folder layout, lifecycle, helpers)
└── πŸ“ logs/                      # Generated at runtime

✍️ Writing a Plugin

Drop a .py file in Plugins/ and subclass Plugin β€” that's it.

# Plugins/my_filter.py
from PIL import Image, ImageOps
from app.plugins.plugin_api import Plugin, PluginContext


class GrayscalePlugin(Plugin):
    name    = "Grayscale"
    version = "1.0.0"

    def register(self, ctx: PluginContext) -> None:
        ctx.register_filter("Grayscale", self.apply)

    @staticmethod
    def apply(image: Image.Image) -> Image.Image:
        return ImageOps.grayscale(image.convert("RGB")).convert("RGBA")

Registration Surfaces

Kind Where it appears Method
Tool Toolbox panel ctx.register_tool(name, Tool)
Filter Filters menu ctx.register_filter(name, fn, settings=...)
Action Plugins menu ctx.register_action(name, fn, settings=...)

Filters and actions accept typed Setting specs β€” int, float, bool, choice, color, string β€” and the host auto-generates the settings dialog, passing values as keyword arguments.

πŸ“˜ See docs/PLUGIN_API.md for the full API surface and invert.py for a complete settings example.


πŸ–Œ Brush Presets & Custom Tools

Tools and brush presets live in two separate trees:

Folder Drives Layout
Plugins/Brushes/ The Tools dock β€” every group folder becomes a split-button with its sub-tools in a dropdown <Group>/<Tool>/tool.py
Brushes/ The brush preset picker β€” preset JSON files per category <Category>/<preset>.json

Adding a brush preset

// Brushes/Inking/04_marker.json
{ "name": "Marker", "icon": "πŸ–Š", "size": 20, "hardness": 0.95, "opacity": 1.0, "spacing": 0.05 }

Adding a custom tool

Every brush is a folder under Plugins/Brushes/<Category>/<ToolName>/ with a tool.py (and optional tool.json). Each Tool subclass declares its own icon, shortcut, and build_ui() β€” settings render in the per-tool settings toolbar at the top of the window.

# Plugins/Brushes/Basic/Brush/tool.py
import importlib.util as _iu, sys as _sys
from pathlib import Path as _P
_KEY = "_layered_brushes_shared"
if _KEY not in _sys.modules:
    _spec = _iu.spec_from_file_location(_KEY, _P(__file__).resolve().parents[2] / "_shared.py")
    _mod = _iu.module_from_spec(_spec); _sys.modules[_KEY] = _mod; _spec.loader.exec_module(_mod)
_sh = _sys.modules[_KEY]

Tool = _sh.Tool; Layer = _sh.Layer
build_brush_settings_ui = _sh.build_brush_settings_ui


class BrushTool(Tool):
    name, tool_id = "Brush", "brush"
    icon, shortcut = "πŸ–Œ", "B"
    is_default = True

    def __init__(self, ctx=None):
        super().__init__(ctx)
        self.brush_size, self.brush_hardness, self.brush_opacity = 20, 0.8, 1.0

    def build_ui(self, parent, ctx):
        return build_brush_settings_ui(self, parent,
            fields=("size", "hardness", "opacity"))

    def press(self, layer, x, y):   ...
    def move(self, layer, x, y):    ...
    def release(self, layer, x, y): self._last_pt = None


TOOL_CLASS = BrushTool

Plugins/Brushes/_shared.py re-exports stdlib, PIL, PyQt6, painting helpers, SliderField, build_brush_settings_ui, and the Tool/Layer/ToolPhase bases β€” one import covers every brush.

πŸ“˜ Full guide with class-attr reference, lifecycle methods, painting helpers, and shape/selection bases: docs/build_brush.md.


🧩 Blend Modes

Mode Effect Best For
Normal Standard alpha compositing Everything
Multiply Darkens β€” multiplies values Shadows, tinting
Screen Lightens β€” inverts multiply Glows, highlights
Overlay Contrast boost (multiply + screen) Detail enhancement
Soft Light Gentle dodge / burn driven by the top layer Subtle shading
Darken Keeps the darker pixel Soft shadows
Lighten Keeps the lighter pixel Soft highlights
Add Brightens additively (linear dodge) Bloom, fire, neon
Subtract Darkens subtractively Dark burn effects
Difference Highlights where layers differ Masking, debug
Color Hue + saturation of top, luma of base Recoloring, tinting
Saturation Saturation of top, hue + luma of base Vibrance tweaks

All modes operate on premultiplied RGBA via NumPy in app/core/blending.py (numba-accelerated when available).


πŸͺ΅ Logging & Crash Reports

Location Contents
logs/layered.log Full session activity, INFO+
logs/errors/<timestamp>.txt Stack trace + context per crash
In-app Console panel Live mirror of the log stream

Plugins get their own sandboxed logger (layered.plugin.<name>) β€” use ctx.logger instead of print so output lands in both the log file and the console panel.


πŸ“¦ Building a Standalone EXE

Windows one-file build via PyInstaller:

build.bat

Output drops in GitHub/Release/. The bundled Plugins/, Brushes/, and Icon.ico folders are picked up automatically.


🀝 Contributing

  1. Fork the repo and create a branch: git checkout -b feature/my-thing
  2. Make changes β€” keep functions small, prefer Pillow / NumPy over hand-rolled loops
  3. Test β€” run the app and verify nothing regressed
  4. Open a PR with a clear description of what changed and why

Bug reports and feature requests live in Issues. All contributions are welcome!


πŸ“„ License

Distributed under the terms described in LICENSE.



Built with Python Β· Powered by PyQt6 & Pillow


Plugin API Build a Brush Issues Releases


If Layered saved you time, consider leaving a ⭐ β€” it helps others find the project!

About

Layered is a desktop image editor written in Python with PyQt6. It supports a non-destructive layer stack with blending modes, full undo/redo history, multi-project tabs, and PNG/JPEG export. A plugin API lets you drop .py files into Plugins/ to add tools, filters, actions; plugins run sandboxed. Safe execution with isolation and crash protectio

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors