Windows의 JavaFX에서 SwingNode의 흐릿한 렌더링

Aug 17 2020

개요

JavaFX 응용 프로그램 내에서 FlyingSaucer를 사용하여 다양한 이유로 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 );

// ...

// 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 측에서는 누군가 다음을 사용하여 애플리케이션을 실행하도록 제안 했습니다.

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

텍스트 렌더링 힌트 가 도움이 되지 않았습니다 .

SwingNode내부 의 내용을 SwingUtilities.invokeLater설정해도 효과가 없습니다.

JavaFX

다른 사람이 언급 떨어져 캐싱을 돌리면 도움이되었다고하지만은 자바 FX를위한이었다 ScrollPane내에서하지 하나 SwingNode. 작동하지 않았습니다.

JScrollPane포함 된 SwingNode에는 정렬 X 및 정렬 Y가 각각 0.5 및 0.5로 설정됩니다. 다른 곳 에서는 절반 픽셀 오프셋을 확보하는 것이 좋습니다 . 내가 설정하는 것을 상상할 수 없다 Scene사용하는 것은 StrokeType.INSIDE내가 아무 소용이 하나의 획 너비를 사용해보십시오 않았지만, 어떤 차이를 만들 것입니다.

FlyingSaucer

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의 출력이 내 자바 FX가 렌더링되는 방법을보다 FlyingSaucer에 의해 렌더링 된 이미지에 영향을 미칠 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

JavaFX가 번들로 제공되는 Bellsoft 의 OpenJDK 사용 . 내가 아는 한 OpenJDK는 한동안 Freetype을 지원했습니다. 또한 글꼴은 Linux에서 멋지게 보이므로 JDK가 아닐 수도 있습니다.

화면

다음 화면 사양은 문제를 나타내지 만 다른 사람들 (의심 할 여지없이 다른 모니터와 해상도에서 보는)이 문제를 언급했습니다.

  • 15.6 형 4 : 3 HD (1366x768)
  • 풀 HD (1920x1080)
  • 광 시야각 LED 백라이트
  • ASUS n56v

질문

왜 FlyingSaucer의는 않습니다 XHTMLPanel내에서 포장 할 때 SwingNodeWindows에서이 될 흐릿 아직 같은 표시 XHTMLPanelA의 JFrame응용 프로그램이 나타납니다 선명 같은 자바 FX에서 실행? 문제를 어떻게 해결할 수 있습니까?

문제는 SplitPane.

답변

1 mipa Aug 19 2020 at 19:17

내가 FlyingSaucer와 그 API를 모른다는 것을 인정해야하지만 시도해 볼 수있는 몇 가지 옵션이 있습니다.

FlyingSaucer에는 다른 렌더러가 있습니다. 따라서 JavaFX에서 직접 모든 렌더링을 수행하기 위해 대신이 라이브러리를 사용하여 Swing / AWT 렌더링을 완전히 피할 수 있습니다.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

Mipa의 제안 중 어느 것도 실제로 작동하지 않습니다. FlyingSaucer는와 밀접하게 통합되어있어 FlyingSaucer가 JScrollPaneJavaFX 기반 패널에 강제로 렌더링 할 가능성을 배제합니다.

또 다른 가능성은 반대 방향으로가는 것입니다. Swing 애플리케이션을 만들고 JFXPanel 사용과 같은 JavaFX 컨트롤을 포함합니다 . 그러나 버그가 발생하기 전까지는 흐릿한 동작을 받아들이는 것이 더 현명 해 보일 것입니다.