Verschwommenes Rendern von SwingNode in JavaFX unter Windows

Aug 17 2020

Überblick

Verwenden von FlyingSaucer in einer JavaFX-Anwendung, um WebView aus verschiedenen Gründen zu vermeiden:

  • bietet keinen direkten API-Zugriff auf die Bildlaufleisten für synchrones Verhalten;
  • bündelt JavaScript, was für meinen Anwendungsfall eine große Aufblähung darstellt; und
  • konnte unter Windows nicht ausgeführt werden.

FlyingSaucer verwendet Swing, für das es erforderlich ist, seine XHTMLPanel(eine Unterklasse von JPanel) in a SwingNodezu verpacken , um sie zusammen mit JavaFX zu verwenden. Alles funktioniert hervorragend, die Anwendung rendert Markdown in Echtzeit und reagiert. Hier ist ein Demo-Video der Anwendung, die unter Linux ausgeführt wird.

Problem

Die Textwiedergabe unter Windows ist verschwommen. Wenn Sie in einer JFrameDatei arbeiten, die nicht von einer Datei umschlossen ist SwingNode, aber dennoch Teil derselben im Video gezeigten Anwendung ist, ist die Qualität des Textes einwandfrei. Die Bildschirmaufnahme zeigt das Hauptfenster der Anwendung (unten), das das SwingNodezusammen mit dem oben genannten JFrame(oben) enthält. Möglicherweise müssen Sie in die gerade Kante des "l" oder "k" zoomen, um zu sehen, warum das eine scharf und das andere verschwommen ist:

Dies geschieht nur unter Windows. Wenn Sie die Schriftart unter Windows über das Schriftartenvorschau-Programm des Systems anzeigen, werden die Schriftarten mithilfe von LCD-Farben antialiasiert. Die Anwendung verwendet Graustufen. Ich vermute, dass das Problem möglicherweise verschwindet, wenn es eine Möglichkeit gibt, das Rendern zu zwingen, Farbe für Antialiasing anstelle von Graustufen zu verwenden. Andererseits JFramegibt es kein Problem , wenn es alleine läuft , und es werden keine LCD-Farben verwendet.

Code

Hier ist der Code für den JFrame, der ein perfektes Rendering hat:

  private static class Flawless {
    private final XHTMLPanel panel = new XHTMLPanel();
    private final JFrame frame = new JFrame( "Single Page Demo" );

    private Flawless() {
      frame.getContentPane().add( new JScrollPane( panel ) );
      frame.pack();
      frame.setSize( 1024, 768 );
    }

    private void update( final org.w3c.dom.Document html ) {
      frame.setVisible( true );

      try {
        panel.setDocument( html );
      } catch( Exception ignored ) {
      }
    }
  }

Der Code für die Unschärfe SwingNodeist etwas komplizierter (siehe vollständige Auflistung ), aber hier sind einige relevante Ausschnitte (Hinweis, der HTMLPanelsich XHTMLPanelnur erstreckt , um unerwünschtes Autoscrolling während Updates zu unterdrücken):

private final HTMLPanel mHtmlRenderer = new HTMLPanel();
private final SwingNode mSwingNode = new SwingNode();
private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );

// ...

final var context = getSharedContext();
final var textRenderer = context.getTextRenderer();
textRenderer.setSmoothingThreshold( 0 );

mSwingNode.setContent( mScrollPane );

// ...

// The "preview pane" contains the SwingNode.
final SplitPane splitPane = new SplitPane(
    getDefinitionPane().getNode(),
    getFileEditorPane().getNode(),
    getPreviewPane().getNode() );

Minimales Arbeitsbeispiel

Hier ist ein ziemlich minimales, in sich geschlossenes Beispiel:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingNode;
import javafx.scene.Scene;
import javafx.scene.control.SplitPane;
import javafx.stage.Stage;
import org.jsoup.Jsoup;
import org.jsoup.helper.W3CDom;
import org.xhtmlrenderer.simple.XHTMLPanel;

import javax.swing.*;

import static javax.swing.SwingUtilities.invokeLater;
import static javax.swing.UIManager.getSystemLookAndFeelClassName;
import static javax.swing.UIManager.setLookAndFeel;

public class FlyingSourceTest extends Application {
  private final static String HTML = "<!DOCTYPE html><html><head" +
      "><style type='text/css'>body{font-family:serif; background-color: " +
      "#fff; color:#454545;}</style></head><body><p style=\"font-size: " +
      "300px\">TEST</p></body></html>";

  public static void main( String[] args ) {
    Application.launch( args );
  }

  @Override
  public void start( Stage primaryStage ) {
    invokeLater( () -> {
      try {
        setLookAndFeel( getSystemLookAndFeelClassName() );
      } catch( Exception ignored ) {
      }

      primaryStage.setTitle( "Hello World!" );

      final var renderer = new XHTMLPanel();
      renderer.getSharedContext().getTextRenderer().setSmoothingThreshold( 0 );
      renderer.setDocument( new W3CDom().fromJsoup( Jsoup.parse( HTML ) ) );

      final var swingNode = new SwingNode();
      swingNode.setContent( new JScrollPane( renderer ) );

      final var root = new SplitPane( swingNode, swingNode );

      // ----------
      // Here be dragons? Using a StackPane, instead of a SplitPane, works.
      // ----------
      //StackPane root = new StackPane();
      //root.getChildren().add( mSwingNode );

      Platform.runLater( () -> {
        primaryStage.setScene( new Scene( root, 300, 250 ) );
        primaryStage.show();
      } );
    } );
  }
}

Verschwommene Erfassung anhand des minimalen Arbeitsbeispiels; Beim Vergrößern werden die Buchstabenkanten stärker antialiasiert als scharfe Kontraste:

Die Verwendung von a JLabelzeigt auch das gleiche Fuzzy-Rendering:

  final var label = new JLabel( "TEST" );
  label.setFont( label.getFont().deriveFont( Font.BOLD, 128f ) );

  final var swingNode = new SwingNode();
  swingNode.setContent( label );

Versuche

Hier sind die meisten Möglichkeiten, wie ich versucht habe, die Unschärfe zu entfernen.

Java

Auf der Java-Seite schlug jemand vor , die Anwendung folgendermaßen auszuführen:

-Dawt.useSystemAAFontSettings=off
-Dswing.aatext=false

Keiner der Hinweise zum Rendern von Text hat geholfen.

Das Einstellen des Inhalts des SwingNodeInneren SwingUtilities.invokeLaterhat keine Auswirkung.

JavaFX

Jemand anderes erwähnte, dass das Deaktivieren des Cachings hilfreich war, aber das war für ein JavaFX ScrollPane, nicht für eines innerhalb eines SwingNode. Es hat nicht funktioniert.

Die JScrollPaneenthaltenen durch das SwingNodehat seine Ausrichtung X und Y Ausrichtung auf 0,5 bzw. 0,5. An anderer Stelle wird empfohlen, einen Versatz von einem halben Pixel sicherzustellen . Ich kann mir nicht vorstellen, dass das Einstellen der Scenezu verwendenden Funktion StrokeType.INSIDEeinen Unterschied machen würde, obwohl ich vergeblich versucht habe, eine Strichbreite von 1 zu verwenden.

Fliegende Untertasse

FlyingSaucer bietet eine Reihe von Konfigurationsoptionen . Verschiedene Kombinationen von Einstellungen umfassen:

java -Dxr.text.fractional-font-metrics=true \
     -Dxr.text.aa-smoothing-level=0 \
     -Dxr.image.render-quality=java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC
     -Dxr.image.scale=HIGH \
     -Dxr.text.aa-rendering-hint=VALUE_TEXT_ANTIALIAS_GASP -jar ...

Die xr.image.Einstellungen wirken sich nur auf Bilder aus, die von FlyingSaucer gerendert werden, und nicht darauf, wie die Ausgabe von FlyingSaucer von JavaFX innerhalb von FlyingSaucer gerendert wird SwingNode.

Das CSS verwendet Punkte für die Schriftgrößen.

Forschung

  • https://stackoverflow.com/a/26227562/59087 - Es sieht so aus, als ob einige Lösungen hilfreich sein könnten.
  • https://bugs.openjdk.java.net/browse/JDK-8089499- scheint nicht zuzutreffen, da dies SwingNodeund verwendet JScrollPane.
  • https://stackoverflow.com/a/24124020/59087 - wahrscheinlich nicht relevant, da kein XML-Szenen-Builder verwendet wird.
  • https://www.cs.mcgill.ca/media/tech_reports/42_Lessons_Learned_in_Migrating_from_Swing_to_JavaFX_LzXl9Xv.pdf - Seite 8 beschreibt die Verschiebung um 0,5 Pixel, aber wie?
  • https://dlsc.com/2014/07/17/javafx-tip-9-do-not-mix-swing-javafx/ - schlägt vor, JavaFX und Swing nicht zu mischen, aber die Umstellung auf reines Swing ist keine Option: Ich würde die App eher in einer anderen Sprache umschreiben.

Als Fehler gegen OpenJDK / JavaFX akzeptiert:

  • https://bugs.openjdk.java.net/browse/JDK-8252255

JDK & JRE

Verwenden von Bellsofts OpenJDK mit JavaFX-Bundle. Meines Wissens hat das OpenJDK seit einiger Zeit Freetype-Unterstützung. Außerdem sieht die Schriftart unter Linux großartig aus, sodass es sich wahrscheinlich nicht um das JDK handelt.

Bildschirm

Die folgenden Bildschirmspezifikationen zeigen das Problem, aber andere Personen (zweifellos auf verschiedenen Monitoren und Auflösungen) haben das Problem erwähnt.

  • 15,6 "4: 3 HD (1366 x 768)
  • Full HD (1920 x 1080)
  • Weitwinkel-LED-Hintergrundbeleuchtung
  • ASUS n56v

Frage

Warum funktioniert FlyingSaucer die , XHTMLPanelwenn sie innerhalb gewickelt SwingNodewerden verschwommen auf Windows, und doch Anzeige desselben XHTMLPanelin ein JFramein der gleichen JavaFX laufenden Anwendung erscheint knackig? Wie kann das Problem behoben werden?

Das Problem betrifft SplitPane.

Antworten

1 mipa Aug 19 2020 at 19:17

Es gibt einige Optionen, die Sie ausprobieren könnten, obwohl ich zugeben muss, dass ich FlyingSaucer und seine API nicht kenne.

FlyingSaucer hat verschiedene Renderer. Daher ist es möglicherweise möglich, das Swing / AWT-Rendering vollständig zu vermeiden, indem Sie stattdessen diese Bibliothek verwenden, um das gesamte Rendering direkt in JavaFX durchzuführen.https://github.com/jfree/fxgraphics2d

Eine andere Möglichkeit besteht darin, FlyingSaucer in ein Bild rendern zu lassen, das über direkte Puffer sehr effizient in JavaFX angezeigt werden kann. Den AWTImage-Code in meinem Repository finden Sie hier:https://github.com/mipastgt/JFXToolsAndDemos

1 weisj Aug 20 2020 at 01:48

Ich konnte das Problem nicht selbst reproduzieren, daher liegt möglicherweise ein Problem in der Kombination der von Ihnen verwendeten JDK / JavaFX-Version vor. Es ist auch möglich, dass das Problem nur bei einer bestimmten Kombination aus Anzeigegröße und Bildschirmskalierung auftritt.

Mein Setup ist das folgende:

  • JavaFX 14
  • OpenJDK 14
import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingNode;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.jsoup.Jsoup;
import org.jsoup.helper.W3CDom;
import org.jsoup.nodes.Document;
import org.xhtmlrenderer.simple.XHTMLPanel;

import javax.swing.*;

public class FlyingSourceTest extends Application {

    private final static String HTML_PREFIX = "<!DOCTYPE html>\n"
            + "<html>\n"
            + "<body>\n";
    private static final String HTML_CONTENT =
            "<p style=\"font-size:500px\">TEST</p>";
    private final static String HTML_SUFFIX = "<p style='height=2em'>&nbsp;</p></body></html>";

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        SwingUtilities.invokeLater(() -> {
            try {
                UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) {
                e.printStackTrace();
            }
            primaryStage.setTitle("Hello World!");

            XHTMLPanel mHtmlRenderer = new XHTMLPanel();
            mHtmlRenderer.getSharedContext().getTextRenderer().setSmoothingThreshold(0);
            SwingNode mSwingNode = new SwingNode();
            JScrollPane mScrollPane = new JScrollPane(mHtmlRenderer);

            String htmlContent = HTML_PREFIX + HTML_CONTENT + HTML_SUFFIX;
            Document jsoupDoc = Jsoup.parse(htmlContent);
            org.w3c.dom.Document w3cDoc = new W3CDom().fromJsoup(jsoupDoc);

            mHtmlRenderer.setDocument(w3cDoc);

            mSwingNode.setContent(mScrollPane);
//            AnchorPane anchorPane = new AnchorPane();
//            anchorPane.getChildren().add(mSwingNode);
//            AnchorPane.setTopAnchor(mSwingNode, 0.5);
//            AnchorPane.setLeftAnchor(mSwingNode, 0.5);
//            mSwingNode.setTranslateX(0.5);
//            mSwingNode.setTranslateY(0.5);

            StackPane root = new StackPane();
            root.getChildren().add(mSwingNode);
            Platform.runLater(() -> {
                primaryStage.setScene(new Scene(root, 300, 250));
                primaryStage.show();
            });
        });
    }
}
DaveJarvis Aug 27 2020 at 01:33

Das Problem wurde als Fehler gegen OpenJDK / JavaFX akzeptiert:

  • https://bugs.openjdk.java.net/browse/JDK-8252255

Keiner der Vorschläge von Mipa würde in der Praxis funktionieren. FlyingSaucer ist eng in a integriert JScrollPane, was die Möglichkeit ausschließt, FlyingSaucer zum Rendern auf einem JavaFX-basierten Panel zu zwingen.

Eine andere Möglichkeit besteht darin, in die entgegengesetzte Richtung zu gehen: Erstellen Sie eine Swing-Anwendung und binden Sie JavaFX-Steuerelemente ein, z. B. mithilfe eines JFXPanel . Es erscheint jedoch vorsichtiger, das verschwommene Verhalten zu akzeptieren, bis der Fehler behoben ist.