Размытый рендеринг SwingNode в JavaFX в Windows

Aug 17 2020

Обзор

Использование FlyingSaucer в приложении JavaFX, чтобы избежать использования WebView по разным причинам:

  • не предоставляет прямой доступ API к своим полосам прокрутки для синхронного поведения;
  • связывает JavaScript, что является огромным раздувом для моего случая использования; и
  • не удалось запустить в Windows.

FlyingSaucer использует Swing, который требует обертывания его XHTMLPanel(подкласса JPanel) в a SwingNodeдля использования вместе с JavaFX. Все работает отлично, приложение отображает Markdown в реальном времени и быстро реагирует. Вот демонстрационное видео приложения, работающего в Linux.

Проблема

Отрисовка текста в Windows размыта. При запуске в a JFrame, но не в оболочке SwingNode, но все еще являющейся частью того же приложения, что показано в видео, качество текста безупречно. На снимке экрана показано главное окно приложения (внизу), которое включает SwingNodeвместе с вышеупомянутым JFrame(вверху). Возможно, вам придется увеличить прямой край букв "l" или "k", чтобы понять, почему один из них резкий, а другой - размытый:

Это происходит только в Windows. При просмотре шрифта в Windows через системную программу предварительного просмотра шрифтов выполняется сглаживание шрифтов с использованием цветов ЖК-дисплея. Приложение использует оттенки серого. Я подозреваю, что если есть способ заставить рендеринг использовать для сглаживания цвет вместо оттенков серого, проблема может исчезнуть. Опять же, при работе в собственном режиме JFrameпроблем нет, и цвета ЖК-дисплея не используются.

Код

Вот код для 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();
      } );
    } );
  }
}

Размытый захват из минимально работающего примера; увеличение масштаба показывает, что края букв сильно сглажены, а не резкие контрасты:

Использование a JLabelтакже демонстрирует ту же нечеткую визуализацию:

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

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

Попытки

Вот большинство способов, которыми я пытался удалить размытие.

Ява

Со стороны Java кто-то предложил запустить приложение, используя:

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

Никакие подсказки по рендерингу текста не помогли.

Установка содержимого SwingNodeвнутри не SwingUtilities.invokeLaterимеет никакого эффекта.

JavaFX

Кто-то еще упомянул, что отключение кеширования помогло, но это было для JavaFX ScrollPane, а не для SwingNode. Это не сработало.

Для JScrollPaneобъекта, содержащегося в SwingNode, для его выравнивания X и Y установлено значение 0,5 и 0,5 соответственно. В другом месте рекомендуется обеспечить смещение на полпикселя . Я не могу себе представить, что установка Sceneиспользования StrokeType.INSIDEбудет иметь какое-либо значение, хотя я пытался использовать ширину штриха 1, но безрезультатно.

Летающая тарелка

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, а не на то, как выходные данные FlyingSaucer отображаются JavaFX в SwingNode.

CSS использует точки для размеров шрифта.

Исследование

  • https://stackoverflow.com/a/26227562/59087 - похоже, несколько решений могут быть полезны.
  • https://bugs.openjdk.java.net/browse/JDK-8089499- похоже, не применяется, потому что здесь используются SwingNodeи JScrollPane.
  • 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

Использование OpenJDK от Bellsoft с JavaFX в комплекте. Насколько мне известно, OpenJDK уже некоторое время поддерживает Freetype. Кроме того, шрифт отлично смотрится в Linux, так что, вероятно, это не JDK.

Экран

Следующие ниже спецификации экрана указывают на проблему, но другие люди (при просмотре на разных мониторах и разрешениях, несомненно) упоминали о проблеме.

  • 15,6-дюймовый, 4: 3 HD (1366x768)
  • Full HD (1920x1080)
  • Светодиодная подсветка с широким углом обзора
  • ASUS n56v

Вопрос

Почему FlyingSaucer, XHTMLPanelобернутый внутрь, SwingNodeстановится размытым в Windows, а отображение того же самого XHTMLPanelпри JFrameзапуске в том же приложении JavaFX выглядит четким? Как решить проблему?

Проблема вовлекает SplitPane.

Ответы

1 mipa Aug 19 2020 at 19:17

Есть несколько вариантов, которые вы можете попробовать, хотя я должен признать, что не знаю FlyingSaucer и его API.

FlyingSaucer имеет разные рендеры. Таким образом, можно было бы полностью избежать рендеринга Swing / AWT, используя вместо этого эту библиотеку, чтобы выполнять весь рендеринг непосредственно в JavaFX.https://github.com/jfree/fxgraphics2d

Другая возможность - позволить 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

Ни одно из предложений Мипы на практике не сработало. FlyingSaucer тесно интегрирован с a JScrollPane, что исключает возможность принудительного рендеринга FlyingSaucer на панели на основе JavaFX.

Другая возможность - пойти в противоположном направлении: создать приложение Swing и встроить элементы управления JavaFX, например, с помощью JFXPanel ; тем не менее, было бы более благоразумно принять размытое поведение, пока ошибка не будет устранена.