SVGY
Generate icons from SVG
Turn one SVG into all the icons a project needs: resized and optimized SVGs, PNGs, Windows .ico macOS .icns, or all of them in a single pass.
Install
Prebuilt binary
Install in seconds using signed binary, for Linux, macOS, and Windows, on x64 and arm64:
cargo binstall svgy
Source
Install from source using crates.io:
cargo install svgy
Local
Install from a clone of this repository:
cargo install --path .
Signatures
Every release archive is signed with minisign. The public
key is in Cargo.toml.
To check an archive downloaded from the releases page:
minisign -V -m <source archive> -P <public key>
Synopsis
svgy <input.svg> [targets] [options]
With one input file, preferrably a square SVG, svgy can generate many icons formats in one pass.
Defaults
The command svgy file.svg without parameter:
- Resizes the SVG so its longest side is
1024, by rewriting theviewBox. - Pads the canvas to a square of
1024by1024, with the artwork centered. - Optimizes and minifies the SVG with
oxvg. - Reduces the decimals as far as the artwork allows, keeping the render within
2%of the source. - Saves the SVG with a default suffix as
file.svgy.svg.
This is equivalent to:
svgy file.svg --svg --suffix=svgy --size=1024 --precision=0.02
--svg: Create an SVG.--suffix=svgy: Add the suffixsvgyto the output file name.--size=1024: Resize so the longest side is 1024.--precision=0.02: Allow the rounded output to differ from the source by up to 2%.
Asking for any target turns the implicit --svg off. Add --svg explicitly to keep it.
Examples
Create two icons for macOS and Windows in their respective subfolders, and a tiny SVG for the favicon in the same directory:
svgy example.svg --icns=macos/example.icns --ico=windows/example.ico --svg=favicon.svg --size=32
example.svgmacos
example.icns16x16 to 1024x1024windows
example.ico16x16 to 256x256favicon.svg32x32
Note: --size never applies to icons, they keep their fixed size sets (see Sizing).
Create a PNG and keep the resized SVG alongside it:
svgy example.svg --png --svg
example.svgexample.svgy.svg1024x1024, --svg always uses thesvgysuffix to prevent overwriting the source SVG.example.svgy.png1024x1024, default size
Fit the artwork inside a circle, for a round contact avatar:
svgy example.svg --round
example.svgexample.round.svg1024x1024, --round uses theroundsuffix.
Output targets
--svg[=<path>]: Convert to an optimized and minified SVG, indented with tabs.--png[=<path>]: Convert to an optimized PNG.--ico[=<path>]: Convert to an optimized Windows icon.ico.--icns[=<path>]: Convert to an optimized macOS icon.icns.--round[=<path>]: Convert to an SVG whose artwork is centered and fitted inside a circle.
The output images are by default resized to fit 1024 x 1024, except icons which have specific sets of sizes per platform.
Sizing
All sizing parameters are optional, and they all preserve the aspect ratio.
--size=<pixels>: Resize to a square of<pixels>by<pixels>.--width=<pixels>: Resize to the specified width.--height=<pixels>: Resize to the specified height.--no-resize: Keep the original dimensions.--no-square: Fit the canvas to the artwork instead of padding it to the requested size.
Notes:
- Passing
--widthand--heighttogether fits the artwork inside that box. - The canvas is padded to the requested size, and the artwork is centered in it. A 620 x 720 source
with
--size=1024gives aviewBox="0 0 1024 1024". Use--no-squareto getviewBox="0 0 882 1024". - Padding needs both dimensions, so
--widthor--heightalone leaves the free axis tight. - The sizing parameters
--size,--width,--heightare mutually exclusive. - For icons, non-square source SVGs are resized and centered.
- Sizing parameters do not apply to
.icoand.icnssince they have fixed size sets.
Rounding
--padding[=<0.0..1.0>]: Fraction of the inscribed circle's radius to leave empty around the artwork.0.0by default, or0.1when the source has a full-canvas background; bare--paddingmeans0.1. svgy prints which default it applied. The upper bound is exclusive.
--padding applies to --round only.
Output naming
- If the destination is not specified, the output is written beside the source as
<name>.<suffix>.<extension>, withsvgyas the default suffix.svgy example.svg --png->example.svgy.pngsvgy example.svg --icns->example.svgy.icns
--suffix=<suffix>: Use<suffix>instead ofsvgy.svgy example.svg --png --suffix=v2->example.v2.png
--roundis the one exception: its default suffix isround, so that--svgand--roundcan be asked for together without both claimingexample.svgy.svg.svgy example.svg --svg --round->example.svgy.svgandexample.round.svg
Behaviour
- Order of operations: read -> strip text -> resize -> round -> optimize -> write each target.
Rounding runs last, inside whatever canvas
--sizeset, and the raster targets are rendered from the resized SVG, before optimization. - The precision search only changes the SVG.
--png,--icoand--icnsare rendered from the document before optimization, so their pixels never pay for a smaller SVG. --roundis a targetsvgy logo.svg --round --icnswrites a round SVG and a normal.icns, the icon is not round-fitted. For round icons, run svgy twice and feed it the round SVG.- Text is removed.
<text>and its children are stripped from the source before anything else runs, so every target agrees on what the artwork is: it is absent from--svgjust as it is from--png,--icoand--icns, and--roundfits the circle to what remains. svgy prints a warning when it removes text. Convert text to paths before converting the icon. In Inkscape, select the text and use Path > Object to Path (Shift+Ctrl+C). - One input at a time. Globs and multiple inputs are not supported; use a shell loop.
- Missing directories are created. A destination may name a subdirectory that does not exist yet, as the icons example above does.
- Existing files are overwritten without asking.
- Exit codes:
0on success,1on any error, with a message on stderr.
Other options
--precision[=<FRACTION>]: Difference allowed between the optimized SVG and the source, as a fraction of the render.0.02by default, and bare--precisionmeans the same. svgy optimizes at each precision from 0 to 5 decimals, renders both the source and the candidate at 1024 x 1024, and keeps the first candidate whose difference fits the budget. It then writes each element at its own precision when that saves bytes and still fits the budget, and prints the range it used, such asprecision 0 to 1 (6 refined). Expect the difference to come closer to the budget, since the mix spends what one precision for the whole document leaves unspent. This costs up to six renders for the search and up to 32 more for the mix, under a second in practice. If no precision fits, svgy warns and writes the output at 3 decimals.--no-precision: Skip the search and write at a fixed 3 decimals.--no-optimize: Skip optimization. SVG output skipsoxvg, and.png,.icoand.icnsskipoxipng. Optimization dominates the runtime of an.icnsor.ico, so this is the flag to reach for when iterating.--zopfli: Compress every PNG with Zopfli. Saves a 1~5% more, but takes 100x more time (minutes instead of seconds).--no-legacy-ico: Skip the 256-color entries in the Windows icon, keeping only the PNG ones. Saves about 3.6 KiB, at the cost of dropping support for 256-color sessions and pre-Vista shells.
Planned
Not implemented yet. Documented here so the intended surface is on record.
Targets
--all-app[=<app>]: Convert to all native app icons, using the source SVG location as the root directory.- Main icon:
app/<app>.svg - Windows:
windows/<app>.ico - macOS:
macos/<app>.icns - Linux:
linux/<app>_size.pngandlinux/<app>.svg
- Main icon:
--svgz[=<path>]: Convert to an optimized and GZIP-compressed SVGZ.--avif[=<path>]: Convert to an optimized lossless AVIF.--liquid[=<icon>]: Convert to an optimized macOS Liquid Glass icon.icondirectory.--linux[=<app>]: Convert to a set of optimized PNGs (<size>.png) and an SVG (scalable.svg)in theappdirectory.
Actions
--set-folder-icon[=<dir>]: Set the folder icon, macOS only. Without a value, the icon is for the directory containing the source SVG.
Parameters
--in-place: Overwrite the source.svg. SVG output only. Warning: this is a destructive parameter.--keep-ids: Keep SVG IDs, for example<path id="this-is-kept">. svgy shortens IDs and drops the unreferenced ones today, and this parameter turns both off.--round-anchor=<shape|canvas>: Whether--roundrecenters the artwork on the canvas (canvas, the default and current behaviour) or scales it in place, leaving its center where it is (shape).--if-exists=<replace|keep|if-smaller|suffix>: What to do when the destination exists.replaceby default.keepto leave existing files alone,if-smallerto replace only when the new file is smaller,suffixto keep existing files and write to the suffixed name instead, replacing an existing suffixed file.--quiet: No output. Mutually exclusive with--verbose.--verbose: Print out all operations. The default prints one line per file written.--all-app-dir=<dir>: See--all-app.--app_id=<app_id>: See--all-app.
Optimization
IDs referenced from the document (url(#gradient), href="#clip", and the like) are never
stripped, whether or not --keep-ids is passed; the parameter governs the unreferenced ones.
References
Sizes
| Type | Extension | Icons in set | Size |
|---|---|---|---|
| Scalable Vector Graphics | .svg |
1 | 1024 x 1024 |
| Compressed Scalable Vector Graphics (gzip) | .svgz |
1 | 1024 x 1024 |
| Portable Network Graphics | .png |
1 | 1024 x 1024 |
| macOS icon | .icns |
11 | 16 x 16 to 1024 x 1024 |
| Windows icon | .ico |
8 | 16 x 16 to 256 x 256 |
| AV1 Image File Format | .avif |
1 | 1024 x 1024 |
.icns and .ico are multi-resolution containers: they hold every entry listed below, and the
OS picks one. Their size sets are fixed and cannot be overridden, on purpose: the default is
meant to be the good, opinionated answer.
macOS icon
File extension: .icns
| Size | Pixels | Type | Format |
|---|---|---|---|
| 16 x 16 | 16 x 16 | ic04 |
ARGB-RLE |
| 16 x 16 @2 | 32 x 32 | ic11 |
PNG |
| 32 x 32 | 32 x 32 | ic05 |
ARGB-RLE |
| 32 x 32 @2 | 64 x 64 | ic12 |
PNG |
| 48 x 48 | 48 x 48 | icp6 |
PNG |
| 128 x 128 | 128 x 128 | ic07 |
PNG |
| 128 x 128 @2 | 256 x 256 | ic13 |
PNG |
| 256 x 256 | 256 x 256 | ic08 |
PNG |
| 256 x 256 @2 | 512 x 512 | ic14 |
PNG |
| 512 x 512 | 512 x 512 | ic09 |
PNG |
| 512 x 512 @2 | 1024 x 1024 | ic10 |
PNG |
Notes:
- The 16 x 16 and 32 x 32 entries store straight-alpha ARGB with each channel PackBits-RLE encoded; every larger entry embeds its PNG bytes verbatim.
- Eleven entries cover eight distinct pixel sizes, and each size is rendered once. The 256 and 512 renders are also encoded once and their PNG bytes reused across both OSTypes. The 32 render is encoded twice, because its two entries want different formats: ARGB-RLE for
ic05and PNG foric11.
Windows icon
File extension: .ico
Eight entries for six sizes: the two smallest are stored twice, once in full color and once for
256-color consumers. Pass --no-legacy-ico for the six PNG entries alone.
| Size | Format | Declared depth | Alpha |
|---|---|---|---|
| 16 x 16 | PNG | 32 bpp | 8-bit |
| 16 x 16 | BMP | 8 bpp | 1-bit mask |
| 32 x 32 | PNG | 32 bpp | 8-bit |
| 32 x 32 | BMP | 8 bpp | 1-bit mask |
| 48 x 48 | PNG | 32 bpp | 8-bit |
| 64 x 64 | PNG | 32 bpp | 8-bit |
| 128 x 128 | PNG | 32 bpp | 8-bit |
| 256 x 256 | PNG | 32 bpp | 8-bit |
Notes:
- 256 x 256 is the largest size an
.icocan address, as the width and height fields are single bytes (0 meaning 256).- PNG entries are optimized by oxipng. Their declared depth is what Windows compares against the display when choosing between two entries of the same size, so it stays at the conventional 32 even where oxipng has reduced the PNG to an 8-bit palette; readers take the real depth from the IHDR.
- The BMP entries are the classic bottom-up 8bpp DIB: a 256-entry color table, then the color data, then the 1-bit AND mask. Colors come from a median-cut palette of the source render.
- They exist for sessions capped at 256 colors, such as an old
mstscor the "Limit maximum color depth" policy, where Windows prefers an entry declaring 8 bpp. They also keep those two sizes readable by pre-Vista shells, which cannot decode PNG entries at all.- They cost about 3.6 KiB: the color table alone is a fixed 1 KiB per entry, written in full rather than trimmed with
biClrUsed, since the point of these entries is to be boring for old readers.--no-legacy-icodrops them.- A 1-bit mask has no partial transparency. Pixels below 50% alpha vanish and the rest become fully opaque, so anti-aliased edges turn hard. This only affects the 256-color entries.
Linux icons
See:
- Icon Theme Specification
- APPImage: The filesystem image
Installation path:
- SVG:
/usr/share/icons/hicolor/scalable/apps/<id.app>.svge.g. com.ystorian.svgy.svg
- PNG:
/usr/share/icons/hicolor/256x256/apps/<id.app>.pnge.g. com.ystorian.svgy.png
Pipelines
- SVG: rewrite the root
viewBox-> multiply the scale into every coordinate in place -> round coordinates to 6 decimals -> optimize with oxvg -> search for the lowest precision - ROUND: measure the artwork's minimum enclosing circle (background fills excluded) -> bake the scale and translation that map it onto the inscribed circle into every coordinate -> optimize with oxvg -> search for the lowest precision
- PNG: render with resvg -> optimize with oxipng
- ICO: render each size with resvg -> encode PNG -> optimize with oxipng -> embed verbatim, plus a median-cut 256-color BMP for 16 x 16 and 32 x 32
- ICNS: render each size with resvg -> 16 and 32 to straight-alpha ARGB with PackBits RLE, larger sizes to PNG -> optimize with oxipng -> embed verbatim
Resizing adds no transform: the scale and the centering offset are multiplied into each
coordinate, which renders identically to the source while preserving the original element structure.
Existing transforms are rewritten in place rather than flattened, and a <symbol> or any other
template a <use> instantiates keeps its own coordinates, since the instance carries the offset.
The output viewBox always starts at 0 0. This describes the resize step alone. The optimizer
that runs after it is free to restructure the document, and it can move a shared transform onto a
group.
Every SVG goes through oxvg with its default job set. Element structure is indented with tabs and
attribute values are minified, so the output stays readable. removeDimensions is on, except under
--no-resize, where the width and height are the source's own and are kept.
Three jobs are added to the default set:
removeXlinkwritesxlink:hrefas the nativehref, which also frees thexmlns:xlinkdeclaration.removeAttrsdropsxml:space. Text is removed before the optimizer, so nothing is left forxml:spaceto act on, and it stops the serializer from indenting the document.convertStyleToAttrsmoves astyledeclaration to a presentation attribute, whichremoveUnknownsAndDefaultsthen drops when it repeats the default.
Five cleanups of svgy frame those jobs.
The id of the root element is removed first, when nothing in the document names it.
removeUselessStrokeAndFill stops at an element that carries an id, and the root carries every
other element, so one dead id keeps every useless stroke="none" in the document.
A gradient that only lends its stops is written into the gradient that borrows them next. Editors
write a paint as a pair: one <linearGradient> holds the stops, and a second one holds the
coordinates and names the first with an href. The pair is one gradient written twice, so it
becomes one element, which saves the wrapper of the lender and the reference, about 37 bytes a pair.
The move needs the lender to stand alone: a second borrower would need the stops copied, which is
not always shorter, and a fill that names the lender would lose its paint. The two gradients need
not be of the same kind, which editors often write: a gradient inherits only the attributes that its
own kind reads, so the move leaves the coordinates of the other kind behind. A <radialGradient>
reads no x1, and a <linearGradient> no cx. No oxvg job does any of this, and
convertOneStopGradients only handles a gradient with a single stop.
A gradientTransform is folded into the coordinates of its gradient next. The color of a
<linearGradient> is an affine function of the point, and an affine function stays affine through
an affine map, so every invertible transform folds into its two ends. A <radialGradient> also
carries a circle, and only a translation, a rotation and a uniform scale keep a circle a circle. A
skew or an axis-dependent scale makes an ellipse, which r cannot express. That transform stays,
and so does one on a coordinate given as a percentage, or on an absent coordinate (a percentage by
default).
An opacity written in a style attribute then keeps three decimals. cleanup_numeric_values reads
attributes, and stop-opacity maps to no attribute that convertStyleToAttrs can write, so an
editor value such as stop-opacity:0.98039216 reaches the output whole. An opacity holds a fraction
of 0 to 1, where three decimals name every step the eye can tell apart. A length keeps its decimals,
since the precision search is what decides those.
A namespace declaration that no element or attribute name uses is removed last. Editors leave
declarations such as xmlns:svg behind, and the oxvg serializer repeats them on every element.
The default xmlns always stays.
A <style> or a <script> decides what an element looks like in a way that reading the attributes
cannot follow. Such a document keeps its style attributes and its root id.
The job list then runs again, up to three times in all. Each job runs once per run, in a fixed
order, and the jobs that merge segments run before the jobs that round the numbers. Rounding can
make two segments collinear, which the merge that already ran can no longer see. The next run reads
what the run before it wrote and takes the merge. A run that writes no fewer bytes than the run
before it is dropped, since convertPathData can trade a curve for a longer arc. Every test file
settles after the second run.
One number then drives the five oxvg rounding settings, and svgy searches for the lowest one that
still looks right. convertPathData dominates the result, since most of an icon is path data. The
error grows as the precision drops, so the first precision that fits the budget is also the smallest
one that fits. mergePaths keeps a tolerance of its own that the search cannot reach, which is why
a small difference remains even at the highest precision.
Artwork already drawn on whole numbers reaches 0 decimals with no difference at all. Detailed
artwork settles higher. Expect --round to land a decimal higher than --svg on the same source,
because rounding has already rescaled the coordinates off whole numbers.
One precision for the whole document gives every element the decimals that the worst element needs,
which leaves budget unspent. svgy then mixes the candidates it has already built: every element
starts at the lowest precision, and the elements that need decimals buy them back one at a time. The
output reports the range it spans, such as precision 0 to 1 (6 refined).
Which element to refine next is an estimate, and only the order of the tries rests on it. A rounded coordinate moves an edge sideways, so the estimate is the length of the outline that moves, divided by the bytes another decimal costs. A gradient has no outline and goes last. Every try is then rendered, and the budget decides, so a poor estimate costs bytes and never accuracy.
A mix needs the candidates to hold the same document, since it moves attributes between them. The
element names, the nesting, the attribute names, and every id, href and class must match. A
job such as mergePaths can restructure the document differently at another precision, and svgy
then keeps what the search chose. It also keeps that document when no mix reaches the budget, or
when the mix saves nothing.
The mix spends the budget you name, so expect the reported difference to rise towards it. Pass a
smaller --precision to buy the accuracy back.
Every PNG goes through oxipng at preset 4, with alpha optimization on. Pass --zopfli to use
the Zopfli deflater, this saves 1~5% more, 2% on average, but this requires minutes instead of
seconds. Pass --no-optimize to skip optimization entirely.
Requirements
Rust, edition 2024.
--set-folder-icon (planned) will be OS-specific. Everything else is cross-platform: an .icns can
be produced on Windows and an .ico on macOS.
Licence
Dual-licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.
Unless you state otherwise, any contribution intentionally submitted for inclusion in svgy, as defined in the Apache-2.0 licence, shall be dual-licensed as above, without any additional terms or conditions.