Renderizado borroso de SwingNode en JavaFX en Windows

Aug 17 2020

Visión general

Usar FlyingSaucer dentro de una aplicación JavaFX, para evitar WebView por varias razones:

  • no proporciona acceso API directo a sus barras de desplazamiento para un comportamiento sincrónico;
  • paquetes de JavaScript, que es una gran cantidad para mi caso de uso; y
  • no se pudo ejecutar en Windows.

FlyingSaucer usa Swing, que requiere envolver su XHTMLPanel(una subclase de JPanel) en un SwingNodepara usar junto con JavaFX. Todo funciona muy bien, la aplicación muestra Markdown en tiempo real y responde. Aquí hay un video de demostración de la aplicación que se ejecuta en Linux.

Problema

La representación del texto en Windows es borrosa. Cuando se ejecuta en a JFrame, no envuelto por a SwingNode, pero sigue siendo parte de la misma aplicación que se muestra en el video, la calidad del texto es impecable. La captura de pantalla muestra la ventana principal de la aplicación (abajo), que incluye SwingNodejunto con la antes mencionada JFrame(arriba). Es posible que deba hacer zoom en el borde recto de la "l" o la "k" para ver por qué una es nítida y la otra borrosa:

Esto solo ocurre en Windows. Al visualizar la fuente en Windows a través del programa de vista previa de fuentes del sistema, las fuentes se suavizan con colores LCD. La aplicación usa escala de grises. Sospecho que si hay una manera de forzar el renderizado a usar color para suavizado en lugar de escala de grises, el problema puede desaparecer. Por otra parte, cuando se ejecuta por sí solo JFrame, no hay problema y los colores de la pantalla LCD no se utilizan.

Código

Aquí está el código para el JFrameque tiene un renderizado perfecto:

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

El código para el desenfoque SwingNodees un poco más complicado (consulte la lista completa ), pero aquí hay algunos fragmentos relevantes (tenga en cuenta que se HTMLPanelextiende desde XHTMLPanelsolo para suprimir algunos desplazamientos automáticos no deseados durante las actualizaciones):

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

Ejemplo de trabajo mínimo

Aquí hay un ejemplo autónomo bastante mínimo:

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

Captura borrosa del ejemplo de trabajo mínimo; al acercar, los bordes de las letras están muy suavizados en lugar de contrastes nítidos:

El uso de a JLabeltambién exhibe el mismo renderizado difuso:

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

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

Intentos

Estas son la mayoría de las formas en que he intentado eliminar el desenfoque.

Java

En el lado de Java, alguien sugirió ejecutar la aplicación usando:

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

Ninguna de las sugerencias de representación de texto ha ayudado.

Establecer el contenido del SwingNodeinterior SwingUtilities.invokeLaterno tiene ningún efecto.

JavaFX

Alguien más mencionó que desactivar el almacenamiento en caché ayudó, pero eso fue para JavaFX ScrollPane, no para uno dentro de SwingNode. No funcionó.

El JScrollPanecontenido de SwingNodetiene su alineación X y alineación Y configuradas en 0.5 y 0.5, respectivamente. En otros lugares se recomienda garantizar un desplazamiento de medio píxel . No puedo imaginar que establecer el Sceneuso StrokeType.INSIDEharía alguna diferencia, aunque intenté usar un ancho de trazo de 1 sin éxito.

Platillo volador

FlyingSaucer tiene varias opciones de configuración . Varias combinaciones de configuraciones incluyen:

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 ...

La xr.image.configuración solo afecta a las imágenes renderizadas por FlyingSaucer, en lugar de cómo la salida de FlyingSaucer es renderizada por JavaFX dentro de SwingNode.

El CSS usa puntos para los tamaños de fuente.

Investigación

  • https://stackoverflow.com/a/26227562/59087 - Parece que algunas soluciones pueden ser útiles.
  • https://bugs.openjdk.java.net/browse/JDK-8089499- no parece aplicarse porque está usando SwingNodey JScrollPane.
  • https://stackoverflow.com/a/24124020/59087 - probablemente no sea relevante porque no hay un generador de escenas XML en uso.
  • https://www.cs.mcgill.ca/media/tech_reports/42_Lessons_Learned_in_Migrating_from_Swing_to_JavaFX_LzXl9Xv.pdf - la página 8 describe el cambio de 0,5 píxeles, pero ¿cómo?
  • https://dlsc.com/2014/07/17/javafx-tip-9-do-not-mix-swing-javafx/ - sugiere no mezclar JavaFX y Swing, pero pasar a Swing puro no es una opción: preferiría reescribir la aplicación en otro idioma.

Aceptado como error contra OpenJDK / JavaFX:

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

JDK y JRE

Usando OpenJDK de Bellsoft con JavaFX incluido. Que yo sepa, OpenJDK ha tenido soporte Freetype por un tiempo. Además, la fuente se ve muy bien en Linux, por lo que probablemente no sea el JDK.

Pantalla

Las siguientes especificaciones de pantalla presentan el problema, pero otras personas (sin duda, que ven en diferentes monitores y resoluciones) han mencionado el problema.

  • 15,6 "4: 3 HD (1366 x 768)
  • Full HD (1920 x 1080)
  • Retroiluminación LED de ángulo de visión amplio
  • ASUS n56v

Pregunta

¿Por qué FlyingSaucer XHTMLPanelcuando se envuelve se SwingNodevuelve borroso en Windows y, sin embargo, mostrar lo mismo XHTMLPanelen una JFrameejecución en la misma aplicación JavaFX parece nítido? ¿Cómo se puede solucionar el problema?

El problema involucra SplitPane.

Respuestas

1 mipa Aug 19 2020 at 19:17

Hay algunas opciones que podrías probar, aunque debo admitir que no conozco FlyingSaucer y su API.

FlyingSaucer tiene diferentes renderizadores. Por lo tanto, podría ser posible evitar el renderizado de Swing / AWT por completo utilizando esta biblioteca en su lugar para hacer todo el renderizado directamente en JavaFX.https://github.com/jfree/fxgraphics2d

Otra posibilidad es permitir que FlyingSaucer se renderice en una imagen que pueda mostrarse en JavaFX de manera muy eficiente a través de búferes directos. Vea el código AWTImage en mi repositorio aquí:https://github.com/mipastgt/JFXToolsAndDemos

1 weisj Aug 20 2020 at 01:48

No pude reproducir el problema por mí mismo, por lo que puede haber algún problema en la combinación de la versión de JDK / JavaFX que está utilizando. También es posible que el problema solo surja con una combinación específica de tamaño de pantalla y escala de pantalla.

Mi configuración es la siguiente:

  • 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

El problema ha sido aceptado como un error contra OpenJDK / JavaFX:

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

Ninguna de las sugerencias de Mipa funcionaría en la práctica. FlyingSaucer está estrechamente integrado con a JScrollPane, lo que excluye la posibilidad de obligar a FlyingSaucer a renderizar en un panel basado en JavaFX.

Otra posibilidad es ir en la dirección opuesta: crear una aplicación Swing e incrustar controles JavaFX, como usar un JFXPanel ; sin embargo, parecería más prudente aceptar el comportamiento borroso hasta que se elimine el error.