├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── build.gradle.kts ├── changelog.md ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── readme.md ├── renovate.json ├── settings.gradle.kts ├── vgo-cli ├── build.gradle.kts ├── optimize.pro └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── jzbrooks │ │ └── vgo │ │ └── cli │ │ ├── ArgReader.kt │ │ └── CommandLineInterface.kt │ └── test │ ├── kotlin │ └── com │ │ └── jzbrooks │ │ └── vgo │ │ └── cli │ │ └── CommandLineInterfaceTests.kt │ └── resources │ ├── avocado_example.xml │ └── simple_heart.xml ├── vgo-core ├── api │ └── vgo-core.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── jzbrooks │ │ └── vgo │ │ └── core │ │ ├── Color.kt │ │ ├── Writer.kt │ │ ├── graphic │ │ ├── ClipPath.kt │ │ ├── ContainerElement.kt │ │ ├── Element.kt │ │ ├── ElementVisitor.kt │ │ ├── Extra.kt │ │ ├── Graphic.kt │ │ ├── Group.kt │ │ ├── Path.kt │ │ └── command │ │ │ ├── ClosePath.kt │ │ │ ├── Command.kt │ │ │ ├── CommandParameter.kt │ │ │ ├── CommandPrinter.kt │ │ │ ├── CommandString.kt │ │ │ ├── CommandVariant.kt │ │ │ ├── CubicBezierCurve.kt │ │ │ ├── CubicCurve.kt │ │ │ ├── EllipticalArcCurve.kt │ │ │ ├── HorizontalLineTo.kt │ │ │ ├── LineTo.kt │ │ │ ├── MoveTo.kt │ │ │ ├── ParameterizedCommand.kt │ │ │ ├── QuadraticBezierCurve.kt │ │ │ ├── SmoothCubicBezierCurve.kt │ │ │ ├── SmoothQuadraticBezierCurve.kt │ │ │ └── VerticalLineTo.kt │ │ ├── optimization │ │ ├── BakeTransformations.kt │ │ ├── BottomUpOptimization.kt │ │ ├── BreakoutImplicitCommands.kt │ │ ├── CollapseGroups.kt │ │ ├── CommandVariant.kt │ │ ├── ConvertCurvesToArcs.kt │ │ ├── MergePaths.kt │ │ ├── Optimization.kt │ │ ├── OptimizationRegistry.kt │ │ ├── Polycommands.kt │ │ ├── RemoveEmptyGroups.kt │ │ ├── RemoveRedundantCommands.kt │ │ ├── RemoveTransparentPaths.kt │ │ ├── SimplifyBezierCurveCommands.kt │ │ ├── SimplifyLineCommands.kt │ │ └── TopDownOptimization.kt │ │ ├── transformation │ │ ├── BakeTransformations.kt │ │ ├── BottomUpTransformer.kt │ │ ├── BreakoutImplicitCommands.kt │ │ ├── CollapseGroups.kt │ │ ├── CommandVariant.kt │ │ ├── ConvertCurvesToArcs.kt │ │ ├── MergePaths.kt │ │ ├── Polycommands.kt │ │ ├── RemoveEmptyGroups.kt │ │ ├── RemoveRedundantCommands.kt │ │ ├── RemoveTransparentPaths.kt │ │ ├── SimplifyBezierCurveCommands.kt │ │ ├── SimplifyLineCommands.kt │ │ ├── TopDownTransformer.kt │ │ └── TransformerSet.kt │ │ └── util │ │ ├── ExperimentalVgoApi.kt │ │ ├── element │ │ └── Traverse.kt │ │ └── math │ │ ├── Circle.kt │ │ ├── Commands.kt │ │ ├── Curves.kt │ │ ├── LineSegment.kt │ │ ├── Matrix3.kt │ │ ├── Point.kt │ │ ├── Rectangle.kt │ │ ├── Surveyor.kt │ │ └── Vector3.kt │ └── test │ ├── kotlin │ └── com │ │ └── jzbrooks │ │ └── vgo │ │ └── core │ │ ├── ColorTest.kt │ │ ├── graphic │ │ └── command │ │ │ ├── FakeCommandPrinter.kt │ │ │ └── ParserTests.kt │ │ ├── transformation │ │ ├── BakeTransformationsTests.kt │ │ ├── BreakoutImplicitCommandsTests.kt │ │ ├── CollapseGroupsTests.kt │ │ ├── CommandVariantTests.kt │ │ ├── ConvertCurvesToArcsTest.kt │ │ ├── MergePathsTests.kt │ │ ├── RemoveEmptyGroupsTests.kt │ │ ├── RemoveRedundantCommandsTests.kt │ │ ├── RemoveTransparentPathsTests.kt │ │ ├── SimplifyBezierCurveCommandsTests.kt │ │ └── SimplifyLineCommandsTests.kt │ │ └── util │ │ ├── assertk │ │ └── Matrix3.kt │ │ ├── element │ │ └── ElementConstructors.kt │ │ └── math │ │ ├── CommandsTest.kt │ │ ├── CurvesTests.kt │ │ ├── LineSegmentTests.kt │ │ ├── Matrix3Tests.kt │ │ ├── RectangleTests.kt │ │ └── SurveyorTest.kt │ └── resources │ ├── avocado_example.xml │ ├── simple_heart.svg │ └── vd_visibilitystrike.xml ├── vgo-plugin ├── .gitignore ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── jzbrooks │ │ └── vgo │ │ └── plugin │ │ ├── OutputFormat.kt │ │ ├── ShrinkVectorArtwork.kt │ │ ├── VgoPlugin.kt │ │ └── VgoPluginExtension.kt │ └── test │ └── kotlin │ └── com │ └── jzbrooks │ └── vgo │ └── plugin │ └── VgoPluginTest.kt └── vgo ├── .gitignore ├── build.gradle.kts ├── gradle.properties └── src ├── main └── kotlin │ └── com │ └── jzbrooks │ └── vgo │ ├── Vgo.kt │ ├── iv │ ├── ImageVector.kt │ ├── ImageVectorConverter.kt │ ├── ImageVectorOptimizationRegistry.kt │ ├── ImageVectorReader.kt │ └── ImageVectorWriter.kt │ ├── svg │ ├── ScalableVectorGraphic.kt │ ├── ScalableVectorGraphicCommandPrinter.kt │ ├── ScalableVectorGraphicReader.kt │ ├── ScalableVectorGraphicWriter.kt │ ├── SvgOptimizationRegistry.kt │ └── VectorDrawableConverter.kt │ ├── util │ ├── Parse.kt │ └── xml │ │ └── XmlExtensions.kt │ └── vd │ ├── SvgConverter.kt │ ├── VectorDrawable.kt │ ├── VectorDrawableCommandPrinter.kt │ ├── VectorDrawableOptimizationRegistry.kt │ ├── VectorDrawableReader.kt │ └── VectorDrawableWriter.kt └── test ├── kotlin └── com │ └── jzbrooks │ └── vgo │ ├── BaselineTests.kt │ ├── VgoTests.kt │ ├── iv │ ├── ImageVectorReaderTest.kt │ ├── ImageVectorWriterTest.kt │ └── Utilities.kt │ ├── svg │ ├── ScalableVectorGraphicReaderTests.kt │ └── ScalableVectorGraphicWriterTests.kt │ ├── util │ ├── assertk │ │ └── Node.kt │ └── element │ │ └── ElementConstructors.kt │ └── vd │ ├── VectorDrawableCommandPrinterTests.kt │ ├── VectorDrawableReaderTests.kt │ └── VectorDrawableWriterTests.kt └── resources ├── android.svg ├── avocado_example.xml ├── baseline ├── android_optimized.svg ├── avocado_example_optimized.xml ├── charging_battery_optimized.xml ├── dribbble_ball_mark_optimized.svg ├── dribbble_ball_mark_optimized.xml ├── eleven_below_single_optimized.svg ├── eleven_below_single_optimized.xml ├── guacamole_optimized.svg ├── nasa_optimized.svg ├── nasa_optimized.xml ├── regression_101_optimized.xml ├── regression_31_optimized.xml ├── regression_33_optimized.xml ├── regression_60_optimized.xml ├── regression_88_optimized.xml ├── simple_heart_optimized.svg ├── simple_heart_optimized.xml ├── star_optimized.kt ├── tiger_optimized.svg ├── tiger_optimized.xml └── visibility_strike_optimized.xml ├── charging_battery.xml ├── dribbble_ball_mark.svg ├── dribbble_ball_mark.xml ├── eleven_below_single.svg ├── eleven_below_single.xml ├── guacamole.svg ├── in-place-modify ├── avocado_example.xml ├── avocado_example_optimized.xml └── non_vector.xml ├── nasa.svg ├── nasa.xml ├── regression_101.xml ├── regression_31.xml ├── regression_33.xml ├── regression_60.xml ├── regression_88.xml ├── simple_heart.svg ├── simple_heart.xml ├── star.kt ├── tiger.svg ├── tiger.xml └── visibility_strike.xml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **vgo version** 14 | Are you using the latest version? 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Using this input '...' 19 | 2. Via the '...' (cli or gradle plugin) 20 | 3. With these argument '...' 21 | 5. Results in this output '...' 22 | 23 | **Visual Comparison** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: new feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | env: 4 | GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | branches: 12 | - master 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | version: ${{ steps.properties.outputs.version }} 19 | changelog: ${{ steps.properties.outputs.changelog }} 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - uses: actions/setup-java@v4 25 | with: 26 | java-version: 21 27 | distribution: zulu 28 | 29 | - name: Setup Gradle 30 | uses: gradle/actions/setup-gradle@v4 31 | 32 | - name: Export Properties 33 | id: properties 34 | shell: bash 35 | run: | 36 | cat changelog.md 37 | PROPERTIES="$(./gradlew properties --console=plain -q)" 38 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 39 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 40 | 41 | echo "version=$VERSION" >> $GITHUB_OUTPUT 42 | echo "changelog<> $GITHUB_OUTPUT 43 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 44 | echo "EOF" >> $GITHUB_OUTPUT 45 | 46 | - name: build 47 | run: ./gradlew build 48 | 49 | - name: archive vgo test results 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: vgo_test_results 53 | path: vgo/build/reports/tests/test/ 54 | 55 | - name: archive vgo core test results 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: vgo_core_test_results 59 | path: vgo-core/build/reports/tests/test/ 60 | 61 | - name: release binary 62 | run: ./gradlew binary 63 | 64 | - name: archive vgo 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: vgo 68 | path: vgo-cli/build/libs/vgo 69 | 70 | # Prepare a draft release for GitHub Releases page for the manual verification 71 | # If accepted and published, release workflow would be triggered 72 | releaseDraft: 73 | name: Release Draft 74 | if: github.event_name != 'pull_request' 75 | needs: build 76 | runs-on: ubuntu-latest 77 | permissions: 78 | contents: write 79 | steps: 80 | # Check out current repository 81 | - name: Fetch Sources 82 | uses: actions/checkout@v4 83 | 84 | - name: archive vgo 85 | uses: actions/download-artifact@v4 86 | with: 87 | name: vgo 88 | path: ${{ github.workspace }}/dist 89 | 90 | - name: Generate checksum 91 | run: | 92 | cd ${{ github.workspace }}/dist 93 | sha256sum vgo > vgo.sha256 94 | 95 | # Remove old release drafts by using the curl request for the available releases with draft flag 96 | - name: Remove Old Release Drafts 97 | env: 98 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 99 | run: | 100 | gh api repos/{owner}/{repo}/releases \ 101 | --jq '.[] | select(.draft == true) | .id' \ 102 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 103 | 104 | # Create new release draft - which is not publicly visible and requires manual acceptance 105 | - name: Create Release Draft 106 | env: 107 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 108 | run: | 109 | gh release create v${{ needs.build.outputs.version }} ${{ github.workspace }}/dist/vgo ${{ github.workspace }}/dist/vgo.sha256 \ 110 | --draft \ 111 | --title "v${{ needs.build.outputs.version }}" \ 112 | --notes "$(cat << 'EOM' 113 | ${{ needs.build.outputs.changelog }} 114 | EOM 115 | )" 116 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | publish: 9 | name: Release build and publish 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | 15 | steps: 16 | - name: Fetch Sources 17 | uses: actions/checkout@v4 18 | with: 19 | ref: ${{ github.event.release.tag_name }} 20 | 21 | - name: Set up a JDK 22 | id: setup-jdk 23 | uses: actions/setup-java@v4 24 | with: 25 | java-version: 21 26 | distribution: zulu 27 | 28 | - name: Setup Gradle 29 | uses: gradle/actions/setup-gradle@v4 30 | 31 | - name: Create binary keyring 32 | env: 33 | GPG_KEY_CONTENTS: ${{ secrets.SIGNING_KEY_SECRET }} 34 | SIGNING_KEY_RING_FILE: ${{ secrets.SIGNING_KEY_FILE_PATH }} 35 | run: | 36 | git fetch --unshallow 37 | sudo bash -c "echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_KEY_RING_FILE'" 38 | 39 | - name: Publish to MavenCentral 40 | env: 41 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }} 42 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }} 43 | SONATYPE_PROFILE_ID: ${{ secrets.SONATYPE_PROFILE_ID }} 44 | run: ./gradlew -Psigning.keyId=${{ secrets.SIGNING_KEY_ID}} -Psigning.password=${{ secrets.SIGNING_KEY_PASSWORD }} -Psigning.secretKeyRingFile=${{ secrets.SIGNING_KEY_FILE_PATH }} --no-configuration-cache publishAndReleaseToMavenCentral 45 | 46 | # Set environment variables 47 | - name: Export Properties 48 | id: properties 49 | shell: bash 50 | run: | 51 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 52 | ${{ github.event.release.body }} 53 | EOM 54 | )" 55 | 56 | echo "changelog<> $GITHUB_OUTPUT 57 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 58 | echo "EOF" >> $GITHUB_OUTPUT 59 | 60 | # Update Unreleased section with the current release note 61 | - name: Patch Changelog 62 | if: ${{ steps.properties.outputs.changelog != '' }} 63 | env: 64 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 65 | run: | 66 | ./gradlew patchChangelog --release-note="$CHANGELOG" 67 | 68 | - name: Open PR for Changelog Update 69 | env: 70 | GH_TOKEN: ${{ github.token }} 71 | run: | 72 | VERSION="${{ github.event.release.tag_name }}" 73 | BRANCH="changelog-update-$VERSION" 74 | git config user.name github-actions 75 | git config user.email github-actions@github.com 76 | git checkout -b $BRANCH 77 | git commit -am "Changelog update - $VERSION" 78 | git push --set-upstream origin $BRANCH 79 | gh pr create \ 80 | --title "Changelog update - \`$VERSION\`" \ 81 | --body "Current pull request contains patched \`changelog.md\` file for the \`$VERSION\` version." \ 82 | --base master \ 83 | --head $BRANCH -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | 3 | build/ 4 | 5 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Justin Brooks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension 4 | import org.jlleitschuh.gradle.ktlint.KtlintExtension 5 | import org.jlleitschuh.gradle.ktlint.KtlintPlugin 6 | 7 | plugins { 8 | id("org.jetbrains.kotlin.jvm") version "2.1.21" 9 | id("org.jlleitschuh.gradle.ktlint") version "12.3.0" 10 | id("com.vanniktech.maven.publish") version "0.32.0" 11 | id("org.jetbrains.changelog") version "2.2.1" 12 | } 13 | 14 | version = property("VERSION_NAME").toString() 15 | 16 | changelog.path.set("changelog.md") 17 | 18 | subprojects { 19 | apply() 20 | configure { 21 | version.set("1.3.1") 22 | } 23 | 24 | pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { 25 | configure { 26 | compilerOptions.jvmTarget.set(JvmTarget.JVM_17) 27 | } 28 | 29 | configure { 30 | sourceCompatibility = JavaVersion.VERSION_17 31 | targetCompatibility = JavaVersion.VERSION_17 32 | } 33 | } 34 | 35 | tasks.withType { 36 | useJUnitPlatform() 37 | 38 | testLogging { 39 | exceptionFormat = TestExceptionFormat.FULL 40 | showExceptions = true 41 | showCauses = true 42 | } 43 | 44 | addTestListener( 45 | object : TestListener { 46 | override fun afterSuite( 47 | suite: TestDescriptor, 48 | result: TestResult, 49 | ) { 50 | if (suite.parent == null) { 51 | val output = 52 | buildString { 53 | append("| Results: ") 54 | append(result.resultType) 55 | append(" (") 56 | append(result.testCount) 57 | append(" tests, ") 58 | append(result.successfulTestCount) 59 | append(" passed, ") 60 | append(result.failedTestCount) 61 | append(" failed, ") 62 | append(result.skippedTestCount) 63 | append(" skipped) |") 64 | } 65 | val border = "-".repeat(output.length) 66 | logger.lifecycle( 67 | """ 68 | $border 69 | $output 70 | $border 71 | """.trimIndent(), 72 | ) 73 | } 74 | } 75 | 76 | override fun afterTest( 77 | testDescriptor: TestDescriptor?, 78 | result: TestResult?, 79 | ) {} 80 | 81 | override fun beforeTest(testDescriptor: TestDescriptor?) {} 82 | 83 | override fun beforeSuite(suite: TestDescriptor?) {} 84 | }, 85 | ) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-XX:+UseParallelGC 2 | 3 | GROUP=com.jzbrooks 4 | VERSION_NAME=3.2.1 5 | 6 | POM_URL=https://github.com/jzbrooks/vgo/ 7 | 8 | POM_LICENSE_NAME=MIT License 9 | POM_LICENSE_URL=https://github.com/jzbrooks/vgo/blob/master/LICENSE 10 | 11 | POM_SCM_URL=https://github.com/jzbrooks/vgo/tree/master 12 | POM_SCM_CONNECTION=scm:git:git:github.com/jzbrooks/vgo.git 13 | POM_SCM_DEV_CONNECTION=scm:git:ssh://github.com/jzbrooks/vgo.git 14 | 15 | POM_DEVELOPER_ID=jzbrooks 16 | POM_DEVELOPER_NAME=Justin Brooks 17 | POM_DEVELOPER_URL=https://github.com/jzbrooks 18 | POM_DEVELOPER_EMAIL=justin@jzbrooks.com 19 | 20 | SONATYPE_HOST=DEFAULT 21 | SONATYPE_AUTOMATIC_RELEASE=true 22 | RELEASE_SIGNING_ENABLED=true 23 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jzbrooks/vgo/35ab28d6cf10a4caa8a95c84f77fd431a3d18d2f/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-8.14.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## vgo 2 | 3 | [![Build Status](https://github.com/jzbrooks/vgo/actions/workflows/build.yml/badge.svg?event=push)](https://github.com/jzbrooks/vgo/actions/workflows/build.yml) 4 | [![Maven Central: vgo](https://img.shields.io/maven-central/v/com.jzbrooks/vgo?label=vgo)](https://ossindex.sonatype.org/component/pkg:maven/com.jzbrooks/vgo) 5 | [![Maven Central: vgo-core](https://img.shields.io/maven-central/v/com.jzbrooks/vgo-core?label=vgo-core)](https://ossindex.sonatype.org/component/pkg:maven/com.jzbrooks/vgo-core) 6 | [![Maven Central: vgo-plugin](https://img.shields.io/maven-central/v/com.jzbrooks/vgo-plugin?label=vgo-plugin)](https://ossindex.sonatype.org/component/pkg:maven/com.jzbrooks/vgo-plugin) 7 | 8 | vgo optimizes vector graphics through a format-agnostic approach by leveraging vgo-core's intermediate representation. 9 | It can convert between common vector formats, including SVG, Android Vector Drawables, and Jetpack Compose ImageVector _and_ optimize them to boot. 10 | 11 | ## Installation 12 | 13 | #### Homebrew 14 | `brew install jzbrooks/repo/vgo` 15 | 16 | #### Manually 17 | Download the distribution from the release page and ensure it has execute permission. On macOS & Linux run `chmod u+x vgo`. 18 | 19 | vgo requires Java 17. 20 | 21 | ## Gradle Plugin 22 | The plugin aims to be fast and small by leveraging (for the entire tool) the JVM your Gradle build is already using. 23 | 24 | The `shrinkVectorArtwork` task is added to your project on plugin application. 25 | 26 | To incorporate the plugin in your build, configure maven central plugin resolution: 27 | ```groovy 28 | pluginManagement { 29 | repositories { 30 | mavenCentral() 31 | gradlePluginPortal() 32 | } 33 | } 34 | ``` 35 | 36 | Then, in the relevant project, add the plugin. 37 | 38 | > [!NOTE] 39 | > You must have the android tools sdk on your build classpath if you are converting SVGs to vector drawables. 40 | > This is typically done by applying the Android Gradle Plugin. 41 | > You must have the kotlin compiler on your build classpath if you are using the `ImageVector` format. 42 | > This is typically done by applying the Kotlin Gradle Plugin. 43 | 44 | ```groovy 45 | plugins { 46 | id 'com.jzbrooks.vgo' 47 | } 48 | 49 | // Default configuration shown 50 | vgo { 51 | inputs = fileTree(projectDir) { 52 | include '**/res/drawable*/*.xml' 53 | } 54 | outputs = inputs 55 | showStatistics = true 56 | format = OutputFormat.UNCHANGED 57 | noOptimization = false 58 | indent = 0 59 | } 60 | ``` 61 | 62 | > [!TIP] 63 | > For Android projects a non-zero indent is better for readability and provides no apk size impact after AAPT processing. 64 | 65 | ## Command Line Interface 66 | 67 | ``` 68 | > vgo [options] [file/directory] 69 | 70 | Options: 71 | -h --help print this message 72 | -o --output file or directory, if not provided the input will be overwritten 73 | -s --stats print statistics on processed files to standard out 74 | -v --version print the version number 75 | --indent [value] write files with value columns of indentation 76 | --format [value] output format (svg, vd, iv) 77 | --no-optimiation skip graphic optimization 78 | ``` 79 | 80 | > `java -jar vgo` for Windows 81 | 82 | ## Examples 83 | 84 | ### CLI 85 | ``` 86 | # Optimize files specified from standard in 87 | > find ./**/ic_*.xml | vgo 88 | 89 | # Optimize vector.xml and overwrite its contents 90 | > vgo vector.xml 91 | 92 | # Optimize vector.xml and write the result into new_vector.xml 93 | > vgo vector.xml -o new_vector.xml 94 | 95 | # Optimize multiple input sources write results to the 96 | > vgo vector.xml -o new_vector.xml ./assets -o ./new_assets 97 | ``` 98 | 99 | ### Gradle Plugin 100 | ```kotlin 101 | // Optimize and convert svgs to vector drawables at build time 102 | vgo { 103 | format = OutputFormat.VECTOR_DRAWABLE 104 | outputs = files(project.fileTree(project.projectDir) { 105 | include("icons/**/*.svg") 106 | }.map { 107 | val file = file("src/main/res/drawable/${it.nameWithoutExtension}.xml") 108 | file.parentFile?.mkdirs() 109 | file.createNewFile() 110 | file 111 | }).asFileTree 112 | } 113 | 114 | tasks.getByName("processDebugResources").configureEach { 115 | dependsOn("shrinkVectorArtwork") 116 | } 117 | ``` 118 | 119 | ## Build instructions 120 | 121 | This project uses the Gradle build system. 122 | 123 | To build the binary: `./gradlew binary` 124 | 125 | To run the tests: `./gradlew check` 126 | 127 | To see all available tasks: `./gradlew tasks` 128 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | mavenCentral() 12 | google() 13 | } 14 | } 15 | 16 | include("vgo-core", "vgo", "vgo-plugin", "vgo-cli") 17 | -------------------------------------------------------------------------------- /vgo-cli/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.jvm") 3 | } 4 | 5 | tasks { 6 | jar { 7 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 8 | 9 | dependsOn(configurations.runtimeClasspath) 10 | manifest { 11 | attributes["Main-Class"] = "com.jzbrooks.vgo.cli.CommandLineInterface" 12 | attributes["Bundle-Version"] = project.properties["VERSION_NAME"] 13 | 14 | exclude( 15 | "META-INF/*.SF", 16 | "META-INF/*.DSA", 17 | "META-INF/*.RSA", 18 | "META-INF/*.EC", 19 | "META-INF/*.SF.*", 20 | "META-INF/*.DSA.*", 21 | "META-INF/*.RSA.*", 22 | "META-INF/*.EC.*", 23 | "META-INF/BCKEY.DSA", 24 | "META-INF/BC2048KE.DSA", 25 | "META-INF/BCKEY.SF", 26 | "META-INF/BC2048KE.SF", 27 | "**/*.kotlin_metadata", 28 | "**/*.kotlin_module", 29 | "**/*.kotlin_builtins", 30 | "**/module-info.class", 31 | "META-INF/maven/**", 32 | "META-INF/versions/**", 33 | "META-INF/*.version", 34 | "META-INF/LICENSE*", 35 | "META-INF/LGPL2.1", 36 | "META-INF/DEPENDENCIES", 37 | "META-INF/AL2.0", 38 | "**/NOTICE*", 39 | "javax/activation/**", 40 | "xsd/catalog.xml", 41 | ) 42 | } 43 | 44 | val sourceClasses = 45 | sourceSets.main 46 | .get() 47 | .output.classesDirs 48 | inputs.files(sourceClasses) 49 | destinationDirectory.set(layout.buildDirectory.dir("libs/debug")) 50 | 51 | doFirst { 52 | from(files(sourceClasses)) 53 | from( 54 | configurations.runtimeClasspath 55 | .get() 56 | .asFileTree.files 57 | .map(::zipTree), 58 | ) 59 | } 60 | } 61 | 62 | val optimize by registering(JavaExec::class) { 63 | description = "Runs r8 on the jar application." 64 | group = "build" 65 | 66 | inputs.file(layout.buildDirectory.file("libs/debug/vgo-cli.jar")) 67 | outputs.file(layout.buildDirectory.file("libs/vgo.jar")) 68 | 69 | val javaHome = System.getProperty("java.home") 70 | 71 | classpath(r8) 72 | mainClass = "com.android.tools.r8.R8" 73 | 74 | args( 75 | "--release", 76 | "--classfile", 77 | "--lib", 78 | javaHome, 79 | "--output", 80 | layout.buildDirectory.file("libs/vgo.jar").get(), 81 | "--pg-conf", 82 | "optimize.pro", 83 | layout.buildDirectory.file("libs/debug/vgo-cli.jar").get(), 84 | ) 85 | 86 | dependsOn(getByName("jar")) 87 | } 88 | 89 | val binaryFileProp = layout.buildDirectory.file("libs/vgo") 90 | val binary by registering { 91 | description = "Prepends shell script in the jar to improve CLI" 92 | group = "build" 93 | 94 | dependsOn(optimize) 95 | 96 | inputs.file(layout.buildDirectory.file("libs/vgo.jar")) 97 | outputs.file(binaryFileProp) 98 | 99 | doLast { 100 | val binaryFile = binaryFileProp.get().asFile 101 | binaryFile.parentFile.mkdirs() 102 | binaryFile.delete() 103 | binaryFile.appendText("#!/bin/sh\n\nexec java \$JAVA_OPTS -jar \$0 \"\$@\"\n\n") 104 | layout.buildDirectory.file("libs/vgo.jar").get().asFile.inputStream().use { 105 | binaryFile.appendBytes(it.readBytes()) 106 | } 107 | binaryFile.setExecutable(true, false) 108 | } 109 | } 110 | } 111 | 112 | val r8: Configuration by configurations.creating 113 | 114 | dependencies { 115 | implementation(project(":vgo")) 116 | 117 | implementation("com.android.tools:sdk-common:31.10.1") 118 | implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:2.1.21") 119 | 120 | r8("com.android.tools:r8:8.7.18") 121 | 122 | testImplementation(platform("org.junit:junit-bom:5.13.1")) 123 | testImplementation("org.junit.jupiter:junit-jupiter-api") 124 | testImplementation("org.junit.jupiter:junit-jupiter-params") 125 | testImplementation("org.junit.platform:junit-platform-launcher") 126 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") 127 | 128 | testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1") 129 | } 130 | -------------------------------------------------------------------------------- /vgo-cli/optimize.pro: -------------------------------------------------------------------------------- 1 | -dontobfuscate 2 | -allowaccessmodification 3 | -overloadaggressively 4 | -dontskipnonpubliclibraryclasses 5 | -mergeinterfacesaggressively 6 | -verbose 7 | 8 | -keep class com.jzbrooks.vgo.cli.CommandLineInterface { 9 | public static void main(java.lang.String[]); 10 | } 11 | 12 | # Used in an EnumMap inside tools-sdk code. It is required for converting vectors with clip paths. 13 | -keep,allowoptimization enum com.android.ide.common.vectordrawable.SvgNode$ClipRule { 14 | public static **[] $VALUES; 15 | public static com.android.ide.common.vectordrawable.SvgNode$ClipRule[] values(); 16 | public static com.android.ide.common.vectordrawable.SvgNode$ClipRule valueOf(java.lang.String); 17 | } 18 | 19 | -dontwarn com.google.auto.service.** 20 | -dontwarn kotlin.annotations.jvm.** 21 | -dontwarn kotlinx.coroutines.future.** 22 | -dontwarn org.jetbrains.annotations.** 23 | -dontwarn org.jetbrains.kotlin.com.google.errorprone.** 24 | -dontwarn org.jetbrains.kotlin.com.google.j2objc.** 25 | -dontwarn org.kxml2.io.** -------------------------------------------------------------------------------- /vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/ArgReader.kt: -------------------------------------------------------------------------------- 1 | package com.jzbrooks.vgo.cli 2 | 3 | import kotlin.collections.any 4 | import kotlin.collections.first 5 | import kotlin.collections.firstOrNull 6 | import kotlin.collections.getOrElse 7 | import kotlin.collections.indexOfFirst 8 | import kotlin.collections.isNotEmpty 9 | import kotlin.collections.map 10 | import kotlin.text.isNotBlank 11 | import kotlin.text.split 12 | 13 | class ArgReader( 14 | private val args: MutableList, 15 | ) { 16 | private val hasArguments 17 | get() = args.isNotEmpty() 18 | 19 | fun readFlag(name: String): Boolean { 20 | require(name.isNotBlank()) 21 | 22 | val names = name.split('|') 23 | if (names.size > 1) { 24 | return names.any(::readFlag) 25 | } 26 | 27 | val index = args.indexOfFirst { isOptionArgument(name, it) } 28 | if (index == -1) return false 29 | 30 | args.removeAt(index) 31 | return true 32 | } 33 | 34 | fun readOption(name: String): String? { 35 | require(name.isNotBlank()) 36 | 37 | val names = name.split('|') 38 | if (names.size > 1) { 39 | return names.map(::readOption).firstOrNull { it != null } 40 | } 41 | 42 | val index = args.indexOfFirst { isOptionArgument(name, it) } 43 | if (index == -1) return null 44 | 45 | val value = 46 | args.getOrElse(index + 1) { 47 | throw IllegalStateException("Missing value after ${if (name.length == 1) "-" else "--"}$name") 48 | } 49 | 50 | args.removeAt(index) 51 | args.removeAt(index) 52 | return value 53 | } 54 | 55 | fun readArguments(): List { 56 | val arguments = kotlin.collections.mutableListOf() 57 | while (hasArguments) { 58 | arguments.add(readArgument()) 59 | } 60 | return arguments 61 | } 62 | 63 | private fun readArgument(): String { 64 | val value = args.first() 65 | 66 | check(!isOption(value)) { "Unexpected option $value" } 67 | 68 | args.removeAt(0) 69 | return value 70 | } 71 | 72 | companion object { 73 | private fun isOption(name: String) = name.length >= 2 && name[0] == '-' 74 | 75 | private fun isOptionArgument( 76 | name: String, 77 | argument: String, 78 | ): Boolean = 79 | if (name.length == 1) { 80 | "-$name" == argument 81 | } else { 82 | "--$name" == argument 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /vgo-cli/src/main/kotlin/com/jzbrooks/vgo/cli/CommandLineInterface.kt: -------------------------------------------------------------------------------- 1 | package com.jzbrooks.vgo.cli 2 | 3 | import com.jzbrooks.vgo.Vgo 4 | import kotlin.system.exitProcess 5 | 6 | class CommandLineInterface { 7 | fun run(args: Array): Int { 8 | val argReader = ArgReader(args.toMutableList()) 9 | 10 | val printHelp = argReader.readFlag("help|h") 11 | if (printHelp) { 12 | println(HELP_MESSAGE) 13 | return 0 14 | } 15 | 16 | val printVersion = argReader.readFlag("version|v") 17 | val printStats = argReader.readFlag("stats|s") 18 | val indent = argReader.readOption("indent")?.toIntOrNull() 19 | 20 | val outputs = 21 | run { 22 | val outputPaths = mutableListOf() 23 | var output = argReader.readOption("output|o") 24 | while (output != null) { 25 | outputPaths.add(output) 26 | output = argReader.readOption("output|o") 27 | } 28 | outputPaths.toList() 29 | } 30 | 31 | val format = argReader.readOption("format") 32 | val noOptimization = argReader.readFlag("no-optimization") 33 | 34 | if (format == null && noOptimization) { 35 | System.err.println("Warning: skipping optimization without --format is a no-op.") 36 | } 37 | 38 | val inputs = argReader.readArguments() 39 | 40 | val options = 41 | Vgo.Options( 42 | printVersion = printVersion, 43 | printStats = printStats, 44 | indent = indent, 45 | output = outputs, 46 | format = format, 47 | noOptimization = noOptimization, 48 | input = inputs, 49 | ) 50 | 51 | return Vgo(options).run() 52 | } 53 | 54 | companion object { 55 | private val HELP_MESSAGE = 56 | """ 57 | > vgo [options] [file/directory] 58 | 59 | Options: 60 | -h --help print this message 61 | -o --output file or directory, if not provided the input will be overwritten 62 | -s --stats print statistics on processed files to standard out 63 | -v --version print the version number 64 | --indent value write files with value columns of indentation 65 | --format value write specified output format (svg, vd, iv) 66 | --no-optimization skip graphic optimization 67 | """.trimIndent() 68 | 69 | @JvmStatic 70 | fun main(args: Array): Unit = exitProcess(CommandLineInterface().run(args)) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /vgo-cli/src/test/resources/avocado_example.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /vgo-cli/src/test/resources/simple_heart.xml: -------------------------------------------------------------------------------- 1 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /vgo-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("org.jetbrains.kotlin.jvm") 3 | id("com.vanniktech.maven.publish") 4 | id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.17.0" 5 | } 6 | 7 | dependencies { 8 | testImplementation(platform("org.junit:junit-bom:5.13.1")) 9 | testImplementation("org.junit.jupiter:junit-jupiter-api") 10 | testImplementation("org.junit.jupiter:junit-jupiter-params") 11 | testImplementation("org.junit.platform:junit-platform-launcher") 12 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") 13 | 14 | testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.1") 15 | } 16 | 17 | tasks { 18 | val sourcesJar by registering(Jar::class) { 19 | archiveClassifier.set("sources") 20 | from(sourceSets["main"].allSource) 21 | } 22 | 23 | val javadocJar by registering(Jar::class) { 24 | archiveClassifier.set("javadoc") 25 | from(this@tasks["javadoc"]) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /vgo-core/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=vgo-core 2 | POM_NAME=vgo-core 3 | POM_DESCRIPTION=vgo-core is a library for optimizing vector artwork files. -------------------------------------------------------------------------------- /vgo-core/src/main/kotlin/com/jzbrooks/vgo/core/Writer.kt: -------------------------------------------------------------------------------- 1 | package com.jzbrooks.vgo.core 2 | 3 | import com.jzbrooks.vgo.core.graphic.Graphic 4 | import java.io.OutputStream 5 | 6 | interface Writer { 7 | val options: Set