Skip to content
/ Vekt Public

🎨 A lightweight, pure Kotlin SVG path renderer for Compose Multiplatform. No Skia. No dependencies. Just vectors.

Notifications You must be signed in to change notification settings

xuwakao/Vekt

Repository files navigation

Vekt

Vekt (from German/Norwegian "vector") is a lightweight, pure Kotlin Multiplatform SVG rendering library. It provides runtime SVG parsing and rendering for Compose Multiplatform, Kuikly Compose, and Android native Views.

Features

  • Pure Kotlin - No native dependencies, works on all Kotlin targets
  • Compose Multiplatform - JVM, iOS, JS/WASM support
  • Kuikly Compose - Tencent Kuikly framework support
  • Android View - Native Android View for non-Compose projects
  • Full SVG Path Support - M, L, H, V, C, S, Q, T, A, Z commands
  • Basic Shapes - rect, circle, ellipse, line, polygon, polyline
  • Groups & Transforms - g elements with translate, scale, rotate, skew, matrix
  • Styles - fill, stroke, opacity, stroke-width, linecap, linejoin
  • Gradients - linearGradient, radialGradient with stops and spread methods
  • Defs & Symbols - use, defs, symbol elements for reusable graphics
  • Text Support - text and tspan elements with font properties and alignment
  • ClipPath - clipPath rendering for element clipping
  • Image - Embedded base64 data URI images
  • Advanced Parsing - mask, filter, pattern elements (rendering planned)
  • viewBox - Proper viewport and scaling support
  • Tinting - Apply tint color to override fill colors

Installation

Add the dependency to your project:

// build.gradle.kts

// For Compose Multiplatform
implementation("io.aimei:vekt-compose:0.1.0")

// For Kuikly Compose
implementation("io.aimei:vekt-kuikly:0.1.0")

// For Android native View
implementation("io.aimei:vekt-android:0.1.0")

// Core only (parsing without rendering)
implementation("io.aimei:vekt-core:0.1.0")

Usage

Compose Multiplatform

import io.aimei.wk.compose.SvgImage

@Composable
fun MyIcon() {
    val svgContent = """
        <svg viewBox="0 0 100 100">
            <circle cx="50" cy="50" r="40" fill="red"/>
        </svg>
    """.trimIndent()

    SvgImage(
        svgContent = svgContent,
        modifier = Modifier.size(48.dp),
        tint = Color.Blue // Optional: override fill color
    )
}

Kuikly Compose

import io.aimei.wk.kuikly.SvgImage

@Composable
fun MyIcon() {
    SvgImage(
        svgContent = svgContent,
        modifier = Modifier.size(48.dp)
    )
}

Android Native View

import io.aimei.wk.android.SvgView

// In Activity/Fragment
val svgView = SvgView(context)
svgView.setSvgContent(svgString)
svgView.setTintColor(Color.BLUE) // Optional

// In XML layout
<io.aimei.wk.android.SvgView
    android:id="@+id/svgView"
    android:layout_width="48dp"
    android:layout_height="48dp"/>

Core API (Parsing Only)

import io.aimei.wk.parser.SvgParser
import io.aimei.wk.parser.SvgPathParser

// Parse complete SVG document
val document = SvgParser.parse(svgContent)
println("Width: ${document.width}, Height: ${document.height}")
println("Elements: ${document.elements.size}")

// Parse path data only
val pathCommands = SvgPathParser.parse("M10 20 L30 40 Z")
pathCommands.commands.forEach { cmd ->
    when (cmd) {
        is PathCommand.MoveTo -> println("Move to ${cmd.x}, ${cmd.y}")
        is PathCommand.LineTo -> println("Line to ${cmd.x}, ${cmd.y}")
        is PathCommand.Close -> println("Close path")
        // ...
    }
}

Supported SVG Features

Elements

Element Support
<path> βœ… Full
<rect> βœ… Full (with rx/ry)
<circle> βœ… Full
<ellipse> βœ… Full
<line> βœ… Full
<polyline> βœ… Full
<polygon> βœ… Full
<g> (group) βœ… Full
<text> βœ… Full (font-family, font-size, font-weight, text-anchor, tspan)
<tspan> βœ… Full (x, y, dx, dy, style inheritance)
<image> βœ… Basic (data URI base64, placeholder for external URLs)
<defs> βœ… Full
<symbol> βœ… Full
<use> βœ… Full (href and xlink:href)
<linearGradient> βœ… Full (stops, gradientUnits, spreadMethod)
<radialGradient> βœ… Compose/Android, ⚠️ Kuikly (fallback to solid)
<clipPath> βœ… Full (rendering with clipping)
<mask> βœ… Parsing (rendering planned)
<filter> βœ… Parsing (rendering planned)
<pattern> βœ… Parsing (rendering planned)

Path Commands

Command Description Support
M/m Move to βœ…
L/l Line to βœ…
H/h Horizontal line βœ…
V/v Vertical line βœ…
C/c Cubic Bezier βœ…
S/s Smooth cubic βœ…
Q/q Quadratic Bezier βœ…
T/t Smooth quadratic βœ…
A/a Elliptical arc βœ…
Z/z Close path βœ…

Transforms

Transform Support
translate(x, y) βœ…
scale(x, y) βœ…
rotate(angle, cx, cy) βœ…
skewX(angle) βœ…
skewY(angle) βœ…
matrix(a,b,c,d,e,f) βœ…

Styles

Property Support
fill βœ… Hex, named colors, url(#gradient)
fill-opacity βœ…
stroke βœ… Hex, named colors, url(#gradient)
stroke-width βœ…
stroke-opacity βœ…
stroke-linecap βœ… butt, round, square
stroke-linejoin βœ… miter, round, bevel
stroke-miterlimit βœ…
opacity βœ…
style (CSS inline) βœ…

Text Properties

Property Support
font-family βœ…
font-size βœ…
font-weight βœ… normal, bold, 100-900
font-style βœ… normal, italic, oblique
text-anchor βœ… start, middle, end
dominant-baseline βœ… auto, middle, hanging, etc.

Filter Primitives (Parsing)

Primitive Support
<feGaussianBlur> βœ… Parsing
<feOffset> βœ… Parsing
<feFlood> βœ… Parsing
<feBlend> βœ… Parsing
<feComposite> βœ… Parsing
<feMerge> βœ… Parsing
<feColorMatrix> βœ… Parsing
<feDropShadow> βœ… Parsing

Not Supported (Yet)

  • CSS classes and external stylesheets
  • Mask rendering (parsing is supported)
  • Filter rendering (parsing is supported)
  • Pattern rendering (parsing is supported)
  • External image URLs (data URIs work)
  • Animations

Platform-Specific Limitations

  • Kuikly: radialGradient not supported (Kuikly's Brush API only has linearGradient). Falls back to first gradient stop color as solid fill.

Demo

Each platform has an independent demo module:

# Run Compose Desktop demo
./gradlew :demo-compose:run

# Build Android demo APK
./gradlew :demo-android:assembleDebug

# Build Kuikly demo (iOS framework)
./gradlew :demo-kuikly:linkDebugFrameworkIosSimulatorArm64

Demo modules showcase:

  • Basic SVG rendering
  • All supported shapes and paths
  • Linear and radial gradients
  • Transforms and groups
  • Text and tspan
  • ClipPath
  • Symbol and use references
  • Tint color override

Architecture

vekt/
β”œβ”€β”€ vekt-core/          # Pure Kotlin parsing, no UI dependencies
β”‚   β”œβ”€β”€ model/          # SVG data models (SvgDocument, PathCommand, etc.)
β”‚   └── parser/         # Parsers (SvgParser, SvgPathParser, TransformParser)
β”œβ”€β”€ vekt-compose/       # Compose Multiplatform adapter
β”œβ”€β”€ vekt-kuikly/        # Kuikly Compose adapter
β”œβ”€β”€ vekt-android/       # Android native View adapter
β”œβ”€β”€ demo-compose/       # Compose Desktop demo app
β”œβ”€β”€ demo-android/       # Android native demo app
└── demo-kuikly/        # Kuikly demo

Performance

Vekt is designed for efficient runtime parsing.

Benchmark Environment

  • JVM: OpenJDK 17+ (tested on OpenJDK 21)
  • OS: macOS 14.5 ARM64 (Apple M2 Pro)
  • Kotlin: 2.1.21
  • Warmup: 100 iterations (JIT optimization)
  • Benchmark: 1000 iterations per test
  • Tests: 404 tests, 100% pass rate

Run benchmarks: ./gradlew :vekt-core:jvmTest --tests "*.ParserBenchmark*"

Synthetic Test Results

Test Case Description Average Median Ops/sec
Simple path M10 20 L30 40 L50 20 Z (4 commands) 2.5Β΅s 2.4Β΅s 398,340
Medium path 2 subpaths with cubic curves (~15 commands) 8.2Β΅s 7.5Β΅s 122,161
Complex path 10+ cubic curves, 8-decimal precision 20.8Β΅s 17.9Β΅s 48,079
Path with scale Complex path with scale=2x, offset=(10,10) 5.8Β΅s 5.9Β΅s 171,296
Simple SVG 104 bytes, 1 path 10.4Β΅s 10.4Β΅s 95,862
Medium SVG 390 bytes, group + shapes 38.4Β΅s 37.5Β΅s 26,021
Complex SVG 782 bytes, multiple paths with transforms 55.9Β΅s 54.6Β΅s 17,875

Real-World SVG Files

File Size Elements Average Median Ops/sec
boy.svg 35KB 23 paths, 23 transforms 683Β΅s 676Β΅s 1,463
confetti-ball.svg 43KB 21 paths, 21 transforms 912Β΅s 891Β΅s 1,096
maps.svg 56KB 19 paths, 19 transforms 919Β΅s 916Β΅s 1,088
swimming-pool.svg 55KB 20 paths, 20 transforms 1.04ms 1.04ms 958
woman.svg 36KB 19 paths, 19 transforms 648Β΅s 638Β΅s 1,543

Performance Characteristics

  • Sub-millisecond parsing: Even complex 50KB+ SVGs parse in under 1ms
  • Memory efficient: Minimal allocations during parsing
  • Cached rendering: Use remember {} in Compose to cache parsed documents
// Recommended: Cache parsed document
val document = remember(svgContent) {
    SvgParser.parse(svgContent)
}

SvgImage(document = document, modifier = Modifier.size(48.dp))

Contributing

Contributions are welcome! Please feel free to submit issues and pull requests.

License

MIT License

Copyright (c) 2024-2025 xuwakao

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

About

🎨 A lightweight, pure Kotlin SVG path renderer for Compose Multiplatform. No Skia. No dependencies. Just vectors.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages