Rendu flou de SwingNode dans JavaFX sous Windows
Aperçu
Utilisation de FlyingSaucer dans une application JavaFX, pour éviter WebView pour diverses raisons:
- ne fournit pas d'accès API direct à ses barres de défilement pour un comportement synchrone;
- regroupe JavaScript, ce qui est un énorme gonflement pour mon cas d'utilisation; et
- n'a pas pu s'exécuter sous Windows.
FlyingSaucer utilise Swing, qui nécessite d'encapsuler sa XHTMLPanel
(une sous-classe de JPanel
) dans un SwingNode
pour l'utiliser avec JavaFX. Tout fonctionne à merveille, l'application rend Markdown en temps réel et est réactive. Voici une vidéo de démonstration de l'application fonctionnant sous Linux.
Problème
Le rendu du texte sous Windows est flou. Lorsqu'il est exécuté dans un JFrame
, non enveloppé par un SwingNode
, mais faisant toujours partie de la même application illustrée dans la vidéo, la qualité du texte est irréprochable. La capture d'écran montre la fenêtre principale de l'application (en bas), qui comprend le SwingNode
avec ce qui précède JFrame
(en haut). Vous devrez peut-être zoomer sur le bord droit du "l" ou du "k" pour voir pourquoi l'un est net et l'autre flou:
Cela ne se produit que sous Windows. Lors de l'affichage de la police sous Windows via le programme de prévisualisation des polices du système, les polices sont anti-crénelées à l'aide des couleurs LCD. L'application utilise des niveaux de gris. Je soupçonne que s'il existe un moyen de forcer le rendu à utiliser la couleur pour l'anticrénelage au lieu de l'échelle de gris, le problème peut disparaître. JFrame
Là encore, lorsque vous utilisez son propre système , il n'y a aucun problème et les couleurs LCD ne sont pas utilisées.
Code
Voici le code pour le JFrame
qui a un rendu parfait:
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 ) {
}
}
}
Le code pour le flou SwingNode
est un peu plus compliqué (voir la liste complète ), mais voici quelques extraits pertinents (notez que cela HTMLPanel
s'étend de XHTMLPanel
seulement à supprimer certains défilement automatique indésirables lors des mises à jour):
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() );
Exemple de travail minimal
Voici un exemple autonome assez minimal:
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();
} );
} );
}
}
Capture floue de l'exemple de travail minimal; le zoom avant révèle que les bords des lettres sont fortement anti-crénelés plutôt que des contrastes nets:
L'utilisation de a JLabel
présente également le même rendu flou:
final var label = new JLabel( "TEST" );
label.setFont( label.getFont().deriveFont( Font.BOLD, 128f ) );
final var swingNode = new SwingNode();
swingNode.setContent( label );
Tentatives
Voici la plupart des façons dont j'ai essayé de supprimer le flou.
Java
Du côté Java, quelqu'un a suggéré d'exécuter l'application en utilisant:
-Dawt.useSystemAAFontSettings=off
-Dswing.aatext=false
Aucun des conseils de rendu de texte n'a aidé.
La définition du contenu de l' SwingNode
intérieur SwingUtilities.invokeLater
n'a aucun effet.
JavaFX
Quelqu'un d'autre a mentionné que la désactivation de la mise en cache avait aidé, mais c'était pour un JavaFX ScrollPane
, pas un dans un SwingNode
. Ça n'a pas marché.
Le JScrollPane
contenu de l ' SwingNode
a son alignement X et son alignement Y mis respectivement à 0,5 et 0,5. Assurer un décalage d'un demi-pixel est recommandé ailleurs . Je ne peux pas imaginer que le réglage de l' Scene
utilisation StrokeType.INSIDE
ferait une différence, même si j'ai essayé d'utiliser une largeur de trait de 1 en vain.
Soucoupe volante
FlyingSaucer a un certain nombre d' options de configuration . Diverses combinaisons de paramètres comprennent:
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 ...
Les xr.image.
paramètres affectent uniquement les images rendues par FlyingSaucer, plutôt que la façon dont la sortie de FlyingSaucer est rendue par JavaFX dans le SwingNode
.
Le CSS utilise des points pour les tailles de police.
Recherche
- https://stackoverflow.com/a/26227562/59087 - semble que quelques solutions peuvent être utiles.
- https://bugs.openjdk.java.net/browse/JDK-8089499- ne semble pas s'appliquer car cela utilise
SwingNode
etJScrollPane
. - https://stackoverflow.com/a/24124020/59087 - probablement pas pertinent car aucun générateur de scène XML n'est utilisé.
- https://www.cs.mcgill.ca/media/tech_reports/42_Lessons_Learned_in_Migrating_from_Swing_to_JavaFX_LzXl9Xv.pdf - la page 8 décrit le décalage de 0,5 pixels, mais comment?
- https://dlsc.com/2014/07/17/javafx-tip-9-do-not-mix-swing-javafx/ - suggère de ne pas mélanger JavaFX et Swing, mais passer à Swing pur n'est pas une option: je préfère réécrire l'application dans une autre langue.
Accepté comme bogue contre OpenJDK / JavaFX:
- https://bugs.openjdk.java.net/browse/JDK-8252255
JDK et JRE
Utilisation d'OpenJDK de Bellsoft avec JavaFX fourni. À ma connaissance, l'OpenJDK prend en charge Freetype depuis un certain temps maintenant. De plus, la police a fière allure sous Linux, donc ce n'est probablement pas le JDK.
Écran
Les spécifications d'écran suivantes présentent le problème, mais d'autres personnes (visionnant sur différents moniteurs et résolutions, sans aucun doute) ont mentionné le problème.
- HD 15,6 "4: 3 (1 366 x 768)
- HD intégrale (1 920 x 1 080)
- Rétroéclairage LED à grand angle de vue
- ASUS n56v
Question
Pourquoi FlyingSaucer, XHTMLPanel
lorsqu'il est enveloppé dans, SwingNode
devient-il flou sous Windows, alors que l'affichage de la même chose XHTMLPanel
dans une JFrame
exécution dans la même application JavaFX semble net? Comment le problème peut-il être résolu?
Le problème implique SplitPane
.
Réponses
Il y a quelques options que vous pourriez essayer, même si je dois admettre que je ne connais pas FlyingSaucer et son API.
FlyingSaucer a différents moteurs de rendu. Ainsi, il pourrait être possible d'éviter complètement le rendu Swing / AWT en utilisant cette bibliothèque à la place afin de faire tout le rendu directement dans JavaFX.https://github.com/jfree/fxgraphics2d
Une autre possibilité est de laisser FlyingSaucer le rendu dans une image qui peut être affichée dans JavaFX très efficacement via des tampons directs. Voir le code AWTImage dans mon référentiel ici:https://github.com/mipastgt/JFXToolsAndDemos
Je n'ai pas pu reproduire le problème moi-même, il peut donc y avoir un problème dans la combinaison de la version JDK / JavaFX que vous utilisez. Il est également possible que le problème se pose uniquement avec une combinaison spécifique de taille d'affichage et de mise à l'échelle de l'écran.
Ma configuration est la suivante:
- 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'> </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();
});
});
}
}
Le problème a été accepté comme un bogue contre OpenJDK / JavaFX:
- https://bugs.openjdk.java.net/browse/JDK-8252255
Aucune des suggestions de Mipa ne fonctionnerait dans la pratique. FlyingSaucer est étroitement intégré à a JScrollPane
, ce qui exclut la possibilité de forcer FlyingSaucer à effectuer un rendu sur un panneau basé sur JavaFX.
Une autre possibilité est d'aller dans la direction opposée: créer une application Swing et incorporer des contrôles JavaFX, comme l'utilisation d'un JFXPanel ; cependant, il semblerait plus prudent d'accepter le comportement flou jusqu'à ce que le bogue soit corrigé.