Unverified Commit 3e575c5c authored by Ivan Matkov's avatar Ivan Matkov Committed by GitHub

Expose `Blender` skia API (#1168)

Required for [CMP-5046](https://youtrack.jetbrains.com/issue/CMP-5046)
Supersedes #1162 with a cleaned-up and fixes

---------
Co-authored-by: 's avatarLuc Girardin <luc.girardin@macrofocus.com>
parent 0c7dafe9
......@@ -37,7 +37,7 @@ Add `-Dskiko.test.ui.enabled=true` to enable UI tests (integration tests, which
For example, if we want to include UI tests when we test JVM target, call this:
```
./gradlew awtTest -Dskiko.test.ui.enabled=true
./gradlew :skiko:awtTest -Dskiko.test.ui.enabled=true
```
Don't run any background tasks, click mouse, or press keys during the tests. Otherwise, they probably fail.
......
package org.jetbrains.skia
/**
* Blends are operators that take in two colors (source, destination) and return a new color.
* Many of these operate the same on all 4 components: red, green, blue, alpha. For these,
* we just document what happens to one component, rather than naming each one separately.
*
* Different SkColorTypes have different representations for color components:
* 8-bit: 0..255
* 6-bit: 0..63
* 5-bit: 0..31
* 4-bit: 0..15
* floats: 0...1
*
* The documentation is expressed as if the component values are always 0..1 (floats).
*
* For brevity, the documentation uses the following abbreviations
* s : source
* d : destination
* sa : source alpha
* da : destination alpha
*
* Results are abbreviated
* r : if all 4 components are computed in the same manner
* ra : result alpha component
* rc : result "color": red, green, blue components
*/
enum class BlendMode {
/** Replaces destination with zero: fully transparent. */
/** Replaces destination with zero: fully transparent. r = 0 */
CLEAR,
/** Replaces destination. */
/** Replaces destination. r = s */
SRC,
/** Preserves destination. */
/** Preserves destination. r = d */
DST,
/** Source over destination. */
/** Source over destination. r = s + (1-sa)*d */
SRC_OVER,
/** Destination over source. */
/** Destination over source. r = d + (1-da)*s */
DST_OVER,
/** Source trimmed inside destination. */
/** Source trimmed inside destination. r = s * da */
SRC_IN,
/** Destination trimmed by source. */
/** Destination trimmed by source. r = d * sa */
DST_IN,
/** Source trimmed outside destination. */
/** Source trimmed outside destination. r = s * (1-da) */
SRC_OUT,
/** Destination trimmed outside source. */
/** Destination trimmed outside source. r = d * (1-sa) */
DST_OUT,
/** Source inside destination blended with destination. */
/** Source inside destination blended with destination. r = s*da + d*(1-sa) */
SRC_ATOP,
/** Destination inside source blended with source. */
/** Destination inside source blended with source. r = d*sa + s*(1-da) */
DST_ATOP,
/** Each of source and destination trimmed outside the other. */
/** Each of source and destination trimmed outside the other. r = s*(1-da) + d*(1-sa) */
XOR,
/** Sum of colors. */
/** Sum of colors. r = min(s + d, 1) */
PLUS,
/** Product of premultiplied colors; darkens destination. */
/** Product of premultiplied colors; darkens destination. r = s*d */
MODULATE,
/** Multiply inverse of pixels, inverting result; brightens destination. */
/** Multiply inverse of pixels, inverting result; brightens destination. r = s + d - s*d */
SCREEN,
/** Multiply or screen, depending on destination. */
OVERLAY,
/** Darker of source and destination. */
/** Darker of source and destination. rc = s + d - max(s*da, d*sa), ra = kSrcOver */
DARKEN,
/** Lighter of source and destination. */
/** Lighter of source and destination. rc = s + d - min(s*da, d*sa), ra = kSrcOver */
LIGHTEN,
/** Brighten destination to reflect source. */
......@@ -67,13 +92,13 @@ enum class BlendMode {
/** Lighten or darken, depending on source. */
SOFT_LIGHT,
/** Subtract darker from lighter with higher contrast. */
/** Subtract darker from lighter with higher contrast. rc = s + d - 2*(min(s*da, d*sa)), ra = kSrcOver */
DIFFERENCE,
/** Subtract darker from lighter with lower contrast. */
/** Subtract darker from lighter with lower contrast. rc = s + d - two(s*d), ra = kSrcOver */
EXCLUSION,
/** Multiply source with destination, darkening image. */
/** Multiply source with destination, darkening image. r = s*(1-da) + d*(1-sa) + s*d */
MULTIPLY,
/** Hue of source with saturation and luminosity of destination. */
......
package org.jetbrains.skia
import org.jetbrains.skia.impl.NativePointer
import org.jetbrains.skia.impl.RefCnt
import org.jetbrains.skia.impl.Stats
import org.jetbrains.skia.impl.Library.Companion.staticLoad
import org.jetbrains.skia.impl.interopScope
/**
* Blender represents a custom blend function in the Skia pipeline. A blender combines a source
* color (the result of our paint) and destination color (from the canvas) into a final color.
*/
class Blender internal constructor(ptr: NativePointer) : RefCnt(ptr) {
companion object {
init {
staticLoad()
}
fun makeMode(mode: BlendMode): Blender {
Stats.onNativeCall()
return Blender(_nMakeMode(mode.ordinal))
}
fun makeArithmetic(
k1: Float,
k2: Float,
k3: Float,
k4: Float,
enforcePMColor: Boolean,
): Blender {
Stats.onNativeCall()
return interopScope {
Blender(
_nMakeArithmetic(
k1,
k2,
k3,
k4,
enforcePMColor,
),
)
}
}
}
}
@ExternalSymbolName("org_jetbrains_skia_Blender__1nMakeArithmetic")
private external fun _nMakeArithmetic(
k1: Float,
k2: Float,
k3: Float,
k4: Float,
enforcePMColor: Boolean
): NativePointer
@ExternalSymbolName("org_jetbrains_skia_Blender__1nMakeMode")
private external fun _nMakeMode(mode: Int): NativePointer
......@@ -3,6 +3,14 @@ package org.jetbrains.skia
import org.jetbrains.skia.impl.Library.Companion.staticLoad
import org.jetbrains.skia.impl.*
/**
* ColorFilters are optional objects in the drawing pipeline. When present in
* a paint, they are called with the "src" colors, and return new colors, which
* are then passed onto the next stage (either ImageFilter or Xfermode).
*
* All subclasses are required to be reentrant-safe : it must be legal to share
* the same instance between several threads.
*/
class ColorFilter : RefCnt {
companion object {
fun makeComposed(outer: ColorFilter?, inner: ColorFilter?): ColorFilter {
......
......@@ -3,6 +3,19 @@ package org.jetbrains.skia
import org.jetbrains.skia.impl.Library.Companion.staticLoad
import org.jetbrains.skia.impl.*
/**
* Base class for image filters. If one is installed in the paint, then all drawing occurs as
* usual, but it is as if the drawing happened into an offscreen (before the xfermode is applied).
* This offscreen bitmap will then be handed to the imagefilter, who in turn creates a new bitmap
* which is what will finally be drawn to the device (using the original xfermode).
*
* The local space of image filters matches the local space of the drawn geometry. For instance if
* there is rotation on the canvas, the blur will be computed along those rotated axes and not in
* the device space. In order to achieve this result, the actual drawing of the geometry may happen
* in an unrotated coordinate system so that the filtered image can be computed more easily, and
* then it will be post transformed to match what would have been produced if the geometry were
* drawn with the total canvas matrix to begin with.
*/
class ImageFilter internal constructor(ptr: NativePointer) : RefCnt(ptr) {
companion object {
......
......@@ -3,6 +3,10 @@ package org.jetbrains.skia
import org.jetbrains.skia.impl.*
import org.jetbrains.skia.impl.Library.Companion.staticLoad
/**
* MaskFilter is the base class for object that perform transformations on
* the mask before drawing it. An example subclass is Blur.
*/
class MaskFilter internal constructor(ptr: NativePointer) : RefCnt(ptr) {
companion object {
init {
......
......@@ -447,6 +447,35 @@ class Paint : Managed {
reachabilityBarrier(this)
}
/**
* Returns the user-supplied blend function, if one has been set.
*
* A null blender signifies the default SrcOver behavior.
*
* For convenience, you can call [blendMode] if the blend effect can be expressed
* as one of those values.
*
* @see [https://fiddle.skia.org/c/@Paint_setBlender](https://fiddle.skia.org/c/@Paint_setBlender)
* @see [https://fiddle.skia.org/c/@Paint_refBlender](https://fiddle.skia.org/c/@Paint_refBlender)
*
* @return the [Blender] assigned to this paint, otherwise null
*/
var blender: Blender?
get() = try {
Stats.onNativeCall()
val blenderPtr = _nGetBlender(_ptr)
if (blenderPtr == NullPointer) null else Blender(blenderPtr)
} finally {
reachabilityBarrier(this)
}
set(value) = try {
Stats.onNativeCall()
_nSetBlender(_ptr, getPtr(value))
} finally {
reachabilityBarrier(value)
reachabilityBarrier(this)
}
/**
* @return true if BlendMode is BlendMode.SRC_OVER, the default.
*/
......@@ -665,5 +694,11 @@ private external fun _nGetImageFilter(ptr: NativePointer): NativePointer
@ExternalSymbolName("org_jetbrains_skia_Paint__1nSetImageFilter")
private external fun _nSetImageFilter(ptr: NativePointer, filterPtr: NativePointer)
@ExternalSymbolName("org_jetbrains_skia_Paint__1nGetBlender")
private external fun _nGetBlender(ptr: NativePointer): NativePointer
@ExternalSymbolName("org_jetbrains_skia_Paint__1nSetBlender")
private external fun _nSetBlender(ptr: NativePointer, blenderPtr: NativePointer)
@ExternalSymbolName("org_jetbrains_skia_Paint__1nHasNothingToDraw")
private external fun _nHasNothingToDraw(ptr: NativePointer): Boolean
......@@ -19,6 +19,13 @@ class RuntimeEffect internal constructor(ptr: NativePointer) : RefCnt(ptr) {
}
}
fun makeForBlender(sksl: String): RuntimeEffect {
Stats.onNativeCall()
return interopScope {
makeFromResultPtr(_nMakeForBlender(toInterop(sksl)))
}
}
init {
staticLoad()
}
......@@ -42,6 +49,18 @@ class RuntimeEffect internal constructor(ptr: NativePointer) : RefCnt(ptr) {
reachabilityBarrier(children)
}
}
fun makeBlender(uniforms: Data?): Blender {
Stats.onNativeCall()
return try {
interopScope {
Blender(_nMakeBlender(_ptr, getPtr(uniforms)))
}
} finally {
reachabilityBarrier(this)
reachabilityBarrier(uniforms)
}
}
}
internal expect fun RuntimeEffect.Companion.makeFromResultPtr(ptr: NativePointer): RuntimeEffect
......@@ -52,6 +71,10 @@ private external fun _nMakeShader(
childCount: Int, localMatrix: InteropPointer
): NativePointer
@ExternalSymbolName("org_jetbrains_skia_RuntimeEffect__1nMakeBlender")
private external fun _nMakeBlender(
runtimeEffectPtr: NativePointer, uniformPtr: NativePointer
): NativePointer
@ExternalSymbolName("org_jetbrains_skia_RuntimeEffect__1nMakeForShader")
private external fun _nMakeForShader(sksl: InteropPointer): NativePointer
......@@ -59,6 +82,9 @@ private external fun _nMakeForShader(sksl: InteropPointer): NativePointer
@ExternalSymbolName("org_jetbrains_skia_RuntimeEffect__1nMakeForColorFilter")
private external fun _nMakeForColorFilter(sksl: InteropPointer): NativePointer
@ExternalSymbolName("org_jetbrains_skia_RuntimeEffect__1nMakeForBlender")
private external fun _nMakeForBlender(sksl: InteropPointer): NativePointer
// The functions below can be used only in JS and native targets
@ExternalSymbolName("org_jetbrains_skia_RuntimeEffect__1Result_nGetPtr")
......
package org.jetbrains.skia
import org.jetbrains.skia.impl.use
import org.jetbrains.skia.util.assertContentDifferent
import org.jetbrains.skiko.tests.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
class BlenderTest {
private val originalBytes: ByteArray by lazy {
renderAndReturnBytes(blender = null)
}
private fun renderAndReturnBytes(blender: Blender? = null): ByteArray {
return Surface.makeRasterN32Premul(20, 20).use {
val paint = Paint().apply {
setStroke(true)
strokeWidth = 2f
}
val region = Region().apply {
op(IRect(3, 3, 18, 18), Region.Op.UNION)
}
paint.blender = blender
it.canvas.drawRegion(region, paint)
val image = it.makeImageSnapshot()
Bitmap.makeFromImage(image).readPixels()!!
}
}
private fun blenderTest(blender: () -> Blender) = runTest {
val modifiedPixels = renderAndReturnBytes(blender = blender())
assertEquals(originalBytes.size, modifiedPixels.size)
// we don't check the actual content of the pixels, we only assume they're different when ImageFilter applied
assertContentDifferent(
array1 = originalBytes,
array2 = modifiedPixels,
message = "pixels with applied Blender should be different"
)
}
@Test
fun makeForBlender() = blenderTest {
val runtimeEffect = RuntimeEffect.makeForBlender("""
half4 main(half4 src, half4 dst) { return half4(0, 1, 0, 1); }
""".trimIndent())
runtimeEffect.makeBlender(null)
}
@Test
fun arithmetic() = blenderTest {
Blender.makeArithmetic(
k1 = 0.5f, k2 = 0.5f, k3 = 0.5f, k4 = 0.5f, enforcePMColor = true
)
}
@Test
fun mode() = blenderTest {
Blender.makeMode(BlendMode.CLEAR)
}
}
#include <jni.h>
#include "interop.hh"
#include "SkBlender.h"
#include "SkBlenders.h"
extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_BlenderKt__1nMakeArithmetic
(JNIEnv* env, jclass jclass, jfloat k1, jfloat k2, jfloat k3, jfloat k4, jboolean enforcePMColor) {
SkBlender* ptr = SkBlenders::Arithmetic(k1, k2, k3, k4, enforcePMColor).release();
return reinterpret_cast<jlong>(ptr);
}
extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_BlenderKt__1nMakeMode
(JNIEnv* env, jclass jclass, jint mode) {
SkBlender* ptr = SkBlender::Mode(static_cast<SkBlendMode>(mode)).release();
return reinterpret_cast<jlong>(ptr);
}
......@@ -2,6 +2,7 @@
#include <jni.h>
#include "SkColorFilter.h"
#include "SkImageFilter.h"
#include "SkBlender.h"
#include "SkMaskFilter.h"
#include "SkPaint.h"
#include "SkPathEffect.h"
......@@ -182,6 +183,19 @@ extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_PaintKt__1nSetImageFil
instance->setImageFilter(sk_ref_sp<SkImageFilter>(filter));
}
extern "C" JNIEXPORT jlong JNICALL Java_org_jetbrains_skia_PaintKt__1nGetBlender
(JNIEnv* env, jclass jclass, jlong ptr) {
SkPaint* instance = reinterpret_cast<SkPaint*>(static_cast<uintptr_t>(ptr));
return reinterpret_cast<jlong>(instance->refBlender().release());
}
extern "C" JNIEXPORT void JNICALL Java_org_jetbrains_skia_PaintKt__1nSetBlender
(JNIEnv* env, jclass jclass, jlong ptr, jlong blenderPtr) {
SkPaint* instance = reinterpret_cast<SkPaint*>(static_cast<uintptr_t>(ptr));
SkBlender* blender = reinterpret_cast<SkBlender*>(static_cast<uintptr_t>(blenderPtr));
instance->setBlender(sk_ref_sp<SkBlender>(blender));
}
extern "C" JNIEXPORT jint JNICALL Java_org_jetbrains_skia_PaintKt__1nGetBlendMode
(JNIEnv* env, jclass jclass, jlong ptr) {
SkPaint* instance = reinterpret_cast<SkPaint*>(static_cast<uintptr_t>(ptr));
......
......@@ -57,3 +57,29 @@ Java_org_jetbrains_skia_RuntimeEffectKt__1nMakeForColorFilter(JNIEnv* env,
return 0;
}
}
extern "C" JNIEXPORT jlong JNICALL
Java_org_jetbrains_skia_RuntimeEffectKt__1nMakeBlender(JNIEnv* env,
jclass jclass,
jlong ptr,
jlong uniformPtr) {
SkRuntimeEffect* runtimeEffect = jlongToPtr<SkRuntimeEffect*>(ptr);
SkData* uniform = jlongToPtr<SkData*>(uniformPtr);
sk_sp<SkBlender> blender = runtimeEffect->makeBlender(sk_ref_sp<SkData>(uniform));
return ptrToJlong(blender.release());
}
extern "C" JNIEXPORT jlong JNICALL
Java_org_jetbrains_skia_RuntimeEffectKt__1nMakeForBlender(JNIEnv* env,
jclass jclass,
jstring sksl) {
SkString skslProper = skString(env, sksl);
SkRuntimeEffect::Result result = SkRuntimeEffect::MakeForBlender(skslProper);
if (result.errorText.isEmpty()) {
return ptrToJlong(result.effect.release());
} else {
env->ThrowNew(java::lang::RuntimeException::cls, result.errorText.c_str());
return 0;
}
}
#include "SkBlender.h"
#include "SkBlenders.h"
#include "common.h"
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Blender__1nMakeArithmetic
(KFloat k1, KFloat k2, KFloat k3, KFloat k4, KBoolean enforcePMColor) {
SkBlender* ptr = SkBlenders::Arithmetic(k1, k2, k3, k4, enforcePMColor).release();
return reinterpret_cast<KNativePointer>(ptr);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Blender__1nMakeMode
(KInt mode) {
SkBlender* ptr = SkBlender::Mode(static_cast<SkBlendMode>(mode)).release();
return reinterpret_cast<KNativePointer>(ptr);
}
#include <iostream>
#include "SkColorFilter.h"
#include "SkImageFilter.h"
#include "SkBlender.h"
#include "SkMaskFilter.h"
#include "SkPaint.h"
#include "SkPathEffect.h"
......@@ -185,6 +186,19 @@ SKIKO_EXPORT void org_jetbrains_skia_Paint__1nSetImageFilter
instance->setImageFilter(sk_ref_sp<SkImageFilter>(filter));
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_Paint__1nGetBlender
(KNativePointer ptr) {
SkPaint* instance = reinterpret_cast<SkPaint*>((ptr));
return reinterpret_cast<KNativePointer>(instance->refBlender().release());
}
SKIKO_EXPORT void org_jetbrains_skia_Paint__1nSetBlender
(KNativePointer ptr, KNativePointer blenderPtr) {
SkPaint* instance = reinterpret_cast<SkPaint*>((ptr));
SkBlender* blender = reinterpret_cast<SkBlender*>((blenderPtr));
instance->setBlender(sk_ref_sp<SkBlender>(blender));
}
SKIKO_EXPORT KInt org_jetbrains_skia_Paint__1nGetBlendMode
(KNativePointer ptr) {
SkPaint* instance = reinterpret_cast<SkPaint*>((ptr));
......
......@@ -39,6 +39,24 @@ SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeEffect__1nMakeForColorFilt
return reinterpret_cast<KNativePointer>(result);
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeEffect__1nMakeBlender
(KNativePointer ptr, KNativePointer uniformPtr) {
SkRuntimeEffect* runtimeEffect = reinterpret_cast<SkRuntimeEffect*>(ptr);
SkData* uniform = reinterpret_cast<SkData*>(uniformPtr);
sk_sp<SkBlender> blender = runtimeEffect->makeBlender(sk_ref_sp<SkData>(uniform));
return reinterpret_cast<KNativePointer>(blender.release());
}
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeEffect__1nMakeForBlender
(KInteropPointer sksl) {
SkString skslProper = skString(sksl);
SkRuntimeEffect::Result* result = new SkRuntimeEffect::Result {
SkRuntimeEffect::MakeForBlender(skslProper)
};
return reinterpret_cast<KNativePointer>(result);
}
// Result
SKIKO_EXPORT KNativePointer org_jetbrains_skia_RuntimeEffect__1Result_nGetPtr
(KNativePointer ptr) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment