Windows上のJavaFXでのSwingNodeのぼやけたレンダリング

Aug 17 2020

概要概要

さまざまな理由でWebViewを回避するために、JavaFXアプリケーション内でFlyingSaucerを使用する。

  • 同期動作のためにスクロールバーへの直接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 );

// ...

// The "preview pane" contains the 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 );

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

最小限の作業例からのぼやけたキャプチャ。ズームインすると、文字のエッジがシャープなコントラストではなく、アンチエイリアス処理されていることがわかります。

を使用して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

テキストレンダリングのヒントはどれも役に立ちませんでした。

SwingNode内の内容を設定しSwingUtilities.invokeLaterても効果はありません。

JavaFX

誰かが他に言及offキャッシュを回すと助けたこと、それはJavaFXのためだったScrollPanewithinない1、 SwingNode。それはうまくいきませんでした。

JScrollPane含まれるのSwingNodeは、アライメントXとアライメントYがそれぞれ0.5と0.5に設定されています。他の場所では、ハーフピクセルオフセットを確保することをお勧めします。ストローク幅1を使用してみましたが、Scene使用StrokeType.INSIDEするように設定しても違いが生じるとは想像できません。

空飛ぶ円盤

FlyingSaucerにはいくつかの構成オプションがあります。設定のさまざまな組み合わせは次のとおりです。

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

xr.image.設定は、むしろFlyingSaucerからの出力は、内のJavaFXでレンダリングする方法よりも、FlyingSaucerによってレンダリングされた画像に影響を与えますSwingNode

CSSは、フォントサイズにポイントを使用します。

研究

  • https://stackoverflow.com/a/26227562/59087 -いくつかの解決策が役立つようです。
  • https://bugs.openjdk.java.net/browse/JDK-8089499-これは、使用しているため、適用されていないようだSwingNodeJScrollPane
  • https://stackoverflow.com/a/24124020/59087 --XMLシーンビルダーが使用されていないため、おそらく関係ありません。
  • https://www.cs.mcgill.ca/media/tech_reports/42_Lessons_Learned_in_Migrating_from_Swing_to_JavaFX_LzXl9Xv.pdf -8ページでは0.5ピクセルのシフトについて説明していますが、どのようにしたらよいでしょうか。
  • https://dlsc.com/2014/07/17/javafx-tip-9-do-not-mix-swing-javafx/ --JavaFXとSwingを混在させないことをお勧めしますが、純粋なSwingに移行することはできません。アプリを別の言語ですぐに書き直したいと思います。

OpenJDK / JavaFXに対するバグとして受け入れられました:

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

JDKとJRE

使用BellsoftさんのJavaFXでのOpenJDKがバンドルされています。私の知る限り、OpenJDKはしばらくの間Freetypeをサポートしてきました。また、フォントはLinuxで見栄えがするので、おそらくJDKではありません。

画面

次の画面仕様は問題を示していますが、他の人(間違いなく異なるモニターと解像度で表示)が問題について言及しています。

  • 15.6インチ4:3 HD(1366x768)
  • フルHD(1920x1080)
  • 広視野角LEDバックライト
  • ASUS n56v

質問

なぜFlyingSaucerのはありませんXHTMLPanel以内に包まれたときにSwingNode、Windows上になってぼやけて、まだ同じ表示XHTMLPanelJFrame同じJavaFXアプリケーションで実行すると、くっきりと表示されますか?どうすれば問題を解決できますか?

問題にはが含まれSplitPaneます。

回答

1 mipa Aug 19 2020 at 19:17

FlyingSaucerとそのAPIを知らないことを認めなければなりませんが、試すことができるいくつかのオプションがあります。

FlyingSaucerにはさまざまなレンダラーがあります。したがって、JavaFXですべてのレンダリングを直接実行するために、代わりにこのライブラリを使用することで、Swing / AWTレンダリングを完全に回避できる可能性があります。https://github.com/jfree/fxgraphics2d

もう1つの可能性は、FlyingSaucerを、直接バッファーを介してJavaFXで非常に効率的に表示できる画像にレンダリングすることです。ここにある私のリポジトリのAWTImageコードを参照してください:https://github.com/mipastgt/JFXToolsAndDemos

1 weisj Aug 20 2020 at 01:48

自分で問題を再現できなかったため、使用している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.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

この問題は、OpenJDK / JavaFXに対するバグとして受け入れられました。

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

Mipaの提案はどちらも実際には機能しません。FlyingSaucerはと緊密に統合されているためJScrollPane、FlyingSaucerをJavaFXベースのパネルに強制的にレンダリングする可能性はありません。

もう1つの可能性は、反対の方向に進むことです。Swingアプリケーションを作成し、JFXPanelを使用するなどのJavaFXコントロールを埋め込みます。ただし、バグが解決されるまで、ぼやけた動作を受け入れる方が賢明なようです。