Converts an SVG file into a Windows cursor (.cur), or an animated cursor (.ani) when the SVG is animated. The file holds one image per size, by default 32, 48, 64, 96, 128 and 256 px, and each size gets its own scaled hotspot. It is a .NET 10 NativeAOT console app built with System.CommandLine, ImageSharp and Svg.Skia, and installed with Velopack.
Download the setup or the portable archive for your processor from the latest release: win-x64 for most PCs, win-arm64 for Windows on Arm, win-x86 for 32-bit Windows.
- Setup (
VectorToCursor-win-x64-Setup.exe): installs into%LocalAppData%\VectorToCursorwithout administrator rights and adds itscurrentfolder to your userPATH, soVectorToCursor, orvtcfor short, runs in every terminal you open afterwards. Uninstalling under Settings > Apps > Installed apps removes thePATHentry again. - Portable (
VectorToCursor-win-x64-Portable.zip): extract it anywhere. It holds aVectorToCursorfolder withVectorToCursor.exeand the two DLLs it needs, which must stay together, and the short commandvtc. Nothing is installed andPATHstays as it is.
VectorToCursor <input> -x <x> -y <y> [-o <file>] [--sizes <list>] [--image-format <bmp|png>] [--bleed <percent>] [--fps <rate>]vtc is short for VectorToCursor and takes the same arguments. It hands them on through cmd, so in PowerShell a file name that contains & or ^ but no space only works with VectorToCursor itself.
| Argument / option | Meaning |
|---|---|
<input> |
The SVG file to convert. CSS or SMIL animations make it an animated cursor; see Animated cursors. |
-x, --hotspot-x |
Hotspot X in SVG coordinates (required). |
-y, --hotspot-y |
Hotspot Y in SVG coordinates (required). |
-o, --output |
The cursor file to write: .cur for a static SVG, .ani for an animated one. Defaults to the input path with that extension. An existing file is overwritten. |
--sizes |
The image sizes in px, from 1 to 256, separated by commas (default 32,48,64,96,128,256). |
--image-format |
How every image in the file is stored: bmp or png. Defaults to bmp for .cur; for .ani to bmp when the frames fit, otherwise png. See Storage. |
--bleed |
Width of the color band around the artwork, in percent of each cursor size (default 5, 0 turns it off). See Transparent pixels. |
--fps |
Frames per second of an animated cursor; must divide 60 (default 30). Ignored for static SVGs. |
Examples:
> VectorToCursor VectorToCursor.Tests/TestData/arrow.svg -x 3 -y 2
Created ...\arrow.cur with BMP images
Size Hotspot
32 3,2
48 4,3
64 6,4
96 9,6
128 12,8
256 24,16
> VectorToCursor VectorToCursor.Tests/TestData/css-blink.svg -x 3 -y 2 --sizes 32,48,64
Created ...\css-blink.ani with BMP images: 6 frames at 30 fps (0.2 s)
Size Hotspot
32 3,2
48 4,3
64 6,4- Coordinates: use the SVG's own coordinate system, the units of its
viewBox, or pixels if it has none. You can read the point straight from your vector editor. - Number format:
.is the decimal separator. Negative values are valid when the viewBox starts below zero. - Scaling: for each size the point is scaled like the artwork, then rounded down to the pixel that contains it.
- Range: a hotspot outside the cursor square is rejected.
- Rendering: every size is drawn directly from the vector data, never downscaled from a larger image.
- Non-square artwork: scaled to fit and centered on whole pixels; the margins stay transparent.
- Requirements: the SVG needs a
viewBoxor an absolutewidth/height. Atransformon the root<svg>element is not supported; wrap the content in a<g>instead. - External resources: only files in the SVG's folder and
data:URIs are loaded. Nothing is fetched from the network.
- Detection: CSS animations (
@keyframeswith theanimationproperties) and SMIL elements such as<animate>and<animateTransform>make the output an.anifile. - Loop: one loop lasts until every animation repeats exactly, the least common multiple of their lengths. Loops longer than 10 minutes are rejected.
- Frames: the loop is sampled evenly at
--fpsframes per second. The rate must divide 60, because animated cursors count time in sixtieths of a second. - Identical frames: a frame that looks exactly like the one before it, pixel for pixel in every size, isn't stored again; the frame before is shown longer instead. A frame that repeats an earlier one is stored once and shown again from there. So a pause or a held pose adds almost nothing to the file, and the summary says how many distinct frames were stored, e.g.
5,010 frames at 30 fps (167 s), stored as 12 distinct frames. A loop may have at most 1,800 distinct frames. - Hotspot: every frame shares the same hotspot.
- CSS support: opacity, fill and stroke with their opacities, stroke width, dash array and dash offset, and the transforms rotate, translate, scale and skew, eased with linear, the
easekeywords,cubic-bezier()without overshoot or single steps. Anything that can't be played back exactly is reported as an error instead of producing a different animation.
-
One format per file: every image of a cursor is stored the same way. Windows ignores the PNG images of a cursor that also holds BMP images and scales up a smaller BMP instead.
-
bmp: 32-bit BMP, which every program that reads cursor files can load. The default for.curfiles. -
png: a fraction of the size, and Windows draws it exactly like BMP at every size. Some programs that load cursor files themselves, such as WinForms, reject it. -
64 KB limit of animated cursors: every frame of an
.anifile is a complete cursor file, and Windows refuses the whole animation when an image starts 64 KB (65,536 bytes) or more into its frame, however large or small the file is. Images are stored smallest first. As BMP they take:Size 32 px 48 px 64 px 96 px 128 px 256 px BMP image 4.3 KB 9.6 KB 16.9 KB 38.1 KB 67.6 KB 270.4 KB So with the default sizes the 128 px image already starts at 69 KB. BMP frames fit with, for example,
--sizes 32,48,64,96,48,64,96,128or32,48,64,256, but never with 128 and 256 px together. PNG frames are far smaller and only reach the limit with very noisy artwork. -
Default for
.anifiles:bmpwhen its frames fit, which the first frame decides because BMP frames all have the same length; otherwisepng. With the default sizes that meanspng. -
Check: every frame is checked as soon as it is rendered. A frame that doesn't fit stops the conversion with an error that names the frame, the image and the byte it starts at, and no file is written.
- Why they get a color: Windows scales a cursor whenever no image matches the requested size (for example 80 or 144 px), and it does so without premultiplying alpha. The color of fully transparent pixels then bleeds into the edges; left black, it shows up as a dark fringe.
- Band: within
--bleedpercent of the cursor size (default 5%, rounded up: 2 px at 32 px up to 13 px at 256 px), transparent pixels take the color of the nearest edge pixel. - Beyond the band: all remaining transparent pixels share one color per image, the average of the band's outer edge. Windows never samples that far from an edge, and a flat area keeps PNG images small.
- Noise filter: pixels below alpha 16 don't pass on their color, because after un-premultiplying it is mostly rounding noise; they take the nearest reliable edge color instead. If the whole artwork is that faint, every visible pixel counts.
- Unchanged look: alpha is never changed, so at native sizes the cursor looks exactly as rendered.
Licensed under the MIT License.