Skip to content

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

License

Notifications You must be signed in to change notification settings

ComposeGears/Valkyrie

Repository files navigation

Valkyrie — SVG/XML to Compose ImageVector converter

Icon

Plugin Homepage Version Downloads

GitHub releases Downloads License

Motivation

On one of the projects, during the migration to Jetpack Compose, we needed to convert a large number of icons from XML ( Android drawable) and SVG (Figma design system) to ImageVector. The existing solutions didn't quite fit our needs due to their inconvenient workflow, numerous bugs, poor output code, and in some cases, even being paid (after 5 icons 😄).

Additionally, with the release of Compose 1.7.0, Google discontinued support for material icons.

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)

Currently supported:

  • IntelliJ IDEA / Android Studio plugin
  • CLI tool
  • Gradle plugin and Web app (🚧 coming soon 🚧)

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

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

Requirements

  • IntelliJ IDEA 2024.1 and later
  • Android Studio Koala and later

Important

K2 mode is available starting from IntelliJ IDEA

2024.2.1 (more details)

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 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

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

LATEST_CLI_RELEASE_URL=$(curl --silent "https://api.github.com/repos/ComposeGears/Valkyrie/releases/latest" \
  | 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 svgxml2imagevector -h

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

Build CLI

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

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!!
  }

Gradle commands

other available gradle commands:

  • run tests: ./gradlew test

  • check code style: ./gradlew spotlessCheck

  • apply formatting: ./gradlew spotlessApply

Join our community

Slack

Contributors

Thank you for your help! ❤️

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.