ejemplos - ¿Por qué Java está destrozando mis cursores semitransparentes?
javafx example (1)
A continuación hay dos imágenes PNG:
Visualmente son exactamente idénticos: la única diferencia es que uno tiene el fondo semitransparente en algunos de los píxeles (puede descargar las imágenes para verificarlo).
Pero cuando uso esas imágenes como cursor de imagen en nodos JavaFX, obtengo el siguiente resultado:
El primer cursor (sin píxeles parcialmente transparentes) sigue siendo nítido, pero el segundo se distorsiona.
Después de luchar con el problema por un tiempo, descubrí el algoritmo que explica esta diferencia: modo de fusión:
-
La forma "esperada" (que puede ver en este navegador, por ejemplo) es tomar la suma de valores por canal, ponderada por los valores alfa:
(1 - alpha) * background_color + alpha * foreground_color
. -
"JavaFX Cursor" proporciona la fórmula diferente:
(1 - alpha) * background_color + alpha^2 * foreground_color
(observe el cuadrado).
Descubrí la distorsión, pero no puedo entender qué hice mal y cómo puedo corregir este problema.
Aquí está el código fuente completo ejecutable para mi programa de prueba:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.scene.ImageCursor;
import javafx.scene.image.Image;
public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
System.out.println(ImageCursor.getBestSize(32, 32));
primaryStage.setTitle("Hello World!");
StackPane root = new StackPane();
root.setCursor(new ImageCursor(new Image("/test-cursor.png"), 0, 0));
primaryStage.setScene(new Scene(root, 100, 100));
primaryStage.show();
}
}
¿Cómo puedo lograr la representación adecuada de tales cursores semitransparentes?
Encontré una forma de solucionar el problema (probado en JDK 8 y Linux y Windows). Es feo y requiere reflexión, pero parece funcionar. Código a continuación (en sintaxis Scala, pero se puede adaptar fácilmente a Java):
import com.sun.prism.PixelFormat
import javafx.scene.ImageCursor
import javafx.scene.image.{Image, WritableImage}
private def undoPremultipliedAlpha(image: Image): Image = {
// Fixes JavaFX bug with semi-transparent cursors -
// somewhere deep in JavaFX code they premultiply alpha
// on already premultiplied image, which screws up transparencies.
// This method attempts to counteract it by removing premultiplied alpha
// directly from bytes of internal JavaFX image.
def getPlatformImage(image: Image) = image.impl_getPlatformImage()
val platformImage = getPlatformImage(image)
val pixelFormat = platformImage.getClass.getDeclaredMethod("getPixelFormat").invoke(platformImage).asInstanceOf[PixelFormat]
if (pixelFormat != PixelFormat.BYTE_BGRA_PRE) {
println(s"wrong platform image pixel format (${pixelFormat}), unable to apply cursor transparency bug workaround")
} else {
val pixelBufferField = platformImage.getClass.getDeclaredField("pixelBuffer")
pixelBufferField.setAccessible(true)
val pixelBuffer = pixelBufferField.get(platformImage).asInstanceOf[java.nio.Buffer]
val pixelArray = pixelBuffer.array().asInstanceOf[Array[Byte]]
for (i <- 0 until pixelArray.length / 4) {
val alpha = (pixelArray(i * 4 + 3).toInt & 0xff) / 255.0
if (alpha != 0) {
pixelArray(i * 4) = math.min(255, math.max(0, ((pixelArray(i * 4).toInt & 0xff).toDouble / alpha))).toInt.toByte
pixelArray(i * 4 + 1) = math.min(255, math.max(0, ((pixelArray(i * 4 + 1).toInt & 0xff).toDouble / alpha))).toInt.toByte
pixelArray(i * 4 + 2) = math.min(255, math.max(0, ((pixelArray(i * 4 + 2).toInt & 0xff).toDouble / alpha))).toInt.toByte
}
}
}
image
}
def createImageCursor(resource: String, hotspotX: Int, hotspotY: Int): ImageCursor = {
new ImageCursor(
undoPremultipliedAlpha(
new Image(resource)),
hotspotX,
hotspotY
)
}