imgc supports a powerful plugin system that allows you to extend its functionality beyond image processing to handle any file type. This guide shows you how to create your own plugins.
Create your plugins in the default plugin directory next to the imgc executable:
- Default:
./plugins/(relative to imgc executable or main.py) - Custom: Use
--plugin-dirsto specify additional directories - Legacy: Previous versions used
~/.imgc/plugins/(still supported with--plugin-dirs)
# my_plugin.py
import sys
from pathlib import Path
# Standard imports for imgc plugins
try:
from imgc.plugin_api import FileProcessor, ProcessorResult
except ImportError:
# Fallback for standalone testing
imgc_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(imgc_root))
from imgc.plugin_api import FileProcessor, ProcessorResult
class MyProcessor(FileProcessor):
@property
def name(self):
return "My Custom Processor"
@property
def supported_extensions(self):
return [".txt", ".log"] # File types you want to handle
def process(self, file_path, context):
# Your processing logic here
try:
# Do something with the file
result = f"Processed {file_path.name}"
return ProcessorResult(
success=True,
message=result,
stats={"processed": True},
context={"my_plugin_ran": True}
)
except Exception as e:
return ProcessorResult(
success=False,
message=f"Processing failed: {e}"
)- Save your plugin as
./plugins/my_plugin.py(next to imgc executable) - Restart imgc or use
--plugin-dirs ./pluginsif running from different directory - Your plugin will automatically process matching files!
Alternative locations:
- Use
--plugin-dirs /custom/pathto load from custom directories - Multiple directories:
--plugin-dirs ./plugins /another/dir
@property
def name(self):
return "Human-readable plugin name"@property
def supported_extensions(self):
return [".txt", ".md", ".log"] # List of file extensionsdef process(self, file_path, context):
# file_path: pathlib.Path object
# context: dict with data from previous processors
return ProcessorResult(
success=True, # bool: whether processing succeeded
message="Done!", # str: human-readable result message
stats={"count": 42}, # dict: any statistics or metrics
context={"done": True} # dict: data for next processors
)@property
def priority(self):
return 100 # Lower numbers run first (default: 100)@property
def version(self):
return "1.0.0" # Your plugin version@property
def description(self):
return "What this plugin does"def can_process(self, file_path):
# Custom logic to determine if this plugin should process the file
# Default implementation checks file extension
if not super().can_process(file_path):
return False
# Add your custom checks here
return file_path.stat().st_size < 1024 * 1024 # Only files < 1MBclass TextAnalyzer(FileProcessor):
@property
def name(self):
return "Text Analyzer"
@property
def supported_extensions(self):
return [".txt", ".md", ".rst"]
def process(self, file_path, context):
with open(file_path, 'r') as f:
content = f.read()
words = len(content.split())
lines = len(content.split('\n'))
chars = len(content)
return ProcessorResult(
success=True,
message=f"Analyzed: {words} words, {lines} lines",
stats={"words": words, "lines": lines, "chars": chars}
)class BackupProcessor(FileProcessor):
@property
def name(self):
return "File Backup"
@property
def supported_extensions(self):
return [".doc", ".pdf", ".xlsx"] # Important documents
@property
def priority(self):
return 10 # Run first (before other processors)
def process(self, file_path, context):
import shutil
backup_dir = Path.home() / "backups"
backup_dir.mkdir(exist_ok=True)
backup_path = backup_dir / file_path.name
shutil.copy2(file_path, backup_path)
return ProcessorResult(
success=True,
message=f"Backed up to {backup_path}",
context={"backed_up": True}
)class PDFOptimizer(FileProcessor):
@property
def name(self):
return "PDF Optimizer"
@property
def supported_extensions(self):
return [".pdf"]
def can_process(self, file_path):
# Check if required tool is available
import shutil
return super().can_process(file_path) and shutil.which('gs') # Ghostscript
def process(self, file_path, context):
import subprocess
output_path = file_path.with_suffix('.optimized.pdf')
cmd = [
'gs',
'-sDEVICE=pdfwrite',
'-dCompatibilityLevel=1.4',
'-dPDFSETTINGS=/screen',
'-dNOPAUSE', '-dQUIET', '-dBATCH',
f'-sOutputFile={output_path}',
str(file_path)
]
try:
subprocess.run(cmd, check=True, timeout=60)
# Replace original if smaller
original_size = file_path.stat().st_size
new_size = output_path.stat().st_size
if new_size < original_size:
output_path.replace(file_path)
saved = original_size - new_size
return ProcessorResult(
success=True,
message=f"PDF optimized: saved {saved} bytes",
stats={"original_size": original_size, "new_size": new_size}
)
else:
output_path.unlink() # Remove if no improvement
return ProcessorResult(
success=True,
message="PDF already optimized"
)
except Exception as e:
return ProcessorResult(
success=False,
message=f"PDF optimization failed: {e}"
)Always wrap your processing logic in try/except blocks:
def process(self, file_path, context):
try:
# Your processing code
return ProcessorResult(success=True, message="Success!")
except Exception as e:
return ProcessorResult(success=False, message=f"Error: {e}")Check files before processing:
def can_process(self, file_path):
if not super().can_process(file_path):
return False
# Check file size, permissions, etc.
try:
size = file_path.stat().st_size
return 0 < size < 100 * 1024 * 1024 # 0-100MB range
except:
return FalseUse temporary files for processing:
def process(self, file_path, context):
temp_path = file_path.with_suffix('.temp')
try:
# Process file_path -> temp_path
# ... processing logic ...
# Replace original with processed version
temp_path.replace(file_path)
return ProcessorResult(success=True, message="Processed!")
finally:
# Clean up temp file if it still exists
if temp_path.exists():
temp_path.unlink()Set reasonable timeouts and limits:
def process(self, file_path, context):
# Check file size
if file_path.stat().st_size > 50 * 1024 * 1024: # 50MB limit
return ProcessorResult(success=False, message="File too large")
# Use timeouts for external commands
subprocess.run(cmd, timeout=30) # 30 second timeoutAdd a test section to your plugin:
if __name__ == "__main__":
# Test your plugin
processor = MyProcessor()
print(f"Plugin: {processor.name}")
print(f"Supports: {processor.supported_extensions}")
# Test with a sample file
test_file = Path("sample.txt")
if test_file.exists() and processor.can_process(test_file):
result = processor.process(test_file, {})
print(f"Test result: {result.message}")- Plugins run in priority order (lower numbers first)
- Each plugin receives the context from previous plugins
- If a plugin fails, the chain continues with other plugins
- Plugin failures are logged but don't stop file processing
Set the log level to debug to see plugin loading and execution details:
imgc --log-level debug --root /path/to/watchfrom imgc.plugin_manager import PluginManager
manager = PluginManager()
manager.discover_plugins()
stats = manager.get_stats()
print("Loaded plugins:", [p['name'] for p in stats['processors']])
print("Failed plugins:", stats['failed'])You can define multiple processor classes in one plugin file:
class TextProcessor(FileProcessor):
# ... implementation ...
class LogProcessor(FileProcessor):
# ... implementation ...Read configuration from environment variables or files:
import os
class ConfigurableProcessor(FileProcessor):
def __init__(self):
self.quality = int(os.getenv('MY_PLUGIN_QUALITY', '80'))
self.enabled = os.getenv('MY_PLUGIN_ENABLED', 'true').lower() == 'true'Plugins can declare CLI arguments and environment variables:
from imgc.plugin_api import FileProcessor, ProcessorResult, PluginArgument
class ConfigurableProcessor(FileProcessor):
def __init__(self):
# Set default values (will be overridden by configure_from_args)
self.quality = 80
self.enabled = True
@property
def name(self):
return "My Processor"
@property
def supported_extensions(self):
return [".txt"]
def get_plugin_arguments(self):
"""Declare CLI arguments for this plugin."""
return [
PluginArgument("quality", int, 80, "Processing quality (1-100)"),
PluginArgument("enabled", bool, True, "Enable processing")
]
def get_plugin_namespace(self):
"""Use custom namespace (default: auto-generated from name)."""
return "my-proc" # Creates --my-proc-quality, --my-proc-enabled
def process(self, file_path, context):
if not self.enabled:
return ProcessorResult(success=True, message="Disabled")
# Use self.quality in processing logic
return ProcessorResult(success=True, message=f"Processed with quality {self.quality}")Usage:
# CLI arguments (auto-generated from plugin)
imgc --root /path --my-proc-quality 90 --my-proc-enabled
# Environment variables (auto-generated)
export IMGC_MY_PROC_QUALITY=90
export IMGC_MY_PROC_ENABLED=true
imgc --root /pathUse context to coordinate between plugins:
def process(self, file_path, context):
# Check what previous plugins did
if context.get('already_processed'):
return ProcessorResult(success=True, message="Skipped (already processed)")
# Do processing...
# Mark for next plugins
return ProcessorResult(
success=True,
message="Processed!",
context={"my_plugin_processed": True}
)- Check the example plugins in
examples/plugins/ - Look at the built-in image processor in
imgc/plugins/builtin/ - Enable debug logging to see what's happening
- Test your plugins standalone before deploying
Happy plugin development! 🚀