├── .github ├── FUNDING.yml ├── actions │ └── gradle-cache │ │ └── action.yml └── workflows │ ├── build-test.yml │ ├── deploy_mavencentral_release.yml │ ├── deploy_mavencentral_staging.yml │ └── deploy_pluginportal.yml ├── .gitignore ├── .idea └── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── docs ├── inputdir_images.png └── output_imagevectors.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── plugin ├── build.gradle.kts ├── core │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ └── kotlin │ │ │ └── io │ │ │ └── github │ │ │ └── irgaly │ │ │ └── compose │ │ │ ├── Logger.kt │ │ │ ├── icons │ │ │ └── xml │ │ │ │ ├── Icon.kt │ │ │ │ ├── IconParser.kt │ │ │ │ ├── IconProcessor.kt │ │ │ │ ├── IconTheme.kt │ │ │ │ ├── IconWriter.kt │ │ │ │ ├── ImageVectorGenerator.kt │ │ │ │ ├── KotlinPoetUtils.kt │ │ │ │ ├── Names.kt │ │ │ │ └── vector │ │ │ │ ├── FillType.kt │ │ │ │ ├── PathNode.kt │ │ │ │ ├── PathParser.kt │ │ │ │ └── Vector.kt │ │ │ └── vector │ │ │ ├── ImageVectorGenerator.kt │ │ │ ├── Names.kt │ │ │ ├── node │ │ │ └── ImageVector.kt │ │ │ └── svg │ │ │ └── SvgParser.kt │ │ └── test │ │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── irgaly │ │ │ └── compose │ │ │ └── vector │ │ │ └── ConverterSpec.kt │ │ └── resources │ │ ├── path.kt │ │ ├── path.svg │ │ ├── svg_nest_viewbox.kt │ │ ├── svg_nest_viewbox.svg │ │ ├── svg_no_size.kt │ │ ├── svg_no_size.svg │ │ ├── svg_no_size_no_viewbox.kt │ │ ├── svg_no_size_no_viewbox.svg │ │ ├── svg_no_viewbox.kt │ │ ├── svg_no_viewbox.svg │ │ ├── svg_over_viewbox.kt │ │ ├── svg_over_viewbox.svg │ │ ├── svg_symbol.kt │ │ ├── svg_symbol.svg │ │ ├── transform_circle.kt │ │ ├── transform_circle.svg │ │ ├── transform_path.kt │ │ └── transform_path.svg ├── gradle │ └── wrapper ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── github │ └── irgaly │ └── compose │ └── vector │ └── plugin │ ├── ComposeVectorExtension.kt │ ├── ComposeVectorPlugin.kt │ └── ComposeVectorTask.kt ├── renovate.json ├── sample ├── android-library │ ├── build.gradle.kts │ ├── images │ │ └── icons │ │ │ └── undo.svg │ └── src │ │ └── main │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── irgaly │ │ └── compose │ │ └── vector │ │ └── sample │ │ └── library │ │ └── Sample.kt ├── android │ ├── build.gradle.kts │ ├── images │ │ ├── icons │ │ │ ├── automirrored │ │ │ │ └── undo.svg │ │ │ └── undo.svg │ │ └── undo.svg │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ └── io │ │ │ │ └── github │ │ │ │ └── irgaly │ │ │ │ └── compose │ │ │ │ └── vector │ │ │ │ └── sample │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MyApplication.kt │ │ │ └── res │ │ │ └── values │ │ │ └── strings.xml │ └── xml │ │ ├── undo.xml │ │ └── undo_auto_mirrored.xml ├── jvm-library │ ├── build.gradle.kts │ └── src │ │ └── jvmMain │ │ └── kotlin │ │ └── io │ │ └── github │ │ └── irgaly │ │ └── compose │ │ └── vector │ │ └── sample │ │ └── Main.kt └── multiplatform │ ├── build.gradle.kts │ ├── images │ ├── icons │ │ ├── automirrored │ │ │ └── undo.svg │ │ └── undo.svg │ └── undo.svg │ └── src │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── github │ │ └── irgaly │ │ └── compose │ │ └── vector │ │ └── sample │ │ └── App.kt │ └── jvmMain │ └── kotlin │ └── io │ └── github │ └── irgaly │ └── compose │ └── vector │ └── sample │ └── Main.kt └── settings.gradle.kts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: irgaly 2 | custom: ["https://github.com/irgaly/irgaly"] 3 | -------------------------------------------------------------------------------- /.github/actions/gradle-cache/action.yml: -------------------------------------------------------------------------------- 1 | name: Gradle Cache 2 | description: Gradle Wrapper, Gradle Cache (1 month), Gradle Build Cache (1 month) 3 | runs: 4 | using: composite 5 | steps: 6 | - id: get-month 7 | shell: bash 8 | run: echo "month=$(TZ=Asia/Tokyo date +%m)" >> $GITHUB_OUTPUT 9 | - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 10 | with: 11 | path: ~/.gradle/wrapper 12 | key: gradle-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }} 13 | - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 14 | with: 15 | path: | 16 | ~/.gradle/caches/jars-* 17 | ~/.gradle/caches/transforms-* 18 | ~/.gradle/caches/modules-* 19 | key: gradle-dependencies-${{ steps.get-month.outputs.month }}-${{ hashFiles('gradle/libs.versions.toml', '**/*.gradle.kts', 'build-logic/**/*.{kt,kts}') }} 20 | restore-keys: gradle-dependencies-${{ steps.get-month.outputs.month }}- 21 | - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 22 | with: 23 | path: | 24 | ~/.konan 25 | ~/.gradle/native 26 | key: ${{ runner.os }}-kotlin-native-${{ steps.get-month.outputs.month }}-${{ hashFiles('gradle/libs.versions.toml', '**/*.gradle.kts') }} 27 | restore-keys: ${{ runner.os }}-kotlin-native-${{ steps.get-month.outputs.month }}- 28 | - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 29 | with: 30 | path: | 31 | ~/.gradle/caches/build-cache-* 32 | ~/.gradle/caches/[0-9]*.* 33 | .gradle 34 | key: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ steps.get-month.outputs.month }}-${{ github.sha }} 35 | restore-keys: ${{ runner.os }}-gradle-build-${{ github.workflow }}-${{ steps.get-month.outputs.month }}- 36 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | on: 3 | push: 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 10 | - uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 11 | with: 12 | distribution: temurin 13 | java-version: 17 14 | - uses: ./.github/actions/gradle-cache 15 | - name: Build 16 | run: | 17 | ./gradlew :plugin:core:testClasses 18 | - name: Test 19 | run: | 20 | ./gradlew :plugin:core:test 21 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 22 | if: always() 23 | with: 24 | name: test-results 25 | path: | 26 | **/build/reports/tests/test 27 | -------------------------------------------------------------------------------- /.github/workflows/deploy_mavencentral_release.yml: -------------------------------------------------------------------------------- 1 | # v*.*.* tag -> deploy to Maven Central 2 | name: Deploy to Maven Central Release 3 | 4 | on: 5 | push: 6 | tags: 7 | - v[0-9]+.[0-9]+.[0-9]+* 8 | 9 | jobs: 10 | deploy-mavencentral: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 14 | - uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 15 | with: 16 | distribution: temurin 17 | java-version: 17 18 | - uses: ./.github/actions/gradle-cache 19 | - name: Deploy to Maven Central 20 | env: 21 | ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME_TOKEN }} 22 | ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD_TOKEN }} 23 | SIGNING_PGP_KEY: ${{ secrets.SIGNING_PGP_KEY }} 24 | SIGNING_PGP_PASSWORD: ${{ secrets.SIGNING_PGP_PASSWORD }} 25 | run: | 26 | ./gradlew :plugin:core:publishToSonatype :plugin:closeAndReleaseSonatypeStagingRepository 27 | -------------------------------------------------------------------------------- /.github/workflows/deploy_mavencentral_staging.yml: -------------------------------------------------------------------------------- 1 | # Open PR -> deploy to Maven Central Staging 2 | name: Deploy to Maven Central Staging 3 | 4 | on: 5 | pull_request: 6 | types: [ opened, reopened, synchronize, ready_for_review ] 7 | 8 | jobs: 9 | deploy-mavencentral: 10 | runs-on: ubuntu-latest 11 | if: ${{ !github.event.pull_request.draft }} 12 | steps: 13 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 14 | - uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 15 | with: 16 | distribution: temurin 17 | java-version: 17 18 | - uses: ./.github/actions/gradle-cache 19 | - name: Set Staging version 20 | run: | 21 | sed -i -E "s/^composeVector = \"(.*)\"$/composeVector = \"\\1-pr${{ github.event.pull_request.number }}.${{ github.run_number }}.${{ github.run_attempt }}\"/g" gradle/libs.versions.toml 22 | grep "^composeVector" gradle/libs.versions.toml 23 | - name: Deploy to Maven Central 24 | env: 25 | ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME_TOKEN }} 26 | ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD_TOKEN }} 27 | SIGNING_PGP_KEY: ${{ secrets.SIGNING_PGP_KEY }} 28 | SIGNING_PGP_PASSWORD: ${{ secrets.SIGNING_PGP_PASSWORD }} 29 | run: | 30 | ./gradlew :plugin:core:publishToSonatype :plugin:closeSonatypeStagingRepository 31 | -------------------------------------------------------------------------------- /.github/workflows/deploy_pluginportal.yml: -------------------------------------------------------------------------------- 1 | # v*.*.* tag -> deploy to Gradle Plugin Portal 2 | name: Deploy to Gradle Plugin Portal 3 | 4 | on: 5 | push: 6 | tags: 7 | - v[0-9]+.[0-9]+.[0-9]+* 8 | 9 | jobs: 10 | deploy-pluginportal: 11 | runs-on: ubuntu-latest 12 | env: 13 | GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} 14 | GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} 15 | SIGNING_PGP_KEY: ${{ secrets.SIGNING_PGP_KEY }} 16 | SIGNING_PGP_PASSWORD: ${{ secrets.SIGNING_PGP_PASSWORD }} 17 | steps: 18 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 19 | - uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 20 | with: 21 | distribution: temurin 22 | java-version: 17 23 | - name: Deploy to Gradle Plugin Portal 24 | run: | 25 | ./gradlew :plugin:publishPlugins 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | !.idea/codeStyles 3 | !.idea/dictionaries 4 | !.idea/externalDependencies.xml 5 | 6 | # Kotlin 7 | .kotlin 8 | 9 | # Created by https://www.toptal.com/developers/gitignore/api/macos,android,windows,androidstudio,intellij 10 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,android,windows,androidstudio,intellij 11 | 12 | ### Android ### 13 | # Gradle files 14 | .gradle/ 15 | build/ 16 | 17 | # Local configuration file (sdk path, etc) 18 | local.properties 19 | 20 | # Log/OS Files 21 | *.log 22 | 23 | # Android Studio generated files and folders 24 | captures/ 25 | .externalNativeBuild/ 26 | .cxx/ 27 | *.apk 28 | output.json 29 | 30 | # IntelliJ 31 | *.iml 32 | #.idea/ 33 | 34 | # Keystore files 35 | *.jks 36 | *.keystore 37 | 38 | # Google Services (e.g. APIs or Firebase) 39 | google-services.json 40 | 41 | # Android Profiling 42 | *.hprof 43 | 44 | ### Android Patch ### 45 | gen-external-apklibs 46 | 47 | # Replacement of .externalNativeBuild directories introduced 48 | # with Android Studio 3.5. 49 | 50 | ### Intellij ### 51 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 52 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 53 | 54 | # User-specific stuff 55 | .idea/**/workspace.xml 56 | .idea/**/tasks.xml 57 | .idea/**/usage.statistics.xml 58 | .idea/**/dictionaries 59 | .idea/**/shelf 60 | 61 | # AWS User-specific 62 | .idea/**/aws.xml 63 | 64 | # Generated files 65 | .idea/**/contentModel.xml 66 | 67 | # Sensitive or high-churn files 68 | .idea/**/dataSources/ 69 | .idea/**/dataSources.ids 70 | .idea/**/dataSources.local.xml 71 | .idea/**/sqlDataSources.xml 72 | .idea/**/dynamic.xml 73 | .idea/**/uiDesigner.xml 74 | .idea/**/dbnavigator.xml 75 | 76 | # Gradle 77 | .idea/**/gradle.xml 78 | .idea/**/libraries 79 | 80 | # Gradle and Maven with auto-import 81 | # When using Gradle or Maven with auto-import, you should exclude module files, 82 | # since they will be recreated, and may cause churn. Uncomment if using 83 | # auto-import. 84 | .idea/artifacts 85 | .idea/compiler.xml 86 | .idea/jarRepositories.xml 87 | .idea/modules.xml 88 | .idea/*.iml 89 | .idea/modules 90 | *.iml 91 | *.ipr 92 | 93 | # CMake 94 | cmake-build-*/ 95 | 96 | # Mongo Explorer plugin 97 | .idea/**/mongoSettings.xml 98 | 99 | # File-based project format 100 | *.iws 101 | 102 | # IntelliJ 103 | out/ 104 | 105 | # mpeltonen/sbt-idea plugin 106 | .idea_modules/ 107 | 108 | # JIRA plugin 109 | atlassian-ide-plugin.xml 110 | 111 | # Cursive Clojure plugin 112 | .idea/replstate.xml 113 | 114 | # SonarLint plugin 115 | .idea/sonarlint/ 116 | 117 | # Crashlytics plugin (for Android Studio and IntelliJ) 118 | com_crashlytics_export_strings.xml 119 | crashlytics.properties 120 | crashlytics-build.properties 121 | fabric.properties 122 | 123 | # Editor-based Rest Client 124 | .idea/httpRequests 125 | 126 | # Android studio 3.1+ serialized cache file 127 | .idea/caches/build_file_checksums.ser 128 | 129 | ### Intellij Patch ### 130 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 131 | 132 | # *.iml 133 | # modules.xml 134 | # .idea/misc.xml 135 | # *.ipr 136 | 137 | # Sonarlint plugin 138 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 139 | .idea/**/sonarlint/ 140 | 141 | # SonarQube Plugin 142 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 143 | .idea/**/sonarIssues.xml 144 | 145 | # Markdown Navigator plugin 146 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 147 | .idea/**/markdown-navigator.xml 148 | .idea/**/markdown-navigator-enh.xml 149 | .idea/**/markdown-navigator/ 150 | 151 | # Cache file creation bug 152 | # See https://youtrack.jetbrains.com/issue/JBR-2257 153 | .idea/$CACHE_FILE$ 154 | 155 | # CodeStream plugin 156 | # https://plugins.jetbrains.com/plugin/12206-codestream 157 | .idea/codestream.xml 158 | 159 | ### macOS ### 160 | # General 161 | .DS_Store 162 | .AppleDouble 163 | .LSOverride 164 | 165 | # Icon must end with two \r 166 | Icon 167 | 168 | 169 | # Thumbnails 170 | ._* 171 | 172 | # Files that might appear in the root of a volume 173 | .DocumentRevisions-V100 174 | .fseventsd 175 | .Spotlight-V100 176 | .TemporaryItems 177 | .Trashes 178 | .VolumeIcon.icns 179 | .com.apple.timemachine.donotpresent 180 | 181 | # Directories potentially created on remote AFP share 182 | .AppleDB 183 | .AppleDesktop 184 | Network Trash Folder 185 | Temporary Items 186 | .apdisk 187 | 188 | ### Windows ### 189 | # Windows thumbnail cache files 190 | Thumbs.db 191 | Thumbs.db:encryptable 192 | ehthumbs.db 193 | ehthumbs_vista.db 194 | 195 | # Dump file 196 | *.stackdump 197 | 198 | # Folder config file 199 | [Dd]esktop.ini 200 | 201 | # Recycle Bin used on file shares 202 | $RECYCLE.BIN/ 203 | 204 | # Windows Installer files 205 | *.cab 206 | *.msi 207 | *.msix 208 | *.msm 209 | *.msp 210 | 211 | # Windows shortcuts 212 | *.lnk 213 | 214 | ### AndroidStudio ### 215 | # Covers files to be ignored for android development using Android Studio. 216 | 217 | # Built application files 218 | *.ap_ 219 | *.aab 220 | 221 | # Files for the ART/Dalvik VM 222 | *.dex 223 | 224 | # Java class files 225 | *.class 226 | 227 | # Generated files 228 | bin/ 229 | gen/ 230 | 231 | # Gradle files 232 | .gradle 233 | 234 | # Signing files 235 | .signing/ 236 | 237 | # Local configuration file (sdk path, etc) 238 | 239 | # Proguard folder generated by Eclipse 240 | proguard/ 241 | 242 | # Log Files 243 | 244 | # Android Studio 245 | /*/build/ 246 | /*/local.properties 247 | /*/out 248 | /*/*/build 249 | /*/*/production 250 | .navigation/ 251 | *.ipr 252 | *~ 253 | *.swp 254 | 255 | # Keystore files 256 | 257 | # Google Services (e.g. APIs or Firebase) 258 | # google-services.json 259 | 260 | # Android Patch 261 | 262 | # External native build folder generated in Android Studio 2.2 and later 263 | .externalNativeBuild 264 | 265 | # NDK 266 | obj/ 267 | 268 | # IntelliJ IDEA 269 | /out/ 270 | 271 | # User-specific configurations 272 | .idea/caches/ 273 | .idea/libraries/ 274 | .idea/shelf/ 275 | .idea/workspace.xml 276 | .idea/tasks.xml 277 | .idea/.name 278 | .idea/compiler.xml 279 | .idea/copyright/profiles_settings.xml 280 | .idea/encodings.xml 281 | .idea/misc.xml 282 | .idea/modules.xml 283 | .idea/scopes/scope_settings.xml 284 | .idea/dictionaries 285 | .idea/vcs.xml 286 | .idea/jsLibraryMappings.xml 287 | .idea/datasources.xml 288 | .idea/dataSources.ids 289 | .idea/sqlDataSources.xml 290 | .idea/dynamic.xml 291 | .idea/uiDesigner.xml 292 | .idea/assetWizardSettings.xml 293 | .idea/gradle.xml 294 | .idea/jarRepositories.xml 295 | .idea/navEditor.xml 296 | 297 | # OS-specific files 298 | .DS_Store? 299 | 300 | # Legacy Eclipse project files 301 | .classpath 302 | .project 303 | .cproject 304 | .settings/ 305 | 306 | # Mobile Tools for Java (J2ME) 307 | .mtj.tmp/ 308 | 309 | # Package Files # 310 | *.war 311 | *.ear 312 | 313 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 314 | hs_err_pid* 315 | 316 | ## Plugin-specific files: 317 | 318 | # mpeltonen/sbt-idea plugin 319 | 320 | # JIRA plugin 321 | 322 | # Mongo Explorer plugin 323 | .idea/mongoSettings.xml 324 | 325 | # Crashlytics plugin (for Android Studio and IntelliJ) 326 | 327 | ### AndroidStudio Patch ### 328 | 329 | !/gradle/wrapper/gradle-wrapper.jar 330 | 331 | # End of https://www.toptal.com/developers/gitignore/api/macos,android,windows,androidstudio,intellij 332 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 120 | 121 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.1.0 - 2025/07/15 JST 2 | 3 | #### Maintenance 4 | 5 | * Support Android KMP Library plugin [#55](https://github.com/irgaly/compose-vector-plugin/pull/55) 6 | 7 | # v1.0.1 - 2025/01/10 JST 8 | 9 | #### Fix 10 | 11 | * fix: incremental build: clean build directory only when full 12 | build [#32](https://github.com/irgaly/compose-vector-plugin/pull/18) 13 | 14 | # v1.0.0 - 2024/10/11 JST 15 | 16 | #### Fix 17 | 18 | * fix: Support Android Library module 19 | * Issue #17 - Fix Android library compatibility [#18](https://github.com/irgaly/compose-vector-plugin/pull/18) 20 | 21 | # v0.2.0 - 2024/09/07 JST 22 | 23 | #### Improvement 24 | 25 | * SVG default viewBox size, skip default value [#9](https://github.com/irgaly/compose-vector-plugin/pull/9) 26 | * SVG default size = 300 x 150 27 | * Skip default values: 28 | * fillAlpha = 1f, strokeAlpha = 1f, strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4 29 | 30 | #### Fix 31 | 32 | * fix: SVG tag viewBox clipping [#11](https://github.com/irgaly/compose-vector-plugin/pull/11) 33 | 34 | #### Test 35 | 36 | * Add Converting Tests [#10](https://github.com/irgaly/compose-vector-plugin/pull/10) 37 | 38 | # v0.1.0 - 2024/08/20 JST 39 | 40 | * Initial release. 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gradle Compose Vector Plugin 2 | 3 | Gradle Plugin for Converting SVG file to [Compose ImageVector](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/vector/ImageVector). 4 | 5 | This plugin supports: 6 | 7 | * Kotlin Multiplatform Project (KMP). 8 | * Android Project. 9 | * Gradle Incremental Build for converting SVG to ImageVector. 10 | 11 | ## Usage 12 | 13 | Apply this plugin to your KMP project or Android Project. 14 | 15 | `app/build.gradle.kts` 16 | 17 | ```kotlin 18 | plugins { 19 | // For example, Android Application Project 20 | id("org.jetbrains.kotlin.android") 21 | id("com.android.application") 22 | // or Android Library Project 23 | //id("com.android.library") 24 | //id("com.android.kotlin.multiplatform.library") 25 | // or KMP Project 26 | //id("org.jetbrains.kotlin.multiplatform") 27 | 28 | // Apply Compose Vector Plugin 29 | id("io.github.irgaly.compose-vector") version "1.1.0" 30 | } 31 | ... 32 | ``` 33 | 34 | Configure plugin with `composeVector` extension. 35 | 36 | `app/build.gradle.kts` 37 | 38 | ```kotlin 39 | composeVector { 40 | // This is a required configuration. 41 | // The destination package that ImageVector Images will place to. 42 | packageName = "io.github.irgaly.compose.vector.sample.image" 43 | 44 | // This is an optional configuration. 45 | // The directory that SVG files are placed. 46 | // Default value is "{project directory}/images" 47 | inputDir = layout.projectDirectory.dir("images") 48 | } 49 | ``` 50 | 51 | Then put your SVG files to `inputDir`. 52 | 53 | `{project directory}/images` directory is default location. 54 | 55 | ![SVG images in {project directory}/images](docs/inputdir_images.png) 56 | 57 | Run `generateImageVector` task for generating ImageVector classes. 58 | 59 | Or `KotlinCompile Task` will trigger generateImageVector task by tasks dependency. 60 | 61 | ```shell 62 | # run generateImageVector 63 | % ./gradlew :app:generateImageVector 64 | 65 | # or compile task 66 | % ./gradlew :app:compileDebugKotlin 67 | ... 68 | > Task :app:generateImageVector UP-TO-DATE 69 | ... 70 | > Task :app:compileDebugKotlin 71 | ... 72 | ``` 73 | 74 | The ImageVector classes will be placed to under `build/compose-vector` directory by default. 75 | 76 | ![ImageVector classes in {build directory}](docs/output_imagevectors.png) 77 | 78 | The outputDir under `build` directory is registered to SourceSet by default, 79 | so generated ImageVector classes can be used from your project. 80 | 81 | ```kotlin 82 | ... 83 | import io.github.irgaly.compose.vector.sample.image.Icons 84 | import io.github.irgaly.compose.vector.sample.image.icons.automirrored.filled.Undo 85 | import io.github.irgaly.compose.vector.sample.image.icons.filled.Undo 86 | ... 87 | MaterialTheme { 88 | Column(Modifier.fillMaxSize()) { 89 | Image(Icons.Filled.Undo, contentDescription = null) 90 | Image(Icons.AutoMirrored.Filled.Undo, contentDescription = null) 91 | } 92 | } 93 | ``` 94 | 95 | The generated ImageVector property will be something like this: 96 | 97 | ```kotlin 98 | ... 99 | @Suppress("RedundantVisibilityModifier") 100 | public val Icons.Filled.Undo: ImageVector 101 | get() { 102 | if (_undo != null) { 103 | return _undo!! 104 | } 105 | _undo = Builder("Undo", 24.dp, 24.dp, 960f, 960f).apply { 106 | group(translationY = 960f) { 107 | val fill0 = SolidColor(Color(0xFFE8EAED)) 108 | val fillAlpha0 = 1f 109 | val strokeAlpha0 = 1f 110 | val strokeLineWidth0 = 1f 111 | val strokeLineCap0 = StrokeCap.Butt 112 | val strokeLineJoin0 = StrokeJoin.Miter 113 | val strokeLineMiter0 = 4f 114 | path(fill = fill0, fillAlpha = fillAlpha0, strokeAlpha = strokeAlpha0, 115 | strokeLineWidth = strokeLineWidth0, strokeLineCap = strokeLineCap0, 116 | strokeLineJoin = strokeLineJoin0, strokeLineMiter = strokeLineMiter0) { 117 | moveTo(280f, -200f) 118 | verticalLineToRelative(-80f) 119 | horizontalLineToRelative(284f) 120 | quadToRelative(63f, 0f, 109.5f, -40f) 121 | reflectiveQuadTo(720f, -420f) 122 | quadToRelative(0f, -60f, -46.5f, -100f) 123 | reflectiveQuadTo(564f, -560f) 124 | horizontalLineTo(312f) 125 | lineToRelative(104f, 104f) 126 | lineToRelative(-56f, 56f) 127 | lineToRelative(-200f, -200f) 128 | lineToRelative(200f, -200f) 129 | lineToRelative(56f, 56f) 130 | lineToRelative(-104f, 104f) 131 | horizontalLineToRelative(252f) 132 | quadToRelative(97f, 0f, 166.5f, 63f) 133 | reflectiveQuadTo(800f, -420f) 134 | quadToRelative(0f, 94f, -69.5f, 157f) 135 | reflectiveQuadTo(564f, -200f) 136 | horizontalLineTo(280f) 137 | close() 138 | } 139 | } 140 | }.build() 141 | return _undo!! 142 | } 143 | 144 | private var _undo: ImageVector? = null 145 | 146 | @Preview 147 | @Composable 148 | private fun UndoPreview() { 149 | Image(Icons.Filled.Undo, null) 150 | } 151 | 152 | @Preview(showBackground = true) 153 | @Composable 154 | private fun UndoBackgroundPreview() { 155 | Image(Icons.Filled.Undo, null) 156 | } 157 | ``` 158 | 159 | ## ImageVector properties structure 160 | 161 | The input directories structure will be mapped to ImageVector properties structure. 162 | 163 | For example: 164 | 165 | ``` 166 | images 167 | └── icons 168 | ├── automirrored 169 | │ └── filled 170 | │ └── undo.svg 171 | └── filled 172 | └── undo.svg 173 | ``` 174 | 175 | This produces two ImageVector properties: 176 | 177 | * `Icons.AutoMirrored.Filled.Undo: ImageVector` 178 | * `Icons.Filled.Undo: ImageVector` 179 | 180 | The first directory's name `icons` will be root Object Class name `Icons`. 181 | The other directories will be used as package names. 182 | SVG file names will be used as ImageVector property names. 183 | 184 | ## Name Conversion 185 | 186 | The package name is same as input directory names. 187 | 188 | The receiver classes and the ImageVector property names will converted by drop Ascii Symbols, then converted from snake cases to camel cases. 189 | In special case, the `automirrored` package name will be `AutoMirrored` receiver class. 190 | 191 | Name conversion example: 192 | 193 | * undo.svg -> `Undo` property 194 | * vector_image.svg -> `VectorImage` property 195 | * 0_image.svg -> `Image` property 196 | * _my_icon.svg -> `MyIcon` property 197 | * my_icon_.svg -> `MyIcon` property 198 | * my_icon_0.svg -> `MyIcon0` property 199 | * 0_my_icon.svg -> `_0MyIcon` property 200 | * MyIcon.svg -> `MyIcon` property 201 | * MySVGIcon.svg -> `MySVGIcon` property 202 | 203 | If you want to apply custom name conversion rule, please use `composeVector` extension's transformer options. 204 | 205 | ## Support AutoMirrored ImageVector 206 | 207 | The `automirrored` package name is a special name. 208 | 209 | The ImageVector classes under `automirrored` package or sub packages will be exported with [autoMirror = true](https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/vector/ImageVector#autoMirror()). 210 | 211 | ```kotlin 212 | public val Icons.AutoMirrored.Filled.Undo: ImageVector 213 | get() { 214 | if (_undo != null) { 215 | return _undo!! 216 | } 217 | _undo = Builder("Undo", 24.dp, 24.dp, 960f, 960f, autoMirror = true).apply { 218 | ... 219 | ``` 220 | 221 | ## Project type and SourceSets 222 | 223 | If the output directory is under the project's `build` directory, The output directory will be registered to SourceSets. 224 | 225 | | Project type | composeVector configuration | registered SourceSets | 226 | |-----------------------|--------------------------------------------------|-------------------------| 227 | | KMP project | multiplatformGenerationTarget = Common (Default) | Common Main SourceSets | 228 | | KMP + Android project | multiplatformGenerationTarget = Android | Android Main SourceSets | 229 | | Android project | - | Android Main SourceSets | 230 | 231 | ## composeVector Extension options 232 | 233 | `build.gradle.kts` 234 | 235 | ```kotlin 236 | import io.github.irgaly.compose.vector.plugin.ComposeVectorExtension 237 | 238 | ... 239 | 240 | composeVector { 241 | // ImageVector classes destination package name 242 | // 243 | // Required 244 | packagenName = "your.package.name" 245 | 246 | // Vector files directory 247 | // 248 | // Optional 249 | // Default: {project directory}/images 250 | inputDir = layout.projectDirectory.dir("images") 251 | 252 | // Generated Kotlin Sources directory. 253 | // outputDir is registered to SourceSet when outputDir is inside of project's buildDirectory. 254 | // 255 | // Optional 256 | // Default: {build directory}/compose-vector/src/main/kotlin 257 | outputDir = layout.buildDirectory.dir("compose-vector/src/main/kotlin") 258 | 259 | // Custom preconverter logic to ImageVector property names and receiver class names. 260 | // 261 | // * args 262 | // * File: source file or directory's File instance 263 | // * String: source file or directory's name 264 | // 265 | // Optional 266 | preClassNameTransformer.set (org.gradle.api.Transformer { (file: File, name: String) -> 267 | // custom logic here 268 | "pre_transformed_class_name" 269 | }) 270 | 271 | // Custom postconverter logic to ImageVector property names and receiver class names. 272 | // 273 | // * args 274 | // * File: source file or directory's File instance 275 | // * String: transformed name 276 | // 277 | // Optional 278 | postClassNameTransformer.set (org.gradle.api.Transformer { (file: File, name: String) -> 279 | // custom logic here 280 | "transformed_class_name" 281 | }) 282 | 283 | // Custom converter logic to package names. 284 | // 285 | // * args 286 | // * File: source directory's File instance 287 | // * String: source directory's name 288 | // 289 | // Optional 290 | packageNameTransformer.set (org.gradle.api.Transformer { (file: File, name: String) -> 291 | // custom logic here 292 | "transformed_package_name" 293 | }) 294 | 295 | // Target SourceSets that generated images belongs to for KMP project. 296 | // This option is affect to KMP Project, not to Android only Project. 297 | // 298 | // Optional 299 | // Default: ComposeVectorExtension.GenerationTarget.Common 300 | multiplatformGenerationTarget = ComposeVectorExtension.GenerationTarget.Common 301 | //multiplatformGenerationTarget = ComposeVectorExtension.GenerationTarget.Android 302 | 303 | // Generate androidx.compose.ui.tooling.preview.Preview functions for Android target or not 304 | // 305 | // Optional 306 | // Default: true 307 | generateAndroidPreview = true 308 | 309 | // Generate org.jetbrains.compose.ui.tooling.preview.Preview functions for KMP common target or not 310 | // 311 | // Optional 312 | // Default: false 313 | generateJetbrainsPreview = false 314 | 315 | // Generate androidx.compose.desktop.ui.tooling.preview.Preview functions for KMP common target or not 316 | // 317 | // Optional 318 | // Default: true 319 | generateDesktopPreview = true 320 | } 321 | ``` 322 | 323 | ## Debug with logging 324 | 325 | This plugin will be logging with `--info` gradle option. 326 | 327 | ```shell 328 | % ./gradlew :app:generateImageVector --info 329 | ... 330 | > Task :app:generateImageVector 331 | Build cache key for task ':app:generateImageVector' is dc07551486b4a33c25fa9d1ef7b64905 332 | Task ':app:generateImageVector' is not up-to-date because: 333 | Task.upToDateWhen is false. 334 | The input changes require a full rebuild for incremental task ':app:generateImageVector'. 335 | 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. 336 | changed: Input file .../app/images/icons/automirrored/filled/undo.svg added for rebuild. 337 | convert icons/automirrored/filled/undo.svg to icons/automirrored/filled/Undo.kt 338 | changed: Input file .../app/images/icons/filled/undo.svg added for rebuild. 339 | convert icons/filled/undo.svg to icons/filled/Undo.kt 340 | write object file: Icons.kt 341 | Stored cache entry for task ':app:generateImageVector' with cache key dc07551486b4a33c25fa9d1ef7b64905 342 | ... 343 | ``` 344 | 345 | ## Use SVG to ImageVector converter as Java Library 346 | 347 | SVG to ImageVector converter logic is packaged as Java library, so it can be used from your java CLI or applications. 348 | 349 | `build.gradle.kts` 350 | 351 | ```kotlin 352 | plugins { 353 | id("org.jetbrains.kotlin.jvm") 354 | } 355 | ... 356 | dependencies { 357 | implementation("io.github.irgaly.compose.vector:compose-vector:1.1.0") 358 | } 359 | ``` 360 | 361 | Then use `SvgParser` class and `ImageVectorGenerator` class. 362 | 363 | ```kotlin 364 | val inputStream = ... // SVG content as InputStream from File or String etc... 365 | val imageVector: io.github.irgaly.compose.vector.node.ImageVector = SvgParser(object : Logger { 366 | override fun debug(message: String) { 367 | println("debug: $message") 368 | } 369 | 370 | override fun info(message: String) { 371 | println("info: $message") 372 | } 373 | 374 | override fun warn(message: String, error: Exception?) { 375 | println("warn: $message | $error") 376 | } 377 | 378 | override fun error(message: String, error: Exception?) { 379 | println("error: $message | $error") 380 | } 381 | }).parse( 382 | inputStream, 383 | name = "Icon" 384 | ) 385 | val kotlinSource: String = ImageVectorGenerator().generate( 386 | imageVector = imageVector, 387 | destinationPackage = "io.github.irgaly.icons", 388 | receiverClasses = listOf("Icons", "AutoMirrored", "Filled"), 389 | extensionPackage = "io.github.irgaly.icons.automirrored.filled", 390 | hasAndroidPreview = true, 391 | ) 392 | println(kotlinSource) 393 | ``` 394 | 395 | ## Supported SVG specifications 396 | 397 | This plugin's converter is using [Apache Batik](https://xmlgraphics.apache.org/batik/) SVG parser, 398 | and supports basics specifications of SVG 1.2 + CSS style tag. 399 | 400 | Here is a supporting table. 401 | 402 | | SVG tag | SVG attribute | Supporting Status | 403 | |----------------|------------------------------------------------------------------------------------------|-------------------------------------------------------------------| 404 | | (any) | id, class | :white_check_mark: | 405 | | (any) | style | :white_check_mark: | 406 | | (any) | transform | :white_check_mark: | 407 | | (any) | display | :white_check_mark: | 408 | | (any) | visibility | :white_check_mark: | 409 | | (any) | color | :white_check_mark: | 410 | | (any) | fill, fill-opacity, fill-rule | :white_check_mark: | 411 | | (any) | stroke, stroke-opacity, stroke-width, stroke-linecap, stroke-linejoin, stroke-miterlimit | :white_check_mark: | 412 | | (any) | clip-path, clip-rule, clipPathUnits | :white_check_mark: | 413 | | svg | viewBox, width, height | :white_check_mark:
Nested SVG tag is supported. | 414 | | symbol | viewBox, x, y, width, height | :white_check_mark: | 415 | | g | | :white_check_mark: | 416 | | path | d | :white_check_mark: | 417 | | rect | x, y, width, height, rx, ry | :white_check_mark: | 418 | | circle | cx, cy, r | :white_check_mark: | 419 | | ellipse | cx, cy, rx, ry | :white_check_mark: | 420 | | line | x1, x2, y1, y2 | :white_check_mark: | 421 | | polyline | points | :white_check_mark: | 422 | | polygon | points | :white_check_mark: | 423 | | clipPath | | :white_check_mark: | 424 | | defs | | :white_check_mark: | 425 | | linearGradient | gradientUnits, spreadMethod, x1, x2, y1, y2 | :white_check_mark: | 426 | | radialGradient | gradientUnits, cx, cy, fr | :white_check_mark: | 427 | | stop | offset, stop-color | :white_check_mark: | 428 | | use | href, xlink:href | :white_check_mark: | 429 | | a | | a tag is treated as same as g tag. No clickable feature. | 430 | | title | | This tag is just ignored | 431 | | desc | | This tag is just ignored | 432 | | metadata | | This tag is just ignored | 433 | | view | | This tag is just ignored | 434 | | script | | This tag is just ignored | 435 | | cursor | | This tag is just ignored | 436 | | animate | | Not supported because ImageVector doesn't have animation feature. | 437 | | text | | Not supported because ImageVector can't draw texts. | 438 | | image | | Not supported because ImageVector can't draw images. | 439 | | filter | | Not supported. | 440 | | mask | | Not supported. | 441 | | switch | | Not supported. | 442 | | foreignObject | | Not supported. | 443 | 444 | ### Color format style 445 | 446 | CSS4 Named Colors and sRGB colors are supported for color format. 447 | 448 | | Color Format Style | Supporting Status | 449 | |-----------------------------------------------------------------------------|--------------------| 450 | | [CSS4 Named Colors](https://www.w3.org/TR/css-color-4/#named-colors) | :white_check_mark: | 451 | | rgb(0 0 0), rgb(0% 0% 0%), rgb(0, 0, 0) | :white_check_mark: | 452 | | rgb(0 0 0 0), rgb(0 0 0 / 0), rgb(0% 0% 0% 0%), rgb(0, 0, 0, 0) | :white_check_mark: | 453 | | rgba(0 0 0 0), rgba(0 0 0 / 0), rgba(0%, 0%, 0%, 0%) | :white_check_mark: | 454 | | #RRGGBB, #RGB | :white_check_mark: | 455 | | #RRGGBBAA, #RGBA | :white_check_mark: | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension 2 | 3 | plugins { 4 | alias(libs.plugins.kotlin.multiplatform) apply false 5 | alias(libs.plugins.android.application) apply false 6 | alias(libs.plugins.android.library) apply false 7 | alias(libs.plugins.kotlin.android) apply false 8 | alias(libs.plugins.kotlin.jvm) apply false 9 | alias(libs.plugins.compose.compiler) apply false 10 | alias(libs.plugins.jetbrains.compose) apply false 11 | } 12 | 13 | subprojects { 14 | afterEvaluate { 15 | extensions.findByType()?.apply { 16 | jvmToolchain(17) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/inputdir_images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irgaly/compose-vector-plugin/a97e4454d23b92fa804aa27b59775e0f73b55ed7/docs/inputdir_images.png -------------------------------------------------------------------------------- /docs/output_imagevectors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irgaly/compose-vector-plugin/a97e4454d23b92fa804aa27b59775e0f73b55ed7/docs/output_imagevectors.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | org.gradle.jvmargs=-Xmx16g -XX:MaxMetaspaceSize=8g 3 | org.gradle.parallel=true 4 | org.gradle.caching=true 5 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | composeVector = "1.1.0" 3 | kotlin = "2.2.0" 4 | gradle-android = "8.13.0" 5 | kotest = "6.0.3" 6 | 7 | [libraries] 8 | kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 9 | android-gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle-android" } 10 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.0" } 11 | androidx-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version = "2.9.1" } 12 | compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2025.10.00" } 13 | compose-activity = { module = "androidx.activity:activity-compose", version = "1.11.0" } 14 | compose-material3 = { module = "androidx.compose.material3:material3" } 15 | compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } 16 | compose-uiTooling = { module = "androidx.compose.ui:ui-tooling" } 17 | xmlpull = { module = "xmlpull:xmlpull", version = "1.1.3.1" } 18 | guava = { module = "com.google.guava:guava", version = "33.5.0-jre" } 19 | kotlinpoet = { module = "com.squareup:kotlinpoet", version = "2.2.0" } 20 | batik = { module = "org.apache.xmlgraphics:batik-all", version = "1.19" } 21 | test-kotest-runner = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } 22 | test-kotest-assertions = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } 23 | composeVector = { module = "io.github.irgaly.compose.vector:compose-vector", version.ref = "composeVector" } 24 | 25 | [bundles] 26 | compose = ["compose-activity", "compose-material3", "compose-material-icons-extended", "compose-uiTooling"] 27 | 28 | [plugins] 29 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 30 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 31 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 32 | jetbrains-compose = { id = "org.jetbrains.compose", version = "1.9.1" } 33 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 34 | android-application = { id = "com.android.application", version.ref = "gradle-android" } 35 | android-library = { id = "com.android.library", version.ref = "gradle-android" } 36 | plugin-publish = { id = "com.gradle.plugin-publish", version = "2.0.0" } 37 | dokka = { id = "org.jetbrains.dokka", version = "2.0.0" } 38 | nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version = "2.0.0" } 39 | composeVector = { id = "io.github.irgaly.compose-vector", version.ref = "composeVector" } 40 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/irgaly/compose-vector-plugin/a97e4454d23b92fa804aa27b59775e0f73b55ed7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension 2 | 3 | plugins { 4 | alias(libs.plugins.kotlin.jvm) 5 | alias(libs.plugins.plugin.publish) 6 | alias(libs.plugins.nexus.publish) 7 | } 8 | 9 | group = "io.github.irgaly.compose-vector" 10 | version = libs.versions.composeVector.get() 11 | 12 | gradlePlugin { 13 | website = "https://github.com/irgaly/compose-vector-plugin" 14 | vcsUrl = "https://github.com/irgaly/compose-vector-plugin" 15 | plugins { 16 | create("plugin") { 17 | id = "io.github.irgaly.compose-vector" 18 | displayName = "Gradle Compose Vector Plugin" 19 | description = "Gradle Plugin for Converting SVG file to Compose ImageVector" 20 | tags = listOf("compose", "svg") 21 | implementationClass = "io.github.irgaly.compose.vector.plugin.ComposeVectorPlugin" 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | compileOnly(gradleKotlinDsl()) 28 | implementation(libs.kotlin.gradle) 29 | implementation(libs.android.gradle) 30 | implementation(projects.core) 31 | testImplementation(libs.test.kotest.runner) 32 | } 33 | 34 | tasks.withType().configureEach { 35 | useJUnitPlatform() 36 | } 37 | 38 | subprojects { 39 | afterEvaluate { 40 | extensions.findByType()?.apply { 41 | jvmToolchain(17) 42 | } 43 | } 44 | } 45 | 46 | java { 47 | withSourcesJar() 48 | withJavadocJar() 49 | } 50 | 51 | if (providers.environmentVariable("CI").isPresent) { 52 | apply(plugin = "signing") 53 | extensions.configure { 54 | useInMemoryPgpKeys( 55 | providers.environmentVariable("SIGNING_PGP_KEY").orNull, 56 | providers.environmentVariable("SIGNING_PGP_PASSWORD").orNull 57 | ) 58 | } 59 | } 60 | 61 | nexusPublishing { 62 | repositories { 63 | sonatype { 64 | stagingProfileId = libs.versions.composeVector.get() 65 | nexusUrl = uri("https://ossrh-staging-api.central.sonatype.com/service/local/") 66 | snapshotRepositoryUrl = 67 | uri("https://central.sonatype.com/repository/maven-snapshots/") 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /plugin/core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.dokka.gradle.DokkaTask 2 | 3 | plugins { 4 | alias(libs.plugins.kotlin.jvm) 5 | alias(libs.plugins.dokka) 6 | `maven-publish` 7 | signing 8 | } 9 | 10 | tasks.withType { 11 | useJUnitPlatform() 12 | } 13 | 14 | dependencies { 15 | implementation(libs.batik) 16 | implementation(libs.kotlinpoet) 17 | testImplementation(libs.test.kotest.runner) 18 | testImplementation(libs.test.kotest.assertions) 19 | 20 | // for temporary sample code 21 | implementation(libs.xmlpull) 22 | implementation(libs.guava) 23 | } 24 | 25 | java { 26 | withSourcesJar() 27 | withJavadocJar() 28 | } 29 | 30 | val dokkaJavadoc by tasks.getting(DokkaTask::class) 31 | val javadocJar by tasks.getting(Jar::class) { 32 | dependsOn(dokkaJavadoc) 33 | from(dokkaJavadoc.outputDirectory) 34 | } 35 | 36 | signing { 37 | useInMemoryPgpKeys( 38 | providers.environmentVariable("SIGNING_PGP_KEY").orNull, 39 | providers.environmentVariable("SIGNING_PGP_PASSWORD").orNull 40 | ) 41 | if (providers.environmentVariable("CI").isPresent) { 42 | sign(extensions.getByType().publications) 43 | } 44 | } 45 | 46 | group = "io.github.irgaly.compose.vector" 47 | version = libs.versions.composeVector.get() 48 | 49 | publishing { 50 | publications { 51 | create("mavenCentral") { 52 | from(components["java"]) 53 | artifactId = "compose-vector" 54 | pom { 55 | name = artifactId 56 | description = "Convert SVG file to Compose ImageVector" 57 | url = "https://github.com/irgaly/compose-vector-plugin" 58 | developers { 59 | developer { 60 | id = "irgaly" 61 | name = "irgaly" 62 | email = "irgaly@gmail.com" 63 | } 64 | } 65 | licenses { 66 | license { 67 | name = "The Apache License, Version 2.0" 68 | url = "https://www.apache.org/licenses/LICENSE-2.0.txt" 69 | } 70 | } 71 | scm { 72 | connection = "git@github.com:irgaly/compose-vector-plugin.git" 73 | developerConnection = 74 | "git@github.com:irgaly/compose-vector-plugin.git" 75 | url = "https://github.com/irgaly/compose-vector-plugin" 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/Logger.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose 2 | 3 | interface Logger { 4 | fun debug(message: String) 5 | fun info(message: String) 6 | fun warn(message: String, error: Exception? = null) 7 | fun error(message: String, error: Exception? = null) 8 | } 9 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/Icon.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/Icon.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml 21 | 22 | /** 23 | * Represents a icon's Kotlin name, processed XML file name, theme, and XML file content. 24 | * 25 | * The [kotlinName] is typically the PascalCase equivalent of the original icon name, with the 26 | * caveat that icons starting with a number are prefixed with an underscore. 27 | * 28 | * @property kotlinName the name of the generated Kotlin property, for example `ZoomOutMap`. 29 | * @property xmlFileName the name of the processed XML file 30 | * @property theme the theme of this icon 31 | * @property fileContent the content of the source XML file that will be parsed. 32 | * @property autoMirrored indicates that this Icon can be auto-mirrored on Right to Left layouts. 33 | */ 34 | internal data class Icon( 35 | val kotlinName: String, 36 | val xmlFileName: String, 37 | val theme: IconTheme, 38 | val fileContent: String, 39 | val autoMirrored: Boolean 40 | ) 41 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/IconParser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/IconParser.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml 21 | 22 | import io.github.irgaly.compose.icons.xml.vector.FillType 23 | import io.github.irgaly.compose.icons.xml.vector.PathParser 24 | import io.github.irgaly.compose.icons.xml.vector.Vector 25 | import io.github.irgaly.compose.icons.xml.vector.VectorNode 26 | import org.xmlpull.v1.XmlPullParser 27 | import org.xmlpull.v1.XmlPullParser.END_DOCUMENT 28 | import org.xmlpull.v1.XmlPullParser.END_TAG 29 | import org.xmlpull.v1.XmlPullParser.START_TAG 30 | import org.xmlpull.v1.XmlPullParserException 31 | import org.xmlpull.v1.XmlPullParserFactory 32 | 33 | /** 34 | * Parser that converts [icon]s into [Vector]s 35 | */ 36 | internal class IconParser(private val icon: Icon) { 37 | 38 | /** 39 | * @return a [Vector] representing the provided [icon]. 40 | */ 41 | fun parse(): Vector { 42 | val parser = XmlPullParserFactory.newInstance().newPullParser().apply { 43 | setInput(icon.fileContent.byteInputStream(), null) 44 | seekToStartTag() 45 | } 46 | 47 | check(parser.name == VECTOR) { "The start tag must be !" } 48 | 49 | val nodes = mutableListOf() 50 | var autoMirrored = false 51 | 52 | var currentGroup: VectorNode.Group? = null 53 | 54 | while (!parser.isAtEnd()) { 55 | when (parser.eventType) { 56 | START_TAG -> { 57 | when (parser.name) { 58 | VECTOR -> { 59 | autoMirrored = parser.getValueAsBoolean(AUTO_MIRRORED) 60 | } 61 | 62 | PATH -> { 63 | val pathData = parser.getAttributeValue( 64 | null, 65 | PATH_DATA 66 | ) 67 | val fillAlpha = parser.getValueAsFloat(FILL_ALPHA) 68 | val strokeAlpha = parser.getValueAsFloat(STROKE_ALPHA) 69 | val fillType = when (parser.getAttributeValue(null, FILL_TYPE)) { 70 | // evenOdd and nonZero are the only supported values here, where 71 | // nonZero is the default if no values are defined. 72 | EVEN_ODD -> FillType.EvenOdd 73 | else -> FillType.NonZero 74 | } 75 | val path = VectorNode.Path( 76 | strokeAlpha = strokeAlpha ?: 1f, 77 | fillAlpha = fillAlpha ?: 1f, 78 | fillType = fillType, 79 | nodes = PathParser.parsePathString(pathData) 80 | ) 81 | if (currentGroup != null) { 82 | currentGroup.paths.add(path) 83 | } else { 84 | nodes.add(path) 85 | } 86 | } 87 | // Material icons are simple and don't have nested groups, so this can be simple 88 | GROUP -> { 89 | val group = VectorNode.Group() 90 | currentGroup = group 91 | nodes.add(group) 92 | } 93 | 94 | CLIP_PATH -> { /* TODO: b/147418351 - parse clipping paths */ 95 | } 96 | } 97 | } 98 | } 99 | parser.next() 100 | } 101 | 102 | return Vector(autoMirrored, nodes) 103 | } 104 | } 105 | 106 | /** 107 | * @return the float value for the attribute [name], or null if it couldn't be found 108 | */ 109 | private fun XmlPullParser.getValueAsFloat(name: String) = 110 | getAttributeValue(null, name)?.toFloatOrNull() 111 | 112 | /** 113 | * @return the boolean value for the attribute [name], or 'false' if it couldn't be found 114 | */ 115 | private fun XmlPullParser.getValueAsBoolean(name: String) = 116 | getAttributeValue(null, name).toBoolean() 117 | 118 | private fun XmlPullParser.seekToStartTag(): XmlPullParser { 119 | var type = next() 120 | while (type != START_TAG && type != END_DOCUMENT) { 121 | // Empty loop 122 | type = next() 123 | } 124 | if (type != START_TAG) { 125 | throw XmlPullParserException("No start tag found") 126 | } 127 | return this 128 | } 129 | 130 | private fun XmlPullParser.isAtEnd() = 131 | eventType == END_DOCUMENT || (depth < 1 && eventType == END_TAG) 132 | 133 | // XML tag names 134 | private const val VECTOR = "vector" 135 | private const val CLIP_PATH = "clip-path" 136 | private const val GROUP = "group" 137 | private const val PATH = "path" 138 | 139 | // XML attribute names 140 | private const val AUTO_MIRRORED = "android:autoMirrored" 141 | private const val PATH_DATA = "android:pathData" 142 | private const val FILL_ALPHA = "android:fillAlpha" 143 | private const val STROKE_ALPHA = "android:strokeAlpha" 144 | private const val FILL_TYPE = "android:fillType" 145 | 146 | // XML attribute values 147 | private const val EVEN_ODD = "evenOdd" 148 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/IconProcessor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/IconProcessor.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml 21 | 22 | import com.google.common.base.CaseFormat 23 | import java.io.File 24 | import java.util.Locale 25 | 26 | /** 27 | * Processes vector drawables in [iconDirectories] into a list of icons, removing any unwanted 28 | * attributes (such as android: attributes that reference the theme) from the XML source. 29 | * 30 | * Each directory in [iconDirectories] should contain a flat list of icons to process. For example, 31 | * given the existing structure in raw-icons: 32 | * 33 | * // Theme name 34 | * ├── filled 35 | * // Icon name 36 | * ├── menu.xml 37 | * └── zoom_out_map.xml 38 | * ├── outlined 39 | * ├── rounded 40 | * ├── twotone 41 | * └── sharp 42 | * 43 | * Each directory in [iconDirectories] should be a theme directory (filled, outlined, etc). 44 | * 45 | * @param iconDirectories list of directories containing icon to process 46 | * @param expectedApiFile location of the checked-in API file that contains the current list of 47 | * all icons processed and generated 48 | * @param generatedApiFile location of the to-be-generated API file in the build directory, 49 | * that we will write to and compare with [expectedApiFile]. This way the generated file can be 50 | * copied to overwrite the expected file, 'confirming' any API changes as a result of changing 51 | * icons in [iconDirectories]. 52 | * @param expectedAutoMirroredApiFile location of the checked-in API file that contains the current 53 | * list of all auto-mirrored icons processed and generated 54 | * @param generatedAutoMirroredApiFile location of the to-be-generated API file in the build 55 | * directory, that we will write to and compare with [expectedAutoMirroredApiFile]. This way the 56 | * generated file can be copied to overwrite the expected file, 'confirming' any API changes as a 57 | * result of changing auto-mirrored icons in [iconDirectories] 58 | */ 59 | internal class IconProcessor( 60 | private val iconDirectories: List, 61 | private val expectedApiFile: File, 62 | private val generatedApiFile: File, 63 | private val expectedAutoMirroredApiFile: File, 64 | private val generatedAutoMirroredApiFile: File, 65 | ) { 66 | /** 67 | * @return a list of processed [Icon]s, from the provided [iconDirectories]. 68 | */ 69 | fun process(): List { 70 | val icons = loadIcons() 71 | 72 | ensureIconsExistInAllThemes(icons) 73 | val (regularIcons, autoMirroredIcons) = icons.partition { !it.autoMirrored } 74 | writeApiFile(regularIcons, generatedApiFile) 75 | writeApiFile(autoMirroredIcons, generatedAutoMirroredApiFile) 76 | checkApi(expectedApiFile, generatedApiFile) 77 | checkApi(expectedAutoMirroredApiFile, generatedAutoMirroredApiFile) 78 | 79 | return icons 80 | } 81 | 82 | private fun loadIcons(): List { 83 | val themeDirs = iconDirectories 84 | 85 | return themeDirs.flatMap { dir -> 86 | val theme = dir.name.toIconTheme() 87 | val icons = dir.walk().filter { !it.isDirectory }.toList() 88 | 89 | val transformedIcons = icons.map { file -> 90 | val filename = file.nameWithoutExtension 91 | val kotlinName = filename.toKotlinPropertyName() 92 | 93 | // Prefix the icon name with a theme so we can ensure they will be unique when 94 | // copied to res/drawable. 95 | val xmlName = "${theme.themePackageName}_$filename" 96 | val fileContent = file.readText() 97 | Icon( 98 | kotlinName = kotlinName, 99 | xmlFileName = xmlName, 100 | theme = theme, 101 | fileContent = processXmlFile(fileContent), 102 | autoMirrored = isAutoMirrored(fileContent) 103 | ) 104 | } 105 | 106 | // Ensure icon names are unique when accounting for case insensitive filesystems - 107 | // workaround for b/216295020 108 | transformedIcons 109 | .groupBy { it.kotlinName.lowercase(Locale.ROOT) } 110 | .filter { it.value.size > 1 } 111 | .filterNot { entry -> 112 | entry.value.map { it.kotlinName }.containsAll(AllowedDuplicateIconNames) 113 | } 114 | .forEach { entry -> 115 | throw IllegalStateException( 116 | """Found multiple icons with the same case-insensitive filename: 117 | | ${entry.value.joinToString()}. Generating icons with the same 118 | | case-insensitive filename will cause issues on devices without 119 | | a case sensitive filesystem (OSX / Windows).""".trimMargin() 120 | ) 121 | } 122 | 123 | transformedIcons 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * Processes the given [fileContent] by removing android theme attributes and values. 130 | */ 131 | private fun processXmlFile(fileContent: String): String { 132 | // Remove any defined tint for paths that use theme attributes 133 | val tintAttribute = Regex.escape("""android:tint="?attr/colorControlNormal"""") 134 | val tintRegex = """\n.*?$tintAttribute""".toRegex(RegexOption.MULTILINE) 135 | 136 | return fileContent 137 | .replace(tintRegex, "") 138 | // The imported icons have white as the default path color, so let's change it to be 139 | // black as is typical on Android. 140 | .replace("@android:color/white", "@android:color/black") 141 | } 142 | 143 | /** 144 | * Returns true if the given [fileContent] includes an `android:autoMirrored="true"` attribute. 145 | */ 146 | private fun isAutoMirrored(fileContent: String): Boolean = 147 | fileContent.contains(Regex.fromLiteral("""android:autoMirrored="true"""")) 148 | 149 | /** 150 | * Ensures that each icon in each theme is available in every other theme 151 | */ 152 | private fun ensureIconsExistInAllThemes(icons: List) { 153 | val groupedIcons = icons.groupBy { it.theme } 154 | 155 | check(groupedIcons.keys.containsAll(IconTheme.values().toList())) { 156 | "Some themes were missing from the generated icons" 157 | } 158 | 159 | val expectedIconNames = groupedIcons.values.map { themeIcons -> 160 | themeIcons.map { icon -> icon.kotlinName }.sorted() 161 | } 162 | 163 | expectedIconNames.first().let { expected -> 164 | expectedIconNames.forEach { actual -> 165 | check(actual == expected) { 166 | "Not all icons were found in all themes $actual $expected" 167 | } 168 | } 169 | } 170 | } 171 | 172 | /** 173 | * Writes an API representation of [icons] to [file]. 174 | */ 175 | private fun writeApiFile(icons: List, file: File) { 176 | val apiText = icons 177 | .groupBy { it.theme } 178 | .map { (theme, themeIcons) -> 179 | themeIcons 180 | .map { icon -> 181 | theme.themeClassName + "." + icon.kotlinName 182 | } 183 | .sorted() 184 | .joinToString(separator = "\n") 185 | } 186 | .sorted() 187 | .joinToString(separator = "\n") 188 | 189 | file.writeText(apiText) 190 | } 191 | 192 | /** 193 | * Ensures that [generatedFile] matches the checked-in API surface in [expectedFile]. 194 | */ 195 | private fun checkApi(expectedFile: File, generatedFile: File) { 196 | check(expectedFile.exists()) { 197 | "API file at ${expectedFile.canonicalPath} does not exist!" 198 | } 199 | 200 | check(expectedFile.readText() == generatedFile.readText()) { 201 | """Found differences when comparing API files! 202 | |Please check the difference and copy over the changes if intended. 203 | |expected file: ${expectedFile.canonicalPath} 204 | |generated file: ${generatedFile.canonicalPath} 205 | |Please manually un-ignore and run ExtendedIconComparisonTest locally before 206 | |uploading. 207 | """.trimMargin() 208 | } 209 | } 210 | 211 | /** 212 | * Converts a snake_case name to a KotlinProperty name. 213 | * 214 | * If the first character of [this] is a digit, the resulting name will be prefixed with an `_` 215 | */ 216 | private fun String.toKotlinPropertyName(): String { 217 | return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, this).let { name -> 218 | if (name.first().isDigit()) "_$name" else name 219 | } 220 | } 221 | 222 | // These icons have already shipped in a stable release, so it is too late to rename / remove one to 223 | // fix the clash. 224 | private val AllowedDuplicateIconNames = listOf("AddChart", "Addchart") 225 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/IconTheme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/IconTheme.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml 21 | 22 | import java.util.Locale 23 | 24 | /** 25 | * Enum representing the different themes for Material icons. 26 | * 27 | * @property themePackageName the lower case name used for package names and in xml files 28 | * @property themeClassName the CameCase name used for the theme objects 29 | */ 30 | internal enum class IconTheme(val themePackageName: String, val themeClassName: String) { 31 | Filled("filled", "Filled"), 32 | Outlined("outlined", "Outlined"), 33 | Rounded("rounded", "Rounded"), 34 | TwoTone("twotone", "TwoTone"), 35 | Sharp("sharp", "Sharp") 36 | } 37 | 38 | /** 39 | * Returns the matching [IconTheme] from [this] [IconTheme.themePackageName]. 40 | */ 41 | internal fun String.toIconTheme() = requireNotNull( 42 | IconTheme.values().find { 43 | it.themePackageName == this 44 | } 45 | ) { "No matching theme found" } 46 | 47 | /** 48 | * The ClassName representing this [IconTheme] object, so we can generate extension properties on 49 | * the object. 50 | * 51 | * @see [autoMirroredClassName] 52 | */ 53 | internal val IconTheme.className 54 | get() = 55 | PackageNames.MaterialIconsPackage.className("Icons", themeClassName) 56 | 57 | /** 58 | * The ClassName representing this [IconTheme] object so we can generate extension properties on the 59 | * object when used for auto-mirrored icons. 60 | * 61 | * @see [className] 62 | */ 63 | internal val IconTheme.autoMirroredClassName 64 | get() = 65 | PackageNames.MaterialIconsPackage.className("Icons", AutoMirroredName, themeClassName) 66 | 67 | internal const val AutoMirroredName = "AutoMirrored" 68 | internal val AutoMirroredPackageName = AutoMirroredName.lowercase(Locale.ROOT) 69 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/IconWriter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/IconWriter.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml 21 | 22 | import java.io.File 23 | 24 | /** 25 | * Generates programmatic representation of all [icons] using [ImageVectorGenerator]. 26 | * 27 | * @property icons the list of [Icon]s to generate Kotlin files for 28 | */ 29 | internal class IconWriter(private val icons: List) { 30 | /** 31 | * Generates icons and writes them to [outputSrcDirectory], using [iconNamePredicate] to 32 | * filter what icons to generate for. 33 | * 34 | * @param outputSrcDirectory the directory to generate source files in 35 | * @param iconNamePredicate the predicate that filters what icons should be generated. If 36 | * false, the icon will not be parsed and generated in [outputSrcDirectory]. 37 | */ 38 | fun generateTo( 39 | outputSrcDirectory: File, 40 | iconNamePredicate: (String) -> Boolean 41 | ) { 42 | icons.forEach { icon -> 43 | if (!iconNamePredicate(icon.kotlinName)) return@forEach 44 | 45 | val vector = IconParser(icon).parse() 46 | 47 | val fileSpec = ImageVectorGenerator( 48 | icon.kotlinName, 49 | icon.theme, 50 | vector 51 | ).createFileSpec() 52 | 53 | fileSpec.writeToWithCopyright(outputSrcDirectory) 54 | 55 | // Write additional file specs for auto-mirrored icons. These files will be written into 56 | // an automirrored package and will hold a similar icons theme structure underneath. 57 | if (vector.autoMirrored) { 58 | val autoMirroredFileSpec = ImageVectorGenerator( 59 | icon.kotlinName, 60 | icon.theme, 61 | vector 62 | ).createAutoMirroredFileSpec() 63 | 64 | autoMirroredFileSpec.writeToWithCopyright(outputSrcDirectory) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/ImageVectorGenerator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/ImageVectorGenerator.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml 21 | 22 | import io.github.irgaly.compose.icons.xml.vector.FillType 23 | import io.github.irgaly.compose.icons.xml.vector.Vector 24 | import io.github.irgaly.compose.icons.xml.vector.VectorNode 25 | import com.squareup.kotlinpoet.AnnotationSpec 26 | import com.squareup.kotlinpoet.CodeBlock 27 | import com.squareup.kotlinpoet.FileSpec 28 | import com.squareup.kotlinpoet.FunSpec 29 | import com.squareup.kotlinpoet.KModifier 30 | import com.squareup.kotlinpoet.PropertySpec 31 | import com.squareup.kotlinpoet.buildCodeBlock 32 | import java.util.Locale 33 | 34 | /** 35 | * Generator for creating a Kotlin source file with an ImageVector property for the given [vector], 36 | * with name [iconName] and theme [iconTheme]. 37 | * 38 | * @param iconName the name for the generated property, which is also used for the generated file. 39 | * I.e if the name is `Menu`, the property will be `Menu` (inside a theme receiver object) and 40 | * the file will be `Menu.kt` (under the theme package name). 41 | * @param iconTheme the theme that this vector belongs to. Used to scope the property to the 42 | * correct receiver object, and also for the package name of the generated file. 43 | * @param vector the parsed vector to generate ImageVector.Builder commands for 44 | */ 45 | internal class ImageVectorGenerator( 46 | private val iconName: String, 47 | private val iconTheme: IconTheme, 48 | private val vector: Vector 49 | ) { 50 | /** 51 | * @return a [FileSpec] representing a Kotlin source file containing the property for this 52 | * programmatic [vector] representation. 53 | * 54 | * The package name and hence file location of the generated file is: 55 | * [PackageNames.MaterialIconsPackage] + [IconTheme.themePackageName]. 56 | */ 57 | fun createFileSpec(): FileSpec { 58 | val builder = createFileSpecBuilder(themePackageName = iconTheme.themePackageName) 59 | val backingProperty = getBackingProperty() 60 | // Create a property with a getter. The autoMirror is always false in this case. 61 | val propertySpecBuilder = 62 | PropertySpec.builder(name = iconName, type = ClassNames.ImageVector) 63 | .receiver(iconTheme.className) 64 | .getter( 65 | iconGetter( 66 | backingProperty = backingProperty, 67 | iconName = iconName, 68 | iconTheme = iconTheme, 69 | autoMirror = false 70 | ) 71 | ) 72 | // Add a deprecation warning with a suggestion to replace this icon's usage with its 73 | // equivalent that was generated under the automirrored package. 74 | if (vector.autoMirrored) { 75 | val autoMirroredPackage = "${PackageNames.MaterialIconsPackage.packageName}." + 76 | "$AutoMirroredPackageName.${iconTheme.themePackageName}" 77 | propertySpecBuilder.addAnnotation( 78 | AnnotationSpec.builder(Deprecated::class) 79 | .addMember( 80 | "\"Use the AutoMirrored version at %N.%N.%N.%N\"", 81 | ClassNames.Icons.simpleName, 82 | AutoMirroredName, 83 | iconTheme.name, 84 | iconName 85 | ) 86 | .addMember( 87 | "ReplaceWith( \"%N.%N.%N.%N\", \"$autoMirroredPackage.%N\")", 88 | ClassNames.Icons.simpleName, 89 | AutoMirroredName, 90 | iconTheme.name, 91 | iconName, 92 | iconName 93 | ) 94 | .build() 95 | ) 96 | } 97 | builder.addProperty(propertySpecBuilder.build()) 98 | builder.addProperty(backingProperty) 99 | return builder.setIndent().build() 100 | } 101 | 102 | /** 103 | * @return a [FileSpec] representing a Kotlin source file containing the property for this 104 | * programmatic, auto-mirrored, [vector] representation. 105 | * 106 | * The package name and hence file location of the generated file is: 107 | * [PackageNames.MaterialIconsPackage] + [AutoMirroredPackageName] + 108 | * [IconTheme.themePackageName]. 109 | */ 110 | fun createAutoMirroredFileSpec(): FileSpec { 111 | // Prepend the AutoMirroredName package name to the IconTheme package name. 112 | val builder = createFileSpecBuilder( 113 | themePackageName = "$AutoMirroredPackageName.${iconTheme.themePackageName}" 114 | ) 115 | val backingProperty = getBackingProperty() 116 | // Create a property with a getter. The autoMirror is always false in this case. 117 | builder.addProperty( 118 | PropertySpec.builder(name = iconName, type = ClassNames.ImageVector) 119 | .receiver(iconTheme.autoMirroredClassName) 120 | .getter( 121 | iconGetter( 122 | backingProperty = backingProperty, 123 | iconName = iconName, 124 | iconTheme = iconTheme, 125 | autoMirror = true 126 | ) 127 | ) 128 | .build() 129 | ) 130 | builder.addProperty(backingProperty) 131 | return builder.setIndent().build() 132 | } 133 | 134 | private fun createFileSpecBuilder(themePackageName: String): FileSpec.Builder { 135 | val iconsPackage = PackageNames.MaterialIconsPackage.packageName 136 | val combinedPackageName = "$iconsPackage.$themePackageName" 137 | return FileSpec.builder( 138 | packageName = combinedPackageName, 139 | fileName = iconName 140 | ) 141 | } 142 | 143 | private fun getBackingProperty(): PropertySpec { 144 | // Use a unique property name for the private backing property. This is because (as of 145 | // Kotlin 1.4) each property with the same name will be considered as a possible candidate 146 | // for resolution, regardless of the access modifier, so by using unique names we reduce 147 | // the size from ~6000 to 1, and speed up compilation time for these icons. 148 | val backingPropertyName = "_" + iconName.replaceFirstChar { it.lowercase(Locale.ROOT) } 149 | return backingProperty(name = backingPropertyName) 150 | } 151 | 152 | /** 153 | * @return the body of the getter for the icon property. This getter returns the backing 154 | * property if it is not null, otherwise creates the icon and 'caches' it in the backing 155 | * property, and then returns the backing property. 156 | */ 157 | private fun iconGetter( 158 | backingProperty: PropertySpec, 159 | iconName: String, 160 | iconTheme: IconTheme, 161 | autoMirror: Boolean 162 | ): FunSpec { 163 | return FunSpec.getterBuilder() 164 | .addCode( 165 | buildCodeBlock { 166 | beginControlFlow("if (%N != null)", backingProperty) 167 | addStatement("return %N!!", backingProperty) 168 | endControlFlow() 169 | } 170 | ) 171 | .addCode( 172 | buildCodeBlock { 173 | val controlFlow = if (autoMirror) { 174 | "%N = %M(name = \"$AutoMirroredName.%N.%N\", autoMirror = true)" 175 | } else { 176 | "%N = %M(name = \"%N.%N\")" 177 | } 178 | beginControlFlow( 179 | controlFlow, 180 | backingProperty, 181 | MemberNames.MaterialIcon, 182 | iconTheme.name, 183 | iconName 184 | ) 185 | vector.nodes.forEach { node -> addRecursively(node) } 186 | endControlFlow() 187 | } 188 | ) 189 | .addStatement("return %N!!", backingProperty) 190 | .build() 191 | } 192 | 193 | /** 194 | * @return The private backing property that is used to cache the ImageVector for a given 195 | * icon once created. 196 | * 197 | * @param name the name of this property 198 | */ 199 | private fun backingProperty(name: String): PropertySpec { 200 | val nullableImageVector = ClassNames.ImageVector.copy(nullable = true) 201 | return PropertySpec.builder(name = name, type = nullableImageVector) 202 | .mutable() 203 | .addModifiers(KModifier.PRIVATE) 204 | .initializer("null") 205 | .build() 206 | } 207 | } 208 | 209 | /** 210 | * Recursively adds function calls to construct the given [vectorNode] and its children. 211 | */ 212 | private fun CodeBlock.Builder.addRecursively(vectorNode: VectorNode) { 213 | when (vectorNode) { 214 | // TODO: b/147418351 - add clip-paths once they are supported 215 | is VectorNode.Group -> { 216 | beginControlFlow("%M", MemberNames.Group) 217 | vectorNode.paths.forEach { path -> 218 | addRecursively(path) 219 | } 220 | endControlFlow() 221 | } 222 | 223 | is VectorNode.Path -> { 224 | addPath(vectorNode) { 225 | vectorNode.nodes.forEach { pathNode -> 226 | addStatement(pathNode.asFunctionCall()) 227 | } 228 | } 229 | } 230 | } 231 | } 232 | 233 | /** 234 | * Adds a function call to create the given [path], with [pathBody] containing the commands for 235 | * the path. 236 | */ 237 | private fun CodeBlock.Builder.addPath( 238 | path: VectorNode.Path, 239 | pathBody: CodeBlock.Builder.() -> Unit 240 | ) { 241 | // Only set the fill type if it is EvenOdd - otherwise it will just be the default. 242 | val setFillType = path.fillType == FillType.EvenOdd 243 | 244 | val parameterList = with(path) { 245 | listOfNotNull( 246 | "fillAlpha = ${fillAlpha}f".takeIf { fillAlpha != 1f }, 247 | "strokeAlpha = ${strokeAlpha}f".takeIf { strokeAlpha != 1f }, 248 | "pathFillType = %M".takeIf { setFillType } 249 | ) 250 | } 251 | 252 | val parameters = if (parameterList.isNotEmpty()) { 253 | parameterList.joinToString(prefix = "(", postfix = ")") 254 | } else { 255 | "" 256 | } 257 | 258 | if (setFillType) { 259 | beginControlFlow("%M$parameters", MemberNames.MaterialPath, MemberNames.EvenOdd) 260 | } else { 261 | beginControlFlow("%M$parameters", MemberNames.MaterialPath) 262 | } 263 | pathBody() 264 | endControlFlow() 265 | } 266 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/KotlinPoetUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/KotlinPoetUtils.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml 21 | 22 | import com.squareup.kotlinpoet.FileSpec 23 | import java.io.File 24 | import java.nio.file.Files 25 | import java.text.SimpleDateFormat 26 | import java.util.Date 27 | 28 | /** 29 | * Writes the given [FileSpec] to [directory], appending a copyright notice to the beginning. 30 | * This is needed as this functionality isn't supported in KotlinPoet natively, and is not 31 | * intended to be supported. https://github.com/square/kotlinpoet/pull/514#issuecomment-441397363 32 | * 33 | * @param directory directory to write this [FileSpec] to 34 | * @param textTransform optional transformation to apply to the source file before writing to disk 35 | */ 36 | internal fun FileSpec.writeToWithCopyright(directory: File, textTransform: ((String) -> String)? = null) { 37 | var outputDirectory = directory 38 | 39 | if (packageName.isNotEmpty()) { 40 | for (packageComponent in packageName.split('.').dropLastWhile { it.isEmpty() }) { 41 | outputDirectory = outputDirectory.resolve(packageComponent) 42 | } 43 | } 44 | 45 | Files.createDirectories(outputDirectory.toPath()) 46 | 47 | val file = outputDirectory.resolve("$name.kt") 48 | 49 | // Write this FileSpec to a StringBuilder, so we can process the text before writing to file. 50 | val fileContent = StringBuilder().run { 51 | writeTo(this) 52 | toString() 53 | } 54 | 55 | val transformedText = textTransform?.invoke(fileContent) ?: fileContent 56 | 57 | file.writeText(copyright + "\n\n" + transformedText) 58 | } 59 | 60 | /** 61 | * Sets the indent for this [FileSpec] to match that of our code style. 62 | */ 63 | internal fun FileSpec.Builder.setIndent() = indent(Indent) 64 | 65 | // Code style indent is 4 spaces, compared to KotlinPoet's default of 2 66 | private val Indent = " ".repeat(4) 67 | 68 | /** 69 | * AOSP copyright notice. Given that we generate this code every build, it is never checked in, 70 | * so we should update the copyright with the current year every time we write to disk. 71 | */ 72 | private val copyright 73 | get() = """ 74 | /* 75 | * Copyright $currentYear The Android Open Source Project 76 | * 77 | * Licensed under the Apache License, Version 2.0 (the "License"); 78 | * you may not use this file except in compliance with the License. 79 | * You may obtain a copy of the License at 80 | * 81 | * http://www.apache.org/licenses/LICENSE-2.0 82 | * 83 | * Unless required by applicable law or agreed to in writing, software 84 | * distributed under the License is distributed on an "AS IS" BASIS, 85 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 86 | * See the License for the specific language governing permissions and 87 | * limitations under the License. 88 | */ 89 | """.trimIndent() 90 | 91 | private val currentYear: String get() = SimpleDateFormat("yyyy").format(Date()) 92 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/Names.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/Names.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml 21 | 22 | import com.squareup.kotlinpoet.ClassName 23 | import com.squareup.kotlinpoet.MemberName 24 | 25 | /** 26 | * Package names used for icon generation. 27 | */ 28 | internal enum class PackageNames(val packageName: String) { 29 | MaterialIconsPackage("androidx.compose.material.icons"), 30 | GraphicsPackage("androidx.compose.ui.graphics"), 31 | VectorPackage(GraphicsPackage.packageName + ".vector") 32 | } 33 | 34 | /** 35 | * [ClassName]s used for icon generation. 36 | */ 37 | internal object ClassNames { 38 | val Icons = PackageNames.MaterialIconsPackage.className("Icons") 39 | val ImageVector = PackageNames.VectorPackage.className("ImageVector") 40 | val PathFillType = PackageNames.GraphicsPackage.className("PathFillType", "Companion") 41 | } 42 | 43 | /** 44 | * [MemberName]s used for icon generation. 45 | */ 46 | internal object MemberNames { 47 | val MaterialIcon = MemberName(PackageNames.MaterialIconsPackage.packageName, "materialIcon") 48 | val MaterialPath = MemberName(PackageNames.MaterialIconsPackage.packageName, "materialPath") 49 | 50 | val EvenOdd = MemberName(ClassNames.PathFillType, "EvenOdd") 51 | val Group = MemberName(PackageNames.VectorPackage.packageName, "group") 52 | } 53 | 54 | /** 55 | * @return the [ClassName] of the given [classNames] inside this package. 56 | */ 57 | internal fun PackageNames.className(vararg classNames: String) = ClassName(this.packageName, *classNames) 58 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/vector/FillType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/vector/FillType.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml.vector 21 | 22 | /** 23 | * Determines the winding rule that decides how the interior of a [VectorNode.Path] is calculated. 24 | * 25 | * This maps to [android.graphics.Path.FillType] used in the framework, and can be defined in XML 26 | * via `android:fillType`. 27 | */ 28 | internal enum class FillType { 29 | NonZero, 30 | EvenOdd 31 | } 32 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/vector/PathNode.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/vector/PathNode.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml.vector 21 | 22 | /** 23 | * Class representing a singular path command in a vector. 24 | * 25 | * @property isCurve whether this command is a curve command 26 | * @property isQuad whether this command is a quad command 27 | */ 28 | /* ktlint-disable max-line-length */ 29 | internal sealed class PathNode(val isCurve: Boolean = false, val isQuad: Boolean = false) { 30 | /** 31 | * Maps a [PathNode] to a string representing an invocation of the corresponding PathBuilder 32 | * function to add this node to the builder. 33 | */ 34 | abstract fun asFunctionCall(): String 35 | 36 | // RelativeClose and Close are considered the same internally, so we represent both with Close 37 | // for simplicity and to make equals comparisons robust. 38 | object Close : PathNode() { 39 | override fun asFunctionCall() = "close()" 40 | } 41 | 42 | data class RelativeMoveTo(val x: Float, val y: Float) : PathNode() { 43 | override fun asFunctionCall() = "moveToRelative(${x}f, ${y}f)" 44 | } 45 | data class MoveTo(val x: Float, val y: Float) : PathNode() { 46 | override fun asFunctionCall() = "moveTo(${x}f, ${y}f)" 47 | } 48 | 49 | data class RelativeLineTo(val x: Float, val y: Float) : PathNode() { 50 | override fun asFunctionCall() = "lineToRelative(${x}f, ${y}f)" 51 | } 52 | data class LineTo(val x: Float, val y: Float) : PathNode() { 53 | override fun asFunctionCall() = "lineTo(${x}f, ${y}f)" 54 | } 55 | 56 | data class RelativeHorizontalTo(val x: Float) : PathNode() { 57 | override fun asFunctionCall() = "horizontalLineToRelative(${x}f)" 58 | } 59 | data class HorizontalTo(val x: Float) : PathNode() { 60 | override fun asFunctionCall() = "horizontalLineTo(${x}f)" 61 | } 62 | 63 | data class RelativeVerticalTo(val y: Float) : PathNode() { 64 | override fun asFunctionCall() = "verticalLineToRelative(${y}f)" 65 | } 66 | data class VerticalTo(val y: Float) : PathNode() { 67 | override fun asFunctionCall() = "verticalLineTo(${y}f)" 68 | } 69 | 70 | data class RelativeCurveTo( 71 | val dx1: Float, 72 | val dy1: Float, 73 | val dx2: Float, 74 | val dy2: Float, 75 | val dx3: Float, 76 | val dy3: Float 77 | ) : PathNode(isCurve = true) { 78 | override fun asFunctionCall() = "curveToRelative(${dx1}f, ${dy1}f, ${dx2}f, ${dy2}f, ${dx3}f, ${dy3}f)" 79 | } 80 | 81 | data class CurveTo( 82 | val x1: Float, 83 | val y1: Float, 84 | val x2: Float, 85 | val y2: Float, 86 | val x3: Float, 87 | val y3: Float 88 | ) : PathNode(isCurve = true) { 89 | override fun asFunctionCall() = "curveTo(${x1}f, ${y1}f, ${x2}f, ${y2}f, ${x3}f, ${y3}f)" 90 | } 91 | 92 | data class RelativeReflectiveCurveTo( 93 | val x1: Float, 94 | val y1: Float, 95 | val x2: Float, 96 | val y2: Float 97 | ) : PathNode(isCurve = true) { 98 | override fun asFunctionCall() = "reflectiveCurveToRelative(${x1}f, ${y1}f, ${x2}f, ${y2}f)" 99 | } 100 | 101 | data class ReflectiveCurveTo( 102 | val x1: Float, 103 | val y1: Float, 104 | val x2: Float, 105 | val y2: Float 106 | ) : PathNode(isCurve = true) { 107 | override fun asFunctionCall() = "reflectiveCurveTo(${x1}f, ${y1}f, ${x2}f, ${y2}f)" 108 | } 109 | 110 | data class RelativeQuadTo( 111 | val x1: Float, 112 | val y1: Float, 113 | val x2: Float, 114 | val y2: Float 115 | ) : PathNode(isQuad = true) { 116 | override fun asFunctionCall() = "quadToRelative(${x1}f, ${y1}f, ${x2}f, ${y2}f)" 117 | } 118 | 119 | data class QuadTo( 120 | val x1: Float, 121 | val y1: Float, 122 | val x2: Float, 123 | val y2: Float 124 | ) : PathNode(isQuad = true) { 125 | override fun asFunctionCall() = "quadTo(${x1}f, ${y1}f, ${x2}f, ${y2}f)" 126 | } 127 | 128 | data class RelativeReflectiveQuadTo( 129 | val x: Float, 130 | val y: Float 131 | ) : PathNode(isQuad = true) { 132 | override fun asFunctionCall() = "reflectiveQuadToRelative(${x}f, ${y}f)" 133 | } 134 | 135 | data class ReflectiveQuadTo( 136 | val x: Float, 137 | val y: Float 138 | ) : PathNode(isQuad = true) { 139 | override fun asFunctionCall() = "reflectiveQuadTo(${x}f, ${y}f)" 140 | } 141 | 142 | data class RelativeArcTo( 143 | val horizontalEllipseRadius: Float, 144 | val verticalEllipseRadius: Float, 145 | val theta: Float, 146 | val isMoreThanHalf: Boolean, 147 | val isPositiveArc: Boolean, 148 | val arcStartDx: Float, 149 | val arcStartDy: Float 150 | ) : PathNode() { 151 | override fun asFunctionCall() = "arcToRelative(${horizontalEllipseRadius}f, ${verticalEllipseRadius}f, ${theta}f, $isMoreThanHalf, $isPositiveArc, ${arcStartDx}f, ${arcStartDy}f)" 152 | } 153 | 154 | data class ArcTo( 155 | val horizontalEllipseRadius: Float, 156 | val verticalEllipseRadius: Float, 157 | val theta: Float, 158 | val isMoreThanHalf: Boolean, 159 | val isPositiveArc: Boolean, 160 | val arcStartX: Float, 161 | val arcStartY: Float 162 | ) : PathNode() { 163 | override fun asFunctionCall() = "arcTo(${horizontalEllipseRadius}f, ${verticalEllipseRadius}f, ${theta}f, $isMoreThanHalf, $isPositiveArc, ${arcStartX}f, ${arcStartY}f)" 164 | } 165 | } 166 | /* ktlint-enable max-line-length */ 167 | 168 | /** 169 | * Return the corresponding [PathNode] for the given character key if it exists. 170 | * If the key is unknown then [IllegalArgumentException] is thrown 171 | * @return [PathNode] that matches the key 172 | * @throws IllegalArgumentException 173 | */ 174 | internal fun Char.toPathNodes(args: FloatArray): List = when (this) { 175 | RelativeCloseKey, CloseKey -> listOf( 176 | PathNode.Close 177 | ) 178 | RelativeMoveToKey -> 179 | pathNodesFromArgs( 180 | args, 181 | NUM_MOVE_TO_ARGS 182 | ) { array -> 183 | PathNode.RelativeMoveTo( 184 | x = array[0], 185 | y = array[1] 186 | ) 187 | } 188 | 189 | MoveToKey -> 190 | pathNodesFromArgs( 191 | args, 192 | NUM_MOVE_TO_ARGS 193 | ) { array -> 194 | PathNode.MoveTo( 195 | x = array[0], 196 | y = array[1] 197 | ) 198 | } 199 | 200 | RelativeLineToKey -> 201 | pathNodesFromArgs( 202 | args, 203 | NUM_LINE_TO_ARGS 204 | ) { array -> 205 | PathNode.RelativeLineTo( 206 | x = array[0], 207 | y = array[1] 208 | ) 209 | } 210 | 211 | LineToKey -> 212 | pathNodesFromArgs( 213 | args, 214 | NUM_LINE_TO_ARGS 215 | ) { array -> 216 | PathNode.LineTo( 217 | x = array[0], 218 | y = array[1] 219 | ) 220 | } 221 | 222 | RelativeHorizontalToKey -> 223 | pathNodesFromArgs( 224 | args, 225 | NUM_HORIZONTAL_TO_ARGS 226 | ) { array -> 227 | PathNode.RelativeHorizontalTo( 228 | x = array[0] 229 | ) 230 | } 231 | 232 | HorizontalToKey -> 233 | pathNodesFromArgs( 234 | args, 235 | NUM_HORIZONTAL_TO_ARGS 236 | ) { array -> 237 | PathNode.HorizontalTo(x = array[0]) 238 | } 239 | 240 | RelativeVerticalToKey -> 241 | pathNodesFromArgs( 242 | args, 243 | NUM_VERTICAL_TO_ARGS 244 | ) { array -> 245 | PathNode.RelativeVerticalTo(y = array[0]) 246 | } 247 | 248 | VerticalToKey -> 249 | pathNodesFromArgs( 250 | args, 251 | NUM_VERTICAL_TO_ARGS 252 | ) { array -> 253 | PathNode.VerticalTo(y = array[0]) 254 | } 255 | 256 | RelativeCurveToKey -> 257 | pathNodesFromArgs( 258 | args, 259 | NUM_CURVE_TO_ARGS 260 | ) { array -> 261 | PathNode.RelativeCurveTo( 262 | dx1 = array[0], 263 | dy1 = array[1], 264 | dx2 = array[2], 265 | dy2 = array[3], 266 | dx3 = array[4], 267 | dy3 = array[5] 268 | ) 269 | } 270 | 271 | CurveToKey -> 272 | pathNodesFromArgs( 273 | args, 274 | NUM_CURVE_TO_ARGS 275 | ) { array -> 276 | PathNode.CurveTo( 277 | x1 = array[0], 278 | y1 = array[1], 279 | x2 = array[2], 280 | y2 = array[3], 281 | x3 = array[4], 282 | y3 = array[5] 283 | ) 284 | } 285 | 286 | RelativeReflectiveCurveToKey -> 287 | pathNodesFromArgs( 288 | args, 289 | NUM_REFLECTIVE_CURVE_TO_ARGS 290 | ) { array -> 291 | PathNode.RelativeReflectiveCurveTo( 292 | x1 = array[0], 293 | y1 = array[1], 294 | x2 = array[2], 295 | y2 = array[3] 296 | ) 297 | } 298 | 299 | ReflectiveCurveToKey -> 300 | pathNodesFromArgs( 301 | args, 302 | NUM_REFLECTIVE_CURVE_TO_ARGS 303 | ) { array -> 304 | PathNode.ReflectiveCurveTo( 305 | x1 = array[0], 306 | y1 = array[1], 307 | x2 = array[2], 308 | y2 = array[3] 309 | ) 310 | } 311 | 312 | RelativeQuadToKey -> 313 | pathNodesFromArgs( 314 | args, 315 | NUM_QUAD_TO_ARGS 316 | ) { array -> 317 | PathNode.RelativeQuadTo( 318 | x1 = array[0], 319 | y1 = array[1], 320 | x2 = array[2], 321 | y2 = array[3] 322 | ) 323 | } 324 | 325 | QuadToKey -> 326 | pathNodesFromArgs( 327 | args, 328 | NUM_QUAD_TO_ARGS 329 | ) { array -> 330 | PathNode.QuadTo( 331 | x1 = array[0], 332 | y1 = array[1], 333 | x2 = array[2], 334 | y2 = array[3] 335 | ) 336 | } 337 | 338 | RelativeReflectiveQuadToKey -> 339 | pathNodesFromArgs( 340 | args, 341 | NUM_REFLECTIVE_QUAD_TO_ARGS 342 | ) { array -> 343 | PathNode.RelativeReflectiveQuadTo( 344 | x = array[0], 345 | y = array[1] 346 | ) 347 | } 348 | 349 | ReflectiveQuadToKey -> 350 | pathNodesFromArgs( 351 | args, 352 | NUM_REFLECTIVE_QUAD_TO_ARGS 353 | ) { array -> 354 | PathNode.ReflectiveQuadTo( 355 | x = array[0], 356 | y = array[1] 357 | ) 358 | } 359 | 360 | RelativeArcToKey -> 361 | pathNodesFromArgs( 362 | args, 363 | NUM_ARC_TO_ARGS 364 | ) { array -> 365 | PathNode.RelativeArcTo( 366 | horizontalEllipseRadius = array[0], 367 | verticalEllipseRadius = array[1], 368 | theta = array[2], 369 | isMoreThanHalf = array[3].compareTo(0.0f) != 0, 370 | isPositiveArc = array[4].compareTo(0.0f) != 0, 371 | arcStartDx = array[5], 372 | arcStartDy = array[6] 373 | ) 374 | } 375 | 376 | ArcToKey -> 377 | pathNodesFromArgs( 378 | args, 379 | NUM_ARC_TO_ARGS 380 | ) { array -> 381 | PathNode.ArcTo( 382 | horizontalEllipseRadius = array[0], 383 | verticalEllipseRadius = array[1], 384 | theta = array[2], 385 | isMoreThanHalf = array[3].compareTo(0.0f) != 0, 386 | isPositiveArc = array[4].compareTo(0.0f) != 0, 387 | arcStartX = array[5], 388 | arcStartY = array[6] 389 | ) 390 | } 391 | 392 | else -> throw IllegalArgumentException("Unknown command for: $this") 393 | } 394 | 395 | private inline fun pathNodesFromArgs( 396 | args: FloatArray, 397 | numArgs: Int, 398 | nodeFor: (subArray: FloatArray) -> PathNode 399 | ): List { 400 | return (0..args.size - numArgs step numArgs).map { index -> 401 | val subArray = args.slice(index until index + numArgs).toFloatArray() 402 | val node = nodeFor(subArray) 403 | when { 404 | // According to the spec, if a MoveTo is followed by multiple pairs of coordinates, 405 | // the subsequent pairs are treated as implicit corresponding LineTo commands. 406 | node is PathNode.MoveTo && index > 0 -> PathNode.LineTo( 407 | subArray[0], 408 | subArray[1] 409 | ) 410 | node is PathNode.RelativeMoveTo && index > 0 -> 411 | PathNode.RelativeLineTo( 412 | subArray[0], 413 | subArray[1] 414 | ) 415 | else -> node 416 | } 417 | } 418 | } 419 | 420 | /** 421 | * Constants used by [Char.toPathNodes] for creating [PathNode]s from parsed paths. 422 | */ 423 | private const val RelativeCloseKey = 'z' 424 | private const val CloseKey = 'Z' 425 | private const val RelativeMoveToKey = 'm' 426 | private const val MoveToKey = 'M' 427 | private const val RelativeLineToKey = 'l' 428 | private const val LineToKey = 'L' 429 | private const val RelativeHorizontalToKey = 'h' 430 | private const val HorizontalToKey = 'H' 431 | private const val RelativeVerticalToKey = 'v' 432 | private const val VerticalToKey = 'V' 433 | private const val RelativeCurveToKey = 'c' 434 | private const val CurveToKey = 'C' 435 | private const val RelativeReflectiveCurveToKey = 's' 436 | private const val ReflectiveCurveToKey = 'S' 437 | private const val RelativeQuadToKey = 'q' 438 | private const val QuadToKey = 'Q' 439 | private const val RelativeReflectiveQuadToKey = 't' 440 | private const val ReflectiveQuadToKey = 'T' 441 | private const val RelativeArcToKey = 'a' 442 | private const val ArcToKey = 'A' 443 | 444 | /** 445 | * Constants for the number of expected arguments for a given node. If the number of received 446 | * arguments is a multiple of these, the excess will be converted into additional path nodes. 447 | */ 448 | private const val NUM_MOVE_TO_ARGS = 2 449 | private const val NUM_LINE_TO_ARGS = 2 450 | private const val NUM_HORIZONTAL_TO_ARGS = 1 451 | private const val NUM_VERTICAL_TO_ARGS = 1 452 | private const val NUM_CURVE_TO_ARGS = 6 453 | private const val NUM_REFLECTIVE_CURVE_TO_ARGS = 4 454 | private const val NUM_QUAD_TO_ARGS = 4 455 | private const val NUM_REFLECTIVE_QUAD_TO_ARGS = 2 456 | private const val NUM_ARC_TO_ARGS = 7 457 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/vector/PathParser.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/vector/PathParser.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml.vector 21 | 22 | import kotlin.math.min 23 | 24 | /** 25 | * Trimmed down copy of PathParser that doesn't handle interacting with Paths, and only is 26 | * responsible for parsing path strings. 27 | */ 28 | internal object PathParser { 29 | /** 30 | * Parses the path string to create a collection of PathNode instances with their corresponding 31 | * arguments 32 | * throws an IllegalArgumentException or NumberFormatException if the parameters are invalid 33 | */ 34 | fun parsePathString(pathData: String): List { 35 | val nodes = mutableListOf() 36 | 37 | fun addNode(cmd: Char, args: FloatArray) { 38 | nodes.addAll(cmd.toPathNodes(args)) 39 | } 40 | 41 | var start = 0 42 | var end = 1 43 | while (end < pathData.length) { 44 | end = nextStart(pathData, end) 45 | val s = pathData.substring(start, end).trim { it <= ' ' } 46 | if (s.isNotEmpty()) { 47 | val args = getFloats(s) 48 | addNode(s[0], args) 49 | } 50 | 51 | start = end 52 | end++ 53 | } 54 | if (end - start == 1 && start < pathData.length) { 55 | addNode(pathData[start], FloatArray(0)) 56 | } 57 | 58 | return nodes 59 | } 60 | 61 | private fun nextStart(s: String, end: Int): Int { 62 | var index = end 63 | var c: Char 64 | 65 | while (index < s.length) { 66 | c = s[index] 67 | // Note that 'e' or 'E' are not valid path commands, but could be 68 | // used for floating point numbers' scientific notation. 69 | // Therefore, when searching for next command, we should ignore 'e' 70 | // and 'E'. 71 | if (((c - 'A') * (c - 'Z') <= 0 || (c - 'a') * (c - 'z') <= 0) && 72 | c != 'e' && c != 'E' 73 | ) { 74 | return index 75 | } 76 | index++ 77 | } 78 | return index 79 | } 80 | 81 | @Throws(NumberFormatException::class) 82 | private fun getFloats(s: String): FloatArray { 83 | if (s[0] == 'z' || s[0] == 'Z') { 84 | return FloatArray(0) 85 | } 86 | val results = FloatArray(s.length) 87 | var count = 0 88 | var startPosition = 1 89 | var endPosition: Int 90 | 91 | val result = 92 | ExtractFloatResult() 93 | val totalLength = s.length 94 | 95 | // The startPosition should always be the first character of the 96 | // current number, and endPosition is the character after the current 97 | // number. 98 | while (startPosition < totalLength) { 99 | extract(s, startPosition, result) 100 | endPosition = result.endPosition 101 | 102 | if (startPosition < endPosition) { 103 | results[count++] = java.lang.Float.parseFloat( 104 | s.substring(startPosition, endPosition) 105 | ) 106 | } 107 | 108 | startPosition = if (result.endWithNegativeOrDot) { 109 | // Keep the '-' or '.' sign with next number. 110 | endPosition 111 | } else { 112 | endPosition + 1 113 | } 114 | } 115 | return copyOfRange(results, 0, count) 116 | } 117 | 118 | private fun copyOfRange(original: FloatArray, start: Int, end: Int): FloatArray { 119 | if (start > end) { 120 | throw IllegalArgumentException() 121 | } 122 | val originalLength = original.size 123 | if (start < 0 || start > originalLength) { 124 | throw ArrayIndexOutOfBoundsException() 125 | } 126 | val resultLength = end - start 127 | val copyLength = min(resultLength, originalLength - start) 128 | val result = FloatArray(resultLength) 129 | original.copyInto(result, 0, start, start + copyLength) 130 | return result 131 | } 132 | 133 | private fun extract(s: String, start: Int, result: ExtractFloatResult) { 134 | // Now looking for ' ', ',', '.' or '-' from the start. 135 | var currentIndex = start 136 | var foundSeparator = false 137 | result.endWithNegativeOrDot = false 138 | var secondDot = false 139 | var isExponential = false 140 | while (currentIndex < s.length) { 141 | val isPrevExponential = isExponential 142 | isExponential = false 143 | when (s[currentIndex]) { 144 | ' ', ',' -> foundSeparator = true 145 | '-' -> 146 | // The negative sign following a 'e' or 'E' is not a separator. 147 | if (currentIndex != start && !isPrevExponential) { 148 | foundSeparator = true 149 | result.endWithNegativeOrDot = true 150 | } 151 | '.' -> 152 | if (!secondDot) { 153 | secondDot = true 154 | } else { 155 | // This is the second dot, and it is considered as a separator. 156 | foundSeparator = true 157 | result.endWithNegativeOrDot = true 158 | } 159 | 'e', 'E' -> isExponential = true 160 | } 161 | if (foundSeparator) { 162 | break 163 | } 164 | currentIndex++ 165 | } 166 | // When there is nothing found, then we put the end position to the end 167 | // of the string. 168 | result.endPosition = currentIndex 169 | } 170 | 171 | private data class ExtractFloatResult( 172 | // We need to return the position of the next separator and whether the 173 | // next float starts with a '-' or a '.'. 174 | var endPosition: Int = 0, 175 | var endWithNegativeOrDot: Boolean = false 176 | ) 177 | } 178 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/icons/xml/vector/Vector.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Forked from: 3 | * https://android.googlesource.com/platform//frameworks/support/+/7b4652f32f5867e5bbe53dcb20ec95c52f3fe979/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/vector/Vector.kt 4 | * 5 | * Copyright 2020 The Android Open Source Project 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | package io.github.irgaly.compose.icons.xml.vector 21 | 22 | /** 23 | * Simplified representation of a vector, with root [nodes]. 24 | * 25 | * @param autoMirrored a boolean that indicates if this Vector can be auto-mirrored on left to right 26 | * locales 27 | * @param nodes may either be a singleton list of the root group, or a list of root paths / groups 28 | * if there are multiple top level declaration 29 | */ 30 | internal class Vector(val autoMirrored: Boolean, val nodes: List) 31 | 32 | /** 33 | * Simplified vector node representation, as the total set of properties we need to care about 34 | * for Material icons is very limited. 35 | */ 36 | internal sealed class VectorNode { 37 | class Group(val paths: MutableList = mutableListOf()) : VectorNode() 38 | class Path( 39 | val strokeAlpha: Float, 40 | val fillAlpha: Float, 41 | val fillType: FillType, 42 | val nodes: List 43 | ) : VectorNode() 44 | } 45 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/vector/Names.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.MemberName 5 | 6 | internal object PackageNames { 7 | val Runtime = "androidx.compose.runtime" 8 | val AndroidPreview = "androidx.compose.ui.tooling.preview" 9 | val JetbrainsPreview = "org.jetbrains.compose.ui.tooling.preview" 10 | val DesktopPreview = "androidx.compose.desktop.ui.tooling.preview" 11 | val Foundation = "androidx.compose.foundation" 12 | val Graphics = "androidx.compose.ui.graphics" 13 | val Vector = "androidx.compose.ui.graphics.vector" 14 | val Unit = "androidx.compose.ui.unit" 15 | val Geometory = "androidx.compose.ui.geometry" 16 | } 17 | 18 | internal object ClassNames { 19 | val Composable = ClassName(PackageNames.Runtime, "Composable") 20 | val AndroidPreview = ClassName(PackageNames.AndroidPreview, "Preview") 21 | val JetbrainsPreview = ClassName(PackageNames.JetbrainsPreview, "Preview") 22 | val DesktopPreview = ClassName(PackageNames.DesktopPreview, "Preview") 23 | val ImageVector = ClassName(PackageNames.Vector, "ImageVector") 24 | val PathFillType = ClassName(PackageNames.Graphics, "PathFillType") 25 | val BrushCompanion = ClassName(PackageNames.Graphics, "Brush", "Companion") 26 | val StrokeCap = ClassName(PackageNames.Graphics, "StrokeCap") 27 | val StrokeJoin = ClassName(PackageNames.Graphics, "StrokeJoin") 28 | } 29 | 30 | internal object MemberNames { 31 | val Image = MemberName(PackageNames.Foundation, "Image") 32 | val Dp = MemberName(PackageNames.Unit, "dp") 33 | val Color = MemberName(PackageNames.Graphics, "Color") 34 | val SolidColor = MemberName(PackageNames.Graphics, "SolidColor") 35 | val TileMode = MemberName(PackageNames.Graphics, "TileMode") 36 | val PathFillType = MemberName(ClassNames.PathFillType.packageName, ClassNames.PathFillType.simpleName) 37 | val StrokeCap = MemberName(ClassNames.StrokeCap.packageName, ClassNames.StrokeCap.simpleName) 38 | val StrokeJoin = MemberName(ClassNames.StrokeJoin.packageName, ClassNames.StrokeJoin.simpleName) 39 | val Offset = MemberName(PackageNames.Geometory, "Offset") 40 | 41 | internal object ImageVector { 42 | val Builder = MemberName(ClassNames.ImageVector, "Builder") 43 | } 44 | 45 | internal object Vector { 46 | val PathData = MemberName(PackageNames.Vector, "PathData") 47 | val Group = MemberName(PackageNames.Vector, "group") 48 | val Path = MemberName(PackageNames.Vector, "path") 49 | } 50 | 51 | internal object Brush { 52 | val LinearGradient = MemberName(ClassNames.BrushCompanion, "linearGradient") 53 | val RadialGradient = MemberName(ClassNames.BrushCompanion, "radialGradient") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /plugin/core/src/main/kotlin/io/github/irgaly/compose/vector/node/ImageVector.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.node 2 | 3 | /** 4 | * ImageVector Node 5 | */ 6 | data class ImageVector( 7 | val name: String, 8 | /** 9 | * dp 10 | */ 11 | val defaultWidth: Double, 12 | /** 13 | * dp 14 | */ 15 | val defaultHeight: Double, 16 | val viewportWidth: Float, 17 | val viewportHeight: Float, 18 | val autoMirror: Boolean, 19 | val rootGroup: VectorNode.VectorGroup, 20 | ) { 21 | sealed interface VectorNode { 22 | data class VectorGroup( 23 | val nodes: List, 24 | val name: String? = null, 25 | val rotate: Float? = null, 26 | val pivotX: Float? = null, 27 | val pivotY: Float? = null, 28 | val scaleX: Float? = null, 29 | val scaleY: Float? = null, 30 | val translationX: Float? = null, 31 | val translationY: Float? = null, 32 | val currentTransformationMatrix: Matrix = Matrix(1f, 0f, 0f, 1f, 0f, 0f), 33 | val clipPathData: List = emptyList(), 34 | val extra: Extra? = null, 35 | val referencedExtra: Extra? = null, 36 | ) : VectorNode { 37 | data class Extra( 38 | val id: String, 39 | val pathFillType: PathFillType? = null, 40 | val fill: Brush? = null, 41 | val fillAlpha: Float? = null, 42 | val stroke: Brush? = null, 43 | val strokeAlpha: Float? = null, 44 | val strokeLineWidth: Float? = null, 45 | val strokeLineCap: StrokeCap? = null, 46 | val strokeLineJoin: StrokeJoin? = null, 47 | val strokeLineMiter: Float? = null, 48 | ) 49 | } 50 | 51 | data class VectorPath( 52 | val pathData: List, 53 | val pathFillType: PathFillType? = null, 54 | val name: String? = null, 55 | val fill: Brush? = null, 56 | val fillAlpha: Float? = null, 57 | val stroke: Brush? = null, 58 | val strokeAlpha: Float? = null, 59 | val strokeLineWidth: Float? = null, 60 | val strokeLineCap: StrokeCap? = null, 61 | val strokeLineJoin: StrokeJoin? = null, 62 | val strokeLineMiter: Float? = null, 63 | val trimPathStart: Float? = null, 64 | val trimPathEnd: Float? = null, 65 | val trimPathOffset: Float? = null, 66 | val extraReference: ExtraReference? = null 67 | ) : VectorNode { 68 | data class ExtraReference( 69 | val pathFillTypeId: String? = null, 70 | val fillId: String? = null, 71 | val fillAlphaId: String? = null, 72 | val strokeId: String? = null, 73 | val strokeAlphaId: String? = null, 74 | val strokeLineWidthId: String? = null, 75 | val strokeLineCapId: String? = null, 76 | val strokeLineJoinId: String? = null, 77 | val strokeLineMiterId: String? = null, 78 | ) 79 | } 80 | } 81 | 82 | enum class PathFillType { 83 | EvenOdd, NonZero 84 | } 85 | 86 | sealed interface Brush { 87 | data class SolidColor( 88 | val color: Color, 89 | ) : Brush 90 | data class LinearGradient( 91 | val colorStops: List>, 92 | val start: Pair, 93 | val end: Pair, 94 | val tileMode: TileMode, 95 | ) : Brush 96 | 97 | data class RadialGradient( 98 | val colorStops: List>, 99 | val center: Pair, 100 | val radius: Float, 101 | val tileMode: TileMode, 102 | ) : Brush 103 | } 104 | 105 | enum class StrokeCap { 106 | Butt, Round, Square 107 | } 108 | 109 | enum class StrokeJoin { 110 | Bevel, Miter, Round 111 | } 112 | 113 | enum class TileMode { 114 | Clamp, Decal, Mirror, Repeated 115 | } 116 | 117 | sealed interface Color 118 | data class RgbColor( 119 | val red: Int, 120 | val green: Int, 121 | val blue: Int, 122 | val alpha: Int = 0xFF, 123 | ) : Color { 124 | fun teHexString(prefix: String = ""): String { 125 | return "%s%02X%02X%02X%02X".format(prefix, alpha, red, green, blue) 126 | } 127 | } 128 | 129 | data class ComposeColor( 130 | val name: String 131 | ) : Color 132 | sealed interface PathNode { 133 | data class ArcTo( 134 | val horizontalEllipseRadius: Float, 135 | val verticalEllipseRadius: Float, 136 | val theta: Float, 137 | val isMoreThanHalf: Boolean, 138 | val isPositiveArc: Boolean, 139 | val arcStartX: Float, 140 | val arcStartY: Float, 141 | ) : PathNode 142 | 143 | data object Close : PathNode 144 | data class CurveTo( 145 | val x1: Float, 146 | val y1: Float, 147 | val x2: Float, 148 | val y2: Float, 149 | val x3: Float, 150 | val y3: Float, 151 | ) : PathNode 152 | 153 | data class HorizontalTo( 154 | val x: Float, 155 | ) : PathNode 156 | 157 | data class LineTo( 158 | val x: Float, 159 | val y: Float, 160 | ) : PathNode 161 | 162 | data class MoveTo( 163 | val x: Float, 164 | val y: Float, 165 | ) : PathNode 166 | 167 | data class QuadTo( 168 | val x1: Float, 169 | val y1: Float, 170 | val x2: Float, 171 | val y2: Float, 172 | ) : PathNode 173 | 174 | data class ReflectiveCurveTo( 175 | val x1: Float, 176 | val y1: Float, 177 | val x2: Float, 178 | val y2: Float, 179 | ) : PathNode 180 | 181 | data class ReflectiveQuadTo( 182 | val x: Float, 183 | val y: Float, 184 | ) : PathNode 185 | 186 | data class RelativeArcTo( 187 | val horizontalEllipseRadius: Float, 188 | val verticalEllipseRadius: Float, 189 | val theta: Float, 190 | val isMoreThanHalf: Boolean, 191 | val isPositiveArc: Boolean, 192 | val arcStartDx: Float, 193 | val arcStartDy: Float, 194 | ) : PathNode 195 | 196 | data class RelativeCurveTo( 197 | val dx1: Float, 198 | val dy1: Float, 199 | val dx2: Float, 200 | val dy2: Float, 201 | val dx3: Float, 202 | val dy3: Float, 203 | ) : PathNode 204 | 205 | data class RelativeHorizontalTo( 206 | val dx: Float, 207 | ) : PathNode 208 | 209 | data class RelativeLineTo( 210 | val dx: Float, 211 | val dy: Float, 212 | ) : PathNode 213 | 214 | data class RelativeMoveTo( 215 | val dx: Float, 216 | val dy: Float, 217 | ) : PathNode 218 | 219 | data class RelativeQuadTo( 220 | val dx1: Float, 221 | val dy1: Float, 222 | val dx2: Float, 223 | val dy2: Float, 224 | ) : PathNode 225 | 226 | data class RelativeReflectiveCurveTo( 227 | val dx1: Float, 228 | val dy1: Float, 229 | val dx2: Float, 230 | val dy2: Float, 231 | ) : PathNode 232 | 233 | data class RelativeReflectiveQuadTo( 234 | val dx: Float, 235 | val dy: Float, 236 | ) : PathNode 237 | 238 | data class RelativeVerticalTo( 239 | val dy: Float, 240 | ) : PathNode 241 | 242 | data class VerticalTo( 243 | val y: Float, 244 | ) : PathNode 245 | } 246 | data class Matrix( 247 | val a: Float, 248 | val b: Float, 249 | val c: Float, 250 | val d: Float, 251 | val e: Float, 252 | val f: Float, 253 | ) { 254 | companion object { 255 | val identityMatrix = Matrix( 256 | a = 1f, 257 | b = 0f, 258 | c = 0f, 259 | d = 1f, 260 | e = 0f, 261 | f = 0f, 262 | ) 263 | } 264 | 265 | override fun toString(): String { 266 | return "[$a, $b, $c, $d, $e, $f]" 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /plugin/core/src/test/kotlin/io/github/irgaly/compose/vector/ConverterSpec.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector 2 | 3 | import io.github.irgaly.compose.Logger 4 | import io.github.irgaly.compose.vector.svg.SvgParser 5 | import io.kotest.core.spec.style.DescribeSpec 6 | import io.kotest.matchers.shouldBe 7 | import java.io.File 8 | 9 | class ConverterSpec: DescribeSpec({ 10 | val isCi = (System.getenv("CI") != null) 11 | val parser = SvgParser(object : Logger { 12 | override fun debug(message: String) { 13 | println("debug: $message") 14 | } 15 | 16 | override fun info(message: String) { 17 | println("info: $message") 18 | } 19 | 20 | override fun warn(message: String, error: Exception?) { 21 | println("warn: $message | $error") 22 | } 23 | 24 | override fun error(message: String, error: Exception?) { 25 | println("error: $message | $error") 26 | } 27 | }) 28 | val generator = ImageVectorGenerator() 29 | describe("SVG file should be exported as expected codes") { 30 | val resources = File("src/test/resources") 31 | resources.listFiles()?.sorted()?.filter { 32 | it.extension == "svg" 33 | }?.forEach { svgFile -> 34 | it(svgFile.name) { 35 | val imageVector = parser.parse( 36 | input = svgFile.inputStream(), 37 | name = svgFile.nameWithoutExtension 38 | ) 39 | val actualCodes = generator.generate( 40 | imageVector = imageVector, 41 | destinationPackage = "io.github.irgaly.compose.vector.test.image", 42 | receiverClasses = emptyList(), 43 | extensionPackage = "io.github.irgaly.compose.vector.test.image", 44 | hasAndroidPreview = true 45 | ) 46 | val resultFile = resources.resolve("${svgFile.nameWithoutExtension}.kt") 47 | if (!isCi) { 48 | if (!resultFile.exists()) { 49 | // First time, create new result file 50 | resultFile.writeText(actualCodes) 51 | } 52 | val previewDirectory = File("../../sample/android/build/test") 53 | if (!previewDirectory.exists()) { 54 | previewDirectory.mkdirs() 55 | } 56 | val previewFile = previewDirectory.resolve("${svgFile.nameWithoutExtension}.kt") 57 | if (!previewFile.exists() || (previewFile.readText() != actualCodes)) { 58 | previewFile.writeText(actualCodes) 59 | } 60 | } 61 | val expectCodes = resultFile.readText() 62 | actualCodes shouldBe expectCodes 63 | } 64 | } 65 | } 66 | }) 67 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/path.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.test.image 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.SolidColor 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import androidx.compose.ui.unit.dp 12 | import kotlin.Suppress 13 | 14 | @Suppress("RedundantVisibilityModifier") 15 | public val path: ImageVector 16 | get() { 17 | if (_path != null) { 18 | return _path!! 19 | } 20 | _path = Builder("path", 300.dp, 400.dp, 300f, 400f).apply { 21 | path(stroke = SolidColor(Color.Blue), strokeLineWidth = 20f) { 22 | moveTo(0f, 0f) 23 | lineTo(100f, 250f) 24 | lineTo(200f, 0f) 25 | } 26 | }.build() 27 | return _path!! 28 | } 29 | 30 | private var _path: ImageVector? = null 31 | 32 | @Preview 33 | @Composable 34 | private fun pathPreview() { 35 | Image(path, null) 36 | } 37 | 38 | @Preview(showBackground = true) 39 | @Composable 40 | private fun pathBackgroundPreview() { 41 | Image(path, null) 42 | } 43 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/path.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_nest_viewbox.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.test.image 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.SolidColor 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 9 | import androidx.compose.ui.graphics.vector.PathData 10 | import androidx.compose.ui.graphics.vector.group 11 | import androidx.compose.ui.graphics.vector.path 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import androidx.compose.ui.unit.dp 14 | import kotlin.Suppress 15 | 16 | @Suppress("RedundantVisibilityModifier") 17 | public val svg_nest_viewbox: ImageVector 18 | get() { 19 | if (_svg_nest_viewbox != null) { 20 | return _svg_nest_viewbox!! 21 | } 22 | _svg_nest_viewbox = Builder("svg_nest_viewbox", 500.dp, 500.dp, 500f, 500f).apply { 23 | val fill0 = SolidColor(Color(0xFF000000)) 24 | val strokeLineWidth0 = 1f 25 | path(fill = fill0, stroke = SolidColor(Color.Blue), strokeLineWidth = strokeLineWidth0) { 26 | moveTo(0f, 0f) 27 | lineTo(500f, 0f) 28 | lineTo(500f, 500f) 29 | lineTo(0f, 500f) 30 | lineTo(0f, 0f) 31 | close() 32 | } 33 | group(clipPathData = PathData { 34 | moveTo(100f, 100f) 35 | lineTo(200f, 100f) 36 | lineTo(200f, 200f) 37 | lineTo(100f, 200f) 38 | lineTo(100f, 100f) 39 | close() 40 | }) { 41 | path(fill = fill0, stroke = SolidColor(Color.Red), strokeLineWidth = strokeLineWidth0) { 42 | moveTo(100f, 100f) 43 | lineTo(300f, 100f) 44 | lineTo(300f, 200f) 45 | lineTo(100f, 200f) 46 | lineTo(100f, 100f) 47 | close() 48 | } 49 | } 50 | }.build() 51 | return _svg_nest_viewbox!! 52 | } 53 | 54 | private var _svg_nest_viewbox: ImageVector? = null 55 | 56 | @Preview 57 | @Composable 58 | private fun svg_nest_viewboxPreview() { 59 | Image(svg_nest_viewbox, null) 60 | } 61 | 62 | @Preview(showBackground = true) 63 | @Composable 64 | private fun svg_nest_viewboxBackgroundPreview() { 65 | Image(svg_nest_viewbox, null) 66 | } 67 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_nest_viewbox.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_no_size.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.test.image 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.SolidColor 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import androidx.compose.ui.unit.dp 12 | import kotlin.Suppress 13 | 14 | @Suppress("RedundantVisibilityModifier") 15 | public val svg_no_size: ImageVector 16 | get() { 17 | if (_svg_no_size != null) { 18 | return _svg_no_size!! 19 | } 20 | _svg_no_size = Builder("svg_no_size", 500.dp, 500.dp, 500f, 500f).apply { 21 | val fill0 = SolidColor(Color(0xFF000000)) 22 | val strokeLineWidth0 = 1f 23 | path(fill = fill0, stroke = SolidColor(Color.Blue), strokeLineWidth = strokeLineWidth0) { 24 | moveTo(200f, 100f) 25 | curveTo(200f, 155.2285f, 155.2285f, 200f, 100f, 200f) 26 | curveTo(44.7715f, 200f, 0f, 155.2285f, 0f, 100f) 27 | curveTo(0f, 44.7715f, 44.7715f, 0f, 100f, 0f) 28 | curveTo(155.2285f, 0f, 200f, 44.7715f, 200f, 100f) 29 | close() 30 | } 31 | }.build() 32 | return _svg_no_size!! 33 | } 34 | 35 | private var _svg_no_size: ImageVector? = null 36 | 37 | @Preview 38 | @Composable 39 | private fun svg_no_sizePreview() { 40 | Image(svg_no_size, null) 41 | } 42 | 43 | @Preview(showBackground = true) 44 | @Composable 45 | private fun svg_no_sizeBackgroundPreview() { 46 | Image(svg_no_size, null) 47 | } 48 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_no_size.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_no_size_no_viewbox.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.test.image 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.SolidColor 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import androidx.compose.ui.unit.dp 12 | import kotlin.Suppress 13 | 14 | @Suppress("RedundantVisibilityModifier") 15 | public val svg_no_size_no_viewbox: ImageVector 16 | get() { 17 | if (_svg_no_size_no_viewbox != null) { 18 | return _svg_no_size_no_viewbox!! 19 | } 20 | _svg_no_size_no_viewbox = Builder("svg_no_size_no_viewbox", 300.dp, 150.dp, 300f, 150f).apply { 21 | val fill0 = SolidColor(Color(0xFF000000)) 22 | val strokeLineWidth0 = 1f 23 | path(fill = fill0, stroke = SolidColor(Color.Blue), strokeLineWidth = strokeLineWidth0) { 24 | moveTo(200f, 100f) 25 | curveTo(200f, 155.2285f, 155.2285f, 200f, 100f, 200f) 26 | curveTo(44.7715f, 200f, 0f, 155.2285f, 0f, 100f) 27 | curveTo(0f, 44.7715f, 44.7715f, 0f, 100f, 0f) 28 | curveTo(155.2285f, 0f, 200f, 44.7715f, 200f, 100f) 29 | close() 30 | } 31 | }.build() 32 | return _svg_no_size_no_viewbox!! 33 | } 34 | 35 | private var _svg_no_size_no_viewbox: ImageVector? = null 36 | 37 | @Preview 38 | @Composable 39 | private fun svg_no_size_no_viewboxPreview() { 40 | Image(svg_no_size_no_viewbox, null) 41 | } 42 | 43 | @Preview(showBackground = true) 44 | @Composable 45 | private fun svg_no_size_no_viewboxBackgroundPreview() { 46 | Image(svg_no_size_no_viewbox, null) 47 | } 48 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_no_size_no_viewbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_no_viewbox.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.test.image 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.SolidColor 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import androidx.compose.ui.unit.dp 12 | import kotlin.Suppress 13 | 14 | @Suppress("RedundantVisibilityModifier") 15 | public val svg_no_viewbox: ImageVector 16 | get() { 17 | if (_svg_no_viewbox != null) { 18 | return _svg_no_viewbox!! 19 | } 20 | _svg_no_viewbox = Builder("svg_no_viewbox", 500.dp, 500.dp, 500f, 500f).apply { 21 | val fill0 = SolidColor(Color(0xFF000000)) 22 | val strokeLineWidth0 = 1f 23 | path(fill = fill0, stroke = SolidColor(Color.Blue), strokeLineWidth = strokeLineWidth0) { 24 | moveTo(200f, 100f) 25 | curveTo(200f, 155.2285f, 155.2285f, 200f, 100f, 200f) 26 | curveTo(44.7715f, 200f, 0f, 155.2285f, 0f, 100f) 27 | curveTo(0f, 44.7715f, 44.7715f, 0f, 100f, 0f) 28 | curveTo(155.2285f, 0f, 200f, 44.7715f, 200f, 100f) 29 | close() 30 | } 31 | }.build() 32 | return _svg_no_viewbox!! 33 | } 34 | 35 | private var _svg_no_viewbox: ImageVector? = null 36 | 37 | @Preview 38 | @Composable 39 | private fun svg_no_viewboxPreview() { 40 | Image(svg_no_viewbox, null) 41 | } 42 | 43 | @Preview(showBackground = true) 44 | @Composable 45 | private fun svg_no_viewboxBackgroundPreview() { 46 | Image(svg_no_viewbox, null) 47 | } 48 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_no_viewbox.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_over_viewbox.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.test.image 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.SolidColor 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import androidx.compose.ui.unit.dp 12 | import kotlin.Suppress 13 | 14 | @Suppress("RedundantVisibilityModifier") 15 | public val svg_over_viewbox: ImageVector 16 | get() { 17 | if (_svg_over_viewbox != null) { 18 | return _svg_over_viewbox!! 19 | } 20 | _svg_over_viewbox = Builder("svg_over_viewbox", 100.dp, 100.dp, 100f, 100f).apply { 21 | val fill0 = SolidColor(Color(0xFF000000)) 22 | val strokeLineWidth0 = 1f 23 | path(fill = fill0, stroke = SolidColor(Color.Red), strokeLineWidth = strokeLineWidth0) { 24 | moveTo(0f, 0f) 25 | lineTo(200f, 0f) 26 | lineTo(200f, 100f) 27 | lineTo(0f, 100f) 28 | lineTo(0f, 0f) 29 | close() 30 | } 31 | }.build() 32 | return _svg_over_viewbox!! 33 | } 34 | 35 | private var _svg_over_viewbox: ImageVector? = null 36 | 37 | @Preview 38 | @Composable 39 | private fun svg_over_viewboxPreview() { 40 | Image(svg_over_viewbox, null) 41 | } 42 | 43 | @Preview(showBackground = true) 44 | @Composable 45 | private fun svg_over_viewboxBackgroundPreview() { 46 | Image(svg_over_viewbox, null) 47 | } 48 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_over_viewbox.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_symbol.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.test.image 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.SolidColor 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 9 | import androidx.compose.ui.graphics.vector.PathData 10 | import androidx.compose.ui.graphics.vector.group 11 | import androidx.compose.ui.graphics.vector.path 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import androidx.compose.ui.unit.dp 14 | import kotlin.Suppress 15 | 16 | @Suppress("RedundantVisibilityModifier") 17 | public val svg_symbol: ImageVector 18 | get() { 19 | if (_svg_symbol != null) { 20 | return _svg_symbol!! 21 | } 22 | _svg_symbol = Builder("svg_symbol", 500.dp, 500.dp, 500f, 500f).apply { 23 | val fill0 = SolidColor(Color(0xFF000000)) 24 | val strokeLineWidth0 = 1f 25 | path(fill = fill0, stroke = SolidColor(Color.Blue), strokeLineWidth = strokeLineWidth0) { 26 | moveTo(0f, 0f) 27 | lineTo(500f, 0f) 28 | lineTo(500f, 500f) 29 | lineTo(0f, 500f) 30 | lineTo(0f, 0f) 31 | close() 32 | } 33 | group(name = "symbol", clipPathData = PathData { 34 | moveTo(50f, 50f) 35 | lineTo(150f, 50f) 36 | lineTo(150f, 150f) 37 | lineTo(50f, 150f) 38 | lineTo(50f, 50f) 39 | close() 40 | }) { 41 | path(fill = fill0, stroke = SolidColor(Color.Red), strokeLineWidth = strokeLineWidth0) { 42 | moveTo(50f, 50f) 43 | lineTo(100f, 50f) 44 | lineTo(100f, 100f) 45 | lineTo(50f, 100f) 46 | lineTo(50f, 50f) 47 | close() 48 | } 49 | } 50 | }.build() 51 | return _svg_symbol!! 52 | } 53 | 54 | private var _svg_symbol: ImageVector? = null 55 | 56 | @Preview 57 | @Composable 58 | private fun svg_symbolPreview() { 59 | Image(svg_symbol, null) 60 | } 61 | 62 | @Preview(showBackground = true) 63 | @Composable 64 | private fun svg_symbolBackgroundPreview() { 65 | Image(svg_symbol, null) 66 | } 67 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/svg_symbol.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/transform_circle.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.test.image 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.SolidColor 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 9 | import androidx.compose.ui.graphics.vector.path 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import androidx.compose.ui.unit.dp 12 | import kotlin.Suppress 13 | 14 | @Suppress("RedundantVisibilityModifier") 15 | public val transform_circle: ImageVector 16 | get() { 17 | if (_transform_circle != null) { 18 | return _transform_circle!! 19 | } 20 | _transform_circle = Builder("transform_circle", 500.dp, 500.dp, 500f, 500f).apply { 21 | val fill0 = SolidColor(Color(0xFF000000)) 22 | val strokeLineWidth0 = 1f 23 | path(fill = fill0, stroke = SolidColor(Color.Red), strokeLineWidth = strokeLineWidth0) { 24 | moveTo(0f, 0f) 25 | lineTo(500f, 0f) 26 | lineTo(500f, 500f) 27 | lineTo(0f, 500f) 28 | lineTo(0f, 0f) 29 | close() 30 | } 31 | path(fill = SolidColor(Color.White), stroke = SolidColor(Color.Blue), strokeLineWidth = strokeLineWidth0) { 32 | moveTo(283.91f, 100f) 33 | curveTo(330.2522f, 155.2285f, 323.0484f, 200f, 267.8199f, 200f) 34 | curveTo(212.5914f, 200f, 130.2522f, 155.2285f, 83.91f, 100f) 35 | curveTo(37.5678f, 44.7715f, 44.7715f, 0f, 100f, 0f) 36 | curveTo(155.2285f, 0f, 237.5678f, 44.7715f, 283.91f, 100f) 37 | close() 38 | } 39 | }.build() 40 | return _transform_circle!! 41 | } 42 | 43 | private var _transform_circle: ImageVector? = null 44 | 45 | @Preview 46 | @Composable 47 | private fun transform_circlePreview() { 48 | Image(transform_circle, null) 49 | } 50 | 51 | @Preview(showBackground = true) 52 | @Composable 53 | private fun transform_circleBackgroundPreview() { 54 | Image(transform_circle, null) 55 | } 56 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/transform_circle.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/transform_path.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.test.image 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.graphics.Color 6 | import androidx.compose.ui.graphics.SolidColor 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import androidx.compose.ui.graphics.vector.ImageVector.Builder 9 | import androidx.compose.ui.graphics.vector.group 10 | import androidx.compose.ui.graphics.vector.path 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | import kotlin.Suppress 14 | 15 | @Suppress("RedundantVisibilityModifier") 16 | public val transform_path: ImageVector 17 | get() { 18 | if (_transform_path != null) { 19 | return _transform_path!! 20 | } 21 | _transform_path = Builder("transform_path", 150.dp, 100.dp, 150f, 100f).apply { 22 | group(translationX = 40f) { 23 | val strokeLineWidth0 = 1f 24 | path(fill = SolidColor(Color.White), strokeLineWidth = strokeLineWidth0) { 25 | moveTo(-40f, 0f) 26 | lineTo(110f, 0f) 27 | lineTo(110f, 100f) 28 | lineTo(-40f, 100f) 29 | lineTo(-40f, 0f) 30 | close() 31 | } 32 | group { 33 | val fill1 = SolidColor(Color(0xFF808080)) 34 | path(name = "heart", fill = fill1, strokeLineWidth = strokeLineWidth0) { 35 | moveTo(-19.3092f, 72.1117f) 36 | curveTo(-24.7661f, 67.4836f, -20.412f, 62.1972f, -9.5684f, 60.2852f) 37 | curveTo(1.2752f, 58.3732f, 14.5292f, 60.5548f, 20.0831f, 65.1658f) 38 | curveTo(14.6262f, 60.5377f, 18.9803f, 55.2513f, 29.8239f, 53.3393f) 39 | curveTo(40.6675f, 51.4273f, 53.9215f, 53.6089f, 59.4754f, 58.2199f) 40 | quadTo(74.4754f, 70.8064f, 50.0831f, 90.3388f) 41 | quadTo(-4.3092f, 84.6982f, -19.3092f, 72.1117f) 42 | close() 43 | } 44 | } 45 | group { 46 | val stroke2 = SolidColor(Color.Red) 47 | path(name = "heart", stroke = stroke2, strokeLineWidth = strokeLineWidth0) { 48 | moveTo(10f, 30f) 49 | arcTo(20f, 20f, 0f, false, true, 50f, 30f) 50 | arcTo(20f, 20f, 0f, false, true, 90f, 30f) 51 | quadTo(90f, 60f, 50f, 90f) 52 | quadTo(10f, 60f, 10f, 30f) 53 | close() 54 | } 55 | } 56 | } 57 | }.build() 58 | return _transform_path!! 59 | } 60 | 61 | private var _transform_path: ImageVector? = null 62 | 63 | @Preview 64 | @Composable 65 | private fun transform_pathPreview() { 66 | Image(transform_path, null) 67 | } 68 | 69 | @Preview(showBackground = true) 70 | @Composable 71 | private fun transform_pathBackgroundPreview() { 72 | Image(transform_path, null) 73 | } 74 | -------------------------------------------------------------------------------- /plugin/core/src/test/resources/transform_path.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /plugin/gradle/wrapper: -------------------------------------------------------------------------------- 1 | ../../gradle/wrapper -------------------------------------------------------------------------------- /plugin/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | pluginManagement { 3 | repositories { 4 | google() 5 | mavenCentral() 6 | gradlePluginPortal() 7 | } 8 | } 9 | dependencyResolutionManagement { 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | versionCatalogs { 15 | create("libs") { 16 | from(files("../gradle/libs.versions.toml")) 17 | } 18 | } 19 | } 20 | plugins { 21 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 22 | } 23 | rootProject.name = "plugin" 24 | include("core") 25 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/io/github/irgaly/compose/vector/plugin/ComposeVectorExtension.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.plugin 2 | 3 | import org.gradle.api.Transformer 4 | import org.gradle.api.file.DirectoryProperty 5 | import org.gradle.api.file.ProjectLayout 6 | import org.gradle.api.provider.Property 7 | import org.gradle.api.tasks.Input 8 | import java.io.File 9 | 10 | abstract class ComposeVectorExtension( 11 | projectLayout: ProjectLayout 12 | ) { 13 | /** 14 | * Image classes package 15 | * 16 | * Example: com.example.your.app.images 17 | */ 18 | abstract val packageName: Property 19 | 20 | /** 21 | * Vector files directory 22 | * 23 | * Optional 24 | * 25 | * Default: {project directory}/images 26 | */ 27 | abstract val inputDir: DirectoryProperty 28 | 29 | /** 30 | * Generated Kotlin Sources directory. 31 | * outputDir is registered to SourceSet when outputDir is inside of project's buildDirectory. 32 | * 33 | * Optional 34 | * 35 | * Default: {build directory}/compose-vector/src/main/kotlin 36 | */ 37 | abstract val outputDir: DirectoryProperty 38 | 39 | /** 40 | * Custom Class Name pre conversion logic to image class names and image receiver class names. 41 | * 42 | * Optional 43 | * 44 | * For example, assume that source svg file is "my_icon.svg". 45 | * 46 | * * Apply custom preClassNameTransformer 47 | * * Pair 48 | * * File: "my_icon.svg" file instance 49 | * * String: "my_icon" 50 | * * for example: returns "pre_custom_my_icon" 51 | * * Apply default transformer 52 | * * "pre_custom_my_icon" -> "PreCustomMyIcon" 53 | * * Apply custom postClassNameTransformer 54 | * * Pair 55 | * * File: "my_icon.svg" file instance 56 | * * String: "PreCustomMyIcon" 57 | * * For example: returns "PreCustomMyIconPostCustom" 58 | * 59 | * This is result to "PreCustomMyIconPostCustom" image class name. 60 | */ 61 | abstract val preClassNameTransformer: Property>> 62 | 63 | /** 64 | * Custom Class Name post conversion logic to image class names and image receiver class names. 65 | * 66 | * Optional 67 | * 68 | * For example, assume that source svg file is "my_icon.svg". 69 | * 70 | * * Apply custom preClassNameTransformer 71 | * * Pair 72 | * * File: "my_icon.svg" file instance 73 | * * String: "my_icon" 74 | * * For example: returns "pre_custom_my_icon" 75 | * * Apply default transformer 76 | * * "pre_custom_my_icon" -> "PreCustomMyIcon" 77 | * * Apply custom postClassNameTransformer 78 | * * Pair 79 | * * File: "my_icon.svg" file instance 80 | * * String: "PreCustomMyIcon" 81 | * * For example: returns "PreCustomMyIconPostCustom" 82 | * 83 | * This is result to "PreCustomMyIconPostCustom" image class name. 84 | */ 85 | abstract val postClassNameTransformer: Property>> 86 | 87 | /** 88 | * Custom Package Name conversion logic. 89 | * 90 | * Optional 91 | * 92 | * * Pair 93 | * * File: target directory instance 94 | * * String: target directory basename 95 | */ 96 | abstract val packageNameTransformer: Property>> 97 | 98 | /** 99 | * Target SourceSets that generated images belongs to for KMP project. 100 | * This option is affect to KMP Project, not to Android only Project. 101 | */ 102 | @get:Input 103 | abstract val multiplatformGenerationTarget: Property 104 | 105 | /** 106 | * Generate androidx.compose.ui.tooling.preview.Preview functions for Android target or not 107 | * 108 | * Default: true 109 | */ 110 | @get:Input 111 | abstract val generateAndroidPreview: Property 112 | 113 | /** 114 | * Generate org.jetbrains.compose.ui.tooling.preview.Preview functions for KMP common target or not 115 | * 116 | * Default: false 117 | */ 118 | @get:Input 119 | abstract val generateJetbrainsPreview: Property 120 | 121 | /** 122 | * Generate androidx.compose.desktop.ui.tooling.preview.Preview functions for KMP common target or not 123 | * 124 | * Default: true 125 | */ 126 | @get:Input 127 | abstract val generateDesktopPreview: Property 128 | 129 | init { 130 | inputDir.convention( 131 | projectLayout.projectDirectory.dir("images") 132 | ) 133 | outputDir.convention( 134 | projectLayout.buildDirectory.dir("compose-vector/src/main/kotlin") 135 | ) 136 | multiplatformGenerationTarget.convention(GenerationTarget.Common) 137 | generateAndroidPreview.convention(true) 138 | generateJetbrainsPreview.convention(false) 139 | generateDesktopPreview.convention(true) 140 | } 141 | 142 | /** 143 | * Target SourceSets that generated images belongs to. 144 | */ 145 | enum class GenerationTarget { 146 | /** 147 | * commonMain target 148 | */ 149 | Common, 150 | 151 | /** 152 | * androidMain target 153 | */ 154 | Android 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/io/github/irgaly/compose/vector/plugin/ComposeVectorPlugin.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.plugin 2 | 3 | import com.android.build.api.variant.AndroidComponentsExtension 4 | import com.android.build.gradle.BaseExtension 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | import org.gradle.api.file.FileCollection 8 | import org.gradle.api.tasks.SourceSet 9 | import org.gradle.kotlin.dsl.configure 10 | import org.gradle.kotlin.dsl.create 11 | import org.gradle.kotlin.dsl.findByType 12 | import org.gradle.kotlin.dsl.register 13 | import org.gradle.kotlin.dsl.withType 14 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 15 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet 16 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 17 | 18 | class ComposeVectorPlugin : Plugin { 19 | override fun apply(target: Project) { 20 | val logger = target.logger 21 | val extension = target.extensions.create("composeVector") 22 | val task = target.tasks.register("generateImageVector") { 23 | group = "generate compose vector" 24 | this.packageName.set(extension.packageName) 25 | inputDir.set(extension.inputDir) 26 | outputDir.set(extension.outputDir) 27 | preClassNameTransformer.set(extension.preClassNameTransformer) 28 | postClassNameTransformer.set(extension.postClassNameTransformer) 29 | packageNameTransformer.set(extension.packageNameTransformer) 30 | } 31 | target.tasks 32 | .withType() 33 | .configureEach { 34 | it.dependsOn(task) 35 | } 36 | target.executeOnFinalize { 37 | val multiplatformExtension = 38 | target.extensions.findByType() 39 | val androidExtension = target.extensions.findByType() 40 | val srcDir = target.files(extension.outputDir).builtBy(task) 41 | val outputDirPath = extension.outputDir.get().asFile.toPath() 42 | val insideBuildDir = 43 | outputDirPath.startsWith(target.layout.buildDirectory.get().asFile.toPath()) 44 | var generationTarget = ComposeVectorExtension.GenerationTarget.Common 45 | if (multiplatformExtension != null) { 46 | when (checkNotNull(extension.multiplatformGenerationTarget.get())) { 47 | ComposeVectorExtension.GenerationTarget.Common -> { 48 | generationTarget = ComposeVectorExtension.GenerationTarget.Common 49 | } 50 | 51 | ComposeVectorExtension.GenerationTarget.Android -> { 52 | if (androidExtension == null) { 53 | error("multiplatformGenerationTarget is Android, but ${target.path} project does not have Android SourceSets.") 54 | } 55 | generationTarget = ComposeVectorExtension.GenerationTarget.Android 56 | } 57 | } 58 | } else if (androidExtension != null) { 59 | // Android only Project 60 | generationTarget = ComposeVectorExtension.GenerationTarget.Android 61 | } 62 | logger.info("generation target: $generationTarget") 63 | if (insideBuildDir) { 64 | if ((multiplatformExtension != null) && 65 | (generationTarget == ComposeVectorExtension.GenerationTarget.Common) 66 | ) { 67 | logger.info("Register $srcDir to Common Main SourceSets") 68 | multiplatformExtension.addCommonMainSourceSet(srcDir) 69 | } 70 | if ((androidExtension != null) && 71 | (generationTarget == ComposeVectorExtension.GenerationTarget.Android) 72 | ) { 73 | logger.info("Register $srcDir to Android Main SourceSets") 74 | androidExtension.addMainSourceSet(srcDir) 75 | } 76 | } 77 | task.configure { 78 | it.apply { 79 | hasAndroidPreview.set( 80 | (generationTarget == ComposeVectorExtension.GenerationTarget.Android) && 81 | extension.generateAndroidPreview.get() 82 | ) 83 | hasJetbrainsPreview.set( 84 | (generationTarget == ComposeVectorExtension.GenerationTarget.Common) && 85 | extension.generateJetbrainsPreview.get() 86 | ) 87 | hasDesktopPreview.set( 88 | (generationTarget == ComposeVectorExtension.GenerationTarget.Common) && 89 | extension.generateDesktopPreview.get() 90 | ) 91 | } 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Run Block in finalizeDsl when Android Plugin available 98 | * or in afterEvaluate when Android Plugin not available. 99 | */ 100 | private fun Project.executeOnFinalize(block: () -> Unit) { 101 | var hasAndroid = false 102 | setOf( 103 | "com.android.application", 104 | "com.android.library", 105 | "com.android.kotlin.multiplatform.library", 106 | ).forEach { pluginId -> 107 | pluginManager.withPlugin(pluginId) { 108 | hasAndroid = true 109 | extensions.configure(type = AndroidComponentsExtension::class) { extension -> 110 | extension.finalizeDsl { 111 | block() 112 | } 113 | } 114 | } 115 | } 116 | afterEvaluate { 117 | if (!hasAndroid) { 118 | block() 119 | } 120 | } 121 | } 122 | 123 | private fun KotlinMultiplatformExtension.addCommonMainSourceSet(srcDir: FileCollection) { 124 | sourceSets.findByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)?.kotlin?.srcDir(srcDir) 125 | } 126 | 127 | private fun BaseExtension.addMainSourceSet(srcDir: FileCollection) { 128 | sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME)?.kotlin?.srcDir(srcDir) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/io/github/irgaly/compose/vector/plugin/ComposeVectorTask.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.plugin 2 | 3 | import io.github.irgaly.compose.Logger 4 | import io.github.irgaly.compose.vector.ImageVectorGenerator 5 | import io.github.irgaly.compose.vector.svg.SvgParser 6 | import org.gradle.api.DefaultTask 7 | import org.gradle.api.Transformer 8 | import org.gradle.api.file.DirectoryProperty 9 | import org.gradle.api.file.FileType 10 | import org.gradle.api.provider.Property 11 | import org.gradle.api.tasks.CacheableTask 12 | import org.gradle.api.tasks.Input 13 | import org.gradle.api.tasks.InputDirectory 14 | import org.gradle.api.tasks.Optional 15 | import org.gradle.api.tasks.OutputDirectory 16 | import org.gradle.api.tasks.PathSensitive 17 | import org.gradle.api.tasks.PathSensitivity 18 | import org.gradle.api.tasks.TaskAction 19 | import org.gradle.work.ChangeType 20 | import org.gradle.work.Incremental 21 | import org.gradle.work.InputChanges 22 | import java.io.File 23 | import java.nio.file.Path 24 | import kotlin.io.path.name 25 | import kotlin.io.path.pathString 26 | 27 | @CacheableTask 28 | abstract class ComposeVectorTask: DefaultTask() { 29 | /** 30 | * Image classes package 31 | */ 32 | @get:Input 33 | abstract val packageName: Property 34 | 35 | /** 36 | * Vector files directory 37 | */ 38 | @get:Incremental 39 | @get:PathSensitive(PathSensitivity.RELATIVE) 40 | @get:InputDirectory 41 | abstract val inputDir: DirectoryProperty 42 | 43 | /** 44 | * Generated Kotlin Sources directory 45 | */ 46 | @get:OutputDirectory 47 | abstract val outputDir: DirectoryProperty 48 | 49 | /** 50 | * Custom Class Name pre conversion logic to image class names and image receiver class names. 51 | */ 52 | @get:Input 53 | @get:Optional 54 | abstract val preClassNameTransformer: Property>> 55 | 56 | /** 57 | * Custom Class Name post conversion logic to image class names and image receiver class names. 58 | */ 59 | @get:Input 60 | @get:Optional 61 | abstract val postClassNameTransformer: Property>> 62 | 63 | /** 64 | * Custom Package Name conversion logic. 65 | */ 66 | @get:Input 67 | @get:Optional 68 | abstract 69 | val packageNameTransformer: Property>> 70 | 71 | /** 72 | * Generated ImageVector classes has androidx.compose.ui.tooling.preview.Preview functions or not 73 | * 74 | * Default: false 75 | */ 76 | @get:Input 77 | @get:Optional 78 | abstract val hasAndroidPreview: Property 79 | 80 | /** 81 | * Generated ImageVector classes has org.jetbrains.compose.ui.tooling.preview.Preview functions or not 82 | * 83 | * Default: false 84 | */ 85 | @get:Input 86 | @get:Optional 87 | abstract val hasJetbrainsPreview: Property 88 | 89 | /** 90 | * Generated ImageVector classes has androidx.compose.desktop.ui.tooling.preview.Preview functions or not 91 | * 92 | * Default: false 93 | */ 94 | @get:Input 95 | @get:Optional 96 | abstract val hasDesktopPreview: Property 97 | 98 | @TaskAction 99 | fun execute(inputChanges: InputChanges) { 100 | val outputBaseDirectory = outputDir.get() 101 | val packageName = packageName.get() 102 | val packageDirectory = outputBaseDirectory.dir(packageName.replace(".", "/")) 103 | val parser = SvgParser(getParserLogger()) 104 | val generator = ImageVectorGenerator() 105 | val buildDirectory = project.layout.buildDirectory.get() 106 | if (!inputChanges.isIncremental && outputBaseDirectory.asFile.startsWith(buildDirectory.asFile)) { 107 | // outputDir is under build directory 108 | logger.info("clean $outputBaseDirectory because of initial build or full rebuild for incremental task and output directory is under project build directory.") 109 | outputBaseDirectory.asFile.deleteRecursively() 110 | } 111 | inputChanges.getFileChanges(inputDir) 112 | .filter { 113 | (it.fileType != FileType.DIRECTORY) 114 | }.filter { 115 | it.file.extension.equals("svg", ignoreCase = true) 116 | }.forEach { change -> 117 | logger.info("changed: $change") 118 | val svgFile = change.file 119 | val relativePath = Path.of(change.normalizedPath) 120 | val hasReceiverClass = (relativePath.parent != null) 121 | val outputDirectoryRelativePath = if (hasReceiverClass) { 122 | relativePath.parent 123 | } else { 124 | Path.of(".") 125 | } 126 | val outputDirectory = packageDirectory.dir(outputDirectoryRelativePath.pathString) 127 | val destinationPropertyName = svgFile 128 | .nameWithoutExtension.let { 129 | preClassNameTransformer.orNull?.transform(Pair(svgFile, it)) ?: it 130 | }.toKotlinName().let { 131 | postClassNameTransformer.orNull?.transform(Pair(svgFile, it)) ?: it 132 | } 133 | val receiverClasses = if (hasReceiverClass) { 134 | (1..outputDirectoryRelativePath.nameCount).map { 135 | outputDirectoryRelativePath.subpath(0, it) 136 | }.map { path -> 137 | val directory = inputDir.dir(path.pathString).get().asFile 138 | directory.name.toKotlinClassName( 139 | directory, 140 | preClassNameTransformer.orNull, 141 | postClassNameTransformer.orNull, 142 | ) 143 | } 144 | } else emptyList() 145 | val extensionPackage = (listOf(packageName) + if (hasReceiverClass) { 146 | (1..outputDirectoryRelativePath.nameCount).map { 147 | outputDirectoryRelativePath.subpath(0, it) 148 | }.map { path -> 149 | val directory = inputDir.dir(path.pathString).get().asFile 150 | packageNameTransformer.orNull?.transform(Pair(directory, directory.name)) 151 | ?: directory.name 152 | } 153 | } else emptyList()).joinToString(".") 154 | val outputFile = outputDirectory.file("${destinationPropertyName}.kt") 155 | when (change.changeType) { 156 | ChangeType.ADDED, 157 | ChangeType.MODIFIED, 158 | -> { 159 | logger.info("convert ${change.normalizedPath} to ${outputDirectoryRelativePath}/${outputFile.asFile.name}") 160 | outputDirectory.asFile.mkdirs() 161 | try { 162 | val imageVector = change.file.inputStream().use { stream -> 163 | parser.parse( 164 | input = stream, 165 | name = destinationPropertyName, 166 | autoMirror = receiverClasses.contains("AutoMirrored") 167 | ) 168 | } 169 | val kotlinSource = generator.generate( 170 | imageVector = imageVector, 171 | destinationPackage = packageName, 172 | receiverClasses = receiverClasses, 173 | extensionPackage = extensionPackage, 174 | hasAndroidPreview = hasAndroidPreview.getOrElse(false), 175 | hasJetbrainsPreview = hasJetbrainsPreview.getOrElse(false), 176 | hasDesktopPreview = hasDesktopPreview.getOrElse(false), 177 | ) 178 | outputFile.asFile.writeText(kotlinSource) 179 | } catch (error: Exception) { 180 | logger.error("SVG Parser Error: $svgFile", error) 181 | } 182 | } 183 | 184 | ChangeType.REMOVED -> { 185 | logger.info("delete ${outputDirectoryRelativePath}/${outputFile.asFile.name}") 186 | // delete target kotlin file 187 | outputFile.asFile.delete() 188 | // try to delete parent directory if empty 189 | outputDirectory.asFile.delete() 190 | } 191 | } 192 | } 193 | inputDir.get().asFile.listFiles(File::isDirectory)?.forEach { rootDirectory -> 194 | fun File.toObjectClass(): ImageVectorGenerator.ObjectClass { 195 | return ImageVectorGenerator.ObjectClass( 196 | name = name.toKotlinClassName( 197 | this, 198 | preClassNameTransformer.orNull, 199 | postClassNameTransformer.orNull 200 | ), 201 | children = listFiles(File::isDirectory)?.sorted()?.map { 202 | it.toObjectClass() 203 | } ?: emptyList() 204 | ) 205 | } 206 | 207 | val objectClass = rootDirectory.toObjectClass() 208 | val objectFileName = "${objectClass.name}.kt" 209 | logger.info("write object file: $objectFileName") 210 | packageDirectory.file(objectFileName).asFile.writeText( 211 | generator.generateObjectClasses(objectClass, packageName) 212 | ) 213 | } 214 | } 215 | 216 | private fun getParserLogger(): Logger { 217 | return object : Logger { 218 | override fun debug(message: String) { 219 | logger.debug(message) 220 | } 221 | 222 | override fun info(message: String) { 223 | logger.info(message) 224 | } 225 | 226 | override fun warn(message: String, error: Exception?) { 227 | logger.warn(message, error) 228 | } 229 | 230 | override fun error(message: String, error: Exception?) { 231 | logger.error(message, error) 232 | } 233 | } 234 | } 235 | 236 | private fun String.toKotlinClassName( 237 | file: File, 238 | preTransformer: Transformer>?, 239 | postTransformer: Transformer>?, 240 | ): String { 241 | return this.let { 242 | preTransformer?.transform(Pair(file, it)) ?: it 243 | }.let { 244 | if (it.equals("automirrored", ignoreCase = true)) { 245 | "AutoMirrored" 246 | } else it 247 | }.toKotlinName().let { 248 | postTransformer?.transform(Pair(file, it)) ?: it 249 | } 250 | } 251 | 252 | /** 253 | * "my_icon" -> "MyIcon" 254 | * "_my_icon" -> "MyIcon" 255 | * "my_icon_" -> "MyIcon" 256 | * "my_icon_0" -> "MyIcon0" 257 | * "0_my_icon" -> "_0MyIcon" 258 | * "MyIcon" -> "MyIcon" 259 | */ 260 | private fun String.toKotlinName(): String { 261 | return this 262 | // replace all "{symbol}" to "_" 263 | .replace(asciiSymbolsPattern, "_") 264 | // split chunks and remove "_" 265 | .splitToSequence("_") 266 | .filter { it.isNotEmpty() } 267 | .flatMap { part -> 268 | val wordChunks = mutableListOf() 269 | // reverse string to parse from end to start 270 | // eg. "MySVGIcon" -> "nocIGVSyM" 271 | var str = part.reversed() 272 | while (str.isNotEmpty()) { 273 | // get word chunks from head 274 | // eg. "nocIGVSyM" -> head chunk = "nocI", remains = "GVSyM" 275 | // "GVSyM" -> head chunk = "GVS", remains = "yM" 276 | // "yM" -> head chunk = "yM", remains = "" 277 | val match = chunkPattern.matchAt(str, 0)?.value ?: str.take(1) 278 | wordChunks.add( 279 | // reverse chunk 280 | // eg. "nocI" -> "Icon" 281 | match.reversed() 282 | ) 283 | str = str.drop(match.length) 284 | } 285 | // reverse chunks 286 | // eg. ["Icon", "SVG", "My"] -> ["My", "SVG", "Icon"] 287 | wordChunks.reversed() 288 | }.map { wordCuhnk -> 289 | // capitalize word 290 | // eg. "icon" -> "Icon" 291 | wordCuhnk.replaceFirstChar { it.uppercase() } 292 | } 293 | // join strings 294 | // eg. ["My", "SVG", "Icon"] -> "MySVGIcon" 295 | .joinToString("") 296 | // add "_" if first character is a number 297 | .replace("^[0-9]".toRegex()) { "_${it.value}" } 298 | } 299 | 300 | companion object { 301 | private val asciiSymbolsPattern: Regex = 302 | """[ !@#\\$%^&*()_+={}\[\]:;"'<>,.?/~`|-]""".toRegex() 303 | 304 | /** 305 | * "esaclemaC" (<- "Camelcase" reversed) 306 | */ 307 | private val reverseCamelPattern: String = "[a-z]+[A-Z]" 308 | 309 | /** 310 | * "UPPERCASE" 311 | */ 312 | private val upperCasesPattern: String = "[A-Z]+" 313 | 314 | /** 315 | * "文字列" 316 | */ 317 | private val nonAlphanumericsPattern: String = "[^a-zA-Z0-9]+" 318 | 319 | /** 320 | * "012" 321 | */ 322 | private val numericsPattern: String = "[0-9]+" 323 | 324 | /** 325 | * "lowercase" 326 | */ 327 | private val lowerCasesPattern: String = "[a-z]+" 328 | 329 | /** 330 | * match chunk string 331 | */ 332 | private val chunkPattern = 333 | "$reverseCamelPattern|$upperCasesPattern|$nonAlphanumericsPattern|$numericsPattern|$lowerCasesPattern".toRegex() 334 | } 335 | } 336 | 337 | /** 338 | * Get path segments sequence 339 | */ 340 | private fun Path.segments(): Sequence { 341 | return sequence { 342 | (0.. 6 | 8 | 9 | -------------------------------------------------------------------------------- /sample/android-library/src/main/kotlin/io/github/irgaly/compose/vector/sample/library/Sample.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.sample.library 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.tooling.preview.Preview 8 | import io.github.irgaly.compose.vector.sample.library.image.Icons 9 | import io.github.irgaly.compose.vector.sample.library.image.icons.Undo 10 | 11 | @Composable 12 | fun Sample() { 13 | Column { 14 | Image( 15 | Icons.Undo, 16 | contentDescription = null, 17 | ) 18 | } 19 | } 20 | 21 | @Preview 22 | @Composable 23 | private fun SamplePreview() { 24 | MaterialTheme { 25 | Sample() 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /sample/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.compose.compiler) 5 | alias(libs.plugins.composeVector) 6 | } 7 | 8 | android { 9 | namespace = "io.github.irgaly.compose.vector.sample" 10 | compileSdk = 34 11 | defaultConfig { 12 | applicationId = "io.github.irgaly.compose.vector.sample" 13 | minSdk = 26 14 | targetSdk = 34 15 | versionCode = 1 16 | versionName = "1.0.0" 17 | } 18 | buildFeatures { 19 | compose = true 20 | } 21 | // compose-vector-pluginの変換結果確認ディレクトリ 22 | sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME)?.kotlin?.srcDir( 23 | layout.buildDirectory.dir("test") 24 | ) 25 | } 26 | 27 | kotlin { 28 | jvmToolchain(17) 29 | } 30 | 31 | composeVector { 32 | packageName = "io.github.irgaly.compose.vector.sample.image" 33 | } 34 | 35 | dependencies { 36 | implementation(dependencies.platform(libs.compose.bom)) 37 | implementation(libs.androidx.appcompat) 38 | implementation(libs.androidx.lifecycle) 39 | implementation(libs.bundles.compose) 40 | } 41 | -------------------------------------------------------------------------------- /sample/android/images/icons/automirrored/undo.svg: -------------------------------------------------------------------------------- 1 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /sample/android/images/icons/undo.svg: -------------------------------------------------------------------------------- 1 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /sample/android/images/undo.svg: -------------------------------------------------------------------------------- 1 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /sample/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sample/android/src/main/kotlin/io/github/irgaly/compose/vector/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.sample 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.toArgb 14 | import androidx.core.view.WindowCompat 15 | import androidx.core.view.WindowInsetsControllerCompat 16 | import io.github.irgaly.compose.vector.sample.image.Icons 17 | import io.github.irgaly.compose.vector.sample.image.Undo 18 | import io.github.irgaly.compose.vector.sample.image.icons.Undo 19 | import io.github.irgaly.compose.vector.sample.image.icons.automirrored.Undo 20 | 21 | class MainActivity : AppCompatActivity() { 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | WindowCompat.setDecorFitsSystemWindows(window, false) 25 | window.statusBarColor = Color.Transparent.toArgb() 26 | window.navigationBarColor = Color.Transparent.toArgb() 27 | WindowInsetsControllerCompat( 28 | window, 29 | findViewById(android.R.id.content) 30 | ).isAppearanceLightStatusBars = true 31 | setContent { 32 | MaterialTheme { 33 | Column( 34 | Modifier.fillMaxSize(), 35 | ) { 36 | Text("Plugin Sample") 37 | Image(Undo, contentDescription = null) 38 | Image(Icons.Undo, contentDescription = null) 39 | Image(Icons.AutoMirrored.Undo, contentDescription = null) 40 | } 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sample/android/src/main/kotlin/io/github/irgaly/compose/vector/sample/MyApplication.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.sample 2 | 3 | import android.app.Application 4 | 5 | class MyApplication: Application() 6 | 7 | -------------------------------------------------------------------------------- /sample/android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Plugin Sample 3 | 4 | -------------------------------------------------------------------------------- /sample/android/xml/undo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/android/xml/undo_auto_mirrored.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /sample/jvm-library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.multiplatform) 3 | } 4 | 5 | kotlin { 6 | jvm { 7 | mainRun { 8 | mainClass = "io.github.irgaly.compose.vector.sample.MainKt" 9 | } 10 | } 11 | sourceSets { 12 | jvmMain { 13 | dependencies { 14 | implementation(libs.composeVector) 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sample/jvm-library/src/jvmMain/kotlin/io/github/irgaly/compose/vector/sample/Main.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.sample 2 | 3 | import io.github.irgaly.compose.Logger 4 | import io.github.irgaly.compose.vector.ImageVectorGenerator 5 | import io.github.irgaly.compose.vector.svg.SvgParser 6 | 7 | @Suppress("RedundantSuspendModifier") 8 | suspend fun main(@Suppress("UNUSED_PARAMETER") args: Array) { 9 | val input = svg.byteInputStream() 10 | val imageVector = SvgParser(object : Logger { 11 | override fun debug(message: String) { 12 | println("debug: $message") 13 | } 14 | 15 | override fun info(message: String) { 16 | println("info: $message") 17 | } 18 | 19 | override fun warn(message: String, error: Exception?) { 20 | println("warn: $message | $error") 21 | } 22 | 23 | override fun error(message: String, error: Exception?) { 24 | println("error: $message | $error") 25 | } 26 | }).parse( 27 | input, 28 | name = "Icon" 29 | ) 30 | val codes = ImageVectorGenerator().generate( 31 | imageVector = imageVector, 32 | destinationPackage = "io.github.irgaly.icons", 33 | receiverClasses = listOf("Icons", "AutoMirrored", "Filled"), 34 | extensionPackage = "io.github.irgaly.icons.automirrored.filled", 35 | hasAndroidPreview = true, 36 | ) 37 | println("--- Output.kt") 38 | print(codes) 39 | println("---") 40 | } 41 | 42 | val svg = """ 43 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 61 | 62 | 63 | 64 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 94 | 95 | 96 | 97 | """ 98 | -------------------------------------------------------------------------------- /sample/multiplatform/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.multiplatform) 3 | alias(libs.plugins.jetbrains.compose) 4 | alias(libs.plugins.compose.compiler) 5 | alias(libs.plugins.composeVector) 6 | } 7 | 8 | kotlin { 9 | jvm() 10 | sourceSets { 11 | commonMain { 12 | dependencies { 13 | implementation(compose.desktop.currentOs) 14 | implementation(compose.runtime) 15 | implementation(compose.foundation) 16 | implementation(compose.material3) 17 | } 18 | } 19 | } 20 | } 21 | 22 | compose.desktop { 23 | application { 24 | mainClass = "io.github.irgaly.compose.vector.sample.MainKt" 25 | } 26 | } 27 | 28 | composeVector { 29 | packageName = "io.github.irgaly.compose.vector.sample.image" 30 | } 31 | -------------------------------------------------------------------------------- /sample/multiplatform/images/icons/automirrored/undo.svg: -------------------------------------------------------------------------------- 1 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /sample/multiplatform/images/icons/undo.svg: -------------------------------------------------------------------------------- 1 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /sample/multiplatform/images/undo.svg: -------------------------------------------------------------------------------- 1 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /sample/multiplatform/src/commonMain/kotlin/io/github/irgaly/compose/vector/sample/App.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.sample 2 | 3 | import androidx.compose.desktop.ui.tooling.preview.Preview 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | 8 | @Composable 9 | @Preview 10 | fun App() { 11 | MaterialTheme { 12 | Text ("Hello") 13 | } 14 | } -------------------------------------------------------------------------------- /sample/multiplatform/src/jvmMain/kotlin/io/github/irgaly/compose/vector/sample/Main.kt: -------------------------------------------------------------------------------- 1 | package io.github.irgaly.compose.vector.sample 2 | 3 | import androidx.compose.ui.window.Window 4 | import androidx.compose.ui.window.application 5 | 6 | fun main() = application { 7 | Window(onCloseRequest = ::exitApplication) { 8 | App() 9 | } 10 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | pluginManagement { 3 | repositories { 4 | google() 5 | mavenCentral() 6 | gradlePluginPortal() 7 | } 8 | } 9 | dependencyResolutionManagement { 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | plugins { 16 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 17 | } 18 | rootProject.name = "compose-vector-plugin" 19 | include(":sample:android") 20 | include(":sample:multiplatform") 21 | include(":sample:android-library") 22 | include(":sample:jvm-library") 23 | includeBuild("plugin") 24 | --------------------------------------------------------------------------------