diff options
| -rw-r--r-- | README.md | 25 | ||||
| -rwxr-xr-x | xcursor2svg.sh | 67 |
2 files changed, 57 insertions, 35 deletions
@@ -4,10 +4,11 @@ Please note that Generative AI was used for making this project In GNOME >=51, it is now required to use only svg based cursor as xcursor got deprecated now, I didn't want to give up on my xcursor themes, which were drawn by artists and not -something rasterized, what this script does is it takes each pixel of the original cursor -and draws a svg pixel-by-pixel, I think desktop environment like GNOME and KDE cache the -svg into bitmap, so having a thousand rectangles in a few hundred frame wont cause any -performance issue. (atleast I hope so, idk I didn't check this). +something rasterized, what this script does is it takes the bitmap of each cursor frame, +resizes it with nearest-neighbor sampling, and embeds it in an svg as a base64 data URI. +This keeps the original pixel art intact while producing the svg cursors that newer +desktops expect. If you prefer the old approach, `--pixel` draws every visible pixel as +an SVG rectangle instead. I tested this on [Hatsune Miku cursor theme by supermariofps](https://github.com/supermariofps/hatsune-miku-windows-linux-cursors) and a @@ -15,8 +16,8 @@ few other legacy xcursor themes. It worked pretty well for me on GNOME 51. Pleas Pull request or submit an issue if something goes wrong. Thank you ^^ > [!IMPORTANT] -> This project requires `xcur2png` and `imagemagick` which you can find in your standard distro -> repos. +> This project requires `xcur2png`, `imagemagick`, and `base64` (part of coreutils), which +> you can find in your standard distro repos. ## Usage : @@ -24,16 +25,20 @@ Pull request or submit an issue if something goes wrong. Thank you ^^ ./xcursor2svg.sh --res 64 ~/.icons/miku-cursor-linux # install from a old xcursor theme ./xcursor2svg.sh --res 64 -y /path/to/theme # Convert and install automatically ./xcursor2svg.sh --res 64 -n /path/to/theme # Convert only +./xcursor2svg.sh --res 64 --pixel /path/to/theme # Draw rectangles instead of embedding the bitmap # For a theme already installed in a standard icon directory: ./xcursor2svg.sh --res 64 miku-cursor-linux ``` `--res` sets the nominal cursor size (default: 32). For each cursor, the closest available Xcursor size is selected, preferring the larger size in a tie. All -frames at that size are resized with nearest-neighbor sampling. Visible pixels -become SVG rectangles with their original colors and transparency. A source at -the requested size keeps its pixels exactly; resizing replicates or drops pixels, -without smoothing. These SVGs retain the bitmap appearance when scaled. +frames at that size are resized with nearest-neighbor sampling and embedded in +the SVG as a base64 PNG. A source at the requested size keeps its pixels exactly; +resizing replicates or drops pixels, without smoothing. + +Pass `--pixel` to use the older output format, where every visible pixel becomes +an SVG rectangle with its original color and transparency. That produces larger, +more verbose files but requires no embedded image data. Animation order, delays, hotspots, and aliases are preserved in `cursors_scalable/`, with missing GNOME/CSS names filled from legacy equivalents. diff --git a/xcursor2svg.sh b/xcursor2svg.sh index 0cd00fb..8ac64f3 100755 --- a/xcursor2svg.sh +++ b/xcursor2svg.sh @@ -1,13 +1,14 @@ #!/usr/bin/env bash -# Convert an Xcursor theme to pixel-based SVG cursors. +# Convert an Xcursor theme to SVG cursors with embedded bitmaps. set -euo pipefail export LC_ALL=C usage() { - echo "Usage: $0 [--res SIZE] [-y | -n] THEME_DIRECTORY_OR_NAME" + echo "Usage: $0 [--res SIZE] [--pixel] [-y | -n] THEME_DIRECTORY_OR_NAME" echo "Point to the Xcursor theme directory containing the cursors/ subdirectory." echo "An installed theme name can also be used." echo " --res SIZE Nominal cursor size (default: 32)." + echo " --pixel Draw each pixel as an SVG rectangle instead of embedding the bitmap." echo " -y Automatically install after conversion." echo " -n Convert only; do not install." echo "Example: $0 --res 64 -y miku-cursor-linux" @@ -17,6 +18,7 @@ die() { echo "Error: $*" >&2; exit 1; } res=32 theme= +method=embed install_mode=ask while (($#)); do case "$1" in @@ -25,6 +27,7 @@ while (($#)); do res=$2 shift 2 ;; + --pixel) method=pixel; shift ;; -y) [[ $install_mode != n ]] || die "Use either -y or -n, not both."; install_mode=y; shift ;; -n) [[ $install_mode != y ]] || die "Use either -y or -n, not both."; install_mode=n; shift ;; -h|--help) usage; exit 0 ;; @@ -40,6 +43,9 @@ done for command in xcur2png awk readlink; do command -v "$command" >/dev/null || die "Required command not found: $command" done +if [[ $method == embed ]]; then + command -v base64 >/dev/null || die "Required command not found: base64" +fi if command -v magick >/dev/null; then image_command=magick elif command -v convert >/dev/null; then @@ -94,7 +100,7 @@ else fi convert_cursor() { - local cursor=$1 shape=$2 selected size xhot yhot png delay + local cursor=$1 shape=$2 selected size xhot yhot png delay bitmap local width height scaled_width scaled_height hotspot_x hotspot_y extraction_log frame=0 comma= local directory=$build/cursors_scalable/$shape @@ -125,28 +131,39 @@ convert_cursor() { read -r hotspot_x hotspot_y < <(awk -v x="$xhot" -v y="$yhot" -v w="$width" -v h="$height" \ -v sw="$scaled_width" -v sh="$scaled_height" 'BEGIN {printf "%.8f %.8f\n", x*sw/w, y*sh/h}') - # Nearest-neighbor resizing preserves hard pixel edges. Each visible - # pixel becomes a unit rectangle, including its original alpha. - "$image_command" "$png" -filter point -resize "${scaled_width}x${scaled_height}!" \ - -colorspace sRGB -alpha on -depth 8 txt:- | - awk -v w="$scaled_width" -v h="$scaled_height" ' - BEGIN { - printf "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\" viewBox=\"0 0 %d %d\" shape-rendering=\"crispEdges\">\n", w,h,w,h - } - /^#/ {next} - { - split($1, xy, /[:,]/) - hex="" - for (i=2; i<=NF; i++) if ($i ~ /^#[[:xdigit:]]+$/) {hex=$i; break} - if (length(hex) != 9) exit 1 - alpha=16*(index("0123456789ABCDEF", toupper(substr(hex,8,1)))-1) + index("0123456789ABCDEF", toupper(substr(hex,9,1)))-1 - if (alpha == 0) next - printf "<rect x=\"%d\" y=\"%d\" width=\"1\" height=\"1\" fill=\"%s\"", xy[1],xy[2],substr(hex,1,7) - if (alpha < 255) printf " fill-opacity=\"%.8f\"", alpha/255 - print "/>" - } - END {print "</svg>"} - ' > "$directory/frame-$frame.svg" + # Nearest-neighbor resizing preserves hard pixel edges. By default the + # bitmap is resized and embedded in the SVG as a base64 data URI. + # With --pixel, each visible pixel becomes a unit rectangle instead. + if [[ $method == pixel ]]; then + "$image_command" "$png" -filter point -resize "${scaled_width}x${scaled_height}!" \ + -colorspace sRGB -alpha on -depth 8 txt:- | + awk -v w="$scaled_width" -v h="$scaled_height" ' + BEGIN { + printf "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\" viewBox=\"0 0 %d %d\" shape-rendering=\"crispEdges\">\n", w,h,w,h + } + /^#/ {next} + { + split($1, xy, /[:,]/) + hex="" + for (i=2; i<=NF; i++) if ($i ~ /^#[[:xdigit:]]+$/) {hex=$i; break} + if (length(hex) != 9) exit 1 + alpha=16*(index("0123456789ABCDEF", toupper(substr(hex,8,1)))-1) + index("0123456789ABCDEF", toupper(substr(hex,9,1)))-1 + if (alpha == 0) next + printf "<rect x=\"%d\" y=\"%d\" width=\"1\" height=\"1\" fill=\"%s\"", xy[1],xy[2],substr(hex,1,7) + if (alpha < 255) printf " fill-opacity=\"%.8f\"", alpha/255 + print "/>" + } + END {print "</svg>"} + ' > "$directory/frame-$frame.svg" + else + bitmap=$("$image_command" "$png" -filter point -resize "${scaled_width}x${scaled_height}!" \ + -colorspace sRGB -alpha on -depth 8 png:- | base64 -w0) || + die "Cannot encode $png" + [[ -n $bitmap ]] || die "Cannot encode $png" + printf '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d" viewBox="0 0 %d %d" image-rendering="optimizeSpeed">\n<image width="%d" height="%d" xlink:href="data:image/png;base64,%s"/>\n</svg>\n' \ + "$scaled_width" "$scaled_height" "$scaled_width" "$scaled_height" \ + "$scaled_width" "$scaled_height" "$bitmap" > "$directory/frame-$frame.svg" + fi printf '%s {"filename":"frame-%s.svg","delay":%s,"hotspot_x":%s,"hotspot_y":%s,"nominal_size":%s}' \ "$comma" "$frame" "${delay:-0}" "$hotspot_x" "$hotspot_y" "$res" >> "$directory/metadata.json" comma=$',\n' |
