在Windows上的JavaFX中SwingNode的模糊渲染

huangapple go评论89阅读模式
英文:

Blurry render of SwingNode in JavaFX on Windows

问题

概述

在JavaFX应用程序中使用FlyingSaucer,以避免使用WebView,原因如下:

  • WebView不提供对其滚动条的直接API访问,无法实现同步行为;
  • 捆绑JavaScript,这对我的用例来说是一个巨大的负担;
  • 在Windows上无法运行。

FlyingSaucer使用Swing,在JavaFX中使用时需要将其XHTMLPanelJPanel的子类)包装在SwingNode中。一切工作正常,应用程序实时渲染Markdown,并且具有响应能力。这是应用程序在Linux上运行的演示视频

问题

Windows上的文本渲染模糊。当在一个JFrame中运行,而不是被SwingNode包装时,但仍然是视频中显示的同一个应用程序的一部分,文本的质量是无瑕的。屏幕截图显示了应用程序的主窗口(底部),其中包括SwingNode以及前面提到的JFrame(顶部)。您可能需要放大“l”或“k”的直边,以查看为什么一个锐利,另一个模糊:

在Windows上的JavaFX中SwingNode的模糊渲染

这种情况只发生在Windows上。在Windows上查看字体时,通过系统的字体预览程序,字体使用LCD颜色进行反锯齿处理。该应用程序使用灰度。我怀疑,如果有一种方法可以强制渲染使用颜色进行反锯齿处理,而不是使用灰度,问题可能会消失。然而,当在其自己的JFrame中运行时,没有问题,也不使用LCD颜色。

代码

下面是拥有完美渲染的JFrame的代码:

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

模糊的SwingNode的代码稍微复杂一些(参见完整代码),以下是一些相关的片段(请注意,HTMLPanel仅扩展自XHTMLPanel,以在更新期间抑制一些不必要的自动滚动):

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

// ...

// "预览面板" 包含 SwingNode。
final SplitPane splitPane = new SplitPane(
    getDefinitionPane().getNode(),
    getFileEditorPane().getNode(),
    getPreviewPane().getNode());

最小工作示例

这是一个相当简洁的自包含示例:

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

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

从最小工作示例中获取的模糊截图;放大可以看到字母边缘被大量抗锯齿处理,而不是锐利的对比:

在Windows上的JavaFX中SwingNode的模糊渲染

使用JLabel也会出现相同的模糊渲染:

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

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

尝试

以下是我尝试消除模糊的大部分方法。

Java

在Java端,有人建议使用以下方式运行应用程序:

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

所有的文本渲染提示都没有帮助。

SwingUtilities.invokeLater内设置SwingNode的内容没有效果。

JavaFX

另外有人[提到](https://stackoverflow

英文:

Overview

Using FlyingSaucer within a JavaFX application, to avoid WebView for various reasons:

  • doesn't provide direct API access to its scrollbars for synchronous behaviour;
  • bundles JavaScript, which is a huge bloat for my use case; and
  • failed to run on Windows.

FlyingSaucer uses Swing, which requires wrapping its XHTMLPanel (a subclass of JPanel) in a SwingNode to use alongside JavaFX. Everything works great, the application renders Markdown in real-time, and is responsive. Here's a demo video of the application running on Linux.

Problem

The text rendering on Windows is blurry. When running in a JFrame, not wrapped by a SwingNode, but still part of the same application shown in the video, the quality of the text is flawless. The screen capture shows the application's main window (bottom), which includes the SwingNode along with the aforementioned JFrame (top). You may have to zoom into the straight edge of the "l" or "k" to see why one is sharp and the other blurry:

在Windows上的JavaFX中SwingNode的模糊渲染

This only happens on Windows. When viewing the font on Windows through the system's font preview program, the fonts are antialiased using LCD colours. The application uses grayscale. I suspect that if there is a way to force the rendering to use colour for antialiasing instead of grayscale, the problem may disappear. Then again, when running within its own JFrame, there is no problem and LCD colours are not used.

Code

Here's the code for the JFrame that has a perfect render:

<!-- language: java -->

  private static class Flawless {
private final XHTMLPanel panel = new XHTMLPanel();
private final JFrame frame = new JFrame( &quot;Single Page Demo&quot; );
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 ) {
}
}
}

The code for the blurry SwingNode is a little more involved (see full listing), but here are some relevant snippets (note that HTMLPanel extends from XHTMLPanel only to suppress some undesired autoscrolling during updates):

<!-- language: java -->

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 &quot;preview pane&quot; contains the SwingNode.
final SplitPane splitPane = new SplitPane(
getDefinitionPane().getNode(),
getFileEditorPane().getNode(),
getPreviewPane().getNode() );

Minimal Working Example

Here's a fairly minimal self-contained example:

<!-- language: java -->

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 = &quot;&lt;!DOCTYPE html&gt;&lt;html&gt;&lt;head&quot; +
&quot;&gt;&lt;style type=&#39;text/css&#39;&gt;body{font-family:serif; background-color: &quot; +
&quot;#fff; color:#454545;}&lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;p style=\&quot;font-size: &quot; +
&quot;300px\&quot;&gt;TEST&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&quot;;
public static void main( String[] args ) {
Application.launch( args );
}
@Override
public void start( Stage primaryStage ) {
invokeLater( () -&gt; {
try {
setLookAndFeel( getSystemLookAndFeelClassName() );
} catch( Exception ignored ) {
}
primaryStage.setTitle( &quot;Hello World!&quot; );
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( () -&gt; {
primaryStage.setScene( new Scene( root, 300, 250 ) );
primaryStage.show();
} );
} );
}
}

Blurry capture from the minimal working example;
zooming in reveals letter edges are heavily antialiased rather than sharp contrasts:

在Windows上的JavaFX中SwingNode的模糊渲染

Using a JLabel also exhibits the same fuzzy render:

<!-- language: java -->

  final var label = new JLabel( &quot;TEST&quot; );
label.setFont( label.getFont().deriveFont( Font.BOLD, 128f ) );
final var swingNode = new SwingNode();
swingNode.setContent( label );

Attempts

Here are most of the ways I've tried to remove the blur.

Java

On the Java side, someone suggested to run the application using:

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

None of the text rendering hints have helped.

Setting the content of the SwingNode within SwingUtilities.invokeLater has no effect.

JavaFX

Someone else mentioned that turning caching off helped, but that was for a JavaFX ScrollPane, not one within a SwingNode. It didn't work.

The JScrollPane contained by the SwingNode has its alignment X and alignment Y set to 0.5 and 0.5, respectively. Ensuring a half-pixel offset is recommended elsewhere. I cannot imagine that setting the Scene to use StrokeType.INSIDE would make any difference, although I did try using a stroke width of 1 to no avail.

FlyingSaucer

FlyingSaucer has a number of configuration options. Various combinations of settings include:

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

The xr.image. settings only affect images rendered by FlyingSaucer, rather than how the output from FlyingSaucer is rendered by JavaFX within the SwingNode.

The CSS uses points for the font sizes.

Research

Accepted as a bug against OpenJDK/JavaFX:

JDK & JRE

Using Bellsoft's OpenJDK with JavaFX bundled. To my knowledge, the OpenJDK has had Freetype support for a while now. Also, the font looks great on Linux, so it's probably not the JDK.

Screen

The following screen specifications exhibit the problem, but other people (viewing on different monitors and resolutions, undoubtedly) have mentioned the issue.

  • 15.6" 4:3 HD (1366x768)
  • Full HD (1920x1080)
  • Wide View Angle LED Backlight
  • ASUS n56v

Question

Why does FlyingSaucer's XHTMLPanel when wrapped within SwingNode become blurry on Windows, and yet displaying the same XHTMLPanel in a JFrame running in the same JavaFX application appears crisp? How can the problem be fixed?

The problem involves SplitPane.

答案1

得分: 1

有几个选项可以尝试,尽管我必须承认我不了解FlyingSaucer及其API。

FlyingSaucer有不同的渲染器。因此,通过使用这个库,完全可以避免使用Swing/AWT渲染,而是直接在JavaFX中进行所有渲染。 https://github.com/jfree/fxgraphics2d

另一个可能性是让FlyingSaucer渲染成一幅图像,然后通过直接缓冲区在JavaFX中高效地显示这幅图像。请参阅我在这里的存储库中的AWTImage代码:https://github.com/mipastgt/JFXToolsAndDemos

英文:

There are a few options that you might try although I have to admit that I do not know FlyingSaucer and its API.

FlyingSaucer has different renderers. Thus it might be possible to avoid the Swing/AWT rendering completely by using this library instead in order to do all the rendering directly in JavaFX. https://github.com/jfree/fxgraphics2d

Another possibility is to let FlyingSaucer render into an image which can the be displayed in JavaFX very efficiently via direct buffers. See the AWTImage code in my repository here: https://github.com/mipastgt/JFXToolsAndDemos

答案2

得分: 1

我无法自己重现这个问题,所以您使用的JDK/JavaFX版本可能存在一些问题。也有可能该问题仅在特定的显示尺寸和屏幕缩放组合下出现。

我的设置如下:

  • 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.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);

            StackPane root = new StackPane();
            root.getChildren().add(mSwingNode);
            Platform.runLater(() -> {
                primaryStage.setScene(new Scene(root, 300, 250));
                primaryStage.show();
            });
        });
    }
}
英文:

I wasn't able to reproduce the issue myself, so there may be some issue in the combination of JDK/JavaFX version you are using. It's also possible that the issue only arises with a specific combination of display size and screen scaling.

My setup is the following:

  • 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 = &quot;&lt;!DOCTYPE html&gt;\n&quot;
            + &quot;&lt;html&gt;\n&quot;
            + &quot;&lt;body&gt;\n&quot;;
    private static final String HTML_CONTENT =
            &quot;&lt;p style=\&quot;font-size:500px\&quot;&gt;TEST&lt;/p&gt;&quot;;
    private final static String HTML_SUFFIX = &quot;&lt;p style=&#39;height=2em&#39;&gt;&amp;nbsp;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;&quot;;

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

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

            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(() -&gt; {
                primaryStage.setScene(new Scene(root, 300, 250));
                primaryStage.show();
            });
        });
    }
}

答案3

得分: 0

这个问题已经被接受为针对OpenJDK/JavaFX的错误:

Mipa的建议在实际中都不适用。FlyingSaucer与JScrollPane紧密集成,这使得强制FlyingSaucer渲染到基于JavaFX的面板的可能性成为不可能。

另一个可能性是走相反的方向:创建一个Swing应用并嵌入JavaFX控件,例如使用JFXPanel;然而,更明智的做法似乎是在错误被修复之前接受模糊的行为。

英文:

The issue has been accepted as a bug against OpenJDK/JavaFX:

Neither of Mipa's suggestions would work in practice. FlyingSaucer is tightly integrated with a JScrollPane, which precludes the possibility of forcing FlyingSaucer to render onto a JavaFX-based panel.

Another possibility is to go the opposite direction: create a Swing application and embed JavaFX controls, such as using a JFXPanel; however, it would seem more prudent to accept the blurry behaviour until the bug is bashed.

huangapple
  • 本文由 发表于 2020年8月17日 12:58:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/63444771.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定