Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve #768: Permit creating arbitrary color spaces #770

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 69 additions & 4 deletions skiko/src/commonMain/kotlin/org/jetbrains/skia/ColorSpace.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,35 @@ class ColorSpace : Managed {

val sRGB = ColorSpace(_nMakeSRGB(), false)
val sRGBLinear = ColorSpace(_nMakeSRGBLinear(), false)
val displayP3 = ColorSpace(_nMakeDisplayP3(), false)
val displayP3: ColorSpace = requireNotNull(makeRGB(TransferFunction.sRGB, Matrix33.displayP3ToXYZD50))

/**
* Creates a color space from a transfer function and a 3x3 transformation to XYZ D50.
* Matrix33 offers various factories for obtaining a toXYZD50 matrix.
*
* @throws IllegalArgumentException If the transfer function is invalid.
*/
fun makeRGB(transferFunction: TransferFunction, toXYZD50: Matrix33): ColorSpace {
return try {
Stats.onNativeCall()
val ptr = interopScope {
_nMakeRGB(toInterop(transferFunction.asArray()), toInterop(toXYZD50.mat))
}
require(ptr != NullPointer) { "Invalid transfer function" }
ColorSpace(ptr)
} finally {
reachabilityBarrier(transferFunction)
reachabilityBarrier(toXYZD50)
}
}

}


/**
* **Warning:** This method only converts between transfer functions while completely disregarding the gamut.
* Hence, the result will not be what you expect when the gamuts of the two color spaces differ!
*/
fun convert(toColor: ColorSpace?, color: Color4f): Color4f {
var to = toColor
to = to ?: sRGB
Expand Down Expand Up @@ -85,6 +110,37 @@ class ColorSpace : Managed {
reachabilityBarrier(this)
}

val transferFunction: TransferFunction
get() = try {
Stats.onNativeCall()
TransferFunction(withResult(FloatArray(7)) {
_nGetTransferFunction(_ptr, it)
})
} finally {
reachabilityBarrier(this)
}

/** The toXYZD50 matrix that converts from this color space's gamut to XYZ adapted to D50. */
val toXYZD50: Matrix33
get() = try {
Stats.onNativeCall()
Matrix33(*withResult(FloatArray(9)) {
_nGetToXYZD50(_ptr, it)
})
} finally {
reachabilityBarrier(this)
}

override fun nativeEquals(other: Native?): Boolean {
return try {
Stats.onNativeCall()
_nEquals(_ptr, getPtr(other))
} finally {
reachabilityBarrier(this)
reachabilityBarrier(other)
}
}

private object _FinalizerHolder {
val PTR = ColorSpace_nGetFinalizer()
}
Expand All @@ -99,12 +155,12 @@ private external fun _nConvert(fromPtr: NativePointer, toPtr: NativePointer, r:
@ExternalSymbolName("org_jetbrains_skia_ColorSpace__1nMakeSRGB")
private external fun _nMakeSRGB(): NativePointer

@ExternalSymbolName("org_jetbrains_skia_ColorSpace__1nMakeDisplayP3")
private external fun _nMakeDisplayP3(): NativePointer

@ExternalSymbolName("org_jetbrains_skia_ColorSpace__1nMakeSRGBLinear")
private external fun _nMakeSRGBLinear(): NativePointer

@ExternalSymbolName("org_jetbrains_skia_ColorSpace__1nMakeRGB")
private external fun _nMakeRGB(transferFunction: InteropPointer, toXYZD50: InteropPointer): NativePointer

@ExternalSymbolName("org_jetbrains_skia_ColorSpace__1nIsGammaCloseToSRGB")
private external fun _nIsGammaCloseToSRGB(ptr: NativePointer): Boolean

Expand All @@ -113,3 +169,12 @@ private external fun _nIsGammaLinear(ptr: NativePointer): Boolean

@ExternalSymbolName("org_jetbrains_skia_ColorSpace__1nIsSRGB")
private external fun _nIsSRGB(ptr: NativePointer): Boolean

@ExternalSymbolName("org_jetbrains_skia_ColorSpace__1nGetTransferFunction")
private external fun _nGetTransferFunction(ptr: NativePointer, result: InteropPointer): NativePointer

@ExternalSymbolName("org_jetbrains_skia_ColorSpace__1nGetToXYZD50")
private external fun _nGetToXYZD50(ptr: NativePointer, result: InteropPointer): NativePointer

@ExternalSymbolName("org_jetbrains_skia_ColorSpace__1nEquals")
private external fun _nEquals(ptr: NativePointer, otherPtr: NativePointer): Boolean
93 changes: 90 additions & 3 deletions skiko/src/commonMain/kotlin/org/jetbrains/skia/Matrix33.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package org.jetbrains.skia

import org.jetbrains.skia.impl.InteropPointer
import org.jetbrains.skia.impl.Library.Companion.staticLoad
import org.jetbrains.skia.impl.Stats
import org.jetbrains.skia.impl.withNullableResult
import org.jetbrains.skia.impl.withResult
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos
Expand All @@ -13,9 +18,8 @@ internal fun Float.toRadians(): Double = this.toDouble() / 180 * PI
* Point and vectors with translation, scaling, skewing, rotation, and
* perspective.
*
*
* Matrix includes a hidden variable that classifies the type of matrix to
* improve performance. Matrix is not thread safe unless getType() is called first.
* 3x3 matrices are also used to characterize the gamut of a color space in the form
* of a conversion matrix to XYZ D50. We call this a toXYZD50 matrix for short.
*
* @see [https://fiddle.skia.org/c/@Matrix_063](https://fiddle.skia.org/c/@Matrix_063)
*/
Expand Down Expand Up @@ -264,10 +268,93 @@ class Matrix33(vararg mat: Float) {
fun makeSkew(sx: Float, sy: Float): Matrix33 {
return Matrix33(1f, sx, 0f, sy, 1f, 0f, 0f, 0f, 1f)
}

init {
staticLoad()
}

// We defensively copy the preset arrays, as otherwise, Matrix33 would expose them to potential mutation:

/** A toXYZD50 matrix to convert sRGB color into XYZ adapted to D50. Use it to create a color space. */
val sRGBToXYZD50 get() = Matrix33(*_sRGBToXYZD50)

/** A toXYZD50 matrix to convert Adobe RGB color into XYZ adapted to D50. Use it to create a color space. */
val adobeRGBToXYZD50 get() = Matrix33(*_adobeRGBToXYZD50)

/** A toXYZD50 matrix to convert Display P3 color into XYZ adapted to D50. Use it to create a color space. */
val displayP3ToXYZD50 get() = Matrix33(*_displayP3ToXYZD50)

/** A toXYZD50 matrix to convert Rec.2020 color into XYZ adapted to D50. Use it to create a color space. */
val rec2020ToXYZD50 get() = Matrix33(*_rec2020ToXYZD50)

/** A toXYZD50 identity matrix. Use it to create a color space. */
val xyzD50ToXYZD50 get() = Matrix33(*_xyzD50ToXYZD50)

private val _sRGBToXYZD50 = withResult(FloatArray(9)) { _nGetSRGB(it) }
private val _adobeRGBToXYZD50 = withResult(FloatArray(9)) { _nGetAdobeRGB(it) }
private val _displayP3ToXYZD50 = withResult(FloatArray(9)) { _nGetDisplayP3(it) }
private val _rec2020ToXYZD50 = withResult(FloatArray(9)) { _nGetRec2020(it) }
private val _xyzD50ToXYZD50 = withResult(FloatArray(9)) { _nGetXYZ(it) }

/**
* Returns a toXYZD50 matrix to adapt XYZ color from given the whitepoint to D50.
* Use it to create a color space.
*
* @throws IllegalArgumentException If the white point is invalid.
*/
fun makeXYZToXYZD50(wx: Float, wy: Float): Matrix33 {
Stats.onNativeCall()
val array = withNullableResult(FloatArray(9)) {
_nAdaptToXYZD50(wx, wy, it)
}
requireNotNull(array) { "Cannot find transformation from the white point to D50" }
return Matrix33(*array)
}

/**
* Returns a toXYZD50 matrix to convert RGB color into XYZ adapted to D50,
* given the primaries and whitepoint of the RGB model.
* Use it to create a color space.
*
* @throws IllegalArgumentException If the primaries or white point are invalid.
*/
fun makePrimariesToXYZD50(
rx: Float, ry: Float, gx: Float, gy: Float, bx: Float, by: Float, wx: Float, wy: Float
): Matrix33 {
Stats.onNativeCall()
val array = withNullableResult(FloatArray(9)) {
_nPrimariesToXYZD50(rx, ry, gx, gy, bx, by, wx, wy, it)
}
requireNotNull(array) { "Cannot find transformation from the primaries and white point to XYZ D50" }
return Matrix33(*array)
}
}

init {
require(mat.size == 9) { "Expected 9 elements, got ${mat.size}" }
this.mat = mat
}
}

@ExternalSymbolName("org_jetbrains_skia_Matrix33__1nGetSRGB")
private external fun _nGetSRGB(result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_Matrix33__1nGetAdobeRGB")
private external fun _nGetAdobeRGB(result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_Matrix33__1nGetDisplayP3")
private external fun _nGetDisplayP3(result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_Matrix33__1nGetRec2020")
private external fun _nGetRec2020(result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_Matrix33__1nGetXYZ")
private external fun _nGetXYZ(result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_Matrix33__1nAdaptToXYZD50")
private external fun _nAdaptToXYZD50(wx: Float, wy: Float, result: InteropPointer): Boolean

@ExternalSymbolName("org_jetbrains_skia_Matrix33__1nPrimariesToXYZD50")
private external fun _nPrimariesToXYZD50(
rx: Float, ry: Float, gx: Float, gy: Float, bx: Float, by: Float, wx: Float, wy: Float, result: InteropPointer
): Boolean
167 changes: 167 additions & 0 deletions skiko/src/commonMain/kotlin/org/jetbrains/skia/TransferFunction.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package org.jetbrains.skia

import org.jetbrains.skia.impl.*
import org.jetbrains.skia.impl.Library.Companion.staticLoad

/**
* A transfer function that maps encoded values to linear values,
* represented by this 7-parameter piecewise function:
*
* linear = sign(encoded) * (c*|encoded| + f) , 0 <= |encoded| < d
* = sign(encoded) * ((a*|encoded| + b)^g + e), d <= |encoded|
*
* A simple gamma transfer function sets g = gamma, a = 1, and the rest = 0.
*/
class TransferFunction(
val g: Float,
val a: Float,
val b: Float,
val c: Float,
val d: Float,
val e: Float,
val f: Float
) {

companion object {

init {
staticLoad()
}

val sRGB = TransferFunction(withResult(FloatArray(7)) { _nGetSRGB(it) })
val gamma2Dot2 = TransferFunction(withResult(FloatArray(7)) { _nGetGamma2Dot2(it) })
val linear = TransferFunction(withResult(FloatArray(7)) { _nGetLinear(it) })
val rec2020 = TransferFunction(withResult(FloatArray(7)) { _nGetRec2020(it) })
val pq = TransferFunction(withResult(FloatArray(7)) { _nGetPQ(it) })
val hlg = TransferFunction(withResult(FloatArray(7)) { _nGetHLG(it) })

/**
* General form of the SMPTE ST 2084 PQ function.
*
* max(A + B|encoded|^C, 0)
* linear = sign(encoded) * (------------------------) ^ F
* D + E|encoded|^C
*/
fun makePQish(A: Float, B: Float, C: Float, D: Float, E: Float, F: Float): TransferFunction {
Stats.onNativeCall()
return TransferFunction(withResult(FloatArray(7)) {
_nMakePQish(A, B, C, D, E, F, it)
})
}

/**
* General form of HLG.
*
* { K * sign(encoded) * ( (R|encoded|)^G ) when 0 <= |encoded| <= 1/R
* linear = { K * sign(encoded) * ( e^(a(|encoded|-c)) + b ) when 1/R < |encoded|
*/
fun makeScaledHLGish(K: Float, R: Float, G: Float, a: Float, b: Float, c: Float): TransferFunction {
Stats.onNativeCall()
return TransferFunction(withResult(FloatArray(7)) {
_nMakeScaledHLGish(K, R, G, a, b, c, it)
})
}

}

val type: TransferFunctionType
get() = try {
Stats.onNativeCall()
TransferFunctionType.values()[interopScope {
_nGetType(toInterop(asArray()))
}]
} finally {
reachabilityBarrier(this)
}

fun eval(x: Float): Float {
return try {
Stats.onNativeCall()
interopScope {
_nEval(toInterop(asArray()), x)
}
} finally {
reachabilityBarrier(this)
}
}

fun invert(): TransferFunction? {
return try {
Stats.onNativeCall()
withNullableResult(FloatArray(7)) {
_nInvert(toInterop(asArray()), it)
}?.let { TransferFunction(it) }
} finally {
reachabilityBarrier(this)
}
}

override fun equals(other: Any?): Boolean {
if (other === this) return true
if (other !is TransferFunction) return false
if (g != other.g) return false
if (a != other.a) return false
if (b != other.b) return false
if (c != other.c) return false
if (d != other.d) return false
if (e != other.e) return false
return f == other.f
}

override fun hashCode(): Int {
val PRIME = 59
var result = 1
result = result * PRIME + g.toBits()
result = result * PRIME + a.toBits()
result = result * PRIME + b.toBits()
result = result * PRIME + c.toBits()
result = result * PRIME + d.toBits()
result = result * PRIME + e.toBits()
result = result * PRIME + f.toBits()
return result
}

override fun toString(): String {
return "TransferFunction(_g=$g, _a=$a, _b=$b, _c=$c, _d=$d, _e=$e, _f=$f)"
}

internal constructor(array: FloatArray) : this(array[0], array[1], array[2], array[3], array[4], array[5], array[6])

internal fun asArray() = floatArrayOf(g, a, b, c, d, e, f)

}

@ExternalSymbolName("org_jetbrains_skia_TransferFunction__1nGetSRGB")
private external fun _nGetSRGB(result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_TransferFunction__1nGetGamma2Dot2")
private external fun _nGetGamma2Dot2(result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_TransferFunction__1nGetLinear")
private external fun _nGetLinear(result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_TransferFunction__1nGetRec2020")
private external fun _nGetRec2020(result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_TransferFunction__1nGetPQ")
private external fun _nGetPQ(result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_TransferFunction__1nGetHLG")
private external fun _nGetHLG(result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_TransferFunction__1nMakePQish")
private external fun _nMakePQish(A: Float, B: Float, C: Float, D: Float, E: Float, F: Float, result: InteropPointer)

@ExternalSymbolName("org_jetbrains_skia_TransferFunction__1nMakeScaledHLGish")
private external fun _nMakeScaledHLGish(
K: Float, R: Float, G: Float, a: Float, b: Float, c: Float, result: InteropPointer
)

@ExternalSymbolName("org_jetbrains_skia_TransferFunction__1nGetType")
private external fun _nGetType(transferFunction: InteropPointer): Int

@ExternalSymbolName("org_jetbrains_skia_TransferFunction__1nEval")
private external fun _nEval(transferFunction: InteropPointer, x: Float): Float

@ExternalSymbolName("org_jetbrains_skia_TransferFunction__1nInvert")
private external fun _nInvert(transferFunction: InteropPointer, result: InteropPointer): Boolean
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.jetbrains.skia

enum class TransferFunctionType {
INVALID,
SRGB_ISH,
PQ_ISH,
HLG_ISH,
HLG_INV_ISH;
}
Loading