Skip to content

SVG/XML to Compose ImageVector: IntelliJ IDEA / Android Studio plugin + build-in previewer without compilation, CLI tool, Gradle plugin

License

Notifications You must be signed in to change notification settings

ComposeGears/Valkyrie

Repository files navigation

Valkyrie — SVG/XML to Compose ImageVector converter

Icon

Marketplace version Github plugin release Downloads Rating

CLI release Homebrew

Gradle Plugin Portal

Telegram Slack Test coverage License

Motivation

During the development Jetpack Compose / Compose Multiplatform, we often faced the challenge of converting icons from SVG or XML format to ImageVector. While there are existing tools available for this purpose, we found that they often fell short in terms of usability, reliability, and the quality of the generated code and in some cases, even being paid 😄.

To address these issues, we decided to create our own tool that would streamline the conversion process and provide a better user experience.

The primary goal of this project is to offer a fast, reliable, and user-friendly solution for converting SVG and XML icons to ImageVector format, while also allowing for customization of the generated code to meet individual project needs.

Note

This project is especially relevant now as Material Icons is no longer maintained and not recommended for use in your apps. Learn more.

Available tools:

Table of Contents

Key features

Core functionality:

  • Support conversion from SVG and XML
  • Custom kotlinpoet generator with streamlined code formatting:
    • code alignment and formatting
    • remove redundant code by default (e.g. public keyword)
    • remove unused imports (e.g. kotlin.* package)
    • skip default ImageVector parameters
    • support generation as backing property or lazy property
    • optional trailing comma and explicit mode
    • customize code indent
  • Ability to create your unique project icon pack (+nested packs if necessary)
  • High performance (6k icons processing ~5sec)

🔌IDEA Plugin

Plugin features

  • Two conversion modes: Simple and IconPack
  • Support for Drag&Drop files/directories and pasting content from clipboard
  • Easy option to add more icons into existing project icon pack
  • Export generated ImageVector to clipboard or file (depends on the mode)
  • Fully customizable setting for generated icons
  • Build-in ImageVector previewer for any icons without compilation ✨
  • The plugin is completely built using Compose Multiplatform and Tiamat navigation library

More exclusive features under development, stay tuned 🌚

Simple mode

Note

One-click solution to convert SVG/XML to ImageVector (requires only specifying the package).

Available quick actions:

  • Rename icon
  • Preview current ImageVector
  • Copy generated ImageVector to clipboard

Demo:

simple_mode_demo.mp4

IconPack mode

New icon pack

Note

Facilitates creating an organized icon pack with extension properties for your pack object, previewing the list of icons, and batch exporting them to your specified directory.

Demo:

iconpack_mode_new_demo.mp4

Existing icon pack

Note

Instead of importing icon pack settings, the plugin provides a direct way to import an already created icon pack from a Kotlin file.

Important

Editing features are limited for now; you can only load an existing pack and add more nested packs.

Demo:

iconpack_mode_existing_demo.mp4

ImageVector Previewer

Embedded Previewer

We personally find it very useful to have a previewer for ImageVector (such we have for SVG or XML). Previewer available for any ImageVector formats (backing or lazy property, legacy google material icons) without compose @Preview annotation and project compilation.

Previewer actions:

  • Change icon background (pixel grid, white, black)
  • Zoom in, zoom out icon without loosing quality
  • Draw as actual size
  • Fit icon to window

Demo:

imagevector_previewer.mp4

AutoCompletion Previewer

When IDEA auto-completion popup is shown for any ImageVector property, the preview image will be displayed in the popup.

Gutter Previewer

Preview inside gutter available for any ImageVector property. By clicking on the gutter icon, the original file will be opened in the editor with embedded previewer.

Requirements

Plugin version Min IntelliJ IDEA / Android Studio
0.1.0 - 0.14.0 IntelliJ IDEA 2024.1, Android Studio Koala
0.15.0+ IntelliJ IDEA 2024.2, Android Studio Ladybug

Installation

Get from Marketplace
  • Find plugin inside IDE:

    Settings > Plugins > Marketplace > Search for "Valkyrie" > Install Plugin

  • Manually: Download the latest release or build your self and install it manually using Settings -> Plugins -> ⚙️ -> Install plugin from disk...

Build plugin

Precondition: IntelliJ IDEA with installed Plugin DevKit

Run ./gradlew buildPlugin to build plugin locally. Artifact will be available in tools/idea-plugin/build/distributions/ folder

or run plugin in IDE using: ./gradlew runIde

🖥CLI tool

CLI tools can be easily integrated into scripts and automated workflows, allowing you to convert icons from specific source with predefined settings.

Install CLI

  • via brew

brew install ComposeGears/repo/valkyrie
  • manually via binary release

Download latest CLI tool from releases.

Unzip the downloaded archive and run the CLI tool from bin folder in the terminal

./valkyrie

You should see this message

  • automatically using bash script

A simple example of how to get the latest version of the CLI tool. It can be executed on CI/CD with predefined parameters.

#!/bin/bash

VERSION="cli-1.0.0"
TARGET_DIR="valkyrie-cli"
ASSET_NAME="tmp.zip"

LATEST_CLI_RELEASE_URL=$(curl --silent "https://api.github.com/repos/ComposeGears/Valkyrie/releases/tags/$VERSION" \
  | jq -r '.assets[] | select(.name | startswith("valkyrie-cli")) | .browser_download_url')

curl -L -o "$ASSET_NAME" "$LATEST_CLI_RELEASE_URL"
mkdir -p "$TARGET_DIR"
unzip -o "$ASSET_NAME" -d "$TARGET_DIR"

rm "$ASSET_NAME"

cd "$TARGET_DIR/bin" || exit
./valkyrie --version

Available commands:

iconpack command

A part of the CLI tool that allows you to create an icon pack with nested packs.

Usage:

./valkyrie iconpack [<options>]

Demo:

cli_iconpack.mp4

svgxml2imagevector command

A part of the CLI tool that allows you to convert SVG/XML files to ImageVector.

Usage:

./valkyrie svgxml2imagevector [<options>]

Demo:

cli_svgxml2imagevector.mp4

changelog command

Additional command to display embedded CLI changelog

Usage:

./valkyrie changelog

Output example:

Build CLI

Run ./gradlew buildCLI to build minified version of CLI tool. Artifact will be available in tools/cli/build/distributions/valkyrie-cli-*.**.*-SNAPSHOT.zip.

🐘Gradle plugin

The Gradle plugin automates the conversion of SVG/XML files to Compose ImageVector format during the build process. It's ideal for projects that need to version control icon sources and generate type-safe Kotlin code automatically.

Common scenarios

  • Team collaboration: Keep SVG/XML sources in version control and let the build system generate Kotlin code for everyone
  • CI/CD pipelines: Ensure icons are always generated consistently across different environments
  • Design system integration: Automatically sync icon updates from design tools without manual conversion
  • Large icon libraries: Efficiently manage hundreds or thousands of icons with minimal manual intervention

Plugin configuration

1. Apply the plugin

Gradle Plugin Portal

Define in your libs.versions.toml:

[plugins]
valkyrie = "io.github.composegears.valkyrie:latest-version"

Add the plugin to your build.gradle.kts:

plugins {
  alias(libs.plugins.valkyrie)
}

2. Configure the plugin

Full gradle plugin API specification (see practical examples below):

valkyrie {
  // Required: Package name for generated icons
  // Defaults to Android 'namespace' if Android Gradle Plugin is applied
  packageName = "com.example.app.icons"

  // Optional: Custom output directory (default: build/generated/sources/valkyrie)
  outputDirectory = layout.buildDirectory.dir("generated/valkyrie")

  // Optional: Resource directory name containing icon files (default: "valkyrieResources")
  // Icons will be discovered in src/{sourceSet}/{resourceDirectoryName}/
  // Example: src/commonMain/valkyrieResources/, src/androidMain/valkyrieResources/
  resourceDirectoryName = "valkyrieResources"

  // Optional: Generate during IDE sync for better developer experience (default: false)
  generateAtSync = false

  // Optional: Force all generated ImageVectors to have a specific autoMirror value (default: not specified)
  // When set to true, all icons will have autoMirror = true
  // When set to false, all icons will have autoMirror = false
  // When not specified, the autoMirror value from the original icon file will be preserved
  // This can be overridden at the icon pack or nested pack level
  autoMirror = false

  // Optional: Code style configuration for generated code
  codeStyle {
    // Add explicit `public` modifier to generated declarations (default: false)
    useExplicitMode = false

    // Number of spaces used for each level of indentation in generated code (default: 4)
    indentSize = 4
  }

  // Optional: ImageVector generation configuration
  imageVector {
    // Output format for generated ImageVectors (default: BackingProperty)
    outputFormat = OutputFormat.BackingProperty // or OutputFormat.LazyProperty

    // Use predefined Compose colors instead of hex color codes (e.g. Color.Black instead of Color(0xFF000000)) (default: true)
    useComposeColors = true

    // Generate `@Preview` function for ImageVector (default: false)
    generatePreview = false

    // Specifies the type of Preview annotation to generate for @Preview
    previewAnnotationType = PreviewAnnotationType.AndroidX

    // Insert a trailing comma after the last element of ImageVector.Builder block and path params (default: false)
    addTrailingComma = false
  }

  // Optional icon pack object configuration
  iconPack {
    // Required: Name of the root icon pack object
    name = "ValkyrieIcons"

    // Required: Target source set for generated icon pack object
    targetSourceSet = "commonMain"

    // Optional: Generate flat package structure without subfolders (default: false)
    useFlatPackage = false

    // Optional: Force all ImageVectors in this icon pack to have a specific autoMirror value (default: not specified)
    // When set, overrides the root level autoMirror setting
    // This can be overridden at the nested pack level
    autoMirror = true

    // Optional: Nested icon packs configuration
    nested {
      // Required: Name of the nested icon pack object
      name = "Outlined"

      // Required: The source folder path containing icons for this nested pack, relative to the `resourceDirectoryName`.
      sourceFolder = "outlined"

      // Optional: Force all ImageVectors in this nested pack to have a specific autoMirror value (default: not specified)
      // When set, overrides the icon pack level and root level autoMirror settings
      autoMirror = false
    }
    // You can add more nested packs if necessary
  }
}

3. Organize your icons

Place your icon files in the resources directory:

src/
└── commonMain/
    └── valkyrieResources/
        ├── add.svg
        ├── delete.svg
        └── ic_home.xml

The plugin automatically discovers icons from src/{sourceSet}/valkyrieResources/ in all source sets.

4. Run generation

Run the Gradle task to generate ImageVector sources:

./gradlew generateValkyrieImageVector

Gradle plugin samples

Basic conversion

Simply convert SVG/XML files to ImageVector in the specified package. For this example, we will use a multiplatform project structure.

plugins {
  kotlin("multiplatform")
  alias(libs.plugins.valkyrie)
}

valkyrie {
  packageName = "com.example.app.icons"
}

// other code

Place icons in the valkyrieResources directory:

src/
└── commonMain/
    └── valkyrieResources/
        ├── ic_brush.xml
        ├── ic_compose_color.xml
        ├── ic_linear_gradient.svg
        ├── ic_several_path.xml
        └── ic_transparent_fill_color.xml

Run the Gradle task:

./gradlew generateValkyrieImageVector

Observe generated code in build directory:

Use icon from your Compose code:

@Composable
fun Demo() {
  Image(
    imageVector = ComposeColor,
    contentDescription = "Color"
  )
}

Icon pack configuration

For better organization and type-safe access to your icons, you can create an icon pack. For this example, we will use a multiplatform project structure.

plugins {
  kotlin("multiplatform")
  alias(libs.plugins.valkyrie)
}

valkyrie {
  packageName = "com.example.app.icons"

  iconPack {
    name = "ValkyrieIcons"
    targetSourceSet = "commonMain" // icon pack object will be generated in commonMain source set
  }
}

Place icons in the valkyrieResources directory:

src/
└── commonMain/
    └── valkyrieResources/
        ├── ic_brush.xml
        ├── ic_compose_color.xml
        ├── ic_linear_gradient.svg
        ├── ic_several_path.xml
        └── ic_transparent_fill_color.xml

Run the Gradle task:

./gradlew generateValkyrieImageVector

Observe generated code in build directory with icon pack object and icons:

Use icon from your Compose code:

@Composable
fun Demo() {
  Image(
    imageVector = ValkyrieIcons.LinearGradient,
    contentDescription = null
  )
}

Icon pack with nested packs configuration

For larger projects with multiple icon sets, you can organize icons into a structured icon pack with nested packs. This approach provides better organization and type-safe access to your icons.

For this example, we will use a multiplatform project structure.

plugins {
  kotlin("multiplatform")
  alias(libs.plugins.valkyrie)
}

valkyrie {
  packageName = "com.example.app.icons"

  iconPack {
    name = "ValkyrieIcons"
    targetSourceSet = "commonMain"

    nested {
      name = "Outlined"
      sourceFolder = "outlined"
    }

    nested {
      name = "Filled"
      sourceFolder = "filled"
    }
  }
}

Organize your icons in nested folders:

src/
└── commonMain/
    └── valkyrieResources/
        ├── outlined/
        │   ├── add.svg
        │   ├── delete.svg
        │   └── settings.svg
        └── filled/
            ├── home.svg
            ├── user.svg
            └── search.svg

Run the Gradle task:

./gradlew generateValkyrieImageVector

Observe generated code in build directory with icon pack object and icons:

Use icons from your Compose code:

@Composable
fun Demo() {
  Image(
    imageVector = ValkyrieIcons.Outlined.Add,
    contentDescription = "Add"
  )

  Image(
    imageVector = ValkyrieIcons.Filled.Home,
    contentDescription = "Home"
  )
}

Tips and tricks

Copy generated icons into source folder instead of build folder

To keep generated icons in your source folder (e.g. for version control), you can create custom Gradle tasks to sync the generated files after the generation task.

val copyTask = tasks.register<Copy>("copyValkyrieIcons") {
  dependsOn(tasks.named("generateValkyrieImageVector"))

  from(layout.buildDirectory.dir("generated/sources/valkyrie"))
  into(layout.projectDirectory.dir("src"))
}

val cleanTask = tasks.register<Delete>("cleanValkyrieGenerated") {
  delete(layout.buildDirectory.dir("generated/sources/valkyrie"))
}

tasks.register("updateValkyrieIcons") {
  dependsOn(copyTask)
  finalizedBy(cleanTask)
}

AutoMirror configuration

The autoMirror parameter controls whether icons should automatically flip horizontally when used in right-to-left ( RTL) layouts. This is particularly useful for directional icons like arrows, chevrons, or navigation elements.

Configuration hierarchy:

The plugin supports a three-level hierarchy for autoMirror configuration:

  1. Root level - applies to all icons across the project
  2. Icon pack level - overrides root level for all icons in the pack
  3. Nested pack level - overrides both icon pack and root level for icons in the nested pack

For example, to force enable RTL support for icons in the Navigation nested pack:

valkyrie {
  packageName = "com.example.app.icons"

  iconPack {
    name = "ValkyrieIcons"
    targetSourceSet = "commonMain"

    nested {
      name = "Navigation"
      sourceFolder = "navigation"

      autoMirror = true
    }

    nested {
      name = "Logos"
      sourceFolder = "logos"
    }
  }
}

In this example:

  • Icons in the Navigation nested pack will have autoMirror = true
  • Icons in the Logos nested pack (and any other nested pack without an explicit autoMirror setting) will preserve the original autoMirror value from the source icon file

Other

Export formats

Original Discussion

Backing property Lazy property
package io.github.composegears.valkyrie.backing.outlined

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
import io.github.composegears.valkyrie.backing.BackingIcons

val BackingIcons.Outlined.Add: ImageVector
get() {
  if (_Add != null) {
    return _Add!!
  }
  _Add = ImageVector.Builder(
    name = "Outlined.Add",
    defaultWidth = 24.dp,
    defaultHeight = 24.dp,
    viewportWidth = 24f,
    viewportHeight = 24f,
  ).apply {
    path(fill = SolidColor(Color(0xFF232F34))) {
      moveTo(19f, 13f)
      lineTo(13f, 13f)
      lineTo(13f, 19f)
      lineTo(11f, 19f)
      lineTo(11f, 13f)
      lineTo(5f, 13f)
      lineTo(5f, 11f)
      lineTo(11f, 11f)
      lineTo(11f, 5f)
      lineTo(13f, 5f)
      lineTo(13f, 11f)
      lineTo(19f, 11f)
      lineTo(19f, 13f)
      close()
    }
  }.build()

  return _Add!!
}

@Suppress("ObjectPropertyName")
private var _Add: ImageVector? = null
package io.github.composegears.valkyrie.lazy.outlined

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp
import io.github.composegears.valkyrie.lazy.LazyIcons

val LazyIcons.Outlined.Add: ImageVector by lazy(LazyThreadSafetyMode.NONE) {
  ImageVector.Builder(
    name = "Outlined.Add",
    defaultWidth = 24.dp,
    defaultHeight = 24.dp,
    viewportWidth = 24f,
    viewportHeight = 24f,
  ).apply {
    path(fill = SolidColor(Color(0xFF232F34))) {
      moveTo(19f, 13f)
      lineTo(13f, 13f)
      lineTo(13f, 19f)
      lineTo(11f, 19f)
      lineTo(11f, 13f)
      lineTo(5f, 13f)
      lineTo(5f, 11f)
      lineTo(11f, 11f)
      lineTo(11f, 5f)
      lineTo(13f, 5f)
      lineTo(13f, 11f)
      lineTo(19f, 11f)
      lineTo(19f, 13f)
      close()
    }
  }.build()
}

Comparison with other solutions

Source SVG icon:

<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e8eaed">
  <path d="M0 0h24v24H0V0z" fill="none"/>
  <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>

ImageVector output:

Valkyrie composables.com
package io.github.composegears.valkyrie

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp

val Add: ImageVector
get() {
  if (_Add != null) {
    return _Add!!
  }
  _Add = ImageVector.Builder(
    name = "Add",
    defaultWidth = 24.dp,
    defaultHeight = 24.dp,
    viewportWidth = 24f,
    viewportHeight = 24f
  ).apply {
    path(fill = SolidColor(Color(0xFFE8EAED))) {
      moveTo(19f, 13f)
      horizontalLineToRelative(-6f)
      verticalLineToRelative(6f)
      horizontalLineToRelative(-2f)
      verticalLineToRelative(-6f)
      horizontalLineTo(5f)
      verticalLineToRelative(-2f)
      horizontalLineToRelative(6f)
      verticalLineTo(5f)
      horizontalLineToRelative(2f)
      verticalLineToRelative(6f)
      horizontalLineToRelative(6f)
      verticalLineToRelative(2f)
      close()
    }
  }.build()

  return _Add!!
}

@Suppress("ObjectPropertyName")
private var _Add: ImageVector? = null
import androidx.compose.runtime.Composable
import androidx.compose.foundation.Image
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp

private var _Add: ImageVector? = null

public val Add: ImageVector
get() {
  if (_Add != null) {
    return _Add!!
  }
  _Add = ImageVector.Builder(
    name = "Add",
    defaultWidth = 24.dp,
    defaultHeight = 24.dp,
    viewportWidth = 24f,
    viewportHeight = 24f
  ).apply {
    path(
      fill = null,
      fillAlpha = 1.0f,
      stroke = null,
      strokeAlpha = 1.0f,
      strokeLineWidth = 1.0f,
      strokeLineCap = StrokeCap.Butt,
      strokeLineJoin = StrokeJoin.Miter,
      strokeLineMiter = 1.0f,
      pathFillType = PathFillType.NonZero
    ) {
      moveTo(0f, 0f)
      horizontalLineToRelative(24f)
      verticalLineToRelative(24f)
      horizontalLineTo(0f)
      verticalLineTo(0f)
      close()
    }
    path(
      fill = SolidColor(Color(0xFFE8EAED)),
      fillAlpha = 1.0f,
      stroke = null,
      strokeAlpha = 1.0f,
      strokeLineWidth = 1.0f,
      strokeLineCap = StrokeCap.Butt,
      strokeLineJoin = StrokeJoin.Miter,
      strokeLineMiter = 1.0f,
      pathFillType = PathFillType.NonZero
    ) {
      moveTo(19f, 13f)
      horizontalLineToRelative(-6f)
      verticalLineToRelative(6f)
      horizontalLineToRelative(-2f)
      verticalLineToRelative(-6f)
      horizontalLineTo(5f)
      verticalLineToRelative(-2f)
      horizontalLineToRelative(6f)
      verticalLineTo(5f)
      horizontalLineToRelative(2f)
      verticalLineToRelative(6f)
      horizontalLineToRelative(6f)
      verticalLineToRelative(2f)
      close()
    }
  }.build()
  return _Add!!
}

Migration guide

v0.13.0 -> v0.14.0

CLI options --iconpack-name and --nested-packs removed in favour of --iconpack

Single pack

❌ ./valkyrie --iconpack-name=ValkyrieIcons
✅ ./valkyrie --iconpack=ValkyrieIcons

Nested packs

❌ ./valkyrie --iconpack-name=ValkyrieIcons --nested-packs=Colored,Filled
✅ ./valkyrie --iconpack=ValkyrieIcons.Colored,ValkyrieIcons.Filled

Join our community

Slack

Contributors

Thank you for your contributions and support! ❤️

License

Developed by ComposeGears 2024

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

About

SVG/XML to Compose ImageVector: IntelliJ IDEA / Android Studio plugin + build-in previewer without compilation, CLI tool, Gradle plugin

Topics

Resources

License

Stars

Watchers

Forks

Languages