chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:31 +08:00
commit f06de36198
4585 changed files with 588418 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: ['https://boosty.to/t8rin']
+83
View File
@@ -0,0 +1,83 @@
name: Bug Report
description: Create a report to help us improve
labels: ["bug"]
body:
- type: textarea
id: description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
placeholder: Tell us what happened...
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
description: Steps to reproduce the behavior
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
placeholder: Tell us what should have happened...
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
placeholder: You can drag and drop images here...
- type: input
id: device
attributes:
label: Device
placeholder: e.g., Samsung Galaxy S23
validations:
required: true
- type: input
id: os
attributes:
label: OS
placeholder: e.g., Android 14
validations:
required: true
- type: input
id: locale
attributes:
label: Locale
placeholder: e.g., en-US, ru-RU
- type: input
id: version
attributes:
label: App Version
placeholder: e.g., 1.2.3
validations:
required: true
- type: dropdown
id: variant
attributes:
label: FOSS or Market
description: Which variant of the app are you using?
options:
- FOSS
- Market (Google Play)
validations:
required: true
@@ -0,0 +1,28 @@
name: Feature Request
description: Suggest an idea for this project
labels: ["enhancement"]
body:
- type: textarea
id: problem
attributes:
label: Is your feature request related to a problem? Please describe.
placeholder: A clear and concise description of what the problem is...
validations:
required: false
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like
placeholder: A clear and concise description of what you want to happen...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
placeholder: A clear and concise description of any alternative solutions or features you've considered...
validations:
required: false
+11
View File
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gradle" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
+82
View File
@@ -0,0 +1,82 @@
name: Android CI
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- uses: actions/checkout@v4
- name: set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'adopt'
java-package: 'jdk'
cache: gradle
- name: Set up Keystore
run: |
sudo apt update -y || true
sudo apt install -y --no-install-recommends coreutils
mkdir -p $RUNNER_TEMP/keystores
echo "${{ secrets.SIGNING_KEY }}" | base64 --decode > $RUNNER_TEMP/keystores/keystore.jks
- name: Build FOSS APKs
run: bash ./gradlew assembleFossRelease
- name: Sign FOSS APKs
run: |
ANDROID_SDK_PATH=$ANDROID_HOME/build-tools/35.0.0/apksigner
for apk in app/build/outputs/apk/foss/release/*.apk; do
$ANDROID_SDK_PATH sign \
--ks $RUNNER_TEMP/keystores/keystore.jks \
--ks-key-alias ${{ secrets.ALIAS }} \
--ks-pass pass:${{ secrets.KEY_STORE_PASS }} \
--key-pass pass:${{ secrets.KEY_STORE_PASS }} \
--out "$apk" \
"$apk"
done
- name: Upload FOSS APKs
uses: actions/upload-artifact@v4
with:
name: Signed FOSS APKs
path: app/build/outputs/apk/foss/release/*.apk
- name: Delete FOSS APKs
run: rm -rf app/build/outputs/apk/foss
- name: Build Market APKs
run: bash ./gradlew assembleMarketRelease
- name: Sign Market APKs
run: |
ANDROID_SDK_PATH=$ANDROID_HOME/build-tools/35.0.0/apksigner
for apk in app/build/outputs/apk/market/release/*.apk; do
$ANDROID_SDK_PATH sign \
--ks $RUNNER_TEMP/keystores/keystore.jks \
--ks-key-alias ${{ secrets.ALIAS }} \
--ks-pass pass:${{ secrets.KEY_STORE_PASS }} \
--key-pass pass:${{ secrets.KEY_STORE_PASS }} \
--out "$apk" \
"$apk"
done
- name: Upload Market APKs
uses: actions/upload-artifact@v4
with:
name: Signed Market APKs
path: app/build/outputs/apk/market/release/*.apk
- name: Delete Market APKs
run: rm -rf app/build/outputs/apk/market
+57
View File
@@ -0,0 +1,57 @@
name: Android CI FOSS
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- uses: actions/checkout@v4
- name: set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'adopt'
java-package: 'jdk'
cache: gradle
- name: Set up Keystore
run: |
sudo apt update -y || true
sudo apt install -y --no-install-recommends coreutils
mkdir -p $RUNNER_TEMP/keystores
echo "${{ secrets.SIGNING_KEY }}" | base64 --decode > $RUNNER_TEMP/keystores/keystore.jks
- name: Build FOSS APKs
run: bash ./gradlew assembleFossRelease
- name: Sign FOSS APKs
run: |
ANDROID_SDK_PATH=$ANDROID_HOME/build-tools/35.0.0/apksigner
for apk in app/build/outputs/apk/foss/release/*.apk; do
$ANDROID_SDK_PATH sign \
--ks $RUNNER_TEMP/keystores/keystore.jks \
--ks-key-alias ${{ secrets.ALIAS }} \
--ks-pass pass:${{ secrets.KEY_STORE_PASS }} \
--key-pass pass:${{ secrets.KEY_STORE_PASS }} \
--out "$apk" \
"$apk"
done
- name: Upload FOSS APKs
uses: actions/upload-artifact@v4
with:
name: Signed FOSS APKs
path: app/build/outputs/apk/foss/release/*.apk
- name: Delete FOSS APKs
run: rm -rf app/build/outputs/apk/foss
+57
View File
@@ -0,0 +1,57 @@
name: Android CI Market
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Set Swap Space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 10
- uses: actions/checkout@v4
- name: set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'adopt'
java-package: 'jdk'
cache: gradle
- name: Set up Keystore
run: |
sudo apt update -y || true
sudo apt install -y --no-install-recommends coreutils
mkdir -p $RUNNER_TEMP/keystores
echo "${{ secrets.SIGNING_KEY }}" | base64 --decode > $RUNNER_TEMP/keystores/keystore.jks
- name: Build Market APKs
run: bash ./gradlew assembleMarketRelease
- name: Sign Market APKs
run: |
ANDROID_SDK_PATH=$ANDROID_HOME/build-tools/35.0.0/apksigner
for apk in app/build/outputs/apk/market/release/*.apk; do
$ANDROID_SDK_PATH sign \
--ks $RUNNER_TEMP/keystores/keystore.jks \
--ks-key-alias ${{ secrets.ALIAS }} \
--ks-pass pass:${{ secrets.KEY_STORE_PASS }} \
--key-pass pass:${{ secrets.KEY_STORE_PASS }} \
--out "$apk" \
"$apk"
done
- name: Upload Market APKs
uses: actions/upload-artifact@v4
with:
name: Signed Market APKs
path: app/build/outputs/apk/market/release/*.apk
- name: Delete Market APKs
run: rm -rf app/build/outputs/apk/market
+113
View File
@@ -0,0 +1,113 @@
name: Create Release
on:
workflow_dispatch:
#on:
# push:
# tags:
# - '*'
jobs:
build_and_release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'adopt'
cache: gradle
- name: Set version variable
run: echo "GITHUB_REF_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV
- name: Assemble release
env:
VERSION_NAME: ${{ env.GITHUB_REF_NAME }}
run: bash ./gradlew assembleRelease
- uses: iota9star/sign-android-release@v1.0.5
name: Sign FOSS APK
# ID used to access action output
id: sign_app_foss
with:
releaseDirectory: app/build/outputs/apk/foss/release
fileRex: .*apk
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASS }}
keyPassword: ${{ secrets.KEY_STORE_PASS }}
env:
BUILD_TOOLS_VERSION: "34.0.0"
- name: Rename foss file stage
run: |
ls -la app/build/outputs/apk/market/release
mv "app/build/outputs/apk/foss/release/image-toolbox-$GITHUB_REF_NAME-foss-arm64-v8a-release-unsigned-signed.apk" "image-toolbox-$GITHUB_REF_NAME-foss-arm64-v8a.apk"
mv "app/build/outputs/apk/foss/release/image-toolbox-$GITHUB_REF_NAME-foss-universal-release-unsigned-signed.apk" "image-toolbox-$GITHUB_REF_NAME-foss-universal.apk"
mv "app/build/outputs/apk/foss/release/image-toolbox-$GITHUB_REF_NAME-foss-armeabi-v7a-release-unsigned-signed.apk" "image-toolbox-$GITHUB_REF_NAME-foss-armeabi-v7a.apk"
mv "app/build/outputs/apk/foss/release/image-toolbox-$GITHUB_REF_NAME-foss-x86_64-release-unsigned-signed.apk" "image-toolbox-$GITHUB_REF_NAME-foss-x86_64.apk"
- uses: iota9star/sign-android-release@v1.0.5
name: Sign Market APK
# ID used to access action output
id: sign_app_market
with:
releaseDirectory: app/build/outputs/apk/market/release
fileRex: .*apk
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASS }}
keyPassword: ${{ secrets.KEY_STORE_PASS }}
env:
BUILD_TOOLS_VERSION: "34.0.0"
- name: Rename market file stage
env:
VERSION_NAME: ${{ env.GITHUB_REF_NAME }}
run: |
ls -la app/build/outputs/apk/market/release
mv "app/build/outputs/apk/market/release/image-toolbox-$VERSION_NAME-market-arm64-v8a-release-unsigned-signed.apk" "image-toolbox-$VERSION_NAME-arm64-v8a.apk"
mv "app/build/outputs/apk/market/release/image-toolbox-$VERSION_NAME-market-universal-release-unsigned-signed.apk" "image-toolbox-$VERSION_NAME-universal.apk"
mv "app/build/outputs/apk/market/release/image-toolbox-$VERSION_NAME-market-armeabi-v7a-release-unsigned-signed.apk" "image-toolbox-$VERSION_NAME-armeabi-v7a.apk"
mv "app/build/outputs/apk/market/release/image-toolbox-$VERSION_NAME-market-x86_64-release-unsigned-signed.apk" "image-toolbox-$VERSION_NAME-x86_64.apk"
- uses: actions/upload-artifact@v4
id: signed-market-apk
with:
name: Signed apks Market
path: "*.apk"
create_release:
runs-on: ubuntu-latest
permissions:
contents: write
needs:
- build_and_release
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download All Artifacts
uses: actions/download-artifact@v4
with:
merge-multiple: true
- name: Display all downloaded files
run: ls -la
- name: Set version variable
run: echo "GITHUB_REF_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV
- name: Set Pre-release flag
run: |
if [[ "$GITHUB_REF_NAME" == *-* ]]; then
# If GITHUB_REF_NAME contains a hyphen, set GITHUB_OTHER_ENV to true
echo "PRE_RELEASE_FLAG=true" >> $GITHUB_ENV
else
# If GITHUB_REF_NAME does not contain a hyphen, set GITHUB_OTHER_ENV to false
echo "PRE_RELEASE_FLAG=false" >> $GITHUB_ENV
fi
- uses: ncipollo/release-action@v1
with:
artifacts: "*.apk"
prerelease: ${{ env.PRE_RELEASE_FLAG }}
+14
View File
@@ -0,0 +1,14 @@
*.iml
.gradle
/local.properties
.DS_Store
/build
/app/release
/captures
.externalNativeBuild
.cxx
/.idea
/libs/nQuant/build
/.kotlin/errors/
/.kotlin
/scripts/
+572
View File
@@ -0,0 +1,572 @@
# 📐 Modules Graph
```mermaid
%%{
init: {
"theme": "base",
"themeVariables": {
"mainBkg": "#121418",
"primaryColor": "#1b3a1b",
"primaryTextColor": "#e0e3de",
"primaryBorderColor": "#76c893",
"nodeBorder": "#76c893",
"lineColor": "#76c893",
"secondaryColor": "#1f2721",
"tertiaryColor": "#232c26",
"clusterBkg": "#182018",
"clusterBorder": "#4caf50",
"nodeTextColor": "#e0e3de",
"edgeLabelBackground": "#111316",
"edgeLabelColor": "#cfe9de",
"fontSize": "28px",
"fontFamily": "JetBrains Mono, Inter, system-ui"
}
}
}%%
graph LR
subgraph :core
:core:data("data")
:core:ui("ui")
:core:domain("domain")
:core:resources("resources")
:core:settings("settings")
:core:di("di")
:core:crash("crash")
:core:utils("utils")
:core:filters("filters")
:core:ksp("ksp")
end
subgraph :feature
:feature:root("root")
:feature:main("main")
:feature:load-net-image("load-net-image")
:feature:crop("crop")
:feature:limits-resize("limits-resize")
:feature:cipher("cipher")
:feature:image-preview("image-preview")
:feature:weight-resize("weight-resize")
:feature:compare("compare")
:feature:delete-exif("delete-exif")
:feature:palette-tools("palette-tools")
:feature:resize-convert("resize-convert")
:feature:pdf-tools("pdf-tools")
:feature:single-edit("single-edit")
:feature:erase-background("erase-background")
:feature:draw("draw")
:feature:filters("filters")
:feature:image-stitch("image-stitch")
:feature:pick-color("pick-color")
:feature:recognize-text("recognize-text")
:feature:gradient-maker("gradient-maker")
:feature:watermarking("watermarking")
:feature:gif-tools("gif-tools")
:feature:apng-tools("apng-tools")
:feature:zip("zip")
:feature:jxl-tools("jxl-tools")
:feature:settings("settings")
:feature:easter-egg("easter-egg")
:feature:svg-maker("svg-maker")
:feature:format-conversion("format-conversion")
:feature:document-scanner("document-scanner")
:feature:scan-qr-code("scan-qr-code")
:feature:image-stacking("image-stacking")
:feature:image-splitting("image-splitting")
:feature:color-tools("color-tools")
:feature:webp-tools("webp-tools")
:feature:noise-generation("noise-generation")
:feature:collage-maker("collage-maker")
:feature:libraries-info("libraries-info")
:feature:markup-layers("markup-layers")
:feature:base64-tools("base64-tools")
:feature:checksum-tools("checksum-tools")
:feature:mesh-gradients("mesh-gradients")
:feature:edit-exif("edit-exif")
:feature:image-cutting("image-cutting")
:feature:audio-cover-extractor("audio-cover-extractor")
:feature:library-details("library-details")
:feature:wallpapers-export("wallpapers-export")
:feature:ascii-art("ascii-art")
:feature:ai-tools("ai-tools")
:feature:media-picker("media-picker")
:feature:quick-tiles("quick-tiles")
end
:feature:root --> :core:data
:feature:root --> :core:ui
:feature:root --> :core:domain
:feature:root --> :core:resources
:feature:root --> :core:settings
:feature:root --> :core:di
:feature:root --> :core:crash
:feature:root --> :feature:main
:feature:root --> :feature:load-net-image
:feature:root --> :feature:crop
:feature:root --> :feature:limits-resize
:feature:root --> :feature:cipher
:feature:root --> :feature:image-preview
:feature:root --> :feature:weight-resize
:feature:root --> :feature:compare
:feature:root --> :feature:delete-exif
:feature:root --> :feature:palette-tools
:feature:root --> :feature:resize-convert
:feature:root --> :feature:pdf-tools
:feature:root --> :feature:single-edit
:feature:root --> :feature:erase-background
:feature:root --> :feature:draw
:feature:root --> :feature:filters
:feature:root --> :feature:image-stitch
:feature:root --> :feature:pick-color
:feature:root --> :feature:recognize-text
:feature:root --> :feature:gradient-maker
:feature:root --> :feature:watermarking
:feature:root --> :feature:gif-tools
:feature:root --> :feature:apng-tools
:feature:root --> :feature:zip
:feature:root --> :feature:jxl-tools
:feature:root --> :feature:settings
:feature:root --> :feature:easter-egg
:feature:root --> :feature:svg-maker
:feature:root --> :feature:format-conversion
:feature:root --> :feature:document-scanner
:feature:root --> :feature:scan-qr-code
:feature:root --> :feature:image-stacking
:feature:root --> :feature:image-splitting
:feature:root --> :feature:color-tools
:feature:root --> :feature:webp-tools
:feature:root --> :feature:noise-generation
:feature:root --> :feature:collage-maker
:feature:root --> :feature:libraries-info
:feature:root --> :feature:markup-layers
:feature:root --> :feature:base64-tools
:feature:root --> :feature:checksum-tools
:feature:root --> :feature:mesh-gradients
:feature:root --> :feature:edit-exif
:feature:root --> :feature:image-cutting
:feature:root --> :feature:audio-cover-extractor
:feature:root --> :feature:library-details
:feature:root --> :feature:wallpapers-export
:feature:root --> :feature:ascii-art
:feature:root --> :feature:ai-tools
:feature:erase-background --> :core:data
:feature:erase-background --> :core:ui
:feature:erase-background --> :core:domain
:feature:erase-background --> :core:resources
:feature:erase-background --> :core:settings
:feature:erase-background --> :core:di
:feature:erase-background --> :core:crash
:feature:erase-background --> :feature:draw
:feature:edit-exif --> :core:data
:feature:edit-exif --> :core:ui
:feature:edit-exif --> :core:domain
:feature:edit-exif --> :core:resources
:feature:edit-exif --> :core:settings
:feature:edit-exif --> :core:di
:feature:edit-exif --> :core:crash
:feature:limits-resize --> :core:data
:feature:limits-resize --> :core:ui
:feature:limits-resize --> :core:domain
:feature:limits-resize --> :core:resources
:feature:limits-resize --> :core:settings
:feature:limits-resize --> :core:di
:feature:limits-resize --> :core:crash
:feature:jxl-tools --> :core:data
:feature:jxl-tools --> :core:ui
:feature:jxl-tools --> :core:domain
:feature:jxl-tools --> :core:resources
:feature:jxl-tools --> :core:settings
:feature:jxl-tools --> :core:di
:feature:jxl-tools --> :core:crash
:feature:libraries-info --> :core:data
:feature:libraries-info --> :core:ui
:feature:libraries-info --> :core:domain
:feature:libraries-info --> :core:resources
:feature:libraries-info --> :core:settings
:feature:libraries-info --> :core:di
:feature:libraries-info --> :core:crash
:feature:gif-tools --> :core:data
:feature:gif-tools --> :core:ui
:feature:gif-tools --> :core:domain
:feature:gif-tools --> :core:resources
:feature:gif-tools --> :core:settings
:feature:gif-tools --> :core:di
:feature:gif-tools --> :core:crash
:feature:library-details --> :core:data
:feature:library-details --> :core:ui
:feature:library-details --> :core:domain
:feature:library-details --> :core:resources
:feature:library-details --> :core:settings
:feature:library-details --> :core:di
:feature:library-details --> :core:crash
:feature:pdf-tools --> :core:data
:feature:pdf-tools --> :core:ui
:feature:pdf-tools --> :core:domain
:feature:pdf-tools --> :core:resources
:feature:pdf-tools --> :core:settings
:feature:pdf-tools --> :core:di
:feature:pdf-tools --> :core:crash
:feature:watermarking --> :core:data
:feature:watermarking --> :core:ui
:feature:watermarking --> :core:domain
:feature:watermarking --> :core:resources
:feature:watermarking --> :core:settings
:feature:watermarking --> :core:di
:feature:watermarking --> :core:crash
:feature:watermarking --> :feature:compare
:app --> :core:data
:app --> :core:ui
:app --> :core:domain
:app --> :core:resources
:app --> :core:settings
:app --> :core:di
:app --> :core:crash
:app --> :core:utils
:app --> :feature:root
:app --> :feature:media-picker
:app --> :feature:quick-tiles
:feature:resize-convert --> :core:data
:feature:resize-convert --> :core:ui
:feature:resize-convert --> :core:domain
:feature:resize-convert --> :core:resources
:feature:resize-convert --> :core:settings
:feature:resize-convert --> :core:di
:feature:resize-convert --> :core:crash
:feature:resize-convert --> :feature:compare
:feature:easter-egg --> :core:data
:feature:easter-egg --> :core:ui
:feature:easter-egg --> :core:domain
:feature:easter-egg --> :core:resources
:feature:easter-egg --> :core:settings
:feature:easter-egg --> :core:di
:feature:easter-egg --> :core:crash
:feature:webp-tools --> :core:data
:feature:webp-tools --> :core:ui
:feature:webp-tools --> :core:domain
:feature:webp-tools --> :core:resources
:feature:webp-tools --> :core:settings
:feature:webp-tools --> :core:di
:feature:webp-tools --> :core:crash
:feature:markup-layers --> :core:data
:feature:markup-layers --> :core:ui
:feature:markup-layers --> :core:domain
:feature:markup-layers --> :core:resources
:feature:markup-layers --> :core:settings
:feature:markup-layers --> :core:di
:feature:markup-layers --> :core:crash
:feature:media-picker --> :core:data
:feature:media-picker --> :core:ui
:feature:media-picker --> :core:domain
:feature:media-picker --> :core:resources
:feature:media-picker --> :core:settings
:feature:media-picker --> :core:di
:feature:media-picker --> :core:crash
:feature:image-stitch --> :core:data
:feature:image-stitch --> :core:ui
:feature:image-stitch --> :core:domain
:feature:image-stitch --> :core:resources
:feature:image-stitch --> :core:settings
:feature:image-stitch --> :core:di
:feature:image-stitch --> :core:crash
:feature:image-stitch --> :core:filters
:core:ui --> :core:resources
:core:ui --> :core:domain
:core:ui --> :core:utils
:core:ui --> :core:di
:core:ui --> :core:settings
:feature:noise-generation --> :core:data
:feature:noise-generation --> :core:ui
:feature:noise-generation --> :core:domain
:feature:noise-generation --> :core:resources
:feature:noise-generation --> :core:settings
:feature:noise-generation --> :core:di
:feature:noise-generation --> :core:crash
:feature:wallpapers-export --> :core:data
:feature:wallpapers-export --> :core:ui
:feature:wallpapers-export --> :core:domain
:feature:wallpapers-export --> :core:resources
:feature:wallpapers-export --> :core:settings
:feature:wallpapers-export --> :core:di
:feature:wallpapers-export --> :core:crash
:feature:document-scanner --> :core:data
:feature:document-scanner --> :core:ui
:feature:document-scanner --> :core:domain
:feature:document-scanner --> :core:resources
:feature:document-scanner --> :core:settings
:feature:document-scanner --> :core:di
:feature:document-scanner --> :core:crash
:feature:document-scanner --> :feature:pdf-tools
:feature:gradient-maker --> :core:data
:feature:gradient-maker --> :core:ui
:feature:gradient-maker --> :core:domain
:feature:gradient-maker --> :core:resources
:feature:gradient-maker --> :core:settings
:feature:gradient-maker --> :core:di
:feature:gradient-maker --> :core:crash
:feature:gradient-maker --> :feature:compare
:feature:zip --> :core:data
:feature:zip --> :core:ui
:feature:zip --> :core:domain
:feature:zip --> :core:resources
:feature:zip --> :core:settings
:feature:zip --> :core:di
:feature:zip --> :core:crash
:core:utils --> :core:domain
:core:utils --> :core:resources
:core:utils --> :core:settings
:feature:cipher --> :core:data
:feature:cipher --> :core:ui
:feature:cipher --> :core:domain
:feature:cipher --> :core:resources
:feature:cipher --> :core:settings
:feature:cipher --> :core:di
:feature:cipher --> :core:crash
:feature:draw --> :core:data
:feature:draw --> :core:ui
:feature:draw --> :core:domain
:feature:draw --> :core:resources
:feature:draw --> :core:settings
:feature:draw --> :core:di
:feature:draw --> :core:crash
:feature:draw --> :core:filters
:feature:draw --> :feature:pick-color
:feature:ai-tools --> :core:data
:feature:ai-tools --> :core:ui
:feature:ai-tools --> :core:domain
:feature:ai-tools --> :core:resources
:feature:ai-tools --> :core:settings
:feature:ai-tools --> :core:di
:feature:ai-tools --> :core:crash
:feature:audio-cover-extractor --> :core:data
:feature:audio-cover-extractor --> :core:ui
:feature:audio-cover-extractor --> :core:domain
:feature:audio-cover-extractor --> :core:resources
:feature:audio-cover-extractor --> :core:settings
:feature:audio-cover-extractor --> :core:di
:feature:audio-cover-extractor --> :core:crash
:core:crash --> :core:ui
:core:crash --> :core:settings
:feature:delete-exif --> :core:data
:feature:delete-exif --> :core:ui
:feature:delete-exif --> :core:domain
:feature:delete-exif --> :core:resources
:feature:delete-exif --> :core:settings
:feature:delete-exif --> :core:di
:feature:delete-exif --> :core:crash
:feature:collage-maker --> :core:data
:feature:collage-maker --> :core:ui
:feature:collage-maker --> :core:domain
:feature:collage-maker --> :core:resources
:feature:collage-maker --> :core:settings
:feature:collage-maker --> :core:di
:feature:collage-maker --> :core:crash
:feature:compare --> :core:data
:feature:compare --> :core:ui
:feature:compare --> :core:domain
:feature:compare --> :core:resources
:feature:compare --> :core:settings
:feature:compare --> :core:di
:feature:compare --> :core:crash
:feature:mesh-gradients --> :core:data
:feature:mesh-gradients --> :core:ui
:feature:mesh-gradients --> :core:domain
:feature:mesh-gradients --> :core:resources
:feature:mesh-gradients --> :core:settings
:feature:mesh-gradients --> :core:di
:feature:mesh-gradients --> :core:crash
:core:settings --> :core:domain
:core:settings --> :core:resources
:core:settings --> :core:di
:feature:scan-qr-code --> :core:data
:feature:scan-qr-code --> :core:ui
:feature:scan-qr-code --> :core:domain
:feature:scan-qr-code --> :core:resources
:feature:scan-qr-code --> :core:settings
:feature:scan-qr-code --> :core:di
:feature:scan-qr-code --> :core:crash
:feature:scan-qr-code --> :core:filters
:feature:svg-maker --> :core:data
:feature:svg-maker --> :core:ui
:feature:svg-maker --> :core:domain
:feature:svg-maker --> :core:resources
:feature:svg-maker --> :core:settings
:feature:svg-maker --> :core:di
:feature:svg-maker --> :core:crash
:feature:weight-resize --> :core:data
:feature:weight-resize --> :core:ui
:feature:weight-resize --> :core:domain
:feature:weight-resize --> :core:resources
:feature:weight-resize --> :core:settings
:feature:weight-resize --> :core:di
:feature:weight-resize --> :core:crash
:feature:image-splitting --> :core:data
:feature:image-splitting --> :core:ui
:feature:image-splitting --> :core:domain
:feature:image-splitting --> :core:resources
:feature:image-splitting --> :core:settings
:feature:image-splitting --> :core:di
:feature:image-splitting --> :core:crash
:benchmark --> :app
:feature:checksum-tools --> :core:data
:feature:checksum-tools --> :core:ui
:feature:checksum-tools --> :core:domain
:feature:checksum-tools --> :core:resources
:feature:checksum-tools --> :core:settings
:feature:checksum-tools --> :core:di
:feature:checksum-tools --> :core:crash
:feature:base64-tools --> :core:data
:feature:base64-tools --> :core:ui
:feature:base64-tools --> :core:domain
:feature:base64-tools --> :core:resources
:feature:base64-tools --> :core:settings
:feature:base64-tools --> :core:di
:feature:base64-tools --> :core:crash
:feature:palette-tools --> :core:data
:feature:palette-tools --> :core:ui
:feature:palette-tools --> :core:domain
:feature:palette-tools --> :core:resources
:feature:palette-tools --> :core:settings
:feature:palette-tools --> :core:di
:feature:palette-tools --> :core:crash
:feature:palette-tools --> :feature:pick-color
:feature:settings --> :core:data
:feature:settings --> :core:ui
:feature:settings --> :core:domain
:feature:settings --> :core:resources
:feature:settings --> :core:settings
:feature:settings --> :core:di
:feature:settings --> :core:crash
:core:data --> :core:utils
:core:data --> :core:domain
:core:data --> :core:resources
:core:data --> :core:filters
:core:data --> :core:settings
:core:data --> :core:di
:feature:pick-color --> :core:data
:feature:pick-color --> :core:ui
:feature:pick-color --> :core:domain
:feature:pick-color --> :core:resources
:feature:pick-color --> :core:settings
:feature:pick-color --> :core:di
:feature:pick-color --> :core:crash
:feature:load-net-image --> :core:data
:feature:load-net-image --> :core:ui
:feature:load-net-image --> :core:domain
:feature:load-net-image --> :core:resources
:feature:load-net-image --> :core:settings
:feature:load-net-image --> :core:di
:feature:load-net-image --> :core:crash
:feature:quick-tiles --> :core:data
:feature:quick-tiles --> :core:ui
:feature:quick-tiles --> :core:domain
:feature:quick-tiles --> :core:resources
:feature:quick-tiles --> :core:settings
:feature:quick-tiles --> :core:di
:feature:quick-tiles --> :core:crash
:feature:quick-tiles --> :feature:erase-background
:feature:recognize-text --> :core:data
:feature:recognize-text --> :core:ui
:feature:recognize-text --> :core:domain
:feature:recognize-text --> :core:resources
:feature:recognize-text --> :core:settings
:feature:recognize-text --> :core:di
:feature:recognize-text --> :core:crash
:feature:recognize-text --> :core:filters
:feature:recognize-text --> :feature:single-edit
:core:domain --> :core:resources
:feature:single-edit --> :core:data
:feature:single-edit --> :core:ui
:feature:single-edit --> :core:domain
:feature:single-edit --> :core:resources
:feature:single-edit --> :core:settings
:feature:single-edit --> :core:di
:feature:single-edit --> :core:crash
:feature:single-edit --> :feature:crop
:feature:single-edit --> :feature:erase-background
:feature:single-edit --> :feature:draw
:feature:single-edit --> :feature:filters
:feature:single-edit --> :feature:pick-color
:feature:single-edit --> :feature:compare
:feature:image-cutting --> :core:data
:feature:image-cutting --> :core:ui
:feature:image-cutting --> :core:domain
:feature:image-cutting --> :core:resources
:feature:image-cutting --> :core:settings
:feature:image-cutting --> :core:di
:feature:image-cutting --> :core:crash
:feature:image-cutting --> :feature:compare
:feature:crop --> :core:data
:feature:crop --> :core:ui
:feature:crop --> :core:domain
:feature:crop --> :core:resources
:feature:crop --> :core:settings
:feature:crop --> :core:di
:feature:crop --> :core:crash
:feature:color-tools --> :core:data
:feature:color-tools --> :core:ui
:feature:color-tools --> :core:domain
:feature:color-tools --> :core:resources
:feature:color-tools --> :core:settings
:feature:color-tools --> :core:di
:feature:color-tools --> :core:crash
:feature:format-conversion --> :core:data
:feature:format-conversion --> :core:ui
:feature:format-conversion --> :core:domain
:feature:format-conversion --> :core:resources
:feature:format-conversion --> :core:settings
:feature:format-conversion --> :core:di
:feature:format-conversion --> :core:crash
:feature:format-conversion --> :feature:compare
:feature:ascii-art --> :core:data
:feature:ascii-art --> :core:ui
:feature:ascii-art --> :core:domain
:feature:ascii-art --> :core:resources
:feature:ascii-art --> :core:settings
:feature:ascii-art --> :core:di
:feature:ascii-art --> :core:crash
:feature:ascii-art --> :feature:filters
:core:filters --> :core:domain
:core:filters --> :core:ui
:core:filters --> :core:resources
:core:filters --> :core:settings
:core:filters --> :core:utils
:feature:apng-tools --> :core:data
:feature:apng-tools --> :core:ui
:feature:apng-tools --> :core:domain
:feature:apng-tools --> :core:resources
:feature:apng-tools --> :core:settings
:feature:apng-tools --> :core:di
:feature:apng-tools --> :core:crash
:feature:image-preview --> :core:data
:feature:image-preview --> :core:ui
:feature:image-preview --> :core:domain
:feature:image-preview --> :core:resources
:feature:image-preview --> :core:settings
:feature:image-preview --> :core:di
:feature:image-preview --> :core:crash
:feature:filters --> :core:filters
:feature:filters --> :core:data
:feature:filters --> :core:ui
:feature:filters --> :core:domain
:feature:filters --> :core:resources
:feature:filters --> :core:settings
:feature:filters --> :core:di
:feature:filters --> :core:crash
:feature:filters --> :core:ksp
:feature:filters --> :feature:draw
:feature:filters --> :feature:pick-color
:feature:filters --> :feature:compare
:feature:image-stacking --> :core:data
:feature:image-stacking --> :core:ui
:feature:image-stacking --> :core:domain
:feature:image-stacking --> :core:resources
:feature:image-stacking --> :core:settings
:feature:image-stacking --> :core:di
:feature:image-stacking --> :core:crash
:feature:main --> :core:data
:feature:main --> :core:ui
:feature:main --> :core:domain
:feature:main --> :core:resources
:feature:main --> :core:settings
:feature:main --> :core:di
:feature:main --> :core:crash
:feature:main --> :feature:settings
```
+612
View File
@@ -0,0 +1,612 @@
%%{
init: {
"theme": "base",
"maxEdges": 1000,
"themeVariables": {
"mainBkg": "#121418",
"primaryColor": "#1b3a1b",
"primaryTextColor": "#e0e3de",
"primaryBorderColor": "#76c893",
"nodeBorder": "#76c893",
"lineColor": "#76c893",
"secondaryColor": "#1f2721",
"tertiaryColor": "#232c26",
"clusterBkg": "#182018",
"clusterBorder": "#4caf50",
"nodeTextColor": "#e0e3de",
"edgeLabelBackground": "#111316",
"edgeLabelColor": "#cfe9de",
"fontSize": "28px",
"fontFamily": "JetBrains Mono, Inter, system-ui"
}
}
}%%
graph LR
subgraph :core
:core:data("data")
:core:ui("ui")
:core:domain("domain")
:core:resources("resources")
:core:settings("settings")
:core:di("di")
:core:crash("crash")
:core:utils("utils")
:core:filters("filters")
:core:ksp("ksp")
end
subgraph :feature
:feature:root("root")
:feature:main("main")
:feature:load-net-image("load-net-image")
:feature:crop("crop")
:feature:limits-resize("limits-resize")
:feature:cipher("cipher")
:feature:image-preview("image-preview")
:feature:weight-resize("weight-resize")
:feature:compare("compare")
:feature:delete-exif("delete-exif")
:feature:palette-tools("palette-tools")
:feature:resize-convert("resize-convert")
:feature:pdf-tools("pdf-tools")
:feature:single-edit("single-edit")
:feature:erase-background("erase-background")
:feature:draw("draw")
:feature:filters("filters")
:feature:image-stitch("image-stitch")
:feature:pick-color("pick-color")
:feature:recognize-text("recognize-text")
:feature:gradient-maker("gradient-maker")
:feature:watermarking("watermarking")
:feature:gif-tools("gif-tools")
:feature:apng-tools("apng-tools")
:feature:zip("zip")
:feature:jxl-tools("jxl-tools")
:feature:settings("settings")
:feature:easter-egg("easter-egg")
:feature:svg-maker("svg-maker")
:feature:format-conversion("format-conversion")
:feature:document-scanner("document-scanner")
:feature:scan-qr-code("scan-qr-code")
:feature:image-stacking("image-stacking")
:feature:image-splitting("image-splitting")
:feature:color-tools("color-tools")
:feature:webp-tools("webp-tools")
:feature:noise-generation("noise-generation")
:feature:collage-maker("collage-maker")
:feature:libraries-info("libraries-info")
:feature:markup-layers("markup-layers")
:feature:base64-tools("base64-tools")
:feature:checksum-tools("checksum-tools")
:feature:mesh-gradients("mesh-gradients")
:feature:edit-exif("edit-exif")
:feature:image-cutting("image-cutting")
:feature:audio-cover-extractor("audio-cover-extractor")
:feature:library-details("library-details")
:feature:wallpapers-export("wallpapers-export")
:feature:ascii-art("ascii-art")
:feature:ai-tools("ai-tools")
:feature:color-library("color-library")
:feature:media-picker("media-picker")
:feature:quick-tiles("quick-tiles")
end
subgraph :lib
:lib:neural-tools("neural-tools")
:lib:opencv-tools("opencv-tools")
:lib:dynamic-theme("dynamic-theme")
:lib:snowfall("snowfall")
:lib:documentscanner("documentscanner")
:lib:collages("collages")
:lib:palette("palette")
:lib:curves("curves")
:lib:ascii("ascii")
end
:feature:root --> :core:data
:feature:root --> :core:ui
:feature:root --> :core:domain
:feature:root --> :core:resources
:feature:root --> :core:settings
:feature:root --> :core:di
:feature:root --> :core:crash
:feature:root --> :feature:main
:feature:root --> :feature:load-net-image
:feature:root --> :feature:crop
:feature:root --> :feature:limits-resize
:feature:root --> :feature:cipher
:feature:root --> :feature:image-preview
:feature:root --> :feature:weight-resize
:feature:root --> :feature:compare
:feature:root --> :feature:delete-exif
:feature:root --> :feature:palette-tools
:feature:root --> :feature:resize-convert
:feature:root --> :feature:pdf-tools
:feature:root --> :feature:single-edit
:feature:root --> :feature:erase-background
:feature:root --> :feature:draw
:feature:root --> :feature:filters
:feature:root --> :feature:image-stitch
:feature:root --> :feature:pick-color
:feature:root --> :feature:recognize-text
:feature:root --> :feature:gradient-maker
:feature:root --> :feature:watermarking
:feature:root --> :feature:gif-tools
:feature:root --> :feature:apng-tools
:feature:root --> :feature:zip
:feature:root --> :feature:jxl-tools
:feature:root --> :feature:settings
:feature:root --> :feature:easter-egg
:feature:root --> :feature:svg-maker
:feature:root --> :feature:format-conversion
:feature:root --> :feature:document-scanner
:feature:root --> :feature:scan-qr-code
:feature:root --> :feature:image-stacking
:feature:root --> :feature:image-splitting
:feature:root --> :feature:color-tools
:feature:root --> :feature:webp-tools
:feature:root --> :feature:noise-generation
:feature:root --> :feature:collage-maker
:feature:root --> :feature:libraries-info
:feature:root --> :feature:markup-layers
:feature:root --> :feature:base64-tools
:feature:root --> :feature:checksum-tools
:feature:root --> :feature:mesh-gradients
:feature:root --> :feature:edit-exif
:feature:root --> :feature:image-cutting
:feature:root --> :feature:audio-cover-extractor
:feature:root --> :feature:library-details
:feature:root --> :feature:wallpapers-export
:feature:root --> :feature:ascii-art
:feature:root --> :feature:ai-tools
:feature:root --> :feature:color-library
:feature:erase-background --> :core:data
:feature:erase-background --> :core:ui
:feature:erase-background --> :core:domain
:feature:erase-background --> :core:resources
:feature:erase-background --> :core:settings
:feature:erase-background --> :core:di
:feature:erase-background --> :core:crash
:feature:erase-background --> :lib:neural-tools
:feature:erase-background --> :feature:draw
:feature:edit-exif --> :core:data
:feature:edit-exif --> :core:ui
:feature:edit-exif --> :core:domain
:feature:edit-exif --> :core:resources
:feature:edit-exif --> :core:settings
:feature:edit-exif --> :core:di
:feature:edit-exif --> :core:crash
:feature:limits-resize --> :core:data
:feature:limits-resize --> :core:ui
:feature:limits-resize --> :core:domain
:feature:limits-resize --> :core:resources
:feature:limits-resize --> :core:settings
:feature:limits-resize --> :core:di
:feature:limits-resize --> :core:crash
:feature:jxl-tools --> :core:data
:feature:jxl-tools --> :core:ui
:feature:jxl-tools --> :core:domain
:feature:jxl-tools --> :core:resources
:feature:jxl-tools --> :core:settings
:feature:jxl-tools --> :core:di
:feature:jxl-tools --> :core:crash
:feature:libraries-info --> :core:data
:feature:libraries-info --> :core:ui
:feature:libraries-info --> :core:domain
:feature:libraries-info --> :core:resources
:feature:libraries-info --> :core:settings
:feature:libraries-info --> :core:di
:feature:libraries-info --> :core:crash
:feature:gif-tools --> :core:data
:feature:gif-tools --> :core:ui
:feature:gif-tools --> :core:domain
:feature:gif-tools --> :core:resources
:feature:gif-tools --> :core:settings
:feature:gif-tools --> :core:di
:feature:gif-tools --> :core:crash
:feature:library-details --> :core:data
:feature:library-details --> :core:ui
:feature:library-details --> :core:domain
:feature:library-details --> :core:resources
:feature:library-details --> :core:settings
:feature:library-details --> :core:di
:feature:library-details --> :core:crash
:feature:pdf-tools --> :core:data
:feature:pdf-tools --> :core:ui
:feature:pdf-tools --> :core:domain
:feature:pdf-tools --> :core:resources
:feature:pdf-tools --> :core:settings
:feature:pdf-tools --> :core:di
:feature:pdf-tools --> :core:crash
:feature:watermarking --> :core:data
:feature:watermarking --> :core:ui
:feature:watermarking --> :core:domain
:feature:watermarking --> :core:resources
:feature:watermarking --> :core:settings
:feature:watermarking --> :core:di
:feature:watermarking --> :core:crash
:feature:watermarking --> :feature:compare
:app --> :core:data
:app --> :core:ui
:app --> :core:domain
:app --> :core:resources
:app --> :core:settings
:app --> :core:di
:app --> :core:crash
:app --> :core:utils
:app --> :feature:root
:app --> :feature:media-picker
:app --> :feature:quick-tiles
:app --> :lib:opencv-tools
:app --> :lib:neural-tools
:feature:resize-convert --> :core:data
:feature:resize-convert --> :core:ui
:feature:resize-convert --> :core:domain
:feature:resize-convert --> :core:resources
:feature:resize-convert --> :core:settings
:feature:resize-convert --> :core:di
:feature:resize-convert --> :core:crash
:feature:resize-convert --> :feature:compare
:feature:easter-egg --> :core:data
:feature:easter-egg --> :core:ui
:feature:easter-egg --> :core:domain
:feature:easter-egg --> :core:resources
:feature:easter-egg --> :core:settings
:feature:easter-egg --> :core:di
:feature:easter-egg --> :core:crash
:feature:webp-tools --> :core:data
:feature:webp-tools --> :core:ui
:feature:webp-tools --> :core:domain
:feature:webp-tools --> :core:resources
:feature:webp-tools --> :core:settings
:feature:webp-tools --> :core:di
:feature:webp-tools --> :core:crash
:feature:markup-layers --> :core:data
:feature:markup-layers --> :core:ui
:feature:markup-layers --> :core:domain
:feature:markup-layers --> :core:resources
:feature:markup-layers --> :core:settings
:feature:markup-layers --> :core:di
:feature:markup-layers --> :core:crash
:feature:media-picker --> :core:data
:feature:media-picker --> :core:ui
:feature:media-picker --> :core:domain
:feature:media-picker --> :core:resources
:feature:media-picker --> :core:settings
:feature:media-picker --> :core:di
:feature:media-picker --> :core:crash
:feature:image-stitch --> :core:data
:feature:image-stitch --> :core:ui
:feature:image-stitch --> :core:domain
:feature:image-stitch --> :core:resources
:feature:image-stitch --> :core:settings
:feature:image-stitch --> :core:di
:feature:image-stitch --> :core:crash
:feature:image-stitch --> :core:filters
:feature:image-stitch --> :lib:opencv-tools
:core:ui --> :core:resources
:core:ui --> :core:domain
:core:ui --> :core:utils
:core:ui --> :lib:dynamic-theme
:core:ui --> :lib:snowfall
:core:ui --> :core:di
:core:ui --> :core:settings
:core:ui --> :lib:documentscanner
:feature:color-library --> :core:data
:feature:color-library --> :core:ui
:feature:color-library --> :core:domain
:feature:color-library --> :core:resources
:feature:color-library --> :core:settings
:feature:color-library --> :core:di
:feature:color-library --> :core:crash
:feature:noise-generation --> :core:data
:feature:noise-generation --> :core:ui
:feature:noise-generation --> :core:domain
:feature:noise-generation --> :core:resources
:feature:noise-generation --> :core:settings
:feature:noise-generation --> :core:di
:feature:noise-generation --> :core:crash
:feature:wallpapers-export --> :core:data
:feature:wallpapers-export --> :core:ui
:feature:wallpapers-export --> :core:domain
:feature:wallpapers-export --> :core:resources
:feature:wallpapers-export --> :core:settings
:feature:wallpapers-export --> :core:di
:feature:wallpapers-export --> :core:crash
:feature:document-scanner --> :core:data
:feature:document-scanner --> :core:ui
:feature:document-scanner --> :core:domain
:feature:document-scanner --> :core:resources
:feature:document-scanner --> :core:settings
:feature:document-scanner --> :core:di
:feature:document-scanner --> :core:crash
:feature:document-scanner --> :feature:pdf-tools
:feature:gradient-maker --> :core:data
:feature:gradient-maker --> :core:ui
:feature:gradient-maker --> :core:domain
:feature:gradient-maker --> :core:resources
:feature:gradient-maker --> :core:settings
:feature:gradient-maker --> :core:di
:feature:gradient-maker --> :core:crash
:feature:gradient-maker --> :feature:compare
:feature:zip --> :core:data
:feature:zip --> :core:ui
:feature:zip --> :core:domain
:feature:zip --> :core:resources
:feature:zip --> :core:settings
:feature:zip --> :core:di
:feature:zip --> :core:crash
:core:utils --> :core:domain
:core:utils --> :core:resources
:core:utils --> :core:settings
:feature:cipher --> :core:data
:feature:cipher --> :core:ui
:feature:cipher --> :core:domain
:feature:cipher --> :core:resources
:feature:cipher --> :core:settings
:feature:cipher --> :core:di
:feature:cipher --> :core:crash
:feature:draw --> :core:data
:feature:draw --> :core:ui
:feature:draw --> :core:domain
:feature:draw --> :core:resources
:feature:draw --> :core:settings
:feature:draw --> :core:di
:feature:draw --> :core:crash
:feature:draw --> :core:filters
:feature:draw --> :feature:pick-color
:feature:ai-tools --> :core:data
:feature:ai-tools --> :core:ui
:feature:ai-tools --> :core:domain
:feature:ai-tools --> :core:resources
:feature:ai-tools --> :core:settings
:feature:ai-tools --> :core:di
:feature:ai-tools --> :core:crash
:feature:ai-tools --> :lib:neural-tools
:feature:audio-cover-extractor --> :core:data
:feature:audio-cover-extractor --> :core:ui
:feature:audio-cover-extractor --> :core:domain
:feature:audio-cover-extractor --> :core:resources
:feature:audio-cover-extractor --> :core:settings
:feature:audio-cover-extractor --> :core:di
:feature:audio-cover-extractor --> :core:crash
:core:crash --> :core:ui
:core:crash --> :core:settings
:lib:documentscanner --> :lib:opencv-tools
:feature:delete-exif --> :core:data
:feature:delete-exif --> :core:ui
:feature:delete-exif --> :core:domain
:feature:delete-exif --> :core:resources
:feature:delete-exif --> :core:settings
:feature:delete-exif --> :core:di
:feature:delete-exif --> :core:crash
:feature:collage-maker --> :core:data
:feature:collage-maker --> :core:ui
:feature:collage-maker --> :core:domain
:feature:collage-maker --> :core:resources
:feature:collage-maker --> :core:settings
:feature:collage-maker --> :core:di
:feature:collage-maker --> :core:crash
:feature:collage-maker --> :lib:collages
:feature:compare --> :core:data
:feature:compare --> :core:ui
:feature:compare --> :core:domain
:feature:compare --> :core:resources
:feature:compare --> :core:settings
:feature:compare --> :core:di
:feature:compare --> :core:crash
:feature:compare --> :lib:opencv-tools
:feature:mesh-gradients --> :core:data
:feature:mesh-gradients --> :core:ui
:feature:mesh-gradients --> :core:domain
:feature:mesh-gradients --> :core:resources
:feature:mesh-gradients --> :core:settings
:feature:mesh-gradients --> :core:di
:feature:mesh-gradients --> :core:crash
:core:settings --> :lib:dynamic-theme
:core:settings --> :core:domain
:core:settings --> :core:resources
:core:settings --> :core:di
:feature:scan-qr-code --> :core:data
:feature:scan-qr-code --> :core:ui
:feature:scan-qr-code --> :core:domain
:feature:scan-qr-code --> :core:resources
:feature:scan-qr-code --> :core:settings
:feature:scan-qr-code --> :core:di
:feature:scan-qr-code --> :core:crash
:feature:scan-qr-code --> :core:filters
:feature:svg-maker --> :core:data
:feature:svg-maker --> :core:ui
:feature:svg-maker --> :core:domain
:feature:svg-maker --> :core:resources
:feature:svg-maker --> :core:settings
:feature:svg-maker --> :core:di
:feature:svg-maker --> :core:crash
:feature:weight-resize --> :core:data
:feature:weight-resize --> :core:ui
:feature:weight-resize --> :core:domain
:feature:weight-resize --> :core:resources
:feature:weight-resize --> :core:settings
:feature:weight-resize --> :core:di
:feature:weight-resize --> :core:crash
:feature:image-splitting --> :core:data
:feature:image-splitting --> :core:ui
:feature:image-splitting --> :core:domain
:feature:image-splitting --> :core:resources
:feature:image-splitting --> :core:settings
:feature:image-splitting --> :core:di
:feature:image-splitting --> :core:crash
:benchmark --> :app
:feature:checksum-tools --> :core:data
:feature:checksum-tools --> :core:ui
:feature:checksum-tools --> :core:domain
:feature:checksum-tools --> :core:resources
:feature:checksum-tools --> :core:settings
:feature:checksum-tools --> :core:di
:feature:checksum-tools --> :core:crash
:feature:base64-tools --> :core:data
:feature:base64-tools --> :core:ui
:feature:base64-tools --> :core:domain
:feature:base64-tools --> :core:resources
:feature:base64-tools --> :core:settings
:feature:base64-tools --> :core:di
:feature:base64-tools --> :core:crash
:feature:palette-tools --> :core:data
:feature:palette-tools --> :core:ui
:feature:palette-tools --> :core:domain
:feature:palette-tools --> :core:resources
:feature:palette-tools --> :core:settings
:feature:palette-tools --> :core:di
:feature:palette-tools --> :core:crash
:feature:palette-tools --> :feature:pick-color
:feature:palette-tools --> :lib:palette
:feature:settings --> :core:data
:feature:settings --> :core:ui
:feature:settings --> :core:domain
:feature:settings --> :core:resources
:feature:settings --> :core:settings
:feature:settings --> :core:di
:feature:settings --> :core:crash
:core:data --> :core:utils
:core:data --> :core:domain
:core:data --> :core:resources
:core:data --> :core:filters
:core:data --> :core:settings
:core:data --> :core:di
:feature:pick-color --> :core:data
:feature:pick-color --> :core:ui
:feature:pick-color --> :core:domain
:feature:pick-color --> :core:resources
:feature:pick-color --> :core:settings
:feature:pick-color --> :core:di
:feature:pick-color --> :core:crash
:feature:load-net-image --> :core:data
:feature:load-net-image --> :core:ui
:feature:load-net-image --> :core:domain
:feature:load-net-image --> :core:resources
:feature:load-net-image --> :core:settings
:feature:load-net-image --> :core:di
:feature:load-net-image --> :core:crash
:feature:quick-tiles --> :core:data
:feature:quick-tiles --> :core:ui
:feature:quick-tiles --> :core:domain
:feature:quick-tiles --> :core:resources
:feature:quick-tiles --> :core:settings
:feature:quick-tiles --> :core:di
:feature:quick-tiles --> :core:crash
:feature:quick-tiles --> :feature:erase-background
:feature:recognize-text --> :core:data
:feature:recognize-text --> :core:ui
:feature:recognize-text --> :core:domain
:feature:recognize-text --> :core:resources
:feature:recognize-text --> :core:settings
:feature:recognize-text --> :core:di
:feature:recognize-text --> :core:crash
:feature:recognize-text --> :core:filters
:feature:recognize-text --> :feature:single-edit
:core:domain --> :core:resources
:feature:single-edit --> :core:data
:feature:single-edit --> :core:ui
:feature:single-edit --> :core:domain
:feature:single-edit --> :core:resources
:feature:single-edit --> :core:settings
:feature:single-edit --> :core:di
:feature:single-edit --> :core:crash
:feature:single-edit --> :feature:crop
:feature:single-edit --> :feature:erase-background
:feature:single-edit --> :feature:draw
:feature:single-edit --> :feature:filters
:feature:single-edit --> :feature:pick-color
:feature:single-edit --> :feature:compare
:feature:single-edit --> :lib:curves
:feature:image-cutting --> :core:data
:feature:image-cutting --> :core:ui
:feature:image-cutting --> :core:domain
:feature:image-cutting --> :core:resources
:feature:image-cutting --> :core:settings
:feature:image-cutting --> :core:di
:feature:image-cutting --> :core:crash
:feature:image-cutting --> :feature:compare
:feature:crop --> :core:data
:feature:crop --> :core:ui
:feature:crop --> :core:domain
:feature:crop --> :core:resources
:feature:crop --> :core:settings
:feature:crop --> :core:di
:feature:crop --> :core:crash
:feature:crop --> :lib:opencv-tools
:feature:color-tools --> :core:data
:feature:color-tools --> :core:ui
:feature:color-tools --> :core:domain
:feature:color-tools --> :core:resources
:feature:color-tools --> :core:settings
:feature:color-tools --> :core:di
:feature:color-tools --> :core:crash
:feature:format-conversion --> :core:data
:feature:format-conversion --> :core:ui
:feature:format-conversion --> :core:domain
:feature:format-conversion --> :core:resources
:feature:format-conversion --> :core:settings
:feature:format-conversion --> :core:di
:feature:format-conversion --> :core:crash
:feature:format-conversion --> :feature:compare
:feature:ascii-art --> :core:data
:feature:ascii-art --> :core:ui
:feature:ascii-art --> :core:domain
:feature:ascii-art --> :core:resources
:feature:ascii-art --> :core:settings
:feature:ascii-art --> :core:di
:feature:ascii-art --> :core:crash
:feature:ascii-art --> :feature:filters
:feature:ascii-art --> :lib:ascii
:core:filters --> :core:domain
:core:filters --> :core:ui
:core:filters --> :core:resources
:core:filters --> :core:settings
:core:filters --> :core:utils
:core:filters --> :lib:curves
:core:filters --> :lib:ascii
:core:filters --> :lib:neural-tools
:feature:apng-tools --> :core:data
:feature:apng-tools --> :core:ui
:feature:apng-tools --> :core:domain
:feature:apng-tools --> :core:resources
:feature:apng-tools --> :core:settings
:feature:apng-tools --> :core:di
:feature:apng-tools --> :core:crash
:feature:image-preview --> :core:data
:feature:image-preview --> :core:ui
:feature:image-preview --> :core:domain
:feature:image-preview --> :core:resources
:feature:image-preview --> :core:settings
:feature:image-preview --> :core:di
:feature:image-preview --> :core:crash
:feature:filters --> :core:filters
:feature:filters --> :core:data
:feature:filters --> :core:ui
:feature:filters --> :core:domain
:feature:filters --> :core:resources
:feature:filters --> :core:settings
:feature:filters --> :core:di
:feature:filters --> :core:crash
:feature:filters --> :core:ksp
:feature:filters --> :feature:draw
:feature:filters --> :feature:pick-color
:feature:filters --> :feature:compare
:feature:filters --> :lib:opencv-tools
:feature:filters --> :lib:neural-tools
:feature:filters --> :lib:curves
:feature:filters --> :lib:ascii
:feature:image-stacking --> :core:data
:feature:image-stacking --> :core:ui
:feature:image-stacking --> :core:domain
:feature:image-stacking --> :core:resources
:feature:image-stacking --> :core:settings
:feature:image-stacking --> :core:di
:feature:image-stacking --> :core:crash
:feature:main --> :core:data
:feature:main --> :core:ui
:feature:main --> :core:domain
:feature:main --> :core:resources
:feature:main --> :core:settings
:feature:main --> :core:di
:feature:main --> :core:crash
:feature:main --> :feature:settings
+89
View File
@@ -0,0 +1,89 @@
# Contributing Guidelines
This documentation contains set of guidelines to help you during the contribution process.
# Submitting Contributions👨🏻‍💻
Below you will find the process and workflow used to review and merge your changes.
## 🌟 : Choose an issue/ Create an issue
- Look for the existing issue or create your own issue.
- Comment on the respective issue you would like to work before creating a Pull Request.
- Wait for the issue to be assigned to you after which you can start working on it.
## 🌟 : Fork the repository
- Fork this repository "ImageToolbox" by clicking on the "Fork" button. This will create a local copy of this respository on your GitHub profile.
## 🌟 : Clone the forked repository
- Once the repository is forked you need to clone it to your local machine.
- Click on the "Code" button in the repository page and copy the link provided in the dropdown menu.
```bash
git clone https://github.com/<your-username>/<ImageToolbox>
```
- Keep a reference to the original project in `upstream` remote.
```bash
cd <repo-name>
git remote add upstream https://github.com/<upstream-owner>/<ImageToolbox>
git remote -v # To the check the remotes for this repository
```
- If the project is forked already, update the copy before working.
```bash
git remote update
git checkout <branch-name>
git rebase upstream/<branch-name>
```
## 🌟 : Create a new branch
- Always create a new branch and name it accordingly so as to identify the issue you are addressing.
```bash
# It will create a new branch with name branch_name and switch to that branch
git checkout -b branch_name
```
## 🌟 : Work on the issue assigned
- Work on the issue(s) assigned to you, make the necessary changes in the files/folders needed.
- After making the changes add them to the branch you've created.
```bash
# To add all new files to branch Branch_Name
git add .
# To add only a few files to Branch_Name
git add <file name>
```
## 🌟 : Commit the changes
- Add your commits.
- Along with the commit give a descriptive message that reflects your changes.
```bash
git commit -m "message"
```
- Note : A Pull Request should always have only one commit.
## 🌟 : Push the changes
- Push the committed changes in your branch to your remote repository.
```bash
git push origin branch_name
```
## 🌟 : Create a Pull Request
- Go to your repository in the browser and click on compare and pull request.
- Add a title and description to your pull request that best describes your contribution.
- After which the pull request will be reviewed and the maintainer will provide the reviews required for the changes.
If no changes are needed, this means that your Pull Request has been reviewed and will be merged to the original code base by the maintainer.
Happy Hacking!
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+1229
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`T8RIN/ImageToolbox`
- 原始仓库:https://github.com/T8RIN/ImageToolbox
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+10
View File
@@ -0,0 +1,10 @@
/build/
/release/
/foss/
/market/
/jxl/
/build
/release
/foss
/market
/jxl
+202
View File
@@ -0,0 +1,202 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
@file:Suppress("UnstableApiUsage")
plugins {
alias(libs.plugins.image.toolbox.application)
alias(libs.plugins.image.toolbox.hilt)
}
android {
val supportedAbi = arrayOf("armeabi-v7a", "arm64-v8a", "x86_64")
namespace = "com.t8rin.imagetoolbox"
defaultConfig {
vectorDrawables.useSupportLibrary = true
//Maintained for compatibility with old version
applicationId = "ru.tech.imageresizershrinker"
versionCode = libs.versions.versionCode.get().toIntOrNull()
versionName = System.getenv("VERSION_NAME") ?: libs.versions.versionName.get()
ndk {
abiFilters.clear()
//noinspection ChromeOsAbiSupport
abiFilters += supportedAbi.toSet()
}
}
androidResources {
generateLocaleConfig = true
}
flavorDimensions += "app"
productFlavors {
create("foss") {
dimension = "app"
versionNameSuffix = "-foss"
extra.set("gmsEnabled", false)
}
create("market") {
dimension = "app"
extra.set("gmsEnabled", true)
}
}
buildTypes {
debug {
applicationIdSuffix = ".debug"
resValue("string", "app_launcher_name", "Image Toolbox DEBUG")
resValue("string", "file_provider", "com.t8rin.imagetoolbox.fileprovider.debug")
}
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
resValue("string", "app_launcher_name", "Image Toolbox")
resValue("string", "file_provider", "com.t8rin.imagetoolbox.fileprovider")
}
create("benchmark") {
initWith(buildTypes.getByName("release"))
signingConfig = signingConfigs.getByName("debug")
matchingFallbacks += listOf("release")
}
}
splits {
abi {
// Detect app bundle and conditionally disable split abis
// This is needed due to a "Sequence contains more than one matching element" error
// present since AGP 8.9.0, for more info see:
// https://issuetracker.google.com/issues/402800800
// AppBundle tasks usually contain "bundle" in their name
//noinspection WrongGradleMethod
val isBuildingBundle =
gradle.startParameter.taskNames.any { it.lowercase().contains("bundle") }
// Disable split abis when building appBundle
isEnable = !isBuildingBundle
reset()
//noinspection ChromeOsAbiSupport
include(*supportedAbi)
isUniversalApk = true
}
}
lint {
disable += "Instantiatable"
}
packaging {
jniLibs {
keepDebugSymbols.add("**/*.so")
pickFirsts.add("lib/*/libcoder.so")
pickFirsts.add("**/libc++_shared.so")
pickFirsts.add("**/libdatstore_shared_counter.so")
useLegacyPackaging = true
}
resources {
excludes += "META-INF/"
excludes += "kotlin/"
excludes += "org/"
excludes += ".properties"
excludes += ".bin"
excludes += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
}
}
buildFeatures {
resValues = true
}
}
base {
archivesName = "image-toolbox-${android.defaultConfig.versionName}"
}
aboutLibraries {
export.excludeFields.addAll("generated")
}
dependencies {
implementation(projects.feature.root)
implementation(projects.feature.mediaPicker)
implementation(projects.feature.quickTiles)
implementation(projects.lib.opencvTools)
implementation(projects.lib.neuralTools)
implementation(projects.lib.collages)
implementation(libs.bouncycastle.pkix)
implementation(libs.bouncycastle.provider)
implementation(libs.pdfbox)
"marketImplementation"(libs.quickie.bundled)
"fossImplementation"(libs.quickie.foss)
}
dependencySubstitution {
substitute(
dependency = "com.caverock:androidsvg-aar:1.4",
using = "com.github.deckerst:androidsvg:cc9d59a88f"
)
}
androidComponents {
beforeVariants(selector().all()) { variantBuilder ->
val flavorName = variantBuilder.productFlavors.firstOrNull()?.second.orEmpty()
val flavorCap = flavorName.replaceFirstChar(Char::uppercase)
val gmsEnabled = android.productFlavors
.findByName(flavorName)
?.extra
?.get("gmsEnabled") == true
tasks.configureEach {
val isTargetTask = listOf("GoogleServices", "Crashlytics").any { marker ->
name.contains(marker)
} && name.contains(flavorCap)
if (isTargetTask) {
enabled = gmsEnabled
}
}
}
}
fun Project.dependencySubstitution(action: DependencySubstitutions.() -> Unit) {
allprojects {
configurations.all {
resolutionStrategy.dependencySubstitution(action)
}
}
}
fun DependencySubstitutions.substitute(
dependency: String,
using: String
) {
substitute(module(dependency)).using(module(using))
}
+135
View File
@@ -0,0 +1,135 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
-keep class * implements com.t8rin.imagetoolbox.core.filters.presentation.model.UiFilter
-keepclassmembers class * implements com.t8rin.imagetoolbox.core.filters.presentation.model.UiFilter {
<init>(...);
}
-keep class * implements com.t8rin.imagetoolbox.core.filters.domain.model.Filter
-keepclassmembers class * implements com.t8rin.imagetoolbox.core.filters.domain.model.Filter {
<init>(...);
}
-keepclassmembers class com.t8rin.imagetoolbox.core.filters.** {
<init>(...);
}
-keep class com.t8rin.imagetoolbox.core.filters.**
-keep class com.t8rin.imagetoolbox.core.filters.*
# Please add these rules to your existing keep rules in order to suppress warnings.
# This is generated automatically by the Android Gradle plugin.
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.conscrypt.ConscryptHostnameVerifier
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE
-keep class org.beyka.tiffbitmapfactory.**{ *; }
-keep class org.bouncycastle.jcajce.provider.** { *; }
-keep class org.bouncycastle.jce.provider.** { *; }
-dontwarn com.google.re2j.Matcher
-dontwarn com.google.re2j.Pattern
-keepclassmembers class * {
public java.lang.String name();
}
-keep enum * { *; }
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * extends java.lang.Enum {
public java.lang.String name();
}
-keep class ai.onnxruntime.** { *; }
-keep class com.google.firebase.crashlytics.** { *; }
-keep class com.google.firebase.analytics.** { *; }
-keep class androidx.pdf.** { *; }
-keepnames class androidx.pdf.** { *; }
# Moshi reflective adapters need generic signatures in release.
-keepattributes Signature
-keepattributes *Annotation*
-keep class kotlin.Metadata { *; }
-keep class com.t8rin.imagetoolbox.core.filters.domain.model.shader.** { *; }
-keep class com.t8rin.imagetoolbox.core.filters.domain.model.shader.**
-keep class com.t8rin.imagetoolbox.core.filters.domain.model.shader.*
-keep class com.t8rin.imagetoolbox.feature.markup_layers.data.project.** { *; }
-keep class com.t8rin.imagetoolbox.feature.markup_layers.data.project.**
-keep class com.t8rin.imagetoolbox.feature.markup_layers.data.project.*
# Moshi reflects image export preset models when saving/loading presets.
-keep class com.t8rin.imagetoolbox.core.domain.image.model.ImageExportProfiles { *; }
-keep class com.t8rin.imagetoolbox.core.domain.image.model.ImageExportProfile { *; }
-keep class com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo { *; }
-keep class com.t8rin.imagetoolbox.core.domain.image.model.Preset { *; }
-keep class com.t8rin.imagetoolbox.core.domain.image.model.Preset$* { *; }
-keep class com.t8rin.imagetoolbox.core.domain.image.model.ResizeType { *; }
-keep class com.t8rin.imagetoolbox.core.domain.image.model.ResizeType$* { *; }
-keep class com.t8rin.imagetoolbox.core.domain.image.model.Quality { *; }
-keep class com.t8rin.imagetoolbox.core.domain.image.model.Quality$* { *; }
-keep class com.t8rin.imagetoolbox.core.domain.model.IntegerSize { *; }
-keep class com.t8rin.imagetoolbox.core.data.json.PresetJson { *; }
-keep class com.t8rin.imagetoolbox.core.data.json.ResizeTypeJson { *; }
-keep class com.t8rin.imagetoolbox.core.data.json.ImageScaleModeJson { *; }
# Moshi reflects app history/statistics models for recent tools.
-keep class com.t8rin.imagetoolbox.core.domain.history.model.LastUsedTool { *; }
-keep class com.t8rin.imagetoolbox.core.domain.history.model.AppUsageStatistics { *; }
-keep class com.t8rin.imagetoolbox.core.data.history.LastUsedTools { *; }
-keep class com.t8rin.imagetoolbox.core.data.history.SavedFormatCounters { *; }
# coil-resvg uses JNA/Uniffi generated names at runtime.
-keep class com.hashsequence.coilresvg.** { *; }
-keep class com.sun.jna.** { *; }
-dontwarn java.awt.Component
-dontwarn java.awt.GraphicsEnvironment
-dontwarn java.awt.HeadlessException
-dontwarn java.awt.Window
-dontwarn javax.naming.NamingEnumeration
-dontwarn javax.naming.NamingException
-dontwarn javax.naming.directory.Attribute
-dontwarn javax.naming.directory.Attributes
-dontwarn javax.naming.directory.DirContext
-dontwarn javax.naming.directory.InitialDirContext
-dontwarn javax.naming.directory.SearchControls
-dontwarn javax.naming.directory.SearchResult
-assumevalues public class androidx.compose.runtime.ComposeRuntimeFlags {
static boolean isLinkBufferComposerEnabled return true;
}
+239
View File
@@ -0,0 +1,239 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ ImageToolbox is an image editor for android
~ Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
~
~ You should have received a copy of the Apache License
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="
com.google.mlkit.vision.segmentation.subject,
androidx.pdf,
androidx.pdf.viewer.fragment,
androidx.pdf.document.service,
androidx.pdf.core" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<uses-permission
android:name="com.google.android.gms.permission.AD_ID"
tools:node="remove" />
<uses-permission
android:name="android.permission.ACCESS_ADSERVICES_AD_ID"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_PHONE_STATE"
tools:node="remove" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.PROJECT_MEDIA" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application
android:name=".app.presentation.components.ImageToolboxApplication"
android:allowBackup="true"
android:colorMode="wideColorGamut"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_launcher_name"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ImageToolbox.Launcher"
android:usesCleartextTraffic="true"
tools:replace="android:allowBackup, android:label"
tools:targetApi="tiramisu">
<profileable
android:shell="true"
tools:targetApi="29" />
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false" />
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="false" />
<activity
android:name=".app.presentation.AppActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:label="@string/app_launcher_name">
<action android:name="android.intent.action.VIEW" />
<action android:name="com.android.camera.action.REVIEW" />
<action android:name="android.provider.action.REVIEW" />
<action android:name="android.provider.action.REVIEW_SECURE" />
<data
android:scheme="content"
tools:ignore="AppLinkUrlError" />
<data android:scheme="file" />
<data android:mimeType="image/*" />
<data android:pathSuffix=".jxl" />
<data android:pathSuffix=".qoi" />
<data android:pathSuffix=".itp" />
<data android:mimeType="application/vnd.shana.informed.formtemplate" />
<data android:mimeType="application/x-imagetoolbox-project" />
<data android:mimeType="application/octet-stream" />
<data android:mimeType="application/zip" />
<data android:mimeType="application/pdf" />
<data android:mimeType="*/pdf" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.ACTION_BUG_REPORT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="image/*"
tools:ignore="AppLinkUrlError" />
<data android:mimeType="application/vnd.shana.informed.formtemplate" />
<data android:mimeType="application/x-imagetoolbox-project" />
<data android:mimeType="application/octet-stream" />
<data android:mimeType="application/zip" />
<data android:pathSuffix=".jxl" />
<data android:pathSuffix=".qoi" />
<data android:pathSuffix=".itp" />
<data android:mimeType="application/pdf" />
<data android:mimeType="*/pdf" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.INSERT_OR_EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="image/*"
tools:ignore="AppLinkUrlError" />
<data android:mimeType="application/vnd.shana.informed.formtemplate" />
<data android:mimeType="application/x-imagetoolbox-project" />
<data android:mimeType="application/octet-stream" />
<data android:mimeType="application/zip" />
<data android:pathSuffix=".jxl" />
<data android:pathSuffix=".qoi" />
<data android:pathSuffix=".itp" />
<data android:mimeType="application/pdf" />
<data android:mimeType="*/pdf" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.INSERT" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="image/*"
tools:ignore="AppLinkUrlError" />
<data android:mimeType="application/vnd.shana.informed.formtemplate" />
<data android:mimeType="application/x-imagetoolbox-project" />
<data android:mimeType="application/octet-stream" />
<data android:mimeType="application/zip" />
<data android:pathSuffix=".jxl" />
<data android:pathSuffix=".qoi" />
<data android:pathSuffix=".itp" />
<data android:mimeType="application/pdf" />
<data android:mimeType="*/pdf" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/file_provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
<!-- Prompt Google Play services to install the backported photo picker module -->
<!--suppress AndroidDomInspection -->
<service
android:name="com.google.android.gms.metadata.ModuleDependencies"
android:enabled="false"
android:exported="false"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
</intent-filter>
<meta-data
android:name="photopicker_activity:0:required"
android:value="" />
</service>
</application>
</manifest>
@@ -0,0 +1,44 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation
import android.content.Intent
import androidx.compose.runtime.Composable
import com.arkivanov.decompose.retainedComponent
import com.t8rin.imagetoolbox.core.ui.utils.ComposeActivity
import com.t8rin.imagetoolbox.feature.root.presentation.RootContent
import com.t8rin.imagetoolbox.feature.root.presentation.screenLogic.RootComponent
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class AppActivity : ComposeActivity() {
@Inject
lateinit var rootComponentFactory: RootComponent.Factory
private val component: RootComponent by lazy {
retainedComponent(factory = rootComponentFactory::invoke)
}
override fun handleIntent(intent: Intent) = component.handleDeeplinks(intent)
@Composable
override fun Content() = RootContent(component = component)
}
@@ -0,0 +1,87 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components
import com.t8rin.imagetoolbox.app.presentation.components.functions.attachLogWriter
import com.t8rin.imagetoolbox.app.presentation.components.functions.initCollages
import com.t8rin.imagetoolbox.app.presentation.components.functions.initColorNames
import com.t8rin.imagetoolbox.app.presentation.components.functions.initNeuralTool
import com.t8rin.imagetoolbox.app.presentation.components.functions.initOpenCV
import com.t8rin.imagetoolbox.app.presentation.components.functions.initPdfBox
import com.t8rin.imagetoolbox.app.presentation.components.functions.initQrScanner
import com.t8rin.imagetoolbox.app.presentation.components.functions.injectBaseComponent
import com.t8rin.imagetoolbox.app.presentation.components.functions.registerSecurityProviders
import com.t8rin.imagetoolbox.app.presentation.components.functions.setupFlags
import com.t8rin.imagetoolbox.app.presentation.components.utils.isMain
import com.t8rin.imagetoolbox.core.crash.presentation.components.applyGlobalExceptionHandler
import com.t8rin.imagetoolbox.core.domain.coroutines.AppScope
import com.t8rin.imagetoolbox.core.domain.remote.AnalyticsManager
import com.t8rin.imagetoolbox.core.domain.saving.KeepAliveService
import com.t8rin.imagetoolbox.core.resources.emoji.Emoji.initEmoji
import com.t8rin.imagetoolbox.core.ui.utils.ComposeApplication
import com.t8rin.imagetoolbox.core.utils.initAppContext
import dagger.hilt.android.HiltAndroidApp
import io.ktor.client.HttpClient
import javax.inject.Inject
@HiltAndroidApp
class ImageToolboxApplication : ComposeApplication() {
@Inject
lateinit var keepAliveService: KeepAliveService
@Inject
lateinit var appScope: AppScope
@Inject
lateinit var httpClient: HttpClient
@Inject
lateinit var analyticsManager: AnalyticsManager
private var isSetupCompleted: Boolean = false
override fun onCreate() {
super.onCreate()
runSetup()
}
override fun runSetup() {
if (isSetupCompleted) return
if (isMain()) {
setupFlags()
initAppContext()
initEmoji()
initOpenCV()
initNeuralTool()
initColorNames()
initQrScanner()
attachLogWriter()
applyGlobalExceptionHandler()
registerSecurityProviders()
initPdfBox()
injectBaseComponent()
initCollages()
isSetupCompleted = true
}
}
}
@@ -0,0 +1,40 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components.functions
import com.t8rin.imagetoolbox.app.presentation.components.ImageToolboxApplication
import com.t8rin.imagetoolbox.core.resources.R
import com.t8rin.imagetoolbox.core.ui.utils.helper.DeviceInfo
import com.t8rin.imagetoolbox.core.utils.Logger
import com.t8rin.imagetoolbox.core.utils.attachLogWriter
internal fun ImageToolboxApplication.attachLogWriter() {
Logger.attachLogWriter(
context = this@attachLogWriter,
fileProvider = getString(R.string.file_provider),
logsFilename = "image_toolbox_logs.txt",
startupLog = Logger.Log(
tag = "Device Info",
message = "--${DeviceInfo.get()}--",
level = Logger.Level.Info
),
errorHandler = analyticsManager::sendReport,
isSyncCreate = false
)
}
@@ -0,0 +1,24 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components.functions
import com.t8rin.collages.public.CollageConstants
import com.t8rin.imagetoolbox.core.data.image.utils.static
fun initCollages() =
CollageConstants.requestMapper { static() }
@@ -0,0 +1,25 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components.functions
import com.t8rin.colors.parser.ColorNameParser
import com.t8rin.imagetoolbox.app.presentation.components.ImageToolboxApplication
import kotlinx.coroutines.launch
internal fun ImageToolboxApplication.initColorNames() =
appScope.launch { ColorNameParser.init(this@initColorNames) }
@@ -0,0 +1,28 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components.functions
import com.t8rin.imagetoolbox.app.presentation.components.ImageToolboxApplication
import com.t8rin.imagetoolbox.core.domain.HF_BASE_URL
import com.t8rin.neural_tools.NeuralTool
internal fun ImageToolboxApplication.initNeuralTool() = NeuralTool.init(
context = this,
httpClient = httpClient,
baseUrl = HF_BASE_URL
)
@@ -0,0 +1,23 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components.functions
import android.app.Application
import com.t8rin.opencv_tools.utils.OpenCV
internal fun Application.initOpenCV() = OpenCV.init(this)
@@ -0,0 +1,23 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components.functions
import android.app.Application
import com.tom_roush.pdfbox.android.PDFBoxResourceLoader
internal fun Application.initPdfBox() = PDFBoxResourceLoader.init(this)
@@ -0,0 +1,28 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components.functions
import android.graphics.Bitmap
import com.t8rin.imagetoolbox.core.ui.utils.helper.ImageUtils.applyPadding
import com.t8rin.opencv_tools.qr_prepare.QrPrepareHelper
import io.github.g00fy2.quickie.extensions.QrProcessor
internal fun initQrScanner() = QrProcessor.setProcessor(::prepareBitmap)
private fun prepareBitmap(bitmap: Bitmap): Bitmap =
QrPrepareHelper.prepareQrForDecode(bitmap.applyPadding(100))
@@ -0,0 +1,23 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components.functions
import com.t8rin.imagetoolbox.app.presentation.components.ImageToolboxApplication
import com.t8rin.imagetoolbox.core.ui.utils.BaseComponent
internal fun ImageToolboxApplication.injectBaseComponent() = BaseComponent.inject(keepAliveService)
@@ -0,0 +1,104 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components.functions
import com.t8rin.imagetoolbox.core.domain.model.CipherType
import com.t8rin.imagetoolbox.core.domain.model.HashingType
import com.t8rin.imagetoolbox.core.utils.makeLog
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.operator.DefaultAlgorithmNameFinder
import java.security.Provider
import java.security.Security
internal fun registerSecurityProviders() {
initBouncyCastle()
HashingType.registerSecurityMessageDigests(
Security.getAlgorithms("MessageDigest").filterNotNull()
)
val finder = DefaultAlgorithmNameFinder()
CipherType.registerSecurityCiphers(
Security.getAlgorithms("Cipher").filterNotNull().mapNotNull { cipher ->
if (CipherType.BROKEN.any { cipher.contains(it, true) }) return@mapNotNull null
val oid = cipher.removePrefix("OID.")
if (oid.all { it.isDigit() || it.isWhitespace() || it == '.' }) {
CipherType.getInstance(
cipher = cipher,
name = finder.getAlgorithmName(
ASN1ObjectIdentifier(oid)
)
)
} else {
CipherType.getInstance(
cipher = cipher
)
}.also {
val extraExclude = it.cipher == "DES"
|| it.name == "DES/CBC"
|| it.name == "THREEFISH-512"
|| it.name == "THREEFISH-1024"
|| it.name == "CCM"
if (extraExclude) return@mapNotNull null
}
}
)
}
private fun initBouncyCastle() {
if (Security.getProvider(WORKAROUND_NAME) != null) return
try {
logProviders("OLD")
Security.addProvider(BouncyCastleWorkaroundProvider())
logProviders("NEW")
} catch (e: Throwable) {
e.makeLog()
"Failed to register BouncyCastleWorkaroundProvider".makeLog()
}
}
private fun logProviders(tag: String): Int {
val providers = Security.getProviders()
providers.forEachIndexed { index, provider ->
"$tag [$index]: ${provider.name} - ${provider.info}".makeLog("Providers")
}
return providers.size
}
private class BouncyCastleWorkaroundProvider(
bouncyCastleProvider: Provider = BouncyCastleProvider()
) : Provider(
WORKAROUND_NAME,
bouncyCastleProvider.version,
bouncyCastleProvider.info
) {
init {
for ((key, value) in bouncyCastleProvider.entries) {
put(key.toString(), value.toString())
}
}
}
private const val WORKAROUND_NAME = "BC_WORKAROUND"
@@ -0,0 +1,35 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components.functions
import androidx.compose.foundation.ComposeFoundationFlags
import androidx.compose.material3.ComposeMaterial3Flags
import androidx.compose.runtime.ComposeRuntimeFlags
import androidx.compose.runtime.Composer
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.tooling.ComposeStackTraceMode
import com.arkivanov.decompose.DecomposeSettings
@OptIn(ExperimentalComposeApi::class)
internal fun setupFlags() {
ComposeRuntimeFlags.isLinkBufferComposerEnabled = true
ComposeFoundationFlags.isPausableCompositionInPrefetchEnabled = true
ComposeMaterial3Flags.isCheckboxStylingFixEnabled = true
Composer.setDiagnosticStackTraceMode(ComposeStackTraceMode.GroupKeys)
DecomposeSettings.update { it.copy(duplicateConfigurationsEnabled = true) }
}
@@ -0,0 +1,69 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.app.presentation.components.utils
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.Application
import android.content.Context.ACTIVITY_SERVICE
import android.os.Build.VERSION.SDK_INT
import com.t8rin.imagetoolbox.core.utils.makeLog
internal fun Application.isMain(): Boolean =
getProcessName().makeLog("Current Process") == packageName.makeLog("Current packageName")
@SuppressLint("PrivateApi")
internal fun Application.getProcessName(): String? {
if (SDK_INT >= 28) {
return Application.getProcessName()
}
// Try using ActivityThread to determine the current process name.
try {
val activityThread = Class.forName(
"android.app.ActivityThread",
false,
this::class.java.classLoader
)
val packageName: Any?
val currentProcessName = activityThread.getDeclaredMethod("currentProcessName")
currentProcessName.isAccessible = true
packageName = currentProcessName.invoke(null)
if (packageName is String) {
return packageName
}
} catch (exception: Throwable) {
exception.makeLog()
}
// Fallback to the most expensive way
val pid: Int = android.os.Process.myPid()
val am = getSystemService(ACTIVITY_SERVICE) as ActivityManager
val processes = am.runningAppProcesses
if (processes != null && processes.isNotEmpty()) {
for (process in processes) {
if (process.pid == pid) {
return process.processName
}
}
}
return null
}
+17
View File
@@ -0,0 +1,17 @@
#
# ImageToolbox is an image editor for android
# Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# You should have received a copy of the Apache License
# along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
#
unqualifiedResLocale=en
+39
View File
@@ -0,0 +1,39 @@
{
"project_info": {
"project_number": "698299287298",
"project_id": "imagetoolbox-13e3c",
"storage_bucket": "imagetoolbox-13e3c.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:698299287298:android:11af7edf43198644b6e41b",
"android_client_info": {
"package_name": "ru.tech.imageresizershrinker.debug"
}
},
"oauth_client": [
{
"client_id": "698299287298-5ukroo831v2bn1vj8ff0oa7ch5h94mfg.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBH5QgaBw6XAfuxhPVJzq-r9s8ZMFalXzU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "698299287298-5ukroo831v2bn1vj8ff0oa7ch5h94mfg.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}
@@ -0,0 +1,39 @@
{
"project_info": {
"project_number": "698299287298",
"project_id": "imagetoolbox-13e3c",
"storage_bucket": "imagetoolbox-13e3c.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:698299287298:android:11af7edf43198644b6e41b",
"android_client_info": {
"package_name": "ru.tech.imageresizershrinker"
}
},
"oauth_client": [
{
"client_id": "698299287298-5ukroo831v2bn1vj8ff0oa7ch5h94mfg.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBH5QgaBw6XAfuxhPVJzq-r9s8ZMFalXzU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "698299287298-5ukroo831v2bn1vj8ff0oa7ch5h94mfg.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}
+1
View File
@@ -0,0 +1 @@
/build
+82
View File
@@ -0,0 +1,82 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
@file:Suppress("UnstableApiUsage")
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("com.android.test")
id("androidx.baselineprofile")
}
android {
namespace = "com.t8rin.imagetoolbox.benchmark"
compileSdk = libs.versions.androidCompileSdk.get().toIntOrNull()
defaultConfig {
minSdk = 23
targetSdk = libs.versions.androidTargetSdk.get().toIntOrNull()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
compileOptions {
sourceCompatibility = JavaVersion.toVersion(libs.versions.jvmTarget.get())
targetCompatibility = JavaVersion.toVersion(libs.versions.jvmTarget.get())
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get()))
}
}
buildTypes {
// This benchmark buildType is used for benchmarking, and should function like your
// release build (for example, with minification on). It"s signed with a debug key
// for easy local/CI testing.
create("benchmark") {
isDebuggable = true
signingConfig = getByName("debug").signingConfig
matchingFallbacks += listOf("release")
}
}
flavorDimensions += listOf("app")
productFlavors {
create("foss") { dimension = "app" }
create("market") { dimension = "app" }
}
targetProjectPath = ":app"
experimentalProperties["android.experimental.self-instrumenting"] = true
}
dependencies {
implementation(libs.androidx.test.ext.junit)
implementation(libs.espresso)
implementation(libs.uiautomator)
implementation(libs.benchmark.macro.junit4)
}
androidComponents {
beforeVariants(selector().all()) {
it.enable = it.buildType == "benchmark"
}
}
+18
View File
@@ -0,0 +1,18 @@
<!--
~ ImageToolbox is an image editor for android
~ Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
~
~ You should have received a copy of the Apache License
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
-->
<manifest />
@@ -0,0 +1,39 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.benchmark
import androidx.annotation.RequiresApi
import androidx.benchmark.macro.junit4.BaselineProfileRule
import org.junit.Rule
import org.junit.Test
@RequiresApi(28)
class BaselineProfileGenerator {
@get:Rule
val baselineProfileRule = BaselineProfileRule()
@Test
fun startup() = baselineProfileRule.collect(
packageName = "com.t8rin.imagetoolbox",
includeInStartupProfile = true,
profileBlock = {
startActivityAndWait()
device.pressBack()
}
)
}
+11
View File
@@ -0,0 +1,11 @@
*.iml
.gradle
/local.properties
.DS_Store
/build
/app/release
/captures
.externalNativeBuild
.cxx
/.idea
/.kotlin/
+1
View File
@@ -0,0 +1 @@
/build
+71
View File
@@ -0,0 +1,71 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
`kotlin-dsl`
}
group = "com.t8rin.imagetoolbox.buildlogic"
// Configure the build-logic plugins to target JDK 17
// This matches the JDK used to build the project, and is not related to what is running on device.
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
dependencies {
compileOnly(libs.agp.gradle)
compileOnly(libs.kotlin.gradle)
compileOnly(libs.detekt.gradle)
compileOnly(libs.compose.compiler.gradle)
compileOnly(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
}
gradlePlugin {
// register the convention plugin
plugins {
register("imageToolboxLibrary") {
id = "image.toolbox.library"
implementationClass = "ImageToolboxLibraryPlugin"
}
register("imageToolboxHiltPlugin") {
id = "image.toolbox.hilt"
implementationClass = "ImageToolboxHiltPlugin"
}
register("imageToolboxLibraryFeature") {
id = "image.toolbox.feature"
implementationClass = "ImageToolboxLibraryFeaturePlugin"
}
register("imageToolboxLibraryComposePlugin") {
id = "image.toolbox.compose"
implementationClass = "ImageToolboxLibraryComposePlugin"
}
register("imageToolboxApplicationPlugin") {
id = "image.toolbox.application"
implementationClass = "ImageToolboxApplicationPlugin"
}
}
}
@@ -0,0 +1,79 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
import com.android.build.api.dsl.ApplicationExtension
import com.t8rin.imagetoolbox.configureCompose
import com.t8rin.imagetoolbox.configureDetekt
import com.t8rin.imagetoolbox.configureKotlinAndroid
import com.t8rin.imagetoolbox.core
import com.t8rin.imagetoolbox.crash
import com.t8rin.imagetoolbox.data
import com.t8rin.imagetoolbox.di
import com.t8rin.imagetoolbox.domain
import com.t8rin.imagetoolbox.implementation
import com.t8rin.imagetoolbox.libs
import com.t8rin.imagetoolbox.projects
import com.t8rin.imagetoolbox.resources
import com.t8rin.imagetoolbox.settings
import com.t8rin.imagetoolbox.ui
import com.t8rin.imagetoolbox.utils
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
@Suppress("UNUSED")
class ImageToolboxApplicationPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.application")
apply(plugin = "kotlin-parcelize")
apply(plugin = "com.google.gms.google-services")
apply(plugin = "com.google.firebase.crashlytics")
apply(plugin = "com.mikepenz.aboutlibraries.plugin.android")
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
apply(plugin = "io.gitlab.arturbosch.detekt")
configureDetekt(extensions.getByType<DetektExtension>())
extensions.configure<ApplicationExtension> {
configureKotlinAndroid(
commonExtension = this,
createFlavors = false
)
defaultConfig.targetSdk = libs.versions.androidTargetSdk.get().toIntOrNull()
}
dependencies {
implementation(libs.androidxCore)
implementation(projects.core.data)
implementation(projects.core.ui)
implementation(projects.core.domain)
implementation(projects.core.resources)
implementation(projects.core.settings)
implementation(projects.core.di)
implementation(projects.core.crash)
implementation(projects.core.utils)
}
configureCompose(extensions.getByType<ApplicationExtension>())
}
}
}
@@ -0,0 +1,40 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
import com.t8rin.imagetoolbox.implementation
import com.t8rin.imagetoolbox.ksp
import com.t8rin.imagetoolbox.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies
@Suppress("UNUSED")
class ImageToolboxHiltPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "dagger.hilt.android.plugin")
apply(plugin = "com.google.devtools.ksp")
dependencies {
implementation(libs.dagger.hilt.android)
ksp(libs.dagger.hilt.compiler)
ksp(libs.kotlin.metadata.jvm)
}
}
}
}
@@ -0,0 +1,35 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
import com.android.build.api.dsl.LibraryExtension
import com.t8rin.imagetoolbox.configureCompose
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.getByType
@Suppress("UNUSED")
class ImageToolboxLibraryComposePlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
apply(plugin = "com.android.library")
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
configureCompose(extensions.getByType<LibraryExtension>())
}
}
}
@@ -0,0 +1,51 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
import com.t8rin.imagetoolbox.configureDetekt
import com.t8rin.imagetoolbox.core
import com.t8rin.imagetoolbox.crash
import com.t8rin.imagetoolbox.data
import com.t8rin.imagetoolbox.di
import com.t8rin.imagetoolbox.domain
import com.t8rin.imagetoolbox.implementation
import com.t8rin.imagetoolbox.projects
import com.t8rin.imagetoolbox.resources
import com.t8rin.imagetoolbox.settings
import com.t8rin.imagetoolbox.ui
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
@Suppress("UNUSED")
class ImageToolboxLibraryFeaturePlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
configureDetekt(extensions.getByType<DetektExtension>())
dependencies {
implementation(projects.core.data)
implementation(projects.core.ui)
implementation(projects.core.domain)
implementation(projects.core.resources)
implementation(projects.core.settings)
implementation(projects.core.di)
implementation(projects.core.crash)
}
}
}
}
@@ -0,0 +1,53 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
import com.android.build.api.dsl.LibraryExtension
import com.t8rin.imagetoolbox.configureDetekt
import com.t8rin.imagetoolbox.configureKotlinAndroid
import com.t8rin.imagetoolbox.implementation
import com.t8rin.imagetoolbox.libs
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.getByType
@Suppress("UNUSED")
class ImageToolboxLibraryPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("com.android.library")
apply("kotlin-parcelize")
apply("kotlinx-serialization")
apply(libs.detekt.gradle.get().group)
}
configureDetekt(extensions.getByType<DetektExtension>())
extensions.configure<LibraryExtension> {
configureKotlinAndroid(this)
defaultConfig.minSdk = libs.versions.androidMinSdk.get().toIntOrNull()
}
dependencies {
implementation(libs.androidxCore)
}
}
}
}
@@ -0,0 +1,48 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
internal fun Project.configureCompose(
commonExtension: CommonExtension
) {
commonExtension.apply {
buildFeatures.apply {
compose = true
}
dependencies {
implementation(libs.androidx.material3)
implementation(libs.window.sizeclass)
implementation(libs.androidx.material)
implementation(libs.compose.preview)
debugImplementation(libs.compose.tooling)
}
}
extensions.configure<ComposeCompilerGradlePluginExtension> {
stabilityConfigurationFiles.addAll(
rootProject.layout.projectDirectory.file("compose_compiler_config.conf")
)
}
}
@@ -0,0 +1,41 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox
import io.gitlab.arturbosch.detekt.Detekt
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
import org.gradle.api.Project
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.named
internal fun Project.configureDetekt(extension: DetektExtension) = extension.apply {
tasks.named<Detekt>("detekt") {
reports {
xml.required.set(true)
html.required.set(true)
txt.required.set(true)
sarif.required.set(true)
md.required.set(true)
}
}
dependencies {
detektPlugins(libs.detekt.formatting)
detektPlugins(libs.detekt.compose)
}
}
@@ -0,0 +1,138 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox
import com.android.build.api.dsl.CommonExtension
import org.gradle.api.JavaVersion
import org.gradle.api.Project
import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.provideDelegate
import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
internal fun Project.configureKotlinAndroid(
commonExtension: CommonExtension,
createFlavors: Boolean = true
) {
commonExtension.apply {
compileSdk = libs.versions.androidCompileSdk.get().toIntOrNull()
compileSdkExtension = libs.versions.androidCompileSdkExtension.get().toIntOrNull()
defaultConfig.apply {
minSdk = libs.versions.androidMinSdk.get().toIntOrNull()
}
if (createFlavors) {
flavorDimensions += "app"
productFlavors.apply {
create("foss") {
dimension = "app"
}
create("market") {
dimension = "app"
}
}
}
compileOptions.apply {
sourceCompatibility = javaVersion
targetCompatibility = javaVersion
isCoreLibraryDesugaringEnabled = true
}
buildFeatures.apply {
compose = false
aidl = false
shaders = false
buildConfig = false
resValues = false
}
packaging.apply {
resources {
excludes.add("/META-INF/{AL2.0,LGPL2.1}")
}
}
lint.apply {
disable += "UsingMaterialAndMaterial3Libraries"
disable += "ModifierParameter"
disable += "ConvertLongToDuration"
}
}
configureKotlin<KotlinAndroidProjectExtension>()
dependencies {
coreLibraryDesugaring(libs.desugaring)
}
}
val Project.javaVersion: JavaVersion
get() = JavaVersion.toVersion(
libs.versions.jvmTarget.get()
)
/**
* Configure base Kotlin options
*/
private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() = configure<T> {
val args = listOf(
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
"-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
"-opt-in=androidx.compose.ui.unit.ExperimentalUnitApi",
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview",
"-opt-in=androidx.compose.material.ExperimentalMaterialApi",
"-opt-in=com.arkivanov.decompose.ExperimentalDecomposeApi",
"-opt-in=coil3.annotation.ExperimentalCoilApi",
"-opt-in=coil3.annotation.DelicateCoilApi",
"-opt-in=kotlin.contracts.ExperimentalContracts",
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-opt-in=androidx.compose.ui.text.ExperimentalTextApi",
"-opt-in=kotlinx.coroutines.DelicateCoroutinesApi",
"-Xannotation-default-target=param-property",
"-XXLanguage:+PropertyParamAnnotationDefaultTargetMode"
)
// Treat all Kotlin warnings as errors (disabled by default)
// Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
val warningsAsErrors: String? by project
when (this) {
is KotlinAndroidProjectExtension -> compilerOptions
is KotlinJvmProjectExtension -> compilerOptions
else -> error("Unsupported project extension $this ${T::class}")
}.apply {
jvmTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
allWarningsAsErrors = warningsAsErrors.toBoolean()
freeCompilerArgs.addAll(args)
}
tasks.withType<KotlinCompile>().configureEach {
compilerOptions.freeCompilerArgs.addAll(args)
}
}
@@ -0,0 +1,96 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox
import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.api.Project
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.provider.Provider
import org.gradle.kotlin.dsl.DependencyHandlerScope
import org.gradle.kotlin.dsl.the
val Project.libs
get(): LibrariesForLibs = the<LibrariesForLibs>()
val Project.projects
get(): Projects = object : Projects, ProjectHolder {
override fun project(path: String): Project = this@projects.project(path)
}
val Projects.core
get(): CoreProjects = object : CoreProjects, ProjectHolder by this {}
val CoreProjects.data
get(): ProjectLibrary = project(":core:data")
val CoreProjects.ui
get(): ProjectLibrary = project(":core:ui")
val CoreProjects.domain
get(): ProjectLibrary = project(":core:domain")
val CoreProjects.resources
get(): ProjectLibrary = project(":core:resources")
val CoreProjects.settings
get(): ProjectLibrary = project(":core:settings")
val CoreProjects.di
get(): ProjectLibrary = project(":core:di")
val CoreProjects.crash
get(): ProjectLibrary = project(":core:crash")
val CoreProjects.utils
get(): ProjectLibrary = project(":core:utils")
fun DependencyHandlerScope.debugImplementation(
dependency: Library
) = add("debugImplementation", dependency)
fun DependencyHandlerScope.implementation(
dependency: Library
) = add("implementation", dependency)
fun DependencyHandlerScope.coreLibraryDesugaring(
dependency: Library
) = add("coreLibraryDesugaring", dependency)
fun DependencyHandlerScope.implementation(
dependency: ProjectLibrary
) = add("implementation", dependency)
fun DependencyHandlerScope.ksp(
dependency: Library
) = add("ksp", dependency)
fun DependencyHandlerScope.detektPlugins(
dependency: Library
) = add("detektPlugins", dependency)
typealias Library = Provider<MinimalExternalModuleDependency>
typealias ProjectLibrary = Project
interface Projects : ProjectHolder
interface CoreProjects : ProjectHolder
interface ProjectHolder {
fun project(path: String): Project
}
+34
View File
@@ -0,0 +1,34 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
@file:Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
versionCatalogs {
create("libs") {
// make sure the file rootProject/gradle/verison.toml exists!
from(files("../gradle/libs.versions.toml"))
}
}
}
rootProject.name = "build-logic"
include(":convention")
+43
View File
@@ -0,0 +1,43 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
buildscript {
repositories {
gradlePluginPortal()
google()
mavenCentral()
maven { setUrl("https://jitpack.io") }
}
dependencies {
classpath(libs.kotlinx.serialization.gradle)
classpath(libs.ksp.gradle)
classpath(libs.agp.gradle)
classpath(libs.kotlin.gradle)
classpath(libs.hilt.gradle)
classpath(libs.gms.gradle)
classpath(libs.firebase.crashlytics.gradle)
classpath(libs.baselineprofile.gradle)
classpath(libs.detekt.gradle)
classpath(libs.aboutlibraries.gradle)
classpath(libs.compose.compiler.gradle)
}
}
tasks.register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
}
+36
View File
@@ -0,0 +1,36 @@
java.time.ZoneId
java.time.ZoneOffset
java.text.DecimalFormat
java.text.NumberFormat
java.time.Instant
java.time.Duration
java.time.LocalDate
java.time.LocalDateTime
java.time.LocalTime
java.time.ZonedDateTime
java.util.Date
java.util.Locale
java.io.File
kotlin.collections.*
kotlin.time.Duration
android.net.Uri
android.graphics.Bitmap
androidx.compose.ui.graphics.painter.Painter
androidx.compose.ui.graphics.vector.ImageVector
androidx.compose.ui.graphics.ImageBitmap
com.t8rin.imagetoolbox.core.crash.*
com.t8rin.imagetoolbox.core.filters.*
com.t8rin.imagetoolbox.core.resources.*
com.t8rin.imagetoolbox.core.data.*
com.t8rin.imagetoolbox.core.domain.*
com.t8rin.imagetoolbox.core.ui.*
com.t8rin.imagetoolbox.core.settings.*
com.t8rin.imagetoolbox.core.*
com.t8rin.imagetoolbox.feature.*
nl.dionsegijn.konfetti.core.*
+1
View File
@@ -0,0 +1 @@
/build
+33
View File
@@ -0,0 +1,33 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
plugins {
alias(libs.plugins.image.toolbox.library)
alias(libs.plugins.image.toolbox.hilt)
alias(libs.plugins.image.toolbox.compose)
}
android.namespace = "com.t8rin.imagetoolbox.core.crash"
dependencies {
implementation(projects.core.ui)
implementation(projects.core.settings)
"marketImplementation"(platform(libs.firebase.bom))
"marketImplementation"(libs.firebase.crashlytics)
"marketImplementation"(libs.firebase.analytics)
}
@@ -0,0 +1,41 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.data
import com.t8rin.imagetoolbox.core.domain.remote.AnalyticsManager
internal object AnalyticsManagerImpl : AnalyticsManager {
override var allowCollectCrashlytics: Boolean = false
override var allowCollectAnalytics: Boolean = false
override fun updateAnalyticsCollectionEnabled(value: Boolean) = Unit
override fun updateAllowCollectCrashlytics(value: Boolean) = Unit
override fun sendReport(throwable: Throwable) = Unit
override fun registerScreenOpen(screenName: String) = Unit
override fun pushMetric(
tag: String,
metric: String
) = Unit
}
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ ImageToolbox is an image editor for android
~ Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
~
~ You should have received a copy of the Apache License
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity android:name=".presentation.CrashActivity" />
</application>
</manifest>
@@ -0,0 +1,36 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.di
import com.t8rin.imagetoolbox.core.crash.data.AnalyticsManagerImpl
import com.t8rin.imagetoolbox.core.domain.remote.AnalyticsManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object CrashModule {
@Provides
@Singleton
fun analyticsManager(): AnalyticsManager = AnalyticsManagerImpl
}
@@ -0,0 +1,47 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.presentation
import androidx.compose.runtime.Composable
import com.arkivanov.decompose.retainedComponent
import com.t8rin.imagetoolbox.core.crash.presentation.components.CrashHandler
import com.t8rin.imagetoolbox.core.crash.presentation.components.CrashRootContent
import com.t8rin.imagetoolbox.core.crash.presentation.screenLogic.CrashComponent
import com.t8rin.imagetoolbox.core.ui.utils.ComposeActivity
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class CrashActivity : ComposeActivity(), CrashHandler {
@Inject
lateinit var componentFactory: CrashComponent.Factory
private val component: CrashComponent by lazy {
retainedComponent { componentContext ->
componentFactory(
componentContext = componentContext,
crashInfo = getCrashInfo()
)
}
}
@Composable
override fun Content() = CrashRootContent(component = component)
}
@@ -0,0 +1,190 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.presentation.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.t8rin.imagetoolbox.core.domain.TELEGRAM_GROUP_LINK
import com.t8rin.imagetoolbox.core.resources.Icons
import com.t8rin.imagetoolbox.core.resources.R
import com.t8rin.imagetoolbox.core.resources.icons.Github
import com.t8rin.imagetoolbox.core.resources.icons.MobileShare
import com.t8rin.imagetoolbox.core.resources.icons.Telegram
import com.t8rin.imagetoolbox.core.ui.theme.Black
import com.t8rin.imagetoolbox.core.ui.theme.Blue
import com.t8rin.imagetoolbox.core.ui.theme.White
import com.t8rin.imagetoolbox.core.ui.theme.outlineVariant
import com.t8rin.imagetoolbox.core.ui.utils.animation.springySpec
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedButton
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedCircularProgressIndicator
import com.t8rin.imagetoolbox.core.ui.widget.text.AutoSizeText
import kotlinx.coroutines.delay
@Composable
internal fun CrashActionButtons(
onCopyCrashInfo: () -> Unit,
onShareLogs: () -> Unit,
isSendingLogs: Boolean,
githubLink: String
) {
BoxWithConstraints(
modifier = Modifier.fillMaxWidth()
) {
val containerWidth = maxWidth
Column {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
val linkHandler = LocalUriHandler.current
LargeEnhancedButton(
onClick = {
onCopyCrashInfo()
linkHandler.openUri(TELEGRAM_GROUP_LINK)
},
modifier = Modifier
.weight(1f)
.width(containerWidth / 2f)
.padding(end = 8.dp),
containerColor = Blue,
contentColor = White,
icon = Icons.Rounded.Telegram,
text = stringResource(R.string.contact_me)
)
LargeEnhancedButton(
onClick = {
onCopyCrashInfo()
linkHandler.openUri(githubLink)
},
modifier = Modifier
.weight(1f)
.width(containerWidth / 2f),
containerColor = Black,
contentColor = White,
icon = Icons.Rounded.Github,
text = stringResource(id = R.string.create_issue),
)
}
Spacer(modifier = Modifier.height(12.dp))
LargeEnhancedButton(
onClick = {
onCopyCrashInfo()
onShareLogs()
},
modifier = Modifier.fillMaxWidth(),
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
icon = Icons.Rounded.MobileShare,
text = stringResource(id = R.string.send_logs),
isLoading = isSendingLogs
)
}
}
}
@Composable
private fun LargeEnhancedButton(
onClick: () -> Unit,
containerColor: Color,
contentColor: Color,
icon: ImageVector,
text: String,
isLoading: Boolean = false,
modifier: Modifier = Modifier,
) {
val progressAnimatable = remember { Animatable(if (isLoading) 1f else 0f) }
val progress = progressAnimatable.value
LaunchedEffect(isLoading) {
delay(400)
if (isLoading) {
progressAnimatable.animateTo(
targetValue = 1f,
animationSpec = springySpec()
)
} else {
progressAnimatable.animateTo(
targetValue = 0f,
animationSpec = tween(200)
)
}
}
EnhancedButton(
onClick = onClick,
modifier = modifier
.height(48.dp),
containerColor = containerColor,
contentColor = contentColor,
borderColor = MaterialTheme.colorScheme.outlineVariant(
onTopOf = containerColor
),
contentPadding = ButtonDefaults.ButtonWithIconContentPadding
) {
AnimatedContent(progress > 0) { showLoading ->
if (showLoading) {
EnhancedCircularProgressIndicator(
modifier = Modifier.size(24.dp * progress),
trackColor = MaterialTheme.colorScheme.onPrimary.copy(0.2f),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 3.dp
)
} else {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Icon(
imageVector = icon,
contentDescription = text
)
Spacer(modifier = Modifier.width(8.dp))
AutoSizeText(
text = text,
maxLines = 1
)
}
}
}
}
}
@@ -0,0 +1,101 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.presentation.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.t8rin.imagetoolbox.core.settings.presentation.provider.LocalSettingsState
import com.t8rin.imagetoolbox.core.ui.theme.blend
import com.t8rin.imagetoolbox.core.ui.theme.takeColorFromScheme
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
import com.t8rin.imagetoolbox.core.ui.widget.modifier.container
import com.t8rin.imagetoolbox.core.ui.widget.other.EmojiItem
@Composable
internal fun CrashAttentionCard(
title: String,
description: String,
emoji: String
) {
val isNightMode = LocalSettingsState.current.isNightMode
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.container(
shape = ShapeDefaults.large,
resultPadding = 16.dp,
color = takeColorFromScheme {
if (isNightMode) {
errorContainer.blend(surfaceContainerLow, 0.75f)
} else {
errorContainer.blend(surfaceContainerLow, 0.65f)
}
}
),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
val contentColor = takeColorFromScheme {
if (isNightMode) {
onError.blend(onSurface, 0.75f)
} else {
error.blend(onSurface, 0.6f)
}
}
EmojiItem(
emoji = "",
fontSize = 0.sp,
animatedEmoji = emoji,
shape = ShapeDefaults.default,
modifier = Modifier
.size(80.dp)
.statusBarsPadding(),
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = title,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center,
fontSize = 22.sp,
lineHeight = 26.sp,
color = contentColor
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = description,
textAlign = TextAlign.Center,
fontSize = 16.sp,
lineHeight = 20.sp,
color = contentColor
)
}
}
@@ -0,0 +1,84 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.presentation.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.t8rin.imagetoolbox.core.resources.Icons
import com.t8rin.imagetoolbox.core.resources.R
import com.t8rin.imagetoolbox.core.resources.icons.ContentCopy
import com.t8rin.imagetoolbox.core.resources.icons.RestartAlt
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedFloatingActionButton
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.EnhancedFloatingActionButtonType
import com.t8rin.imagetoolbox.core.ui.widget.text.AutoSizeText
@Composable
internal fun CrashBottomButtons(
modifier: Modifier,
onCopy: (() -> Unit)?,
onRestartApp: () -> Unit
) {
Row(
modifier = modifier
.padding(8.dp)
.navigationBarsPadding()
.displayCutoutPadding()
) {
EnhancedFloatingActionButton(
modifier = Modifier
.weight(1f, false),
onClick = onRestartApp,
content = {
Spacer(Modifier.width(16.dp))
Icon(
imageVector = Icons.Rounded.RestartAlt,
contentDescription = stringResource(R.string.restart_app)
)
Spacer(Modifier.width(16.dp))
AutoSizeText(
text = stringResource(R.string.restart_app),
maxLines = 1
)
Spacer(Modifier.width(16.dp))
}
)
onCopy?.let {
Spacer(Modifier.width(8.dp))
EnhancedFloatingActionButton(
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
onClick = onCopy,
type = EnhancedFloatingActionButtonType.SecondaryHorizontal
) {
Icon(
imageVector = Icons.Rounded.ContentCopy,
contentDescription = stringResource(R.string.copy)
)
}
}
}
}
@@ -0,0 +1,121 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.presentation.components
import android.content.Intent
import android.util.Log
import com.t8rin.imagetoolbox.core.domain.ISSUE_TRACKER
import com.t8rin.imagetoolbox.core.ui.utils.helper.DeviceInfo
import com.t8rin.imagetoolbox.core.utils.encodeEscaped
interface CrashHandler {
fun getIntent(): Intent
private val intent: Intent
@JvmName("getIntentValue")
get() = getIntent()
private fun getCrashReason(): String = intent.getStringExtra(EXCEPTION_EXTRA) ?: ""
fun getCrashInfo(): CrashInfo {
val crashReason = getCrashReason()
val splitData = crashReason.split(DELIMITER)
val exceptionName = splitData.first().trim()
val stackTrace = splitData.drop(1).joinToString(DELIMITER)
val title = "[Bug] App Crash: $exceptionName"
val deviceInfo = DeviceInfo.getAsString()
val body = listOf(deviceInfo, stackTrace).joinToString(DELIMITER)
return CrashInfo(
title = title,
body = body,
exceptionName = exceptionName,
stackTrace = stackTrace
)
}
companion object {
internal const val EXCEPTION_EXTRA = "EXCEPTION_EXTRA"
fun getCrashInfoAsExtra(
throwable: Throwable
): String {
val exceptionName = throwable::class.java.simpleName
val stackTrace = Log.getStackTraceString(throwable)
return listOf(exceptionName, stackTrace).joinToString(DELIMITER)
}
}
}
data class CrashInfo(
val title: String,
val body: String,
val exceptionName: String,
val stackTrace: String
) {
val isOutOfMemory: Boolean = listOf(
title,
body,
exceptionName,
stackTrace
).any { text ->
text.contains(
other = "OutOfMemoryError",
ignoreCase = true
) || text.contains(
other = "Failed to allocate",
ignoreCase = true
)
}
val isForegroundServiceDidNotStartInTime: Boolean = listOf(
title,
body,
exceptionName,
stackTrace
).any { text ->
text.contains(
other = "ForegroundServiceDidNotStartInTimeException",
ignoreCase = true
)
}
val isNonReportable: Boolean = isOutOfMemory || isForegroundServiceDidNotStartInTime
val emoji: String = if (isNonReportable) {
if (isOutOfMemory) {
"file:///android_asset/lottie/transportation/construction.lottie"
} else {
"file:///android_asset/lottie/objects/bomb.lottie"
}
} else {
"file:///android_asset/lottie/objects/siren.lottie"
}
val textToSend = listOf(title, body).joinToString(DELIMITER)
val githubLink =
"$ISSUE_TRACKER/new?title=${title.encodeEscaped()}&body=${body.encodeEscaped()}"
}
private const val DELIMITER = "\n\n"
@@ -0,0 +1,73 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.presentation.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.selection.SelectionContainer
import com.t8rin.imagetoolbox.core.resources.Icons
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.t8rin.imagetoolbox.core.resources.icons.BugReport
import com.t8rin.imagetoolbox.core.ui.widget.modifier.ShapeDefaults
import com.t8rin.imagetoolbox.core.ui.widget.other.ExpandableItem
import com.t8rin.imagetoolbox.core.ui.widget.text.AutoSizeText
@Composable
internal fun CrashInfoCard(crashInfo: CrashInfo) {
ExpandableItem(
shape = ShapeDefaults.extraLarge,
modifier = Modifier.fillMaxWidth(),
visibleContent = {
Icon(
imageVector = Icons.Rounded.BugReport,
contentDescription = "crash",
modifier = Modifier.padding(
start = 16.dp,
top = 16.dp,
bottom = 16.dp
)
)
AutoSizeText(
text = crashInfo.exceptionName,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Start,
modifier = Modifier
.padding(16.dp)
.weight(1f)
)
},
expandableContent = {
AnimatedVisibility(visible = it) {
SelectionContainer {
Text(
text = crashInfo.stackTrace,
textAlign = TextAlign.Center,
modifier = Modifier.padding(16.dp)
)
}
}
}
)
}
@@ -0,0 +1,113 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.presentation.components
import android.content.Intent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.t8rin.imagetoolbox.core.crash.presentation.screenLogic.CrashComponent
import com.t8rin.imagetoolbox.core.resources.R
import com.t8rin.imagetoolbox.core.settings.presentation.model.toUiState
import com.t8rin.imagetoolbox.core.ui.utils.helper.AppActivityClass
import com.t8rin.imagetoolbox.core.ui.utils.helper.Clipboard
import com.t8rin.imagetoolbox.core.ui.utils.provider.ImageToolboxCompositionLocals
import com.t8rin.imagetoolbox.core.ui.widget.enhanced.enhancedVerticalScroll
@Composable
internal fun CrashRootContent(component: CrashComponent) {
val context = LocalContext.current
val crashInfo = component.crashInfo
ImageToolboxCompositionLocals(
settingsState = component.settingsState.toUiState()
) {
val copyCrashInfo: () -> Unit = {
Clipboard.copy(crashInfo.textToSend)
}
Column(
modifier = Modifier
.align(Alignment.Center)
.enhancedVerticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
.padding(bottom = 80.dp)
.navigationBarsPadding()
.displayCutoutPadding(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
when {
crashInfo.isOutOfMemory -> {
CrashAttentionCard(
title = stringResource(R.string.crash_oom_title),
description = stringResource(R.string.crash_oom_description),
emoji = crashInfo.emoji
)
}
crashInfo.isForegroundServiceDidNotStartInTime -> {
CrashAttentionCard(
title = stringResource(R.string.crash_foreground_service_timeout_title),
description = stringResource(R.string.crash_foreground_service_timeout_description),
emoji = crashInfo.emoji
)
}
else -> {
CrashAttentionCard(
title = stringResource(R.string.crash_title),
description = stringResource(R.string.crash_subtitle),
emoji = crashInfo.emoji
)
Spacer(modifier = Modifier.height(24.dp))
CrashActionButtons(
onCopyCrashInfo = copyCrashInfo,
onShareLogs = component::shareLogs,
isSendingLogs = component.isSendingLogs,
githubLink = crashInfo.githubLink
)
}
}
Spacer(modifier = Modifier.height(24.dp))
CrashInfoCard(crashInfo = crashInfo)
}
CrashBottomButtons(
modifier = Modifier.align(Alignment.BottomCenter),
onCopy = copyCrashInfo.takeIf { !crashInfo.isNonReportable },
onRestartApp = {
context.startActivity(
Intent(context, AppActivityClass)
)
}
)
}
}
@@ -0,0 +1,83 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.presentation.components
import android.content.Context
import android.content.Intent
import com.t8rin.imagetoolbox.core.crash.di.CrashModule
import com.t8rin.imagetoolbox.core.crash.presentation.CrashActivity
import com.t8rin.imagetoolbox.core.domain.remote.AnalyticsManager
import com.t8rin.imagetoolbox.core.utils.makeLog
import kotlin.system.exitProcess
private class GlobalExceptionHandler<T : CrashHandler> private constructor(
private val applicationContext: Context,
private val defaultHandler: Thread.UncaughtExceptionHandler?,
private val activityToBeLaunched: Class<T>
) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(
p0: Thread,
p1: Throwable
) {
sendReport(p1)
runCatching {
p1.makeLog("FATAL_EXCEPTION")
applicationContext.launchActivity(activityToBeLaunched, p1)
exitProcess(0)
}.getOrElse {
defaultHandler?.uncaughtException(p0, p1)
}
}
private fun Context.launchActivity(
activity: Class<*>,
throwable: Throwable
) = applicationContext.startActivity(
Intent(applicationContext, activity).putExtra(
CrashHandler.EXCEPTION_EXTRA,
CrashHandler.getCrashInfoAsExtra(throwable)
).addFlags(defFlags)
)
companion object : AnalyticsManager by CrashModule.analyticsManager() {
fun <T : CrashHandler> initialize(
applicationContext: Context,
activityToBeLaunched: Class<T>,
) = Thread.setDefaultUncaughtExceptionHandler(
GlobalExceptionHandler(
applicationContext = applicationContext,
defaultHandler = Thread.getDefaultUncaughtExceptionHandler()!!,
activityToBeLaunched = activityToBeLaunched
)
)
}
}
private const val defFlags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
fun Context.applyGlobalExceptionHandler() = GlobalExceptionHandler.initialize(
applicationContext = applicationContext,
activityToBeLaunched = CrashActivity::class.java,
)
@@ -0,0 +1,85 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.presentation.screenLogic
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import com.arkivanov.decompose.ComponentContext
import com.t8rin.imagetoolbox.core.crash.presentation.components.CrashInfo
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
import com.t8rin.imagetoolbox.core.domain.image.ShareProvider
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
import com.t8rin.imagetoolbox.core.settings.domain.SettingsManager
import com.t8rin.imagetoolbox.core.settings.domain.model.SettingsState
import com.t8rin.imagetoolbox.core.ui.utils.BaseComponent
import com.t8rin.imagetoolbox.core.ui.utils.state.update
import com.t8rin.imagetoolbox.core.utils.makeLog
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
class CrashComponent @AssistedInject internal constructor(
@Assisted componentContext: ComponentContext,
@Assisted val crashInfo: CrashInfo,
private val settingsManager: SettingsManager,
private val shareProvider: ShareProvider,
dispatchersHolder: DispatchersHolder
) : BaseComponent(dispatchersHolder, componentContext) {
private val _settingsState = mutableStateOf(SettingsState.Default)
val settingsState: SettingsState by _settingsState
private val _isSendingLogs = mutableStateOf(false)
val isSendingLogs by _isSendingLogs
init {
runBlocking {
_settingsState.value = settingsManager.getSettingsState()
}
settingsManager.settingsState.onEach {
_settingsState.value = it
}.launchIn(componentScope)
crashInfo.makeLog("CrashComponent")
}
fun shareLogs() {
componentScope.launch {
_isSendingLogs.update { true }
runSuspendCatching {
shareProvider.shareUri(
uri = settingsManager.createLogsExport(),
onComplete = {}
)
}
_isSendingLogs.update { false }
}
}
@AssistedFactory
fun interface Factory {
operator fun invoke(
componentContext: ComponentContext,
crashInfo: CrashInfo
): CrashComponent
}
}
@@ -0,0 +1,87 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.crash.data
import com.google.firebase.Firebase
import com.google.firebase.analytics.FirebaseAnalytics.Event
import com.google.firebase.analytics.FirebaseAnalytics.Param
import com.google.firebase.analytics.analytics
import com.google.firebase.analytics.logEvent
import com.google.firebase.crashlytics.crashlytics
import com.t8rin.imagetoolbox.core.domain.remote.AnalyticsManager
import com.t8rin.imagetoolbox.core.ui.utils.helper.DeviceInfo.Companion.get
internal object AnalyticsManagerImpl : AnalyticsManager {
override var allowCollectCrashlytics: Boolean = false
override var allowCollectAnalytics: Boolean = false
override fun updateAnalyticsCollectionEnabled(value: Boolean) {
analytics.setAnalyticsCollectionEnabled(value)
allowCollectAnalytics = value
}
override fun updateAllowCollectCrashlytics(value: Boolean) {
crashlytics.isCrashlyticsCollectionEnabled = value
allowCollectCrashlytics = value
if (value) {
crashlytics.sendUnsentReports()
}
}
override fun sendReport(throwable: Throwable) {
if (allowCollectCrashlytics) {
crashlytics.apply {
recordException(throwable)
sendUnsentReports()
}
}
}
override fun registerScreenOpen(screenName: String) {
if (allowCollectAnalytics) {
analytics.apply {
logEvent(Event.SELECT_CONTENT) { param(Param.CONTENT_TYPE, screenName) }
logEvent(screenName) { param(Param.CONTENT, deviceInfo()) }
}
}
}
override fun pushMetric(
tag: String,
metric: String
) {
if (allowCollectAnalytics) {
analytics.logEvent(tag) { param(Param.CONTENT, metric) }
}
}
private fun deviceInfo(): String {
val info = get()
return listOf(
"Device: ${info.device}",
"App Version: ${info.appVersion}"
).joinToString(",")
}
private val analytics get() = Firebase.analytics
private val crashlytics get() = Firebase.crashlytics
}
+1
View File
@@ -0,0 +1 @@
/build
+74
View File
@@ -0,0 +1,74 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
plugins {
alias(libs.plugins.image.toolbox.library)
alias(libs.plugins.image.toolbox.hilt)
}
android.namespace = "com.t8rin.imagetoolbox.core.data"
dependencies {
api(libs.coil)
implementation(libs.coilNetwork)
api(libs.ktor)
api(libs.ktor.logging)
implementation(libs.coilGif)
implementation(libs.coilSvg)
implementation(libs.coil.resvg)
implementation(libs.trickle)
implementation(libs.androidx.compose.ui.graphics)
api(libs.datastore.preferences.android)
api(libs.datastore.core.android)
implementation(libs.avif.coder)
implementation(libs.jxl.coder.coil) {
exclude(module = "com.github.awxkee:jxl-coder")
}
implementation(libs.jxl.coder)
implementation(libs.aire)
implementation(libs.jpegli.coder)
implementation(libs.moshi)
implementation(libs.moshi.adapters)
api(libs.androidx.documentfile)
implementation(libs.toolbox.gifConverter)
implementation(libs.toolbox.exif)
implementation(libs.tiffdecoder)
implementation(libs.toolbox.qoiCoder)
implementation(libs.toolbox.jp2decoder)
implementation(libs.toolbox.awebp)
implementation(libs.toolbox.psd)
implementation(libs.toolbox.apng)
implementation(libs.toolbox.djvuCoder)
implementation(libs.pdfbox)
implementation(libs.trickle)
implementation(projects.core.domain)
implementation(projects.core.resources)
implementation(projects.core.filters)
implementation(projects.core.settings)
implementation(projects.core.di)
api(projects.core.utils)
}
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ ImageToolbox is an image editor for android
~ Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
~
~ You should have received a copy of the Apache License
~ along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<service
android:name=".saving.KeepAliveForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="true" />
</application>
</manifest>
@@ -0,0 +1,68 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.coil
import android.util.Base64
import coil3.ImageLoader
import coil3.Uri
import coil3.decode.DataSource
import coil3.decode.ImageSource
import coil3.fetch.FetchResult
import coil3.fetch.Fetcher
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import com.t8rin.imagetoolbox.core.domain.utils.isBase64
import com.t8rin.imagetoolbox.core.domain.utils.trimToBase64
import okio.Buffer
internal class Base64Fetcher(
private val options: Options,
private val base64: String
) : Fetcher {
override suspend fun fetch(): FetchResult? {
val byteArray = runCatching {
Base64.decode(base64, Base64.DEFAULT)
}.getOrNull() ?: return null
return SourceFetchResult(
source = ImageSource(
source = Buffer().apply { write(byteArray) },
fileSystem = options.fileSystem,
),
mimeType = null,
dataSource = DataSource.MEMORY,
)
}
class Factory : Fetcher.Factory<Uri> {
override fun create(
data: Uri,
options: Options,
imageLoader: ImageLoader,
): Fetcher? {
val stripped = data.toString().trimToBase64()
return if (stripped.isBase64()) {
Base64Fetcher(
options = options,
base64 = stripped
)
} else null
}
}
}
@@ -0,0 +1,65 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.coil
import coil3.util.DebugLogger
import coil3.util.Logger
import com.t8rin.imagetoolbox.core.utils.makeLog
import com.t8rin.imagetoolbox.core.utils.Logger as RealLogger
internal class CoilLogger : Logger {
private val delegate = DebugLogger()
override var minLevel: Logger.Level
get() = delegate.minLevel
set(value) {
delegate.minLevel = value
}
override fun log(
tag: String,
level: Logger.Level,
message: String?,
throwable: Throwable?
) {
message?.takeIf {
"NullRequestData" !in it && "PDF" !in it
}?.makeLog(tag, level.toLogger())
throwable?.takeIf {
"The request's data is null" !in it.message.orEmpty() && "PDF" !in it.message.orEmpty()
}?.makeLog(tag)
delegate.log(
tag = tag,
level = level,
message = message,
throwable = throwable
)
}
private fun Logger.Level.toLogger(): RealLogger.Level = when (this) {
Logger.Level.Verbose -> RealLogger.Level.Verbose
Logger.Level.Debug -> RealLogger.Level.Debug
Logger.Level.Info -> RealLogger.Level.Info
Logger.Level.Warn -> RealLogger.Level.Warn
Logger.Level.Error -> RealLogger.Level.Error
}
}
@@ -0,0 +1,143 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.coil
import android.graphics.Bitmap
import android.os.Build
import coil3.ImageLoader
import coil3.asImage
import coil3.decode.DecodeResult
import coil3.decode.Decoder
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.request.allowHardware
import coil3.request.allowRgb565
import coil3.request.bitmapConfig
import coil3.size.Scale
import coil3.size.Size
import coil3.size.pxOrElse
import com.radzivon.bartoshyk.avif.coder.Coder
import com.radzivon.bartoshyk.avif.coder.PreferredColorConfig
import com.radzivon.bartoshyk.avif.coder.ScaleMode
import com.t8rin.imagetoolbox.core.utils.makeLog
import kotlinx.coroutines.runInterruptible
import okio.ByteString.Companion.encodeUtf8
class HeifDecoder(
private val source: SourceFetchResult,
private val options: Options,
private val exceptionLogger: ((Exception) -> Unit)? = null,
) : Decoder {
private val coder = Coder()
override suspend fun decode(): DecodeResult? = runInterruptible {
try {
// ColorSpace is preferred to be ignored due to lib is trying to handle all color profile by itself
val sourceData = source.source.source().readByteArray()
var mPreferredColorConfig: PreferredColorConfig = when (options.bitmapConfig) {
Bitmap.Config.ALPHA_8 -> PreferredColorConfig.RGBA_8888
Bitmap.Config.RGB_565 -> if (options.allowRgb565) PreferredColorConfig.RGB_565 else PreferredColorConfig.DEFAULT
Bitmap.Config.ARGB_8888 -> PreferredColorConfig.RGBA_8888
else -> {
if (options.allowHardware) {
PreferredColorConfig.DEFAULT
} else {
PreferredColorConfig.RGBA_8888
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && options.bitmapConfig == Bitmap.Config.RGBA_F16) {
mPreferredColorConfig = PreferredColorConfig.RGBA_F16
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && options.bitmapConfig == Bitmap.Config.HARDWARE) {
mPreferredColorConfig = PreferredColorConfig.HARDWARE
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && options.bitmapConfig == Bitmap.Config.RGBA_1010102) {
mPreferredColorConfig = PreferredColorConfig.RGBA_1010102
}
if (options.size == Size.ORIGINAL) {
val originalImage = coder.decode(
byteArray = sourceData,
preferredColorConfig = mPreferredColorConfig
)
return@runInterruptible DecodeResult(
image = originalImage.asImage(),
isSampled = false
)
}
val dstWidth = options.size.width.pxOrElse { 0 }
val dstHeight = options.size.height.pxOrElse { 0 }
val scaleMode = when (options.scale) {
Scale.FILL -> ScaleMode.FILL
Scale.FIT -> ScaleMode.FIT
}
val originalImage = coder.decodeSampled(
byteArray = sourceData,
scaledWidth = dstWidth,
scaledHeight = dstHeight,
preferredColorConfig = mPreferredColorConfig,
scaleMode = scaleMode,
)
return@runInterruptible DecodeResult(
image = originalImage.asImage(),
isSampled = true
)
} catch (e: Exception) {
e.makeLog("HeifDecoder")
exceptionLogger?.invoke(e)
return@runInterruptible null
}
}
class Factory : Decoder.Factory {
override fun create(
result: SourceFetchResult,
options: Options,
imageLoader: ImageLoader
): Decoder? {
return if (
AVAILABLE_BRANDS.any {
result.source.source().rangeEquals(4, it)
}
) {
HeifDecoder(
source = result,
options = options
)
} else null
}
companion object {
private val MIF = "ftypmif1".encodeUtf8()
private val MSF = "ftypmsf1".encodeUtf8()
private val HEIC = "ftypheic".encodeUtf8()
private val HEIX = "ftypheix".encodeUtf8()
private val HEVC = "ftyphevc".encodeUtf8()
private val HEVX = "ftyphevx".encodeUtf8()
private val AVIF = "ftypavif".encodeUtf8()
private val AVIS = "ftypavis".encodeUtf8()
private val AVAILABLE_BRANDS = listOf(MIF, MSF, HEIC, HEIX, HEVC, HEVX, AVIF, AVIS)
}
}
}
@@ -0,0 +1,26 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.coil
import coil3.memory.MemoryCache
fun MemoryCache.remove(key: String) = keys.filter {
it.key == key
}.ifEmpty {
listOf(MemoryCache.Key(key))
}.all(::remove)
@@ -0,0 +1,462 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
@file:Suppress("MagicNumber", "ReturnCount")
package com.t8rin.imagetoolbox.core.data.coil
import android.graphics.Bitmap
import android.graphics.Matrix
import coil3.ImageLoader
import coil3.asImage
import coil3.decode.BitmapFactoryDecoder
import coil3.decode.DecodeResult
import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.toBitmap
import okio.Buffer
import okio.BufferedSource
import okio.ByteString.Companion.toByteString
import java.io.File
import java.io.RandomAccessFile
import java.util.Locale
internal class NefDecoder private constructor(
private val source: ImageSource,
private val options: Options
) : Decoder {
override suspend fun decode(): DecodeResult? {
val preview = runCatching {
NefPreviewExtractor(
file = source.file().toFile()
).extract()
}.getOrNull() ?: return null
val result = BitmapFactoryDecoder(
source = ImageSource(
source = Buffer().write(preview.bytes),
fileSystem = options.fileSystem
),
options = options
).decode()
return result.applyOrientation(preview.orientation)
}
private fun DecodeResult.applyOrientation(
orientation: Int
): DecodeResult {
if (orientation == ORIENTATION_NORMAL) return this
val bitmap = image.toBitmap()
val matrix = Matrix().applyTiffOrientation(orientation) ?: return this
val rotated = Bitmap.createBitmap(
bitmap,
0,
0,
bitmap.width,
bitmap.height,
matrix,
true
)
if (rotated != bitmap) bitmap.recycle()
return DecodeResult(
image = rotated.asImage(),
isSampled = isSampled
)
}
private fun Matrix.applyTiffOrientation(
orientation: Int
): Matrix? {
val transformation = TIFF_ORIENTATION_TRANSFORMS[orientation] ?: return null
transformation(this)
return this
}
class Factory : Decoder.Factory {
override fun create(
result: SourceFetchResult,
options: Options,
imageLoader: ImageLoader
): Decoder? {
return if (isTiff(result.source.source())) {
NefDecoder(
source = result.source,
options = options
)
} else {
null
}
}
private fun isTiff(source: BufferedSource): Boolean {
val littleEndianMagic = byteArrayOf(0x49, 0x49, 0x2a, 0x00)
val bigEndianMagic = byteArrayOf(0x4d, 0x4d, 0x00, 0x2a)
return source.rangeEquals(0, littleEndianMagic.toByteString()) ||
source.rangeEquals(0, bigEndianMagic.toByteString())
}
}
private class NefPreviewExtractor(
private val file: File
) {
fun extract(): PreviewData? = RandomAccessFile(file, "r").use { raf ->
val reader = TiffReader.create(raf) ?: return null
val rootDirectory = reader.readDirectory(reader.firstDirectoryOffset) ?: return null
val make = rootDirectory.asciiValue(TAG_MAKE, reader)
?.uppercase(Locale.ROOT)
.orEmpty()
if (!make.contains("NIKON")) return null
val subIfdOffsets = rootDirectory.longValues(TAG_SUB_IFD, reader)
if (subIfdOffsets.isEmpty()) return null
val subIfds = subIfdOffsets.mapNotNull(reader::readDirectory)
if (!subIfds.any { it.isNikonRawImage(reader) }) return null
val preview = subIfds
.mapNotNull { it.jpegPreview(reader) }
.filter(reader::isJpeg)
.maxByOrNull(JpegPreview::length)
?: return null
val bytes = reader.readBytes(
offset = preview.offset,
byteCount = preview.length
) ?: return null
PreviewData(
bytes = bytes,
orientation = rootDirectory.longValue(TAG_ORIENTATION, reader)?.toInt()
?: ORIENTATION_NORMAL
)
}
}
private class TiffReader private constructor(
private val raf: RandomAccessFile,
private val byteOrder: ByteOrder,
val firstDirectoryOffset: Long
) {
fun readDirectory(offset: Long): TiffDirectory? {
if (offset <= 0 || offset >= raf.length()) return null
raf.seek(offset)
val entryCount = readUShort()
if (entryCount > MAX_IFD_ENTRIES) return null
val entries = buildMap {
repeat(entryCount) {
val bytes = ByteArray(IFD_ENTRY_SIZE)
raf.readFully(bytes)
val entry = TiffEntry(
tag = readUShort(bytes, offset = 0),
type = readUShort(bytes, offset = 2),
count = readUInt(bytes, offset = 4),
valueOrOffset = readUInt(bytes, offset = 8),
inlineValue = bytes.copyOfRange(8, 12)
)
put(entry.tag, entry)
}
}
return TiffDirectory(entries)
}
fun values(entry: TiffEntry): ByteArray? {
val typeSize = TYPE_SIZES[entry.type] ?: return null
val byteCount = entry.count * typeSize
if (byteCount <= 0 || byteCount > Int.MAX_VALUE) return null
if (byteCount <= INLINE_VALUE_SIZE) {
return entry.inlineValue.copyOf(byteCount.toInt())
}
return readBytes(
offset = entry.valueOrOffset,
byteCount = byteCount
)
}
fun readBytes(
offset: Long,
byteCount: Long
): ByteArray? {
if (offset < 0 || byteCount <= 0 || byteCount > Int.MAX_VALUE) return null
if (offset + byteCount > raf.length()) return null
val bytes = ByteArray(byteCount.toInt())
raf.seek(offset)
raf.readFully(bytes)
return bytes
}
fun isJpeg(preview: JpegPreview): Boolean {
val bytes = readBytes(
offset = preview.offset,
byteCount = JPEG_SIGNATURE_SIZE
) ?: return false
return bytes[0] == JPEG_START_BYTE_0 && bytes[1] == JPEG_START_BYTE_1
}
fun readUShort(bytes: ByteArray, offset: Int): Int {
val first = bytes[offset].toInt() and 0xff
val second = bytes[offset + 1].toInt() and 0xff
return when (byteOrder) {
ByteOrder.LittleEndian -> first or (second shl 8)
ByteOrder.BigEndian -> (first shl 8) or second
}
}
fun readUInt(bytes: ByteArray, offset: Int): Long {
val first = bytes[offset].toLong() and 0xff
val second = bytes[offset + 1].toLong() and 0xff
val third = bytes[offset + 2].toLong() and 0xff
val fourth = bytes[offset + 3].toLong() and 0xff
return when (byteOrder) {
ByteOrder.LittleEndian -> first or (second shl 8) or (third shl 16) or (fourth shl 24)
ByteOrder.BigEndian -> (first shl 24) or (second shl 16) or (third shl 8) or fourth
}
}
private fun readUShort(): Int {
val bytes = ByteArray(2)
raf.readFully(bytes)
return readUShort(bytes, offset = 0)
}
companion object {
fun create(raf: RandomAccessFile): TiffReader? {
if (raf.length() < TIFF_HEADER_SIZE) return null
raf.seek(0)
val header = ByteArray(TIFF_HEADER_SIZE)
raf.readFully(header)
val byteOrder = when {
header[0] == LITTLE_ENDIAN_BYTE && header[1] == LITTLE_ENDIAN_BYTE -> {
ByteOrder.LittleEndian
}
header[0] == BIG_ENDIAN_BYTE && header[1] == BIG_ENDIAN_BYTE -> {
ByteOrder.BigEndian
}
else -> return null
}
val reader = TiffReader(
raf = raf,
byteOrder = byteOrder,
firstDirectoryOffset = 0
)
val magic = reader.readUShort(header, offset = 2)
if (magic != TIFF_MAGIC) return null
return TiffReader(
raf = raf,
byteOrder = byteOrder,
firstDirectoryOffset = reader.readUInt(header, offset = 4)
)
}
}
}
private data class TiffDirectory(
private val entries: Map<Int, TiffEntry>
) {
fun longValue(
tag: Int,
reader: TiffReader
): Long? = longValues(tag, reader).firstOrNull()
fun longValues(
tag: Int,
reader: TiffReader
): List<Long> {
val entry = entries[tag] ?: return emptyList()
val bytes = reader.values(entry) ?: return emptyList()
val count = entry.count.toIntOrNull() ?: return emptyList()
return when (entry.type) {
TYPE_BYTE -> List(count) { index ->
bytes[index].toLong() and 0xff
}
TYPE_SHORT -> List(count) { index ->
reader.readUShort(bytes, index * SHORT_SIZE).toLong()
}
TYPE_LONG -> List(count) { index ->
reader.readUInt(bytes, index * LONG_SIZE)
}
else -> emptyList()
}
}
fun asciiValue(
tag: Int,
reader: TiffReader
): String? {
val entry = entries[tag] ?: return null
if (entry.type != TYPE_ASCII) return null
val bytes = reader.values(entry) ?: return null
return String(bytes, Charsets.US_ASCII).substringBefore(NULL_CHAR)
}
fun isNikonRawImage(reader: TiffReader): Boolean {
val compression = longValue(TAG_COMPRESSION, reader)
val photometric = longValue(TAG_PHOTOMETRIC, reader)
val bitsPerSample = longValue(TAG_BITS_PER_SAMPLE, reader)
return compression == COMPRESSION_NIKON_RAW ||
(photometric == PHOTOMETRIC_CFA && bitsPerSample != null && bitsPerSample > 8)
}
fun jpegPreview(reader: TiffReader): JpegPreview? {
val offset = longValue(TAG_JPEG_INTERCHANGE_FORMAT, reader) ?: return null
val length = longValue(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, reader) ?: return null
if (length <= 0) return null
return JpegPreview(
offset = offset,
length = length
)
}
}
private class TiffEntry(
val tag: Int,
val type: Int,
val count: Long,
val valueOrOffset: Long,
val inlineValue: ByteArray
)
private data class JpegPreview(
val offset: Long,
val length: Long
)
private class PreviewData(
val bytes: ByteArray,
val orientation: Int
)
private enum class ByteOrder {
LittleEndian,
BigEndian
}
private companion object {
private const val TIFF_HEADER_SIZE = 8
private const val TIFF_MAGIC = 42
private const val IFD_ENTRY_SIZE = 12
private const val INLINE_VALUE_SIZE = 4
private const val MAX_IFD_ENTRIES = 512
private const val SHORT_SIZE = 2
private const val LONG_SIZE = 4
private const val JPEG_SIGNATURE_SIZE = 2L
private const val LITTLE_ENDIAN_BYTE = 0x49.toByte()
private const val BIG_ENDIAN_BYTE = 0x4d.toByte()
private const val JPEG_START_BYTE_0 = 0xff.toByte()
private const val JPEG_START_BYTE_1 = 0xd8.toByte()
private const val NULL_CHAR = '\u0000'
private const val TYPE_BYTE = 1
private const val TYPE_ASCII = 2
private const val TYPE_SHORT = 3
private const val TYPE_LONG = 4
private const val TAG_SUB_IFD = 330
private const val TAG_MAKE = 271
private const val TAG_ORIENTATION = 274
private const val TAG_BITS_PER_SAMPLE = 258
private const val TAG_COMPRESSION = 259
private const val TAG_PHOTOMETRIC = 262
private const val TAG_JPEG_INTERCHANGE_FORMAT = 513
private const val TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514
private const val COMPRESSION_NIKON_RAW = 34713L
private const val PHOTOMETRIC_CFA = 32803L
private const val ORIENTATION_NORMAL = 1
private const val ORIENTATION_FLIP_HORIZONTAL = 2
private const val ORIENTATION_ROTATE_180 = 3
private const val ORIENTATION_FLIP_VERTICAL = 4
private const val ORIENTATION_TRANSPOSE = 5
private const val ORIENTATION_ROTATE_90 = 6
private const val ORIENTATION_TRANSVERSE = 7
private const val ORIENTATION_ROTATE_270 = 8
private val TIFF_ORIENTATION_TRANSFORMS = mapOf<Int, Matrix.() -> Unit>(
ORIENTATION_FLIP_HORIZONTAL to {
postScale(-1f, 1f)
},
ORIENTATION_ROTATE_180 to {
postRotate(180f)
},
ORIENTATION_FLIP_VERTICAL to {
postScale(1f, -1f)
},
ORIENTATION_TRANSPOSE to {
postRotate(90f)
postScale(1f, -1f)
},
ORIENTATION_ROTATE_90 to {
postRotate(90f)
},
ORIENTATION_TRANSVERSE to {
postRotate(90f)
postScale(-1f, 1f)
},
ORIENTATION_ROTATE_270 to {
postRotate(270f)
}
)
private val TYPE_SIZES = mapOf(
TYPE_BYTE to 1L,
TYPE_ASCII to 1L,
TYPE_SHORT to SHORT_SIZE.toLong(),
TYPE_LONG to LONG_SIZE.toLong()
)
private fun Long.toIntOrNull(): Int? = takeIf {
it in 0..Int.MAX_VALUE.toLong()
}?.toInt()
}
}
@@ -0,0 +1,38 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.coil
import coil3.map.Mapper
import coil3.request.Options
import coil3.toUri
import com.t8rin.imagetoolbox.core.utils.UriReplacements
internal object OriginalUriMapper : Mapper<Any, Any> {
override fun map(
data: Any,
options: Options
): Any? = when (data) {
is String -> UriReplacements.resolve(data).takeIf { it != data }
is android.net.Uri -> UriReplacements.resolve(data).takeIf { it != data }
is coil3.Uri -> UriReplacements.resolve(data.toString())
.takeIf { it != data.toString() }
?.toUri()
else -> null
}
}
@@ -0,0 +1,152 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
@file:Suppress("FunctionName", "unused")
package com.t8rin.imagetoolbox.core.data.coil
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor
import androidx.core.graphics.createBitmap
import coil3.Extras
import coil3.ImageLoader
import coil3.asImage
import coil3.decode.DecodeResult
import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.getExtra
import coil3.request.ImageRequest
import coil3.request.Options
import coil3.size.Size
import coil3.size.pxOrElse
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
import com.t8rin.imagetoolbox.core.domain.model.flexibleResize
import com.t8rin.imagetoolbox.core.utils.appContext
import okio.ByteString.Companion.toByteString
import kotlin.math.roundToInt
internal class PdfDecoder(
private val source: ImageSource,
private val options: Options,
) : Decoder {
override suspend fun decode(): DecodeResult {
val file = source.file().toFile()
val image = ParcelFileDescriptor.open(
file,
ParcelFileDescriptor.MODE_READ_ONLY
).use { fileDescriptor ->
PdfRenderer(fileDescriptor).use { renderer ->
val pageIndex = options.pdfPage.coerceIn(0, renderer.pageCount - 1)
renderer.openPage(pageIndex).use { page ->
val originalWidth = page.width
val originalHeight = page.height
val targetSize = IntegerSize(
width = originalWidth,
height = originalHeight
).flexibleResize(
w = options.size.width.pxOrElse { 0 },
h = options.size.height.pxOrElse { 0 }
)
val scaleX = targetSize.width.toFloat() / originalWidth
val scaleY = targetSize.height.toFloat() / originalHeight
val scale = minOf(scaleX, scaleY).coerceAtMost(2f)
val bitmap = createBitmap(
(originalWidth * scale).roundToInt().coerceAtLeast(1),
(originalHeight * scale).roundToInt().coerceAtLeast(1)
).apply {
eraseColor(Color.WHITE)
}
page.render(
bitmap,
null,
Matrix().apply { setScale(scale, scale) },
PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY
)
bitmap.asImage()
}
}
}
return DecodeResult(
image = image,
isSampled = true
)
}
class Factory : Decoder.Factory {
override fun create(
result: SourceFetchResult,
options: Options,
imageLoader: ImageLoader
): Decoder? {
return if (isPdf(result)) {
PdfDecoder(
source = result.source,
options = options
)
} else null
}
private fun isPdf(result: SourceFetchResult): Boolean {
val pdfMagic = byteArrayOf(0x25, 0x50, 0x44, 0x46).toByteString()
return result.source.source()
.rangeEquals(0, pdfMagic) || result.mimeType == "application/pdf"
}
}
}
fun PdfImageRequest(
data: Any?,
pdfPage: Int = 0,
size: Size? = null
): ImageRequest = ImageRequest.Builder(appContext)
.data(data)
.pdfPage(pdfPage)
.memoryCacheKey(data.toString() + pdfPage)
.diskCacheKey(data.toString() + pdfPage)
.apply {
size?.let(::size)
}
.build()
fun ImageRequest.Builder.pdfPage(pdfPage: Int) = apply {
extras[pdfPageKey] = pdfPage
}
val ImageRequest.pdfPage: Int
get() = getExtra(pdfPageKey)
val Options.pdfPage: Int
get() = getExtra(pdfPageKey)
val Extras.Key.Companion.pdfPage: Extras.Key<Int>
get() = pdfPageKey
private val pdfPageKey = Extras.Key(default = 0)
@@ -0,0 +1,114 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.coil
import coil3.ImageLoader
import coil3.decode.DecodeResult
import coil3.decode.DecodeUtils
import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.size.Size
import coil3.size.pxOrElse
import coil3.svg.SvgDecoder
import coil3.svg.isSvg
import com.hashsequence.coilresvg.ResvgDecoder
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
import com.t8rin.imagetoolbox.core.utils.makeLog
internal class SvgDecoderCompat(
private val source: ImageSource,
options: Options,
minimumSize: Int? = null
) : Decoder {
private val options = minimumSize?.let {
options.copy(
size = options.size.coerceAtLeast(minimumSize)
)
} ?: options
private fun source() = ImageSource(
file = source.file(),
fileSystem = source.fileSystem,
metadata = source.metadata
)
override suspend fun decode(): DecodeResult {
if (!isResvgAvailable) return decodeDefault()
return runSuspendCatching {
decodeNative()
}.onFailure {
if (it is LinkageError) isResvgAvailable = false
}.getOrNull() ?: decodeDefault()
}
private suspend fun decodeNative() = ResvgDecoder(
source = source(),
options = options
).decode()
private suspend fun decodeDefault() = SvgDecoder(
source = source(),
options = options
).decode().also {
"fallback coil-svg decoder".makeLog()
}
private fun Size.coerceAtLeast(size: Int): Size = Size(
width = width.pxOrElse { 0 }.coerceAtLeast(size),
height = height.pxOrElse { 0 }.coerceAtLeast(size)
)
class Factory(
private val minimumSize: Int? = null
) : Decoder.Factory {
override fun create(
result: SourceFetchResult,
options: Options,
imageLoader: ImageLoader
): Decoder? {
if (!isApplicable(result)) return null
return SvgDecoderCompat(
source = result.source,
options = options,
minimumSize = minimumSize
)
}
private fun isApplicable(result: SourceFetchResult): Boolean {
return result.mimeType == MIME_TYPE_SVG ||
result.mimeType == MIME_TYPE_XML ||
DecodeUtils.isSvg(result.source.source())
}
private companion object {
private const val MIME_TYPE_SVG = "image/svg+xml"
private const val MIME_TYPE_XML = "text/xml"
}
}
private companion object {
@Volatile
private var isResvgAvailable = true
}
}
@@ -0,0 +1,105 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.coil
import android.graphics.Bitmap
import android.os.Build
import coil3.ImageLoader
import coil3.asImage
import coil3.decode.DecodeResult
import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.request.bitmapConfig
import coil3.size.Size
import coil3.size.pxOrElse
import okio.BufferedSource
import okio.ByteString.Companion.toByteString
import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import oupson.apng.utils.Utils.flexibleResize
internal class TiffDecoder private constructor(
private val source: ImageSource,
private val options: Options
) : Decoder {
@Suppress("DEPRECATION")
override suspend fun decode(): DecodeResult? {
val config = options.bitmapConfig.takeIf {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
it != Bitmap.Config.HARDWARE
} else true
} ?: Bitmap.Config.ARGB_8888
val decoded = runCatching {
TiffBitmapFactory.decodeFile(
source.file().toFile()
)
}.getOrNull() ?: return null
val image = decoded
.createScaledBitmap(options.size)
.copy(config, false)
.asImage()
return DecodeResult(
image = image,
isSampled = options.size != Size.ORIGINAL
)
}
private fun Bitmap.createScaledBitmap(
size: Size
): Bitmap {
if (size == Size.ORIGINAL) return this
return flexibleResize(
maxOf(
size.width.pxOrElse { 1 },
size.height.pxOrElse { 1 }
)
)
}
class Factory : Decoder.Factory {
override fun create(
result: SourceFetchResult,
options: Options,
imageLoader: ImageLoader
): Decoder? {
return if (isTiff(result.source.source())) {
TiffDecoder(
source = result.source,
options = options
)
} else null
}
private fun isTiff(source: BufferedSource): Boolean {
val magic1 = byteArrayOf(0x49, 0x49, 0x2a, 0x00)
val magic2 = byteArrayOf(0x4d, 0x4d, 0x00, 0x2a)
val cr2Magic = byteArrayOf(0x49, 0x49, 0x2a, 0x00, 0x10, 0x00, 0x00, 0x00, 0x43, 0x52)
if (source.rangeEquals(0, cr2Magic.toByteString())) return false
if (source.rangeEquals(0, magic1.toByteString())) return true
return source.rangeEquals(0, magic2.toByteString())
}
}
}
@@ -0,0 +1,49 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.coil
import coil3.intercept.Interceptor
import coil3.request.ImageResult
import coil3.request.transformations
import com.t8rin.imagetoolbox.core.utils.makeLog
internal object TimeMeasureInterceptor : Interceptor {
override suspend fun intercept(
chain: Interceptor.Chain
): ImageResult {
val time = System.currentTimeMillis()
val result = chain.proceed()
val endTime = System.currentTimeMillis()
val delta = endTime - time
val transformations = chain.request.transformations.joinToString(", ") {
it.toString()
}
if (transformations.isNotEmpty()) {
"Time $delta ms for transformations = $transformations, with ${result.request.sizeResolver.size()}".makeLog(
"RealImageLoader"
)
}
"Time $delta ms for ${chain.size}".makeLog("RealImageLoader")
return result
}
}
@@ -0,0 +1,103 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.coil
import coil3.ImageLoader
import coil3.asImage
import coil3.decode.DecodeResult
import coil3.decode.Decoder
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.size.Scale
import coil3.size.Size
import coil3.size.pxOrElse
import com.t8rin.trickle.VvcDecoder
import com.t8rin.trickle.VvcScaleMode
import kotlinx.coroutines.runInterruptible
import okio.ByteString.Companion.encodeUtf8
import okio.ByteString.Companion.toByteString
class VVCDecoder(
private val source: SourceFetchResult,
private val options: Options,
private val exceptionLogger: ((Exception) -> Unit)? = null
) : Decoder {
override suspend fun decode(): DecodeResult? = runInterruptible {
try {
val sourceData = source.source.source().readByteArray()
if (options.size == Size.ORIGINAL) {
return@runInterruptible DecodeResult(
image = VvcDecoder.decode(sourceData).asImage(),
isSampled = false
)
}
val width = options.size.width.pxOrElse { 0 }
val height = options.size.height.pxOrElse { 0 }
val scaleMode = when (options.scale) {
Scale.FILL -> VvcScaleMode.FILL
Scale.FIT -> VvcScaleMode.FIT
}
DecodeResult(
image = VvcDecoder.decodeSampled(
encoded = sourceData,
scaledWidth = width,
scaledHeight = height,
scaleMode = scaleMode
).asImage(),
isSampled = true
)
} catch (e: Exception) {
exceptionLogger?.invoke(e)
null
}
}
class Factory : Decoder.Factory {
override fun create(
result: SourceFetchResult,
options: Options,
imageLoader: ImageLoader
): Decoder? {
val source = result.source.source()
val isRawVvc = RAW_VVC_START_CODES.any { source.rangeEquals(0, it) }
val isVvcInHeif = source.rangeEquals(4, FTYP) &&
source.indexOf(VVC_ITEM_TYPE, 0, SEARCH_LIMIT) >= 0
return if (isRawVvc || isVvcInHeif) {
VVCDecoder(
source = result,
options = options
)
} else null
}
private companion object {
const val SEARCH_LIMIT = 64L * 1024L
val FTYP = "ftyp".encodeUtf8()
val VVC_ITEM_TYPE = "vvc1".encodeUtf8()
val RAW_VVC_START_CODES = listOf(
byteArrayOf(0, 0, 1).toByteString(),
byteArrayOf(0, 0, 0, 1).toByteString()
)
}
}
}
@@ -0,0 +1,35 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.coroutines
import com.t8rin.imagetoolbox.core.di.DecodingDispatcher
import com.t8rin.imagetoolbox.core.di.DefaultDispatcher
import com.t8rin.imagetoolbox.core.di.EncodingDispatcher
import com.t8rin.imagetoolbox.core.di.IoDispatcher
import com.t8rin.imagetoolbox.core.di.UiDispatcher
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
internal data class AndroidDispatchersHolder @Inject constructor(
@UiDispatcher override val uiDispatcher: CoroutineContext,
@IoDispatcher override val ioDispatcher: CoroutineContext,
@EncodingDispatcher override val encodingDispatcher: CoroutineContext,
@DecodingDispatcher override val decodingDispatcher: CoroutineContext,
@DefaultDispatcher override val defaultDispatcher: CoroutineContext
) : DispatchersHolder
@@ -0,0 +1,34 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.coroutines
import com.t8rin.imagetoolbox.core.domain.coroutines.AppScope
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
import com.t8rin.imagetoolbox.core.utils.makeLog
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.SupervisorJob
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
internal class AppScopeImpl @Inject constructor(
dispatchersHolder: DispatchersHolder
) : AppScope, DispatchersHolder by dispatchersHolder {
override val coroutineContext: CoroutineContext =
defaultDispatcher + CoroutineExceptionHandler { _, e -> e.makeLog("AppScopeImpl") } + SupervisorJob()
}
@@ -0,0 +1,94 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.di
import com.t8rin.imagetoolbox.core.data.coroutines.AndroidDispatchersHolder
import com.t8rin.imagetoolbox.core.data.coroutines.AppScopeImpl
import com.t8rin.imagetoolbox.core.data.utils.executorDispatcher
import com.t8rin.imagetoolbox.core.di.DecodingDispatcher
import com.t8rin.imagetoolbox.core.di.DefaultDispatcher
import com.t8rin.imagetoolbox.core.di.EncodingDispatcher
import com.t8rin.imagetoolbox.core.di.IoDispatcher
import com.t8rin.imagetoolbox.core.di.UiDispatcher
import com.t8rin.imagetoolbox.core.domain.coroutines.AppScope
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.Dispatchers
import java.util.concurrent.Executors
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
@Module
@InstallIn(SingletonComponent::class)
internal interface CoroutinesModule {
@Binds
@Singleton
fun dispatchersHolder(
dispatchers: AndroidDispatchersHolder
): DispatchersHolder
@Binds
@Singleton
fun appScope(
impl: AppScopeImpl
): AppScope
companion object {
@DefaultDispatcher
@Singleton
@Provides
fun defaultDispatcher(): CoroutineContext = executorDispatcher {
Executors.newCachedThreadPool()
}
@DecodingDispatcher
@Singleton
@Provides
fun decodingDispatcher(): CoroutineContext = executorDispatcher {
Executors.newFixedThreadPool(
2 * Runtime.getRuntime().availableProcessors() + 1
)
}
@EncodingDispatcher
@Singleton
@Provides
fun encodingDispatcher(): CoroutineContext = executorDispatcher {
Executors.newSingleThreadExecutor()
}
@IoDispatcher
@Singleton
@Provides
fun ioDispatcher(): CoroutineContext = Dispatchers.IO
@UiDispatcher
@Singleton
@Provides
fun uiDispatcher(): CoroutineContext = Dispatchers.Main.immediate
}
}
@@ -0,0 +1,138 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.di
import android.content.Context
import android.os.Build
import coil3.ComponentRegistry
import coil3.ImageLoader
import coil3.SingletonImageLoader
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.gif.AnimatedImageDecoder
import coil3.gif.GifDecoder
import coil3.imageLoader
import coil3.memory.MemoryCache
import coil3.network.DeDupeConcurrentRequestStrategy
import coil3.network.ktor3.KtorNetworkFetcherFactory
import coil3.request.allowHardware
import coil3.request.maxBitmapSize
import coil3.size.Size
import coil3.util.Logger
import com.awxkee.jxlcoder.coil.AnimatedJxlDecoder
import com.gemalto.jp2.coil.Jpeg2000Decoder
import com.t8rin.awebp.coil.AnimatedWebPDecoder
import com.t8rin.djvu_coder.coil.DjvuDecoder
import com.t8rin.imagetoolbox.core.data.coil.Base64Fetcher
import com.t8rin.imagetoolbox.core.data.coil.CoilLogger
import com.t8rin.imagetoolbox.core.data.coil.HeifDecoder
import com.t8rin.imagetoolbox.core.data.coil.NefDecoder
import com.t8rin.imagetoolbox.core.data.coil.OriginalUriMapper
import com.t8rin.imagetoolbox.core.data.coil.PdfDecoder
import com.t8rin.imagetoolbox.core.data.coil.SvgDecoderCompat
import com.t8rin.imagetoolbox.core.data.coil.TiffDecoder
import com.t8rin.imagetoolbox.core.data.coil.TimeMeasureInterceptor
import com.t8rin.imagetoolbox.core.data.coil.VVCDecoder
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
import com.t8rin.imagetoolbox.core.resources.BuildConfig
import com.t8rin.psd.coil.PsdDecoder
import com.t8rin.qoi_coder.coil.QoiDecoder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import oupson.apng.coil.AnimatedPngDecoder
import java.io.File
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ImageLoaderModule {
@Provides
@Singleton
fun provideImageLoader(
@ApplicationContext context: Context,
logger: Logger?,
componentRegistry: ComponentRegistry,
dispatchersHolder: DispatchersHolder
): ImageLoader = context.imageLoader.newBuilder()
.components(componentRegistry)
.coroutineContext(dispatchersHolder.defaultDispatcher)
.decoderCoroutineContext(dispatchersHolder.decodingDispatcher)
.fetcherCoroutineContext(dispatchersHolder.ioDispatcher)
.allowHardware(false)
.maxBitmapSize(Size.ORIGINAL)
.diskCache {
DiskCache.Builder()
.directory(File(context.cacheDir, "coil").apply(File::mkdirs))
.maxSizePercent(0.2)
.cleanupCoroutineContext(dispatchersHolder.ioDispatcher)
.build()
}
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(context, 0.3)
.build()
}
.logger(logger)
.build()
.also(SingletonImageLoader::setUnsafe)
@Provides
@Singleton
fun provideCoilLogger(): Logger = CoilLogger()
@Provides
@Singleton
fun provideComponentRegistry(
client: HttpClient
): ComponentRegistry = ComponentRegistry.Builder()
.apply {
add(OriginalUriMapper)
add(
KtorNetworkFetcherFactory(
httpClient = client,
concurrentRequestStrategy = DeDupeConcurrentRequestStrategy()
)
)
add(AnimatedPngDecoder.Factory())
if (Build.VERSION.SDK_INT >= 28) add(AnimatedImageDecoder.Factory())
else {
add(GifDecoder.Factory())
add(AnimatedWebPDecoder.Factory())
}
add(SvgDecoderCompat.Factory())
add(VVCDecoder.Factory())
add(HeifDecoder.Factory())
add(AnimatedJxlDecoder.Factory())
add(Jpeg2000Decoder.Factory())
add(NefDecoder.Factory())
add(TiffDecoder.Factory())
add(QoiDecoder.Factory())
add(PsdDecoder.Factory())
add(DjvuDecoder.Factory())
add(Base64Fetcher.Factory())
add(PdfDecoder.Factory())
if (BuildConfig.DEBUG) add(TimeMeasureInterceptor)
}
.build()
}
@@ -0,0 +1,94 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.di
import android.graphics.Bitmap
import com.t8rin.imagetoolbox.core.data.image.AndroidImageCompressor
import com.t8rin.imagetoolbox.core.data.image.AndroidImageGetter
import com.t8rin.imagetoolbox.core.data.image.AndroidImagePreviewCreator
import com.t8rin.imagetoolbox.core.data.image.AndroidImageScaler
import com.t8rin.imagetoolbox.core.data.image.AndroidImageTransformer
import com.t8rin.imagetoolbox.core.data.image.AndroidShareProvider
import com.t8rin.imagetoolbox.core.data.image.ImageExportProfilesUseCaseImpl
import com.t8rin.imagetoolbox.core.domain.image.ImageCompressor
import com.t8rin.imagetoolbox.core.domain.image.ImageExportProfilesUseCase
import com.t8rin.imagetoolbox.core.domain.image.ImageGetter
import com.t8rin.imagetoolbox.core.domain.image.ImagePreviewCreator
import com.t8rin.imagetoolbox.core.domain.image.ImageScaler
import com.t8rin.imagetoolbox.core.domain.image.ImageShareProvider
import com.t8rin.imagetoolbox.core.domain.image.ImageTransformer
import com.t8rin.imagetoolbox.core.domain.image.ShareProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ImageModule {
@Singleton
@Binds
fun provideImageManager(
transformer: AndroidImageTransformer
): ImageTransformer<Bitmap>
@Singleton
@Binds
fun provideImageScaler(
scaler: AndroidImageScaler
): ImageScaler<Bitmap>
@Singleton
@Binds
fun provideImageCompressor(
compressor: AndroidImageCompressor
): ImageCompressor<Bitmap>
@Singleton
@Binds
fun provideImageGetter(
getter: AndroidImageGetter
): ImageGetter<Bitmap>
@Singleton
@Binds
fun provideImagePreviewCreator(
creator: AndroidImagePreviewCreator
): ImagePreviewCreator<Bitmap>
@Singleton
@Binds
fun provideShareProvider(
provider: AndroidShareProvider
): ShareProvider
@Singleton
@Binds
fun provideImageShareProvider(
provider: AndroidShareProvider
): ImageShareProvider<Bitmap>
@Singleton
@Binds
fun provideImageExportProfilesUseCase(
useCase: ImageExportProfilesUseCaseImpl
): ImageExportProfilesUseCase
}
@@ -0,0 +1,94 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.di
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.t8rin.imagetoolbox.core.data.json.ImageFormatJsonAdapter
import com.t8rin.imagetoolbox.core.data.json.ImageScaleModeJsonAdapter
import com.t8rin.imagetoolbox.core.data.json.MoshiParser
import com.t8rin.imagetoolbox.core.data.json.PresetJsonAdapter
import com.t8rin.imagetoolbox.core.data.json.ResizeTypeJsonAdapter
import com.t8rin.imagetoolbox.core.domain.image.model.Quality
import com.t8rin.imagetoolbox.core.domain.json.JsonParser
import com.t8rin.imagetoolbox.core.settings.domain.model.FilenameBehavior
import com.t8rin.imagetoolbox.core.settings.domain.model.ShapeType
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface JsonModule {
@Binds
@Singleton
fun parser(
impl: MoshiParser,
): JsonParser
companion object {
@Provides
@Singleton
fun moshi(): Moshi = Moshi.Builder()
.add(
PolymorphicJsonAdapterFactory.of(Quality::class.java, "quality_type")
.withSubtype(Quality.Jxl::class.java, "jxl")
.withSubtype(Quality.Avif::class.java, "avif")
.withSubtype(Quality.Heic::class.java, "heic")
.withSubtype(Quality.Vvc::class.java, "vvc")
.withSubtype(Quality.PngLossy::class.java, "png")
.withSubtype(Quality.PngQuant::class.java, "pngquant")
.withSubtype(Quality.Tiff::class.java, "tiff")
.withSubtype(Quality.Base::class.java, "base")
.withDefaultValue(Quality.Base())
)
.add(
PolymorphicJsonAdapterFactory.of(ShapeType::class.java, "shape_type")
.withSubtype(ShapeType.Rounded::class.java, "rounded")
.withSubtype(ShapeType.Cut::class.java, "cut")
.withSubtype(ShapeType.Squircle::class.java, "squircle")
.withSubtype(ShapeType.Smooth::class.java, "smooth")
.withSubtype(ShapeType.Wavy::class.java, "wavy")
.withSubtype(ShapeType.Scoop::class.java, "scoop")
.withSubtype(ShapeType.Notch::class.java, "notch")
.withDefaultValue(ShapeType.Rounded())
)
.add(
PolymorphicJsonAdapterFactory.of(FilenameBehavior::class.java, "filename_type")
.withSubtype(FilenameBehavior.None::class.java, "none")
.withSubtype(FilenameBehavior.Overwrite::class.java, "overwrite")
.withSubtype(FilenameBehavior.Checksum::class.java, "checksum")
.withSubtype(FilenameBehavior.Random::class.java, "random")
.withDefaultValue(FilenameBehavior.None())
)
.add(ImageFormatJsonAdapter())
.add(PresetJsonAdapter())
.add(ResizeTypeJsonAdapter())
.add(ImageScaleModeJsonAdapter())
.addLast(KotlinJsonAdapterFactory())
.build()
}
}
@@ -0,0 +1,45 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStoreFile
import com.t8rin.imagetoolbox.core.domain.GLOBAL_STORAGE_NAME
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object LocalModule {
@Provides
@Singleton
fun dataStore(
@ApplicationContext context: Context
): DataStore<Preferences> = PreferenceDataStoreFactory.create(
produceFile = { context.preferencesDataStoreFile(GLOBAL_STORAGE_NAME) }
)
}
@@ -0,0 +1,76 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.di
import com.t8rin.imagetoolbox.core.data.remote.AndroidDownloadManager
import com.t8rin.imagetoolbox.core.data.remote.AndroidRemoteResourcesStore
import com.t8rin.imagetoolbox.core.domain.remote.DownloadManager
import com.t8rin.imagetoolbox.core.domain.remote.RemoteResourcesStore
import com.t8rin.imagetoolbox.core.utils.makeLog
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import javax.inject.Singleton
import kotlin.time.Duration.Companion.minutes
@Module
@InstallIn(SingletonComponent::class)
internal interface RemoteModule {
@Binds
@Singleton
fun remoteResources(
impl: AndroidRemoteResourcesStore
): RemoteResourcesStore
@Binds
@Singleton
fun downloadManager(
impl: AndroidDownloadManager
): DownloadManager
companion object {
@Provides
@Singleton
fun client(): HttpClient = HttpClient {
install(HttpTimeout) {
val timeout = 5.minutes.inWholeMilliseconds
requestTimeoutMillis = timeout
connectTimeoutMillis = timeout
socketTimeoutMillis = timeout
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
message.makeLog("Ktor")
}
}
level = LogLevel.HEADERS
}
}
}
}
@@ -0,0 +1,40 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2025 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.di
import com.t8rin.imagetoolbox.core.data.resource.AndroidResourceManager
import com.t8rin.imagetoolbox.core.domain.resource.ResourceManager
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ResourcesModule {
@Binds
@Singleton
fun resManager(
impl: AndroidResourceManager
): ResourceManager
}
@@ -0,0 +1,80 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.di
import com.t8rin.imagetoolbox.core.data.history.AppHistoryRepositoryImpl
import com.t8rin.imagetoolbox.core.data.saving.AndroidFileController
import com.t8rin.imagetoolbox.core.data.saving.AndroidFilenameCreator
import com.t8rin.imagetoolbox.core.data.saving.AndroidKeepAliveService
import com.t8rin.imagetoolbox.core.data.saving.FileControllerEventEmitter
import com.t8rin.imagetoolbox.core.data.saving.OriginalFileDeletionHelper
import com.t8rin.imagetoolbox.core.domain.history.AppHistoryRepository
import com.t8rin.imagetoolbox.core.domain.image.MetadataProvider
import com.t8rin.imagetoolbox.core.domain.saving.FileController
import com.t8rin.imagetoolbox.core.domain.saving.FileController.Companion.toMetadataProvider
import com.t8rin.imagetoolbox.core.domain.saving.FilenameCreator
import com.t8rin.imagetoolbox.core.domain.saving.KeepAliveService
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface SavingModule {
@Singleton
@Binds
fun provideFileController(
impl: AndroidFileController
): FileController
@Binds
fun provideFileControllerEventEmitter(
impl: OriginalFileDeletionHelper
): FileControllerEventEmitter
@Singleton
@Binds
fun filenameCreator(
impl: AndroidFilenameCreator
): FilenameCreator
@Singleton
@Binds
fun service(
impl: AndroidKeepAliveService
): KeepAliveService
@Singleton
@Binds
fun history(
impl: AppHistoryRepositoryImpl
): AppHistoryRepository
companion object {
@Singleton
@Provides
fun provideMetadata(
impl: AndroidFileController
): MetadataProvider = impl.toMetadataProvider()
}
}
@@ -0,0 +1,211 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.history
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import com.t8rin.imagetoolbox.core.domain.history.AppHistoryRepository
import com.t8rin.imagetoolbox.core.domain.history.model.AppUsageStatistics
import com.t8rin.imagetoolbox.core.domain.history.model.LastUsedTool
import com.t8rin.imagetoolbox.core.domain.json.JsonParser
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
import com.t8rin.imagetoolbox.core.settings.domain.SettingsProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import java.time.LocalDate
import javax.inject.Inject
internal class AppHistoryRepositoryImpl @Inject constructor(
private val dataStore: DataStore<Preferences>,
private val jsonParser: JsonParser,
private val settingsProvider: SettingsProvider
) : AppHistoryRepository {
private val rawLastUsedTools: Flow<List<LastUsedTool>>
get() = dataStore.data.map { preferences ->
preferences[LAST_USED_TOOLS]?.let { lastTools ->
jsonParser.fromJson<LastUsedTools>(
json = lastTools,
type = LastUsedTools::class.java
)?.tools
}.orEmpty()
}
override fun lastUsedTools(
maxCount: Int
): Flow<List<LastUsedTool>> = settingsProvider.settingsState.flatMapLatest { settings ->
if (settings.showToolsHistory) {
rawLastUsedTools.map { rawList ->
rawList.sortedByDescending { it.openCount }.let {
if (maxCount < Int.MAX_VALUE) {
val last = rawList.lastOrNull()
val limited = it.take(maxCount)
if (last != null) {
listOf(last) + limited.minus(last)
} else {
limited
}
} else {
it
}
}
}
} else {
flowOf(emptyList())
}
}
override fun toolUsageStatistics(): Flow<List<LastUsedTool>> = rawLastUsedTools.map { rawList ->
rawList.sortedWith(
compareByDescending<LastUsedTool> { it.openCount }
.thenByDescending { it.lastOpenedTimestamp }
)
}
override fun successfulSavesCount(): Flow<Int> = dataStore.data.map { preferences ->
preferences[SUCCESSFUL_SAVES_COUNT] ?: 0
}
override fun appUsageStatistics(): Flow<AppUsageStatistics> =
dataStore.data.map { preferences ->
AppUsageStatistics(
successfulSavesCount = preferences[SUCCESSFUL_SAVES_COUNT] ?: 0,
savedBytes = preferences[SAVED_BYTES] ?: 0,
lastActivityDayEpoch = preferences[LAST_ACTIVITY_DAY_EPOCH] ?: 0,
currentActivityStreak = preferences[CURRENT_ACTIVITY_STREAK] ?: 0,
savedFormatCounts = preferences[SAVED_FORMAT_COUNTERS]?.let { counters ->
jsonParser.fromJson<SavedFormatCounters>(
json = counters,
type = SavedFormatCounters::class.java
)?.counters
}.orEmpty()
)
}
override suspend fun pushLastTool(screenId: Int) {
dataStore.edit { preferences ->
val todayEpochDay = LocalDate.now().toEpochDay()
val lastActivityDayEpoch = preferences[LAST_ACTIVITY_DAY_EPOCH]
if (lastActivityDayEpoch != todayEpochDay) {
preferences[CURRENT_ACTIVITY_STREAK] =
if (lastActivityDayEpoch == todayEpochDay - 1) {
(preferences[CURRENT_ACTIVITY_STREAK] ?: 0) + 1
} else {
1
}
preferences[LAST_ACTIVITY_DAY_EPOCH] = todayEpochDay
}
val current = preferences[LAST_USED_TOOLS]?.let {
jsonParser.fromJson(
json = it,
type = LastUsedTools::class.java
)
} ?: LastUsedTools(emptyList())
val screenEntry = current.tools.find {
it.screenId == screenId
} ?: LastUsedTool(
screenId = screenId,
openCount = 0,
lastOpenedTimestamp = System.currentTimeMillis()
)
val newValue = current.copy(
tools = current.tools
.minus(screenEntry)
.plus(
screenEntry.copy(
openCount = screenEntry.openCount + 1,
lastOpenedTimestamp = System.currentTimeMillis()
)
)
)
preferences[LAST_USED_TOOLS] = jsonParser.toJson(
obj = newValue,
type = LastUsedTools::class.java
) ?: preferences[LAST_USED_TOOLS].orEmpty()
}
}
override suspend fun registerSuccessfulSave(
savedBytes: Long,
savedFormat: String
) {
dataStore.edit { preferences ->
preferences[SUCCESSFUL_SAVES_COUNT] = (preferences[SUCCESSFUL_SAVES_COUNT] ?: 0) + 1
preferences[SAVED_BYTES] = (preferences[SAVED_BYTES] ?: 0) + savedBytes.coerceAtLeast(0)
val format = savedFormat.lowercase().trim()
if (format.isNotEmpty()) {
val current = preferences[SAVED_FORMAT_COUNTERS]?.let {
runSuspendCatching {
jsonParser.fromJson<SavedFormatCounters>(
json = it,
type = SavedFormatCounters::class.java
)
}.getOrNull()
} ?: SavedFormatCounters(emptyMap())
preferences[SAVED_FORMAT_COUNTERS] = jsonParser.toJson(
obj = current.copy(
counters = current.counters + (format to ((current.counters[format]
?: 0) + 1))
),
type = SavedFormatCounters::class.java
) ?: preferences[SAVED_FORMAT_COUNTERS].orEmpty()
}
}
}
override suspend fun resetUsageStatistics() {
dataStore.edit { preferences ->
preferences.remove(LAST_USED_TOOLS)
preferences.remove(SUCCESSFUL_SAVES_COUNT)
preferences.remove(SAVED_BYTES)
preferences.remove(LAST_ACTIVITY_DAY_EPOCH)
preferences.remove(CURRENT_ACTIVITY_STREAK)
preferences.remove(SAVED_FORMAT_COUNTERS)
}
}
}
private data class LastUsedTools(
val tools: List<LastUsedTool>
)
private data class SavedFormatCounters(
val counters: Map<String, Int>
)
private val LAST_USED_TOOLS = stringPreferencesKey("LAST_USED_TOOLS")
private val SUCCESSFUL_SAVES_COUNT = intPreferencesKey("SUCCESSFUL_SAVES_COUNT")
private val SAVED_BYTES = longPreferencesKey("SAVED_BYTES")
private val LAST_ACTIVITY_DAY_EPOCH = longPreferencesKey("LAST_ACTIVITY_DAY_EPOCH")
private val CURRENT_ACTIVITY_STREAK = intPreferencesKey("CURRENT_ACTIVITY_STREAK")
private val SAVED_FORMAT_COUNTERS = stringPreferencesKey("SAVED_FORMAT_COUNTERS")
@@ -0,0 +1,174 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.image
import android.content.Context
import android.graphics.Bitmap
import androidx.core.net.toUri
import com.t8rin.imagetoolbox.core.data.image.utils.ImageCompressorBackend
import com.t8rin.imagetoolbox.core.data.utils.toSoftware
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
import com.t8rin.imagetoolbox.core.domain.image.ImageCompressor
import com.t8rin.imagetoolbox.core.domain.image.ImageGetter
import com.t8rin.imagetoolbox.core.domain.image.ImageScaler
import com.t8rin.imagetoolbox.core.domain.image.ImageTransformer
import com.t8rin.imagetoolbox.core.domain.image.ShareProvider
import com.t8rin.imagetoolbox.core.domain.image.model.ImageFormat
import com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo
import com.t8rin.imagetoolbox.core.domain.image.model.Quality
import com.t8rin.imagetoolbox.core.domain.image.model.alphaContainedEntries
import com.t8rin.imagetoolbox.core.domain.model.sizeTo
import com.t8rin.imagetoolbox.core.settings.domain.SettingsProvider
import com.t8rin.imagetoolbox.core.settings.domain.model.FilenameBehavior
import com.t8rin.imagetoolbox.core.utils.fileSize
import com.t8rin.trickle.Trickle
import dagger.Lazy
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext
import javax.inject.Inject
internal class AndroidImageCompressor @Inject constructor(
@ApplicationContext private val context: Context,
private val imageTransformer: ImageTransformer<Bitmap>,
private val imageScaler: ImageScaler<Bitmap>,
private val imageGetter: ImageGetter<Bitmap>,
private val shareProvider: Lazy<ShareProvider>,
private val settingsProvider: SettingsProvider,
dispatchersHolder: DispatchersHolder
) : DispatchersHolder by dispatchersHolder, ImageCompressor<Bitmap> {
private val settingsState get() = settingsProvider.settingsState.value
override suspend fun compress(
image: Bitmap,
imageFormat: ImageFormat,
quality: Quality
): ByteArray = withContext(encodingDispatcher) {
val coercedQuality = quality.coerceIn(imageFormat)
val transformedImage = image.toSoftware().let { software ->
val enableForAlpha = settingsState.enableBackgroundColorForAlphaFormats
val isNonAlpha = imageFormat !in ImageFormat.alphaContainedEntries
if (isNonAlpha || coercedQuality.isNonAlpha() || enableForAlpha) {
withContext(defaultDispatcher) {
Trickle.drawColorBehind(
color = settingsState.backgroundForNoAlphaImageFormats.colorInt,
input = software
)
}
} else software
}
ImageCompressorBackend.Factory()
.create(
imageFormat = imageFormat,
context = context,
imageScaler = imageScaler
)
.compress(
image = transformedImage,
quality = coercedQuality
)
}
override suspend fun compressAndTransform(
image: Bitmap,
imageInfo: ImageInfo,
onImageReadyToCompressInterceptor: suspend (Bitmap) -> Bitmap,
applyImageTransformations: Boolean
): ByteArray = withContext(encodingDispatcher) {
val currentImage = if (applyImageTransformations) {
val size = imageInfo.originalUri?.let {
imageGetter.getImage(
data = it,
originalSize = true
)?.run { width sizeTo height }
}
ensureActive()
imageScaler
.scaleImage(
image = imageTransformer.rotate(
image = image.apply { setHasAlpha(true) },
degrees = imageInfo.rotationDegrees
),
width = imageInfo.width,
height = imageInfo.height,
resizeType = imageInfo.resizeType.withOriginalSizeIfCrop(size),
imageScaleMode = imageInfo.imageScaleMode
)
.let {
imageTransformer.flip(
image = it,
isFlipped = imageInfo.isFlipped
)
}
.let {
onImageReadyToCompressInterceptor(it)
}
} else onImageReadyToCompressInterceptor(image)
val extension = imageInfo.originalUri?.let { imageGetter.getExtension(it) }
val imageFormat =
if (settingsState.filenameBehavior is FilenameBehavior.Overwrite && extension != null) {
val target = ImageFormat[extension]
if (imageInfo.imageFormat.extension == target.extension) {
imageInfo.imageFormat
} else {
target
}
} else imageInfo.imageFormat
ensureActive()
compress(
image = currentImage,
imageFormat = imageFormat,
quality = imageInfo.quality
)
}
override suspend fun calculateImageSize(
image: Bitmap,
imageInfo: ImageInfo
): Long = withContext(encodingDispatcher) {
val newInfo = imageInfo.let {
if (it.width == 0 || it.height == 0) {
it.copy(
width = image.width,
height = image.height
)
} else it
}
compressAndTransform(
image = image,
imageInfo = newInfo
).let {
shareProvider.get().cacheByteArray(
byteArray = it,
filename = "temp.${newInfo.imageFormat.extension}"
)?.toUri()
?.fileSize() ?: it.size.toLong()
}
}
}
@@ -0,0 +1,263 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.image
import android.content.Context
import android.graphics.Bitmap
import androidx.core.net.toUri
import coil3.ImageLoader
import coil3.request.ImageRequest
import coil3.request.transformations
import coil3.size.Precision
import coil3.size.Size
import coil3.toBitmap
import com.t8rin.imagetoolbox.core.data.image.utils.static
import com.t8rin.imagetoolbox.core.data.utils.toCoil
import com.t8rin.imagetoolbox.core.domain.coroutines.AppScope
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
import com.t8rin.imagetoolbox.core.domain.image.ImageGetter
import com.t8rin.imagetoolbox.core.domain.image.MetadataProvider
import com.t8rin.imagetoolbox.core.domain.image.model.ImageData
import com.t8rin.imagetoolbox.core.domain.image.model.ImageFormat
import com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
import com.t8rin.imagetoolbox.core.domain.saving.FailureNotifier
import com.t8rin.imagetoolbox.core.domain.transformation.Transformation
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
import com.t8rin.imagetoolbox.core.resources.R
import com.t8rin.imagetoolbox.core.settings.domain.SettingsProvider
import com.t8rin.imagetoolbox.core.utils.extension
import com.t8rin.imagetoolbox.core.utils.makeLog
import dagger.Lazy
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
internal class AndroidImageGetter @Inject constructor(
@ApplicationContext private val context: Context,
private val imageLoader: ImageLoader,
private val appScope: AppScope,
private val failureNotifier: FailureNotifier,
metadataProvider: Lazy<MetadataProvider>,
settingsProvider: SettingsProvider,
dispatchersHolder: DispatchersHolder,
) : DispatchersHolder by dispatchersHolder, ImageGetter<Bitmap> {
private val _settingsState = settingsProvider.settingsState
private val settingsState get() = _settingsState.value
private val metadataProvider by lazy {
metadataProvider.get()
}
override suspend fun getImage(
uri: String,
originalSize: Boolean,
onFailure: ((Throwable) -> Unit)?
): ImageData<Bitmap>? = withContext(defaultDispatcher) {
getImageImpl(
data = uri,
size = null,
addSizeToRequest = originalSize,
onFailure = onFailure ?: failureNotifier::send
)?.let { bitmap ->
ImageData(
image = bitmap,
imageInfo = ImageInfo(
width = bitmap.width,
height = bitmap.height,
imageFormat = settingsState.defaultImageFormat
?: ImageFormat[getExtension(uri)],
originalUri = uri,
resizeType = settingsState.defaultResizeType
),
metadata = metadataProvider.readMetadata(uri)
)
}
}
override suspend fun getImage(
data: Any,
originalSize: Boolean
): Bitmap? = getImageImpl(
data = data,
size = null,
addSizeToRequest = originalSize
)
override suspend fun getImage(
data: Any,
size: IntegerSize?
): Bitmap? = getImageImpl(
data = data,
size = size
)
override suspend fun getImage(
data: Any,
size: Int?
): Bitmap? = getImageImpl(
data = data,
size = size?.let {
IntegerSize(
width = it,
height = it
)
},
precision = Precision.INEXACT
)
override suspend fun getImageData(
uri: String,
size: Int?,
onFailure: (Throwable) -> Unit
): ImageData<Bitmap>? = withContext(defaultDispatcher) {
getImageImpl(
data = uri,
size = size?.let {
IntegerSize(
width = it,
height = it
)
},
precision = Precision.INEXACT,
onFailure = onFailure
)?.let { bitmap ->
ImageData(
image = bitmap,
imageInfo = ImageInfo(
width = bitmap.width,
height = bitmap.height,
imageFormat = settingsState.defaultImageFormat
?: ImageFormat[getExtension(uri)],
originalUri = uri,
resizeType = settingsState.defaultResizeType
),
metadata = metadataProvider.readMetadata(uri)
)
}
}
override suspend fun getImageWithTransformations(
uri: String,
transformations: List<Transformation<Bitmap>>,
originalSize: Boolean
): ImageData<Bitmap>? = withContext(defaultDispatcher) {
getImageImpl(
data = uri,
transformations = transformations,
size = null,
addSizeToRequest = originalSize
)?.let { bitmap ->
ImageData(
image = bitmap,
imageInfo = ImageInfo(
width = bitmap.width,
height = bitmap.height,
imageFormat = ImageFormat[getExtension(uri)],
originalUri = uri,
resizeType = settingsState.defaultResizeType
),
metadata = metadataProvider.readMetadata(uri)
)
}
}
override suspend fun getImageWithTransformations(
data: Any,
transformations: List<Transformation<Bitmap>>,
size: IntegerSize?
): Bitmap? = getImageImpl(
data = data,
transformations = transformations,
size = size
)
override fun getImageAsync(
uri: String,
originalSize: Boolean,
onGetImage: (ImageData<Bitmap>) -> Unit,
onFailure: (Throwable) -> Unit
) {
appScope.launch {
var failureDelivered = false
val imageData = getImage(
uri = uri,
originalSize = originalSize,
onFailure = {
failureDelivered = true
onFailure(it)
}
)
if (imageData != null) {
onGetImage(imageData)
} else if (!failureDelivered) {
onFailure(
IllegalStateException(context.getString(R.string.failed_to_open))
)
}
}
}
override fun getExtension(uri: String): String? = uri.toUri().extension(context)
private suspend fun getImageImpl(
data: Any,
size: IntegerSize?,
precision: Precision = Precision.EXACT,
transformations: List<Transformation<Bitmap>> = emptyList(),
onFailure: (Throwable) -> Unit = {},
addSizeToRequest: Boolean = true
): Bitmap? = withContext(defaultDispatcher) {
if ((size == null || !addSizeToRequest) && data is Bitmap) return@withContext data
val request = ImageRequest
.Builder(context)
.data(data)
.static()
.precision(precision)
.transformations(
transformations.map(Transformation<Bitmap>::toCoil)
)
.apply {
if (addSizeToRequest) {
size(
size?.let {
Size(size.width, size.height)
} ?: Size.ORIGINAL
)
}
}
.build()
ensureActive()
runSuspendCatching {
imageLoader.execute(request).image?.toBitmap()
}.onFailure {
it.makeLog("ImageGetter")
onFailure(it)
}.getOrNull()
}
}
@@ -0,0 +1,135 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2024 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.image
import android.graphics.Bitmap
import android.os.Build
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
import com.t8rin.imagetoolbox.core.domain.image.ImageCompressor
import com.t8rin.imagetoolbox.core.domain.image.ImageGetter
import com.t8rin.imagetoolbox.core.domain.image.ImagePreviewCreator
import com.t8rin.imagetoolbox.core.domain.image.ImageScaler
import com.t8rin.imagetoolbox.core.domain.image.ImageTransformer
import com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo
import com.t8rin.imagetoolbox.core.domain.image.model.ResizeType
import com.t8rin.imagetoolbox.core.domain.transformation.Transformation
import com.t8rin.imagetoolbox.core.settings.domain.SettingsProvider
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlin.math.roundToInt
internal class AndroidImagePreviewCreator @Inject constructor(
private val imageCompressor: ImageCompressor<Bitmap>,
private val imageGetter: ImageGetter<Bitmap>,
private val imageTransformer: ImageTransformer<Bitmap>,
private val imageScaler: ImageScaler<Bitmap>,
settingsProvider: SettingsProvider,
dispatchersHolder: DispatchersHolder
) : DispatchersHolder by dispatchersHolder, ImagePreviewCreator<Bitmap> {
private val _settingsState = settingsProvider.settingsState
private val settingsState get() = _settingsState.value
override suspend fun createPreview(
image: Bitmap,
imageInfo: ImageInfo,
transformations: List<Transformation<Bitmap>>,
onGetByteCount: (Int) -> Unit
): Bitmap? = withContext(defaultDispatcher) {
launch(encodingDispatcher) {
onGetByteCount(0)
ensureActive()
onGetByteCount(
imageCompressor.calculateImageSize(
image = image,
imageInfo = imageInfo
).toInt()
)
}
if (!settingsState.generatePreviews) return@withContext null
if (imageInfo.height == 0 || imageInfo.width == 0) return@withContext image
ensureActive()
val shouldTransform = transformations.isNotEmpty()
|| (imageInfo.width != image.width)
|| (imageInfo.height != image.height)
|| !imageInfo.quality.isDefault()
|| (imageInfo.rotationDegrees != 0f)
|| imageInfo.isFlipped
val targetImage = if (shouldTransform) {
var width = imageInfo.width
var height = imageInfo.height
var scaleFactor = 1f
while (height * width * 4 > SMALL_SIZE) {
height = (height * 0.85f).roundToInt()
width = (width * 0.85f).roundToInt()
scaleFactor *= 0.85f
}
ensureActive()
val bytes = imageCompressor.compressAndTransform(
image = image,
imageInfo = imageInfo.copy(
width = width,
height = height,
resizeType = if (imageInfo.resizeType is ResizeType.CenterCrop) {
(imageInfo.resizeType as ResizeType.CenterCrop).copy(scaleFactor = scaleFactor)
} else imageInfo.resizeType
),
onImageReadyToCompressInterceptor = {
ensureActive()
imageTransformer.transform(
image = it,
transformations = transformations
) ?: it
}
)
ensureActive()
imageGetter.getImage(bytes) ?: image
} else {
image
}
ensureActive()
imageScaler.scaleUntilCanShow(targetImage)
}
override fun canShow(image: Bitmap?): Boolean = image?.run { size() <= BIG_SIZE } ?: false
private val Bitmap.configSize: Int
get() = when (config) {
Bitmap.Config.RGB_565 -> 2
else -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (config == Bitmap.Config.RGBA_F16) 8 else 4
} else 4
}
}
private fun Bitmap.size(): Int = width * height * configSize
}
private const val SMALL_SIZE = 1500 * 1500 * 3
private const val BIG_SIZE = 3096 * 3096 * 4
@@ -0,0 +1,497 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.image
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.PorterDuff
import androidx.core.graphics.BitmapCompat
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.createBitmap
import androidx.core.graphics.scale
import com.awxkee.aire.Aire
import com.awxkee.aire.ResizeFunction
import com.t8rin.imagetoolbox.core.data.image.utils.drawBitmap
import com.t8rin.imagetoolbox.core.data.utils.aspectRatio
import com.t8rin.imagetoolbox.core.data.utils.safeConfig
import com.t8rin.imagetoolbox.core.data.utils.toSoftware
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
import com.t8rin.imagetoolbox.core.domain.image.ImageScaler
import com.t8rin.imagetoolbox.core.domain.image.ImageTransformer
import com.t8rin.imagetoolbox.core.domain.image.model.ImageScaleMode
import com.t8rin.imagetoolbox.core.domain.image.model.ResizeAnchor
import com.t8rin.imagetoolbox.core.domain.image.model.ResizeType
import com.t8rin.imagetoolbox.core.domain.image.model.ScaleColorSpace
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
import com.t8rin.imagetoolbox.core.domain.model.Position
import com.t8rin.imagetoolbox.core.domain.utils.runSuspendCatching
import com.t8rin.imagetoolbox.core.filters.domain.FilterProvider
import com.t8rin.imagetoolbox.core.filters.domain.model.Filter
import com.t8rin.imagetoolbox.core.filters.domain.model.createFilter
import com.t8rin.imagetoolbox.core.settings.domain.SettingsProvider
import com.t8rin.imagetoolbox.core.utils.makeLog
import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
import com.awxkee.aire.ScaleColorSpace as AireScaleColorSpace
internal class AndroidImageScaler @Inject constructor(
settingsProvider: SettingsProvider,
private val imageTransformer: ImageTransformer<Bitmap>,
private val filterProvider: FilterProvider<Bitmap>,
dispatchersHolder: DispatchersHolder
) : DispatchersHolder by dispatchersHolder, ImageScaler<Bitmap> {
private val _settingsState = settingsProvider.settingsState
private val settingsState get() = _settingsState.value
override suspend fun scaleImage(
image: Bitmap,
width: Int,
height: Int,
resizeType: ResizeType,
imageScaleMode: ImageScaleMode
): Bitmap = withContext(defaultDispatcher) {
val widthInternal = width.takeIf { it > 0 } ?: image.width
val heightInternal = height.takeIf { it > 0 } ?: image.height
runSuspendCatching {
when (resizeType) {
ResizeType.Explicit -> {
createScaledBitmap(
image = image,
width = widthInternal,
height = heightInternal,
imageScaleMode = imageScaleMode
)
}
is ResizeType.Flexible -> {
flexibleResize(
image = image,
width = widthInternal,
height = heightInternal,
resizeAnchor = resizeType.resizeAnchor,
imageScaleMode = imageScaleMode
)
}
is ResizeType.CenterCrop -> {
resizeType.performCenterCrop(
image = image,
targetWidth = widthInternal,
targetHeight = heightInternal,
imageScaleMode = imageScaleMode
)
}
is ResizeType.Fit -> {
resizeType.performFitResize(
image = image,
targetWidth = widthInternal,
targetHeight = heightInternal,
imageScaleMode = imageScaleMode
)
}
}
}.onFailure {
it.makeLog("AndroidImageScaler")
}.getOrNull() ?: image
}
override suspend fun scaleUntilCanShow(
image: Bitmap?
): Bitmap? = withContext(defaultDispatcher) {
if (image == null) return@withContext null
if (canShow(image.width * image.height * 4)) return@withContext image
var (height, width) = image.run { height to width }
var iterations = 0
while (!canShow(size = height * width * 4)) {
height = (height * 0.85f).roundToInt()
width = (width * 0.85f).roundToInt()
iterations++
}
if (iterations == 0) image
else scaleImage(
image = image,
height = height,
width = width,
imageScaleMode = ImageScaleMode.Bicubic()
)
}
private fun canShow(size: Int): Boolean {
return size < 3096 * 3096 * 3
}
private suspend fun Bitmap.fitResize(
targetWidth: Int,
targetHeight: Int,
imageScaleMode: ImageScaleMode
): Bitmap {
val aspectRatio = width.toFloat() / height.toFloat()
val targetAspectRatio = targetWidth.toFloat() / targetHeight.toFloat()
val finalWidth: Int
val finalHeight: Int
if (aspectRatio > targetAspectRatio) {
// Image is wider than the target aspect ratio
finalWidth = targetWidth
finalHeight = (targetWidth / aspectRatio).toInt()
} else {
// Image is taller than or equal to the target aspect ratio
finalWidth = (targetHeight * aspectRatio).toInt()
finalHeight = targetHeight
}
return createScaledBitmap(
image = this,
width = finalWidth,
height = finalHeight,
imageScaleMode = imageScaleMode
)
}
private suspend fun ResizeType.Fit.performFitResize(
image: Bitmap,
targetWidth: Int,
targetHeight: Int,
imageScaleMode: ImageScaleMode
): Bitmap = withContext(defaultDispatcher) {
if (targetWidth == image.width && targetHeight == image.height) {
return@withContext image
}
val originalWidth: Int
val originalHeight: Int
val aspect = image.aspectRatio
val originalAspect = image.aspectRatio
if (abs(aspect - originalAspect) > 0.001f) {
originalWidth = image.height
originalHeight = image.width
} else {
originalWidth = image.width
originalHeight = image.height
}
val drawImage = image.fitResize(
targetWidth = targetWidth,
targetHeight = targetHeight,
imageScaleMode = imageScaleMode
)
val blurredBitmap = imageTransformer.transform(
image = drawImage.let { bitmap ->
val xScale: Float = targetWidth.toFloat() / originalWidth
val yScale: Float = targetHeight.toFloat() / originalHeight
val scale = xScale.coerceAtLeast(yScale)
createScaledBitmap(
image = bitmap,
width = (scale * originalWidth).toInt(),
height = (scale * originalHeight).toInt(),
imageScaleMode = imageScaleMode
)
},
transformations = listOf(
filterProvider.filterToTransformation(
createFilter<Float, Filter.NativeStackBlur>(
blurRadius.toFloat() / 1000 * max(targetWidth, targetHeight)
)
)
)
)
createBitmap(targetWidth, targetHeight, drawImage.safeConfig).apply { setHasAlpha(true) }
.applyCanvas {
drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
canvasColor?.let {
drawColor(it)
} ?: blurredBitmap?.let {
drawBitmap(
bitmap = blurredBitmap,
position = Position.Center
)
}
drawBitmap(
bitmap = drawImage,
position = position
)
}
}
private suspend fun ResizeType.CenterCrop.performCenterCrop(
image: Bitmap,
targetWidth: Int,
targetHeight: Int,
imageScaleMode: ImageScaleMode
): Bitmap = withContext(defaultDispatcher) {
val originalSize = if (!originalSize.isDefined()) {
IntegerSize(
width = image.width,
height = image.height
)
} else {
originalSize
} * scaleFactor
if (targetWidth == originalSize.width && targetHeight == originalSize.height) {
return@withContext image
}
val originalWidth: Int
val originalHeight: Int
val aspect = image.aspectRatio
val originalAspect = originalSize.aspectRatio
if (abs(aspect - originalAspect) > 0.001f) {
originalWidth = originalSize.height
originalHeight = originalSize.width
} else {
originalWidth = originalSize.width
originalHeight = originalSize.height
}
val drawImage = createScaledBitmap(
image = image,
width = originalWidth,
height = originalHeight,
imageScaleMode = imageScaleMode
)
val blurredBitmap = if (canvasColor == null) {
imageTransformer.transform(
image = drawImage.let { bitmap ->
val xScale: Float = targetWidth.toFloat() / originalWidth
val yScale: Float = targetHeight.toFloat() / originalHeight
val scale = xScale.coerceAtLeast(yScale)
createScaledBitmap(
image = bitmap,
width = (scale * originalWidth).toInt(),
height = (scale * originalHeight).toInt(),
imageScaleMode = imageScaleMode
)
},
transformations = listOf(
filterProvider.filterToTransformation(
createFilter<Float, Filter.NativeStackBlur>(
blurRadius.toFloat() / 1000 * max(targetWidth, targetHeight)
)
)
)
)
} else null
createBitmap(targetWidth, targetHeight, drawImage.safeConfig).apply { setHasAlpha(true) }
.applyCanvas {
drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
canvasColor?.let {
drawColor(it)
} ?: blurredBitmap?.let {
drawBitmap(
bitmap = blurredBitmap,
position = Position.Center
)
}
drawBitmap(
bitmap = drawImage,
position = position
)
}
}
private suspend fun createScaledBitmap(
image: Bitmap,
width: Int,
height: Int,
imageScaleMode: ImageScaleMode
): Bitmap = withContext(defaultDispatcher) {
if (width == image.width && height == image.height) return@withContext image
val softwareImage = image.toSoftware()
if (imageScaleMode is ImageScaleMode.Base) {
return@withContext if (width < softwareImage.width && height < softwareImage.width) {
BitmapCompat.createScaledBitmap(softwareImage, width, height, null, true)
} else {
softwareImage.scale(width, height)
}
}
val mode = imageScaleMode.takeIf {
it != ImageScaleMode.NotPresent && it.value >= 0
} ?: settingsState.defaultImageScaleMode
Aire.scale(
bitmap = softwareImage,
dstWidth = width,
dstHeight = height,
scaleMode = mode.toResizeFunction(),
colorSpace = mode.scaleColorSpace.toColorSpace()
)
}
private fun ImageScaleMode.toResizeFunction(): ResizeFunction = when (this) {
ImageScaleMode.NotPresent,
ImageScaleMode.Base -> ResizeFunction.Bilinear
is ImageScaleMode.Bilinear -> ResizeFunction.Bilinear
is ImageScaleMode.Nearest -> ResizeFunction.Nearest
is ImageScaleMode.Cubic -> ResizeFunction.Cubic
is ImageScaleMode.Mitchell -> ResizeFunction.MitchellNetravalli
is ImageScaleMode.Catmull -> ResizeFunction.CatmullRom
is ImageScaleMode.Hermite -> ResizeFunction.Hermite
is ImageScaleMode.BSpline -> ResizeFunction.BSpline
is ImageScaleMode.Hann -> ResizeFunction.Hann
is ImageScaleMode.Bicubic -> ResizeFunction.Bicubic
is ImageScaleMode.Hamming -> ResizeFunction.Hamming
is ImageScaleMode.Hanning -> ResizeFunction.Hanning
is ImageScaleMode.Blackman -> ResizeFunction.Blackman
is ImageScaleMode.Welch -> ResizeFunction.Welch
is ImageScaleMode.Quadric -> ResizeFunction.Quadric
is ImageScaleMode.Gaussian -> ResizeFunction.Gaussian
is ImageScaleMode.Sphinx -> ResizeFunction.Sphinx
is ImageScaleMode.Bartlett -> ResizeFunction.Bartlett
is ImageScaleMode.Robidoux -> ResizeFunction.Robidoux
is ImageScaleMode.RobidouxSharp -> ResizeFunction.RobidouxSharp
is ImageScaleMode.Spline16 -> ResizeFunction.Spline16
is ImageScaleMode.Spline36 -> ResizeFunction.Spline36
is ImageScaleMode.Spline64 -> ResizeFunction.Spline64
is ImageScaleMode.Kaiser -> ResizeFunction.Kaiser
is ImageScaleMode.BartlettHann -> ResizeFunction.BartlettHann
is ImageScaleMode.Box -> ResizeFunction.Box
is ImageScaleMode.Bohman -> ResizeFunction.Bohman
is ImageScaleMode.Lanczos2 -> ResizeFunction.Lanczos2
is ImageScaleMode.Lanczos3 -> ResizeFunction.Lanczos3
is ImageScaleMode.Lanczos4 -> ResizeFunction.Lanczos4
is ImageScaleMode.Lanczos2Jinc -> ResizeFunction.Lanczos2Jinc
is ImageScaleMode.Lanczos3Jinc -> ResizeFunction.Lanczos3Jinc
is ImageScaleMode.Lanczos4Jinc -> ResizeFunction.Lanczos4Jinc
is ImageScaleMode.EwaHanning -> ResizeFunction.EwaHanning
is ImageScaleMode.EwaRobidoux -> ResizeFunction.EwaRobidoux
is ImageScaleMode.EwaBlackman -> ResizeFunction.EwaBlackman
is ImageScaleMode.EwaQuadric -> ResizeFunction.EwaQuadric
is ImageScaleMode.EwaRobidouxSharp -> ResizeFunction.EwaRobidouxSharp
is ImageScaleMode.EwaLanczos3Jinc -> ResizeFunction.EwaLanczos3Jinc
is ImageScaleMode.Ginseng -> ResizeFunction.Ginseng
is ImageScaleMode.EwaGinseng -> ResizeFunction.EwaGinseng
is ImageScaleMode.EwaLanczosSharp -> ResizeFunction.EwaLanczosSharp
is ImageScaleMode.EwaLanczos4Sharpest -> ResizeFunction.EwaLanczos4Sharpest
is ImageScaleMode.EwaLanczosSoft -> ResizeFunction.EwaLanczosSoft
is ImageScaleMode.HaasnSoft -> ResizeFunction.HaasnSoft
is ImageScaleMode.Lagrange2 -> ResizeFunction.Lagrange2
is ImageScaleMode.Lagrange3 -> ResizeFunction.Lagrange3
is ImageScaleMode.Lanczos6 -> ResizeFunction.Lanczos6
is ImageScaleMode.Lanczos6Jinc -> ResizeFunction.Lanczos6Jinc
is ImageScaleMode.Area -> ResizeFunction.Area
}
private fun ScaleColorSpace.toColorSpace(): AireScaleColorSpace = when (this) {
ScaleColorSpace.LAB -> AireScaleColorSpace.LAB
ScaleColorSpace.Linear -> AireScaleColorSpace.LINEAR
ScaleColorSpace.SRGB -> AireScaleColorSpace.SRGB
ScaleColorSpace.LUV -> AireScaleColorSpace.LUV
ScaleColorSpace.Sigmoidal -> AireScaleColorSpace.SIGMOIDAL
ScaleColorSpace.XYZ -> AireScaleColorSpace.XYZ
ScaleColorSpace.F32Gamma22 -> AireScaleColorSpace.LINEAR_F32_GAMMA_2_2
ScaleColorSpace.F32Gamma28 -> AireScaleColorSpace.LINEAR_F32_GAMMA_2_8
ScaleColorSpace.F32Rec709 -> AireScaleColorSpace.LINEAR_F32_REC709
ScaleColorSpace.F32sRGB -> AireScaleColorSpace.LINEAR_F32_SRGB
ScaleColorSpace.LCH -> AireScaleColorSpace.LCH
ScaleColorSpace.OklabGamma22 -> AireScaleColorSpace.OKLAB_GAMMA_2_2
ScaleColorSpace.OklabGamma28 -> AireScaleColorSpace.OKLAB_GAMMA_2_8
ScaleColorSpace.OklabRec709 -> AireScaleColorSpace.OKLAB_REC709
ScaleColorSpace.OklabSRGB -> AireScaleColorSpace.OKLAB_SRGB
ScaleColorSpace.JzazbzGamma22 -> AireScaleColorSpace.JZAZBZ_GAMMA_2_2
ScaleColorSpace.JzazbzGamma28 -> AireScaleColorSpace.JZAZBZ_GAMMA_2_8
ScaleColorSpace.JzazbzRec709 -> AireScaleColorSpace.JZAZBZ_REC709
ScaleColorSpace.JzazbzSRGB -> AireScaleColorSpace.JZAZBZ_SRGB
}
private suspend fun flexibleResize(
image: Bitmap,
width: Int,
height: Int,
resizeAnchor: ResizeAnchor,
imageScaleMode: ImageScaleMode
): Bitmap = withContext(defaultDispatcher) {
val max = max(width, height)
val scaleByWidth = suspend {
val aspectRatio = image.aspectRatio
createScaledBitmap(
image = image,
width = width,
height = (width / aspectRatio).toInt(),
imageScaleMode = imageScaleMode
)
}
val scaleByHeight = suspend {
val aspectRatio = image.aspectRatio
createScaledBitmap(
image = image,
width = (height * aspectRatio).toInt(),
height = height,
imageScaleMode = imageScaleMode
)
}
when (resizeAnchor) {
ResizeAnchor.Max -> {
if (width >= height) {
scaleByWidth()
} else scaleByHeight()
}
ResizeAnchor.Min -> {
if (width >= height) {
scaleByHeight()
} else scaleByWidth()
}
ResizeAnchor.Width -> scaleByWidth()
ResizeAnchor.Height -> scaleByHeight()
ResizeAnchor.Default -> {
runSuspendCatching {
if (image.height >= image.width) {
val aspectRatio = image.aspectRatio
val targetWidth = (max * aspectRatio).toInt()
createScaledBitmap(image, targetWidth, max, imageScaleMode)
} else {
val aspectRatio = 1f / image.aspectRatio
val targetHeight = (max * aspectRatio).toInt()
createScaledBitmap(image, max, targetHeight, imageScaleMode)
}
}.getOrNull() ?: image
}
}
}
}
@@ -0,0 +1,189 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.image
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Matrix
import coil3.ImageLoader
import coil3.request.ImageRequest
import coil3.request.transformations
import coil3.size.Size
import coil3.toBitmap
import com.t8rin.imagetoolbox.core.data.utils.toCoil
import com.t8rin.imagetoolbox.core.domain.coroutines.DispatchersHolder
import com.t8rin.imagetoolbox.core.domain.image.ImageTransformer
import com.t8rin.imagetoolbox.core.domain.image.model.ImageFormat
import com.t8rin.imagetoolbox.core.domain.image.model.ImageInfo
import com.t8rin.imagetoolbox.core.domain.image.model.Preset
import com.t8rin.imagetoolbox.core.domain.image.model.Quality
import com.t8rin.imagetoolbox.core.domain.image.model.ResizeType
import com.t8rin.imagetoolbox.core.domain.model.IntegerSize
import com.t8rin.imagetoolbox.core.domain.model.sizeTo
import com.t8rin.imagetoolbox.core.domain.transformation.Transformation
import com.t8rin.imagetoolbox.core.utils.makeLog
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.roundToInt
internal class AndroidImageTransformer @Inject constructor(
@ApplicationContext private val context: Context,
private val imageLoader: ImageLoader,
defaultDispatchersHolder: DispatchersHolder
) : DispatchersHolder by defaultDispatchersHolder, ImageTransformer<Bitmap> {
override suspend fun transform(
image: Bitmap,
transformations: List<Transformation<Bitmap>>,
originalSize: Boolean
): Bitmap? = withContext(defaultDispatcher) {
if (transformations.isEmpty()) return@withContext image
val request = ImageRequest
.Builder(context)
.data(image)
.transformations(
transformations.map {
it.toCoil()
}
)
.apply {
if (originalSize) size(Size.ORIGINAL)
}
.build()
return@withContext imageLoader.execute(request).image?.toBitmap()
}
override suspend fun transform(
image: Bitmap,
transformations: List<Transformation<Bitmap>>,
size: IntegerSize
): Bitmap? = withContext(defaultDispatcher) {
if (transformations.isEmpty()) return@withContext image
val request = ImageRequest
.Builder(context)
.data(image)
.transformations(
transformations.map {
it.toCoil()
}
)
.size(size.width, size.height)
.build()
return@withContext imageLoader.execute(request).image?.toBitmap()
}
override suspend fun applyPresetBy(
image: Bitmap?,
preset: Preset,
currentInfo: ImageInfo
): ImageInfo = withContext(defaultDispatcher) {
if (image == null || preset is Preset.None) return@withContext currentInfo
val size = currentInfo.originalUri.makeLog("applyPresetBy originalUri")?.let { uri ->
imageLoader.execute(
ImageRequest.Builder(context)
.data(uri)
.size(Size.ORIGINAL)
.build()
).image?.run { width sizeTo height }.makeLog("applyPresetBy using orig size")
} ?: IntegerSize(image.width, image.height).makeLog("applyPresetBy using image size")
val rotated = abs(currentInfo.rotationDegrees) % 180 != 0f
fun calcWidth() = if (rotated) size.height else size.width
fun calcHeight() = if (rotated) size.width else size.height
fun Int.calc(cnt: Int): Int = (this * (cnt / 100f)).toInt()
when (preset) {
is Preset.Telegram -> {
currentInfo.copy(
width = 512,
height = 512,
imageFormat = ImageFormat.Png.Lossless,
resizeType = ResizeType.Flexible,
quality = Quality.Base(100)
)
}
is Preset.Percentage -> currentInfo.copy(
width = calcWidth().calc(preset.value),
height = calcHeight().calc(preset.value),
)
is Preset.AspectRatio -> {
val originalWidth = calcWidth().toFloat()
val originalHeight = calcHeight().toFloat()
val newWidth: Float
val newHeight: Float
val condition = if (preset.isFit) preset.ratio > originalWidth / originalHeight
else preset.ratio < originalWidth / originalHeight
if (condition) {
newWidth = originalHeight * preset.ratio
newHeight = originalHeight
} else {
newWidth = originalWidth
newHeight = originalWidth / preset.ratio
}
currentInfo.copy(
width = newWidth.roundToInt(),
height = newHeight.roundToInt()
)
}
Preset.None -> currentInfo
}
}
override suspend fun flip(
image: Bitmap,
isFlipped: Boolean
): Bitmap = withContext(defaultDispatcher) {
if (isFlipped) {
val matrix = Matrix().apply { postScale(-1f, 1f, image.width / 2f, image.height / 2f) }
Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, true)
} else image
}
override suspend fun rotate(
image: Bitmap,
degrees: Float
): Bitmap = withContext(defaultDispatcher) {
if (degrees == 0f) return@withContext image
if (degrees % 90 == 0f) {
val matrix = Matrix().apply { postRotate(degrees) }
Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, true)
} else {
val matrix = Matrix().apply {
setRotate(degrees, image.width.toFloat() / 2, image.height.toFloat() / 2)
}
Bitmap.createBitmap(image, 0, 0, image.width, image.height, matrix, true)
}
}
}
@@ -0,0 +1,51 @@
/*
* ImageToolbox is an image editor for android
* Copyright (c) 2026 T8RIN (Malik Mukhametzyanov)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* You should have received a copy of the Apache License
* along with this program. If not, see <http://www.apache.org/licenses/LICENSE-2.0>.
*/
package com.t8rin.imagetoolbox.core.data.image
import com.t8rin.exif.ExifInterface
import com.t8rin.imagetoolbox.core.domain.image.Metadata
import com.t8rin.imagetoolbox.core.domain.image.model.MetadataTag
import com.t8rin.imagetoolbox.core.domain.image.toMap
import java.io.FileDescriptor
private data class ExifInterfaceMetadata(
private val exifInterface: ExifInterface
) : Metadata {
override fun saveAttributes(): Metadata = apply {
exifInterface.saveAttributes()
}
override fun getAttribute(
tag: MetadataTag
): String? = exifInterface.getAttribute(tag.key)
override fun setAttribute(
tag: MetadataTag,
value: String?
): Metadata = apply {
exifInterface.setAttribute(tag.key, value)
}
override fun toString(): String = "Android(${toMap()})"
}
internal fun ExifInterface.toMetadata(): Metadata = ExifInterfaceMetadata(this)
internal fun FileDescriptor.toMetadata(): Metadata = ExifInterface(this).toMetadata()

Some files were not shown because too many files have changed in this diff Show More