Skip to content

irgaly/compose-vector-plugin

Repository files navigation

Gradle Compose Vector Plugin

Gradle Plugin for Converting SVG file to Compose ImageVector.

This plugin supports:

  • Kotlin Multiplatform Project (KMP).
  • Android Project.
  • Gradle Incremental Build for converting SVG to ImageVector.

Usage

Apply this plugin to your KMP project or Android Project.

app/build.gradle.kts

plugins {
    // For example, Android Application Project
    id("org.jetbrains.kotlin.android")
    id("com.android.application")
    // or Android Library Project
    //id("com.android.library")
    // or KMP Project
    //id("org.jetbrains.kotlin.multiplatform")

    // Apply Compose Vector Plugin
    id("io.github.irgaly.compose-vector") version "1.0.1"
}
...

Configure plugin with composeVector extension.

app/build.gradle.kts

composeVector {
    // This is a required configuration.
    // The destination package that ImageVector Images will place to.
    packageName = "io.github.irgaly.compose.vector.sample.image"

    // This is an optional configuration.
    // The directory that SVG files are placed.
    // Default value is "{project directory}/images"
    inputDir = layout.projectDirectory.dir("images")
}

Then put your SVG files to inputDir.

{project directory}/images directory is default location.

SVG images in {project directory}/images

Run generateImageVector task for generating ImageVector classes.

Or KotlinCompile Task will trigger generateImageVector task by tasks dependency.

# run generateImageVector
% ./gradlew :app:generateImageVector

# or compile task
% ./gradlew :app:compileDebugKotlin
...
> Task :app:generateImageVector UP-TO-DATE
...
> Task :app:compileDebugKotlin
...

The ImageVector classes will be placed to under build/compose-vector directory by default.

ImageVector classes in {build directory}

The outputDir under build directory is registered to SourceSet by default, so generated ImageVector classes can be used from your project.

...
import io.github.irgaly.compose.vector.sample.image.Icons
import io.github.irgaly.compose.vector.sample.image.icons.automirrored.filled.Undo
import io.github.irgaly.compose.vector.sample.image.icons.filled.Undo
...
    MaterialTheme {
        Column(Modifier.fillMaxSize()) {
            Image(Icons.Filled.Undo, contentDescription = null)
            Image(Icons.AutoMirrored.Filled.Undo, contentDescription = null)
        }
    }

The generated ImageVector property will be something like this:

...
@Suppress("RedundantVisibilityModifier")
public val Icons.Filled.Undo: ImageVector
    get() {
        if (_undo != null) {
            return _undo!!
        }
        _undo = Builder("Undo", 24.dp, 24.dp, 960f, 960f).apply {
            group(translationY = 960f) {
                val fill0 = SolidColor(Color(0xFFE8EAED))
                val fillAlpha0 = 1f
                val strokeAlpha0 = 1f
                val strokeLineWidth0 = 1f
                val strokeLineCap0 = StrokeCap.Butt
                val strokeLineJoin0 = StrokeJoin.Miter
                val strokeLineMiter0 = 4f
                path(fill = fill0, fillAlpha = fillAlpha0, strokeAlpha = strokeAlpha0,
                        strokeLineWidth = strokeLineWidth0, strokeLineCap = strokeLineCap0,
                        strokeLineJoin = strokeLineJoin0, strokeLineMiter = strokeLineMiter0) {
                    moveTo(280f, -200f)
                    verticalLineToRelative(-80f)
                    horizontalLineToRelative(284f)
                    quadToRelative(63f, 0f, 109.5f, -40f)
                    reflectiveQuadTo(720f, -420f)
                    quadToRelative(0f, -60f, -46.5f, -100f)
                    reflectiveQuadTo(564f, -560f)
                    horizontalLineTo(312f)
                    lineToRelative(104f, 104f)
                    lineToRelative(-56f, 56f)
                    lineToRelative(-200f, -200f)
                    lineToRelative(200f, -200f)
                    lineToRelative(56f, 56f)
                    lineToRelative(-104f, 104f)
                    horizontalLineToRelative(252f)
                    quadToRelative(97f, 0f, 166.5f, 63f)
                    reflectiveQuadTo(800f, -420f)
                    quadToRelative(0f, 94f, -69.5f, 157f)
                    reflectiveQuadTo(564f, -200f)
                    horizontalLineTo(280f)
                    close()
                }
            }
        }.build()
        return _undo!!
    }

private var _undo: ImageVector? = null

@Preview
@Composable
private fun UndoPreview() {
    Image(Icons.Filled.Undo, null)
}

@Preview(showBackground = true)
@Composable
private fun UndoBackgroundPreview() {
    Image(Icons.Filled.Undo, null)
}

ImageVector properties structure

The input directories structure will be mapped to ImageVector properties structure.

For example:

images
└── icons
    ├── automirrored
    │   └── filled
    │       └── undo.svg
    └── filled
        └── undo.svg

This produces two ImageVector properties:

  • Icons.AutoMirrored.Filled.Undo: ImageVector
  • Icons.Filled.Undo: ImageVector

The first directory's name icons will be root Object Class name Icons. The other directories will be used as package names. SVG file names will be used as ImageVector property names.

Name Conversion

The package name is same as input directory names.

The receiver classes and the ImageVector property names will converted by drop Ascii Symbols, then converted from snake cases to camel cases. In special case, the automirrored package name will be AutoMirrored receiver class.

Name conversion example:

  • undo.svg -> Undo property
  • vector_image.svg -> VectorImage property
  • 0_image.svg -> Image property
  • _my_icon.svg -> MyIcon property
  • my_icon_.svg -> MyIcon property
  • my_icon_0.svg -> MyIcon0 property
  • 0_my_icon.svg -> _0MyIcon property
  • MyIcon.svg -> MyIcon property
  • MySVGIcon.svg -> MySVGIcon property

If you want to apply custom name conversion rule, please use composeVector extension's transformer options.

Support AutoMirrored ImageVector

The automirrored package name is a special name.

The ImageVector classes under automirrored package or sub packages will be exported with autoMirror = true.

public val Icons.AutoMirrored.Filled.Undo: ImageVector
    get() {
        if (_undo != null) {
            return _undo!!
        }
        _undo = Builder("Undo", 24.dp, 24.dp, 960f, 960f, autoMirror = true).apply {
            ...

Project type and SourceSets

If the output directory is under the project's build directory, The output directory will be registered to SourceSets.

Project type composeVector configuration registered SourceSets
KMP project multiplatformGenerationTarget = Common (Default) Common Main SourceSets
KMP + Android project multiplatformGenerationTarget = Android Android Main SourceSets
Android project - Android Main SourceSets

composeVector Extension options

build.gradle.kts

import io.github.irgaly.compose.vector.plugin.ComposeVectorExtension

...

composeVector {
    // ImageVector classes destination package name
    // 
    // Required
    packagenName = "your.package.name"

    // Vector files directory
    //
    // Optional
    // Default: {project directory}/images
    inputDir = layout.projectDirectory.dir("images")
    
    // Generated Kotlin Sources directory.
    // outputDir is registered to SourceSet when outputDir is inside of project's buildDirectory.
    //
    // Optional
    // Default: {build directory}/compose-vector/src/main/kotlin
    outputDir = layout.buildDirectory.dir("compose-vector/src/main/kotlin")
    
    // Custom preconverter logic to ImageVector property names and receiver class names.
    //
    // * args
    //   * File: source file or directory's File instance
    //   * String: source file or directory's name
    //
    // Optional
    preClassNameTransformer.set (org.gradle.api.Transformer { (file: File, name: String) ->
        // custom logic here
        "pre_transformed_class_name"
    })

    // Custom postconverter logic to ImageVector property names and receiver class names.
    //
    // * args
    //   * File: source file or directory's File instance
    //   * String: transformed name
    //
    // Optional
    postClassNameTransformer.set (org.gradle.api.Transformer { (file: File, name: String) ->
        // custom logic here
        "transformed_class_name"
    })

    // Custom converter logic to package names.
    //
    // * args
    //   * File: source directory's File instance
    //   * String: source directory's name
    //
    // Optional
    packageNameTransformer.set (org.gradle.api.Transformer { (file: File, name: String) ->
        // custom logic here
        "transformed_package_name"
    })
    
    // Target SourceSets that generated images belongs to for KMP project.
    // This option is affect to KMP Project, not to Android only Project.
    //
    // Optional
    // Default: ComposeVectorExtension.GenerationTarget.Common
    multiplatformGenerationTarget = ComposeVectorExtension.GenerationTarget.Common
    //multiplatformGenerationTarget = ComposeVectorExtension.GenerationTarget.Android

    // Generate androidx.compose.ui.tooling.preview.Preview functions for Android target or not
    //
    // Optional
    // Default: true
    generateAndroidPreview = true

    // Generate org.jetbrains.compose.ui.tooling.preview.Preview functions for KMP common target or not
    //
    // Optional
    // Default: false
    generateJetbrainsPreview = false

    // Generate androidx.compose.desktop.ui.tooling.preview.Preview functions for KMP common target or not
    //
    // Optional
    // Default: true
    generateDesktopPreview = true
}

Debug with logging

This plugin will be logging with --info gradle option.

% ./gradlew :app:generateImageVector --info
...
> Task :app:generateImageVector
Build cache key for task ':app:generateImageVector' is dc07551486b4a33c25fa9d1ef7b64905
Task ':app:generateImageVector' is not up-to-date because:
  Task.upToDateWhen is false.
The input changes require a full rebuild for incremental task ':app:generateImageVector'.
clean .../app/build/compose-vector/src/main/kotlin because of initial build or full rebuild for incremental task and there in under project build directory.
changed: Input file .../app/images/icons/automirrored/filled/undo.svg added for rebuild.
convert icons/automirrored/filled/undo.svg to icons/automirrored/filled/Undo.kt
changed: Input file .../app/images/icons/filled/undo.svg added for rebuild.
convert icons/filled/undo.svg to icons/filled/Undo.kt
write object file: Icons.kt
Stored cache entry for task ':app:generateImageVector' with cache key dc07551486b4a33c25fa9d1ef7b64905
...

Use SVG to ImageVector converter as Java Library

SVG to ImageVector converter logic is packaged as Java library, so it can be used from your java CLI or applications.

build.gradle.kts

plugins {
    id("org.jetbrains.kotlin.jvm")
}
...
dependencies {
    implementation("io.github.irgaly.compose.vector:compose-vector:1.0.1")
}

Then use SvgParser class and ImageVectorGenerator class.

val inputStream = ... // SVG content as InputStream from File or String etc...
val imageVector: io.github.irgaly.compose.vector.node.ImageVector = SvgParser(object : Logger {
    override fun debug(message: String) {
        println("debug: $message")
    }

    override fun info(message: String) {
        println("info: $message")
    }

    override fun warn(message: String, error: Exception?) {
        println("warn: $message | $error")
    }

    override fun error(message: String, error: Exception?) {
        println("error: $message | $error")
    }
}).parse(
    inputStream,
    name = "Icon"
)
val kotlinSource: String = ImageVectorGenerator().generate(
    imageVector = imageVector,
    destinationPackage = "io.github.irgaly.icons",
    receiverClasses = listOf("Icons", "AutoMirrored", "Filled"),
    extensionPackage = "io.github.irgaly.icons.automirrored.filled",
    hasAndroidPreview = true,
)
println(kotlinSource)

Supported SVG specifications

This plugin's converter is using Apache Batik SVG parser, and supports basics specifications of SVG 1.2 + CSS style tag.

Here is a supporting table.

SVG tag SVG attribute Supporting Status
(any) id, class
(any) style
(any) transform
(any) display
(any) visibility
(any) color
(any) fill, fill-opacity, fill-rule
(any) stroke, stroke-opacity, stroke-width, stroke-linecap, stroke-linejoin, stroke-miterlimit
(any) clip-path, clip-rule, clipPathUnits
svg viewBox, width, height
Nested SVG tag is supported.
symbol viewBox, x, y, width, height
g
path d
rect x, y, width, height, rx, ry
circle cx, cy, r
ellipse cx, cy, rx, ry
line x1, x2, y1, y2
polyline points
polygon points
clipPath
defs
linearGradient gradientUnits, spreadMethod, x1, x2, y1, y2
radialGradient gradientUnits, cx, cy, fr
stop offset, stop-color
use href, xlink:href
a a tag is treated as same as g tag. No clickable feature.
title This tag is just ignored
desc This tag is just ignored
metadata This tag is just ignored
view This tag is just ignored
script This tag is just ignored
cursor This tag is just ignored
animate Not supported because ImageVector doesn't have animation feature.
text Not supported because ImageVector can't draw texts.
image Not supported because ImageVector can't draw images.
filter Not supported.
mask Not supported.
switch Not supported.
foreignObject Not supported.

Color format style

CSS4 Named Colors and sRGB colors are supported for color format.

Color Format Style Supporting Status
CSS4 Named Colors
rgb(0 0 0), rgb(0% 0% 0%), rgb(0, 0, 0)
rgb(0 0 0 0), rgb(0 0 0 / 0), rgb(0% 0% 0% 0%), rgb(0, 0, 0, 0)
rgba(0 0 0 0), rgba(0 0 0 / 0), rgba(0%, 0%, 0%, 0%)
#RRGGBB, #RGB
#RRGGBBAA, #RGBA