Skip to content

Commit

Permalink
Photoshop style blend mode for each layer (#10)
Browse files Browse the repository at this point in the history
What's new?
- Photoshop style blend mode for each layer
- Demo app updated to include support for blend mode
- Demo app UI updated
- Fixed bug: Exporting without layer was producing artifacts
  • Loading branch information
forkachild authored May 12, 2023
1 parent 1c74353 commit 3c8becb
Show file tree
Hide file tree
Showing 55 changed files with 1,286 additions and 375 deletions.
74 changes: 43 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,56 @@
# Imagine
[![Release](https://jitpack.io/v/forkachild/imagine.svg?style=flat-square)](https://jitpack.io/#forkachild/imagine)

GPU accelerated, blisteringly fast, highly optimised, easy-to-use, layer based image editing library for Android using OpenGL ES 2.0
GPU accelerated, blisteringly fast, easy-to-use, layer based image editing library with Photoshop style blend mode support for Android using OpenGL ES 2.0.

### Blog
I have written a blog on the entire story of this library. Check it out here [Imagine: A story of the evergreen OpenGL on Android](https://medium.com/@suhelchakraborty/imagine-a-story-of-the-evergreen-opengl-on-android-c36b4e8463f0)
## Features
- Multiple consecutive customizable layers of processing.
- Photoshop style blending mode for each layer to merge a layer atop the previous.
- Allows alpha blending between layers.
- Easy to implement abstract interface `ImagineLayer` which must provide the following:
- **`source: String`**: GLSL code snippet implementing a `vec4 process(vec4 color)` function.
- **`intensity: Float`**: Mixing factor of pixel from this and the previous layer. Must lie between 0.0f to 1.0f.
- **`blendMode: ImagineBlendMode`**: How to blend the current layer atop the previous layer.
- 2 purposeful modes of operation
- **`preview`**: Scaled down image for low memory footprint and faster viewport previews, invoked by `ImagineEngine.updatePreview()`.
- **`export`**: Full resolution mode for extracting an edited `Bitmap`, invoked by `ImagineEngine.exportBitmap()`.

### Demo
A beautiful _Material You_ themed simple image editor is provided in the `editor` module. You can refer to the source code of the same and maybe also use it.

<img alt="Screencast" height="30%" src="assets/screencast.gif" width="30%"/>
![Screencast](assets/screencast.gif "Imagine Editor Demo")

## Features
- `Layer` abstraction representing each processing stage in the pipeline
- Write only a single function in GLSL to manipulate per-pixel color
- Process a chain of `Layer`s with a single function call
- Provides a pre-scaled lower resolution preview mode for faster previews and only bumps up resolution during final render
- Provides a `Bitmap` at final render to be used at your will
### Documentation
The library code is extensively documented. Additionally, check out the story style blog [Imagine: A story of the evergreen OpenGL on Android](https://medium.com/@suhelchakraborty/imagine-a-story-of-the-evergreen-opengl-on-android-c36b4e8463f0) that details the conception of this library!

## Installation
Add the source repository

In project level `build.gradle`
```groovy
### In project level `build.gradle` (Older Gradle versions)
```
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
```
or in `settings.gradle` in newer gradle
```groovy

### In `settings.gradle` (Newer Gradle versions)
```
dependencyResolutionManagement {
...
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
...
maven { url 'https://jitpack.io' }
}
}
```
Add the dependency in module level `build.gradle`
```groovy

### In module level `build.gradle`
```
dependencies {
...
implementation 'com.github.forkachild:imagine:1.0.2'
implementation 'com.github.forkachild:imagine:1.1.0'
}
```

Expand All @@ -58,9 +64,6 @@ dependencies {
```
2. Configure `ImagineEngine`
```kotlin
import com.suhel.imagine.core.ImagineView
import com.suhel.imagine.core.ImagineEngine

private lateinit var imagineView: ImagineView
private lateinit var imagineEngine: ImagineEngine

Expand All @@ -78,30 +81,38 @@ dependencies {
imagineEngine.imageProvider = ResImageProvider(context, resId) // Or from a drawable res
imagineEngine.imageProvider = ... // Or your custom ImageProvider implementation
```
4. Create one or more `Layer` objects, writing a `vec4 process(vec4 color)` GLSL function for each
4. Create one or more `ImagineLayer` objects, writing a `vec4 process(vec4 color)` GLSL function for each
```kotlin
class SampleLayer: Layer {
class InvertGreenLayer: ImagineLayer {
// The fragment shader source for this layer. The texture is pre-sampled
// and the color is passed to this function. The function signature must
// be accurate!
override val source: String = """
vec4 process(vec4 color) {
return vec4(color.r, 1.0, color.b, 1.0);
return vec4(color.r, 1.0 - color.g, color.b, color.a); // The alpha channel is important!
}
""".trimIndent()

// Configures how each layer (after processing) will be blended with the previous
// layer in the chain
override val blendMode: ImagineBlendMode = ImagineBlendMode.Normal

// Configures the intensity of application of the pixel color output from this layer
override val intensity: Float = 1.0f

override fun create(program: Int) {
override fun create(program: ImagineShader.Program) {
// Optional override to extract your custom uniforms from the shader program
}

override fun bind(program: Int) {
override fun bind(program: ImagineShader.Program) {
// Optional override to bind your custom uniforms during processing
}
}
```
5. Assign a list of `Layer` objects to `ImagineEngine`
5. Assign a `List<ImagineLayer>` to `ImagineEngine`
```kotlin
imagineEngine.layers = listOf(
SampleLayer(),
InvertGreenLayer(),
...
)
```
Expand All @@ -118,8 +129,9 @@ dependencies {
```

## TODO
- [ ] Photoshop like blend mode support for each `Layer`
- [ ] More customisations in `Layer` shaders
- [X] Photoshop style blend mode support for each `ImagineLayer`
- [ ] Custom texture sampling in `ImagineLayer` fragment shader code
- [ ] Ability to conditionally render an `ImagineLayer`
- [ ] Viewport background color customisation

## License
Expand Down
Binary file modified assets/screencast.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.suhel.imagine.editor.extensions

import com.suhel.imagine.core.types.ImagineBlendMode

val ImagineBlendMode.displayText: String
get() = when (this) {
ImagineBlendMode.Normal -> "Normal"
ImagineBlendMode.Darken -> "Darken"
ImagineBlendMode.Multiply -> "Multiply"
ImagineBlendMode.ColorBurn -> "Color burn"
ImagineBlendMode.LinearBurn -> "Linear burn"
ImagineBlendMode.DarkerColor -> "Darker color"
ImagineBlendMode.Lighten -> "Lighten"
ImagineBlendMode.Screen -> "Screen"
ImagineBlendMode.ColorDodge -> "Color dodge"
ImagineBlendMode.LinearDodge -> "Linear dodge"
ImagineBlendMode.LighterColor -> "Lighter color"
ImagineBlendMode.Overlay -> "Overlay"
ImagineBlendMode.SoftLight -> "Soft light"
ImagineBlendMode.HardLight -> "Hard light"
ImagineBlendMode.VividLight -> "Vivid light"
ImagineBlendMode.LinearLight -> "Linear light"
ImagineBlendMode.PinLight -> "Pin light"
ImagineBlendMode.HardMix -> "Hard mix"
ImagineBlendMode.Difference -> "Difference"
ImagineBlendMode.Exclusion -> "Exclusion"
ImagineBlendMode.Subtract -> "Subtract"
ImagineBlendMode.Divide -> "Divide"
ImagineBlendMode.Hue -> "Hue"
ImagineBlendMode.Saturation -> "Saturation"
ImagineBlendMode.Color -> "Color"
ImagineBlendMode.Luminosity -> "Luminosity"
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,34 @@ import android.os.Looper
import android.provider.MediaStore
import com.suhel.imagine.editor.model.BitmapSaveException
import com.suhel.imagine.editor.model.BitmapSaveFormat
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale

class BitmapSaveTask(
private val context: Context,
private val bitmap: Bitmap,
private val format: BitmapSaveFormat,
private val onStart: () -> Unit,
private val onSuccess: () -> Unit,
private val onError: (BitmapSaveException) -> Unit,
) : Runnable {

private val mainThread = Handler(Looper.getMainLooper())

override fun run() {
val fileName =
SimpleDateFormat("dd-MM-yyyy-HH-mm-ss-Z", Locale.getDefault()).format(Date())
val timestamp = SimpleDateFormat(
"dd-MM-yyyy-HH-mm-SSSS",
Locale.getDefault()
).format(Date())
val fileName = "imagine-$timestamp"

val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, format.mimeType)
put(MediaStore.Images.ImageColumns.DISPLAY_NAME, fileName)
put(MediaStore.Images.ImageColumns.MIME_TYPE, format.mimeType)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
put(MediaStore.Images.ImageColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}

val contentResolver = context.contentResolver
Expand All @@ -42,6 +48,8 @@ class BitmapSaveTask(
return
}

postStart()

val outputStream = contentResolver.openOutputStream(uri)

if (outputStream == null) {
Expand All @@ -61,6 +69,10 @@ class BitmapSaveTask(
postSuccess()
}

private fun postStart() {
mainThread.post(onStart)
}

private fun postSuccess() {
mainThread.post(onSuccess)
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.suhel.imagine.editor.helper

import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView

class ReorderItemsCallback(
private val onItemMove: (Int, Int) -> Boolean,
) : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
0,
) {

override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = onItemMove(viewHolder.adapterPosition, target.adapterPosition)

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// Do nothing
}

override fun isLongPressDragEnabled(): Boolean = false

override fun isItemViewSwipeEnabled(): Boolean = false

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ enum class BitmapSaveFormat {
val mimeType: String
get() = when (this) {
PNG -> "image/png"
JPEG -> "image/jpg"
JPEG -> "image/jpeg"
}

val compressFormat: Bitmap.CompressFormat
Expand Down
Loading

0 comments on commit 3c8becb

Please sign in to comment.