Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
trueddd committed Apr 28, 2024
0 parents commit 1c95113
Show file tree
Hide file tree
Showing 21 changed files with 1,071 additions and 0 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Release

on:
workflow_dispatch:
push:
branches:
- master

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 16
cache: 'gradle'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v2
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Build JAR
run: ./gradlew build
- name: Release
uses: softprops/action-gh-release@v2
with:
draft: true
files: ./app/build/libs/vektr.jar
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*.iml
.gradle
**/build/
!src/**/build/
local.properties
.idea
captures
111 changes: 111 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
## Vektr
Vektr is a tool for converting [Material icons](https://fonts.google.com/icons?icon.set=Material+Icons)
`.xml` files (Android Vector Drawable) into the code
in a way Material icons are delivered with Jetpack Compose / Compose Multiplatform.
This tool basically parses icon XML file, parses path data of icon and splits it into
separate path elements and finally generates .kt file containing path drawing instructions.

<details>
<summary>Example of generated file</summary>

```kotlin
package com.github.trueddd.ui.res.icons.map

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.materialPath
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp

@Suppress("UnusedReceiverParameter")
val Icons.Rounded.Map: ImageVector
get() {
if (savedMap != null) {
return savedMap!!
}
savedMap = ImageVector.Builder(
name = "Map",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
)
.materialPath {
moveTo(14.65f, 4.98f)
lineToRelative(-5.0f, -1.75f)
curveToRelative(-0.42f, -0.15f, -0.88f, -0.15f, -1.3f, -0.01f)
lineTo(4.36f, 4.56f)
curveTo(3.55f, 4.84f, 3.0f, 5.6f, 3.0f, 6.46f)
verticalLineToRelative(11.85f)
curveToRelative(0.0f, 1.41f, 1.41f, 2.37f, 2.72f, 1.86f)
lineToRelative(2.93f, -1.14f)
curveToRelative(0.22f, -0.09f, 0.47f, -0.09f, 0.69f, -0.01f)
lineToRelative(5.0f, 1.75f)
curveToRelative(0.42f, 0.15f, 0.88f, 0.15f, 1.3f, 0.01f)
lineToRelative(3.99f, -1.34f)
curveToRelative(0.81f, -0.27f, 1.36f, -1.04f, 1.36f, -1.9f)
verticalLineTo(5.69f)
curveToRelative(0.0f, -1.41f, -1.41f, -2.37f, -2.72f, -1.86f)
lineToRelative(-2.93f, 1.14f)
curveToRelative(-0.22f, 0.08f, -0.46f, 0.09f, -0.69f, 0.01f)
close()
moveTo(15.0f, 18.89f)
lineToRelative(-6.0f, -2.11f)
verticalLineTo(5.11f)
lineToRelative(6.0f, 2.11f)
verticalLineToRelative(11.67f)
close()
}
.build()
return savedMap!!
}

private var savedMap: ImageVector? = null
```
</details>

There is a lot of Material icons that are not included into `androidx.compose.material.icons` package,
and in order to use them you have to either import them as resource or parse path data in runtime.
Both these approaches are valid, but I wanted new icons easy to modify and compile-time checked.

### CLI tool usage
Run downloaded or locally built jar file on the terminal:
```shell
java -jar vektr.jar <arguments>
```
Here is the list of available arguments:

| Name | Value | Required | Example | Description |
|-----------------------|----------------|----------|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| Input file | `in` | true | `--in=./search_24px.xml` | Defines the source XML file |
| Output directory | `out` | true | `--out=./dir/` | Defines the directory where generated file will be placed |
| Name of icon | `n`, `name` | false | `--name=Search` | The icon name, if not specified name of input file will be used |
| Output file name | `f`, `file` | false | `--file=RoundedSearch` | Name of file where generated code will be saved. If not specified, name of icon will be used |
| Package name | `p`, `package` | false | `--package=com.github.trueddd` | Package name for the generated file |
| Icon style | `s`, `style` | false | `--style=Outlined` | Icon style that will be used as Icon receiver type. Must be one of these: `Outlined`, `Rounded`, `TwoTone`, `Sharp`, `Filled`. |
| Auto mirror | `a` | false | `-a` (flag) | Generate this icon as auto mirrored |
| Use path data builder | `pd` | false | `-pd` (flag) | Use path data builder instead of materialPath function (not recommended) |

For example, sample code hidden in spoiler was generated with following command:
```shell
java -jar vektr.jar --name=Map --file=Rounded --package=com.github.trueddd.ui.res.icons.map --style=Rounded --in=./rounded_map_24px.xml --out=./
```

### Build locally
Run following command on terminal to build jar.
```shell
gradlew build
```
Optionally, you can create `local.properties` file in root folder and set path
where you want jar to be placed after build:
```properties
target_path=<path>
```

### To-do
Here is planned tasks for this project,
but do not hesitate to offer some changes or features via issues.
- [ ] Move code composing into common module
- [ ] Add web app for online file conversions
- [ ] Reduce jar size
- [ ] Use downloaded zip archives as conversion source (currently only XML files are supported)
- [ ] Use Material icon URL as conversion source
54 changes: 54 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import java.util.*

plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.shadowJar)
alias(libs.plugins.jetbrainsCompose)
application
java
}

kotlin {
jvm {
withJava()
}
jvmToolchain(16)

sourceSets {
val jvmMain by getting

commonMain.dependencies {
implementation(compose.material)
implementation(compose.ui)
}
jvmMain.dependencies {
implementation(libs.poet)
implementation(libs.jackson.kotlin)
implementation(libs.jackson.xml)
implementation(libs.clikt)
}
}
}

application {
mainClass.set("com.github.trueddd.MainKt")
}

tasks.shadowJar {
archiveFileName.set("vektr.jar")
minimize {
exclude("META-INF/*.DSA")
exclude("META-INF/*.SF")
exclude(dependency("com.fasterxml.woodstox:.*:.*"))
exclude(dependency("org.jetbrains.kotlin:kotlin-reflect:.*"))
}
doLast {
val targetPath = Properties().apply {
load(project.rootProject.file("local.properties").inputStream())
}.getProperty("target_path") ?: return@doLast
copy {
from(archiveFile)
into(targetPath)
}
}
}
9 changes: 9 additions & 0 deletions app/src/commonMain/kotlin/com/github/trueddd/IconStyle.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.github.trueddd

enum class IconStyle {
Rounded,
Outlined,
Filled,
TwoTone,
Sharp,
}
176 changes: 176 additions & 0 deletions app/src/commonMain/kotlin/com/github/trueddd/PathNodeMapper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package com.github.trueddd

import androidx.compose.ui.graphics.vector.PathBuilder
import androidx.compose.ui.graphics.vector.PathNode
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.MemberName.Companion.member
import com.squareup.kotlinpoet.asClassName

internal fun PathNode.asMethodCall(): CodeBlock.Builder {
val className = PathBuilder::class.asClassName()
val representation = when (this) {
is PathNode.MoveTo -> CodeBlock.of(
"%L(%Lf, %Lf)",
className.member("moveTo").simpleName, x, y
)
is PathNode.LineTo -> CodeBlock.of(
"%L(%Lf, %Lf)",
className.member("lineTo").simpleName, x, y
)
is PathNode.HorizontalTo -> CodeBlock.of(
"%L(%Lf)",
className.member("horizontalLineTo").simpleName, x
)
is PathNode.VerticalTo -> CodeBlock.of(
"%L(%Lf)",
className.member("verticalLineTo").simpleName, y
)
is PathNode.QuadTo -> CodeBlock.of(
"%L(%Lf, %Lf, %Lf, %Lf)",
className.member("quadTo").simpleName, x1, y1, x2, y2
)
is PathNode.CurveTo -> CodeBlock.of(
"%L(%Lf, %Lf, %Lf, %Lf, %Lf, %Lf)",
className.member("curveTo").simpleName, x1, y1, x2, y2, x3, y3
)
is PathNode.ArcTo -> CodeBlock.of(
"%L(%Lf, %Lf, %Lf, %L, %L, %Lf, %Lf)",
className.member("arcTo").simpleName, horizontalEllipseRadius, verticalEllipseRadius, theta,
isMoreThanHalf, isPositiveArc, arcStartX , arcStartY
)
is PathNode.Close -> CodeBlock.of(
"%L()",
className.member("close").simpleName
)
is PathNode.ReflectiveCurveTo -> CodeBlock.of(
"%L(%Lf, %Lf, %Lf, %Lf)",
className.member("reflectiveCurveTo").simpleName, x1, y1, x2, y2
)
is PathNode.ReflectiveQuadTo -> CodeBlock.of(
"%L(%Lf, %Lf)",
className.member("reflectiveQuadTo").simpleName, x, y
)
is PathNode.RelativeArcTo -> CodeBlock.of(
"%L(%Lf, %Lf, %Lf, %L, %L, %Lf, %Lf)",
className.member("arcToRelative").simpleName, horizontalEllipseRadius, verticalEllipseRadius, theta,
isMoreThanHalf, isPositiveArc, arcStartDx, arcStartDy
)
is PathNode.RelativeCurveTo -> CodeBlock.of(
"%L(%Lf, %Lf, %Lf, %Lf, %Lf, %Lf)",
className.member("curveToRelative").simpleName, dx1, dy1, dx2, dy2, dx3, dy3
)
is PathNode.RelativeHorizontalTo -> CodeBlock.of(
"%L(%Lf)",
className.member("horizontalLineToRelative").simpleName, dx
)
is PathNode.RelativeLineTo -> CodeBlock.of(
"%L(%Lf, %Lf)",
className.member("lineToRelative").simpleName, dx, dy
)
is PathNode.RelativeMoveTo -> CodeBlock.of(
"%L(%Lf, %Lf)",
className.member("moveToRelative").simpleName, dx, dy
)
is PathNode.RelativeQuadTo -> CodeBlock.of(
"%L(%Lf, %Lf, %Lf, %Lf)",
className.member("quadToRelative").simpleName, dx1, dy1, dx2, dy2
)
is PathNode.RelativeReflectiveCurveTo -> CodeBlock.of(
"%L(%Lf, %Lf, %Lf, %Lf)",
className.member("reflectiveCurveToRelative").simpleName, dx1, dy1, dx2, dy2
)
is PathNode.RelativeReflectiveQuadTo -> CodeBlock.of(
"%L(%Lf, %Lf)",
className.member("reflectiveQuadToRelative").simpleName, dx, dy
)
is PathNode.RelativeVerticalTo -> CodeBlock.of(
"%L(%Lf)",
className.member("verticalLineToRelative").simpleName, dy
)
}
return CodeBlock.builder().add(representation)
}

internal fun PathNode.asClassCall(): CodeBlock.Builder {
val representation = when (this) {
is PathNode.MoveTo -> CodeBlock.of(
"%T(%Lf, %Lf)",
PathNode.MoveTo::class, x, y
)
is PathNode.LineTo -> CodeBlock.of(
"%T(%Lf, %Lf)",
PathNode.LineTo::class, x, y
)
is PathNode.HorizontalTo -> CodeBlock.of(
"%T(%Lf)",
PathNode.HorizontalTo::class, x
)
is PathNode.VerticalTo -> CodeBlock.of(
"%T(%Lf)",
PathNode.VerticalTo::class, y
)
is PathNode.QuadTo -> CodeBlock.of(
"%T(%Lf, %Lf, %Lf, %Lf)",
PathNode.QuadTo::class, x1, y1, x2, y2
)
is PathNode.CurveTo -> CodeBlock.of(
"%T(%Lf, %Lf, %Lf, %Lf, %Lf, %Lf)",
PathNode.CurveTo::class, x1, y1, x2, y2, x3, y3
)
is PathNode.ArcTo -> CodeBlock.of(
"%T(%Lf, %Lf, %Lf, %L, %L, %Lf, %Lf)",
PathNode.ArcTo::class, horizontalEllipseRadius, verticalEllipseRadius, theta,
isMoreThanHalf, isPositiveArc, arcStartX , arcStartY
)
is PathNode.Close -> CodeBlock.of(
"%T",
PathNode.Close::class
)
is PathNode.ReflectiveCurveTo -> CodeBlock.of(
"%T(%Lf, %Lf, %Lf, %Lf)",
PathNode.ReflectiveCurveTo::class, x1, y1, x2, y2
)
is PathNode.ReflectiveQuadTo -> CodeBlock.of(
"%T(%Lf, %Lf)",
PathNode.ReflectiveQuadTo::class, x, y
)
is PathNode.RelativeArcTo -> CodeBlock.of(
"%T(%Lf, %Lf, %Lf, %L, %L, %Lf, %Lf)",
PathNode.RelativeArcTo::class, horizontalEllipseRadius, verticalEllipseRadius, theta,
isMoreThanHalf, isPositiveArc, arcStartDx, arcStartDy
)
is PathNode.RelativeCurveTo -> CodeBlock.of(
"%T(%Lf, %Lf, %Lf, %Lf, %Lf, %Lf)",
PathNode.RelativeCurveTo::class, dx1, dy1, dx2, dy2, dx3, dy3
)
is PathNode.RelativeHorizontalTo -> CodeBlock.of(
"%T(%Lf)",
PathNode.RelativeHorizontalTo::class, dx
)
is PathNode.RelativeLineTo -> CodeBlock.of(
"%T(%Lf, %Lf)",
PathNode.RelativeLineTo::class, dx, dy
)
is PathNode.RelativeMoveTo -> CodeBlock.of(
"%T(%Lf, %Lf)",
PathNode.RelativeMoveTo::class, dx, dy
)
is PathNode.RelativeQuadTo -> CodeBlock.of(
"%T(%Lf, %Lf, %Lf, %Lf)",
PathNode.RelativeQuadTo::class, dx1, dy1, dx2, dy2
)
is PathNode.RelativeReflectiveCurveTo -> CodeBlock.of(
"%T(%Lf, %Lf, %Lf, %Lf)",
PathNode.RelativeReflectiveCurveTo::class, dx1, dy1, dx2, dy2
)
is PathNode.RelativeReflectiveQuadTo -> CodeBlock.of(
"%T(%Lf, %Lf)",
PathNode.RelativeReflectiveQuadTo::class, dx, dy
)
is PathNode.RelativeVerticalTo -> CodeBlock.of(
"%T(%Lf)",
PathNode.RelativeVerticalTo::class, dy
)
}
return CodeBlock.builder().add(representation)
}
Loading

0 comments on commit 1c95113

Please sign in to comment.