Skip to main content

InspyreSplash

Documentation Status PyPI version Python versions License: MIT

inspyre-splash is a small PySide6-powered splash screen composer for Python desktop apps. It builds transparent, frameless splash windows from ordered layers: static images, animated images decoded by Pillow, and animated typography.

Author: Taylor B. | Inspyre-Softworks

This is an MVP. It intentionally avoids progress bars, themes, audio, video, Lottie, and other extras so the public API stays practical.

Features

  • Transparent, frameless PySide6 splash windows.
  • Ordered layers where earlier layers render behind later layers.
  • Static PNG/JPEG/WebP image layers backed by QPixmap.
  • Animated WebP, GIF, and APNG layers decoded through Pillow.
  • Text sequence layers with built-in FadeIn, FlyIn, Typewriter, WipeIn, and ExplodeIn effects.
  • Optional text backgrounds, opacity, shadows, custom fonts, absolute positioning, and loop control.
  • Blocking run_until() startup work for app launch flows.
  • Non-blocking start_until() startup work for IPython, ptipython, notebooks, and other interactive prompts.
  • Cooperative cancellation through SplashTask.cancel() and caller-owned threading.Event objects.
  • Headless-friendly tests with QT_QPA_PLATFORM=offscreen.

Documentation

The full Sphinx documentation is configured for Read the Docs and lives under docs/.

poetry install --with docs --no-interaction
poetry run sphinx-build -W -b html docs docs/_build/local-html

Read the Docs builds from docs/conf.py using .readthedocs.yaml.

Install

From the project root:

poetry install

For an editable pip install:

python -m pip install -e .

Basic Example

from pathlib import Path

from inspyre_splash import Splash
from inspyre_splash.layers import AnimatedImageLayer

splash = Splash(width=700, height=700, transparent=True, stay_on_top=True)
splash.add_layer(
    AnimatedImageLayer(
        Path('assets/transparent_splash.webp'),
        loops=None,
        scale=1.0,
        position='center',
    )
)
splash.close_after_ms(5000)
splash.show()
splash.run()

The splash can also be used as a context manager. Exiting the block stops all layers and closes the splash widget:

with Splash(width=700, height=700) as splash:
    splash.add_layer(AnimatedImageLayer(Path('assets/transparent_splash.webp')))
    splash.close_after_ms(5000)
    splash.show()
    splash.run()

For startup work, use run_until(). It runs your callable while the splash is active, keeps Qt's event loop alive for animations, then closes the splash when the callable returns:

def load_application():
    return build_main_window()


with Splash(width=700, height=700) as splash:
    splash.add_layer(AnimatedImageLayer(Path('assets/transparent_splash.webp')))
    main_window = splash.run_until(load_application)
    splash.finish(main_window)
    splash.run()

For an interactive prompt, notebook, or ptipython session where you want the prompt back immediately, use start_until() instead. It shows the splash, starts your callable on a worker thread, and returns a SplashTask handle.

%gui qt

splash = Splash(width=1200, height=1200, transparent=True, stay_on_top=True)
splash.add_layer(ImageLayer(Path(PNG), scale=1.0, position='center'))
splash.add_layer(AnimatedImageLayer(Path(WEBP), scale=1.0, position='center'))
splash.add_layer(text_sequence)

task = splash.start_until(__load__, 5, cancel_kwarg='cancel_event')

start_until() does not use the Splash context manager because leaving a with Splash(...) block closes the splash. Keep splash and task assigned until the work is done. It also shows the splash by default, so do not call splash.show() or splash.run() after start_until().

To cancel from the prompt, call:

task.cancel()

Cancellation is cooperative: the worker callable must accept and check the injected cancel_event to stop its own Python work:

def __load__(temporal_pad: int, cancel_event):
    for _ in range(temporal_pad):
        if cancel_event.is_set():
            return 'cancelled'
        sleep(1)
    return 'loaded'

Later, inspect the result or any exception:

task.done()
task.result()
task.exception()

If you prefer to own the event yourself, pass it to your worker and then either set it directly or call task.cancel():

event = Event()
task = splash.start_until(__load__, 5, event)

event.set()
# or:
task.cancel()

splash.run() blocks while Qt's event loop is active. If you manage your own callbacks, use splash.stop() or splash.exit() when the work is done and you want run() to return. stop() is safe to call from a worker thread because it queues the shutdown back onto Qt's thread.

For synchronous code inside a with Splash(...) block, use splash.leave() to close the splash and leave the context immediately:

with Splash(width=700, height=700) as splash:
    splash.show()

    if startup_failed:
        splash.leave()

    splash.run()

Use leave() from normal Python control flow. Use exit() from Qt callbacks, because exceptions raised inside Qt callbacks do not reliably unwind the Python context manager.

Package Asset Discovery

Host applications can keep splash assets beside their own package and let inspyre-splash discover them. Discovery is on by default for the helper calls, but it can be disabled per call, globally, or with the INSPYRE_SPLASH_AUTO_DISCOVERY=0 environment variable.

Supported package-side layouts:

your_app/
  splash.json
  splash/
    intro/
      splash.json
      transparent_splash.webp
      glow.png
    updater/
      splash.json
      updater.webp
  assets/
    splashes/
      release/
        inspyre-splash.json
        release.webp

The same layouts are supported below PlatformDirs(appname='your_app').user_data_path, which lets users or installers override bundled splash assets without writing inside the package directory.

From inside the host package, call auto_splash() without arguments and it will infer the calling package:

from inspyre_splash import auto_splash


def launch():
    splash = auto_splash(name='intro')
    if splash is None:
        return load_application()

    main_window = splash.run_until(load_application)
    splash.finish(main_window)
    splash.run()

You can also be explicit, which is nicer for tests and command-line entry points:

from inspyre_splash import auto_splash, configure_auto_discovery

configure_auto_discovery(True)
splash = auto_splash('your_app', enabled=True)

A splash JSON definition can describe window options and ordered layers:

{
  "name": "intro",
  "splash": {
    "width": 700,
    "height": 700,
    "transparent": true,
    "stay_on_top": true
  },
  "layers": [
    {
      "type": "image",
      "path": "glow.png",
      "scale": 1.15,
      "position": "center"
    },
    {
      "type": "animated_image",
      "path": "transparent_splash.webp",
      "loops": null,
      "scale": 1.0,
      "position": "center"
    },
    {
      "type": "text_sequence",
      "font_family": "Segoe UI",
      "font_size": 34,
      "color": "#ffffff",
      "x": 70,
      "y": 565,
      "background_color": "#111827",
      "background_opacity": 0.62,
      "shadow_color": "#000000",
      "shadow_opacity": 0.7,
      "loops": null,
      "entries": [
        {"text": "TAYLOR SUITE", "effect": "wipe_in"},
        {"text": "Loading modules...", "effect": {"type": "fly_in", "direction": "bottom"}},
        {"text": "Almost there...", "effect": {"type": "typewriter", "speed_ms": 42}},
        {"text": "READY", "effect": "explode_in"}
      ]
    }
  ]
}

Layer paths and custom font paths are resolved relative to the folder containing the JSON file, so every animation can be a self-contained folder.

Layered Typography Example

from pathlib import Path

from inspyre_splash import Splash
from inspyre_splash.effects import ExplodeIn, FlyIn, Typewriter, WipeIn
from inspyre_splash.layers import AnimatedImageLayer, ImageLayer, TextSequenceLayer

splash = Splash(width=700, height=700, transparent=True, stay_on_top=True)

splash.add_layer(
    ImageLayer(
        Path('assets/transparent_45deg_glow_ellipse.png'),
        scale=1.15,
        position='center',
    )
)
splash.add_layer(
    AnimatedImageLayer(
        Path('assets/transparent_splash.webp'),
        loops=None,
        scale=1.0,
        position='center',
    )
)
splash.add_layer(
    TextSequenceLayer(
        entries=[
            ('TAYLOR SUITE', WipeIn()),
            ('Loading modules...', FlyIn(direction='bottom')),
            ('Almost there...', Typewriter(speed_ms=42)),
            ('READY', ExplodeIn()),
        ],
        font_family='Segoe UI',
        font_size=34,
        color='#ffffff',
        x=70,
        y=565,
        background_color='#111827',
        background_opacity=0.62,
        shadow_color='#000000',
        shadow_opacity=0.7,
        shadow_x_offset=3,
        shadow_y_offset=3,
        loops=None,
    )
)

splash.show()
splash.run()

Development

Use Poetry for contributor and AI-helper workflows:

poetry install --with dev --no-interaction
$env:QT_QPA_PLATFORM = 'offscreen'
poetry run python -m unittest discover -s tests
poetry run python -m compileall src tests examples docs

See CONTRIBUTING.md for the full contributor workflow.

Notes

Animated WebP, GIF, and APNG files are decoded through Pillow with ImageSequence. Per-frame durations are respected when Pillow exposes them.

Layer order is simple: earlier layers are behind later layers. Add a static transparent PNG first, then an animated WebP, then text if the text should render above the images.

Text sequence layers use position='center' by default, or position='bottom' for bottom placement. Pass x and y to place the text at an exact top-left pixel inside the transparent splash window. Pass background_color and background_opacity to paint a colored backing behind the text. Pass shadow_color, shadow_opacity, shadow_x_offset, and shadow_y_offset to paint a drop shadow with the text.

Image layers fit inside the splash bounds by default so oversized PNG/WebP/GIF/APNG assets are not unexpectedly clipped. Pass fit_to_parent=False if you want native pixel sizing where an oversized layer can intentionally bleed outside the splash.

Animated image layers and text sequence layers accept loops=None for forever or an integer for a finite number of loops. Any layer can be interrupted with layer.interrupt(); animated layers stop their timers, text layers stop the active effect, and static layers hide themselves.

Transparent window behavior can vary by operating system, display server, and window compositor. The package requests a translucent, frameless PySide6 window, but final blending is controlled by the host OS.

Metadata

Release files for inspyre-splash 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for inspyre-splash 0.2.0
File Size Uploaded Provenance Actions
inspyre_splash-0.2.0.tar.gz 41.5 kB Without provenance Details for inspyre_splash-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for inspyre-splash 0.2.0
File Interpreter ABI Platform Actions
inspyre_splash-0.2.0-py3-none-any.whl Python 3 none any Details for inspyre_splash-0.2.0-py3-none-any.whl

Total release size: 71.0 kB

Release files / inspyre_splash-0.2.0.tar.gz

Download URL inspyre_splash-0.2.0.tar.gz
Size 41.5 kB
Tags Source
SHA-256 checksum
How to use checksums
744f603f56463ebe498c3b416036efe1e8c84370a210ff83da881ee36260c5d1
BLAKE2b-256 checksum
How to use checksums
ec280bf823094bac4cb27209a150ac3bcfac232c05101dcc5303a34ade54cfc0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.2.1 CPython/3.14.5 Windows/11

Release files / inspyre_splash-0.2.0-py3-none-any.whl

Download URL inspyre_splash-0.2.0-py3-none-any.whl
Size 29.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8ef88a6a824c4e980352a1200ec58f830fd4924d128e9a1a239eefe7ab14f3ad
BLAKE2b-256 checksum
How to use checksums
06bb80c6c72fcf7feed133d9676a3042188fd7cbf13e907917264e20b00a90fe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.2.1 CPython/3.14.5 Windows/11

Security for inspyre-splash 0.2.0

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A 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.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page