A lightweight, modular image processing framework with Blender-like operator philosophy and unified Node Graph execution. Supports grid splitting, custom guide-based cutting, resizing, canvas adjustment, rounded corners, smart crop, borders, and more �?with modern customtkinter GUI and auto-generated UI metadata.
- Unified Node Graph Execution: All processor invocations flow through the same DAG-based Node Graph engine �?single execution path for CLI, GUI, and scripting.
- Modern GUI: customtkinter with dark theme, framework-agnostic GuiState ViewModel for future Dear PyGui migration.
- Metadata-Driven UI: GUI auto-generates parameter panels from dataclass field annotations. New features = drop-in plugin.
- Everything as Operators: Complete decoupling between core logic and UI. Blender-style operator invocation logs and command dispatching.
- Typed Property System:
IntProp,FloatProp,BoolProp,EnumProp,StrProp,ListProp,ColorPropdescriptors with coercion, validation, and update callbacks. (Experimental �?reserved for future Node Graph UI.) - Image Data Blocks: Named, versioned, reference-counted image containers with class-level registry �?Blender ID-style.
- Operation History: Undo/Redo stack records every operation, exportable as JSON log.
- Macro Recording: One-click recording generates reusable Python scripts with restricted execution. Note: the restriction is an anti-footgun guard, not a security boundary �?only play back macros you authored.
- Parameter Presets: Save/load named parameter presets per operator (CLI
--presetand GUI). - Visual Pipeline Editor: Compose multi-step operator chains via GUI (Ctrl+P).
- Interactive Console: Built-in command console with tab completion and history (Ctrl+`).
- Plugin System: Drop a
BaseProcessorsubclass inplugins/and it's auto-discovered. - High-Performance Engine: CLI and GUI batch runs share a multi-processing runner (
max_workerssetting, CPU-core default) with cancel-pending abort semantics. - Industrial-Grade Safety: Strict Pillow handle management with Path Traversal interception and cell leak protection.
- Cross-Platform: Full Windows/macOS/Linux support with platform-aware font loading.
- Static Type Checking: Full mypy compliance with
--ignore-missing-imports�?zero type errors across 44 source files.
pip install -e . # CLI only (Pillow-only core)
pip install -e ".[gui]" # CLI + GUI (adds customtkinter)A single-file desktop build is supported via PyInstaller:
pip install pyinstaller pillow customtkinter
pyinstaller packaging/image_splitter.spec
# �?dist/ImageSplitterPro(.exe)python -m image_splitter.gui
# or
image-splitter-gui# Split test.png into 3x3 grid with 8 parallel processes
python -m image_splitter.cli test.png -r 3 -c 3 -o ./output -j 8
# Custom guide-based split (arbitrary horizontal/vertical lines)
python -m image_splitter.cli test.png -p custom_splitter --set h_lines=[100,300,500] --set v_lines=[200,400]
# Use a saved preset
python -m image_splitter.cli input.png --preset "my_split"
# Save current parameters as a preset (output_dir/template excluded)
python -m image_splitter.cli input.png -r 3 -c 3 --preset-save "3x3_grid"
# List saved presets (no input required)
python -m image_splitter.cli --preset-list# Execute script file
python -m image_splitter.cli input.png -s script.txt
# Chain operations
python -m image_splitter.cli input.png --chain "resizer(width=0.5)|grid_splitter(rows=2,cols=2)"
# Custom splitter in chain (arbitrary guide positions)
python -m image_splitter.cli input.png --chain "custom_splitter(h_lines=[100,300],v_lines=[200,400])|resizer(width=0.5)"Chain semantics (V15): operators apply per image with flat-map fan-out. A splitter in the middle of a chain fans out �?every following operator is applied to each cell and all results are collected:
# 2x2 split �?each cell resized �?4 outputs (not 1)
python -m image_splitter.cli input.png --chain "grid_splitter(rows=2,cols=2)|resizer(width=0.5)"| Processor | Description |
|---|---|
grid_splitter |
Split image into uniform grid (rows x cols) |
custom_splitter |
Custom guide-based splitting �?arbitrary h_lines + v_lines at any pixel positions |
resizer |
Proportional image scaling |
canvas_adjuster |
Canvas padding, cropping, background fill |
format_converter |
WebP/JPEG/PNG/BMP format conversion with quality control |
geometry |
Rotation (90/180/270) and flip operations |
filters |
Grayscale and invert filters |
color_adjuster |
Brightness, contrast, sharpness, saturation tuning |
metadata_cleaner |
Strip EXIF/GPS privacy data |
text_watermark |
Add semi-transparent text watermark |
rounded_corner |
Rounded corner crop with configurable radius |
border |
Add solid/dashed/double border with configurable width and color |
smart_crop |
Content-aware crop using alpha channel or luminance edge detection (background-agnostic: works on dark and light backgrounds) |
| Placeholder | Description |
|---|---|
{filename} |
Original filename without extension |
{row} / {col} |
Current row/column number (1-based) |
{index} |
Global sequence number (01-based, zero-padded) |
{batch} |
1-based sequence of the input file within the batch �?use it to disambiguate files that share a stem ({index} resets per file) |
{w} |
Processed image width in pixels |
{h} |
Processed image height in pixels |
{ext} |
File extension |
{anchor} |
Anchor position (TL, TR, BL, BR, C) |
{text} |
Watermark text content |
{quality} |
Export quality parameter |
Settings are persisted to ~/.image_splitter/settings.json:
{
"output_dir": "./output",
"default_processor": "grid_splitter",
"template": "{filename}_{index}",
"max_workers": 0,
"default_rows": 3,
"default_cols": 3,
"window_geometry": "1100x820"
}default_rows / default_cols are the CLI grid fallbacks (explicit -r/-c
and presets take precedence); template is the GUI's startup filename
template; window_geometry restores the GUI window size. max_workers
controls batch concurrency for both CLI (-j default) and GUI batch
runs (0 = CPU core count). GUI diagnostics
are additionally logged to ~/.image_splitter/logs/gui.log (rotating,
1 MB × 3 backups).
- Concurrency: CLI and GUI share one parallel runner
(
core.run_parallel_batch). GUI batch runs are parallel by default; Abort cancels pending files and lets the in-flight file finish. - Same-stem safety: files that share a stem produce colliding output
names (
{index}resets per file). The CLI warns about this at pre-flight; add{batch}to your template to keep every file's output distinct.
Default keybindings in ~/.image_splitter/keymap.json:
{
"global": {
"<Control-o>": "select_files",
"<Control-Return>": "run_batch",
"<Delete>": "remove_selected",
"<Control-Shift-Delete>": "clear_list",
"<Control-grave>": "toggle_console",
"<Control-p>": "toggle_pipeline",
"<Control-Shift-R>": "toggle_macro_record",
"<Control-z>": "undo_history",
"<Control-Shift-Z>": "redo_history",
"<Control-e>": "open_output_dir",
"<Escape>": "stop_tasks"
}
}| Shortcut | Action |
|---|---|
Ctrl+O |
Open images |
Ctrl+Enter |
Run batch processing |
Delete |
Remove selected file |
Ctrl+Shift+Delete |
Clear file list |
Ctrl+P |
Toggle pipeline editor |
Ctrl+Shift+R |
Start/stop macro recording |
Ctrl+Z |
Undo last operation |
Ctrl+Shift+Z |
Redo last operation |
Ctrl+E |
Open output directory |
Escape |
Abort current task |
Ctrl+` |
Toggle command console |
image_splitter/ # repository root
├── pyproject.toml # Package configuration
├── image_splitter/ # the package
�? ├── py.typed # PEP 561 type marker
�? ├── cli.py # CLI entry point (multiprocessing + presets)
�? ├── gui.py # GUI entry point (customtkinter + GuiState ViewModel)
�? ├── core.py # Core processing pipeline + unified Node Graph execution
�? ├── settings.py # User settings persistence
�? ├── keymap.py # Keybinding system
�? ├── script_engine.py # Batch scripting engine
�? ├── logging_config.py # Logging configuration
�? ├── models.py # Configuration dataclasses (13 models)
�? ├── engine/
�? �? ├── base.py # BaseProcessor abstract class
�? �? ├── registry.py # Processor registry (thread-safe singleton)
�? �? ├── dispatcher.py # Command dispatcher + chain execution
�? �? ├── config_coercion.py # Parameter type coercion (int/float/bool/enum/list/str)
�? �? ├── history.py # Operation history stack (undo/redo)
�? �? ├── macro.py # Macro recording & sandboxed playback
�? �? ├── presets.py # Parameter presets (save/load/import/export)
�? �? ├── props.py # Typed Property descriptor system (Blender bpy.props style) �?Experimental
�? �? ├── data_blocks.py # ImageDataBlock (named, versioned, ref-counted)
�? �? ├── nodes.py # DAG node graph (Socket, BaseNode, 4 concrete nodes)
�? �? ├── evaluator.py # NodeGraph evaluator + LRU EvaluationCache
�? �? ├── legacy_adapter.py # ProcessorNodeAdapter + ChainAsGraph (unified execution bridge)
�? �? └── _ui_metadata_util.py # Auto-generate UI metadata from dataclass field annotations
�? ├── processors/ # Processor plugins (13 built-in)
�? ├── plugins/ # User plugin directory (auto-discovered)
�? �? └── example_plugin.py # Example: invert colors plugin
�? └── ui/
�? ├── _state.py # GuiState �?framework-agnostic ViewModel
�? ├── console.py # Interactive command console panel (customtkinter)
�? ├── pipeline.py # Visual pipeline chain editor (customtkinter)
�? └── param_widgets.py # Shared parameter widget factory (customtkinter)
├── .github/workflows/
�? └── ci.yml # CI/CD pipeline (multi-OS, Python 3.10-3.13)
└── tests/ # Test suite (498 tests, 41 files)
The engine layer provides a Blender-inspired foundation with unified execution:
| Module | Purpose |
|---|---|
_ui_metadata_util.py |
Auto-generates get_ui_metadata() from dataclass field(metadata={"label": ...}) �?single source of truth |
props.py |
Typed Property descriptors (Experimental �?reserved for future Node Graph UI) |
data_blocks.py |
ImageDataBlock �?named, versioned, reference-counted image containers |
nodes.py |
DAG primitives: SocketType, Socket, Connection, BaseNode, concrete nodes |
evaluator.py |
NodeGraph �?topological evaluation, dirty propagation; EvaluationCache �?LRU cache |
legacy_adapter.py |
ProcessorNodeAdapter wraps processors as nodes; ChainAsGraph �?unified chain execution |
All processor invocations use _execute_via_graph() �?a single Node Graph path shared by process_image() and ChainAsGraph.execute_chain().
This project supports plugin development. Simply inherit from BaseProcessor and implement your logic to automatically get CLI support and GUI panel generation.
See DEVELOPER.md for details and TECHNICAL.md for a comprehensive technical guide.
- Python 3.10+
- Pillow 10.2.0+
- customtkinter 5.2.0+ (GUI only �?optional)
MIT
The project includes 498 tests across 41 test files:
| Test File | Description |
|---|---|
conftest.py |
Shared fixtures: BaseTest, test images, color constants |
test_adjuster.py |
Canvas adjuster: padding, cropping, ratio, fail-fast validation |
test_bug_fixes.py |
Regression tests for all confirmed bug fixes (30 tests) |
test_chain_as_graph_all.py |
ChainAsGraph vs CommandDispatcher pixel-identical for all 13 processors (22 tests) |
test_cli.py |
CLI mode: basic flow, recursive discovery, concurrency, error handling |
test_config_coercion.py |
Parameter type coercion for all supported types |
test_console_panel.py |
UI: interactive command console panel (customtkinter) |
test_custom_splitter.py |
Custom line splitter: simple, irregular, out-of-bounds, negative rejection |
test_data_blocks.py |
ImageDataBlock: name registry, version tracking, ref counting, clone, metadata (22 tests) |
test_dispatcher.py |
Command parsing + chain execution with coercion and extra_config |
test_edge_cases.py |
Edge case tests for all processors: 1x1, tiny ratios, empty text, P-mode |
test_engine_v4.py |
Registry integrity, processor smoke test, path security, format adaptation |
test_error_paths.py |
Error handling: missing files, invalid templates, bad enum/list values |
test_execute_via_graph.py |
Unified execution path: _execute_via_graph() for single/multi-output and context |
test_gui_param_sync.py |
UI: GUI widget→state parameter synchronization data flow |
test_gui_smoke.py |
GUI initialization, file selection, processor switching, stop/cancel |
test_gui_state.py |
GuiState ViewModel: processor selection, progress tracking, param values |
test_gui_workflows.py |
GUI workflow: full parameter coercion and batch run verification |
test_history.py |
History system: push, undo, redo, clear, max-depth, export log |
test_integration.py |
End-to-end: full pipeline, RGBA/L-mode smoke, CLI chain, template vars |
test_keymap.py |
Keybinding: bind/unbind/lookup, import/export, reset, multiple contexts |
test_legacy_adapter.py |
Legacy Adapter: ChainAsGraph vs CommandDispatcher pixel-identical output (15 tests) |
test_macro.py |
Macro recording: record, stop, generate script, playback, save |
test_new_processors.py |
New processor coverage: metadata P-mode, border styles, smart crop edge cases |
test_node_graph.py |
Node Graph + Evaluator: DAG topology, dirty propagation, cache, end-to-end (35 tests) |
test_operator_compliance.py |
Compliance audit + parameter contract for all processors |
test_param_widgets.py |
UI: shared parameter widget factory (customtkinter) |
test_pipeline_editor.py |
UI: visual pipeline chain editor (customtkinter) |
test_plugin.py |
Plugin system: auto-discovery, metadata, functional inversion |
test_presets.py |
Presets: save/load/list/delete/export/import + CLI integration (14 tests) |
test_processors_expanded.py |
Deep: rounding consistency, watermark positioning, pixel accuracy, stress |
test_props.py |
Property System: coercion, validation, update callbacks, ui_metadata (30 tests) |
test_save_compatibility.py |
Save: ICC profile preservation, format conversion extension changes |
test_script_engine.py |
Script engine: process, chain, batch script, error handling, operators list |
test_settings.py |
Settings: defaults, save/load roundtrip, merge, get/set, corrupted JSON |
test_ui_metadata_util.py |
UI metadata auto-generation from dataclass fields |
test_ui_preview.py |
Preview rendering: all processor draw_preview, graceful dirty-data handling |
test_v13_fixes.py |
V13 audit regressions: packaging, keymap coverage, border double, console chain, CLI dir expansion, preset precedence, chain format (17 tests) |
test_v14_fixes.py |
V14 audit regressions: border dashed, macro chain playback, keymap typing guard, graph cleanup, CLI settings defaults, chain ICC, preset sanitization (22 tests) |
test_v15_fixes.py |
V15 audit regressions: chain fan-out, copy-count reduction, GUI startup param panel, config robustness, smart_crop light backgrounds, import_preset guard (16 tests) |
test_v16_fixes.py |
V16 deployability regressions: parallel runner semantics, {batch} placeholder, case-insensitive discovery, duplicate-stem warning, registry policy, BlendNode hardening, cache close consistency, console reject suppression (17 tests) |
test_workflow.py |
Full end-to-end workflows: chains, batch, presets+macro+history, boundary values |
Run tests:
# Full suite (GUI tests skip automatically if the [gui] extra is absent)
python -m pytest tests/ -v
# Type check
python -m mypy image_splitter --ignore-missing-imports