├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | xmlns:android
19 |
20 | ^$
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | xmlns:.*
30 |
31 | ^$
32 |
33 |
34 | BY_NAME
35 |
36 |
37 |
38 |
39 |
40 |
41 | .*:id
42 |
43 | http://schemas.android.com/apk/res/android
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | .*:name
53 |
54 | http://schemas.android.com/apk/res/android
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | name
64 |
65 | ^$
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | style
75 |
76 | ^$
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | .*
86 |
87 | ^$
88 |
89 |
90 | BY_NAME
91 |
92 |
93 |
94 |
95 |
96 |
97 | .*
98 |
99 | http://schemas.android.com/apk/res/android
100 |
101 |
102 | ANDROID_ATTRIBUTE_ORDER
103 |
104 |
105 |
106 |
107 |
108 |
109 | .*
110 |
111 | .*
112 |
113 |
114 | BY_NAME
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 | 
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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------