├── .fleet └── run.json ├── .github ├── renovate.json └── workflows │ ├── publish-release.yml │ └── publish-snapshot.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kamel-core ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── core │ │ ├── ApplicationContextInitializer.kt │ │ └── cache │ │ └── httpCache.android.kt │ ├── appleMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── core │ │ ├── cache │ │ └── httpCache.darwin.kt │ │ ├── fetcher │ │ ├── FileFetcher.kt │ │ └── FileUrlFetcher.kt │ │ ├── mapper │ │ └── Mappers.kt │ │ └── utils │ │ └── Platform.kt │ ├── appleTest │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── core │ │ └── utils │ │ └── MappersUtils.kt │ ├── commonJvmMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── core │ │ ├── fetcher │ │ ├── FileUrlFetcher.commonJvm.kt │ │ └── JvmFileFetcher.kt │ │ ├── mapper │ │ └── JvmMappers.kt │ │ └── utils │ │ └── JvmPlatform.kt │ ├── commonJvmTest │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── core │ │ └── utils │ │ └── JvmMappersUtils.kt │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── core │ │ ├── AnimatedImage.kt │ │ ├── DataSource.kt │ │ ├── ExperimentalKamelApi.kt │ │ ├── ImageLoading.kt │ │ ├── Resource.kt │ │ ├── cache │ │ ├── Cache.kt │ │ ├── LruCache.kt │ │ ├── disk │ │ │ ├── DiskCacheStorage.kt │ │ │ ├── DiskLruCache.kt │ │ │ └── FaultHidingSink.kt │ │ └── httpCache.kt │ │ ├── config │ │ ├── KamelConfig.kt │ │ ├── KamelConfigBuilder.kt │ │ ├── ResourceConfig.kt │ │ └── ResourceConfigBuilder.kt │ │ ├── decoder │ │ └── Decoder.kt │ │ ├── fetcher │ │ ├── Fetcher.kt │ │ ├── FileFetcher.kt │ │ ├── FileUrlFetcher.kt │ │ └── HttpFetcher.kt │ │ ├── mapper │ │ ├── Mapper.kt │ │ └── Mappers.kt │ │ └── utils │ │ ├── CacheControl.kt │ │ ├── ConfigUtils.kt │ │ ├── FileSystem.kt │ │ └── Platform.kt │ ├── commonTest │ ├── composeResources │ │ └── files │ │ │ ├── Compose.png │ │ │ ├── ComposeXml.xml │ │ │ └── Kotlin.svg │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── core │ │ ├── cache │ │ ├── LruCacheTest.kt │ │ └── disk │ │ │ └── DiskCacheStorageTest.kt │ │ ├── config │ │ ├── KamelConfigBuilderTest.kt │ │ ├── KamelConfigUtilsTest.kt │ │ └── ResourceConfigBuilderTest.kt │ │ ├── fetcher │ │ └── HttpFetcherTest.kt │ │ ├── mapper │ │ ├── MappersTest.kt │ │ └── StringMapperTest.kt │ │ ├── tests │ │ └── HttpMockEngine.kt │ │ └── utils │ │ └── MappersUtils.kt │ ├── desktopMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── core │ │ └── cache │ │ └── httpCache.desktop.kt │ ├── jsMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── core │ │ ├── cache │ │ └── httpCache.js.kt │ │ ├── fetcher │ │ ├── FileFetcher.kt │ │ └── FileUrlFetcher.kt │ │ ├── mapper │ │ └── Mappers.kt │ │ └── utils │ │ └── Platform.kt │ ├── jsTest │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── core │ │ └── utils │ │ └── MappersUtils.kt │ ├── wasmJsMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── core │ │ ├── cache │ │ └── httpCache.js.kt │ │ ├── fetcher │ │ ├── FileFetcher.kt │ │ └── FileUrlFetcher.kt │ │ ├── mapper │ │ └── Mappers.kt │ │ └── utils │ │ └── Platform.kt │ └── wasmJsTest │ └── kotlin │ └── io │ └── kamel │ └── core │ └── utils │ └── MappersUtils.kt ├── kamel-decoder ├── kamel-decoder-animated-image │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── io │ │ │ └── kamel │ │ │ └── image │ │ │ └── decoder │ │ │ └── AnimatedImageDecoder.android.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── io │ │ │ └── kamel │ │ │ └── image │ │ │ ├── config │ │ │ └── KamelConfigAnimatedImageDecoder.kt │ │ │ └── decoder │ │ │ ├── AnimatedImageDecoder.kt │ │ │ └── BlankBitmap.kt │ │ └── nonAndroidCommonMain │ │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── decoder │ │ ├── AnimatedImage.kt │ │ └── AnimatedImageDecoder.kt ├── kamel-decoder-image-bitmap-resizing │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── io │ │ │ └── kamel │ │ │ └── image │ │ │ └── decoder │ │ │ └── ImageBitmapResizingDecoder.android.kt │ │ └── commonMain │ │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ ├── config │ │ └── KamelConfigImageBitmapResizingDecoder.kt │ │ └── decoder │ │ └── ImageBitmapResizingDecoder.kt ├── kamel-decoder-image-bitmap │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── io │ │ │ └── kamel │ │ │ └── image │ │ │ └── decoder │ │ │ └── AndroidImageBitmapDecoder.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── io │ │ │ └── kamel │ │ │ └── image │ │ │ ├── config │ │ │ └── KamelConfigImageBitmapDecoder.kt │ │ │ └── decoder │ │ │ └── ImageBitmapDecoder.kt │ │ └── nonAndroidMain │ │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── decoder │ │ └── ImageBitmapDecoder.kt ├── kamel-decoder-image-vector │ ├── build.gradle.kts │ └── src │ │ ├── androidMain │ │ └── kotlin │ │ │ └── filterIsElement.kt │ │ ├── appleMain │ │ └── kotlin │ │ │ └── filterIsElement.kt │ │ ├── commonMain │ │ └── kotlin │ │ │ └── io │ │ │ └── kamel │ │ │ └── image │ │ │ ├── config │ │ │ └── KamelConfigImageVectorDecoder.kt │ │ │ └── decoder │ │ │ └── ImageVectorDecoder.kt │ │ ├── jsMain │ │ └── kotlin │ │ │ └── filterIsElement.kt │ │ ├── jvmMain │ │ └── kotlin │ │ │ └── io │ │ │ └── kamel │ │ │ └── image │ │ │ └── decoder │ │ │ └── DesktopImageVectorDecoder.kt │ │ ├── nonJvmAndAndroidMain │ │ └── kotlin │ │ │ ├── ValueParsers.kt │ │ │ ├── XmlVectorParser.kt │ │ │ ├── filterIsElement.kt │ │ │ ├── io │ │ │ └── kamel │ │ │ │ └── image │ │ │ │ └── decoder │ │ │ │ └── ImageVectorDecoder.kt │ │ │ └── loadXmlImageVector.kt │ │ └── wasmJsMain │ │ └── kotlin │ │ └── filterIsElement.kt ├── kamel-decoder-svg-batik │ ├── build.gradle.kts │ └── src │ │ └── jvmMain │ │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ ├── config │ │ └── KamelConfigBaticSvgDecoder.kt │ │ └── decoder │ │ └── BatikSvgDecoder.kt └── kamel-decoder-svg-std │ ├── build.gradle.kts │ └── src │ ├── androidMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── decoder │ │ └── AndroidSvgDecoder.kt │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ ├── config │ │ └── KamelConfigStdSvgDecoder.kt │ │ └── decoder │ │ └── SvgDecoder.kt │ ├── jvmMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── decoder │ │ └── SvgDecoder.jvm.kt │ └── nonJvmMain │ └── kotlin │ ├── DrawCache.kt │ ├── io │ └── kamel │ │ └── image │ │ └── decoder │ │ └── SvgDecoder.kt │ └── loadSvgPainter.kt ├── kamel-fetcher ├── kamel-fetcher-resources-android │ ├── build.gradle.kts │ └── src │ │ └── androidMain │ │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ ├── config │ │ └── KamelConfigResourcesFetcher.kt │ │ └── fetcher │ │ └── ResourcesFetcher.kt └── kamel-fetcher-resources-jvm │ ├── build.gradle.kts │ └── src │ ├── jvmMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ ├── config │ │ └── KamelConfigResourcesFetcher.kt │ │ └── fetcher │ │ └── ResourcesFetcher.kt │ └── jvmTest │ ├── kotlin │ └── io │ │ └── kamel │ │ └── image │ │ └── fetcher │ │ └── ResourcesFetcherTest.kt │ └── resources │ └── Compose.png ├── kamel-image-default ├── build.gradle.kts └── src │ ├── androidMain │ ├── kotlin │ │ └── io │ │ │ └── kamel │ │ │ └── image │ │ │ └── config │ │ │ └── KamelConfig.android.kt │ └── resources │ │ └── META-INF │ │ └── services │ │ └── io.kamel.image.config.KamelConfigService │ ├── appleMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── config │ │ └── KamelConfig.apple.kt │ ├── commonJvmMain │ ├── kotlin │ │ └── io │ │ │ └── kamel │ │ │ └── image │ │ │ └── config │ │ │ └── DefaultKamelConfigService.kt │ └── resources │ │ └── META-INF │ │ └── services │ │ └── io.kamel.image.config.KamelConfigService │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── config │ │ └── KamelConfigImageDefault.kt │ ├── jsMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── config │ │ ├── DefaultKamelConfigServiceInitializer.js.kt │ │ └── KamelConfigImageDefault.js.kt │ ├── jvmMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── config │ │ └── KamelConfig.desktop.kt │ ├── nativeMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── config │ │ └── DefaultKamelConfigServiceInitializer.native.kt │ ├── nonJvmMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── config │ │ └── DefaultKamelConfigServiceInitializer.native.kt │ └── wasmJsMain │ └── kotlin │ └── io │ └── kamel │ └── image │ └── config │ ├── DefaultKamelConfigServiceInitializer.wasmJs.kt │ └── KamelConfigImageDefault.wasmJs.kt ├── kamel-image ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── config │ │ └── DetectedKamelConfig.android.kt │ ├── commonJvmMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── config │ │ └── KamelConfigService.kt │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ ├── AsyncPainterResource.kt │ │ ├── KamelImage.kt │ │ └── config │ │ └── LocalKamelConfig.kt │ ├── commonTest │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── KamelImageTest.kt │ ├── desktopJvmMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── image │ │ └── config │ │ └── DetectedKamelConfig.desktopJvm.kt │ └── nonJvmMain │ └── kotlin │ └── io │ └── kamel │ └── image │ └── config │ └── DetectedKamelConfig.nonJvm.kt ├── kamel-mapper └── kamel-mapper-resources-id-android │ ├── build.gradle.kts │ └── src │ └── androidMain │ └── kotlin │ └── io │ └── kamel │ └── image │ ├── config │ └── KamelConfigResourcesIdMapper.kt │ └── mapper │ └── ResourcesIdMapper.kt ├── kamel-sample-ios ├── .gitignore ├── Configuration │ └── Config.xcconfig ├── iosApp.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── Info.plist │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift ├── kamel-samples ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── kotlin │ │ └── io │ │ │ └── kamel │ │ │ └── samples │ │ │ ├── AndroidSample.kt │ │ │ ├── Utils.android.kt │ │ │ └── getResourceFile.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── raw │ │ └── compose.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ ├── appleMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── samples │ │ └── darwinCellsCount.kt │ ├── commonMain │ ├── composeResources │ │ └── files │ │ │ ├── Compose.png │ │ │ ├── ComposeXml.xml │ │ │ ├── Kotlin.svg │ │ │ └── XlImage.png │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── samples │ │ ├── FileSample.kt │ │ ├── Gallery.kt │ │ ├── UrlFileSample.kt │ │ ├── Utils.kt │ │ ├── getResourceFile.kt │ │ └── launcher.kt │ ├── desktopMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── samples │ │ ├── CustomImageLoading.kt │ │ ├── DesktopSample.kt │ │ ├── ImageVectorSample.kt │ │ ├── ResourcesSample.kt │ │ ├── SvgSample.kt │ │ ├── Utils.desktop.kt │ │ └── getResourceFile.kt │ ├── iosMain │ └── kotlin │ │ ├── io │ │ └── kamel │ │ │ └── samples │ │ │ └── getResourceFile.ios.kt │ │ └── main.ios.kt │ ├── jsMain │ ├── kotlin │ │ ├── io │ │ │ └── kamel │ │ │ │ └── samples │ │ │ │ ├── Utils.js.kt │ │ │ │ ├── cellsCount.kt │ │ │ │ └── getResourceFile.kt │ │ └── main.js.kt │ └── resources │ │ ├── index.html │ │ └── styles.css │ ├── macosMain │ └── kotlin │ │ ├── io │ │ └── kamel │ │ │ └── samples │ │ │ └── getResourceFile.macos.kt │ │ └── main.macos.kt │ ├── nativeMain │ └── kotlin │ │ └── io │ │ └── kamel │ │ └── samples │ │ └── Utils.native.kt │ └── wasmJsMain │ ├── kotlin │ ├── io │ │ └── kamel │ │ │ └── samples │ │ │ ├── Utils.wasmJs.kt │ │ │ ├── cellsCount.kt │ │ │ └── getResourceFile.kt │ └── main.wasmJs.kt │ └── resources │ ├── index.html │ └── styles.css ├── kotlin-js-store └── package-lock.json └── settings.gradle.kts /.fleet/run.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "kamel-samples:run", 5 | "type": "gradle", 6 | "workingDir": "$PROJECT_DIR$", 7 | "tasks": [ 8 | "kamel-samples:run" 9 | ], 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseBranches": [ 3 | "dev" 4 | ] 5 | } -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release to Maven Central 2 | on: 3 | push: 4 | branches: 5 | - release 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | name: Build and Publish Release 10 | steps: 11 | - name: Checkout Repository 12 | uses: actions/checkout@v4 13 | - name: Setup JDK 14 | uses: actions/setup-java@v4 15 | with: 16 | distribution: 'temurin' 17 | java-version: '21' 18 | cache: 'gradle' 19 | 20 | - name: Publish Release 21 | run: ./gradlew publishAndReleaseToMavenCentral 22 | env: 23 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }} 24 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }} 25 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} 26 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY_CONTENTS }} 27 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Publish Snapshot to Maven Central 2 | on: 3 | push: 4 | branches: 5 | - dev 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | name: Build and publish snapshot 10 | steps: 11 | - name: Checkout Repository 12 | uses: actions/checkout@v4 13 | - name: Setup JDK 14 | uses: actions/setup-java@v4 15 | with: 16 | distribution: 'temurin' 17 | java-version: '21' 18 | cache: 'gradle' 19 | 20 | - name: Publish snapshot 21 | run: ./gradlew publish 22 | env: 23 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }} 24 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }} 25 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} 26 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY_CONTENTS }} 27 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.idea/* 2 | *.fleet/* 3 | build/ 4 | .gradle/* 5 | */.gradle/* 6 | local.properties 7 | *.iml 8 | *.gpg 9 | *.DS_STORE 10 | .kotlin 11 | 12 | # IntelliJ Code Styles 13 | .kotlin/ 14 | 15 | !.fleet/run.json 16 | versions.properties 17 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | 4 | plugins { 5 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) apply false 6 | alias(libs.plugins.com.android.application) apply false 7 | alias(libs.plugins.com.android.library) apply false 8 | alias(libs.plugins.org.jetbrains.compose) apply false 9 | alias(libs.plugins.compose.compiler) apply false 10 | alias(libs.plugins.com.vanniktech.maven.publish) apply false 11 | } 12 | 13 | 14 | allprojects { 15 | 16 | group = project.property("GROUP") as String 17 | version = project.property("VERSION_NAME") as String 18 | 19 | 20 | afterEvaluate { 21 | extensions.findByType()?.apply { 22 | publications.withType().configureEach { 23 | 24 | pom { 25 | 26 | name.set("Kamel") 27 | description.set("Kotlin Asynchronous Media Loading Library that supports Compose") 28 | url.set("https://github.com/Kamel-Media/Kamel") 29 | 30 | licenses { 31 | license { 32 | name.set("The Apache Software License, Version 2.0") 33 | url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") 34 | distribution.set("repo") 35 | } 36 | } 37 | 38 | developers { 39 | developer { 40 | id.set("alialbaali") 41 | name.set("Ali Albaali") 42 | } 43 | developer { 44 | id.set("luca992") 45 | name.set("Luca Spinazzola") 46 | } 47 | } 48 | 49 | scm { 50 | connection.set("scm:git:github.com/Kamel-Media/Kamel.git") 51 | developerConnection.set("scm:git:ssh://github.com/Kamel-Media/Kamel.git") 52 | url.set("https://github.com/Kamel-Media/Kamel/tree/main") 53 | } 54 | 55 | } 56 | 57 | } 58 | 59 | } 60 | } 61 | 62 | tasks.withType { 63 | compilerOptions.jvmTarget = JvmTarget.JVM_11 64 | } 65 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx3g 2 | org.gradle.parallel=true 3 | kotlin.code.style=official 4 | compose.desktop.verbose=true 5 | android.useAndroidX=true 6 | kotlin.mpp.stability.nowarn=true 7 | kotlin.js.yarn=false 8 | kotlin.native.enableKlibsCrossCompilation=true 9 | 10 | org.jetbrains.compose.experimental.jscanvas.enabled=true 11 | org.jetbrains.compose.experimental.macos.enabled=true 12 | org.jetbrains.compose.experimental.uikit.enabled=true 13 | org.jetbrains.compose.experimental.wasm.enabled=true 14 | 15 | GROUP=media.kamel 16 | VERSION_NAME=1.0.5 17 | 18 | SONATYPE_HOST=S01 19 | RELEASE_SIGNING_ENABLED=true 20 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | ## Generated by $ ./gradlew refreshVersionsCatalog 2 | 3 | [plugins] 4 | 5 | org-jetbrains-kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 6 | org-jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose" } 7 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 8 | com-vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech-publish" } 9 | com-android-library = { id = "com.android.library", version.ref = "agp" } 10 | com-android-application = { id = "com.android.application", version.ref = "agp" } 11 | 12 | [versions] 13 | 14 | kotlin = "2.1.20" 15 | agp = "8.9.2" 16 | 17 | okio = "3.11.0" 18 | startup-runtime = "1.2.0" 19 | vanniktech-publish = "0.31.0" 20 | compose = "1.8.0" 21 | coroutines = "1.10.2" 22 | ktor = "3.1.3" 23 | cache4k = "0.14.0" 24 | xmlutil = "0.91.0" 25 | activity-compose = "1.10.1" 26 | appcompat = "1.7.0" 27 | material = "1.12.0" 28 | androidsvg = "1.4" 29 | batik = "1.19" 30 | slf4j = "2.0.17" 31 | annotation = "1.9.1" 32 | core-ctx = "1.16.0" 33 | ui-tooling = "1.8.1" 34 | 35 | [libraries] 36 | 37 | okio = { module = "com.squareup.okio:okio", version.ref = "okio" } 38 | okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okio" } 39 | 40 | cache4k = { module = "io.github.reactivecircus.cache4k:cache4k", version.ref = "cache4k" } 41 | 42 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } 43 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } 44 | 45 | ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } 46 | ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } 47 | ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } 48 | ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } 49 | ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } 50 | ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } 51 | 52 | pdvrieze-xmlutil-serialization = { module = "io.github.pdvrieze.xmlutil:serialization", version.ref = "xmlutil" } 53 | 54 | androidx-startup = { module = "androidx.startup:startup-runtime", version.ref = "startup-runtime" } 55 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } 56 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } 57 | androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" } 58 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ctx" } 59 | 60 | google-android-material = { module = "com.google.android.material:material", version.ref = "material" } 61 | com-caverok-androidsvg = { module = "com.caverock:androidsvg-aar", version.ref = "androidsvg" } 62 | apache-batik-transcoder = { module = "org.apache.xmlgraphics:batik-transcoder", version.ref = "batik" } 63 | apache-batik-codec = { module = "org.apache.xmlgraphics:batik-codec", version.ref = "batik" } 64 | slf4j = { module = "org.slf4j:slf4j-nop", version.ref = "slf4j" } 65 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "ui-tooling" } 66 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/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-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 | -------------------------------------------------------------------------------- /kamel-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 2 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 3 | 4 | plugins { 5 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 6 | alias(libs.plugins.org.jetbrains.compose) 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.com.android.library) 9 | alias(libs.plugins.com.vanniktech.maven.publish) 10 | } 11 | 12 | kotlin { 13 | 14 | explicitApi = ExplicitApiMode.Warning 15 | 16 | androidTarget { 17 | publishAllLibraryVariants() 18 | } 19 | jvm("desktop") 20 | js(IR) { 21 | browser() 22 | } 23 | @OptIn(ExperimentalWasmDsl::class) 24 | wasmJs { 25 | browser() 26 | } 27 | iosArm64() 28 | iosSimulatorArm64() 29 | iosX64() 30 | macosX64() 31 | macosArm64() 32 | 33 | applyDefaultHierarchyTemplate() 34 | 35 | sourceSets { 36 | all { 37 | languageSettings.apply { 38 | optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") 39 | } 40 | } 41 | 42 | val commonMain by getting { 43 | dependencies { 44 | implementation(compose.foundation) 45 | implementation(libs.kotlinx.coroutines.core) 46 | implementation(libs.ktor.client.core) 47 | implementation(libs.okio) 48 | implementation(libs.cache4k) 49 | // todo: remove this https://youtrack.jetbrains.com/issue/CMP-4442 50 | implementation(compose.components.resources) 51 | } 52 | } 53 | 54 | val commonTest by getting { 55 | dependencies { 56 | implementation(kotlin("test")) 57 | implementation(libs.ktor.client.mock) 58 | implementation(libs.kotlinx.coroutines.test) 59 | implementation(libs.okio.fakefilesystem) 60 | implementation(compose.components.resources) 61 | } 62 | } 63 | 64 | val commonJvmMain = create("commonJvmMain") { 65 | dependsOn(commonMain) 66 | } 67 | 68 | val commonJvmTest = create("commonJvmTest") { 69 | dependsOn(commonTest) 70 | } 71 | 72 | val desktopMain by getting { 73 | dependsOn(commonJvmMain) 74 | } 75 | 76 | val desktopTest by getting { 77 | dependsOn(commonJvmTest) 78 | } 79 | 80 | val androidMain by getting { 81 | dependsOn(commonJvmMain) 82 | dependencies { 83 | implementation(libs.androidx.startup) 84 | } 85 | } 86 | 87 | val androidUnitTest by getting { 88 | dependsOn(commonJvmTest) 89 | } 90 | 91 | val nonJvmMain by creating { 92 | dependsOn(commonMain) 93 | } 94 | 95 | val nonJvmTest by creating { 96 | dependsOn(commonTest) 97 | } 98 | 99 | val jsMain by getting { 100 | dependsOn(nonJvmMain) 101 | } 102 | 103 | val wasmJsMain by getting { 104 | dependsOn(nonJvmMain) 105 | } 106 | 107 | val appleMain by getting { 108 | dependsOn(nonJvmMain) 109 | } 110 | 111 | val appleTest by getting { 112 | dependsOn(nonJvmTest) 113 | } 114 | 115 | } 116 | } 117 | 118 | android { 119 | namespace = "io.kamel.core.cache" 120 | compileSdk = 36 121 | 122 | defaultConfig { 123 | minSdk = 21 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /kamel-core/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /kamel-core/src/androidMain/kotlin/io/kamel/core/ApplicationContextInitializer.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core 2 | 3 | import android.content.Context 4 | import androidx.startup.Initializer 5 | 6 | 7 | public var applicationContext: Context? = null 8 | 9 | internal class ApplicationContextInitializer : Initializer { 10 | override fun create(context: Context): Context = context.also { 11 | applicationContext = it 12 | } 13 | 14 | override fun dependencies(): List>> = emptyList() 15 | } -------------------------------------------------------------------------------- /kamel-core/src/androidMain/kotlin/io/kamel/core/cache/httpCache.android.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.cache 2 | 3 | import io.kamel.core.applicationContext 4 | import io.kamel.core.cache.disk.DiskCacheStorage 5 | import io.ktor.client.plugins.cache.storage.* 6 | import okio.FileSystem 7 | import okio.Path.Companion.toOkioPath 8 | 9 | private val cacheDir = applicationContext?.cacheDir?.toOkioPath() 10 | 11 | internal actual fun httpCacheStorage(maxSize: Long): CacheStorage { 12 | return if (cacheDir == null) { 13 | println( 14 | "Warning: applicationContext is null, DiskCacheStorage is disabled") 15 | CacheStorage.Disabled 16 | } else { 17 | DiskCacheStorage( 18 | fileSystem = FileSystem.SYSTEM, 19 | directory = cacheDir, 20 | maxSize = maxSize 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /kamel-core/src/appleMain/kotlin/io/kamel/core/cache/httpCache.darwin.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.cache 2 | 3 | import io.kamel.core.cache.disk.DiskCacheStorage 4 | import io.ktor.client.plugins.cache.storage.* 5 | import okio.FileSystem 6 | import okio.Path 7 | import okio.Path.Companion.toPath 8 | import platform.Foundation.NSCachesDirectory 9 | import platform.Foundation.NSSearchPathForDirectoriesInDomains 10 | import platform.Foundation.NSUserDomainMask 11 | 12 | private val cacheDir: Path = NSSearchPathForDirectoriesInDomains( 13 | directory = NSCachesDirectory, 14 | domainMask = NSUserDomainMask, 15 | expandTilde = true 16 | ).first().toString().toPath() 17 | 18 | 19 | internal actual fun httpCacheStorage(maxSize: Long): CacheStorage = DiskCacheStorage( 20 | fileSystem = FileSystem.SYSTEM, 21 | directory = cacheDir, 22 | maxSize = maxSize 23 | ) -------------------------------------------------------------------------------- /kamel-core/src/appleMain/kotlin/io/kamel/core/fetcher/FileFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.Resource 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.utils.File 7 | import io.ktor.utils.io.* 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.flow 10 | import kotlin.reflect.KClass 11 | 12 | /** 13 | * Fetcher that fetchers [ByteReadChannel] from a file. 14 | */ 15 | internal actual val FileFetcher = object : Fetcher { 16 | 17 | override val inputDataKClass: KClass = File::class 18 | 19 | override val source: DataSource = DataSource.Disk 20 | 21 | override val File.isSupported: Boolean 22 | get() = true 23 | 24 | override fun fetch( 25 | data: File, resourceConfig: ResourceConfig 26 | ): Flow> = flow { 27 | val byteReadChannel = ByteReadChannel(data.availableData) 28 | emit(Resource.Success(byteReadChannel, source)) 29 | } 30 | } -------------------------------------------------------------------------------- /kamel-core/src/appleMain/kotlin/io/kamel/core/fetcher/FileUrlFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.Resource 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.utils.File 7 | import io.ktor.http.* 8 | import io.ktor.utils.io.* 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlin.reflect.KClass 11 | 12 | /** 13 | * Fetcher that fetches [ByteReadChannel] from the localhost using [Url]. 14 | */ 15 | internal actual val FileUrlFetcher = object : Fetcher { 16 | override val inputDataKClass: KClass = Url::class 17 | 18 | override val source: DataSource = DataSource.Disk 19 | 20 | override val Url.isSupported: Boolean 21 | get() = protocol.name == "file" 22 | 23 | override fun fetch(data: Url, resourceConfig: ResourceConfig): Flow> { 24 | return FileFetcher.fetch(File(data.encodedPath), resourceConfig) 25 | } 26 | } -------------------------------------------------------------------------------- /kamel-core/src/appleMain/kotlin/io/kamel/core/mapper/Mappers.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.mapper 2 | 3 | import io.kamel.core.utils.URI 4 | import io.kamel.core.utils.URL 5 | import io.ktor.http.* 6 | import kotlin.reflect.KClass 7 | 8 | internal actual val URLMapper: Mapper = object : Mapper { 9 | override val inputKClass: KClass 10 | get() = URL::class 11 | override val outputKClass: KClass 12 | get() = Url::class 13 | 14 | override fun map(input: URL): Url = StringMapper.map(input.absoluteString()!!) 15 | } 16 | 17 | 18 | internal actual val URIMapper: Mapper = object : Mapper { 19 | override val inputKClass: KClass 20 | get() = URI::class 21 | override val outputKClass: KClass 22 | get() = Url::class 23 | 24 | override fun map(input: URI): Url = StringMapper.map(input.str) 25 | } -------------------------------------------------------------------------------- /kamel-core/src/appleMain/kotlin/io/kamel/core/utils/Platform.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | import kotlinx.cinterop.ExperimentalForeignApi 4 | import kotlinx.cinterop.addressOf 5 | import kotlinx.cinterop.usePinned 6 | import kotlinx.coroutines.CoroutineDispatcher 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.IO 9 | import platform.Foundation.NSData 10 | import platform.Foundation.NSFileHandle 11 | import platform.Foundation.NSURL 12 | import platform.Foundation.fileHandleForReadingAtPath 13 | import platform.posix.memcpy 14 | 15 | 16 | internal actual val Dispatchers.Kamel: CoroutineDispatcher get() = IO 17 | 18 | @OptIn(ExperimentalForeignApi::class) 19 | public actual class File(public val path: String) { 20 | 21 | private val fileHandle: NSFileHandle? = NSFileHandle.fileHandleForReadingAtPath(path) 22 | public val availableData: ByteArray get() = fileHandle?.availableData?.toByteArray() ?: byteArrayOf() 23 | override fun toString(): String = path 24 | // memScoped { 25 | // fileHandle ?: return@memScoped "null" 26 | // println("File.toString()") 27 | // val buffer = CharArray(MAXPATHLEN) { 0.toChar() } 28 | // val result = buffer.usePinned { pinned -> 29 | // fcntl(fileHandle.fileDescriptor, F_GETPATH, pinned.addressOf(0)) 30 | // } 31 | // println(result) 32 | // return@memScoped buffer.joinToString("") 33 | // } 34 | 35 | private fun NSData.toByteArray(): ByteArray = ByteArray(this@toByteArray.length.toInt()).apply { 36 | usePinned { 37 | memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length) 38 | } 39 | } 40 | 41 | } 42 | 43 | public actual class URL(public val nsUrl: NSURL) { 44 | public fun absoluteString(): String? = nsUrl.absoluteString 45 | } 46 | 47 | public actual class URI actual constructor(public val str: String) 48 | -------------------------------------------------------------------------------- /kamel-core/src/appleTest/kotlin/io/kamel/core/utils/MappersUtils.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | import platform.Foundation.NSURL 4 | 5 | 6 | internal actual fun createURL(url: String): URL = URL(NSURL.URLWithString(url)!!) 7 | 8 | internal actual fun createURI(url: String): URI = URI(url) -------------------------------------------------------------------------------- /kamel-core/src/commonJvmMain/kotlin/io/kamel/core/fetcher/FileUrlFetcher.commonJvm.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.Resource 5 | import io.kamel.core.config.ResourceConfig 6 | import io.ktor.http.* 7 | import io.ktor.utils.io.* 8 | import kotlinx.coroutines.flow.Flow 9 | import java.io.File 10 | import kotlin.reflect.KClass 11 | 12 | /** 13 | * Fetcher that fetches [ByteReadChannel] from the localhost using [Url]. 14 | */ 15 | internal actual val FileUrlFetcher = object : Fetcher { 16 | override val inputDataKClass: KClass = Url::class 17 | 18 | override val source: DataSource = DataSource.Disk 19 | 20 | override val Url.isSupported: Boolean 21 | get() = protocol.name == "file" 22 | 23 | override fun fetch(data: Url, resourceConfig: ResourceConfig): Flow> { 24 | return FileFetcher.fetch(File(data.toURI()), resourceConfig) 25 | } 26 | } -------------------------------------------------------------------------------- /kamel-core/src/commonJvmMain/kotlin/io/kamel/core/fetcher/JvmFileFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.Resource 5 | import io.kamel.core.config.ResourceConfig 6 | import io.ktor.util.cio.* 7 | import io.ktor.utils.io.* 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.flow 10 | import java.io.File 11 | import kotlin.reflect.KClass 12 | 13 | /** 14 | * Fetcher that fetchers [ByteReadChannel] from a file. 15 | */ 16 | internal actual val FileFetcher = object : Fetcher { 17 | 18 | override val inputDataKClass: KClass = File::class 19 | 20 | override val source: DataSource = DataSource.Disk 21 | 22 | override val File.isSupported: Boolean 23 | get() = exists() && isFile 24 | 25 | override fun fetch( 26 | data: File, resourceConfig: ResourceConfig 27 | ): Flow> = flow { 28 | val bytes = data.readChannel(coroutineContext = resourceConfig.coroutineContext) 29 | emit(Resource.Success(bytes, source)) 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /kamel-core/src/commonJvmMain/kotlin/io/kamel/core/mapper/JvmMappers.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.mapper 2 | 3 | import io.kamel.core.utils.URI 4 | import io.kamel.core.utils.URL 5 | import io.ktor.http.* 6 | import kotlin.reflect.KClass 7 | 8 | internal actual val URLMapper: Mapper = object : Mapper { 9 | override val inputKClass: KClass 10 | get() = URL::class 11 | override val outputKClass: KClass 12 | get() = Url::class 13 | 14 | override fun map(input: URL): Url = Url(input.toURI()) 15 | } 16 | 17 | internal actual val URIMapper: Mapper = object : Mapper { 18 | override val inputKClass: KClass 19 | get() = URI::class 20 | override val outputKClass: KClass 21 | get() = Url::class 22 | 23 | override fun map(input: URI): Url = Url(input) 24 | } -------------------------------------------------------------------------------- /kamel-core/src/commonJvmMain/kotlin/io/kamel/core/utils/JvmPlatform.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | import java.net.URI 6 | import java.net.URL 7 | 8 | internal actual val Dispatchers.Kamel: CoroutineDispatcher get() = IO 9 | 10 | public actual typealias File = java.io.File 11 | 12 | public actual typealias URL = URL 13 | 14 | public actual typealias URI = URI -------------------------------------------------------------------------------- /kamel-core/src/commonJvmTest/kotlin/io/kamel/core/utils/JvmMappersUtils.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | import java.net.URI 4 | import java.net.URL 5 | 6 | internal actual fun createURL(url: String): URL = URL(url) 7 | 8 | internal actual fun createURI(url: String): URI = URI(url) -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/AnimatedImage.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core; 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.graphics.ImageBitmap 5 | 6 | public interface AnimatedImage { 7 | @Composable 8 | public fun animate(): ImageBitmap 9 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/DataSource.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core 2 | 3 | import io.kamel.core.cache.Cache 4 | import io.kamel.core.utils.File 5 | import io.ktor.http.* 6 | 7 | /** 8 | * Represents the source from where data has been loaded. 9 | */ 10 | public enum class DataSource { 11 | 12 | /** 13 | * Represents an in-memory data source (e.g. [Cache]). 14 | */ 15 | Memory, 16 | 17 | /** 18 | * Represents a disk data source (e.g. [File]). 19 | */ 20 | Disk, 21 | 22 | /** 23 | * Represents a network data source (e.g. [Url]). 24 | */ 25 | Network, 26 | 27 | /** 28 | * Represents no data source. 29 | */ 30 | None, 31 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/ExperimentalKamelApi.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core 2 | 3 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) 4 | @RequiresOptIn( 5 | message = "This is an experimental Kamel API, and it is likely to be changed or removed in the future.", 6 | level = RequiresOptIn.Level.WARNING 7 | ) 8 | public annotation class ExperimentalKamelApi -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/ImageLoading.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import androidx.compose.ui.graphics.painter.Painter 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import io.kamel.core.cache.Cache 7 | import io.kamel.core.config.KamelConfig 8 | import io.kamel.core.config.ResourceConfig 9 | import io.kamel.core.decoder.Decoder 10 | import io.kamel.core.fetcher.Fetcher 11 | import io.kamel.core.mapper.Mapper 12 | import io.kamel.core.utils.findDecoderFor 13 | import io.kamel.core.utils.findFetcherFor 14 | import io.kamel.core.utils.mapInput 15 | import kotlinx.coroutines.flow.* 16 | import kotlin.reflect.KClass 17 | 18 | /** 19 | * Loads an [ImageBitmap]. This includes mapping, fetching, decoding and caching the image resource. 20 | * @see Fetcher 21 | * @see Decoder 22 | * @see Mapper 23 | * @see Cache 24 | */ 25 | public fun KamelConfig.loadImageBitmapResource( 26 | data: I, 27 | resourceConfig: ResourceConfig, 28 | dataKClass: KClass<*> = data::class, 29 | ): Flow> = loadResource(data, dataKClass, resourceConfig, imageBitmapCache) 30 | 31 | /** 32 | * Loads an [ImageVector]. This includes mapping, fetching, decoding and caching the image resource. 33 | * @see Fetcher 34 | * @see Decoder 35 | * @see Mapper 36 | * @see Cache 37 | */ 38 | public fun KamelConfig.loadImageVectorResource( 39 | data: Any, 40 | resourceConfig: ResourceConfig, 41 | dataKClass: KClass<*> = data::class 42 | ): Flow> = loadResource(data, dataKClass, resourceConfig, imageVectorCache) 43 | 44 | /** 45 | * Loads SVG [Painter]. This includes mapping, fetching, decoding and caching the image resource. 46 | * @see Fetcher 47 | * @see Decoder 48 | * @see Mapper 49 | * @see Cache 50 | */ 51 | public fun KamelConfig.loadSvgResource( 52 | data: Any, 53 | resourceConfig: ResourceConfig, 54 | dataKClass: KClass<*> = data::class 55 | ): Flow> = loadResource(data, dataKClass, resourceConfig, svgCache) 56 | 57 | /** 58 | * Loads a gif [AnimatedImage]. This includes mapping, fetching, decoding and caching the image resource. 59 | * @see Fetcher 60 | * @see Decoder 61 | * @see Mapper 62 | * @see Cache 63 | */ 64 | public fun KamelConfig.loadAnimatedImageResource( 65 | data: Any, 66 | resourceConfig: ResourceConfig, 67 | dataKClass: KClass<*> = data::class 68 | ): Flow> = loadResource(data, dataKClass, resourceConfig, animatedImageCache) 69 | 70 | private inline fun KamelConfig.loadResource( 71 | data: Any, 72 | dataKClass: KClass<*>, 73 | resourceConfig: ResourceConfig, 74 | cache: Cache, 75 | ): Flow> = flow { 76 | val output = mapInput(data, dataKClass) 77 | val cachedData = cache[output] 78 | if (cachedData != null) { 79 | val resource = Resource.Success(cachedData, DataSource.Memory) 80 | emit(resource) 81 | } else { 82 | val fetcher = findFetcherFor(output) 83 | val decoder = findDecoderFor() 84 | val bytesFlow = fetcher.fetch(output, resourceConfig) 85 | val dataFlow = bytesFlow.map { resource -> 86 | resource.map { channel -> 87 | decoder.decode(channel, resourceConfig).also { 88 | cache[output] = it 89 | } 90 | } 91 | } 92 | emitAll(dataFlow) 93 | } 94 | }.catch { emit(Resource.Failure(it)) } 95 | 96 | /** 97 | * Loads a cached [ImageBitmap], [ImageVector], or SVG [Painter] from memory. This includes mapping and loading the 98 | * cached image resource. If no resource has been cached for the provided data, `null` is returned. 99 | * @see Mapper 100 | * @see Cache 101 | */ 102 | public fun KamelConfig.loadCachedResourceOrNull( 103 | data: Any, 104 | cache: Cache, 105 | dataKClass: KClass<*> = data::class, 106 | ): Resource? { 107 | val output = mapInput(data, dataKClass) 108 | return cache[output]?.let { Resource.Success(it, DataSource.Memory) } 109 | } 110 | -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/Resource.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core 2 | 3 | import io.kamel.core.Resource.* 4 | 5 | /** 6 | * A class represents an asynchronous resource loading. 7 | */ 8 | public sealed interface Resource { 9 | 10 | /** 11 | * Source from where data has been loaded. 12 | */ 13 | public val source: DataSource 14 | 15 | /** 16 | * Represents the resource is still in the loading state. 17 | */ 18 | public data class Loading( 19 | public val progress: Float, 20 | public override val source: DataSource = DataSource.None, 21 | ) : Resource 22 | 23 | /** 24 | * Represents the resource as a successful outcome. 25 | */ 26 | public data class Success( 27 | public val value: T, 28 | public override val source: DataSource = DataSource.None, 29 | ) : Resource 30 | 31 | /** 32 | * Represents the resource as a failure outcome. 33 | */ 34 | public data class Failure( 35 | public val exception: Throwable, 36 | public override val source: DataSource = DataSource.None, 37 | ) : Resource 38 | } 39 | 40 | /** 41 | * Returns true if the resource still in the loading state, false otherwise. 42 | */ 43 | public val Resource<*>.isLoading: Boolean 44 | get() = this is Loading 45 | 46 | /** 47 | * Returns true if the resource represents a successful outcome, false otherwise. 48 | */ 49 | public val Resource<*>.isSuccess: Boolean 50 | get() = this is Success 51 | 52 | /** 53 | * Returns true if the resource represents a failure outcome, false otherwise. 54 | */ 55 | public val Resource<*>.isFailure: Boolean 56 | get() = this is Failure 57 | 58 | /** 59 | * Returns [Success] with the [transform] function applied on the value if the resource represents success. 60 | * or [Failure] with the original exception if the resource represents failure. 61 | */ 62 | public inline fun Resource.map(transform: (T) -> R): Resource = when (this) { 63 | is Loading -> Loading(progress, source) 64 | is Success -> Success(transform(value), source) 65 | is Failure -> Failure(exception, source) 66 | } 67 | 68 | /** 69 | * Returns value if the resource is [Success] or `null` otherwise. 70 | */ 71 | public fun Resource.getOrNull(): T? = when (this) { 72 | is Loading -> null 73 | is Success -> value 74 | is Failure -> null 75 | } 76 | 77 | /** 78 | * Returns value if the resource is [Success], result of [onFailure] function if it is [Failure] 79 | * or result of [onLoading] function if it is [Loading]. 80 | */ 81 | public inline fun Resource.getOrElse( 82 | onLoading: (Float) -> T, 83 | onFailure: (Throwable) -> T, 84 | ): T = when (this) { 85 | is Loading -> onLoading(progress) 86 | is Success -> value 87 | is Failure -> onFailure(exception) 88 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/cache/Cache.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.cache 2 | 3 | /** 4 | * A generic cache interface. 5 | */ 6 | public interface Cache { 7 | 8 | /** 9 | * Current size of cache entries. 10 | */ 11 | public val size: Int 12 | 13 | /** 14 | * Maximum size of cache entries. 15 | */ 16 | public val maxSize: Int 17 | 18 | /** 19 | * Returns the value corresponding to the given [key], or `null` if there's no value associated with this [key]. 20 | */ 21 | public operator fun get(key: K): V? 22 | 23 | /** 24 | * Associates the specified [key] with the specified [value] in the cache. 25 | */ 26 | public operator fun set(key: K, value: V) 27 | 28 | /** 29 | * Removes the specified key and its corresponding value from this cache. 30 | * 31 | * @return true if the key was present in the cache, or `null` if there's no value associated with this [key]. 32 | */ 33 | public fun remove(key: K): Boolean 34 | 35 | /** 36 | * Removes all the entries in the cache. 37 | */ 38 | public fun clear() 39 | 40 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/cache/LruCache.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.cache 2 | 3 | /** 4 | * Cache implementation which evicts items using an LRU algorithm. 5 | */ 6 | internal class LruCache(override val maxSize: Int) : Cache { 7 | 8 | private val cache: io.github.reactivecircus.cache4k.Cache = 9 | io.github.reactivecircus.cache4k.Cache.Builder() 10 | .maximumCacheSize(maxSize.toLong()) 11 | .build() 12 | 13 | override val size: Int 14 | get() = cache.asMap().size 15 | 16 | init { 17 | require(maxSize >= 0) { "Cache max size must be positive number" } 18 | } 19 | 20 | override fun get(key: K): V? = cache.get(key) 21 | 22 | override fun set(key: K, value: V) = cache.put(key, value) 23 | 24 | override fun remove(key: K): Boolean { 25 | cache.invalidate(key) 26 | return true 27 | } 28 | 29 | override fun clear(): Unit = cache.invalidateAll() 30 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/cache/disk/FaultHidingSink.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.cache.disk 2 | 3 | import okio.Buffer 4 | import okio.IOException 5 | import okio.Sink 6 | 7 | /** A sink that never throws [IOException]s, even if the underlying sink does. */ 8 | internal class FaultHidingSink( 9 | private val delegate: Sink, 10 | private val onException: (IOException) -> Unit 11 | ) : Sink by delegate { 12 | 13 | private var hasErrors = false 14 | 15 | override fun write(source: Buffer, byteCount: Long) { 16 | if (hasErrors) { 17 | source.skip(byteCount) 18 | return 19 | } 20 | try { 21 | delegate.write(source, byteCount) 22 | } catch (e: IOException) { 23 | hasErrors = true 24 | onException(e) 25 | } 26 | } 27 | 28 | override fun flush() { 29 | try { 30 | delegate.flush() 31 | } catch (e: IOException) { 32 | hasErrors = true 33 | onException(e) 34 | } 35 | } 36 | 37 | override fun close() { 38 | try { 39 | delegate.close() 40 | } catch (e: IOException) { 41 | hasErrors = true 42 | onException(e) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/cache/httpCache.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.cache 2 | 3 | import io.ktor.client.plugins.cache.storage.CacheStorage 4 | 5 | internal expect fun httpCacheStorage(maxSize:Long): CacheStorage -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/config/KamelConfig.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.config 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import androidx.compose.ui.graphics.painter.Painter 5 | import androidx.compose.ui.graphics.vector.ImageVector 6 | import io.kamel.core.AnimatedImage 7 | import io.kamel.core.cache.Cache 8 | import io.kamel.core.decoder.Decoder 9 | import io.kamel.core.fetcher.Fetcher 10 | import io.kamel.core.mapper.Mapper 11 | import kotlin.reflect.KClass 12 | 13 | public const val DefaultCacheSize: Int = 100 14 | public const val DefaultHttpCacheSize: Long = 10 * 1024 * 1024 //10 MiB 15 | 16 | /** 17 | * Represents global configuration for this library. 18 | * @see KamelConfig to configure one. 19 | */ 20 | public interface KamelConfig { 21 | 22 | public val fetchers: List> 23 | 24 | public val decoders: List> 25 | 26 | public val mappers: Map, List>> 27 | 28 | /** 29 | * Number of entries to cache. Default is 100. 30 | */ 31 | public val imageBitmapCache: Cache 32 | 33 | public val imageVectorCache: Cache 34 | 35 | public val svgCache: Cache 36 | 37 | public val animatedImageCache: Cache 38 | 39 | public companion object 40 | } 41 | 42 | /** 43 | * Configures [KamelConfig] using [KamelConfigBuilder]. 44 | */ 45 | public inline fun KamelConfig(block: KamelConfigBuilder.() -> Unit): KamelConfig = 46 | KamelConfigBuilder().apply(block).build() 47 | 48 | public val KamelConfig.Companion.Core: KamelConfig 49 | get() = KamelConfig { 50 | imageBitmapCacheSize = DefaultCacheSize 51 | imageVectorCacheSize = DefaultCacheSize 52 | svgCacheSize = DefaultCacheSize 53 | animatedImageCacheSize = DefaultCacheSize 54 | stringMapper() 55 | urlMapper() 56 | uriMapper() 57 | fileFetcher() 58 | fileUrlFetcher() 59 | httpUrlFetcher { 60 | httpCache(DefaultHttpCacheSize) 61 | } 62 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/config/ResourceConfig.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.config 2 | 3 | import androidx.compose.ui.unit.Density 4 | import androidx.compose.ui.unit.IntSize 5 | import io.ktor.client.request.* 6 | import kotlin.coroutines.CoroutineContext 7 | 8 | /** 9 | * Represents a single resource configuration. 10 | * @see ResourceConfigBuilder to create mutable configuration. 11 | */ 12 | public interface ResourceConfig { 13 | 14 | /** 15 | * Http Request configuration. 16 | * @see ResourceConfigBuilder.requestBuilder 17 | */ 18 | public val requestData: HttpRequestData 19 | 20 | /** 21 | * CoroutineContext used while loading the resource. 22 | * @see ResourceConfigBuilder.coroutineContext 23 | */ 24 | public val coroutineContext: CoroutineContext 25 | 26 | /** 27 | * Screen density. 28 | * @see ResourceConfigBuilder.density 29 | */ 30 | public val density: Density 31 | 32 | /** 33 | * Maximum size of the bitmap to decode. 34 | * If the bitmap is larger than this size, it will be downsampled. 35 | */ 36 | val maxBitmapDecodeSize: IntSize 37 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/config/ResourceConfigBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.config 2 | 3 | import androidx.compose.ui.unit.Density 4 | import androidx.compose.ui.unit.IntSize 5 | import io.kamel.core.utils.Kamel 6 | import io.ktor.client.request.* 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | public class ResourceConfigBuilder(parentScope: CoroutineContext) { 11 | 12 | /** 13 | * [HttpRequestBuilder] to configure the request for this resource. 14 | * @see ResourceConfig.requestData 15 | */ 16 | private val requestBuilder: HttpRequestBuilder = HttpRequestBuilder() 17 | 18 | /** 19 | * CoroutineContext used while loading the resource. 20 | * @see ResourceConfig.coroutineContext 21 | */ 22 | public var coroutineContext: CoroutineContext = parentScope.plus(Dispatchers.Kamel) 23 | 24 | /** 25 | * Screen density. 26 | * @see ResourceConfig.density 27 | */ 28 | public var density: Density = Density(1F, 1F) 29 | 30 | /** 31 | * Maximum size of the bitmap to decode. 32 | * If the bitmap is larger than this size, it will be downsampled. 33 | */ 34 | public var maxBitmapDecodeSize: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE) 35 | 36 | /** 37 | * Executes a [block] that configures the [HttpRequestBuilder] associated with this request. 38 | */ 39 | public fun requestBuilder(block: HttpRequestBuilder.() -> Unit): HttpRequestBuilder = 40 | requestBuilder.apply(block) 41 | 42 | /** 43 | * Creates immutable [ResourceConfig]. 44 | */ 45 | public fun build(): ResourceConfig = object : ResourceConfig { 46 | 47 | override val requestData: HttpRequestData = requestBuilder.build() 48 | 49 | override val coroutineContext: CoroutineContext = 50 | this@ResourceConfigBuilder.coroutineContext 51 | 52 | override val density: Density = this@ResourceConfigBuilder.density 53 | 54 | override val maxBitmapDecodeSize: IntSize = this@ResourceConfigBuilder.maxBitmapDecodeSize 55 | 56 | } 57 | 58 | } 59 | 60 | /** 61 | * Copies all the data from [builder] and uses it as base for [this]. 62 | */ 63 | public fun ResourceConfigBuilder.takeFrom(builder: ResourceConfigBuilder): ResourceConfigBuilder = 64 | takeFrom(builder.build()) 65 | 66 | /** 67 | * Copies all the data from [config] and uses it as base for [this]. 68 | */ 69 | public fun ResourceConfigBuilder.takeFrom(config: ResourceConfig): ResourceConfigBuilder { 70 | coroutineContext = config.coroutineContext 71 | density = config.density 72 | requestBuilder { takeFrom(config.requestData) } 73 | return this 74 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/decoder/Decoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.decoder 2 | 3 | import io.kamel.core.config.ResourceConfig 4 | import io.ktor.utils.io.* 5 | import kotlin.reflect.KClass 6 | 7 | /** 8 | * Decodes [ByteReadChannel] to [T]. 9 | */ 10 | public interface Decoder { 11 | 12 | /** 13 | * The KClass of the output of this decoder 14 | */ 15 | public val outputKClass: KClass 16 | 17 | /** 18 | * Decodes [channel] to [T]. 19 | */ 20 | public suspend fun decode(channel: ByteReadChannel, resourceConfig: ResourceConfig): T 21 | } 22 | -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/fetcher/Fetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.Resource 5 | import io.kamel.core.config.ResourceConfig 6 | import io.ktor.utils.io.* 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlin.reflect.KClass 9 | 10 | /** 11 | * Fetches and transfers data into a [ByteReadChannel] asynchronously. 12 | */ 13 | public interface Fetcher { 14 | 15 | /** 16 | * The KClass type for which this fetcher supports as a data input 17 | */ 18 | public val inputDataKClass: KClass 19 | 20 | /** 21 | * Source from where data has been loaded. 22 | */ 23 | public val source: DataSource 24 | 25 | /** 26 | * Whether fetching from [T] is supported or not. 27 | */ 28 | public val T.isSupported: Boolean 29 | 30 | /** 31 | * fetches data [T] asynchronously as [Resource] holding a [ByteReadChannel]. 32 | * @param data type of data to fetch. 33 | * @param resourceConfig configuration used while fetching the resource. 34 | */ 35 | public fun fetch(data: T, resourceConfig: ResourceConfig): Flow> 36 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/fetcher/FileFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.utils.File 4 | 5 | 6 | internal expect val FileFetcher: Fetcher -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/fetcher/FileUrlFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.ktor.http.* 4 | import io.ktor.utils.io.* 5 | 6 | /** 7 | * Fetcher that fetches [ByteReadChannel] from the localhost using [Url]. 8 | */ 9 | internal expect val FileUrlFetcher: Fetcher -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/fetcher/HttpFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.Resource 5 | import io.kamel.core.config.ResourceConfig 6 | import io.ktor.client.* 7 | import io.ktor.client.plugins.* 8 | import io.ktor.client.request.* 9 | import io.ktor.client.statement.* 10 | import io.ktor.http.* 11 | import io.ktor.utils.io.* 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.channelFlow 14 | import kotlin.reflect.KClass 15 | 16 | /** 17 | * Fetcher that fetches [ByteReadChannel] from network using [Url]. 18 | */ 19 | internal class HttpUrlFetcher(private val client: HttpClient) : Fetcher { 20 | 21 | override val inputDataKClass: KClass = Url::class 22 | 23 | override val source: DataSource = DataSource.Network 24 | 25 | override val Url.isSupported: Boolean 26 | get() = protocol.name == "https" || protocol.name == "http" 27 | 28 | override fun fetch( 29 | data: Url, resourceConfig: ResourceConfig 30 | ): Flow> = channelFlow { 31 | val response = client.request { 32 | onDownload { bytesSentTotal, contentLength -> 33 | val progress = contentLength?.let { 34 | (bytesSentTotal.toFloat() / contentLength).coerceIn(0F..1F).takeUnless { it.isNaN() } 35 | } 36 | if (progress != null) send(Resource.Loading(progress, source)) 37 | } 38 | takeFrom(resourceConfig.requestData) 39 | url(data) 40 | } 41 | val bytes = response.bodyAsChannel() 42 | send(Resource.Success(bytes, source)) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/mapper/Mapper.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.mapper 2 | 3 | import kotlin.reflect.KClass 4 | 5 | /** 6 | * Mapper used to map input [I] to output [O]. 7 | * @see StringMapper 8 | * @see URLMapper 9 | * @see URIMapper 10 | */ 11 | public interface Mapper { 12 | 13 | public val inputKClass: KClass 14 | public val outputKClass: KClass 15 | 16 | /** 17 | * Maps input [I] to output [O]. 18 | */ 19 | public fun map(input: I): O 20 | 21 | /** 22 | * Whether mapping [I] is supported or not. 23 | */ 24 | public val I.isSupported: Boolean 25 | get() = true 26 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/mapper/Mappers.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.mapper 2 | 3 | import io.kamel.core.utils.URI 4 | import io.kamel.core.utils.URL 5 | import io.ktor.http.* 6 | import kotlin.reflect.KClass 7 | 8 | internal val StringMapper: Mapper = object : Mapper { 9 | override val inputKClass: KClass 10 | get() = String::class 11 | override val outputKClass: KClass 12 | get() = Url::class 13 | 14 | override fun map(input: String): Url { 15 | val regex = Regex("^file:/+(?!/)") 16 | return if (regex.containsMatchIn(input)) { 17 | // Replace 'file:/' or `file:///` with 'file:///' using regex 18 | // https://youtrack.jetbrains.com/issue/KTOR-6709 19 | Url(input.replaceFirst(regex, "file:///")) 20 | } else { 21 | // If input does not match regex and does not start with '/', use it as is 22 | Url(input) 23 | } 24 | } 25 | 26 | } 27 | 28 | internal expect val URLMapper: Mapper 29 | 30 | internal expect val URIMapper: Mapper -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/utils/CacheControl.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | import io.ktor.client.request.* 4 | import io.ktor.http.* 5 | 6 | /** 7 | * Configures cache control for [HttpRequest]. 8 | * @see io.ktor.http.CacheControl 9 | */ 10 | public fun HttpRequestBuilder.cacheControl(cacheControl: CacheControl): Unit = 11 | header(HttpHeaders.CacheControl, cacheControl) 12 | 13 | /** 14 | * Configures cache control for [HttpRequest]. 15 | * @see io.ktor.client.utils.CacheControl 16 | */ 17 | public fun HttpRequestBuilder.cacheControl(cacheControl: String): Unit = 18 | header(HttpHeaders.CacheControl, cacheControl) -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/utils/ConfigUtils.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST") 2 | 3 | package io.kamel.core.utils 4 | 5 | import io.kamel.core.config.KamelConfig 6 | import io.kamel.core.decoder.Decoder 7 | import io.kamel.core.fetcher.Fetcher 8 | import kotlin.reflect.KClass 9 | 10 | internal fun KamelConfig.mapInput(input: Any, inputKClass: KClass<*>): Any { 11 | 12 | val output = mappers[inputKClass] 13 | ?.lastOrNull { mapper -> with(mapper) { input.isSupported } } 14 | ?.map(input) 15 | 16 | return output ?: input 17 | } 18 | 19 | internal fun KamelConfig.findFetcherFor(data: T): Fetcher { 20 | 21 | val type = data::class 22 | 23 | val fetcher = fetchers.findLast { fetcher -> 24 | 25 | val fetcherType = fetcher.inputDataKClass 26 | 27 | val isSameType = fetcherType == type 28 | 29 | isSameType && with(fetcher) { data.isSupported } 30 | } 31 | 32 | checkNotNull(fetcher) { "Unable to find a fetcher for $type" } 33 | 34 | return fetcher as Fetcher 35 | } 36 | 37 | internal inline fun KamelConfig.findDecoderFor(): Decoder { 38 | 39 | val type = T::class 40 | 41 | val decoder = decoders.findLast { decoder -> 42 | 43 | val decoderType = decoder.outputKClass 44 | 45 | decoderType == type 46 | } 47 | 48 | checkNotNull(decoder) { "Unable to find a decoder for $type" } 49 | 50 | return decoder as Decoder 51 | } 52 | -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/utils/FileSystem.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | import okio.Closeable 4 | import kotlin.jvm.JvmName 5 | 6 | /* 7 | * Copyright (C) 2011 The Android Open Source Project 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * http://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | */ 21 | 22 | import okio.FileNotFoundException 23 | import okio.FileSystem 24 | import okio.IOException 25 | import okio.Path 26 | 27 | 28 | internal fun Closeable.closeQuietly() { 29 | try { 30 | close() 31 | } catch (_: RuntimeException) { 32 | 33 | } 34 | } 35 | 36 | /** Create a new empty file if one doesn't already exist. */ 37 | internal fun FileSystem.createFile(file: Path) { 38 | if (!exists(file)) sink(file).closeQuietly() 39 | } 40 | 41 | /** Tolerant delete, try to clear as many files as possible even after a failure. */ 42 | internal fun FileSystem.deleteContents(directory: Path) { 43 | var exception: IOException? = null 44 | val files = try { 45 | list(directory) 46 | } catch (_: FileNotFoundException) { 47 | return 48 | } 49 | for (file in files) { 50 | try { 51 | if (metadata(file).isDirectory) { 52 | deleteContents(file) 53 | } 54 | delete(file) 55 | } catch (e: IOException) { 56 | if (exception == null) { 57 | exception = e 58 | } 59 | } 60 | } 61 | if (exception != null) { 62 | throw exception 63 | } 64 | } -------------------------------------------------------------------------------- /kamel-core/src/commonMain/kotlin/io/kamel/core/utils/Platform.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | 6 | internal expect val Dispatchers.Kamel: CoroutineDispatcher 7 | 8 | public expect class File 9 | 10 | public expect class URL 11 | 12 | 13 | /** 14 | * @param str The string to parse as a URI 15 | */ 16 | public expect class URI public constructor(str: String) -------------------------------------------------------------------------------- /kamel-core/src/commonTest/composeResources/files/Compose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-core/src/commonTest/composeResources/files/Compose.png -------------------------------------------------------------------------------- /kamel-core/src/commonTest/composeResources/files/Kotlin.svg: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /kamel-core/src/commonTest/kotlin/io/kamel/core/cache/LruCacheTest.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.cache 2 | 3 | import kotlin.test.* 4 | 5 | private const val LruCacheMaxSize = 10 6 | 7 | class LruCacheTest { 8 | 9 | private lateinit var cache: Cache 10 | 11 | @BeforeTest 12 | fun setup() { 13 | cache = LruCache(LruCacheMaxSize) 14 | } 15 | 16 | @Test 17 | fun testMaxSize() { 18 | assertEquals(LruCacheMaxSize, cache.maxSize) 19 | } 20 | 21 | @Test 22 | fun testAddingSingleElement() { 23 | cache["Key"] = 1 24 | 25 | assertEquals(1, cache.size) 26 | assertTrue { cache["Key"] == 1 } 27 | } 28 | 29 | @Test 30 | fun testRemovingSingleElement() { 31 | cache["Key"] = 5 32 | 33 | assertEquals(1, cache.size) 34 | assertTrue { cache["Key"] == 5 } 35 | 36 | cache.remove("Key") 37 | 38 | assertEquals(0, cache.size) 39 | assertNull(cache["Key"]) 40 | } 41 | 42 | @Test 43 | fun testClearElements() { 44 | fillCache() 45 | 46 | assertEquals(10, cache.size) 47 | cache.clear() 48 | assertEquals(0, cache.size) 49 | } 50 | 51 | @Test 52 | fun testRemovingEldestEntry() { 53 | fillCache() 54 | 55 | assertEquals(10, cache.size) 56 | 57 | fillCache() 58 | 59 | assertEquals(10, cache.size) 60 | } 61 | 62 | private fun fillCache() { 63 | repeat(10) { 64 | val value = (1..1000).random() 65 | cache[it.toString()] = value 66 | } 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /kamel-core/src/commonTest/kotlin/io/kamel/core/cache/disk/DiskCacheStorageTest.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.cache.disk 2 | 3 | import io.ktor.client.plugins.cache.storage.* 4 | import io.ktor.http.* 5 | import io.ktor.util.date.* 6 | import kotlinx.coroutines.ExperimentalCoroutinesApi 7 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 8 | import kotlinx.coroutines.test.runTest 9 | import okio.ByteString.Companion.encodeUtf8 10 | import okio.FileSystem 11 | import okio.Path.Companion.toPath 12 | import okio.fakefilesystem.FakeFileSystem 13 | import kotlin.test.* 14 | 15 | 16 | 17 | fun newResponseData(url: Url) = CachedResponseData( 18 | url = url, 19 | statusCode = HttpStatusCode.OK, 20 | requestTime = GMTDate.START, 21 | responseTime = GMTDate(0), 22 | version = HttpProtocolVersion.HTTP_1_0, 23 | expires = GMTDate.START, 24 | headers = headers { }, 25 | varyKeys = emptyMap(), 26 | body = byteArrayOf(1, 2) 27 | ) 28 | 29 | @OptIn(ExperimentalCoroutinesApi::class) 30 | class DiskCacheStorageTest { 31 | 32 | private lateinit var cache: DiskCacheStorage 33 | 34 | private lateinit var fileSystem: FileSystem 35 | 36 | private val directory = "/cache".toPath() 37 | 38 | private val dispatcher = UnconfinedTestDispatcher() 39 | 40 | @BeforeTest 41 | fun setup() { 42 | fileSystem = FakeFileSystem() 43 | cache = DiskCacheStorage( 44 | fileSystem = fileSystem, 45 | directory = directory, 46 | maxSize = 100, 47 | dispatcher = dispatcher 48 | ) 49 | } 50 | 51 | @Test 52 | fun testJournalFileCreationOnInitialization() = runTest { 53 | val url = Url("http://localhost") 54 | 55 | cache.findAll(url) 56 | 57 | assertTrue { 58 | fileSystem.exists(directory / DiskLruCache.JOURNAL_FILE) 59 | } 60 | } 61 | 62 | @Test 63 | fun testCacheEntryIsSaved() = runTest { 64 | 65 | val url = Url("http://localhost") 66 | 67 | val data = newResponseData(url) 68 | 69 | val key = url.toString().encodeUtf8().md5().hex() 70 | 71 | cache.store(url, data) 72 | 73 | assertTrue { 74 | fileSystem.exists(directory / "$key.0") 75 | } 76 | } 77 | 78 | @Test 79 | fun testOldCachedEntryIsReturned() = runTest { 80 | 81 | val url = Url("http://localhost") 82 | 83 | val data = newResponseData(url) 84 | 85 | cache.store(url, data) 86 | 87 | val cachedData = cache.find(url, emptyMap()) 88 | 89 | assertTrue { 90 | cachedData == data 91 | } 92 | } 93 | 94 | @Test 95 | fun testOldCachedEntryIsRemovedWhenMaxSizeIsReached() = runTest { 96 | 97 | val url1 = Url("http://localhost") 98 | val url2 = Url("http://localhost/1") 99 | 100 | val key1 = url1.toString().encodeUtf8().md5().hex() 101 | val key2 = url2.toString().encodeUtf8().md5().hex() 102 | 103 | val data1 = newResponseData(url1) 104 | val data2 = newResponseData(url2) 105 | 106 | cache.store(url1, data1) 107 | 108 | assertTrue { 109 | fileSystem.exists(directory / "$key1.0") 110 | } 111 | 112 | cache.store(url2, data2) 113 | 114 | assertFalse { 115 | fileSystem.exists(directory / "$key1.0") 116 | } 117 | 118 | assertTrue { 119 | fileSystem.exists(directory / "$key2.0") 120 | } 121 | 122 | assertNull(cache.find(url1, emptyMap())) 123 | assertNotNull(cache.find(url2, emptyMap())) 124 | } 125 | } -------------------------------------------------------------------------------- /kamel-core/src/commonTest/kotlin/io/kamel/core/config/KamelConfigUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.config 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import androidx.compose.ui.graphics.vector.ImageVector 5 | import io.kamel.core.decoder.Decoder 6 | import io.kamel.core.fetcher.HttpUrlFetcher 7 | import io.kamel.core.mapper.Mapper 8 | import io.kamel.core.tests.HttpMockEngine 9 | import io.kamel.core.tests.TestStringUrl 10 | import io.kamel.core.utils.* 11 | import io.ktor.http.* 12 | import io.ktor.utils.io.* 13 | import kotlin.reflect.KClass 14 | import kotlin.test.Test 15 | import kotlin.test.assertFails 16 | import kotlin.test.assertTrue 17 | 18 | class KamelConfigUtilsTest { 19 | 20 | private val config = KamelConfig { 21 | stringMapper() 22 | urlMapper() 23 | uriMapper() 24 | fileFetcher() 25 | fakeImageBitmapDecoder() 26 | httpUrlFetcher(HttpMockEngine ) 27 | } 28 | 29 | @Test 30 | fun testMapStringInput() { 31 | val result = config.mapInput(TestStringUrl, String::class) 32 | 33 | assertTrue(result is Url) 34 | } 35 | 36 | @Test 37 | fun testMapURLInput() { 38 | val result = config.mapInput(createURL(TestStringUrl), URL::class) 39 | 40 | assertTrue(result is Url) 41 | } 42 | 43 | @Test 44 | fun testMapURIInput() { 45 | val result = config.mapInput(createURI(TestStringUrl), URI::class) 46 | 47 | assertTrue(result is Url) 48 | } 49 | 50 | @Test 51 | fun testUsesSupportedMapper() { 52 | val twoMappersConfig = KamelConfig { 53 | mapper(object : Mapper { 54 | override val inputKClass: KClass = String::class 55 | override val outputKClass: KClass = String::class 56 | 57 | override fun map(input: String): String = "Fake" 58 | override val String.isSupported: Boolean get() = false 59 | }) 60 | stringMapper() 61 | } 62 | val result = twoMappersConfig.mapInput(TestStringUrl, String::class) 63 | assertTrue(result is Url) 64 | } 65 | 66 | @Test 67 | fun testFindHttpUrlFetcher() { 68 | val fetcher = config.findFetcherFor(Url(TestStringUrl)) 69 | 70 | assertTrue { fetcher is HttpUrlFetcher } 71 | } 72 | 73 | @Test 74 | fun testFindDecoder() { 75 | val decoder = config.findDecoderFor() 76 | 77 | assertTrue { decoder is FakeImageBitmapDecoder } 78 | } 79 | 80 | @Test 81 | fun testFindInvalidDecoder() { 82 | assertFails { 83 | config.findDecoderFor() 84 | } 85 | } 86 | 87 | 88 | @Test 89 | fun testFindInvalidFetcher() { 90 | assertFails { 91 | config.findFetcherFor(2024) 92 | } 93 | } 94 | 95 | } 96 | 97 | fun KamelConfigBuilder.fakeImageBitmapDecoder() = decoder(FakeImageBitmapDecoder) 98 | 99 | private object FakeImageBitmapDecoder : Decoder { 100 | 101 | override val outputKClass: KClass = ImageBitmap::class 102 | 103 | override suspend fun decode(channel: ByteReadChannel, resourceConfig: ResourceConfig): ImageBitmap { 104 | return ImageBitmap(1, 1) 105 | } 106 | } -------------------------------------------------------------------------------- /kamel-core/src/commonTest/kotlin/io/kamel/core/config/ResourceConfigBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.config 2 | 3 | import androidx.compose.ui.unit.Density 4 | import io.kamel.core.utils.cacheControl 5 | import io.ktor.client.request.* 6 | import io.ktor.client.utils.CacheControl 7 | import io.ktor.http.* 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlin.coroutines.EmptyCoroutineContext 10 | import kotlin.test.* 11 | 12 | class ResourceConfigBuilderTest { 13 | 14 | private lateinit var builder: ResourceConfigBuilder 15 | 16 | @BeforeTest 17 | fun setup() { 18 | builder = ResourceConfigBuilder(EmptyCoroutineContext) 19 | .apply { density = Density(1F) } 20 | } 21 | 22 | @Test 23 | fun testDispatcher() { 24 | builder.coroutineContext = Dispatchers.Unconfined 25 | 26 | assertEquals(expected = Dispatchers.Unconfined, actual = builder.build().coroutineContext) 27 | } 28 | 29 | @Test 30 | fun testRequestBuilder() { 31 | val requestData = builder.requestBuilder { 32 | url { 33 | encodedPath = "example/items" 34 | } 35 | header("Key", "Value") 36 | cacheControl(CacheControl.NO_CACHE) 37 | method = HttpMethod.Put 38 | }.build() 39 | 40 | assertFalse { requestData.headers.isEmpty() } 41 | assertTrue { requestData.headers.contains("Key", "Value") } 42 | assertTrue { requestData.headers.contains(HttpHeaders.CacheControl, CacheControl.NO_CACHE) } 43 | assertTrue { requestData.method == HttpMethod.Put } 44 | assertEquals(expected = "/example/items", actual = requestData.url.encodedPath) 45 | } 46 | 47 | @Test 48 | fun testDensity() { 49 | builder.density = Density(5F) 50 | val config = builder.build() 51 | 52 | assertFalse { config == Density(1F) } 53 | assertTrue { config.density == builder.density } 54 | } 55 | 56 | 57 | } -------------------------------------------------------------------------------- /kamel-core/src/commonTest/kotlin/io/kamel/core/fetcher/HttpFetcherTest.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.config.ResourceConfig 5 | import io.kamel.core.config.ResourceConfigBuilder 6 | import io.kamel.core.getOrNull 7 | import io.kamel.core.isLoading 8 | import io.kamel.core.map 9 | import io.kamel.core.tests.HttpMockEngine 10 | import io.ktor.client.* 11 | import io.ktor.http.* 12 | import io.ktor.utils.io.toByteArray 13 | import kotlinx.coroutines.flow.first 14 | import kotlinx.coroutines.test.runTest 15 | import kotlin.test.Test 16 | import kotlin.test.assertFalse 17 | import kotlin.test.assertTrue 18 | 19 | class HttpUrlFetcherTest { 20 | 21 | private val fetcher: HttpUrlFetcher = HttpUrlFetcher(HttpClient(HttpMockEngine)) 22 | 23 | @Test 24 | fun testWebSocketUrlIsSupported() = runTest { 25 | val urlBuilder = URLBuilder(protocol = URLProtocol.WS) 26 | val isSupported = with(fetcher) { Url(urlBuilder).isSupported } 27 | 28 | assertFalse { isSupported } 29 | } 30 | 31 | @Test 32 | fun testHttpUrlIsSupported() = runTest { 33 | val urlBuilder = URLBuilder(protocol = URLProtocol.HTTP) 34 | val isSupported = with(fetcher) { Url(urlBuilder).isSupported } 35 | 36 | assertTrue { isSupported } 37 | } 38 | 39 | @Test 40 | fun testHttpsUrlIsSupported() = runTest { 41 | val urlBuilder = URLBuilder(protocol = URLProtocol.HTTPS) 42 | val isSupported = with(fetcher) { Url(urlBuilder).isSupported } 43 | 44 | assertTrue { isSupported } 45 | } 46 | 47 | @Test 48 | fun testFetchingEmptyImageBytes() = runTest { 49 | val resourceConfig: ResourceConfig = ResourceConfigBuilder(coroutineContext).build() 50 | val url = Url("/emptyImage.jpg") 51 | val resource = fetcher.fetch(url, resourceConfig) 52 | .first { !it.isLoading } 53 | .map { it.toByteArray() } 54 | 55 | assertTrue { resource.getOrNull()!!.isEmpty() } 56 | assertTrue { resource.source == DataSource.Network } 57 | } 58 | 59 | @Test 60 | // fails due to https://github.com/JetBrains/compose-multiplatform/issues/4442 61 | // it will pass if you copy the test resources to the main resources folder, 62 | // but I do not want to check this in. 63 | fun testFetchingNonEmptyImageBytes() = runTest { 64 | val resourceConfig: ResourceConfig = ResourceConfigBuilder(coroutineContext).build() 65 | val url = Url("/image.svg") 66 | val resource = fetcher.fetch(url, resourceConfig) 67 | .first { !it.isLoading } 68 | .map { it.toByteArray() } 69 | 70 | assertTrue { resource.getOrNull()!!.isNotEmpty() } 71 | assertTrue { resource.source == DataSource.Network } 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /kamel-core/src/commonTest/kotlin/io/kamel/core/mapper/MappersTest.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.mapper 2 | 3 | import io.kamel.core.utils.URI 4 | import io.kamel.core.utils.URL 5 | import io.kamel.core.utils.createURI 6 | import io.kamel.core.utils.createURL 7 | import io.ktor.http.* 8 | import kotlin.test.Test 9 | import kotlin.test.assertEquals 10 | 11 | class MappersTest { 12 | 13 | private val urlMapper: Mapper = URLMapper 14 | private val uriMapper: Mapper = URIMapper 15 | 16 | @Test 17 | fun testURLMapper() { 18 | val url = urlMapper.map(createURL("https://www.example.com:443")) 19 | 20 | assertEquals(Url("https://www.example.com:443"), url) 21 | } 22 | 23 | @Test 24 | fun testURIMapper() { 25 | val url = uriMapper.map(createURI("https://www.example.com:443")) 26 | 27 | assertEquals(Url("https://www.example.com:443"), url) 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /kamel-core/src/commonTest/kotlin/io/kamel/core/mapper/StringMapperTest.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.mapper 2 | 3 | import io.ktor.http.* 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class StringMapperTest { 8 | 9 | private val stringMapper: Mapper = StringMapper 10 | 11 | @Test 12 | fun testHttpsUrl() { 13 | val url = stringMapper.map("https://www.example.com") 14 | 15 | assertEquals(Url("https://www.example.com"), url) 16 | assertEquals(URLProtocol.HTTPS.name, url.protocol.name) 17 | } 18 | 19 | @Test 20 | fun testHttpUrl() { 21 | val url = stringMapper.map("http://www.example.com") 22 | 23 | assertEquals(Url("http://www.example.com"), url) 24 | assertEquals(URLProtocol.HTTP.name, url.protocol.name) 25 | } 26 | 27 | @Test 28 | fun testFileWithSingleSlash() { 29 | val input = "file:/path/to/image.png" 30 | val expected = "file:///path/to/image.png" 31 | val url = stringMapper.map(input) 32 | assertEquals(expected, url.toString()) 33 | assertEquals("file", url.protocol.name) 34 | } 35 | 36 | @Test 37 | fun testFileWithTripleSlash() { 38 | val input = "file:///path/to/image.png" 39 | val expected = "file:///path/to/image.png" 40 | val url = stringMapper.map(input) 41 | assertEquals(expected, url.toString()) 42 | assertEquals("file", url.protocol.name) 43 | } 44 | 45 | /*** 46 | * This is currently broken. 47 | * Should be fixed with https://github.com/Kamel-Media/Kamel/pull/78 48 | */ 49 | @Test 50 | fun testAbsoluteFilePath() { 51 | val input = "/path/to/image.png" 52 | val expected = "/path/to/image.png" 53 | val url = stringMapper.map(input) 54 | assertEquals(expected, url.toString()) 55 | assertEquals("", url.protocol.name) 56 | } 57 | 58 | /*** 59 | * This is currently broken. 60 | * Should be fixed with https://github.com/Kamel-Media/Kamel/pull/78 61 | */ 62 | @Test 63 | fun testRelativeFilePaths() { 64 | val input = "path/to/image.png" 65 | val expected = "path/to/image.png" 66 | val url = stringMapper.map(input) 67 | assertEquals(expected, url.toString()) 68 | // Assuming the protocol is empty or null for relative paths 69 | assertEquals("", url.protocol.name) 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /kamel-core/src/commonTest/kotlin/io/kamel/core/tests/HttpMockEngine.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.tests 2 | 3 | import io.ktor.client.engine.mock.* 4 | import io.ktor.client.request.* 5 | import io.ktor.http.* 6 | import io.ktor.utils.io.* 7 | import media.kamel.kamel_core.generated.resources.Res 8 | 9 | val HttpMockEngine = MockEngine { request -> 10 | when (request.url.encodedPath) { 11 | "/emptyImage.jpg" -> respond(ByteReadChannel.Empty) 12 | "/image.jpg" -> resourceImageResponse() 13 | "/image.svg" -> svgImageResponse() 14 | else -> respondError(HttpStatusCode.NotFound) 15 | } 16 | } 17 | 18 | const val TestStringUrl = "https://www.example.com" 19 | 20 | suspend fun MockRequestHandleScope.resourceImageResponse(): HttpResponseData { 21 | val bytes = Res.readBytes("files/Compose.png") 22 | return respond( 23 | ByteReadChannel(bytes), 24 | headers = headers { 25 | set(HttpHeaders.ContentType, ContentType.Image.PNG.toString()) 26 | set(HttpHeaders.ContentLength, bytes.size.toString()) 27 | } 28 | ) 29 | } 30 | 31 | suspend fun MockRequestHandleScope.svgImageResponse(): HttpResponseData { 32 | val bytes = Res.readBytes("files/Kotlin.svg") 33 | return respond( 34 | ByteReadChannel(bytes), 35 | headers = headers { 36 | set(HttpHeaders.ContentType, ContentType.Image.SVG.toString()) 37 | set(HttpHeaders.ContentLength, bytes.size.toString()) 38 | } 39 | ) 40 | } -------------------------------------------------------------------------------- /kamel-core/src/commonTest/kotlin/io/kamel/core/utils/MappersUtils.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | 4 | internal expect fun createURI(url: String): URI 5 | 6 | internal expect fun createURL(url: String): URL -------------------------------------------------------------------------------- /kamel-core/src/desktopMain/kotlin/io/kamel/core/cache/httpCache.desktop.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.cache 2 | 3 | import io.kamel.core.cache.disk.DiskCacheStorage 4 | import io.ktor.client.plugins.cache.storage.CacheStorage 5 | import okio.FileSystem 6 | import okio.Path 7 | import okio.Path.Companion.toPath 8 | import org.jetbrains.skiko.OS 9 | import org.jetbrains.skiko.hostOs 10 | import java.io.File 11 | import kotlin.jvm.optionals.getOrNull 12 | 13 | private fun dataPath(): Path { 14 | 15 | val appName = ProcessHandle.current().info().command().getOrNull() 16 | ?.split(File.separator)?.lastOrNull() ?: "Java App" 17 | 18 | return when (hostOs) { 19 | OS.Windows -> System.getenv("APPDATA").toPath() / appName 20 | OS.MacOS -> System.getProperty("user.home") 21 | .toPath() / "Library/Application Support" / appName 22 | 23 | OS.Linux -> System.getProperty("user.home").toPath() / ".$appName" 24 | else -> System.getProperty("user.dir", appName).toPath(); 25 | } 26 | } 27 | 28 | 29 | private val cacheDir = dataPath() / "cache" 30 | 31 | internal actual fun httpCacheStorage(maxSize: Long): CacheStorage = DiskCacheStorage( 32 | fileSystem = FileSystem.SYSTEM, 33 | directory = cacheDir, 34 | maxSize = maxSize 35 | ) 36 | 37 | 38 | -------------------------------------------------------------------------------- /kamel-core/src/jsMain/kotlin/io/kamel/core/cache/httpCache.js.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.cache 2 | 3 | import io.ktor.client.plugins.cache.storage.CacheStorage 4 | 5 | internal actual fun httpCacheStorage(maxSize: Long): CacheStorage = CacheStorage.Disabled 6 | -------------------------------------------------------------------------------- /kamel-core/src/jsMain/kotlin/io/kamel/core/fetcher/FileFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.Resource 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.utils.File 7 | import io.ktor.utils.io.* 8 | import kotlinx.coroutines.ExperimentalCoroutinesApi 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.flow 11 | import kotlinx.coroutines.suspendCancellableCoroutine 12 | import org.khronos.webgl.ArrayBuffer 13 | import org.khronos.webgl.Int8Array 14 | import org.w3c.dom.ErrorEvent 15 | import org.w3c.files.FileReader 16 | import kotlin.coroutines.resumeWithException 17 | import kotlin.reflect.KClass 18 | 19 | /** 20 | * Fetcher that fetchers [ByteReadChannel] from a file. 21 | */ 22 | internal actual val FileFetcher = object : Fetcher { 23 | 24 | override val inputDataKClass: KClass = File::class 25 | 26 | override val source: DataSource = DataSource.Disk 27 | 28 | override val File.isSupported: Boolean 29 | get() = true 30 | 31 | @OptIn(ExperimentalCoroutinesApi::class) 32 | override fun fetch( 33 | data: File, resourceConfig: ResourceConfig 34 | ): Flow> = flow { 35 | val byteReadChannel = ByteReadChannel(getBase64(data.file)) 36 | emit(Resource.Success(byteReadChannel, source)) 37 | } 38 | 39 | @ExperimentalCoroutinesApi 40 | private suspend fun getBase64(file: org.w3c.files.File): ByteArray = suspendCancellableCoroutine { continuation -> 41 | val reader = FileReader() 42 | reader.readAsArrayBuffer(file) 43 | reader.onload = { 44 | val arrayBuffer = reader.result as ArrayBuffer 45 | val bytes = Int8Array(arrayBuffer).unsafeCast() 46 | continuation.resume(bytes, null) 47 | } 48 | reader.onerror = { error -> 49 | continuation.resumeWithException(Error((error as ErrorEvent).message)) 50 | } 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /kamel-core/src/jsMain/kotlin/io/kamel/core/fetcher/FileUrlFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.Resource 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.utils.File 7 | import io.ktor.http.* 8 | import io.ktor.utils.io.* 9 | import kotlinx.browser.window 10 | import kotlinx.coroutines.await 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.first 13 | import kotlinx.coroutines.flow.flow 14 | import kotlin.reflect.KClass 15 | 16 | /** 17 | * Fetcher that fetches [ByteReadChannel] from the localhost using [Url]. 18 | */ 19 | internal actual val FileUrlFetcher = object : Fetcher { 20 | override val inputDataKClass: KClass = Url::class 21 | 22 | override val source: DataSource = DataSource.Disk 23 | 24 | override val Url.isSupported: Boolean 25 | get() = protocol.name == "file" 26 | 27 | override fun fetch( 28 | data: Url, resourceConfig: ResourceConfig 29 | ): Flow> = flow { 30 | val filePath = data.encodedPath 31 | val blob = window.fetch(data.encodedPath).await().blob().await() 32 | val file = File( 33 | org.w3c.files.File( 34 | arrayOf(blob), filePath 35 | ) 36 | ) 37 | val byteReadChannel = FileFetcher.fetch(file, resourceConfig).first() as Resource.Success 38 | emit(Resource.Success(byteReadChannel.value, FileFetcher.source)) 39 | } 40 | } -------------------------------------------------------------------------------- /kamel-core/src/jsMain/kotlin/io/kamel/core/mapper/Mappers.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.mapper 2 | 3 | import io.kamel.core.utils.URI 4 | import io.kamel.core.utils.URL 5 | import io.ktor.http.* 6 | import kotlin.reflect.KClass 7 | 8 | internal actual val URLMapper: Mapper = object : Mapper { 9 | override val inputKClass: KClass 10 | get() = URL::class 11 | override val outputKClass: KClass 12 | get() = Url::class 13 | 14 | override fun map(input: URL): Url = StringMapper.map(input.toString().removeSuffix("/")) 15 | } 16 | 17 | 18 | internal actual val URIMapper: Mapper = object : Mapper { 19 | override val inputKClass: KClass 20 | get() = URI::class 21 | override val outputKClass: KClass 22 | get() = Url::class 23 | 24 | override fun map(input: URI): Url = StringMapper.map(input.str) 25 | } -------------------------------------------------------------------------------- /kamel-core/src/jsMain/kotlin/io/kamel/core/utils/Platform.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.Dispatchers 5 | 6 | 7 | internal actual val Dispatchers.Kamel: CoroutineDispatcher get() = Default 8 | 9 | public actual class File(public val file: org.w3c.files.File) { 10 | override fun toString(): String { 11 | return file.name 12 | } 13 | } 14 | 15 | 16 | public actual typealias URL = org.w3c.dom.url.URL 17 | 18 | public actual class URI actual constructor(public val str: String) -------------------------------------------------------------------------------- /kamel-core/src/jsTest/kotlin/io/kamel/core/utils/MappersUtils.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | 4 | 5 | internal actual fun createURL(url: String): URL = URL(url) 6 | 7 | internal actual fun createURI(url: String): URI = URI(url) -------------------------------------------------------------------------------- /kamel-core/src/wasmJsMain/kotlin/io/kamel/core/cache/httpCache.js.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.cache 2 | 3 | import io.ktor.client.plugins.cache.storage.CacheStorage 4 | 5 | internal actual fun httpCacheStorage(maxSize: Long): CacheStorage = CacheStorage.Disabled 6 | -------------------------------------------------------------------------------- /kamel-core/src/wasmJsMain/kotlin/io/kamel/core/fetcher/FileFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.Resource 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.utils.File 7 | import io.ktor.util.* 8 | import io.ktor.utils.io.* 9 | import kotlinx.coroutines.ExperimentalCoroutinesApi 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.flow 12 | import kotlinx.coroutines.suspendCancellableCoroutine 13 | import org.khronos.webgl.ArrayBuffer 14 | import org.khronos.webgl.Int8Array 15 | import org.w3c.dom.ErrorEvent 16 | import org.w3c.files.FileReader 17 | import kotlin.coroutines.resumeWithException 18 | import kotlin.reflect.KClass 19 | 20 | /** 21 | * Fetcher that fetchers [ByteReadChannel] from a file. 22 | */ 23 | internal actual val FileFetcher = object : Fetcher { 24 | 25 | override val inputDataKClass: KClass = File::class 26 | 27 | override val source: DataSource = DataSource.Disk 28 | 29 | override val File.isSupported: Boolean 30 | get() = true 31 | 32 | @OptIn(ExperimentalCoroutinesApi::class) 33 | override fun fetch( 34 | data: File, 35 | resourceConfig: ResourceConfig 36 | ): Flow> = flow { 37 | val byteReadChannel = ByteReadChannel(getBase64(data.file)) 38 | emit(Resource.Success(byteReadChannel, source)) 39 | } 40 | 41 | @ExperimentalCoroutinesApi 42 | private suspend fun getBase64(file: org.w3c.files.File): ByteArray = suspendCancellableCoroutine { continuation -> 43 | val reader = FileReader() 44 | reader.readAsArrayBuffer(file) 45 | reader.onload = { 46 | val arrayBuffer = reader.result as ArrayBuffer 47 | val bytes: ByteArray = Int8Array(arrayBuffer).toByteArray() 48 | continuation.resume(bytes, null) 49 | } 50 | reader.onerror = { error -> 51 | continuation.resumeWithException(Error((error as ErrorEvent).message)) 52 | } 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /kamel-core/src/wasmJsMain/kotlin/io/kamel/core/fetcher/FileUrlFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.Resource 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.utils.File 7 | import io.ktor.http.* 8 | import io.ktor.utils.io.* 9 | import kotlinx.browser.window 10 | import kotlinx.coroutines.await 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.first 13 | import kotlinx.coroutines.flow.flow 14 | import org.w3c.fetch.Response 15 | import org.w3c.files.Blob 16 | import kotlin.reflect.KClass 17 | 18 | /** 19 | * Fetcher that fetches [ByteReadChannel] from the localhost using [Url]. 20 | */ 21 | internal actual val FileUrlFetcher = object : Fetcher { 22 | override val inputDataKClass: KClass = Url::class 23 | 24 | override val source: DataSource = DataSource.Disk 25 | 26 | override val Url.isSupported: Boolean 27 | get() = protocol.name == "file" 28 | 29 | override fun fetch( 30 | data: Url, resourceConfig: ResourceConfig 31 | ): Flow> = flow { 32 | val filePath = data.encodedPath 33 | val blob: JsAny = window.fetch(data.encodedPath).await().blob().await() 34 | val file = File( 35 | org.w3c.files.File( 36 | jsArrayOf(blob), filePath 37 | ) 38 | ) 39 | val byteReadChannel = FileFetcher.fetch(file, resourceConfig).first() as Resource.Success 40 | emit(Resource.Success(byteReadChannel.value, FileFetcher.source)) 41 | } 42 | } 43 | 44 | internal fun jsArrayOf(vararg elements: T?): JsArray { 45 | val array = JsArray() 46 | for (i in elements.indices) { 47 | array[i] = elements[i] 48 | } 49 | return array 50 | } -------------------------------------------------------------------------------- /kamel-core/src/wasmJsMain/kotlin/io/kamel/core/mapper/Mappers.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.mapper 2 | 3 | import io.kamel.core.utils.URI 4 | import io.kamel.core.utils.URL 5 | import io.ktor.http.* 6 | import kotlin.reflect.KClass 7 | 8 | internal actual val URLMapper: Mapper = object : Mapper { 9 | override val inputKClass: KClass 10 | get() = URL::class 11 | override val outputKClass: KClass 12 | get() = Url::class 13 | 14 | // TODO: https://youtrack.jetbrains.com/issue/KT-64638/java.util.NoSuchElementException-Key-CLASS-CLASS-nameURL-modalityOPEN-visibilitypublic-external-superTypeskotlin.js.JsAny-is 15 | override fun map(input: URL): Url = input//StringMapper.map(input.toString().removeSuffix("/")) 16 | } 17 | 18 | 19 | internal actual val URIMapper: Mapper = object : Mapper { 20 | override val inputKClass: KClass 21 | get() = URI::class 22 | override val outputKClass: KClass 23 | get() = Url::class 24 | 25 | override fun map(input: URI): Url = StringMapper.map(input.str) 26 | } -------------------------------------------------------------------------------- /kamel-core/src/wasmJsMain/kotlin/io/kamel/core/utils/Platform.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | import io.ktor.http.* 4 | import kotlinx.coroutines.CoroutineDispatcher 5 | import kotlinx.coroutines.Dispatchers 6 | 7 | 8 | internal actual val Dispatchers.Kamel: CoroutineDispatcher get() = Default 9 | 10 | public actual class File(public val file: org.w3c.files.File) { 11 | override fun toString(): String { 12 | return file.name 13 | } 14 | } 15 | 16 | // TODO: https://youtrack.jetbrains.com/issue/KT-64638/java.util.NoSuchElementException-Key-CLASS-CLASS-nameURL-modalityOPEN-visibilitypublic-external-superTypeskotlin.js.JsAny-is 17 | public actual typealias URL = Url //org.w3c.dom.url.URL 18 | 19 | public actual class URI actual constructor(public val str: String) -------------------------------------------------------------------------------- /kamel-core/src/wasmJsTest/kotlin/io/kamel/core/utils/MappersUtils.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.core.utils 2 | 3 | import io.ktor.http.* 4 | 5 | 6 | internal actual fun createURL(url: String): URL = Url(url) 7 | 8 | internal actual fun createURI(url: String): URI = URI(url) -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-animated-image/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | 4 | plugins { 5 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 6 | alias(libs.plugins.org.jetbrains.compose) 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.com.android.library) 9 | alias(libs.plugins.com.vanniktech.maven.publish) 10 | } 11 | 12 | android { 13 | compileSdk = 36 14 | 15 | defaultConfig { 16 | minSdk = 21 17 | multiDexEnabled = true 18 | } 19 | 20 | namespace = "io.kamel.image" 21 | 22 | compileOptions { 23 | sourceCompatibility = JavaVersion.VERSION_1_8 24 | targetCompatibility = JavaVersion.VERSION_1_8 25 | } 26 | 27 | testOptions { 28 | unitTests { 29 | isIncludeAndroidResources = true 30 | } 31 | } 32 | 33 | } 34 | 35 | kotlin { 36 | explicitApi = ExplicitApiMode.Warning 37 | 38 | androidTarget { 39 | publishAllLibraryVariants() 40 | } 41 | jvm() 42 | js(IR) { 43 | browser() 44 | } 45 | @OptIn(ExperimentalWasmDsl::class) 46 | wasmJs { 47 | browser() 48 | } 49 | iosArm64() 50 | iosSimulatorArm64() 51 | iosX64() 52 | macosX64() 53 | macosArm64() 54 | applyDefaultHierarchyTemplate() 55 | sourceSets { 56 | 57 | commonMain { 58 | dependencies { 59 | implementation(projects.kamelCore) 60 | // todo: remove ktor dependency related to https://github.com/Kamel-Media/Kamel/issues/35 61 | implementation(libs.ktor.client.core) 62 | implementation(compose.runtime) 63 | implementation(compose.foundation) 64 | } 65 | } 66 | 67 | androidMain { 68 | dependencies { 69 | implementation(libs.androidx.core.ktx) 70 | } 71 | } 72 | val nonAndroidCommonMain by creating { 73 | dependsOn(commonMain.get()) 74 | } 75 | 76 | nativeMain.get().dependsOn(nonAndroidCommonMain) 77 | jvmMain.get().dependsOn(nonAndroidCommonMain) 78 | jsMain.get().dependsOn(nonAndroidCommonMain) 79 | val wasmJsMain by getting { 80 | dependsOn(nonAndroidCommonMain) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-animated-image/src/androidMain/kotlin/io/kamel/image/decoder/AnimatedImageDecoder.android.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import android.graphics.drawable.AnimatedImageDrawable 4 | import android.os.Build 5 | import androidx.annotation.RequiresApi 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.graphics.ImageBitmap 8 | import androidx.compose.ui.graphics.asImageBitmap 9 | import androidx.core.graphics.drawable.toBitmap 10 | import io.kamel.core.AnimatedImage 11 | import io.kamel.core.config.ResourceConfig 12 | import io.kamel.core.decoder.Decoder 13 | import io.ktor.utils.io.* 14 | import io.ktor.utils.io.jvm.javaio.* 15 | import kotlinx.coroutines.delay 16 | import kotlin.reflect.KClass 17 | 18 | /** 19 | * Decodes and transfers [ByteReadChannel] to [AnimatedImage] using Skia [Image]. 20 | */ 21 | internal actual val AnimatedImageDecoder = object : Decoder { 22 | 23 | override val outputKClass: KClass = AnimatedImage::class 24 | 25 | override suspend fun decode( 26 | channel: ByteReadChannel, resourceConfig: ResourceConfig 27 | ): AnimatedImage { 28 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { 29 | throw UnsupportedOperationException("Animated images are supported only on Android P (API 28) and above.") 30 | } 31 | return decodeAnimatedImage(channel) 32 | } 33 | 34 | @RequiresApi(Build.VERSION_CODES.P) 35 | private suspend fun decodeAnimatedImage(channel: ByteReadChannel): AnimatedImage { 36 | val inStream = channel.toInputStream() 37 | val animatedImage = AnimatedImageDrawable.createFromStream(inStream, null) 38 | if (animatedImage == null) { 39 | val bytes = channel.toByteArray() 40 | throw IllegalArgumentException( 41 | "Failed to decode ${bytes.size} bytes to an animated drawable. Decoded bytes:\n${bytes.decodeToString()}\n" 42 | ) 43 | } 44 | return AndroidAnimatedImage(animatedImage as AnimatedImageDrawable) 45 | } 46 | } 47 | 48 | public class AndroidAnimatedImage(public val drawable: AnimatedImageDrawable) : AnimatedImage { 49 | 50 | @RequiresApi(Build.VERSION_CODES.P) 51 | @Composable 52 | override fun animate(): ImageBitmap { 53 | var bitmapState by remember { mutableStateOf(drawable.toBitmap().asImageBitmap()) } 54 | LaunchedEffect(Unit) { 55 | val delayMillis: Long = (1000 / 60) // Approximate 60 FPS 56 | while (true) { 57 | delay(delayMillis) 58 | bitmapState = drawable.toBitmap().asImageBitmap() 59 | } 60 | } 61 | return bitmapState.also { drawable.start() } 62 | } 63 | } -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-animated-image/src/commonMain/kotlin/io/kamel/image/config/KamelConfigAnimatedImageDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.AnimatedImage 4 | import io.kamel.core.config.KamelConfigBuilder 5 | import io.kamel.image.decoder.AnimatedImageDecoder 6 | 7 | /** 8 | * Adds an [AnimatedImage] decoder to the [KamelConfigBuilder]. 9 | */ 10 | public fun KamelConfigBuilder.animatedImageDecoder(): Unit = decoder(AnimatedImageDecoder) 11 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-animated-image/src/commonMain/kotlin/io/kamel/image/decoder/AnimatedImageDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import io.kamel.core.AnimatedImage 4 | import io.kamel.core.decoder.Decoder 5 | import io.ktor.utils.io.* 6 | 7 | /** 8 | * Decodes and transfers [ByteReadChannel] to [AnimatedImage] using Skia [Image]. 9 | */ 10 | internal expect val AnimatedImageDecoder: Decoder -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-animated-image/src/commonMain/kotlin/io/kamel/image/decoder/BlankBitmap.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | 5 | private val BlankBitmap = ImageBitmap(1, 1) 6 | 7 | /** 8 | * Object used to represent a blank ImageBitmap with the minimum possible size. 9 | */ 10 | public val ImageBitmap.Companion.Blank: ImageBitmap get() = BlankBitmap -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-animated-image/src/nonAndroidCommonMain/kotlin/io/kamel/image/decoder/AnimatedImage.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.graphics.ImageBitmap 8 | import androidx.compose.ui.graphics.asComposeImageBitmap 9 | import io.kamel.core.AnimatedImage 10 | import org.jetbrains.skia.AnimationFrameInfo 11 | import org.jetbrains.skia.Bitmap 12 | import org.jetbrains.skia.Codec 13 | 14 | private const val DEFAULT_FRAME_DURATION = 100 15 | 16 | public class AnimatedImageImpl(public val codec: Codec) : AnimatedImage { 17 | 18 | @Composable 19 | public override fun animate(): ImageBitmap { 20 | when (codec.frameCount) { 21 | 0 -> return ImageBitmap.Blank // No frames at all 22 | 1 -> { 23 | // Just one frame, no animation 24 | val bitmap = remember(codec) { Bitmap().apply { allocPixels(codec.imageInfo) } } 25 | remember(bitmap) { 26 | codec.readPixels(bitmap, 0) 27 | } 28 | return bitmap.asComposeImageBitmap() 29 | } 30 | 31 | else -> { 32 | val transition = rememberInfiniteTransition() 33 | val frameIndex by transition.animateValue( 34 | initialValue = 0, 35 | targetValue = codec.frameCount - 1, 36 | Int.VectorConverter, 37 | animationSpec = infiniteRepeatable(animation = keyframes { 38 | durationMillis = 0 39 | for ((index, frame) in codec.framesInfo.withIndex()) { 40 | index at durationMillis 41 | val frameDuration = calcFrameDuration(frame) 42 | 43 | durationMillis += frameDuration 44 | } 45 | }) 46 | ) 47 | 48 | val bitmap = remember(codec) { Bitmap().apply { allocPixels(codec.imageInfo) } } 49 | 50 | remember(bitmap, frameIndex) { 51 | codec.readPixels(bitmap, frameIndex) 52 | } 53 | 54 | return bitmap.asComposeImageBitmap() 55 | } 56 | } 57 | } 58 | 59 | private fun calcFrameDuration(frame: AnimationFrameInfo): Int { 60 | // If the frame does not contain information about a duration, set a reasonable constant duration 61 | val frameDuration = frame.duration 62 | return if (frameDuration == 0) DEFAULT_FRAME_DURATION else frameDuration 63 | } 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-animated-image/src/nonAndroidCommonMain/kotlin/io/kamel/image/decoder/AnimatedImageDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import io.kamel.core.AnimatedImage 4 | import io.kamel.core.config.ResourceConfig 5 | import io.kamel.core.decoder.Decoder 6 | import io.ktor.util.* 7 | import io.ktor.utils.io.* 8 | import org.jetbrains.skia.Codec 9 | import org.jetbrains.skia.Data 10 | import org.jetbrains.skia.Image 11 | import kotlin.reflect.KClass 12 | 13 | 14 | /** 15 | * Decodes and transfers [ByteReadChannel] to [AnimatedImage] using Skia [Image]. 16 | */ 17 | internal actual val AnimatedImageDecoder = object : Decoder { 18 | 19 | override val outputKClass: KClass = AnimatedImage::class 20 | 21 | override suspend fun decode( 22 | channel: ByteReadChannel, resourceConfig: ResourceConfig 23 | ): AnimatedImage { 24 | val bytes = channel.toByteArray() 25 | return try { 26 | val data = Data.makeFromBytes(bytes) 27 | val codec = Codec.makeFromData(data) 28 | AnimatedImageImpl(codec) 29 | } catch (t: Throwable) { 30 | throw throw IllegalArgumentException( 31 | "Failed to decode ${bytes.size} bytes to a bitmap. Decoded bytes:\n${ 32 | bytes.slice(0 until 1024).toByteArray().decodeToString() 33 | }\n", t 34 | ) 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-bitmap-resizing/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | 4 | plugins { 5 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 6 | alias(libs.plugins.org.jetbrains.compose) 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.com.android.library) 9 | alias(libs.plugins.com.vanniktech.maven.publish) 10 | } 11 | 12 | android { 13 | compileSdk = 36 14 | 15 | defaultConfig { 16 | minSdk = 21 17 | multiDexEnabled = true 18 | } 19 | 20 | namespace = "io.kamel.image" 21 | 22 | compileOptions { 23 | sourceCompatibility = JavaVersion.VERSION_1_8 24 | targetCompatibility = JavaVersion.VERSION_1_8 25 | } 26 | 27 | testOptions { 28 | unitTests { 29 | isIncludeAndroidResources = true 30 | } 31 | } 32 | 33 | } 34 | 35 | kotlin { 36 | explicitApi = ExplicitApiMode.Warning 37 | 38 | androidTarget { 39 | publishAllLibraryVariants() 40 | } 41 | applyDefaultHierarchyTemplate() 42 | sourceSets { 43 | 44 | commonMain { 45 | dependencies { 46 | implementation(projects.kamelCore) 47 | // // todo: remove ktor dependency related to https://github.com/Kamel-Media/Kamel/issues/35 48 | implementation(libs.ktor.client.core) 49 | implementation(compose.ui) 50 | } 51 | } 52 | 53 | androidMain { 54 | dependencies { 55 | implementation(libs.com.caverok.androidsvg) 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-bitmap-resizing/src/androidMain/kotlin/io/kamel/image/decoder/ImageBitmapResizingDecoder.android.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import androidx.compose.ui.graphics.ImageBitmap 6 | import androidx.compose.ui.graphics.asImageBitmap 7 | import io.kamel.core.config.ResourceConfig 8 | import io.kamel.core.decoder.Decoder 9 | import io.ktor.util.* 10 | import io.ktor.utils.io.* 11 | import kotlin.math.min 12 | import kotlin.reflect.KClass 13 | 14 | private const val Offset = 0 15 | 16 | internal actual val ImageBitmapResizingDecoder = object : Decoder { 17 | 18 | override val outputKClass: KClass = ImageBitmap::class 19 | 20 | override suspend fun decode(channel: ByteReadChannel, resourceConfig: ResourceConfig): ImageBitmap { 21 | val bytes = channel.toByteArray() 22 | val opt = BitmapFactory.Options() 23 | val bitmap = BitmapFactory.decodeByteArray(bytes, Offset, bytes.size, opt) 24 | ?: throw IllegalArgumentException("Failed to decode ${bytes.size} bytes to a bitmap. Decoded bytes:\n${bytes.decodeToString()}\n") 25 | val width = min(bitmap.width, resourceConfig.maxBitmapDecodeSize.width) 26 | val height = min(bitmap.height, resourceConfig.maxBitmapDecodeSize.height) 27 | val minScale = min(width / bitmap.width.toFloat(), height / bitmap.height.toFloat()) 28 | return if (minScale < 1) { 29 | return Bitmap.createScaledBitmap( 30 | bitmap, 31 | (bitmap.width * minScale).toInt(), 32 | (bitmap.height * minScale).toInt(), 33 | true 34 | ).asImageBitmap() 35 | } else { 36 | bitmap.asImageBitmap() 37 | } 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-bitmap-resizing/src/commonMain/kotlin/io/kamel/image/config/KamelConfigImageBitmapResizingDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import io.kamel.core.ExperimentalKamelApi 5 | import io.kamel.core.config.KamelConfigBuilder 6 | import io.kamel.image.decoder.ImageBitmapResizingDecoder 7 | 8 | /** 9 | * Adds an [ImageBitmap] decoder to the [KamelConfigBuilder]. 10 | */ 11 | @ExperimentalKamelApi 12 | public fun KamelConfigBuilder.imageBitmapResizingDecoder(): Unit = decoder(ImageBitmapResizingDecoder) 13 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-bitmap-resizing/src/commonMain/kotlin/io/kamel/image/decoder/ImageBitmapResizingDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import io.kamel.core.ExperimentalKamelApi 5 | import io.kamel.core.decoder.Decoder 6 | 7 | /** 8 | * Decodes and transfers [ByteReadChannel] to [ImageBitmap]. 9 | */ 10 | internal expect val ImageBitmapResizingDecoder: Decoder -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-bitmap/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | 4 | plugins { 5 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 6 | alias(libs.plugins.org.jetbrains.compose) 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.com.android.library) 9 | alias(libs.plugins.com.vanniktech.maven.publish) 10 | } 11 | 12 | android { 13 | compileSdk = 36 14 | 15 | defaultConfig { 16 | minSdk = 21 17 | multiDexEnabled = true 18 | } 19 | 20 | namespace = "io.kamel.image" 21 | 22 | compileOptions { 23 | sourceCompatibility = JavaVersion.VERSION_1_8 24 | targetCompatibility = JavaVersion.VERSION_1_8 25 | } 26 | 27 | testOptions { 28 | unitTests { 29 | isIncludeAndroidResources = true 30 | } 31 | } 32 | 33 | } 34 | 35 | kotlin { 36 | explicitApi = ExplicitApiMode.Warning 37 | 38 | androidTarget { 39 | publishAllLibraryVariants() 40 | } 41 | jvm() 42 | js(IR) { 43 | browser() 44 | } 45 | @OptIn(ExperimentalWasmDsl::class) 46 | wasmJs { 47 | browser() 48 | } 49 | iosArm64() 50 | iosSimulatorArm64() 51 | iosX64() 52 | macosX64() 53 | macosArm64() 54 | applyDefaultHierarchyTemplate() 55 | sourceSets { 56 | 57 | commonMain { 58 | dependencies { 59 | implementation(projects.kamelCore) 60 | // // todo: remove ktor dependency related to https://github.com/Kamel-Media/Kamel/issues/35 61 | implementation(libs.ktor.client.core) 62 | implementation(compose.ui) 63 | } 64 | } 65 | 66 | androidMain { 67 | dependencies { 68 | implementation(libs.com.caverok.androidsvg) 69 | } 70 | } 71 | 72 | val nonAndroidMain by creating { 73 | dependsOn(commonMain.get()) 74 | } 75 | 76 | jvmMain { 77 | dependsOn(nonAndroidMain) 78 | } 79 | 80 | jsMain { 81 | dependsOn(nonAndroidMain) 82 | } 83 | 84 | val wasmJsMain by getting { 85 | dependsOn(nonAndroidMain) 86 | } 87 | 88 | appleMain { 89 | dependsOn(nonAndroidMain) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-bitmap/src/androidMain/kotlin/io/kamel/image/decoder/AndroidImageBitmapDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import android.graphics.BitmapFactory 4 | import androidx.compose.ui.graphics.ImageBitmap 5 | import androidx.compose.ui.graphics.asImageBitmap 6 | import io.kamel.core.config.ResourceConfig 7 | import io.kamel.core.decoder.Decoder 8 | import io.ktor.util.* 9 | import io.ktor.utils.io.* 10 | import kotlin.reflect.KClass 11 | 12 | private const val Offset = 0 13 | 14 | internal actual val ImageBitmapDecoder = object : Decoder { 15 | 16 | override val outputKClass: KClass = ImageBitmap::class 17 | 18 | override suspend fun decode(channel: ByteReadChannel, resourceConfig: ResourceConfig): ImageBitmap { 19 | val bytes = channel.toByteArray() 20 | val bitmap = BitmapFactory.decodeByteArray(bytes, Offset, bytes.size) 21 | ?: throw IllegalArgumentException("Failed to decode ${bytes.size} bytes to a bitmap. Decoded bytes:\n${bytes.decodeToString()}\n") 22 | return bitmap.asImageBitmap() 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-bitmap/src/commonMain/kotlin/io/kamel/image/config/KamelConfigImageBitmapDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import io.kamel.core.config.KamelConfigBuilder 5 | import io.kamel.image.decoder.ImageBitmapDecoder 6 | 7 | /** 8 | * Adds an [ImageBitmap] decoder to the [KamelConfigBuilder]. 9 | */ 10 | public fun KamelConfigBuilder.imageBitmapDecoder(): Unit = decoder(ImageBitmapDecoder) 11 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-bitmap/src/commonMain/kotlin/io/kamel/image/decoder/ImageBitmapDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import io.kamel.core.decoder.Decoder 5 | 6 | /** 7 | * Decodes and transfers [ByteReadChannel] to [ImageBitmap]. 8 | */ 9 | internal expect val ImageBitmapDecoder : Decoder -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-bitmap/src/nonAndroidMain/kotlin/io/kamel/image/decoder/ImageBitmapDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.graphics.ImageBitmap 4 | import androidx.compose.ui.graphics.toComposeImageBitmap 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.decoder.Decoder 7 | import io.ktor.util.* 8 | import io.ktor.utils.io.* 9 | import org.jetbrains.skia.Image 10 | import kotlin.reflect.KClass 11 | 12 | /** 13 | * Decodes and transfers [ByteReadChannel] to [ImageBitmap] using Skia [Image]. 14 | */ 15 | internal actual val ImageBitmapDecoder = object : Decoder { 16 | 17 | override val outputKClass: KClass = ImageBitmap::class 18 | 19 | override suspend fun decode( 20 | channel: ByteReadChannel, 21 | resourceConfig: ResourceConfig 22 | ): ImageBitmap { 23 | val bytes = channel.toByteArray() 24 | return try { 25 | Image.makeFromEncoded(bytes).toComposeImageBitmap() 26 | } catch (t: Throwable) { 27 | throw throw IllegalArgumentException("Failed to decode ${bytes.size} bytes to a bitmap. Decoded bytes:\n${bytes.decodeToString()}\n") 28 | } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-vector/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | 4 | plugins { 5 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 6 | alias(libs.plugins.org.jetbrains.compose) 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.com.android.library) 9 | alias(libs.plugins.com.vanniktech.maven.publish) 10 | } 11 | 12 | android { 13 | compileSdk = 36 14 | 15 | defaultConfig { 16 | minSdk = 21 17 | multiDexEnabled = true 18 | } 19 | 20 | namespace = "io.kamel.image" 21 | 22 | compileOptions { 23 | sourceCompatibility = JavaVersion.VERSION_1_8 24 | targetCompatibility = JavaVersion.VERSION_1_8 25 | } 26 | 27 | testOptions { 28 | unitTests { 29 | isIncludeAndroidResources = true 30 | } 31 | } 32 | 33 | } 34 | 35 | kotlin { 36 | explicitApi = ExplicitApiMode.Warning 37 | 38 | androidTarget { 39 | publishAllLibraryVariants() 40 | } 41 | jvm() 42 | js(IR) { 43 | browser() 44 | } 45 | @OptIn(ExperimentalWasmDsl::class) 46 | wasmJs { 47 | browser() 48 | } 49 | iosArm64() 50 | iosSimulatorArm64() 51 | iosX64() 52 | macosX64() 53 | macosArm64() 54 | applyDefaultHierarchyTemplate() 55 | 56 | sourceSets { 57 | 58 | commonMain { 59 | dependencies { 60 | api(projects.kamelCore) 61 | implementation(compose.ui) 62 | // todo: remove ktor dependency related to https://github.com/Kamel-Media/Kamel/issues/35 63 | implementation(libs.ktor.client.core) 64 | } 65 | } 66 | 67 | val nonJvmAndAndroidMain by creating { 68 | dependsOn(commonMain.get()) 69 | dependencies { 70 | implementation(libs.pdvrieze.xmlutil.serialization) 71 | } 72 | } 73 | 74 | androidMain { 75 | dependsOn(nonJvmAndAndroidMain) 76 | } 77 | 78 | jsMain { 79 | dependsOn(nonJvmAndAndroidMain) 80 | } 81 | 82 | val wasmJsMain by getting { 83 | dependsOn(nonJvmAndAndroidMain) 84 | } 85 | 86 | appleMain { 87 | dependsOn(nonJvmAndAndroidMain) 88 | } 89 | 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-vector/src/androidMain/kotlin/filterIsElement.kt: -------------------------------------------------------------------------------- 1 | import nl.adaptivity.xmlutil.dom2.Element 2 | import nl.adaptivity.xmlutil.dom2.Node 3 | 4 | internal actual fun Sequence.filterIsElement(): Sequence = filterIsInstance() 5 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-vector/src/appleMain/kotlin/filterIsElement.kt: -------------------------------------------------------------------------------- 1 | import nl.adaptivity.xmlutil.dom2.Element 2 | import nl.adaptivity.xmlutil.dom2.Node 3 | 4 | internal actual fun Sequence.filterIsElement(): Sequence = filterIsInstance() 5 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-vector/src/commonMain/kotlin/io/kamel/image/config/KamelConfigImageVectorDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfigBuilder 4 | import io.kamel.image.decoder.ImageVectorDecoder 5 | 6 | /** 7 | * Adds Decoder for XML Images to the [KamelConfigBuilder] 8 | */ 9 | public fun KamelConfigBuilder.imageVectorDecoder(): Unit = decoder(ImageVectorDecoder) 10 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-vector/src/commonMain/kotlin/io/kamel/image/decoder/ImageVectorDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | import io.kamel.core.decoder.Decoder 5 | 6 | /** 7 | * Decodes and transfers [ByteReadChannel] to [ImageVector]. 8 | */ 9 | internal expect val ImageVectorDecoder : Decoder -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-vector/src/jsMain/kotlin/filterIsElement.kt: -------------------------------------------------------------------------------- 1 | import nl.adaptivity.js.util.asElement 2 | import nl.adaptivity.xmlutil.dom2.Element 3 | import nl.adaptivity.xmlutil.dom2.Node 4 | 5 | @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") 6 | internal actual fun Sequence.filterIsElement(): Sequence = 7 | mapNotNull { 8 | (it as org.w3c.dom.Node).asElement() as Element? 9 | } 10 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-vector/src/jvmMain/kotlin/io/kamel/image/decoder/DesktopImageVectorDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | import androidx.compose.ui.res.loadXmlImageVector 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.decoder.Decoder 7 | import io.ktor.utils.io.* 8 | import io.ktor.utils.io.jvm.javaio.* 9 | import org.xml.sax.InputSource 10 | import kotlin.reflect.KClass 11 | 12 | internal actual val ImageVectorDecoder = object : Decoder { 13 | 14 | override val outputKClass: KClass = ImageVector::class 15 | 16 | override suspend fun decode(channel: ByteReadChannel, resourceConfig: ResourceConfig): ImageVector { 17 | val inputSource = InputSource(channel.toInputStream()) 18 | return loadXmlImageVector(inputSource, resourceConfig.density) 19 | } 20 | } -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-vector/src/nonJvmAndAndroidMain/kotlin/filterIsElement.kt: -------------------------------------------------------------------------------- 1 | import nl.adaptivity.xmlutil.dom2.Element 2 | import nl.adaptivity.xmlutil.dom2.Node 3 | 4 | internal expect fun Sequence.filterIsElement(): Sequence 5 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-vector/src/nonJvmAndAndroidMain/kotlin/io/kamel/image/decoder/ImageVectorDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | //import androidx.compose.ui.res.loadXmlImageVector 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.decoder.Decoder 7 | import io.ktor.util.* 8 | import io.ktor.utils.io.* 9 | import loadXmlImageVector 10 | import kotlin.reflect.KClass 11 | 12 | internal actual val ImageVectorDecoder = object : Decoder { 13 | 14 | override val outputKClass: KClass = ImageVector::class 15 | 16 | override suspend fun decode(channel: ByteReadChannel, resourceConfig: ResourceConfig): ImageVector { 17 | val xml = channel.toByteArray().decodeToString() 18 | return loadXmlImageVector(xml, resourceConfig.density) 19 | } 20 | } -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-vector/src/nonJvmAndAndroidMain/kotlin/loadXmlImageVector.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.graphics.vector.ImageVector 2 | import androidx.compose.ui.unit.Density 3 | import nl.adaptivity.xmlutil.dom2.Element 4 | import nl.adaptivity.xmlutil.serialization.XML 5 | 6 | /** 7 | * Synchronously load an xml vector image from some [xmlString]. 8 | * 9 | * XML Vector Image came from Android world. See: 10 | * https://developer.android.com/guide/topics/graphics/vector-drawable-resources 11 | * 12 | * On desktop it is fully implemented except there is no resource linking 13 | * (for example, we can't reference to color defined in another file) 14 | * 15 | * @param xmlString input xml vector image string. 16 | * @param density density that will be used to set the default size of the ImageVector. If the image 17 | * will be drawn with the specified size, density will have no effect. 18 | * @return the decoded vector image associated with the image 19 | */ 20 | internal fun loadXmlImageVector( 21 | xmlString: String, 22 | density: Density 23 | ): ImageVector { 24 | val element: Element = XML.decodeFromString(xmlString) 25 | return element.parseVectorRoot(density) 26 | } 27 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-image-vector/src/wasmJsMain/kotlin/filterIsElement.kt: -------------------------------------------------------------------------------- 1 | import nl.adaptivity.xmlutil.dom2.Element 2 | import nl.adaptivity.xmlutil.dom2.Node 3 | 4 | internal actual fun Sequence.filterIsElement(): Sequence = filterIsInstance() 5 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-svg-batik/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 2 | 3 | plugins { 4 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 5 | alias(libs.plugins.org.jetbrains.compose) 6 | alias(libs.plugins.compose.compiler) 7 | alias(libs.plugins.com.vanniktech.maven.publish) 8 | } 9 | 10 | kotlin { 11 | explicitApi = ExplicitApiMode.Warning 12 | jvm() 13 | applyDefaultHierarchyTemplate() 14 | sourceSets { 15 | val jvmMain by getting { 16 | dependencies { 17 | implementation(projects.kamelCore) 18 | // todo: remove ktor dependency related to https://github.com/Kamel-Media/Kamel/issues/35 19 | implementation(libs.ktor.client.core) 20 | implementation(compose.ui) 21 | implementation(libs.apache.batik.transcoder) 22 | // https://stackoverflow.com/a/45318410/1363742 23 | implementation(libs.apache.batik.codec) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-svg-batik/src/jvmMain/kotlin/io/kamel/image/config/KamelConfigBaticSvgDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfigBuilder 4 | import io.kamel.image.decoder.BatikSvgDecoder 5 | 6 | 7 | /** 8 | * Adds Batik as decoder for SVG Images to the [KamelConfigBuilder] 9 | * 10 | * Batik is useful if your SVGs depend on classes for styling as [SKIA doesn't support it currently](https://bugs.chromium.org/p/skia/issues/detail?id=12251) 11 | * On the other hand using Batik may result in lower image quality 12 | */ 13 | public fun KamelConfigBuilder.batikSvgDecoder(): Unit = decoder(BatikSvgDecoder) 14 | 15 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-svg-batik/src/jvmMain/kotlin/io/kamel/image/decoder/BatikSvgDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.graphics.painter.Painter 4 | import androidx.compose.ui.graphics.toPainter 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.decoder.Decoder 7 | import io.ktor.utils.io.* 8 | import io.ktor.utils.io.jvm.javaio.* 9 | import org.apache.batik.transcoder.Transcoder 10 | import org.apache.batik.transcoder.TranscoderInput 11 | import org.apache.batik.transcoder.TranscoderOutput 12 | import org.apache.batik.transcoder.image.PNGTranscoder 13 | import java.io.ByteArrayOutputStream 14 | import javax.imageio.ImageIO 15 | import kotlin.reflect.KClass 16 | 17 | internal object BatikSvgDecoder : Decoder { 18 | 19 | override val outputKClass: KClass 20 | get() = Painter::class 21 | 22 | override suspend fun decode(channel: ByteReadChannel, resourceConfig: ResourceConfig): Painter { 23 | val t: Transcoder = PNGTranscoder() 24 | val input = TranscoderInput(channel.toInputStream()) 25 | 26 | // Create the transcoder output. 27 | val outputStream = ByteArrayOutputStream() 28 | outputStream.use { 29 | val output = TranscoderOutput(it) 30 | 31 | // Save the image. 32 | t.transcode(input, output) 33 | } 34 | 35 | return ImageIO.read(outputStream.toByteArray().inputStream()).toPainter() 36 | } 37 | } -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-svg-std/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | 4 | plugins { 5 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 6 | alias(libs.plugins.org.jetbrains.compose) 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.com.android.library) 9 | alias(libs.plugins.com.vanniktech.maven.publish) 10 | } 11 | 12 | android { 13 | compileSdk = 36 14 | 15 | defaultConfig { 16 | minSdk = 21 17 | multiDexEnabled = true 18 | } 19 | 20 | namespace = "io.kamel.image" 21 | 22 | compileOptions { 23 | sourceCompatibility = JavaVersion.VERSION_1_8 24 | targetCompatibility = JavaVersion.VERSION_1_8 25 | } 26 | 27 | testOptions { 28 | unitTests { 29 | isIncludeAndroidResources = true 30 | } 31 | } 32 | 33 | } 34 | 35 | kotlin { 36 | explicitApi = ExplicitApiMode.Warning 37 | 38 | androidTarget { 39 | publishAllLibraryVariants() 40 | } 41 | jvm() 42 | js(IR) { 43 | browser() 44 | } 45 | @OptIn(ExperimentalWasmDsl::class) 46 | wasmJs { 47 | browser() 48 | } 49 | iosArm64() 50 | iosSimulatorArm64() 51 | iosX64() 52 | macosX64() 53 | macosArm64() 54 | applyDefaultHierarchyTemplate() 55 | sourceSets { 56 | 57 | commonMain { 58 | dependencies { 59 | implementation(projects.kamelCore) 60 | // // todo: remove ktor dependency related to https://github.com/Kamel-Media/Kamel/issues/35 61 | implementation(libs.ktor.client.core) 62 | implementation(compose.ui) 63 | } 64 | } 65 | 66 | androidMain { 67 | dependencies { 68 | implementation(libs.com.caverok.androidsvg) 69 | } 70 | } 71 | 72 | val nonJvmMain by creating { 73 | dependsOn(commonMain.get()) 74 | } 75 | 76 | jsMain { 77 | dependsOn(nonJvmMain) 78 | } 79 | 80 | val wasmJsMain by getting { 81 | dependsOn(nonJvmMain) 82 | } 83 | 84 | appleMain { 85 | dependsOn(nonJvmMain) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-svg-std/src/androidMain/kotlin/io/kamel/image/decoder/AndroidSvgDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.geometry.Size 4 | import androidx.compose.ui.geometry.isSpecified 5 | import androidx.compose.ui.graphics.drawscope.DrawScope 6 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas 7 | import androidx.compose.ui.graphics.nativeCanvas 8 | import androidx.compose.ui.graphics.painter.Painter 9 | import androidx.compose.ui.unit.Density 10 | import androidx.compose.ui.unit.IntSize 11 | import androidx.compose.ui.unit.toSize 12 | import com.caverock.androidsvg.PreserveAspectRatio 13 | import com.caverock.androidsvg.SVG 14 | import io.kamel.core.config.ResourceConfig 15 | import io.kamel.core.decoder.Decoder 16 | import io.ktor.utils.io.* 17 | import io.ktor.utils.io.jvm.javaio.* 18 | import kotlin.math.ceil 19 | import kotlin.reflect.KClass 20 | 21 | internal actual val SvgDecoder = object : Decoder { 22 | 23 | override val outputKClass: KClass 24 | get() = Painter::class 25 | 26 | override suspend fun decode(channel: ByteReadChannel, resourceConfig: ResourceConfig): Painter { 27 | val svg = SVG.getFromInputStream(channel.toInputStream()) 28 | return SVGPainter(svg, resourceConfig.density) 29 | } 30 | } 31 | 32 | internal class SVGPainter( 33 | private val dom: SVG, 34 | private val density: Density 35 | ) : Painter() { 36 | 37 | private val defaultSize: Size = run { 38 | val width = dom.documentWidth 39 | val height = dom.documentHeight 40 | if (width == 0f && height == 0f) { 41 | Size.Unspecified 42 | } else { 43 | Size(width, height) 44 | } 45 | } 46 | 47 | override val intrinsicSize: Size 48 | get() { 49 | return if (defaultSize.isSpecified) { 50 | defaultSize * density.density 51 | } else { 52 | Size.Unspecified 53 | } 54 | } 55 | 56 | override fun DrawScope.onDraw() { 57 | drawSvg( 58 | size = IntSize(ceil(size.width).toInt(), ceil(size.height).toInt()).toSize() 59 | ) 60 | } 61 | 62 | private fun DrawScope.drawSvg(size: Size) { 63 | drawIntoCanvas { canvas -> 64 | if (dom.documentViewBox == null) { 65 | dom.setDocumentViewBox(0f, 0f, dom.documentWidth, dom.documentHeight) 66 | } 67 | dom.documentWidth = size.width 68 | dom.documentHeight = size.height 69 | dom.documentPreserveAspectRatio = PreserveAspectRatio.STRETCH 70 | dom.renderToCanvas(canvas.nativeCanvas) 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-svg-std/src/commonMain/kotlin/io/kamel/image/config/KamelConfigStdSvgDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfigBuilder 4 | import io.kamel.image.decoder.SvgDecoder 5 | 6 | /** 7 | * Adds Decoder for SVG Images to the [KamelConfigBuilder] 8 | */ 9 | public fun KamelConfigBuilder.svgDecoder(): Unit = decoder(SvgDecoder) 10 | -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-svg-std/src/commonMain/kotlin/io/kamel/image/decoder/SvgDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.graphics.painter.Painter 4 | import io.kamel.core.decoder.Decoder 5 | 6 | /** 7 | * Decodes and transfers [ByteReadChannel] to [Painter]. 8 | */ 9 | internal expect val SvgDecoder : Decoder -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-svg-std/src/jvmMain/kotlin/io/kamel/image/decoder/SvgDecoder.jvm.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.graphics.painter.Painter 4 | import androidx.compose.ui.res.loadSvgPainter 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.decoder.Decoder 7 | import io.ktor.utils.io.* 8 | import io.ktor.utils.io.jvm.javaio.* 9 | import kotlin.reflect.KClass 10 | 11 | internal actual val SvgDecoder = object : Decoder { 12 | 13 | override val outputKClass: KClass = Painter::class 14 | 15 | override suspend fun decode(channel: ByteReadChannel, resourceConfig: ResourceConfig): Painter { 16 | return loadSvgPainter( 17 | channel.toInputStream(), 18 | resourceConfig.density 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-svg-std/src/nonJvmMain/kotlin/DrawCache.kt: -------------------------------------------------------------------------------- 1 | package androidx.compose.ui.res 2 | 3 | import androidx.compose.ui.graphics.* 4 | import androidx.compose.ui.graphics.drawscope.CanvasDrawScope 5 | import androidx.compose.ui.graphics.drawscope.DrawScope 6 | import androidx.compose.ui.unit.Density 7 | import androidx.compose.ui.unit.IntSize 8 | import androidx.compose.ui.unit.LayoutDirection 9 | import androidx.compose.ui.unit.toSize 10 | 11 | /** 12 | * Creates a drawing environment that directs its drawing commands to an [ImageBitmap] 13 | * which can be drawn directly in another [DrawScope] instance. This is useful to cache 14 | * complicated drawing commands across frames especially if the content has not changed. 15 | * Additionally some drawing operations such as rendering paths are done purely in 16 | * software so it is beneficial to cache the result and render the contents 17 | * directly through a texture as done by [DrawScope.drawImage] 18 | */ 19 | // Note copied from here: 20 | // https://github.com/JetBrains/compose-multiplatform-core/blob/fcaca4dc0666ca101a4d5c8200d9851e3f6cb88d/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/DrawCache.kt 21 | // todo: remove when available in common androidx 22 | internal class DrawCache { 23 | 24 | @PublishedApi 25 | internal var mCachedImage: ImageBitmap? = null 26 | private var cachedCanvas: Canvas? = null 27 | private var scopeDensity: Density? = null 28 | private var layoutDirection: LayoutDirection = LayoutDirection.Ltr 29 | private var size: IntSize = IntSize.Zero 30 | private var config: ImageBitmapConfig = ImageBitmapConfig.Argb8888 31 | 32 | private val cacheScope = CanvasDrawScope() 33 | 34 | /** 35 | * Draw the contents of the lambda with receiver scope into an [ImageBitmap] with the provided 36 | * size. If the same size is provided across calls, the same [ImageBitmap] instance is 37 | * re-used and the contents are cleared out before drawing content in it again 38 | */ 39 | fun drawCachedImage( 40 | config: ImageBitmapConfig, 41 | size: IntSize, 42 | density: Density, 43 | layoutDirection: LayoutDirection, 44 | block: DrawScope.() -> Unit 45 | ) { 46 | this.scopeDensity = density 47 | this.layoutDirection = layoutDirection 48 | var targetImage = mCachedImage 49 | var targetCanvas = cachedCanvas 50 | if (targetImage == null || targetCanvas == null || size.width > targetImage.width || size.height > targetImage.height || this.config != config) { 51 | targetImage = ImageBitmap(size.width, size.height, config = config) 52 | targetCanvas = Canvas(targetImage) 53 | 54 | mCachedImage = targetImage 55 | cachedCanvas = targetCanvas 56 | this.config = config 57 | } 58 | this.size = size 59 | cacheScope.draw(density, layoutDirection, targetCanvas, size.toSize()) { 60 | clear() 61 | block() 62 | } 63 | targetImage.prepareToDraw() 64 | } 65 | 66 | /** 67 | * Draw the cached content into the provided [DrawScope] instance 68 | */ 69 | fun drawInto( 70 | target: DrawScope, alpha: Float = 1.0f, colorFilter: ColorFilter? = null 71 | ) { 72 | val targetImage = mCachedImage 73 | check(targetImage != null) { 74 | "drawCachedImage must be invoked first before attempting to draw the result " + "into another destination" 75 | } 76 | target.drawImage(targetImage, srcSize = size, alpha = alpha, colorFilter = colorFilter) 77 | } 78 | 79 | /** 80 | * Helper method to clear contents of the draw environment from the given bounds of the 81 | * DrawScope 82 | */ 83 | private fun DrawScope.clear() { 84 | drawRect(color = Color.Black, blendMode = BlendMode.Clear) 85 | } 86 | } -------------------------------------------------------------------------------- /kamel-decoder/kamel-decoder-svg-std/src/nonJvmMain/kotlin/io/kamel/image/decoder/SvgDecoder.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.decoder 2 | 3 | import androidx.compose.ui.graphics.painter.Painter 4 | import androidx.compose.ui.res.loadSvgPainter 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.decoder.Decoder 7 | import io.ktor.util.* 8 | import io.ktor.utils.io.* 9 | import kotlin.reflect.KClass 10 | 11 | 12 | internal actual val SvgDecoder = object : Decoder { 13 | 14 | override val outputKClass: KClass = Painter::class 15 | 16 | override suspend fun decode(channel: ByteReadChannel, resourceConfig: ResourceConfig): Painter { 17 | return loadSvgPainter( 18 | channel.toByteArray(), 19 | resourceConfig.density 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /kamel-fetcher/kamel-fetcher-resources-android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 2 | 3 | plugins { 4 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 5 | alias(libs.plugins.com.android.library) 6 | alias(libs.plugins.com.vanniktech.maven.publish) 7 | } 8 | 9 | android { 10 | compileSdk = 36 11 | 12 | defaultConfig { 13 | minSdk = 21 14 | multiDexEnabled = true 15 | } 16 | 17 | namespace = "io.kamel.image" 18 | 19 | compileOptions { 20 | sourceCompatibility = JavaVersion.VERSION_1_8 21 | targetCompatibility = JavaVersion.VERSION_1_8 22 | } 23 | 24 | testOptions { 25 | unitTests { 26 | isIncludeAndroidResources = true 27 | } 28 | } 29 | 30 | } 31 | 32 | kotlin { 33 | explicitApi = ExplicitApiMode.Warning 34 | androidTarget { 35 | publishAllLibraryVariants() 36 | } 37 | applyDefaultHierarchyTemplate() 38 | sourceSets { 39 | val androidMain by getting { 40 | dependencies { 41 | implementation(projects.kamelCore) 42 | // todo: remove ktor dependency related to https://github.com/Kamel-Media/Kamel/issues/35 43 | implementation(libs.ktor.client.core) 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /kamel-fetcher/kamel-fetcher-resources-android/src/androidMain/kotlin/io/kamel/image/config/KamelConfigResourcesFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import android.content.Context 4 | import io.kamel.core.config.KamelConfigBuilder 5 | import io.kamel.image.fetcher.ResourcesFetcher 6 | 7 | /** 8 | * Adds an Android resources fetcher to the [KamelConfigBuilder]. 9 | */ 10 | public fun KamelConfigBuilder.resourcesFetcher(context: Context): Unit = fetcher(ResourcesFetcher(context)) 11 | -------------------------------------------------------------------------------- /kamel-fetcher/kamel-fetcher-resources-android/src/androidMain/kotlin/io/kamel/image/fetcher/ResourcesFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.fetcher 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import io.kamel.core.DataSource 6 | import io.kamel.core.Resource 7 | import io.kamel.core.config.ResourceConfig 8 | import io.kamel.core.fetcher.Fetcher 9 | import io.ktor.http.* 10 | import io.ktor.utils.io.* 11 | import io.ktor.utils.io.jvm.javaio.* 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flow 14 | import kotlin.reflect.KClass 15 | 16 | private val Url.path: String get() = encodedPath.removePrefix("/") 17 | 18 | internal class ResourcesFetcher(private val context: Context) : Fetcher { 19 | 20 | override val inputDataKClass: KClass = Url::class 21 | 22 | override val source: DataSource = DataSource.Disk 23 | 24 | override val Url.isSupported: Boolean 25 | get() = protocol.name == ContentResolver.SCHEME_ANDROID_RESOURCE 26 | 27 | override fun fetch( 28 | data: Url, 29 | resourceConfig: ResourceConfig 30 | ): Flow> = flow { 31 | val resId = data.path 32 | .toIntOrNull() ?: throw IllegalArgumentException("Invalid resource id $data") 33 | 34 | val bytes = context.resources.openRawResource(resId) 35 | .toByteReadChannel() 36 | 37 | emit(Resource.Success(bytes)) 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /kamel-fetcher/kamel-fetcher-resources-jvm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 2 | 3 | plugins { 4 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 5 | alias(libs.plugins.com.vanniktech.maven.publish) 6 | } 7 | 8 | kotlin { 9 | explicitApi = ExplicitApiMode.Warning 10 | jvm() 11 | applyDefaultHierarchyTemplate() 12 | sourceSets { 13 | val jvmMain by getting { 14 | dependencies { 15 | implementation(projects.kamelCore) 16 | // todo: remove ktor dependency related to https://github.com/Kamel-Media/Kamel/issues/35 17 | implementation(libs.ktor.client.core) 18 | } 19 | } 20 | val commonTest by getting { 21 | dependencies { 22 | implementation(kotlin("test")) 23 | implementation(libs.kotlinx.coroutines.test) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /kamel-fetcher/kamel-fetcher-resources-jvm/src/jvmMain/kotlin/io/kamel/image/config/KamelConfigResourcesFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfigBuilder 4 | import io.kamel.image.fetcher.ResourcesFetcher 5 | 6 | /** 7 | * Adds application resources fetcher to the [KamelConfigBuilder]. 8 | */ 9 | public fun KamelConfigBuilder.resourcesFetcher(): Unit = fetcher(ResourcesFetcher) 10 | -------------------------------------------------------------------------------- /kamel-fetcher/kamel-fetcher-resources-jvm/src/jvmMain/kotlin/io/kamel/image/fetcher/ResourcesFetcher.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.Resource 5 | import io.kamel.core.config.ResourceConfig 6 | import io.kamel.core.fetcher.Fetcher 7 | import io.ktor.http.* 8 | import io.ktor.utils.io.* 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.flow 11 | import kotlin.reflect.KClass 12 | 13 | private val Url.path: String get() = encodedPath.removePrefix("/") 14 | 15 | internal object ResourcesFetcher : Fetcher { 16 | 17 | override val inputDataKClass: KClass = Url::class 18 | 19 | override val source: DataSource = DataSource.Disk 20 | 21 | override val Url.isSupported: Boolean 22 | get() = Thread.currentThread().contextClassLoader?.getResource(path) != null 23 | 24 | override fun fetch( 25 | data: Url, 26 | resourceConfig: ResourceConfig 27 | ): Flow> = flow { 28 | val bytes = Thread.currentThread().contextClassLoader 29 | ?.getResource(data.path) 30 | ?.readBytes() 31 | ?.let { ByteReadChannel(it) } ?: error("Unable to find resource $data") 32 | emit(Resource.Success(bytes, source)) 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /kamel-fetcher/kamel-fetcher-resources-jvm/src/jvmTest/kotlin/io/kamel/image/fetcher/ResourcesFetcherTest.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.fetcher 2 | 3 | import io.kamel.core.DataSource 4 | import io.kamel.core.config.ResourceConfigBuilder 5 | import io.kamel.core.getOrNull 6 | import io.kamel.core.isLoading 7 | import io.kamel.core.map 8 | import io.ktor.http.* 9 | import io.ktor.utils.io.toByteArray 10 | import kotlinx.coroutines.flow.first 11 | import kotlinx.coroutines.test.runTest 12 | import kotlin.test.Test 13 | import kotlin.test.assertFailsWith 14 | import kotlin.test.assertFalse 15 | import kotlin.test.assertTrue 16 | 17 | class ResourcesFetcherTest { 18 | 19 | private val fetcher = ResourcesFetcher 20 | 21 | @Test 22 | fun testUrlIsSupported() { 23 | val imageUrl = Url("Compose.png") 24 | val isSupported = with(fetcher) { imageUrl.isSupported } 25 | 26 | assertTrue { isSupported } 27 | } 28 | 29 | @Test 30 | fun testUrlIsNotSupported() { 31 | val imageUrl = Url("invalidImage.jpg") 32 | val isSupported = with(fetcher) { imageUrl.isSupported } 33 | 34 | assertFalse { isSupported } 35 | } 36 | 37 | @Test 38 | fun loadImageBitmapResource() = runTest { 39 | val resourceConfig = ResourceConfigBuilder(coroutineContext).build() 40 | val imageUrl = Url("Compose.png") 41 | val resource = fetcher.fetch(imageUrl, resourceConfig).first { !it.isLoading }.map { it.toByteArray() } 42 | 43 | assertTrue { resource.getOrNull()!!.isNotEmpty() } 44 | assertTrue { resource.source == DataSource.Disk } 45 | } 46 | 47 | @Test 48 | fun loadInvalidImageResource() = runTest { 49 | val resourceConfig = ResourceConfigBuilder(coroutineContext).build() 50 | val imageUrl = Url("invalidImage.jpg") 51 | 52 | assertFailsWith { 53 | fetcher.fetch(imageUrl, resourceConfig).first { !it.isLoading } 54 | } 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /kamel-fetcher/kamel-fetcher-resources-jvm/src/jvmTest/resources/Compose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-fetcher/kamel-fetcher-resources-jvm/src/jvmTest/resources/Compose.png -------------------------------------------------------------------------------- /kamel-image-default/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | 4 | plugins { 5 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 6 | alias(libs.plugins.org.jetbrains.compose) 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.com.android.library) 9 | alias(libs.plugins.com.vanniktech.maven.publish) 10 | } 11 | 12 | android { 13 | compileSdk = 36 14 | 15 | defaultConfig { 16 | minSdk = 21 17 | multiDexEnabled = true 18 | } 19 | 20 | namespace = "io.kamel.image" 21 | 22 | compileOptions { 23 | sourceCompatibility = JavaVersion.VERSION_1_8 24 | targetCompatibility = JavaVersion.VERSION_1_8 25 | } 26 | 27 | testOptions { 28 | unitTests { 29 | isIncludeAndroidResources = true 30 | } 31 | } 32 | 33 | } 34 | 35 | kotlin { 36 | explicitApi = ExplicitApiMode.Warning 37 | 38 | androidTarget { 39 | publishAllLibraryVariants() 40 | } 41 | jvm() 42 | js(IR) { 43 | browser() 44 | } 45 | @OptIn(ExperimentalWasmDsl::class) wasmJs { 46 | browser() 47 | } 48 | iosArm64() 49 | iosSimulatorArm64() 50 | iosX64() 51 | macosX64() 52 | macosArm64() 53 | applyDefaultHierarchyTemplate() 54 | 55 | sourceSets { 56 | 57 | val commonMain by getting { 58 | dependencies { 59 | api(projects.kamelImage) 60 | api(projects.kamelDecoder.kamelDecoderSvgStd) 61 | api(projects.kamelDecoder.kamelDecoderImageBitmap) 62 | api(projects.kamelDecoder.kamelDecoderImageVector) 63 | api(projects.kamelDecoder.kamelDecoderAnimatedImage) 64 | implementation(compose.foundation) 65 | } 66 | } 67 | 68 | val commonJvmMain by creating { 69 | dependsOn(commonMain) 70 | } 71 | 72 | jvmMain { 73 | dependsOn(commonJvmMain) 74 | dependencies { 75 | api(projects.kamelFetcher.kamelFetcherResourcesJvm) 76 | implementation(libs.ktor.client.cio) 77 | } 78 | } 79 | 80 | androidMain { 81 | resources.srcDirs("src/commonJvmMain/resources") 82 | dependsOn(commonJvmMain) 83 | dependencies { 84 | api(projects.kamelFetcher.kamelFetcherResourcesAndroid) 85 | api(projects.kamelMapper.kamelMapperResourcesIdAndroid) 86 | implementation(libs.ktor.client.android) 87 | } 88 | } 89 | 90 | val nonJvmMain by creating { 91 | dependsOn(commonMain) 92 | } 93 | 94 | jsMain { 95 | dependsOn(nonJvmMain) 96 | dependencies { 97 | implementation(libs.ktor.client.js) 98 | } 99 | } 100 | 101 | val wasmJsMain by getting { 102 | dependsOn(nonJvmMain) 103 | dependencies { 104 | implementation(libs.ktor.client.js) 105 | } 106 | } 107 | 108 | nativeMain { 109 | dependsOn(nonJvmMain) 110 | } 111 | 112 | appleMain { 113 | dependencies { 114 | implementation(libs.ktor.client.darwin) 115 | } 116 | } 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /kamel-image-default/src/androidMain/kotlin/io/kamel/image/config/KamelConfig.android.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.applicationContext 4 | import io.kamel.core.config.KamelConfigBuilder 5 | 6 | internal actual fun KamelConfigBuilder.platformSpecificConfig() { 7 | if (applicationContext == null) { 8 | println("Warning: Android application context is not provided. Skipping adding Kamel Components requiring Android application context.") 9 | } 10 | 11 | applicationContext?.applicationContext?.let { context -> 12 | resourcesIdMapper(context) 13 | resourcesFetcher(context) 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /kamel-image-default/src/androidMain/resources/META-INF/services/io.kamel.image.config.KamelConfigService: -------------------------------------------------------------------------------- 1 | io.kamel.image.config.DefaultKamelConfigService -------------------------------------------------------------------------------- /kamel-image-default/src/appleMain/kotlin/io/kamel/image/config/KamelConfig.apple.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfigBuilder 4 | 5 | internal actual fun KamelConfigBuilder.platformSpecificConfig() { 6 | } -------------------------------------------------------------------------------- /kamel-image-default/src/commonJvmMain/kotlin/io/kamel/image/config/DefaultKamelConfigService.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfig 4 | 5 | public class DefaultKamelConfigService : KamelConfigService { 6 | override fun getKamelConfig(): KamelConfig { 7 | return KamelConfig.Default 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /kamel-image-default/src/commonJvmMain/resources/META-INF/services/io.kamel.image.config.KamelConfigService: -------------------------------------------------------------------------------- 1 | io.kamel.image.config.DefaultKamelConfigService -------------------------------------------------------------------------------- /kamel-image-default/src/commonMain/kotlin/io/kamel/image/config/KamelConfigImageDefault.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.Core 4 | import io.kamel.core.config.KamelConfig 5 | import io.kamel.core.config.KamelConfigBuilder 6 | import io.kamel.core.config.takeFrom 7 | 8 | public val KamelConfig.Companion.Default: KamelConfig 9 | get() = KamelConfig { 10 | takeFrom(KamelConfig.Core) 11 | imageBitmapDecoder() 12 | imageVectorDecoder() 13 | svgDecoder() 14 | animatedImageDecoder() 15 | platformSpecificConfig() 16 | } 17 | 18 | internal expect fun KamelConfigBuilder.platformSpecificConfig() 19 | -------------------------------------------------------------------------------- /kamel-image-default/src/jsMain/kotlin/io/kamel/image/config/DefaultKamelConfigServiceInitializer.js.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | @Suppress("DEPRECATION", "NON_EXPORTABLE_TYPE") 4 | @OptIn(ExperimentalStdlibApi::class, ExperimentalJsExport::class) 5 | @EagerInitialization 6 | // https://youtrack.jetbrains.com/issue/KT-51626/Kotlin-JS-EagerInitialization-annotation-has-no-effect-on-unused-properties 7 | @JsExport 8 | public val initializer: ConfigInitializer = ConfigInitializer -------------------------------------------------------------------------------- /kamel-image-default/src/jsMain/kotlin/io/kamel/image/config/KamelConfigImageDefault.js.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfigBuilder 4 | 5 | internal actual fun KamelConfigBuilder.platformSpecificConfig() { 6 | } 7 | -------------------------------------------------------------------------------- /kamel-image-default/src/jvmMain/kotlin/io/kamel/image/config/KamelConfig.desktop.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfigBuilder 4 | 5 | internal actual fun KamelConfigBuilder.platformSpecificConfig() { 6 | } -------------------------------------------------------------------------------- /kamel-image-default/src/nativeMain/kotlin/io/kamel/image/config/DefaultKamelConfigServiceInitializer.native.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | @Suppress("DEPRECATION") 4 | @OptIn(ExperimentalStdlibApi::class) 5 | @EagerInitialization 6 | private val initializer: ConfigInitializer = ConfigInitializer -------------------------------------------------------------------------------- /kamel-image-default/src/nonJvmMain/kotlin/io/kamel/image/config/DefaultKamelConfigServiceInitializer.native.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfig 4 | 5 | public object ConfigInitializer { 6 | init { 7 | detectedKamelConfig = KamelConfig.Default 8 | } 9 | } -------------------------------------------------------------------------------- /kamel-image-default/src/wasmJsMain/kotlin/io/kamel/image/config/DefaultKamelConfigServiceInitializer.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | @Suppress("DEPRECATION") 4 | @OptIn(ExperimentalStdlibApi::class) 5 | @EagerInitialization 6 | private val initializer: ConfigInitializer = ConfigInitializer 7 | -------------------------------------------------------------------------------- /kamel-image-default/src/wasmJsMain/kotlin/io/kamel/image/config/KamelConfigImageDefault.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfigBuilder 4 | 5 | internal actual fun KamelConfigBuilder.platformSpecificConfig() { 6 | } 7 | -------------------------------------------------------------------------------- /kamel-image/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.ExperimentalComposeLibrary 2 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 3 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 4 | 5 | plugins { 6 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 7 | alias(libs.plugins.org.jetbrains.compose) 8 | alias(libs.plugins.compose.compiler) 9 | alias(libs.plugins.com.android.library) 10 | alias(libs.plugins.com.vanniktech.maven.publish) 11 | } 12 | 13 | android { 14 | compileSdk = 36 15 | 16 | defaultConfig { 17 | minSdk = 21 18 | multiDexEnabled = true 19 | } 20 | 21 | namespace = "io.kamel.image" 22 | 23 | compileOptions { 24 | sourceCompatibility = JavaVersion.VERSION_1_8 25 | targetCompatibility = JavaVersion.VERSION_1_8 26 | } 27 | 28 | testOptions { 29 | unitTests { 30 | isIncludeAndroidResources = true 31 | } 32 | } 33 | 34 | } 35 | 36 | kotlin { 37 | explicitApi = ExplicitApiMode.Warning 38 | 39 | androidTarget { 40 | publishAllLibraryVariants() 41 | } 42 | jvm("desktopJvm") 43 | js(IR) { 44 | browser() 45 | } 46 | @OptIn(ExperimentalWasmDsl::class) wasmJs { 47 | browser() 48 | } 49 | iosArm64() 50 | iosSimulatorArm64() 51 | iosX64() 52 | macosX64() 53 | macosArm64() 54 | applyDefaultHierarchyTemplate() 55 | 56 | sourceSets { 57 | 58 | val commonMain by getting { 59 | dependencies { 60 | api(projects.kamelCore) 61 | implementation(compose.foundation) 62 | implementation(libs.ktor.client.core) 63 | } 64 | } 65 | 66 | val commonTest by getting { 67 | dependencies { 68 | implementation(compose.material3) 69 | implementation(kotlin("test")) 70 | @OptIn(ExperimentalComposeLibrary::class) 71 | implementation(compose.uiTest) 72 | implementation(libs.ktor.client.mock) 73 | implementation(libs.kotlinx.coroutines.test) 74 | } 75 | } 76 | 77 | val commonJvmMain by creating { 78 | dependsOn(commonMain) 79 | } 80 | 81 | val desktopJvmTest by getting { 82 | dependencies { 83 | implementation(compose.desktop.currentOs) 84 | } 85 | } 86 | 87 | val desktopJvmMain by getting { 88 | dependsOn(commonJvmMain) 89 | dependencies { 90 | implementation(compose.desktop.currentOs) 91 | } 92 | } 93 | androidMain.get().dependsOn(commonJvmMain) 94 | 95 | val nonJvmMain by creating { 96 | dependsOn(commonMain) 97 | } 98 | 99 | val wasmJsMain by getting { 100 | dependsOn(nonJvmMain) 101 | } 102 | jsMain.get().dependsOn(nonJvmMain) 103 | nativeMain.get().dependsOn(nonJvmMain) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /kamel-image/src/androidMain/kotlin/io/kamel/image/config/DetectedKamelConfig.android.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfig 4 | import java.util.* 5 | 6 | public actual var detectedKamelConfig: KamelConfig? = 7 | ServiceLoader.load(KamelConfigService::class.java).firstOrNull()?.getKamelConfig() -------------------------------------------------------------------------------- /kamel-image/src/commonJvmMain/kotlin/io/kamel/image/config/KamelConfigService.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfig 4 | 5 | public interface KamelConfigService { 6 | public fun getKamelConfig(): KamelConfig 7 | } -------------------------------------------------------------------------------- /kamel-image/src/commonMain/kotlin/io/kamel/image/config/LocalKamelConfig.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import androidx.compose.runtime.ProvidableCompositionLocal 4 | import androidx.compose.runtime.staticCompositionLocalOf 5 | import io.kamel.core.config.Core 6 | import io.kamel.core.config.KamelConfig 7 | 8 | /** 9 | * Static CompositionLocal that provides the default configuration of [KamelConfig]. 10 | */ 11 | public val LocalKamelConfig: ProvidableCompositionLocal = 12 | staticCompositionLocalOf { detectedKamelConfig ?: KamelConfig.Core } 13 | 14 | public expect var detectedKamelConfig: KamelConfig? -------------------------------------------------------------------------------- /kamel-image/src/commonTest/kotlin/io/kamel/image/KamelImageTest.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.CircularProgressIndicator 6 | import androidx.compose.material3.Text 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.ImageBitmap 9 | import androidx.compose.ui.graphics.painter.BitmapPainter 10 | import androidx.compose.ui.graphics.painter.Painter 11 | import androidx.compose.ui.platform.testTag 12 | import androidx.compose.ui.test.* 13 | import androidx.compose.ui.unit.dp 14 | import io.kamel.core.Resource 15 | import io.kamel.core.isLoading 16 | import io.kamel.core.map 17 | import kotlin.test.Test 18 | 19 | 20 | private const val ImageContentDescription = "Image" 21 | private const val ImageTag = "Image" 22 | private const val ErrorMessage = "Error" 23 | 24 | @OptIn(ExperimentalTestApi::class) 25 | class KamelImageTest { 26 | 27 | 28 | @Test 29 | fun testDisplayingSuccessImageResource() = runComposeUiTest { 30 | 31 | val imageBitmap = ImageBitmap(1, 1) 32 | 33 | val painterResource: Resource = Resource.Success(imageBitmap) 34 | .map { BitmapPainter(it) } 35 | 36 | setContent { 37 | KamelImage( 38 | resource = { painterResource }, 39 | contentDescription = ImageContentDescription, 40 | modifier = Modifier 41 | .size(256.dp) 42 | ) 43 | } 44 | 45 | onNodeWithContentDescription(ImageContentDescription) 46 | .assertExists() 47 | .assertHeightIsEqualTo(256.dp) 48 | .assertWidthIsEqualTo(256.dp) 49 | } 50 | 51 | @Test 52 | fun testDisplayingLoadingImageResource() = runComposeUiTest { 53 | val painterResource: Resource = Resource.Loading(0F) 54 | setContent { 55 | if (painterResource.isLoading) 56 | Box(Modifier.size(200.dp).testTag(ImageTag)) { 57 | CircularProgressIndicator() 58 | } 59 | 60 | KamelImage( 61 | resource = { painterResource }, 62 | contentDescription = ImageContentDescription, 63 | onLoading = {}, 64 | ) 65 | } 66 | 67 | onNodeWithTag(ImageTag) 68 | .assertExists() 69 | .assertHeightIsEqualTo(200.dp) 70 | .assertWidthIsEqualTo(200.dp) 71 | } 72 | 73 | @Test 74 | fun testDisplayingFailureImageResource() = runComposeUiTest { 75 | val painterResource: Resource = Resource.Failure(Throwable(ErrorMessage)) 76 | 77 | setContent { 78 | KamelImage( 79 | resource = { painterResource }, 80 | contentDescription = ImageContentDescription, 81 | onFailure = { 82 | } 83 | ) 84 | if (painterResource is Resource.Failure) 85 | Text(painterResource.exception.message!!, Modifier.testTag(ImageTag)) 86 | } 87 | 88 | onNodeWithTag(ImageTag) 89 | .assertExists() 90 | .assertTextEquals(ErrorMessage) 91 | } 92 | 93 | } -------------------------------------------------------------------------------- /kamel-image/src/desktopJvmMain/kotlin/io/kamel/image/config/DetectedKamelConfig.desktopJvm.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfig 4 | import java.util.* 5 | 6 | public actual var detectedKamelConfig: KamelConfig? = 7 | ServiceLoader.load(KamelConfigService::class.java).findFirst().orElse(null)?.getKamelConfig() 8 | -------------------------------------------------------------------------------- /kamel-image/src/nonJvmMain/kotlin/io/kamel/image/config/DetectedKamelConfig.nonJvm.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import io.kamel.core.config.KamelConfig 4 | 5 | public actual var detectedKamelConfig: KamelConfig? = null -------------------------------------------------------------------------------- /kamel-mapper/kamel-mapper-resources-id-android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 2 | 3 | plugins { 4 | alias(libs.plugins.org.jetbrains.kotlin.multiplatform) 5 | alias(libs.plugins.com.android.library) 6 | alias(libs.plugins.com.vanniktech.maven.publish) 7 | } 8 | 9 | android { 10 | compileSdk = 36 11 | 12 | defaultConfig { 13 | minSdk = 21 14 | multiDexEnabled = true 15 | } 16 | 17 | namespace = "io.kamel.image" 18 | 19 | compileOptions { 20 | sourceCompatibility = JavaVersion.VERSION_1_8 21 | targetCompatibility = JavaVersion.VERSION_1_8 22 | } 23 | 24 | testOptions { 25 | unitTests { 26 | isIncludeAndroidResources = true 27 | } 28 | } 29 | 30 | } 31 | 32 | kotlin { 33 | explicitApi = ExplicitApiMode.Warning 34 | androidTarget { 35 | publishAllLibraryVariants() 36 | } 37 | applyDefaultHierarchyTemplate() 38 | sourceSets { 39 | val androidMain by getting { 40 | dependencies { 41 | implementation(projects.kamelCore) 42 | // todo: remove ktor dependency related to https://github.com/Kamel-Media/Kamel/issues/35 43 | implementation(libs.ktor.client.core) 44 | implementation(libs.androidx.annotation) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /kamel-mapper/kamel-mapper-resources-id-android/src/androidMain/kotlin/io/kamel/image/config/KamelConfigResourcesIdMapper.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.config 2 | 3 | import android.content.Context 4 | import io.kamel.core.config.KamelConfigBuilder 5 | import io.kamel.image.mapper.ResourcesIdMapper 6 | 7 | /** 8 | * Adds Android resources fetcher to the [KamelConfigBuilder]. 9 | */ 10 | public fun KamelConfigBuilder.resourcesIdMapper(context: Context): Unit = mapper(ResourcesIdMapper(context)) 11 | -------------------------------------------------------------------------------- /kamel-mapper/kamel-mapper-resources-id-android/src/androidMain/kotlin/io/kamel/image/mapper/ResourcesIdMapper.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.image.mapper 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import androidx.annotation.DrawableRes 6 | import io.kamel.core.mapper.Mapper 7 | import io.ktor.http.* 8 | import kotlin.reflect.KClass 9 | 10 | internal class ResourcesIdMapper(private val context: Context) : Mapper { 11 | 12 | override val inputKClass: KClass 13 | get() = Int::class 14 | 15 | override val outputKClass: KClass 16 | get() = Url::class 17 | 18 | override fun map(@DrawableRes input: Int): Url { 19 | val packageName = context.packageName 20 | val protocol = URLProtocol(name = ContentResolver.SCHEME_ANDROID_RESOURCE, defaultPort = -1) 21 | 22 | return URLBuilder(protocol = protocol, host = packageName, pathSegments = listOf(input.toString())).build() 23 | } 24 | } -------------------------------------------------------------------------------- /kamel-sample-ios/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=io.kamel.samples 3 | APP_NAME=sample 4 | -------------------------------------------------------------------------------- /kamel-sample-ios/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /kamel-sample-ios/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /kamel-sample-ios/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } -------------------------------------------------------------------------------- /kamel-sample-ios/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /kamel-sample-ios/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import shared 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | Main_iosKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /kamel-sample-ios/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /kamel-sample-ios/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /kamel-sample-ios/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct iOSApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | ContentView() 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/kotlin/io/kamel/samples/AndroidSample.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.samples 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.compose.runtime.CompositionLocalProvider 7 | import androidx.compose.runtime.remember 8 | import io.kamel.core.config.KamelConfig 9 | import io.kamel.core.config.takeFrom 10 | import io.kamel.image.config.Default 11 | import io.kamel.image.config.LocalKamelConfig 12 | import io.kamel.image.config.imageBitmapResizingDecoder 13 | import io.kamel.image.config.resourcesFetcher 14 | 15 | public actual val cellsCount: Int = 2 16 | 17 | public class AndroidSample : AppCompatActivity() { 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContent { 22 | val kamelConfig = remember { 23 | KamelConfig { 24 | takeFrom(KamelConfig.Default) 25 | resourcesFetcher(this@AndroidSample) 26 | imageBitmapResizingDecoder() 27 | } 28 | } 29 | CompositionLocalProvider(LocalKamelConfig provides kamelConfig) { 30 | launcher() 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/kotlin/io/kamel/samples/Utils.android.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.samples 2 | 3 | import okio.FileSystem 4 | 5 | public actual val fileSystem: FileSystem = FileSystem.SYSTEM -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/kotlin/io/kamel/samples/getResourceFile.kt: -------------------------------------------------------------------------------- 1 | package io.kamel.samples 2 | 3 | import io.kamel.core.utils.File 4 | import media.kamel.kamel_samples.generated.resources.Res 5 | import java.io.FileOutputStream 6 | 7 | 8 | public actual suspend fun getResourceFile(fileResourcePath: String): File { 9 | val file = java.io.File.createTempFile("temp", ".${fileResourcePath.substringAfterLast(".")}") 10 | FileOutputStream(file).use { os -> 11 | val buffer = Res.readBytes(fileResourcePath) 12 | os.write(buffer, 0, buffer.size) 13 | } 14 | return file 15 | } -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-samples/src/androidMain/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-samples/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-samples/src/androidMain/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-samples/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-samples/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-samples/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-samples/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-samples/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-samples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-samples/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/raw/compose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kamel-Media/Kamel/90a4fff9b04578ab83e32f6b8b5882aebc3a9622/kamel-samples/src/androidMain/res/raw/compose.png -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | My Application 3 | -------------------------------------------------------------------------------- /kamel-samples/src/androidMain/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |