add wallpaper effects

This commit is contained in:
EdenQwQ 2025-04-04 20:17:53 +08:00
parent 27eb2246a9
commit 4851a35f73
8 changed files with 430 additions and 16 deletions

View file

@ -11,10 +11,21 @@ let
getWallpaper =
wallpaper:
let
inherit (wallpaper) name path convertMethod;
inherit (wallpaper)
name
baseImageName
path
convertMethod
effects
;
in
{
inherit name convertMethod;
inherit
name
baseImageName
convertMethod
effects
;
}
// (
if path == null then
@ -25,23 +36,59 @@ let
{ inherit path; }
);
getName =
path:
baseNameOf path |> match "(.*)\\..*" |> head |> lib.splitString "-" |> tail |> concatStringsSep "-";
applyEffect =
{
name,
path,
}:
effect:
if hasAttr effect.name config.lib.wallpapers.effects then
config.lib.wallpapers.effects.${effect.name} { inherit name path; } // effect.passthru
else
path;
applyEffects =
wallpaper:
let
inherit (wallpaper)
name
baseImageName
path
convertMethod
effects
;
in
{
inherit name baseImageName convertMethod;
}
// (
if effects == null then
{ inherit path; }
else
{
path = foldl' (
acc: elem:
applyEffect {
inherit name;
path = acc;
} elem
) path effects;
}
);
generateWallpaper =
wallpaper:
let
inherit (wallpaper) path convertMethod;
# name = getName path;
name = match "(.*)\\..*" wallpaper.name |> head;
baseImageName = if wallpaper.baseImageName == null then name else wallpaper.baseImageName;
live = (toString path |> match ".*gif$") != null;
thisWallpaper = { inherit name path live; };
in
{
inherit name live;
path =
if lib.strings.hasPrefix name config.lib.stylix.colors.scheme then
if lib.strings.hasPrefix baseImageName config.lib.stylix.colors.scheme then
path
else if convertMethod == "gonord" then
goNord thisWallpaper
@ -85,6 +132,7 @@ in
lib.wallpapers = {
inherit
getWallpaper
applyEffects
convertWallpaper
generateWallpaper
setWallpaper

View file

@ -3,5 +3,6 @@
./goNord.nix
./lutgen.nix
./buildWallpaper.nix
./effects.nix
];
}

View file

@ -0,0 +1,43 @@
{ pkgs, config, ... }:
{
lib.wallpapers.effects = {
hydrogen =
{
name,
path,
cropAspectRatio ? with config.lib.monitors.mainMonitor.mode; "${toString width}x${toString height}",
extraArguments ? "",
}:
pkgs.runCommand name { buildInputs = [ pkgs.imagemagick ]; } ''
${pkgs.python3}/bin/python ${./hydrogen.py} -i ${path} --aspect-ratio-crop ${cropAspectRatio} -o $out ${extraArguments}
'';
vignette =
{
name,
path,
extraArguments ? "-d 100 -o 20",
}:
let
# Fred's ImageMagick script
fredVignette = pkgs.stdenv.mkDerivation {
name = "vignette";
src = pkgs.fetchurl {
url = "http://www.fmwconcepts.com/imagemagick/downloadcounter.php?scriptname=vignette3&dirname=vignette3";
sha256 = "XlisDt8FTK20UY3R+nqaTP96rHM3fE20wKM1sT6udrM=";
};
nativeBuildInputs = [ pkgs.bash ];
dontUnpack = true;
installPhase = ''
install -Dm755 $src $out/bin/vignette
'';
};
in
pkgs.runCommand name
{
buildInputs = [ pkgs.imagemagick ];
}
''
${fredVignette}/bin/vignette ${extraArguments} ${path} $out
'';
};
}

View file

@ -0,0 +1,265 @@
# Generated by DeepSeek and Gemini
import argparse
import subprocess
import os
import shlex
import re # Import re for parsing the aspect ratio string
def get_image_dimensions(image_path):
"""Gets the width and height of an image using ImageMagick identify."""
# Append [0] to the image path to select the first frame,
# which works for both single-frame and multi-frame images.
image_path_with_frame = f"{image_path}[0]"
# Construct the command list correctly
cmd = ["magick", "identify", "-format", "%wx%h", image_path_with_frame]
# ^-- image_path_with_frame is now a single string element in the list
try:
process = subprocess.run(cmd, check=True, capture_output=True, text=True)
output = process.stdout.strip()
if "x" in output:
# Handle potential output like '1920x1080+0+0' from some identify versions
# by splitting only on 'x' and taking the first two parts.
dims = output.split("x")
width = int(dims[0])
height = int(dims[1].split("+")[0]) # Handle potential offset info
return width, height
else:
print(
f"Warning: Could not parse dimensions from identify output: '{output}' for image '{image_path}'"
)
return None, None
except subprocess.CalledProcessError as e:
print(f"Error running identify: {e}")
# Use cmd here, not the base command without the modified path
print(f"Command failed: {' '.join(shlex.quote(c) for c in cmd)}")
print("ImageMagick Error Output:\n", e.stderr)
return None, None
except FileNotFoundError:
print(
"Error: 'magick' command not found. Is ImageMagick installed and in your PATH?"
)
return None, None
except ValueError as e:
print(f"Error parsing dimensions '{output}' from identify output: {e}")
return None, None
except Exception as e:
# Catching the specific error from the traceback helps debugging
print(f"An unexpected error occurred during identify ({type(e).__name__}): {e}")
return None, None
def calculate_centered_crop(orig_w, orig_h, target_aspect_ratio):
"""Calculates the WxH for a centered crop preserving target aspect ratio."""
orig_aspect_ratio = orig_w / orig_h
if (
abs(orig_aspect_ratio - target_aspect_ratio) < 1e-6
): # Check for floating point equality
# Already the correct aspect ratio, no crop needed based on ratio
return f"{orig_w}x{orig_h}+0+0"
if orig_aspect_ratio > target_aspect_ratio:
# Original image is wider than target: crop width
crop_h = orig_h
crop_w = int(round(orig_h * target_aspect_ratio))
else:
# Original image is taller than target: crop height
crop_w = orig_w
crop_h = int(round(orig_w / target_aspect_ratio))
# Return geometry string suitable for -crop with -gravity Center
return f"{crop_w}x{crop_h}+0+0"
def main():
parser = argparse.ArgumentParser(
description="Optionally crop an image (manually or by aspect ratio), then overlay it on its blurred version with a shadow."
)
parser.add_argument(
"-i", "--input", required=True, type=str, help="Input image file"
)
parser.add_argument(
"-o", "--output", type=str, default="output.jpg", help="Output image file"
)
# --- Cropping Arguments Group ---
crop_group = (
parser.add_mutually_exclusive_group()
) # Ensure only one crop method is used
crop_group.add_argument(
"-c",
"--crop",
type=str,
default=None,
help="Crop geometry BEFORE processing (e.g., '600x400+100+50'). See ImageMagick -crop documentation. Incompatible with --aspect-ratio-crop.",
)
crop_group.add_argument(
"--aspect-ratio-crop",
metavar="WxH", # Indicate expected format in help
type=str,
default=None,
help="Automatically crop to maintain the aspect ratio given by WxH (e.g., '16x9' or '2240x1400'), centering the crop. Incompatible with --crop.",
)
# --- End of Cropping Arguments ---
parser.add_argument(
"-s",
"--scale",
type=float,
default=0.8,
help="Scale factor for the overlay (e.g., 0.8 for 80%% of original size)",
)
parser.add_argument(
"--blur-arguments",
type=str,
default="0x20",
help="Arguments for the blur (e.g., '0x10' for sigma 10)",
)
parser.add_argument(
"--shadow-arguments",
type=str,
default="80x40+0+0",
help="Arguments for the shadow (e.g., '80x3+0+0')",
)
args = parser.parse_args()
if not os.path.isfile(args.input):
print(f"Error: Input file '{args.input}' not found or is not a file.")
exit(1)
scale_percent = args.scale * 100
crop_geometry_to_use = None
use_gravity_center_for_crop = False
# --- Determine Crop Geometry ---
if args.aspect_ratio_crop:
# Validate and parse the aspect ratio string
match = re.fullmatch(r"(\d+)[x:](\d+)", args.aspect_ratio_crop)
if not match:
print(
f"Error: Invalid format for --aspect-ratio-crop '{args.aspect_ratio_crop}'. Use WxH or W:H (e.g., '16x9')."
)
exit(1)
try:
target_w = int(match.group(1))
target_h = int(match.group(2))
if target_w <= 0 or target_h <= 0:
raise ValueError("Width and height must be positive.")
target_aspect_ratio = target_w / target_h
except (ValueError, ZeroDivisionError) as e:
print(
f"Error: Invalid values in --aspect-ratio-crop '{args.aspect_ratio_crop}': {e}"
)
exit(1)
# Get original dimensions
orig_w, orig_h = get_image_dimensions(args.input)
if orig_w is None or orig_h is None:
print(
"Error: Could not determine image dimensions. Cannot perform aspect ratio crop."
)
exit(1)
# Calculate the crop needed
crop_geometry_to_use = calculate_centered_crop(
orig_w, orig_h, target_aspect_ratio
)
use_gravity_center_for_crop = True # Center this type of crop
print(
f"Calculated aspect ratio crop geometry: {crop_geometry_to_use} (centered)"
)
elif args.crop:
# Use the manually specified crop geometry
# We assume the user knows what they're doing with offsets etc.
# Manual crop typically defaults to top-left unless gravity is set elsewhere.
crop_geometry_to_use = args.crop
use_gravity_center_for_crop = False # Do not force center for manual crop
print(f"Using manual crop geometry: {crop_geometry_to_use}")
# --- End Determine Crop Geometry ---
# --- Build the command list dynamically ---
cmd = ["magick"]
# 1. Add the input file
cmd.append(args.input)
# 2. Add cropping if specified
if crop_geometry_to_use:
if use_gravity_center_for_crop:
# Aspect ratio crop needs gravity center first
cmd.extend(["-gravity", "Center"])
cmd.extend(["-crop", crop_geometry_to_use])
cmd.append("+repage") # Reset page geometry after crop
# 3. Add the rest of the processing steps
cmd.extend(
[
# These steps now operate on the (potentially cropped) image
"(",
"+clone", # Clone the (potentially cropped) image for blurring
"-blur",
args.blur_arguments,
")", # Result: [cropped_image, blurred_background]
"(",
"-clone",
"0", # Clone the original (potentially cropped) image again
"-resize",
f"{scale_percent}%", # Resize this clone
# Result: [cropped_image, blurred_background, resized_overlay]
"(",
"+clone", # Clone the resized overlay to create shadow from
"-background",
"black",
"-shadow",
args.shadow_arguments,
")", # Result: [cropped_image, blurred_background, resized_overlay, shadow]
"+swap", # Swap shadow and resized overlay
# Result: [cropped_image, blurred_background, shadow, resized_overlay]
"-background",
"none",
"-layers",
"merge", # Merge shadow onto resized overlay
"+repage", # Reset page geometry after merge
")", # Result: [cropped_image, blurred_background, overlay_with_shadow]
"-delete",
"0", # Delete the original (potentially cropped) image at index 0
# Result: [blurred_background, overlay_with_shadow]
"-gravity", # <<< This gravity is for the FINAL composite step
"center",
"-compose",
"over",
"-composite", # Composite overlay_with_shadow onto blurred_background
args.output, # Specify the output file
]
)
# --- End of command building ---
print(f"Executing command: {' '.join(shlex.quote(c) for c in cmd)}")
try:
# Use stderr=subprocess.PIPE to capture errors from magick
process = subprocess.run(cmd, check=True, capture_output=True, text=True)
print(f"Successfully created {args.output}")
if process.stdout: # Print stdout from magick if any
print("ImageMagick Output:\n", process.stdout)
if process.stderr:
print("ImageMagick Warnings/Messages:\n", process.stderr)
except subprocess.CalledProcessError as e:
print(f"Error processing image: {e}")
print(f"Command failed: {' '.join(shlex.quote(c) for c in cmd)}")
print("ImageMagick Error Output:\n", e.stderr) # Print stderr on error
except FileNotFoundError:
print(
"Error: 'magick' command not found. Is ImageMagick installed and in your PATH?"
)
except Exception as e:
print(f"An unexpected error occurred: {e}")
if __name__ == "__main__":
main()

View file

@ -1,12 +1,12 @@
{ pkgs, ... }:
{
home.packages = with pkgs; [
python312
python312Packages.pip
python312Packages.virtualenv
python312Packages.numpy
python312Packages.matplotlib
python312Packages.pandas
python3
python3Packages.pip
python3Packages.virtualenv
python3Packages.numpy
python3Packages.matplotlib
python3Packages.pandas
black
conda
];

View file

@ -1,3 +1,4 @@
{ pkgs, ... }:
{
wallpapers = [
{
@ -8,6 +9,23 @@
name = "frieren-butterflies.jpg";
convertMethod = "lutgen";
}
{
name = "frieren-butterflies-hydrogen.jpg";
baseImageName = "frieren-butterflies";
path = "${pkgs.wallpapers}/frieren-butterflies.jpg";
convertMethod = "lutgen";
effects = [
{
name = "hydrogen";
passthru = {
extraArguments = "--shadow-arguments '80x50+0+0'";
};
}
{
name = "vignette";
}
];
}
{
name = "frieren-fire.jpg";
convertMethod = "lutgen";
@ -28,6 +46,17 @@
name = "bangqiaoyan-girl-sky.jpg";
convertMethod = "gonord";
}
{
name = "bangqiaoyan-girl-sky-hydrogen.jpg";
baseImageName = "bangqiaoyan-girl-sky";
path = "${pkgs.wallpapers}/bangqiaoyan-girl-sky.jpg";
convertMethod = "gonord";
effects = [
{
name = "hydrogen";
}
];
}
{
name = "morncolour-pink-landscape.png";
convertMethod = "gonord";