Plugins¶
The photobooth-app backend can be extended by plugins since version 6.
Note
This page is a guide and documentation on how to develop a plugin. The use of the included plugins is not decribed here but in the setup section.
If you want to develop plugins you need python skills. The support to develop plugins is quite limited but feel free to ask questions in the Github discussions.
Plugins Built-In¶
There are some plugins built in and can be used as reference to build your own plugins:
- GPIO to control light
- WLED signaling countdown, capture and recording
- pilgram2 filter apply color filter in post processing
- commander hook http requests and commands to events
Plugin Architecture¶
Plugins listen to event-hooks triggered by the pluggy package.
- photobooth-data (working directory)
- plugins
- plugin_name <-\
- __init__.py (empty) | same name
- plugin_name.py <-/
- config.py (optional)
Available hooks¶
Plugins can register to hooks, so called hookspecs. The hookspecs are categorized currently as follows:
PluginManagementSpec
: Start and Stop the Plugin during app startup/shutdownPluginStatemachineSpec
: Hooks triggered during actions like countdown start, capture, finished, ...PluginAcquisitionSpec
: Hooks directly triggered by the backends when a capture is triggered and capture is done.PluginMediaprocessingSpec
: Hooks triggered for post processing images to apply filter.
Plugin Skeleton¶
In this very basic example the plugin only prints to the console during app startup:
import logging
from photobooth.plugins import hookimpl
from photobooth.plugins.base_plugin import BasePlugin
from .config import PluginNameConfig # if the plugin shall have a config, create the config.py file and import here
logger = logging.getLogger(__name__) # use to emit log messages
# NOTE: name the class following to the folder name -> plugin_name will require the class to be PluginName
class PluginName(BasePlugin[PluginNameConfig]):
def __init__(self):
super().__init__()
# when this _config is present, the plugin manager detects it and adds the configuration to the admin dashboard
self._config: PluginNameConfig = PluginNameConfig()
@hookimpl # mark a function as hook implementation, check the hookspec for available hooks
def start(self):
if self._config.plugin_enabled:
print("Message is printed when enabled in config!")
else:
print("Message is printed when not enabled in config!")
The configuration consists of a class and one bool variable to enable the plugin.
from pydantic import Field
from pydantic_settings import SettingsConfigDict
from photobooth import CONFIG_PATH
from photobooth.services.config.baseconfig import BaseConfig
class PluginNameConfig(BaseConfig):
# The plugin uses pydantic-settings to store settings.
model_config = SettingsConfigDict(title="PluginName Config", json_file=f"{CONFIG_PATH}plugin_pluginname.json")
plugin_enabled: bool = Field(
default=False,
description="Enable to start the plugin with app startup",
)
# ... add more configuration here...
Filter Plugin Skeleton¶
The filters drawer can be extended by plugins since v6. Check following example for a basic understanding, also have a look at the integrated pilgram2 filter for reference.
Most of the file below is boilerplate, the relevant functions to modify and hook your filter are mp_filter_pipeline_step
and do_filter
.
import logging
from typing import cast, get_args
from PIL import Image, ImageFilter
from photobooth.plugins import hookimpl
from photobooth.plugins.base_plugin import BaseFilter
from .config import FilterStableConfig, available_filter
logger = logging.getLogger(__name__)
# NOTE: name the class following to the folder name -> plugin_filter will require the class to be PluginFilter
class PluginFilter(BaseFilter[FilterStableConfig]):
def __init__(self):
super().__init__()
self._config: FilterStableConfig = FilterStableConfig()
@hookimpl
def mp_avail_filter(self) -> list[str]:
return [self.unify(f) for f in get_args(available_filter)]
@hookimpl
def mp_userselectable_filter(self) -> list[str]:
if self._config.add_userselectable_filter:
return [self.unify(f) for f in self._config.userselectable_filter]
else:
return []
@hookimpl
def mp_filter_pipeline_step(self, image: Image.Image, plugin_filter: str, preview: bool) -> Image.Image | None:
filter = self.deunify(plugin_filter)
if filter: # if anything, then filter, else for None this plugin is not requested, leave.
return self.do_filter(image, cast(available_filter, filter))
def do_filter(self, image: Image.Image, config_filter: available_filter) -> Image.Image:
filter = getattr(ImageFilter, config_filter)
filtered_image = image.filter(filter=filter)
return filtered_image
The configuration consists of a class and one bool variable to enable the plugin.
import typing
from pydantic import Field
from pydantic_settings import SettingsConfigDict
from photobooth import CONFIG_PATH
from photobooth.services.config.baseconfig import BaseConfig
available_filter = typing.Literal[
"CONTOUR",
"FIND_EDGES",
"EMBOSS",
]
class FilterStableConfig(BaseConfig):
model_config = SettingsConfigDict(title="Filter Plugin Config", json_file=f"{CONFIG_PATH}plugin_filter_example.json")
add_userselectable_filter: bool = Field(
default=True,
description="Add userselectable filter to the list the user can choose from.",
)
userselectable_filter: list[available_filter] = Field(
default=[f for f in typing.get_args(available_filter)],
description="Select filter, the user can choose from. Even if unselected here, the filter is still available in the admin configuration.",
)
# ... add more configuration here...
Publish Plugins¶
Functionality not tested yet. Basically you can package the plugin and use the entry group 'photobooth.plugins' which is scanned during photobooth-app startup. If the plugin is detected, it will be used.