mirror of
https://github.com/EdenQwQ/nixos.git
synced 2026-02-22 21:05:50 +08:00
add wallpaper effects
This commit is contained in:
parent
27eb2246a9
commit
4851a35f73
8 changed files with 430 additions and 16 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
./goNord.nix
|
||||
./lutgen.nix
|
||||
./buildWallpaper.nix
|
||||
./effects.nix
|
||||
];
|
||||
}
|
||||
|
|
|
|||
43
home/lib/wallpaper/effects.nix
Normal file
43
home/lib/wallpaper/effects.nix
Normal 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
|
||||
'';
|
||||
};
|
||||
}
|
||||
265
home/lib/wallpaper/hydrogen.py
Normal file
265
home/lib/wallpaper/hydrogen.py
Normal 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()
|
||||
|
|
@ -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
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue