Warum zerfetzt Javafx meine halbtransparenten Cursor?

75

Unten sind zwei PNG-Bilder:

Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein

Optisch sind sie genau identisch - der einzige Unterschied besteht darin, dass einige Pixel einen halbtransparenten Hintergrund haben (Sie können die Bilder herunterladen, um sie zu überprüfen).

Wenn ich diese Bilder jedoch als Bildcursor auf JavaFX-Knoten verwende, erhalte ich das folgende Ergebnis:

Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein

Der erste Cursor (ohne teilweise transparente Pixel) ist immer noch scharf, der zweite wird jedoch verzerrt.

Nachdem ich eine Weile mit dem Problem gekämpft hatte, entdeckte ich den Algorithmus, der diesen Unterschied erklärt - den Mischmodus:

  • Der "erwartete" Weg (den Sie beispielsweise in diesem Browser sehen können) besteht darin, die Summe der Werte pro Kanal zu nehmen, gewichtet mit Alpha-Werten : (1 - alpha) * background_color + alpha * foreground_color.

  • "JavaFX Cursor" gibt die andere Formel an: (1 - alpha) * background_color + alpha^2 * foreground_color(beachten Sie das Quadrat).

Ich habe die Verzerrung entdeckt, kann aber nicht herausfinden, was ich falsch gemacht habe und wie ich dieses Problem beheben kann.

Hier ist der vollständige ausführbare Quellcode für mein Testprogramm:

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();
    }
}

Wie kann ich solche halbtransparenten Cursor richtig rendern?

Rogach
quelle
2
Haben Sie auf stackoverflow.com/questions/39692625/…
Tschallacka
2
@ Tschallacka - Ja, ich habe es getan. Der JFXCustom-Cursor ist in Bezug auf die Leistung nicht gut (er ruckelt mit der Anwendung, während der native Cursor reibungslos funktioniert), und das Beispiel in der von Ihnen verknüpften Frage weist das gleiche Problem auf: Die weiße Farbe wird im halbtransparenten Zustand als schwarz dargestellt. Hier ist ein etwas neu geschrieben Code von dieser Frage, dass Highlights des Problem: pastebin.com/G5D0wK80
Rogach
2
Rastergrafiken sollten nicht in Grafikelementen wie Cursorn verwendet werden. Das von Ihnen verwendete Bild wird vergrößert und somit verzerrt. Versuchen Sie, mehrere Bilder mit unterschiedlichen Abmessungen hinzuzufügen, und verwenden Sie die ImageCursor.chooseBestCursor()Funktion. Für mich hat ein Bild mit den Abmessungen 240 x 240 Pixel großartig funktioniert.
11
@ Rogach Könnten Sie einen Fehler dafür melden und das JavaFX-Team einen Blick darauf werfen lassen?
ItachiUchiha

Antworten:

1

UPDATE : Bei genauerer Betrachtung scheint JavaFX nicht fehlerhaft zu sein - der Fehler scheint in Implementierungen von Videotreibern zu liegen. Der folgende Code funktioniert auf einigen Kombinationen von Hardware, Treibern und Betriebssystemen - jedoch nicht auf allen.

Leider scheint es derzeit die beste Lösung zu sein, Cursor mit teilweise transparenten weißen oder grauen Pixeln zu vermeiden. Teiltransparente schwarze Pixel sind jedoch in Ordnung.


Ich habe einen Weg gefunden, um das Problem zu umgehen (getestet unter JDK 8 und Linux & Windows). Es ist hässlich und erfordert Reflexion, scheint aber zu funktionieren. Code unten (in Scala-Syntax, kann aber leicht an Java angepasst werden):

  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
    )
  }


Rogach
quelle