Render kabur dari SwingNode di JavaFX pada Windows

Aug 17 2020

Gambaran

Menggunakan FlyingSaucer dalam aplikasi JavaFX, untuk menghindari WebView karena berbagai alasan:

  • tidak menyediakan akses API langsung ke bilah gulirnya untuk perilaku sinkron;
  • bundel JavaScript, yang merupakan pembengkakan besar untuk kasus penggunaan saya; dan
  • gagal dijalankan di Windows.

FlyingSaucer menggunakan Swing, yang memerlukan pembungkusnya XHTMLPanel(subkelasnya JPanel) SwingNodeuntuk digunakan bersama JavaFX. Semuanya bekerja dengan baik, aplikasi membuat penurunan harga secara real-time, dan responsif. Berikut adalah video demo dari aplikasi yang berjalan di Linux.

Masalah

Rendering teks di Windows buram. Saat berjalan di JFrame, tidak dibungkus oleh SwingNode, tetapi masih menjadi bagian dari aplikasi yang sama yang ditampilkan dalam video, kualitas teksnya sempurna. Tangkapan layar menunjukkan jendela utama aplikasi (bawah), yang mencakup SwingNodebersama dengan yang disebutkan di atas JFrame(atas). Anda mungkin harus memperbesar tepi lurus "l" atau "k" untuk melihat mengapa yang satu tajam dan yang lainnya buram:

Ini hanya terjadi di Windows. Saat melihat font di Windows melalui program pratinjau font sistem, font tersebut antialiased menggunakan warna LCD. Aplikasi menggunakan grayscale. Saya menduga bahwa jika ada cara untuk memaksa rendering menggunakan warna untuk antialiasing alih-alih skala abu-abu, masalahnya mungkin hilang. Kemudian lagi, saat berjalan sendiri JFrame, tidak ada masalah dan warna LCD tidak digunakan.

Kode

Berikut kode untuk JFrameyang memiliki render yang sempurna:

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

Kode untuk blur SwingNodesedikit lebih terlibat (lihat daftar lengkap ), tetapi berikut adalah beberapa cuplikan yang relevan (perhatikan yang HTMLPanelmeluas dari XHTMLPanelhanya untuk menekan beberapa autoscrolling yang tidak diinginkan selama pembaruan):

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

Contoh Kerja Minimal

Berikut adalah contoh mandiri yang cukup 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();
      } );
    } );
  }
}

Pengambilan gambar kabur dari contoh kerja minimal; memperbesar tampilan tepi huruf sangat antialiased daripada kontras tajam:

Menggunakan a JLabeljuga menunjukkan render fuzzy yang sama:

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

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

Percobaan

Berikut ini sebagian besar cara saya mencoba menghilangkan keburaman.

Jawa

Di sisi Java, seseorang menyarankan untuk menjalankan aplikasi menggunakan:

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

Tidak ada petunjuk rendering teks yang membantu.

Pengaturan konten di SwingNodedalam SwingUtilities.invokeLatertidak berpengaruh.

JavaFX

Orang lain menyebutkan bahwa mematikan caching memang membantu, tapi itu untuk JavaFX ScrollPane, bukan di dalam file SwingNode. Tidak berhasil.

Yang JScrollPaneterkandung oleh SwingNodememiliki perataan X dan perataan Y diatur ke 0,5 dan 0,5, masing-masing. Memastikan offset setengah piksel direkomendasikan di tempat lain . Saya tidak dapat membayangkan bahwa pengaturan Sceneuntuk digunakan StrokeType.INSIDEakan membuat perbedaan apa pun, meskipun saya mencoba menggunakan lebar guratan 1 tetapi tidak berhasil.

FlyingSaucer

FlyingSaucer memiliki sejumlah opsi konfigurasi . Berbagai kombinasi pengaturan meliputi:

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.pengaturan hanya mempengaruhi gambar yang diberikan oleh FlyingSaucer, bukan bagaimana output dari FlyingSaucer diberikan oleh JavaFX dalam SwingNode.

CSS menggunakan poin untuk ukuran font.

Penelitian

  • https://stackoverflow.com/a/26227562/59087 - sepertinya ada beberapa solusi yang bisa membantu.
  • https://bugs.openjdk.java.net/browse/JDK-8089499- sepertinya tidak berlaku karena ini menggunakan SwingNodedan JScrollPane.
  • https://stackoverflow.com/a/24124020/59087 - mungkin tidak relevan karena tidak ada pembuat adegan XML yang digunakan.
  • https://www.cs.mcgill.ca/media/tech_reports/42_Lessons_Learned_in_Migrating_from_Swing_to_JavaFX_LzXl9Xv.pdf - halaman 8 menjelaskan pergeseran sebesar 0,5 piksel, tapi bagaimana caranya?
  • https://dlsc.com/2014/07/17/javafx-tip-9-do-not-mix-swing-javafx/ - menyarankan untuk tidak mencampur JavaFX dan Swing, tetapi pindah ke Swing murni bukanlah suatu pilihan: Saya akan segera menulis ulang aplikasi dalam bahasa lain.

Diterima sebagai bug terhadap OpenJDK / JavaFX:

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

JDK & JRE

Menggunakan OpenJDK Bellsoft dengan paket JavaFX. Sepengetahuan saya, OpenJDK telah memiliki dukungan Freetype untuk beberapa waktu sekarang. Selain itu, fonta terlihat bagus di Linux, jadi mungkin ini bukan JDK.

Layar

Spesifikasi layar berikut menunjukkan masalah tersebut, tetapi orang lain (melihat pada monitor dan resolusi yang berbeda, tidak diragukan lagi) telah menyebutkan masalah tersebut.

  • 15,6 "4: 3 HD (1366x768)
  • Full HD (1920x1080)
  • Lampu Latar LED Sudut Pandang Lebar
  • ASUS n56v

Pertanyaan

Mengapa FlyingSaucer XHTMLPanelsaat dibungkus SwingNodemenjadi buram di Windows, namun menampilkan hal yang sama XHTMLPaneldalam JFramemenjalankan aplikasi JavaFX yang sama tampak tajam? Bagaimana masalah ini bisa diperbaiki?

Masalahnya melibatkan SplitPane.

Jawaban

1 mipa Aug 19 2020 at 19:17

Ada beberapa opsi yang bisa Anda coba meskipun saya harus mengakui bahwa saya tidak tahu FlyingSaucer dan API-nya.

FlyingSaucer memiliki perender yang berbeda. Oleh karena itu, dimungkinkan untuk menghindari rendering Swing / AWT sepenuhnya dengan menggunakan pustaka ini sebagai gantinya untuk melakukan semua rendering secara langsung di JavaFX.https://github.com/jfree/fxgraphics2d

Kemungkinan lain adalah membiarkan FlyingSaucer merender menjadi gambar yang dapat ditampilkan di JavaFX dengan sangat efisien melalui buffer langsung. Lihat kode AWTImage di repositori saya di sini:https://github.com/mipastgt/JFXToolsAndDemos

1 weisj Aug 20 2020 at 01:48

Saya tidak dapat mereproduksi masalah itu sendiri, jadi mungkin ada beberapa masalah dalam kombinasi versi JDK / JavaFX yang Anda gunakan. Mungkin juga masalah hanya muncul dengan kombinasi tertentu dari ukuran tampilan dan skala layar.

Pengaturan saya adalah sebagai berikut:

  • 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

Masalah ini telah diterima sebagai bug terhadap OpenJDK / JavaFX:

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

Tak satu pun dari saran Mipa akan berhasil dalam praktiknya. FlyingSaucer terintegrasi erat dengan a JScrollPane, yang menghalangi kemungkinan memaksa FlyingSaucer untuk merender ke panel berbasis JavaFX.

Kemungkinan lain adalah dengan pergi ke arah yang berlawanan: buat aplikasi Swing dan embed kontrol JavaFX, seperti menggunakan JFXPanel ; namun, akan tampak lebih bijaksana untuk menerima perilaku buram sampai bug dihancurkan.