Renderização borrada de SwingNode em JavaFX no Windows

Aug 17 2020

Visão geral

Usando o FlyingSaucer em um aplicativo JavaFX, para evitar o WebView por vários motivos:

  • não fornece acesso direto à API para suas barras de rolagem para comportamento síncrono;
  • agrupa JavaScript, que é um grande inchaço para o meu caso de uso; e
  • Falha ao executar no Windows.

FlyingSaucer usa Swing, que requer o empacotamento de seu XHTMLPanel(uma subclasse de JPanel) em um SwingNodepara usar junto com o JavaFX. Tudo funciona muito bem, o aplicativo renderiza Markdown em tempo real e é responsivo. Aqui está um vídeo de demonstração do aplicativo em execução no Linux.

Problema

A renderização do texto no Windows está borrada. Quando executado em a JFrame, não envolvido por a SwingNode, mas ainda parte do mesmo aplicativo mostrado no vídeo, a qualidade do texto é perfeita. A captura de tela mostra a janela principal do aplicativo (parte inferior), que inclui o SwingNodejunto com a mencionada JFrame(parte superior). Você pode ter que aumentar o zoom na borda reta do "l" ou "k" para ver por que um está nítido e o outro embaçado:

Isso só acontece no Windows. Ao visualizar a fonte no Windows por meio do programa de visualização de fontes do sistema, as fontes são suavizadas usando as cores do LCD. O aplicativo usa tons de cinza. Suspeito que, se houver uma maneira de forçar a renderização a usar cores para suavização em vez de tons de cinza, o problema pode desaparecer. Então, novamente, quando executado por conta própria JFrame, não há problema e as cores do LCD não são usadas.

Código

Aqui está o código para o JFrameque tem uma renderização perfeita:

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

O código para o embaçado SwingNodeé um pouco mais complicado (veja a lista completa ), mas aqui estão alguns trechos relevantes (observe que HTMLPanelse estende de XHTMLPanelapenas para suprimir algumas rolagens automáticas indesejadas durante atualizações):

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

Exemplo de trabalho mínimo

Aqui está um exemplo bastante mínimo independente:

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 borrada do exemplo de trabalho mínimo; aumentar o zoom revela que as bordas das letras são fortemente suavizadas em vez de contrastes nítidos:

Usar um JLabeltambém exibe a mesma renderização difusa:

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

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

Tentativas

Aqui estão muitas das maneiras que tentei remover o desfoque.

Java

No lado do Java, alguém sugeriu executar o aplicativo usando:

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

Nenhuma das dicas de renderização de texto ajudou.

Definir o conteúdo de SwingNodedentro SwingUtilities.invokeLaternão tem efeito.

JavaFX

Alguém mencionou que desativar o cache ajudou, mas isso era para um JavaFX ScrollPane, não para um SwingNode. Não funcionou.

O JScrollPanecontido por SwingNodetem seu alinhamento X e alinhamento Y definidos para 0,5 e 0,5, respectivamente. Garantir um deslocamento de meio pixel é recomendado em outro lugar . Não consigo imaginar que definir o Scenepara usar StrokeType.INSIDEfaria alguma diferença, embora eu tenha tentado usar uma largura de traço de 1 sem sucesso.

Disco voador

FlyingSaucer possui várias opções de configuração . Várias combinações de configurações incluem:

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

As xr.image.configurações afetam apenas as imagens renderizadas pelo FlyingSaucer, em vez de como a saída do FlyingSaucer é renderizada pelo JavaFX dentro do SwingNode.

O CSS usa pontos para os tamanhos das fontes.

Pesquisa

  • https://stackoverflow.com/a/26227562/59087 - parece que algumas soluções podem ser úteis.
  • https://bugs.openjdk.java.net/browse/JDK-8089499- não parece se aplicar porque está usando SwingNodee JScrollPane.
  • https://stackoverflow.com/a/24124020/59087 - provavelmente não é relevante porque não há nenhum construtor de cena XML em uso.
  • https://www.cs.mcgill.ca/media/tech_reports/42_Lessons_Learned_in_Migrating_from_Swing_to_JavaFX_LzXl9Xv.pdf - a página 8 descreve o deslocamento de 0,5 pixels, mas como?
  • https://dlsc.com/2014/07/17/javafx-tip-9-do-not-mix-swing-javafx/ - sugere não misturar JavaFX e Swing, mas mudar para Swing puro não é uma opção: preferiria reescrever o aplicativo em outra linguagem.

Aceito como um bug em OpenJDK / JavaFX:

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

JDK e JRE

Usando o OpenJDK da Bellsoft com o JavaFX incluído. Até onde sei, o OpenJDK já tem suporte para Freetype há algum tempo. Além disso, a fonte parece ótima no Linux, então provavelmente não é o JDK.

Tela

As especificações de tela a seguir exibem o problema, mas outras pessoas (exibindo em monitores e resoluções diferentes, sem dúvida) mencionaram o problema.

  • 15,6 "HD 4: 3 (1366 x 768)
  • Full HD (1920x1080)
  • Retroiluminação LED de ângulo amplo
  • ASUS n56v

Questão

Por que o FlyingSaucer XHTMLPanelquando empacotado em SwingNodefica embaçado no Windows e, ainda assim, exibindo o mesmo XHTMLPanelem um JFrameaplicativo JavaFX em execução parece nítido? Como o problema pode ser resolvido?

O problema envolve SplitPane.

Respostas

1 mipa Aug 19 2020 at 19:17

Existem algumas opções que você pode tentar, embora eu deva admitir que não conheço o FlyingSaucer e sua API.

FlyingSaucer tem diferentes renderizadores. Portanto, pode ser possível evitar a renderização Swing / AWT completamente usando esta biblioteca para fazer toda a renderização diretamente no JavaFX.https://github.com/jfree/fxgraphics2d

Outra possibilidade é deixar o FlyingSaucer renderizar em uma imagem que pode ser exibida em JavaFX de forma muito eficiente por meio de buffers diretos. Veja o código AWTImage em meu repositório aqui:https://github.com/mipastgt/JFXToolsAndDemos

1 weisj Aug 20 2020 at 01:48

Não consegui reproduzir o problema sozinho, então pode haver algum problema na combinação da versão JDK / JavaFX que você está usando. Também é possível que o problema surja apenas com uma combinação específica de tamanho de exibição e escala de tela.

Minha configuração é a seguinte:

  • 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

O problema foi aceito como um bug no OpenJDK / JavaFX:

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

Nenhuma das sugestões de Mipa funcionaria na prática. FlyingSaucer é totalmente integrado com um JScrollPane, o que exclui a possibilidade de forçar o FlyingSaucer a renderizar em um painel baseado em JavaFX.

Outra possibilidade é ir na direção oposta: criar um aplicativo Swing e incorporar controles JavaFX, como usar um JFXPanel ; entretanto, parece mais prudente aceitar o comportamento borrado até que o bug seja eliminado.