英文:
Blurry render of SwingNode in JavaFX on Windows
问题
概述
在JavaFX应用程序中使用FlyingSaucer,以避免使用WebView,原因如下:
- WebView不提供对其滚动条的直接API访问,无法实现同步行为;
- 捆绑JavaScript,这对我的用例来说是一个巨大的负担;
- 在Windows上无法运行。
FlyingSaucer使用Swing,在JavaFX中使用时需要将其XHTMLPanel
(JPanel
的子类)包装在SwingNode
中。一切工作正常,应用程序实时渲染Markdown,并且具有响应能力。这是应用程序在Linux上运行的演示视频。
问题
Windows上的文本渲染模糊。当在一个JFrame
中运行,而不是被SwingNode
包装时,但仍然是视频中显示的同一个应用程序的一部分,文本的质量是无瑕的。屏幕截图显示了应用程序的主窗口(底部),其中包括SwingNode
以及前面提到的JFrame
(顶部)。您可能需要放大“l”或“k”的直边,以查看为什么一个锐利,另一个模糊:
这种情况只发生在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();
});
});
}
}
从最小工作示例中获取的模糊截图;放大可以看到字母边缘被大量抗锯齿处理,而不是锐利的对比:
使用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:
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( "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 ) {
}
}
}
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 "preview pane" 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 = "<!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();
} );
} );
}
}
Blurry capture from the minimal working example;
zooming in reveals letter edges are heavily antialiased rather than sharp contrasts:
Using a JLabel
also exhibits the same fuzzy render:
<!-- language: java -->
final var label = new JLabel( "TEST" );
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
- https://stackoverflow.com/a/26227562/59087 -- looks like a few solutions may be helpful.
- https://bugs.openjdk.java.net/browse/JDK-8089499 -- doesn't seem to apply because this is using
SwingNode
andJScrollPane
. - https://stackoverflow.com/a/24124020/59087 -- probably not relevant because there is no XML scene builder in use.
- https://www.cs.mcgill.ca/media/tech_reports/42_Lessons_Learned_in_Migrating_from_Swing_to_JavaFX_LzXl9Xv.pdf -- page 8 describes shifting by 0.5 pixels, but how?
- https://dlsc.com/2014/07/17/javafx-tip-9-do-not-mix-swing-javafx/ -- suggests not mixing JavaFX and Swing, but moving to pure Swing isn't an option: I'd sooner rewrite the app in another language.
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'> </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 = "<!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();
});
});
}
}
答案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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论