├── jfxtools-awtimage ├── .gitignore ├── pom.xml └── src │ └── main │ └── java │ └── de │ └── mpmediasoft │ └── jfxtools │ └── awtimage │ └── AWTImage.java ├── jfxtools-canvas ├── .gitignore ├── src │ └── main │ │ ├── java │ │ └── de │ │ │ └── mpmediasoft │ │ │ └── jfxtools │ │ │ └── canvas │ │ │ ├── ScrollAction.java │ │ │ ├── NativeColorModel.java │ │ │ ├── NativeRenderer.java │ │ │ ├── Viewport.java │ │ │ └── NativeRenderingCanvas.java │ │ └── c │ │ └── de_mpmediasoft_jfxtools_canvas_NativeRenderer.c ├── pom.xml ├── build-native.sh └── docs │ └── NativeRenderingCanvas.adoc ├── jfxtools-skiafx ├── .gitignore ├── pom.xml └── src │ └── main │ └── java │ └── de │ └── mpmediasoft │ └── jfxtools │ └── skiafx │ └── SkiaSurfaceFX.java ├── jfxtools-vlcjfx ├── .gitignore ├── pom.xml └── src │ └── main │ └── java │ └── de │ └── mpmediasoft │ └── jfxtools │ └── vlcjfx │ └── VLCJFXVideoPlayer.java ├── jfxtools-awtimage-demos ├── .gitignore ├── pom.xml └── src │ └── main │ └── java │ └── de │ └── mpmediasoft │ └── jfxtools │ └── awtimage │ └── demo │ ├── AWTImageDemo.java │ ├── PDFViewerDemo.java │ └── PDFViewerDemo2.java ├── jfxtools-canvas-demos ├── .gitignore ├── src │ └── main │ │ └── java │ │ └── de │ │ └── mpmediasoft │ │ └── jfxtools │ │ └── canvas │ │ └── demo │ │ └── NativeRenderingCanvasDemo.java └── pom.xml ├── jfxtools-jarprocessor ├── .gitignore ├── src │ └── main │ │ └── java │ │ └── de │ │ └── mpmediasoft │ │ └── jfxtools │ │ └── jarprocessor │ │ ├── JARProcessor.java │ │ ├── JARProcessorException.java │ │ ├── AbstractJARProcessor.java │ │ ├── processors │ │ ├── NativeArtifactChecker.java │ │ ├── FXMLChecker.java │ │ └── ModuleChecker.java │ │ └── main │ │ └── JARProcessorRunner.java ├── pom.xml └── doc │ └── example_output_modulechecker.txt ├── jfxtools-skiafx-demos ├── .gitignore ├── demo1.png ├── pom.xml └── src │ └── main │ └── java │ └── de │ └── mpmediasoft │ └── jfxtools │ └── skiafx │ └── demo │ └── SkiaSurfaceFXDemo1.java ├── jfxtools-vlcjfx-demos ├── .gitignore ├── src │ └── main │ │ ├── resources │ │ └── video_player.css │ │ └── java │ │ └── de │ │ └── mpmediasoft │ │ └── jfxtools │ │ └── vlcjfx │ │ └── demo │ │ ├── VLCJFXVideoPlayerDemo1.java │ │ └── VLCJFXVideoPlayerDemo2.java └── pom.xml ├── docs └── articles │ ├── JFX-Android │ ├── .gitignore │ ├── images │ │ ├── 001l-mpcopilotapp2.png │ │ ├── 001p-mpcopilotapp2.png │ │ ├── 002-mpcopilotapp2.png │ │ ├── 003-mpcopilotapp2.png │ │ ├── 004-mpcopilotapp2.png │ │ ├── 005-mpcopilotapp2.png │ │ └── 006-mpcopilotapp2.png │ ├── Screenshots │ │ ├── Screenshot_20200614-143510.jpg │ │ ├── Screenshot_20200614-143532.jpg │ │ ├── Screenshot_20200614-143553.jpg │ │ ├── Screenshot_20200614-143624.jpg │ │ ├── Screenshot_20200614-143700.jpg │ │ ├── Screenshot_20200614-143811.jpg │ │ ├── Screenshot_20200614-160151.jpg │ │ └── Screenshot_20200621-133841.jpg │ └── JFX-Android.adoc │ ├── JFX-Bundles │ ├── .gitignore │ └── JFX-Bundles.adoc │ ├── JFX-Native │ ├── .gitignore │ ├── images │ │ └── 000_mac_DAeCAirspaceValidator.png │ ├── ScreenShots │ │ └── 000_mac_DAeCAirspaceValidator.png │ ├── updates.txt │ └── JFX-Native.adoc │ └── index.adoc ├── .gitignore ├── .github └── workflows │ └── maven-publish.yml ├── pom.xml ├── README.md └── LICENSE /jfxtools-awtimage/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /jfxtools-canvas/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /jfxtools-skiafx/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /jfxtools-vlcjfx/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /jfxtools-awtimage-demos/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /jfxtools-canvas-demos/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /jfxtools-jarprocessor/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /jfxtools-skiafx-demos/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /jfxtools-vlcjfx-demos/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | -------------------------------------------------------------------------------- /docs/articles/JFX-Android/.gitignore: -------------------------------------------------------------------------------- 1 | /JFX-Android.html 2 | -------------------------------------------------------------------------------- /docs/articles/JFX-Bundles/.gitignore: -------------------------------------------------------------------------------- 1 | /JFX-Bundles.html 2 | -------------------------------------------------------------------------------- /docs/articles/JFX-Native/.gitignore: -------------------------------------------------------------------------------- 1 | /JFX-Native.html 2 | -------------------------------------------------------------------------------- /jfxtools-vlcjfx-demos/src/main/resources/video_player.css: -------------------------------------------------------------------------------- 1 | .videoPane { 2 | -fx-background-color: black; 3 | } 4 | -------------------------------------------------------------------------------- /jfxtools-skiafx-demos/demo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/jfxtools-skiafx-demos/demo1.png -------------------------------------------------------------------------------- /jfxtools-canvas/src/main/java/de/mpmediasoft/jfxtools/canvas/ScrollAction.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.canvas; 2 | 3 | enum ScrollAction {PAN, ZOOM} -------------------------------------------------------------------------------- /docs/articles/JFX-Android/images/001l-mpcopilotapp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/images/001l-mpcopilotapp2.png -------------------------------------------------------------------------------- /docs/articles/JFX-Android/images/001p-mpcopilotapp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/images/001p-mpcopilotapp2.png -------------------------------------------------------------------------------- /docs/articles/JFX-Android/images/002-mpcopilotapp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/images/002-mpcopilotapp2.png -------------------------------------------------------------------------------- /docs/articles/JFX-Android/images/003-mpcopilotapp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/images/003-mpcopilotapp2.png -------------------------------------------------------------------------------- /docs/articles/JFX-Android/images/004-mpcopilotapp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/images/004-mpcopilotapp2.png -------------------------------------------------------------------------------- /docs/articles/JFX-Android/images/005-mpcopilotapp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/images/005-mpcopilotapp2.png -------------------------------------------------------------------------------- /docs/articles/JFX-Android/images/006-mpcopilotapp2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/images/006-mpcopilotapp2.png -------------------------------------------------------------------------------- /docs/articles/JFX-Native/images/000_mac_DAeCAirspaceValidator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Native/images/000_mac_DAeCAirspaceValidator.png -------------------------------------------------------------------------------- /jfxtools-canvas/src/main/java/de/mpmediasoft/jfxtools/canvas/NativeColorModel.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.canvas; 2 | 3 | public enum NativeColorModel {INT_ARGB_PRE, BYTE_BGRA_PRE} -------------------------------------------------------------------------------- /docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143510.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143510.jpg -------------------------------------------------------------------------------- /docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143532.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143532.jpg -------------------------------------------------------------------------------- /docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143553.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143553.jpg -------------------------------------------------------------------------------- /docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143624.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143624.jpg -------------------------------------------------------------------------------- /docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143700.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143700.jpg -------------------------------------------------------------------------------- /docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143811.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/Screenshots/Screenshot_20200614-143811.jpg -------------------------------------------------------------------------------- /docs/articles/JFX-Android/Screenshots/Screenshot_20200614-160151.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/Screenshots/Screenshot_20200614-160151.jpg -------------------------------------------------------------------------------- /docs/articles/JFX-Android/Screenshots/Screenshot_20200621-133841.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Android/Screenshots/Screenshot_20200621-133841.jpg -------------------------------------------------------------------------------- /docs/articles/JFX-Native/ScreenShots/000_mac_DAeCAirspaceValidator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mipastgt/JFXToolsAndDemos/HEAD/docs/articles/JFX-Native/ScreenShots/000_mac_DAeCAirspaceValidator.png -------------------------------------------------------------------------------- /jfxtools-jarprocessor/src/main/java/de/mpmediasoft/jfxtools/jarprocessor/JARProcessor.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.jarprocessor; 2 | 3 | import java.util.spi.ToolProvider; 4 | 5 | public interface JARProcessor { 6 | 7 | public void initialize(ToolProvider jarToolProvider, boolean verbose); 8 | 9 | public void start(); 10 | 11 | public void process(String arg) throws JARProcessorException; 12 | 13 | public void finish(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # IDE and Maven artifacts 26 | .project 27 | .settings/ 28 | .classpath 29 | .idea/ 30 | *.iml 31 | target/ 32 | 33 | 34 | /.classpath 35 | /*.project 36 | /awtimage.png 37 | -------------------------------------------------------------------------------- /jfxtools-jarprocessor/src/main/java/de/mpmediasoft/jfxtools/jarprocessor/JARProcessorException.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.jarprocessor; 2 | 3 | @SuppressWarnings("serial") 4 | public class JARProcessorException extends Exception { 5 | 6 | public JARProcessorException() { 7 | } 8 | 9 | public JARProcessorException(String message) { 10 | super(message); 11 | } 12 | 13 | public JARProcessorException(Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | public JARProcessorException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /jfxtools-vlcjfx/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.mpmediasoft.jfxtools 8 | jfxtools 9 | 1.0.0-SNAPSHOT 10 | 11 | jfxtools-vlcjfx 12 | 13 | 14 | 15 | uk.co.caprica 16 | vlcj 17 | 4.7.0 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /jfxtools-jarprocessor/src/main/java/de/mpmediasoft/jfxtools/jarprocessor/AbstractJARProcessor.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.jarprocessor; 2 | 3 | import java.io.File; 4 | 5 | abstract public class AbstractJARProcessor implements JARProcessor { 6 | 7 | protected File jarFile(String arg) throws JARProcessorException { 8 | File jarFile = new File(arg); 9 | if (!jarFile.canRead()) { 10 | throw new JARProcessorException("File does not exist: " + jarFile); 11 | } else if (!jarFile.getName().toLowerCase().endsWith(".jar")) { 12 | throw new JARProcessorException("File does not seem to be a JAR file: " + jarFile); 13 | } else { 14 | return jarFile; 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /jfxtools-canvas/src/main/java/de/mpmediasoft/jfxtools/canvas/NativeRenderer.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.canvas; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | /** 6 | * The JNI interface to the native renderer. 7 | * 8 | * @author Michael Paus 9 | */ 10 | public class NativeRenderer { 11 | 12 | static { 13 | System.loadLibrary("nativerenderer"); 14 | } 15 | 16 | // Initialization and disposal: 17 | 18 | public native void init(); 19 | 20 | public native void dispose(); 21 | 22 | // Canvas creation and rendering: 23 | 24 | public native ByteBuffer createCanvas(int width, int height, int numBuffers, int nativeColorModel); 25 | 26 | public native int render(); 27 | 28 | // Actions, e.g., due to user input events: 29 | 30 | public native void moveTo(int x, int y); 31 | 32 | // TODO: zoom, rotate, ... 33 | 34 | } 35 | -------------------------------------------------------------------------------- /jfxtools-awtimage/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.mpmediasoft.jfxtools 8 | jfxtools 9 | 1.0.0-SNAPSHOT 10 | 11 | jfxtools-awtimage 12 | 13 | 14 | 15 | 16 | org.apache.maven.plugins 17 | maven-compiler-plugin 18 | ${maven-compiler-plugin.version} 19 | 20 | false 21 | false 22 | 17 23 | 17 24 | false 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/articles/JFX-Native/updates.txt: -------------------------------------------------------------------------------- 1 | == Missing features for Gluon/GraalVM/native-image toolchain 2 | 3 | === Missing support for more than one locale 4 | * https://github.com/oracle/graal/issues/2908 5 | 6 | === Media 7 | * No media support on some platforms 8 | 9 | === NSMenuFX 10 | * The platform menu bar isn't supported yet. (VERIFY) 11 | * The new version has problems with the JNA library. 12 | 13 | === Configuration too complicated and error prone 14 | * Integrate native image agent into workflow. 15 | Seems to work perfectly well! 16 | (Does it conflict with the default config options?) 17 | 18 | === Native images far too big to be useful 19 | * UPX to the rescue 20 | * This could be a unique selling point for JavaFX, compared to other competing technologies such as Electron. 21 | 22 | === No option to create a proper application bundle 23 | * Seems to be easy on macOS. It's just a tiny wrapper plus one icon. 24 | 25 | === No option to create a platform installer bundle 26 | * Should be easy to do with jpackage. (VERIFY) 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/maven-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path 3 | 4 | name: Maven Package 5 | 6 | on: [push, workflow_dispatch] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | packages: write 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up JDK 16 19 | uses: actions/setup-java@v2 20 | with: 21 | java-version: '16' 22 | distribution: 'adopt' 23 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 24 | settings-path: ${{ github.workspace }} # location for the settings.xml file 25 | 26 | - name: Build with Maven 27 | run: mvn -B package --file pom.xml 28 | 29 | - name: Publish to GitHub Packages Apache Maven 30 | run: mvn deploy -s $GITHUB_WORKSPACE/settings.xml 31 | env: 32 | GITHUB_TOKEN: ${{ github.token }} 33 | -------------------------------------------------------------------------------- /jfxtools-canvas/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.mpmediasoft.jfxtools 8 | jfxtools 9 | 1.0.0-SNAPSHOT 10 | 11 | jfxtools-canvas 12 | 13 | 14 | 15 | 16 | exec-maven-plugin 17 | org.codehaus.mojo 18 | ${exec-maven-plugin.version} 19 | 20 | 21 | Build native renderer 22 | install 23 | 24 | exec 25 | 26 | 27 | 28 | 29 | ${project.basedir} 30 | ./build-native.sh 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /jfxtools-jarprocessor/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.mpmediasoft.jfxtools 8 | jfxtools 9 | 1.0.0-SNAPSHOT 10 | 11 | jfxtools-jarprocessor 12 | 13 | 14 | de.mpmediasoft.jfxtools.jarprocessor.main.JARProcessorRunner 15 | 16 | 17 | 18 | 19 | 20 | 21 | org.codehaus.mojo 22 | exec-maven-plugin 23 | ${exec-maven-plugin.version} 24 | 25 | 26 | 27 | java 28 | 29 | 30 | 31 | 32 | ${mainClassName} 33 | 34 | 35 | 36 | 37 | org.apache.maven.plugins 38 | maven-jar-plugin 39 | ${maven-jar-plugin.version} 40 | 41 | 42 | 43 | ${mainClassName} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /jfxtools-canvas-demos/src/main/java/de/mpmediasoft/jfxtools/canvas/demo/NativeRenderingCanvasDemo.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.canvas.demo; 2 | 3 | import de.mpmediasoft.jfxtools.canvas.NativeRenderingCanvas; 4 | import javafx.application.Application; 5 | import javafx.scene.Scene; 6 | import javafx.scene.control.Label; 7 | import javafx.scene.layout.StackPane; 8 | import javafx.stage.Stage; 9 | 10 | /** 11 | * A simple demo to show how the NativeRenderingCanvas class is supposed to be used. 12 | * 13 | * @author Michael Paus 14 | */ 15 | public class NativeRenderingCanvasDemo extends Application { 16 | 17 | private NativeRenderingCanvas canvas; 18 | 19 | @Override 20 | public void init() { 21 | System.out.println("java.runtime.version: " + System.getProperty("java.runtime.version", "(undefined)")); 22 | System.out.println("javafx.runtime.version: " + System.getProperty("javafx.runtime.version", "(undefined)")); 23 | } 24 | 25 | @Override 26 | public void start(Stage primaryStage) throws Exception { 27 | StackPane root = new StackPane(); 28 | 29 | canvas = new NativeRenderingCanvas(); 30 | 31 | Label label = new Label("This is JavaFX"); 32 | label.setMouseTransparent(true); 33 | label.setStyle("-fx-font-size: 64pt; -fx-font-family: Arial; -fx-font-weight: bold; -fx-text-fill: white; -fx-opacity: 0.8;"); 34 | 35 | root.getChildren().addAll(canvas.getRoot(), label); 36 | 37 | Scene scene = new Scene(root, 1000, 800); 38 | primaryStage.setScene(scene); 39 | primaryStage.show(); 40 | } 41 | 42 | @Override 43 | public void stop() { 44 | canvas.dispose(); 45 | } 46 | 47 | public static void main(String[] args) { 48 | launch(args); 49 | } 50 | 51 | } 52 | 53 | class NativeRenderingCanvasDemoLauncher {public static void main(String[] args) {NativeRenderingCanvasDemo.main(args);}} 54 | -------------------------------------------------------------------------------- /jfxtools-skiafx/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.mpmediasoft.jfxtools 8 | jfxtools 9 | 1.0.0-SNAPSHOT 10 | 11 | jfxtools-skiafx 12 | 13 | 14 | 15 | build-mac 16 | 17 | mac 18 | 19 | 20 | macos-x64 21 | 22 | 23 | 24 | build-linux 25 | 26 | linux 27 | 28 | 29 | linux-x64 30 | 31 | 32 | 33 | build-windows 34 | 35 | windows 36 | 37 | 38 | windows-x64 39 | 40 | 41 | 42 | 43 | 44 | 45 | io.github.humbleui 46 | skija-${skija.platform} 47 | 0.116.1 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /jfxtools-canvas-demos/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.mpmediasoft.jfxtools 8 | jfxtools 9 | 1.0.0-SNAPSHOT 10 | 11 | jfxtools-canvas-demos 12 | 13 | 14 | 15 | 16 | org.codehaus.mojo 17 | exec-maven-plugin 18 | ${exec-maven-plugin.version} 19 | 20 | 21 | 22 | exec 23 | 24 | 25 | 26 | 27 | ${JAVA_HOME}/bin/java 28 | 29 | -classpath 30 | 31 | -ea 32 | -Djava.library.path=../jfxtools-canvas/target/libs/ 33 | de.mpmediasoft.jfxtools.canvas.demo.NativeRenderingCanvasDemoLauncher 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | de.mpmediasoft.jfxtools 43 | jfxtools-canvas 44 | ${jfxtools.version} 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /jfxtools-skiafx-demos/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.mpmediasoft.jfxtools 8 | jfxtools 9 | 1.0.0-SNAPSHOT 10 | 11 | jfxtools-skiafx-demos 12 | 13 | 14 | 15 | 16 | org.codehaus.mojo 17 | exec-maven-plugin 18 | ${exec-maven-plugin.version} 19 | 20 | 21 | 22 | exec 23 | 24 | 25 | 26 | 27 | ${JAVA_HOME}/bin/java 28 | 29 | -classpath 30 | 31 | -ea 32 | --enable-preview 33 | --add-opens=java.base/java.nio=ALL-UNNAMED 34 | de.mpmediasoft.jfxtools.skiafx.demo.SkiaSurfaceFXDemo1Launcher 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | de.mpmediasoft.jfxtools 44 | jfxtools-skiafx 45 | ${jfxtools.version} 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /jfxtools-vlcjfx-demos/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.mpmediasoft.jfxtools 8 | jfxtools 9 | 1.0.0-SNAPSHOT 10 | 11 | jfxtools-vlcjfx-demos 12 | 13 | 14 | 15 | 16 | org.codehaus.mojo 17 | exec-maven-plugin 18 | 1.6.0 19 | 20 | 21 | vlcjfx-demo1 22 | 23 | java 24 | 25 | 26 | de.mpmediasoft.jfxtools.vlcjfx.demo.VLCJFXVideoPlayerDemo1Launcher 27 | 28 | 29 | 30 | 31 | vlcjfx-demo2 32 | 33 | java 34 | 35 | 36 | de.mpmediasoft.jfxtools.vlcjfx.demo.VLCJFXVideoPlayerDemo2Launcher 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | de.mpmediasoft.jfxtools 47 | jfxtools-vlcjfx 48 | ${jfxtools.version} 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/articles/index.adoc: -------------------------------------------------------------------------------- 1 | = Articles about various topics 2 | Dr.-Ing. Michael Paus, mpMediaSoft GmbH 3 | Version 1.0.0, 2021-01-23 4 | :doctype: article 5 | :encoding: utf-8 6 | :lang: en 7 | :toc: left 8 | :numbered: 9 | :ext-relative: .html 10 | 11 | [.lead] 12 | Articles about various software and aviation topics. The software articles are mostly related to Java, JavaFX and GraalVM/Native-image as well as Kotlin and Jetpack Compose. The aviation topics are about using the software in the air. 13 | 14 | [NOTE] 15 | ==== 16 | Scroll down for the latest articles. 17 | ==== 18 | 19 | == Software 20 | 21 | === Building native, AOT compiled, real-world JavaFX applications 22 | 23 | A summary of my lessons learned converting an existing JavaFX application to a native, 24 | AOT compiled, real-world application using the Gluon Client Maven plugin which is based 25 | on the GraalVM `native-image` toolchain. 26 | 27 | link:JFX-Native/JFX-Native{ext-relative}[Building native, AOT compiled, real-world JavaFX applications] 28 | 29 | === Bundling real-world JavaFX applications 30 | 31 | A summary of my lessons learned bundling real-world JavaFX application with 32 | various different techniques. 33 | 34 | link:JFX-Bundles/JFX-Bundles{ext-relative}[Bundling real-world JavaFX applications] 35 | 36 | === JavaFX on Android 37 | 38 | A summary of my lessons learned building a first, still simple, pure JavaFX Android 39 | application based on the components of an already existing JavaFX application using 40 | the Gluon Client Maven plugin which is based on the GraalVM `native-image` toolchain. 41 | 42 | link:JFX-Android/JFX-Android{ext-relative}[JavaFX on Android] 43 | 44 | == Aviation 45 | 46 | === Flight testing standard mobile cellular phone network technology in the lower airspace 47 | 48 | Flight test on 24.10.2021 in Hermuthausen to investigate the performance of standard mobile cellular phone network technology in the lower airspace. 49 | 50 | link:https://mpmediasoft.de/articles/FlightTest20211024/FlightTest20211024.html[Performance of standard mobile cellular phone network technology in the lower airspace] 51 | -------------------------------------------------------------------------------- /jfxtools-canvas/build-native.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Building the native parts of the project. 4 | # It follows https://www.baeldung.com/jni 5 | # For Windows you will need to have http://www.mingw.org/ installed. 6 | # It should work for macOS out of the box but the script 7 | # is totally untested on Linux and Windows (with MinGW) 8 | 9 | cd `dirname $0` 10 | 11 | JSRC=src/main/java 12 | CSRC=src/main/c 13 | LIBS=target/libs 14 | TINC=target/include 15 | TTMP=target/tmp 16 | 17 | mkdir -p $LIBS 18 | mkdir -p $TINC 19 | mkdir -p $TTMP 20 | 21 | echo "Generate JNI C header file" 22 | javac -h $TINC $JSRC/de/mpmediasoft/jfxtools/canvas/NativeRenderer.java 23 | rm $JSRC/de/mpmediasoft/jfxtools/canvas/NativeRenderer.class 24 | 25 | if [[ "$OSTYPE" == "linux-gnu" ]]; then 26 | echo "Creating native library for Linux" 27 | gcc -c -fPIC -I${TINC} -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux \ 28 | $CSRC/de_mpmediasoft_jfxtools_canvas_NativeRenderer.c \ 29 | -o $TTMP/de_mpmediasoft_jfxtools_canvas_NativeRenderer.o 30 | gcc -shared -o $LIBS/libnativerenderer.so $TTMP/de_mpmediasoft_jfxtools_canvas_NativeRenderer.o -lc 31 | 32 | elif [[ "$OSTYPE" == "darwin"* ]]; then 33 | echo "Creating native library for macOS" 34 | gcc -c -fPIC -I${TINC} -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin \ 35 | $CSRC/de_mpmediasoft_jfxtools_canvas_NativeRenderer.c \ 36 | -o $TTMP/de_mpmediasoft_jfxtools_canvas_NativeRenderer.o 37 | gcc -dynamiclib -o $LIBS/libnativerenderer.dylib $TTMP/de_mpmediasoft_jfxtools_canvas_NativeRenderer.o -lc 38 | 39 | elif [[ "$OSTYPE" == "cygwin" ]]; then 40 | # POSIX compatibility layer and Linux environment emulation for Windows 41 | echo "Currently unsupported OS" 42 | 43 | elif [[ "$OSTYPE" == "msys" ]]; then 44 | echo "Creating native library for Lightweight shell and GNU utilities compiled for Windows (part of MinGW)" 45 | gcc -c -I${TINC} -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 $CSRC\de_mpmediasoft_jfxtools_canvas_NativeRenderer.c -o $TTMP\de_mpmediasoft_jfxtools_canvas_NativeRenderer.o 46 | gcc -shared -o $LIBS\libnativerenderer.dll $TTMP\de_mpmediasoft_jfxtools_canvas_NativeRenderer.o -Wl,--add-stdcall-alias 47 | 48 | elif [[ "$OSTYPE" == "win32" ]]; then 49 | # I'm not sure this can happen. 50 | echo "Currently unsupported OS" 51 | 52 | elif [[ "$OSTYPE" == "freebsd"* ]]; then 53 | # ... 54 | echo "Currently unsupported OS" 55 | 56 | else 57 | # Unknown. 58 | echo "Unknown OS" 59 | fi 60 | 61 | -------------------------------------------------------------------------------- /jfxtools-vlcjfx-demos/src/main/java/de/mpmediasoft/jfxtools/vlcjfx/demo/VLCJFXVideoPlayerDemo1.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.vlcjfx.demo; 2 | 3 | import de.mpmediasoft.jfxtools.vlcjfx.VLCJFXVideoPlayer; 4 | import javafx.application.Application; 5 | import javafx.scene.Scene; 6 | import javafx.scene.image.ImageView; 7 | import javafx.scene.layout.StackPane; 8 | import javafx.stage.Stage; 9 | 10 | /** 11 | * A minimal demo program to show how the VLCJFXVideoPlayer class is supposed to be used. 12 | * 13 | * This should work on macOS and Linux but there currently seem to be problems with the VLC code 14 | * on Windows. 15 | * 16 | * In order to run the code, a recent version of the VLC player (3.0.x+) must be installed 17 | * on the system. Other dependencies can be found in the pom.xml. 18 | * 19 | * @author Michael Paus 20 | */ 21 | public class VLCJFXVideoPlayerDemo1 extends Application { 22 | 23 | private static final String VIDEO_FILE = 24 | // "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"; 25 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4"; 26 | // "http://ftp.nluug.nl/pub/graphics/blender/demo/movies/ToS/tearsofsteel_4k.mov"; 27 | 28 | private final double WIDTH = 1200; 29 | private final double HEIGHT = 675; 30 | 31 | private VLCJFXVideoPlayer videoPlayer; 32 | 33 | private ImageView videoImageView; 34 | 35 | @Override 36 | public void init() { 37 | System.out.println("java.runtime.version: " + System.getProperty("java.runtime.version", "(undefined)")); 38 | System.out.println("javafx.runtime.version: " + System.getProperty("javafx.runtime.version", "(undefined)")); 39 | } 40 | 41 | @Override 42 | public void start(Stage primaryStage) throws Exception { 43 | videoPlayer = new VLCJFXVideoPlayer(); 44 | 45 | StackPane root = new StackPane(); 46 | root.getStyleClass().add("videoPane"); 47 | videoImageView = new ImageView(); 48 | videoImageView.setPreserveRatio(true); 49 | videoImageView.fitWidthProperty().bind(root.widthProperty()); 50 | videoImageView.fitHeightProperty().bind(root.heightProperty()); 51 | videoImageView.imageProperty().bind(videoPlayer.videoImageProperty()); 52 | root.getChildren().add(videoImageView); 53 | 54 | Scene scene = new Scene(root, WIDTH, HEIGHT); 55 | scene.getStylesheets().add("/video_player.css"); 56 | primaryStage.setScene(scene); 57 | primaryStage.show(); 58 | 59 | videoPlayer.mediaResourceLocatorProperty().set(VIDEO_FILE); 60 | } 61 | 62 | @Override 63 | public void stop() throws Exception { 64 | videoPlayer.dispose(); 65 | } 66 | 67 | public static void main(String[] args) { 68 | launch(args); 69 | } 70 | 71 | } 72 | 73 | //Launch via this class to avoid module system headaches. 74 | class VLCJFXVideoPlayerDemo1Launcher {public static void main(String[] args) {VLCJFXVideoPlayerDemo1.main(args);}} 75 | -------------------------------------------------------------------------------- /jfxtools-awtimage-demos/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | de.mpmediasoft.jfxtools 8 | jfxtools 9 | 1.0.0-SNAPSHOT 10 | 11 | jfxtools-awtimage-demos 12 | 13 | 14 | 15 | 16 | org.codehaus.mojo 17 | exec-maven-plugin 18 | 1.6.0 19 | 20 | 21 | AWTImageDemo 22 | 23 | java 24 | 25 | 26 | de.mpmediasoft.jfxtools.awtimage.demo.AWTImageDemoLauncher 27 | 28 | 29 | 30 | 31 | PDFViewerDemo 32 | 33 | java 34 | 35 | 36 | de.mpmediasoft.jfxtools.awtimage.demo.PDFViewerDemo 37 | 38 | 39 | 40 | 41 | PDFViewerDemo2 42 | 43 | java 44 | 45 | 46 | de.mpmediasoft.jfxtools.awtimage.demo.PDFViewerDemo2 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | de.mpmediasoft.jfxtools 57 | jfxtools-awtimage 58 | ${jfxtools.version} 59 | 60 | 61 | 62 | org.openjfx 63 | javafx-swing 64 | ${openjfx.version} 65 | 66 | 67 | 68 | org.apache.pdfbox 69 | pdfbox 70 | ${pdfbox.version} 71 | 72 | 73 | 74 | org.apache.pdfbox 75 | io 76 | ${pdfbox-io.version} 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /jfxtools-canvas/src/main/java/de/mpmediasoft/jfxtools/canvas/Viewport.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.canvas; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * Immutable viewport class. 7 | * 8 | * @author Michael Paus 9 | */ 10 | public class Viewport { 11 | 12 | private final int minX; 13 | private final int minY; 14 | private final int width; 15 | private final int height; 16 | 17 | public Viewport() { 18 | this(0, 0, -1, -1); 19 | } 20 | 21 | public Viewport(int minX, int minY, int width, int height) { 22 | this.minX = minX; 23 | this.minY = minY; 24 | this.width = width; 25 | this.height = height; 26 | } 27 | 28 | public Viewport withLocation(int minX, int minY) { 29 | return new Viewport(minX, minY, width, height); 30 | } 31 | 32 | public Viewport withDeltaLocation(int deltaX, int deltaY) { 33 | return new Viewport(minX + deltaX, minY + deltaY, width, height); 34 | } 35 | 36 | public Viewport withSize(int width, int height) { 37 | return new Viewport(minX, minY, width, height); 38 | } 39 | 40 | // Increment or decrement the view size in steps of view_incr. 41 | public Viewport withSizeIncrement(int width, int height, int sizeIncrement) { 42 | if (width > 0 && height > 0 && sizeIncrement > 0) { 43 | int newNrViewWidth = (width % sizeIncrement > 0) ? (width / sizeIncrement + 1) * sizeIncrement : (width / sizeIncrement) * sizeIncrement; 44 | int newNrViewHeight = (height % sizeIncrement > 0) ? (height / sizeIncrement + 1) * sizeIncrement : (height / sizeIncrement) * sizeIncrement; 45 | 46 | if (newNrViewWidth != this.width || newNrViewHeight != this.height) { 47 | return this.withSize(newNrViewWidth, newNrViewHeight); 48 | } 49 | } 50 | return this; 51 | } 52 | 53 | public Viewport withDeltaSize(int deltaWidth, int deltaHeight) { 54 | return new Viewport(minX, minY, width + deltaWidth, height + deltaHeight); 55 | } 56 | 57 | public int getMinX() { 58 | return minX; 59 | } 60 | 61 | public int getMinY() { 62 | return minY; 63 | } 64 | 65 | public int getWidth() { 66 | return width; 67 | } 68 | 69 | public int getHeight() { 70 | return height; 71 | } 72 | 73 | public boolean isEmpty() { 74 | return width < 0 || height < 0; 75 | } 76 | 77 | @Override 78 | public int hashCode() { 79 | return Objects.hash(height, minX, minY, width); 80 | } 81 | 82 | @Override 83 | public boolean equals(Object obj) { 84 | if (this == obj) 85 | return true; 86 | if (obj == null) 87 | return false; 88 | if (getClass() != obj.getClass()) 89 | return false; 90 | Viewport other = (Viewport) obj; 91 | return height == other.height && minX == other.minX && minY == other.minY && width == other.width; 92 | } 93 | 94 | @Override 95 | public String toString() { 96 | return "Viewport [minX=" + minX + ", minY=" + minY + ", width=" + width + ", height=" + height + "]"; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /docs/articles/JFX-Bundles/JFX-Bundles.adoc: -------------------------------------------------------------------------------- 1 | = Bundling real-world JavaFX applications 2 | Dr.-Ing. Michael Paus, mpMediaSoft GmbH 3 | Version 2.0.1, 2021-01-31 4 | :doctype: article 5 | :encoding: utf-8 6 | :lang: en 7 | :toc: left 8 | :numbered: 9 | 10 | [.lead] 11 | A summary of my lessons learned bundling a real-world JavaFX application with 12 | various different techniques. 13 | 14 | == Bundle sizes 15 | 16 | The following table shows the resulting executable bundle sizes (application bundle and installer bundles) resulting from the various bundling techniques. 17 | 18 | .Bundle sizes for packaged application. 19 | |=== 20 | |Technique |Size (.app) |Size (.dmg) 21 | 22 | |jpackage 23 | |210.9 MB 24 | |132.0 MB 25 | 26 | |jpackage + jlink 27 | |126.4 MB 28 | |109.2 MB 29 | 30 | |jpackage + jlink + + 31 | Maven shade with minimizeJar 32 | |<100 MB (currently not available) 33 | |<100 MB (currently not available) 34 | |=== 35 | 36 | In this table the following techniques are compared: 37 | 38 | * jpackage 39 | ** Pure jpackage bundling the standard JRE (17-ea+6) 40 | * jpackage + jlink 41 | ** Jpackage bundling a custom JRE created via jlink (17-ea+6, customized via jdeps). 42 | * jpackage + jlink + Maven shade with minimizeJar 43 | ** Jpackage bundling a shrinked uber-jar with a custom JRE created via jlink (17-ea+6, customized via jdeps). 44 | 45 | The next table shows the resulting executable native-image sizes. 46 | 47 | .Native image sizes. 48 | |=== 49 | |Technique |Size 50 | 51 | |Original 52 | |134.8 MB 53 | 54 | |UPX 55 | |44.9 MB 56 | 57 | |UPX --best 58 | |42.4 MB 59 | |=== 60 | 61 | In this table the following techniques are compared: 62 | 63 | * Original 64 | ** GluonHQ client-maven-plugin + substrate building on GraalVM/native-image (21.0.0) 65 | * UPX 66 | ** Original compressed with UPX 67 | * UPX --best 68 | ** Original compressed with UPX --best (takes very long) 69 | 70 | == Discussion 71 | 72 | The jpackage variants could be further improved if one would also shrink the JDK part, 73 | which jlink does only on a whole module basis but not for individual classes or even methods. 74 | 75 | I will try to further shrink that via ProGuard but this is complicated by the fact 76 | that the JDK is not packaged as a JAR file anymore. I am pretty sure though that the total size can be brought down to less than 50 MB if all optimization potential is exploited. 77 | 78 | The original native variant is in this respect disappointing though. All the potential optimizations 79 | that a tool like ProGuard could still apply to the other variants are, according to the 80 | documentation, already done by the native variant, which leaves no more opportunity for 81 | further optimizations. 82 | In addition to that the native variant is not even feature complete. E.g., it only supports 83 | a single locale which of course reduces the size of the resulting bundle in a non-acceptable way. 84 | Some normally used code is also not included because it is currently not supported. 85 | 86 | A further possibility is to compress the native image with a tool like UPX (the Ultimate Packer for eXecutables, https://upx.github.io/). This brings down the native image size substantially and I haven't observed any noticable impact on the startup speed. So, this seems the way to go here for me. With UPX applied, this is currently the variant which provides the smallest image size and best startup speed. -------------------------------------------------------------------------------- /jfxtools-canvas/docs/NativeRenderingCanvas.adoc: -------------------------------------------------------------------------------- 1 | = NativeRenderingCanvas 2 | 3 | This is an example to show how some native renderer can be integrated seemlessly into 4 | JavaFX. It uses the new WritableImage of JavaFX 13 with support for Buffers to improve performance. 5 | 6 | It consists of the following parts: 7 | 8 | * NativeRenderingCanvasDemo: A simple demo to show how the NativeRenderingCanvas class is supposed 9 | to be used. 10 | * NativeRenderingCanvas: A native rendering canvas. The assumption is that some native renderer 11 | produces an image provided as an IntBuffer or ByteBuffer. The PixelFormats 12 | must be IntArgbPre or ByteBgraPre respectively. For the API see NativeRenderer. 13 | This buffer is then used to create an Image which is bound to an ImageView. 14 | This class manages the direct display of this Image in a Pane and reacts to 15 | user input via mouse input or gestures on touch devices. 16 | * NativeRenderer: The JNI interface to the native renderer. 17 | * de_mpmediasoft_jfxtools_canvas_NativeRenderer.c: A C implementation of the JNI interface as defined in NativeRenderer.java. 18 | The code in here is not really relevant for the example. It just renders a map 19 | consisting of square red and green tiles on a blue background. It is 20 | basically just a placeholder for some real code which uses, e.g., OpenGL or 21 | some other rendering library to create an image representation in a memory buffer. 22 | 23 | The key points of this example are the following: The NativeRenderingCanvas provides a 24 | Pane which can be directly connected to some layout-pane of the JavaFX scene graph. 25 | Whenever this pane is resized it is decided whether the rendered image also has to be 26 | resized. For performance reasons this is only done in increments of 64 pixels. If the 27 | image size has to be changed, the native renderer is told to create a new canvas which 28 | is returned as a ByteBuffer. At this point the native renderer is also told how many 29 | buffers and which color model should be used. 30 | 31 | JavaFX currently does not support double-buffering but it can be emulated with a little 32 | trick. When we use two buffers, we actually create an image which has twice the hight 33 | of the actually required image. The renderer then renders intermittently into the upper 34 | and the lower half of this image. When the rendering completes the viewport of the internal 35 | ImageView is set according to the used buffer. The only prerequisite for this trick is 36 | that the native renderer does support double-buffering and can ensure that the two 37 | buffers reside in a contiguous piece of memory with the described layout. 38 | 39 | It is crucial for the performance that the native renderer always renders directly into the 40 | allocated buffer and does not create copies of the buffer other than getting the rendered 41 | image from the graphics hardware into main memory. This transfer into main memory is 42 | not ideal but is currently the most portable solution for a JavaFX integration and is 43 | sufficient for many applications. The nice thing is that this is a constant overhead which 44 | is independent of the complexity of the rendered graphics. 45 | 46 | The user can interact with the renderer via mouse input or gestures on touch devices. 47 | These events are mapped to corresponding application specific commands to update the 48 | rendering parameters. For this example I have implemented moving arround the graphics 49 | via dragging with the mouse or a scrolling gesture on a touch device, e.g., movement 50 | of two fingers on a touch pad. (The generated inertial events show nicely how smooth 51 | the movement is.) 52 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | de.mpmediasoft.jfxtools 6 | jfxtools 7 | 1.0.0-SNAPSHOT 8 | pom 9 | JFXToolsAndDemos 10 | A collection of tools and demos for JavaFX. 11 | 12 | 13 | 14 | 15 | UTF-8 16 | 17 | 3.11.0 18 | 3.5.0 19 | 3.3.0 20 | 3.2.2 21 | 3.3.0 22 | 3.1.1 23 | 24 | 25 | 26 | 1.0.0-SNAPSHOT 27 | 3.0.0-RC1 28 | 3.0.0-alpha3 29 | 21 30 | 21.0.2 31 | 32 | 33 | 34 | 35 | 36 | org.apache.maven.plugins 37 | maven-compiler-plugin 38 | ${maven-compiler-plugin.version} 39 | 40 | false 41 | false 42 | ${openjdk.version} 43 | ${openjdk.version} 44 | true 45 | 46 | 47 | 48 | org.apache.maven.plugins 49 | maven-jar-plugin 50 | ${maven-jar-plugin.version} 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-surefire-plugin 57 | ${maven-surefire-plugin.version} 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-source-plugin 62 | ${maven-source-plugin.version} 63 | 64 | 65 | attach-sources 66 | 67 | jar 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | github 78 | GitHub mipastgt Apache Maven Packages 79 | https://maven.pkg.github.com/mipastgt/JFXToolsAndDemos 80 | 81 | 82 | 83 | 84 | 85 | org.openjfx 86 | javafx-base 87 | ${openjfx.version} 88 | 89 | 90 | org.openjfx 91 | javafx-graphics 92 | ${openjfx.version} 93 | 94 | 95 | org.openjfx 96 | javafx-controls 97 | ${openjfx.version} 98 | 99 | 100 | 101 | 102 | jfxtools-jarprocessor 103 | jfxtools-vlcjfx 104 | jfxtools-vlcjfx-demos 105 | jfxtools-awtimage 106 | jfxtools-awtimage-demos 107 | jfxtools-canvas 108 | jfxtools-canvas-demos 109 | jfxtools-skiafx 110 | jfxtools-skiafx-demos 111 | 112 | 113 | -------------------------------------------------------------------------------- /jfxtools-awtimage/src/main/java/de/mpmediasoft/jfxtools/awtimage/AWTImage.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.awtimage; 2 | 3 | import java.awt.Graphics2D; 4 | import java.awt.image.BufferedImage; 5 | import java.awt.image.DataBuffer; 6 | import java.awt.image.DataBufferInt; 7 | import java.nio.IntBuffer; 8 | 9 | import javafx.geometry.Rectangle2D; 10 | import javafx.scene.image.Image; 11 | import javafx.scene.image.PixelBuffer; 12 | import javafx.scene.image.PixelFormat; 13 | import javafx.scene.image.WritableImage; 14 | import javafx.util.Callback; 15 | 16 | /** 17 | * A simple wrapper arround an AWT image which utilizes the new WritableImage 18 | * of JavaFX 13 with support for Buffers. Internally a JavaFX image is created 19 | * which directly uses the same memory as the AWT image. So if you render 20 | * into the AWT image with a AWT graphics context, the result will immediately 21 | * appear on the screen. 22 | * 23 | * @author Michael Paus 24 | */ 25 | public class AWTImage { 26 | 27 | private BufferedImage awtImage; 28 | private Graphics2D g2d; 29 | private PixelBuffer pixelBuffer; 30 | private WritableImage fxImage; 31 | private Callback registeredUpdateCallback; 32 | 33 | /** 34 | * Constructs an internal BufferedImage with the given width and height. 35 | * 36 | * @param width image width 37 | * @param height image height 38 | */ 39 | public AWTImage(int width, int height) { 40 | this(new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE)); 41 | } 42 | 43 | /** 44 | * Wraps an already existing BufferedImage. (Must be of type TYPE_INT_ARGB_PRE). 45 | * 46 | * @param awtImage the image to be wrapped. 47 | */ 48 | public AWTImage(BufferedImage awtImage) { 49 | this.awtImage = awtImage; 50 | g2d = (Graphics2D) awtImage.getGraphics(); 51 | 52 | DataBuffer db = awtImage.getRaster().getDataBuffer(); 53 | DataBufferInt dbi = (DataBufferInt) db; 54 | int[] rawInts = dbi.getData(); 55 | IntBuffer ib = IntBuffer.wrap(rawInts); 56 | assert rawInts.length == awtImage.getWidth() * awtImage.getHeight(); 57 | 58 | PixelFormat pixelFormat = PixelFormat.getIntArgbPreInstance(); 59 | pixelBuffer = new PixelBuffer<>(awtImage.getWidth(), awtImage.getHeight(), ib, pixelFormat); 60 | fxImage = new WritableImage(pixelBuffer); 61 | pixelBuffer.updateBuffer(pb -> null); 62 | } 63 | 64 | /** 65 | * Get access to the internal JavaFX image. 66 | * 67 | * @return the internal JavaFX image. 68 | */ 69 | public Image getFXImage() {return fxImage;} 70 | 71 | /** 72 | * Get access to the internal AWT image. 73 | * 74 | * @return the internal AWT image. 75 | */ 76 | public BufferedImage getAWTImage() {return awtImage;} 77 | 78 | /** 79 | * Get the width of the image. 80 | * 81 | * @return the width of the image. 82 | */ 83 | public int getWidth() {return awtImage.getWidth();} 84 | 85 | /** 86 | * Get the height of the image. 87 | * 88 | * @return the height of the image. 89 | */ 90 | public int getHeight() {return awtImage.getHeight();} 91 | 92 | /** 93 | * Update the image via a one-time-callback. 94 | * 95 | * @param oneTimeUpdateCallback a one-time-callback. 96 | */ 97 | public void update(Callback oneTimeUpdateCallback) { 98 | if (oneTimeUpdateCallback != null) { 99 | pixelBuffer.updateBuffer(pb -> { 100 | final java.awt.geom.Rectangle2D r = oneTimeUpdateCallback.call(g2d); 101 | return (r != null) ? (r.isEmpty() ? Rectangle2D.EMPTY : new Rectangle2D(r.getX(), r.getY(), r.getWidth(), r.getHeight())) : null; 102 | }); 103 | } 104 | } 105 | 106 | /** 107 | * Register a call-back which is used every time the update() function is called. 108 | * 109 | * @param registeredUpdateCallback a call-back which is used every time the update() function is called. 110 | */ 111 | public void setOnUpdate(Callback registeredUpdateCallback) { 112 | this.registeredUpdateCallback = registeredUpdateCallback; 113 | } 114 | 115 | /** 116 | * Update the image via the registerd call-back. 117 | */ 118 | public void update() { 119 | update(registeredUpdateCallback); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /jfxtools-jarprocessor/src/main/java/de/mpmediasoft/jfxtools/jarprocessor/processors/NativeArtifactChecker.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.jarprocessor.processors; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.PrintStream; 7 | import java.net.URI; 8 | import java.nio.file.FileSystem; 9 | import java.nio.file.FileSystems; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.util.Collections; 13 | import java.util.HashSet; 14 | import java.util.Set; 15 | import java.util.spi.ToolProvider; 16 | 17 | import de.mpmediasoft.jfxtools.jarprocessor.AbstractJARProcessor; 18 | import de.mpmediasoft.jfxtools.jarprocessor.JARProcessorException; 19 | 20 | /** 21 | * This jar-processor analyzes a list of jar-files and lists all native artifacts 22 | * contained in each file. All files which end with ".o", ".a", ".so", ".dll", ".dylib" 23 | * or ".jnilib" are listed. 24 | * 25 | * This is useful to know if you want to strip unneeded files from a build or if you want 26 | * to check if all native artifacts in your build have been properly signed. This is, e.g, 27 | * necessary to get a bundled app notarized by Apple. 28 | * 29 | * This code needs Java 11+. 30 | * 31 | * @author mpaus 32 | */ 33 | public class NativeArtifactChecker extends AbstractJARProcessor { 34 | 35 | private final static String indent = " "; 36 | 37 | private final Set fxmlClasses = new HashSet<>(); 38 | 39 | private ToolProvider jar; 40 | 41 | private boolean verbose = false; 42 | 43 | private int errors = 0; 44 | 45 | private int counter = 0; 46 | 47 | @Override 48 | public void initialize(ToolProvider jarToolProvider, boolean verbose) { 49 | this.jar = jarToolProvider; 50 | this.verbose = verbose; 51 | } 52 | 53 | @Override 54 | public void start() { 55 | fxmlClasses.clear(); 56 | errors = 0; 57 | } 58 | 59 | @Override 60 | public void process(String arg) throws JARProcessorException { 61 | File jarFile = jarFile(arg); 62 | if (verbose) System.out.println("JAR: " + jarFile); 63 | 64 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 65 | PrintStream ps = new PrintStream(baos); 66 | 67 | String[] jarListArgs = { "--list", "--file", jarFile.getAbsolutePath() }; 68 | 69 | int status = jar.run(ps, System.err, jarListArgs); 70 | 71 | if (status == 0) { 72 | String res = new String(baos.toByteArray()); 73 | res.lines().filter(s -> { 74 | return 75 | s.endsWith(".o") || 76 | s.endsWith(".a") || 77 | s.endsWith(".so") || 78 | s.endsWith(".dll") || 79 | s.endsWith(".dylib") || 80 | s.endsWith(".jnilib"); 81 | }).forEach(l -> { 82 | try { 83 | processFXML(jarFile, l); 84 | } catch (JARProcessorException e) { 85 | ++errors; 86 | if (verbose) e.printStackTrace(); 87 | } 88 | }); 89 | if (errors > 0) throw new JARProcessorException("process terminated with errors."); 90 | } else { 91 | throw new JARProcessorException("jar tool terminated with errors."); 92 | } 93 | } 94 | 95 | @Override 96 | public void finish() { 97 | if (verbose) System.out.println(counter + " native artifacts found."); 98 | } 99 | 100 | private void processFXML(File jarFile, String line) throws JARProcessorException { 101 | ++counter; 102 | URI uri = URI.create(String.format("jar:" + jarFile.toURI())); 103 | try (FileSystem jfs = FileSystems.newFileSystem(uri, Collections.emptyMap())) { 104 | Path path = jfs.getPath(line); 105 | if (Files.isRegularFile(path)) { 106 | System.out.println(indent + path); 107 | } else { 108 | throw new JARProcessorException("No regular file: " + path); 109 | } 110 | } catch (IOException e) { 111 | throw new JARProcessorException("Error processing line: " + line); 112 | } 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /jfxtools-awtimage-demos/src/main/java/de/mpmediasoft/jfxtools/awtimage/demo/AWTImageDemo.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.awtimage.demo; 2 | 3 | import java.awt.BasicStroke; 4 | import java.awt.Color; 5 | import java.awt.geom.Path2D; 6 | import java.awt.image.BufferedImage; 7 | import java.io.File; 8 | import java.io.IOException; 9 | 10 | import javax.imageio.ImageIO; 11 | 12 | import de.mpmediasoft.jfxtools.awtimage.AWTImage; 13 | import javafx.application.Application; 14 | import javafx.embed.swing.SwingFXUtils; 15 | import javafx.scene.Scene; 16 | import javafx.scene.control.Button; 17 | import javafx.scene.control.ToolBar; 18 | import javafx.scene.image.Image; 19 | import javafx.scene.image.ImageView; 20 | import javafx.scene.layout.BorderPane; 21 | import javafx.stage.Stage; 22 | 23 | /** 24 | * A simple demo to show how the AWTImage class is supposed to be used. 25 | * 26 | * @author Michael Paus 27 | */ 28 | public class AWTImageDemo extends Application { 29 | 30 | private AWTImage awtImage = new AWTImage(800, 600); 31 | 32 | @Override 33 | public void init() { 34 | System.out.println("java.runtime.version: " + System.getProperty("java.runtime.version", "(undefined)")); 35 | System.out.println("javafx.runtime.version: " + System.getProperty("javafx.runtime.version", "(undefined)")); 36 | } 37 | 38 | Color c1 = Color.red; 39 | Color c2 = Color.green; 40 | Color c3 = Color.blue; 41 | 42 | Color c; 43 | 44 | @Override 45 | public void start(Stage primaryStage) throws Exception { 46 | Button b1 = new Button("Full (RED)"); 47 | b1.setOnAction(e -> { 48 | c = c1; 49 | awtImage.update(); 50 | }); 51 | 52 | Button b2 = new Button("Partial (GREEN)"); 53 | b2.setOnAction(e -> { 54 | c = c2; 55 | awtImage.update(); 56 | }); 57 | 58 | Button b3 = new Button("Empty (BLUE)"); 59 | b3.setOnAction(e -> { 60 | c = c3; 61 | awtImage.update(); 62 | }); 63 | 64 | Button b4 = new Button("Save to 'awtimage.png'"); 65 | b4.setOnAction(e -> { 66 | try { 67 | ImageIO.write(awtImage.getAWTImage(), "png", new File("awtimage.png")); 68 | } catch (IOException e1) { 69 | e1.printStackTrace(); 70 | } 71 | }); 72 | 73 | Button b5 = new Button("Save to 'awtimage.jpg'"); 74 | b5.setOnAction(e -> { 75 | try { 76 | // This is a work-around for a java bug if images with alpha are stored as JPEGs. 77 | Image fxImage = awtImage.getFXImage(); 78 | BufferedImage awtImage = new BufferedImage((int) fxImage.getWidth(), (int) fxImage.getHeight(), BufferedImage.TYPE_INT_RGB); 79 | ImageIO.write(SwingFXUtils.fromFXImage(fxImage, awtImage), "jpeg", new File("awtimage.jpg")); 80 | } catch (IOException e1) { 81 | e1.printStackTrace(); 82 | } 83 | }); 84 | 85 | ToolBar toolbar = new ToolBar(b1, b2, b3, b4, b5); 86 | 87 | BorderPane root = new BorderPane(); 88 | root.setTop(toolbar); 89 | 90 | root.setCenter(new ImageView(awtImage.getFXImage())); 91 | Scene scene = new Scene(root); 92 | primaryStage.setScene(scene); 93 | primaryStage.show(); 94 | 95 | awtImage.setOnUpdate(g2d -> { 96 | // This is pure AWT. 97 | 98 | g2d.setBackground(Color.decode("#F0F0FF")); 99 | g2d.clearRect(0, 0, awtImage.getWidth(), awtImage.getHeight()); 100 | 101 | Path2D p = new Path2D.Double(); 102 | p.moveTo(100, 100); 103 | p.lineTo(700, 300); 104 | p.lineTo(200, 500); 105 | p.closePath(); 106 | 107 | g2d.setColor(c); 108 | g2d.fill(p); 109 | g2d.setColor(new Color(50, 100, 150)); 110 | g2d.setStroke(new BasicStroke(10)); 111 | g2d.draw(p); 112 | 113 | if (c == c1) { 114 | System.out.println("Full update."); 115 | return null; // Full 116 | } else if (c == c2) { 117 | System.out.println("Partial update."); 118 | return new java.awt.geom.Rectangle2D.Double(0, 0, awtImage.getWidth() / 2, awtImage.getHeight()); // Partial 119 | } else { 120 | System.out.println("Empty update."); 121 | return new java.awt.geom.Rectangle2D.Double(); // Empty 122 | } 123 | }); 124 | } 125 | 126 | public static void main(String[] args) { 127 | launch(args); 128 | } 129 | 130 | } 131 | 132 | class AWTImageDemoLauncher {public static void main(String[] args) {AWTImageDemo.main(args);}} 133 | -------------------------------------------------------------------------------- /jfxtools-skiafx/src/main/java/de/mpmediasoft/jfxtools/skiafx/SkiaSurfaceFX.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.skiafx; 2 | 3 | import java.lang.foreign.MemorySegment; // new 4 | //import jdk.incubator.foreign.MemorySegment; // old 5 | 6 | import java.lang.reflect.Field; 7 | import java.nio.Buffer; 8 | import java.nio.ByteBuffer; 9 | import java.nio.IntBuffer; 10 | 11 | import io.github.humbleui.skija.Canvas; 12 | import io.github.humbleui.skija.ImageInfo; 13 | import io.github.humbleui.skija.Surface; 14 | 15 | import javafx.geometry.Rectangle2D; 16 | import javafx.scene.image.Image; 17 | import javafx.scene.image.PixelBuffer; 18 | import javafx.scene.image.PixelFormat; 19 | import javafx.scene.image.WritableImage; 20 | import javafx.util.Callback; 21 | 22 | /** 23 | * A JavaFX wrapper class for a Skia Surface. 24 | * You can draw into the Skia Canvas of it via the render method. 25 | * The rendering result is made directly available in an image via 26 | * a PixelBuffer. 27 | * 28 | * @author Michael Paus 29 | */ 30 | public class SkiaSurfaceFX { 31 | 32 | // Uses the "Foreign Memory Access API" introduced in Java 14. 33 | // Needs --add-modules=jdk.incubator.foreign on the command line if set to true! 34 | private final static boolean AVOID_ILLEGAL_REFLECTION = true; 35 | 36 | public static interface RenderCallback extends Callback {} 37 | 38 | private final ByteBuffer byteBuffer; 39 | 40 | private final PixelBuffer pixelBuffer; 41 | 42 | private final Image image; 43 | 44 | private final Surface surface; 45 | 46 | /** 47 | * Generates a raster Skia surface.
48 | * Note: Skia does not do any automatic scaling for Retina or HighDPI screens 49 | * like the JavaFX canvas does. The user has to take care of that manually. 50 | * 51 | * @param width the width in pixels. 52 | * @param height the height in pixels. 53 | */ 54 | public SkiaSurfaceFX(int width, int height) { 55 | try { 56 | byteBuffer = ByteBuffer.allocateDirect(width * height * 4); 57 | pixelBuffer = new PixelBuffer<>(width, height, byteBuffer.asIntBuffer(), PixelFormat.getIntArgbPreInstance()); 58 | image = new WritableImage(pixelBuffer); 59 | surface = Surface.makeRasterDirect(ImageInfo.makeN32Premul(width, height), getBufferPointer(byteBuffer), width * 4); 60 | } catch (Exception e) { 61 | throw new RuntimeException("Creation of Skia surface failed.", e); 62 | } 63 | } 64 | 65 | /** 66 | * Get the byte buffer if you need it. 67 | * 68 | * @return the byte buffer. 69 | */ 70 | public ByteBuffer getByteBuffer() { 71 | return byteBuffer; 72 | } 73 | 74 | /** 75 | * Get the pixel buffer if you need it. 76 | * 77 | * @return the pixel buffer. 78 | */ 79 | public PixelBuffer getPixelBuffer() { 80 | return pixelBuffer; 81 | } 82 | 83 | /** 84 | * Get the image the Canvas is rendering to. 85 | * 86 | * @return the image. 87 | */ 88 | public Image getImage() { 89 | return image; 90 | } 91 | 92 | /** 93 | * Get the Skija Surface if you need it. 94 | * 95 | * @return the Skija Surface. 96 | */ 97 | public Surface getSurface() { 98 | return surface; 99 | } 100 | 101 | /** 102 | * Render something into the Canvas of this Surface. 103 | * You have to provide a Callback which takes a Canvas and returns a JavaFX 104 | * Rectangle2D or null. See the PixelBuffer.updateBuffer method for more details. 105 | * 106 | * @param renderer the renderer Callback. 107 | */ 108 | public void render(RenderCallback renderer) { 109 | pixelBuffer.updateBuffer(pb -> renderer.call(surface.getCanvas())); 110 | } 111 | 112 | private final long getBufferPointer(ByteBuffer byteBuffer) throws Exception { 113 | if (AVOID_ILLEGAL_REFLECTION) { 114 | // return MemorySegment.ofByteBuffer(byteBuffer).address().toRawLongValue(); // old 115 | return MemorySegment.ofBuffer(byteBuffer).address(); // new 116 | } else { 117 | Field address = Buffer.class.getDeclaredField("address"); 118 | address.setAccessible(true); 119 | return address.getLong(byteBuffer); 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /jfxtools-jarprocessor/src/main/java/de/mpmediasoft/jfxtools/jarprocessor/main/JARProcessorRunner.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.jarprocessor.main; 2 | 3 | import java.io.PrintStream; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.Locale; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | import java.util.spi.ToolProvider; 11 | 12 | import de.mpmediasoft.jfxtools.jarprocessor.JARProcessor; 13 | import de.mpmediasoft.jfxtools.jarprocessor.JARProcessorException; 14 | import de.mpmediasoft.jfxtools.jarprocessor.processors.FXMLChecker; 15 | import de.mpmediasoft.jfxtools.jarprocessor.processors.ModuleChecker; 16 | import de.mpmediasoft.jfxtools.jarprocessor.processors.NativeArtifactChecker; 17 | 18 | public class JARProcessorRunner { 19 | 20 | private static Optional jarToolProvider = ToolProvider.findFirst("jar"); 21 | 22 | private static Map processorMap = Map.of( 23 | "ModuleChecker", new ModuleChecker(), 24 | "FXMLChecker", new FXMLChecker(), 25 | "NativeArtifactChecker", new NativeArtifactChecker() 26 | ); 27 | 28 | private boolean verbose = false; 29 | 30 | private JARProcessor jarProcessor; 31 | 32 | public JARProcessorRunner(JARProcessor jarProcessor) { 33 | this.jarProcessor = jarProcessor; 34 | } 35 | 36 | private void process(List argsList) { 37 | if (jarToolProvider.isPresent()) { 38 | if (argsList.size() > 0) { 39 | if (argsList.get(0).equals("-v")) { 40 | verbose = true; 41 | argsList.remove(0); 42 | } 43 | if (argsList.size() > 0) { 44 | jarProcessor.initialize(jarToolProvider.get(), verbose); 45 | jarProcessor.start(); 46 | for (String arg : argsList) { 47 | try { 48 | jarProcessor.process(arg); 49 | } catch (JARProcessorException e) { 50 | if (verbose) e.printStackTrace(); 51 | errorExit("Processing failed for: " + arg); 52 | } 53 | } 54 | jarProcessor.finish(); 55 | } 56 | } else { 57 | errorExit("No JAR file specified."); 58 | } 59 | } else { 60 | errorExit("JAR tool not found."); 61 | } 62 | } 63 | 64 | private static void errorExit(String message) { 65 | System.err.println(message); 66 | System.err.println(); 67 | printUsage(System.err); 68 | System.exit(1); 69 | } 70 | 71 | private static void printUsage(PrintStream ps) { 72 | ps.println("USAGE: "); 73 | ps.println(" : Command suitable to launch the class JARProcessorRunner. (Depends on packaging.)"); 74 | ps.println(" : Options for the launcher."); 75 | ps.println(" ModuleChecker | FXMLChecker | NativeArtifactChecker : Selects the processor."); 76 | ps.println(" -v : (optional) Makes the output verbose."); 77 | ps.println(" : Options only valid for the selected processor."); 78 | ps.println(" -p|s|n : (only FXMLChecker) : Output format (P)lain, Maven (S)hade or Maven Gluon (N)ative."); 79 | ps.println(" : Space separated list of fully qualified JAR files."); 80 | } 81 | 82 | public static void main(String[] args) { 83 | // It is important to set the default locale because different 84 | // Java distributions handle the default language used by tools 85 | // differently. 86 | // The output of the 'jar' tool with Oracle JDK is always 87 | // english whereas AdoptOpenJDK translates the output into 88 | // the language determined by the default locale, which would make 89 | // parsing of the results impossible. Therefore we enforce a US locale. 90 | Locale.setDefault(Locale.US); 91 | 92 | List argsList = new ArrayList(Arrays.asList(args)); // We need a mutable list here! 93 | 94 | if (argsList.size() > 1) { 95 | JARProcessor processor = processorMap.get(argsList.remove(0)); 96 | if (processor != null) { 97 | JARProcessorRunner runner = new JARProcessorRunner(processor); 98 | runner.process(argsList); 99 | } else { 100 | errorExit("No valid processor specified."); 101 | } 102 | } else { 103 | errorExit("Not enough arguments."); 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /jfxtools-vlcjfx-demos/src/main/java/de/mpmediasoft/jfxtools/vlcjfx/demo/VLCJFXVideoPlayerDemo2.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.vlcjfx.demo; 2 | 3 | import java.util.Map; 4 | 5 | import de.mpmediasoft.jfxtools.vlcjfx.VLCJFXVideoPlayer; 6 | import javafx.application.Application; 7 | import javafx.collections.FXCollections; 8 | import javafx.geometry.Pos; 9 | import javafx.scene.Scene; 10 | import javafx.scene.control.Button; 11 | import javafx.scene.control.ComboBox; 12 | import javafx.scene.image.ImageView; 13 | import javafx.scene.layout.AnchorPane; 14 | import javafx.scene.layout.HBox; 15 | import javafx.scene.layout.StackPane; 16 | import javafx.stage.Stage; 17 | 18 | /** 19 | * An extended demo program to show how the VLCJFXVideoPlayer class is supposed to be used. 20 | * It provides some controls to select a video and play, pause and stop it as an overlay 21 | * on top of the video. 22 | * 23 | * This should work on macOS and Linux but there currently seem to be problems with the VLC code 24 | * on Windows. 25 | * 26 | * In order to run the code, a recent version of the VLC player (3.0.x+) must be installed 27 | * on the system. Other dependencies can be found in the pom.xml. 28 | * 29 | * @author Michael Paus 30 | */ 31 | public class VLCJFXVideoPlayerDemo2 extends Application { 32 | 33 | private final Map videoMap = Map.of( 34 | "BigBuckBunny", "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", 35 | "ElephantsDream", "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", 36 | "Sintel", "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4", 37 | "TearsOfSteel", "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4" 38 | ); 39 | 40 | private final ComboBox videoSelectionComboBox = new ComboBox<>(FXCollections.observableArrayList(videoMap.keySet())); 41 | 42 | private final double WIDTH = 1200; 43 | private final double HEIGHT = 675; 44 | 45 | private VLCJFXVideoPlayer videoPlayer; 46 | 47 | private ImageView videoImageView; 48 | 49 | @Override 50 | public void init() { 51 | System.out.println("java.runtime.version: " + System.getProperty("java.runtime.version", "(undefined)")); 52 | System.out.println("javafx.runtime.version: " + System.getProperty("javafx.runtime.version", "(undefined)")); 53 | } 54 | 55 | @Override 56 | public void start(Stage primaryStage) throws Exception { 57 | videoPlayer = new VLCJFXVideoPlayer(); 58 | videoPlayer.autoPlayProperty().set(true); 59 | 60 | StackPane root = new StackPane(); 61 | root.getStyleClass().add("videoPane"); 62 | 63 | videoImageView = new ImageView(); 64 | videoImageView.setPreserveRatio(true); 65 | videoImageView.fitWidthProperty().bind(root.widthProperty()); 66 | videoImageView.fitHeightProperty().bind(root.heightProperty()); 67 | videoImageView.imageProperty().bind(videoPlayer.videoImageProperty()); 68 | root.getChildren().add(videoImageView); 69 | 70 | AnchorPane controlsPane = new AnchorPane(); 71 | root.getChildren().add(controlsPane); 72 | 73 | videoSelectionComboBox.getSelectionModel().select(0); 74 | videoSelectionComboBox.setOnAction(ev -> updateMediaResource()); 75 | 76 | Button playButton = new Button("Play"); 77 | playButton.setOnAction(ev -> videoPlayer.controls().play()); 78 | 79 | Button pauseButton = new Button("Pause"); 80 | pauseButton.setOnAction(ev -> videoPlayer.controls().setPause(true)); 81 | 82 | Button stopButton = new Button("Stop"); 83 | stopButton.setOnAction(ev -> videoPlayer.controls().stop()); 84 | 85 | HBox controls = new HBox(); 86 | controls.setAlignment(Pos.CENTER); 87 | controls.setSpacing(20); 88 | controls.getChildren().addAll(videoSelectionComboBox, playButton, pauseButton, stopButton); 89 | 90 | AnchorPane.setLeftAnchor(controls, 20.0); 91 | AnchorPane.setBottomAnchor(controls, 20.0); 92 | AnchorPane.setRightAnchor(controls, 20.0); 93 | 94 | controlsPane.getChildren().add(controls); 95 | 96 | Scene scene = new Scene(root, WIDTH, HEIGHT); 97 | scene.getStylesheets().add("/video_player.css"); 98 | primaryStage.setScene(scene); 99 | primaryStage.show(); 100 | 101 | updateMediaResource(); 102 | } 103 | 104 | @Override 105 | public void stop() throws Exception { 106 | videoPlayer.dispose(); 107 | } 108 | 109 | private void updateMediaResource() { 110 | String mediaResource = videoMap.get(videoSelectionComboBox.getSelectionModel().getSelectedItem()); 111 | videoPlayer.mediaResourceLocatorProperty().set(mediaResource); 112 | } 113 | 114 | public static void main(String[] args) { 115 | launch(args); 116 | } 117 | 118 | } 119 | 120 | //Launch via this class to avoid module system headaches. 121 | class VLCJFXVideoPlayerDemo2Launcher {public static void main(String[] args) {VLCJFXVideoPlayerDemo2.main(args);}} 122 | -------------------------------------------------------------------------------- /docs/articles/JFX-Android/JFX-Android.adoc: -------------------------------------------------------------------------------- 1 | = Building a native, pure JavaFX Android application 2 | Dr.-Ing. Michael Paus, mpMediaSoft GmbH 3 | Version 1.1.0, 2020-06-21 4 | :doctype: article 5 | :encoding: utf-8 6 | :lang: en 7 | :toc: left 8 | :numbered: 9 | 10 | [.lead] 11 | A summary of my lessons learned building a first, still simple, pure JavaFX Android 12 | application based on the components of an already existing JavaFX application using 13 | the Gluon Client Maven plugin which is based on the GraalVM `native-image` toolchain. 14 | 15 | == Overview 16 | 17 | First of all I would like to thank the team at Gluon again which made all the following possible. 18 | Without their continuous, hard work it would not be possible at all to compile a modern 19 | JavaFX application into a native Android application. 20 | My thanks of course also include all the people involved in the GraalVM project 21 | which laid the foundation for the work done for JavaFX by Gluon. 22 | 23 | This article summarizes the experiences which I collected while converting one of my 24 | existing Java applications to a native Android application using the Gluon Client Maven plugin. 25 | The application is still relatively small and manageable but otherwise a real-world application 26 | with a lot of external dependencies and technical challenges. It was not always 27 | an easy ride but in the end it worked out nicely. 28 | 29 | == The application 30 | 31 | [.float-group] 32 | -- 33 | [.left] 34 | .Welcome screen in portrait mode. 35 | image::images/001p-mpcopilotapp2.png[Welcome, 400, 400] 36 | 37 | This is the initial welcome screen which also serves the purpose of an about screen. 38 | 39 | Two variants of this screen had to be provided so that it looks nice in portrait as well 40 | as in landscape mode. The switching between the two can then be done easily in standard JavaFX. 41 | -- 42 | 43 | [.float-group] 44 | -- 45 | [.left] 46 | .Welcome screen in landscape mode. 47 | image::images/001l-mpcopilotapp2.png[Welcome, 400, 400] 48 | -- 49 | 50 | [.float-group] 51 | -- 52 | [.left] 53 | .Map. 54 | image::images/002-mpcopilotapp2.png[Map, 400, 400] 55 | 56 | This is the normal map mode. You can navigate the map with the usual 57 | gestures (scroll, zoom and rotate). 58 | 59 | The map display is written in pure JavaFx and is identical to the one 60 | used on desktop with the regular Java VM. For better performance it 61 | makes even use of some 3D features which also work nicely on Android. 62 | -- 63 | 64 | [.float-group] 65 | -- 66 | [.left] 67 | .Menu. 68 | image::images/003-mpcopilotapp2.png[Menu, 400, 400] 69 | 70 | Via the menu you can reach several other pages. 71 | -- 72 | 73 | [.float-group] 74 | -- 75 | [.left] 76 | .Location tracking. 77 | image::images/004-mpcopilotapp2.png[Location tracking, 400, 400] 78 | 79 | On this page you can activate tracking. It takes the position data from 80 | the built-in GPS and sends it via MQTT to a public MQTT-Server. 81 | 82 | In this case I had to implement my own Position-Service because the 83 | existing one does not provide all the data I need, e.g., the precise 84 | time stamp and also speed and bearing. My service is just an extension of 85 | the existing one, so doing this was not a big deal. 86 | 87 | The MQTT-Client is just the standard Eclipse Paho Client out of the box. 88 | -- 89 | 90 | [.float-group] 91 | -- 92 | [.left] 93 | .Location and orientation on map. 94 | image::images/005-mpcopilotapp2.png[Location and orientation on map, 400, 400] 95 | 96 | When you go back to the map while tracking is activated, the map will show 97 | an own-ship symbol and will be rotated according to the current bearing 98 | (track-up mode). 99 | -- 100 | 101 | [.float-group] 102 | -- 103 | [.left] 104 | .Map settings. 105 | image::images/006-mpcopilotapp2.png[Map settings, 400, 400] 106 | 107 | On the map settings page you can manage the locally installed maps and select one map as the current map. The image shows the view while it is just downloading the map for Austria. The colors of the progress bar indicate the status of the respective map. The load/unload buttons on the left are disabled while a download is running in the background. 108 | -- 109 | == Software 110 | 111 | === Libraries 112 | 113 | This first, still relatively simple application already uses several external libraries 114 | which can all be used in their standard implementation right from the Maven repository. 115 | No special Android version is needed. 116 | 117 | * slf4j 118 | * ikonli 119 | * paho 120 | * jackson 121 | * miglayout 122 | * commons-io 123 | * commons-lang3 124 | * commons-math3 125 | * earth-gravitational-model 126 | * GeographicLib 127 | * plus all the JavaFX-specific libraries from OpenJFX/Gluon 128 | 129 | === Development 130 | 131 | The build step still takes a long time. For this project, on my Intel NUC with Ubuntu 20.04, 132 | a build takes about 4-5 minutes. This isn't nice but it also isn't a big problem because 133 | you do not have to go through this all the time. I currently use a development style 134 | where I do all development and testing on my preferred platform (macOS) with the regular 135 | Java VM. This works because the GUI is all pure JavaFX and even the mobile specific controls 136 | work on desktop. Therefore you do not even need any emulator because you can just work 137 | in your preferred development environment. When I am ready with some funtionality I synchronize 138 | the NUC with the GIT repository and start a build for Android, so that I can test it on a real 139 | device. 140 | 141 | == Conclusion 142 | 143 | This is only a snapshot of my experiences so far in getting a JavaFX 144 | application compiled into a native Android application. 145 | The resulting binary seems to be 146 | very stable and the performance is also quite good. All in all we seem to be on 147 | a very good track here. 148 | -------------------------------------------------------------------------------- /jfxtools-jarprocessor/src/main/java/de/mpmediasoft/jfxtools/jarprocessor/processors/FXMLChecker.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.jarprocessor.processors; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.PrintStream; 7 | import java.net.URI; 8 | import java.nio.file.FileSystem; 9 | import java.nio.file.FileSystems; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.util.Collections; 13 | import java.util.HashSet; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.Set; 17 | import java.util.spi.ToolProvider; 18 | 19 | import de.mpmediasoft.jfxtools.jarprocessor.AbstractJARProcessor; 20 | import de.mpmediasoft.jfxtools.jarprocessor.JARProcessorException; 21 | 22 | /** 23 | * This jar-processor analyzes a list of jar-files and lists all FXML files 24 | * contained in each file. 25 | * 26 | * It further examines all import statements of the FXML files and finally 27 | * prints out a combined list of all classes loaded by all the jar files. 28 | * This is useful for tools like the maven shade plugin, ProGuard of GluonHQs 29 | * client-maven-plugin for which you have to provide a list of all classes 30 | * which have to be kept but which cannot be found by static analysis of the 31 | * code. 32 | * 33 | * This code needs Java 11+. 34 | * 35 | * @author mpaus 36 | */ 37 | public class FXMLChecker extends AbstractJARProcessor { 38 | 39 | private final static String indent = " "; 40 | 41 | public static enum TargetFormat {PLAIN, MAVEN_SHADE, MAVEN_GLUON_NATIVE} 42 | 43 | private final static Map formatMap = Map.of( 44 | TargetFormat.PLAIN, "%s\n", 45 | TargetFormat.MAVEN_SHADE, "%s\n", 46 | TargetFormat.MAVEN_GLUON_NATIVE, "%s\n" 47 | ); 48 | 49 | private final Set fxmlClasses = new HashSet<>(); 50 | 51 | private ToolProvider jar; 52 | 53 | private TargetFormat targetFormat = TargetFormat.PLAIN; 54 | 55 | private boolean verbose = false; 56 | 57 | private int errors = 0; 58 | 59 | @Override 60 | public void initialize(ToolProvider jarToolProvider, boolean verbose) { 61 | this.jar = jarToolProvider; 62 | this.verbose = verbose; 63 | } 64 | 65 | @Override 66 | public void start() { 67 | fxmlClasses.clear(); 68 | errors = 0; 69 | } 70 | 71 | @Override 72 | public void process(String arg) throws JARProcessorException { 73 | if (arg.startsWith("-")) { 74 | if (arg.equals("-p")) targetFormat = TargetFormat.PLAIN; 75 | if (arg.equals("-s")) targetFormat = TargetFormat.MAVEN_SHADE; 76 | if (arg.equals("-n")) targetFormat = TargetFormat.MAVEN_GLUON_NATIVE; 77 | } else { 78 | File jarFile = jarFile(arg); 79 | if (verbose) System.out.println("JAR: " + jarFile); 80 | 81 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 82 | PrintStream ps = new PrintStream(baos); 83 | 84 | String[] jarListArgs = { "--list", "--file", jarFile.getAbsolutePath() }; 85 | 86 | int status = jar.run(ps, System.err, jarListArgs); 87 | 88 | if (status == 0) { 89 | String res = new String(baos.toByteArray()); 90 | res.lines().filter(s -> { 91 | return s.endsWith(".fxml"); 92 | }).forEach(l -> { 93 | try { 94 | processFXML(jarFile, l); 95 | } catch (JARProcessorException e) { 96 | ++errors; 97 | if (verbose) e.printStackTrace(); 98 | } 99 | }); 100 | if (errors > 0) throw new JARProcessorException("process terminated with errors."); 101 | } else { 102 | throw new JARProcessorException("jar tool terminated with errors."); 103 | } 104 | } 105 | } 106 | 107 | @Override 108 | public void finish() { 109 | if (verbose) System.out.println("===================================="); 110 | if (verbose) System.out.println("Combined list of classes to include."); 111 | if (verbose) System.out.println("===================================="); 112 | 113 | String format = formatMap.get(targetFormat); 114 | fxmlClasses.forEach(c -> System.out.format(format, (targetFormat == TargetFormat.MAVEN_SHADE) ? c.replaceAll("\\.", "/"): c)); 115 | } 116 | 117 | private void processFXML(File jarFile, String line) throws JARProcessorException { 118 | URI uri = URI.create(String.format("jar:" + jarFile.toURI())); 119 | try (FileSystem jfs = FileSystems.newFileSystem(uri, Collections.emptyMap())) { 120 | Path path = jfs.getPath(line); 121 | if (Files.isRegularFile(path)) { 122 | if (verbose) System.out.println(indent + "FXML: " + path); 123 | List lines = Files.readAllLines(path); 124 | for (String impl : lines) { 125 | String trImpl = impl.trim(); 126 | if (trImpl.startsWith("", "").trim(); 128 | fxmlClasses.add(fxmlClass); 129 | if (verbose) System.out.println(indent + indent + "include: " + fxmlClass); 130 | } 131 | } 132 | } else { 133 | throw new JARProcessorException("No regular file: " + path); 134 | } 135 | } catch (IOException e) { 136 | throw new JARProcessorException("Error processing line: " + line); 137 | } 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /jfxtools-canvas/src/main/c/de_mpmediasoft_jfxtools_canvas_NativeRenderer.c: -------------------------------------------------------------------------------- 1 | /* 2 | A C implementation of the JNI interface as defined in NativeRenderer.java. 3 | The code in here is not really relevant for the example. It just renders a map 4 | consisting of quadratic red and green tiles on a blue background. It is 5 | basically just a placeholder for some real code which uses, e.g., OpenGL or 6 | some other rendering library to create an image representation in a memory buffer. 7 | */ 8 | 9 | #include "de_mpmediasoft_jfxtools_canvas_NativeRenderer.h" 10 | #include 11 | #include 12 | 13 | #define FALSE 0 14 | #define TRUE !(FALSE) 15 | 16 | // Using fbo_clear() without double-buffering may cause flickering. 17 | // We use that here to show the effectiveness of using double-buffering. 18 | int USE_FBO_CLEAR = TRUE; 19 | 20 | int *bak_buf = 0; 21 | int *buf = 0; 22 | long buf_single_size_int = 0; 23 | jlong buf_total_size_byte = 0; 24 | 25 | int buffers = 0; 26 | int current_buffer_index = 0; 27 | int current_buffer_offset_int = 0; 28 | 29 | int view_x = 0; 30 | int view_y = 0; 31 | int view_width = 0; 32 | int view_height = 0; 33 | 34 | int num_tiles_x = 11; // must be an odd number 35 | int num_tiles_y = 11; // " 36 | int tile_size = 256; 37 | 38 | int alpha_mask = 0xFF000000; 39 | int even_color = 0xFFFF0000; 40 | int odd_color = 0xFF00FF00; 41 | int bg_color = 0xFF0000FF; 42 | 43 | void fbo_clear() { 44 | int c = bg_color | alpha_mask; 45 | for (int i = 0; i < buf_single_size_int; ++i) { 46 | buf[current_buffer_offset_int + i] = c; 47 | } 48 | } 49 | 50 | void fbo_fill(int minx, int miny, int maxx, int maxy, int color) { 51 | int c = color | alpha_mask; 52 | for (int y = miny; y <= maxy; ++y) { 53 | if (0 <= y & y < view_height) { 54 | int row_offset_int = view_width * y; 55 | for (int x = minx; x <= maxx; ++x) { 56 | if (0 <= x & x < view_width) { 57 | buf[current_buffer_offset_int + row_offset_int + x] = c; 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | void fill(int minx, int miny, int maxx, int maxy, int color) { 65 | int fbo_minx = minx - view_x; 66 | int fbo_miny = miny - view_y; 67 | int fbo_maxx = maxx - view_x; 68 | int fbo_maxy = maxy - view_y; 69 | if (((0 <= fbo_minx && fbo_minx < view_width) || (0 <= fbo_maxx && fbo_maxx < view_width)) && 70 | ((0 <= fbo_miny && fbo_miny < view_height) || (0 <= fbo_maxy && fbo_maxy < view_height))) { 71 | fbo_fill(fbo_minx, fbo_miny, fbo_maxx, fbo_maxy, color); 72 | } 73 | } 74 | 75 | JNIEXPORT void JNICALL Java_de_mpmediasoft_jfxtools_canvas_NativeRenderer_init (JNIEnv * env, jobject thisObject) { 76 | // Nothing to be done yet. 77 | } 78 | 79 | JNIEXPORT void JNICALL Java_de_mpmediasoft_jfxtools_canvas_NativeRenderer_dispose (JNIEnv* env, jobject thisObject) { 80 | if (bak_buf != 0) free(bak_buf); 81 | if (buf != 0) free(buf); 82 | } 83 | 84 | JNIEXPORT jobject JNICALL Java_de_mpmediasoft_jfxtools_canvas_NativeRenderer_createCanvas (JNIEnv * env, jobject thisObject, jint width, jint height, jint numBuffers, jint nativeColorModel) { 85 | if (1 <= numBuffers && numBuffers <= 2 && nativeColorModel == 0) { 86 | if (USE_FBO_CLEAR && numBuffers == 1) { 87 | fprintf(stdout, "Using fbo_clear() without double-buffering may cause flickering.\n"); fflush(stdout); 88 | } 89 | view_x = 0; 90 | view_y = 0; 91 | view_width = width; 92 | view_height = height; 93 | 94 | buffers = numBuffers; 95 | current_buffer_index = 0; 96 | 97 | buf_single_size_int = view_width * view_height; 98 | current_buffer_offset_int = 0; 99 | buf_total_size_byte = buf_single_size_int * buffers * sizeof(int); 100 | 101 | // Delay cleanup of buffer because it may still be used by the rendering thread. 102 | if (bak_buf != 0) free(bak_buf); 103 | bak_buf = buf; 104 | 105 | buf = (int *) malloc(buf_total_size_byte); 106 | return (*env)->NewDirectByteBuffer(env, buf, buf_total_size_byte); 107 | } else { 108 | return 0L; 109 | } 110 | } 111 | 112 | JNIEXPORT void JNICALL Java_de_mpmediasoft_jfxtools_canvas_NativeRenderer_moveTo (JNIEnv* env, jobject thisObject, jint x, jint y) { 113 | view_x = x; 114 | view_y = y; 115 | } 116 | 117 | JNIEXPORT jint JNICALL Java_de_mpmediasoft_jfxtools_canvas_NativeRenderer_render (JNIEnv* env, jobject thisObject) { 118 | if (buf != 0) { 119 | ++current_buffer_index; 120 | if (current_buffer_index >= buffers) current_buffer_index = 0; 121 | 122 | current_buffer_offset_int = current_buffer_index * buf_single_size_int; 123 | 124 | if (USE_FBO_CLEAR) { 125 | fbo_clear(); // This flickers without double-buffering. 126 | } else { 127 | if (view_x < 0) {fbo_fill(0, 0, -1 - view_x, view_height, bg_color);} 128 | if (view_y < 0) {fbo_fill(0, 0, view_width, -1 - view_y, bg_color);} 129 | 130 | int map_width = num_tiles_x * tile_size; 131 | int map_height = num_tiles_y * tile_size; 132 | 133 | if (view_x >= map_width - view_width) {fbo_fill(view_width - (view_x - map_width + view_width), 0, view_width, view_height, bg_color);} 134 | if (view_y >= map_height - view_height) {fbo_fill(0, view_height - (view_y - map_height + view_height), view_width, view_height, bg_color);} 135 | } 136 | 137 | int k = 0; 138 | for (int i = 0; i < num_tiles_y; ++i) { 139 | for (int j = 0; j < num_tiles_x; ++j) { 140 | int minx = j * tile_size; 141 | int miny = i * tile_size; 142 | int maxx = minx + tile_size - 1; 143 | int maxy = miny + tile_size - 1; 144 | fill(minx, miny, maxx, maxy, (k%2 == 0) ? even_color : odd_color); 145 | ++k; 146 | } 147 | } 148 | 149 | return current_buffer_index; 150 | } 151 | return 0; 152 | } 153 | 154 | -------------------------------------------------------------------------------- /jfxtools-skiafx-demos/src/main/java/de/mpmediasoft/jfxtools/skiafx/demo/SkiaSurfaceFXDemo1.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.skiafx.demo; 2 | 3 | import static java.lang.Math.cos; 4 | import static java.lang.Math.sin; 5 | 6 | import io.github.humbleui.skija.Color; 7 | import io.github.humbleui.skija.Paint; 8 | import io.github.humbleui.skija.PaintMode; 9 | import io.github.humbleui.skija.Path; 10 | import io.github.humbleui.skija.PathEffect; 11 | import io.github.humbleui.types.Point; 12 | import io.github.humbleui.skija.Shader; 13 | 14 | import de.mpmediasoft.jfxtools.skiafx.SkiaSurfaceFX; 15 | import de.mpmediasoft.jfxtools.skiafx.SkiaSurfaceFX.RenderCallback; 16 | import javafx.application.Application; 17 | import javafx.scene.Scene; 18 | import javafx.scene.control.Button; 19 | import javafx.scene.image.ImageView; 20 | import javafx.scene.layout.BorderPane; 21 | import javafx.stage.Stage; 22 | 23 | public class SkiaSurfaceFXDemo1 extends Application { 24 | 25 | public static enum Variant {Discrete, Dash, Composed, Sum, Shaders} 26 | 27 | private int WIDTH = 600, HEIGHT = 400; 28 | 29 | private Path star() { 30 | final float R = 115.2f, C = 128.0f; 31 | Path path = new Path(); 32 | path.moveTo(C + R, C); 33 | for (int i = 1; i < 8; ++i) { 34 | float a = 2.6927937f * i; 35 | path.lineTo(C + R * (float)cos(a), C + R * (float)sin(a)); 36 | } 37 | return path; 38 | } 39 | 40 | private RenderCallback starDemo(Variant variant) { 41 | return canvas -> { 42 | Paint paint = new Paint(); 43 | paint.setPathEffect(PathEffect.makeDiscrete(10.0f, 4.0f, 0)); 44 | paint.setStrokeWidth(2.0f); 45 | paint.setAntiAlias(true); 46 | paint.setColor(0xff4285F4); 47 | 48 | if (variant == Variant.Discrete) { 49 | paint.setMode(PaintMode.STROKE); 50 | paint.setPathEffect(PathEffect.makeDiscrete(10.0f, 4.0f, 0)); 51 | } else if (variant == Variant.Dash) { 52 | paint.setMode(PaintMode.STROKE); 53 | paint.setPathEffect(PathEffect.makeDash(intervals, 0.0f)); 54 | } else if (variant == Variant.Composed) { 55 | paint.setMode(PaintMode.STROKE); 56 | paint.setPathEffect(PathEffect.makeDash(intervals, 0.0f).makeCompose(PathEffect.makeDiscrete(30.0f, 12.0f, 0))); 57 | } else if (variant == Variant.Sum) { 58 | paint.setMode(PaintMode.STROKE); 59 | paint.setPathEffect(PathEffect.makeDiscrete(10.0f, 4.0f, 0).makeSum(PathEffect.makeDiscrete(10.0f, 4.0f, 1245))); 60 | } else if (variant == Variant.Shaders) { 61 | paint.setPathEffect(PathEffect.makeDiscrete(10.0f, 4.0f, 0)); 62 | Point p0 = new Point(0.0f, 0.0f); 63 | Point p1 = new Point(256.0f, 256.0f); 64 | int[] colors = new int[] {Color.makeRGB(66, 133, 244), Color.makeRGB(15, 157, 88)}; 65 | paint.setShader(Shader.makeLinearGradient(p0, p1, colors)); 66 | } 67 | 68 | canvas.clear(0xFFFFFFD0); 69 | Path path = star(); 70 | canvas.drawPath(path, paint); 71 | 72 | return null; 73 | }; 74 | }; 75 | 76 | private float[] intervals = new float[] {20.0f, 10.0f, 4.0f, 10.0f}; 77 | 78 | @SuppressWarnings("resource") 79 | private RenderCallback[] demos = new RenderCallback[] { 80 | canvas -> { // Demo 0 81 | canvas.clear(0xFFD0FFD0); 82 | 83 | Paint paint = new Paint(); 84 | paint.setColor(0xFFFF0000); 85 | canvas.drawCircle(100, 100, 50, paint); 86 | 87 | return null; 88 | }, 89 | canvas -> { // Demo 1 90 | canvas.clear(0xFFFFD0FF); 91 | 92 | canvas.drawTriangles(new Point[] { 93 | new Point(320, 70), 94 | new Point(194, 287), 95 | new Point(446, 287) 96 | }, 97 | new int[] { 0xFFFF0000, 0xFF00FF00, 0xFF0000FF }, 98 | new Paint()); 99 | 100 | Path path = new Path().moveTo(253, 216) 101 | .cubicTo(283, 163.5f, 358, 163.5f, 388, 216) 102 | .cubicTo(358, 268.5f, 283, 268.5f, 253, 216) 103 | .closePath(); 104 | canvas.drawPath(path, new Paint().setColor(0xFFFFFFFF)); 105 | 106 | canvas.drawCircle(320, 217, 16, new Paint().setColor(0xFF000000)); 107 | 108 | return null; 109 | }, 110 | starDemo(Variant.Discrete), // Demo 2 111 | starDemo(Variant.Dash), // Demo 3 112 | starDemo(Variant.Composed), // Demo 4 113 | starDemo(Variant.Sum), // Demo 5 114 | starDemo(Variant.Shaders) // Demo 6 115 | }; 116 | 117 | private SkiaSurfaceFX surface; 118 | 119 | private int demoId = 0; 120 | 121 | @Override 122 | public void start(Stage primaryStage) throws Exception { 123 | Button nextDemoButton = new Button("Next"); 124 | nextDemoButton.setOnAction(ev -> { 125 | ++demoId; 126 | if (demoId >= demos.length) demoId = 0; 127 | surface.render(demos[demoId]); 128 | }); 129 | nextDemoButton.setMaxWidth(Double.MAX_VALUE); 130 | 131 | surface = new SkiaSurfaceFX(WIDTH, HEIGHT); 132 | ImageView imageView = new ImageView(surface.getImage()); 133 | 134 | BorderPane root = new BorderPane(); 135 | root.setTop(nextDemoButton); 136 | root.setCenter(imageView); 137 | 138 | Scene scene = new Scene(root); 139 | primaryStage.setScene(scene); 140 | primaryStage.show(); 141 | 142 | surface.render(demos[demoId]); 143 | } 144 | 145 | public static void main(String[] args){ 146 | launch(args); 147 | } 148 | 149 | } 150 | 151 | class SkiaSurfaceFXDemo1Launcher {public static void main(String[] args) {SkiaSurfaceFXDemo1.main(args);}} 152 | -------------------------------------------------------------------------------- /jfxtools-vlcjfx/src/main/java/de/mpmediasoft/jfxtools/vlcjfx/VLCJFXVideoPlayer.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.vlcjfx; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | import javafx.application.Platform; 6 | import javafx.beans.property.BooleanProperty; 7 | import javafx.beans.property.ReadOnlyBooleanProperty; 8 | import javafx.beans.property.ReadOnlyBooleanWrapper; 9 | import javafx.beans.property.ReadOnlyObjectProperty; 10 | import javafx.beans.property.ReadOnlyObjectWrapper; 11 | import javafx.beans.property.SimpleBooleanProperty; 12 | import javafx.beans.property.SimpleStringProperty; 13 | import javafx.beans.property.StringProperty; 14 | import javafx.scene.image.Image; 15 | import javafx.scene.image.PixelBuffer; 16 | import javafx.scene.image.PixelFormat; 17 | import javafx.scene.image.WritableImage; 18 | import uk.co.caprica.vlcj.factory.MediaPlayerFactory; 19 | import uk.co.caprica.vlcj.player.base.ControlsApi; 20 | import uk.co.caprica.vlcj.player.base.MediaPlayer; 21 | import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer; 22 | import uk.co.caprica.vlcj.player.embedded.videosurface.CallbackVideoSurface; 23 | import uk.co.caprica.vlcj.player.embedded.videosurface.VideoSurfaceAdapters; 24 | import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormat; 25 | import uk.co.caprica.vlcj.player.embedded.videosurface.callback.BufferFormatCallback; 26 | import uk.co.caprica.vlcj.player.embedded.videosurface.callback.RenderCallback; 27 | import uk.co.caprica.vlcj.player.embedded.videosurface.callback.format.RV32BufferFormat; 28 | 29 | /** 30 | * This is a very simple example of a video player which uses the new WritableImage 31 | * of JavaFX 13 with support for Buffers. The idea is to let VLC directly render into 32 | * this buffer and use the image directly in an ImageView without any explicit rendering 33 | * into a canvas or such thing. Only this brings the desired performance boost. 34 | * 35 | * What I have not considered yet is any kind of synchronization. 36 | * I think an extension of the PixelBuffer to support some kine of double-buffering 37 | * would be the right thing to do. 38 | * 39 | * This should work on macOS and Linux but there currently seem to be problems with the VLC code 40 | * on Windows. 41 | * 42 | * In order to run the code, a recent version of the VLC player (3.0.x+) must be installed 43 | * on the system. Other dependencies can be found in the pom.xml. 44 | * 45 | * Tested on macOS 10.14.6 and Linux. 46 | * 47 | * @author Michael Paus 48 | */ 49 | public class VLCJFXVideoPlayer { 50 | 51 | private final MediaPlayerFactory mediaPlayerFactory; 52 | 53 | private final EmbeddedMediaPlayer embeddedMediaPlayer; 54 | 55 | private PixelBuffer videoPixelBuffer; 56 | 57 | private final StringProperty mediaResourceLocator = new SimpleStringProperty(null); 58 | public StringProperty mediaResourceLocatorProperty() {return mediaResourceLocator;}; 59 | 60 | private final BooleanProperty autoPlay = new SimpleBooleanProperty(true); 61 | public BooleanProperty autoPlayProperty() {return autoPlay;}; 62 | 63 | private final ReadOnlyObjectWrapper videoImage = new ReadOnlyObjectWrapper<>(); 64 | public ReadOnlyObjectProperty videoImageProperty() {return videoImage.getReadOnlyProperty();}; 65 | 66 | private final ReadOnlyBooleanWrapper error = new ReadOnlyBooleanWrapper(false); 67 | public ReadOnlyBooleanProperty errorProperty() {return error.getReadOnlyProperty();}; 68 | 69 | /** 70 | * The constructor to create the video player. 71 | */ 72 | public VLCJFXVideoPlayer() { 73 | mediaPlayerFactory = new MediaPlayerFactory(); 74 | embeddedMediaPlayer = mediaPlayerFactory.mediaPlayers().newEmbeddedMediaPlayer(); 75 | embeddedMediaPlayer.videoSurface().set(new FXCallbackVideoSurface()); 76 | 77 | mediaResourceLocator.addListener((v,o,n) -> { 78 | if (autoPlay.get()) { 79 | error.set(embeddedMediaPlayer.media().play(n)); 80 | } else { 81 | error.set(embeddedMediaPlayer.media().prepare(n)); 82 | } 83 | }); 84 | } 85 | 86 | /** 87 | * Get access to the media player controls. 88 | * 89 | * @return the media player controls. 90 | */ 91 | public ControlsApi controls() { 92 | return embeddedMediaPlayer.controls(); 93 | } 94 | 95 | /** 96 | * Dispose the media player resources. 97 | */ 98 | public void dispose() { 99 | embeddedMediaPlayer.controls().stop(); 100 | embeddedMediaPlayer.release(); 101 | mediaPlayerFactory.release(); 102 | } 103 | 104 | private class FXCallbackVideoSurface extends CallbackVideoSurface { 105 | FXCallbackVideoSurface() { 106 | super(new FXBufferFormatCallback(), new FXRenderCallback(), true, VideoSurfaceAdapters.getVideoSurfaceAdapter()); 107 | } 108 | } 109 | 110 | private class FXBufferFormatCallback implements BufferFormatCallback { 111 | private int sourceWidth; 112 | private int sourceHeight; 113 | 114 | @Override 115 | public BufferFormat getBufferFormat(int sourceWidth, int sourceHeight) { 116 | this.sourceWidth = sourceWidth; 117 | this.sourceHeight = sourceHeight; 118 | return new RV32BufferFormat(sourceWidth, sourceHeight); 119 | } 120 | 121 | @Override 122 | public void allocatedBuffers(ByteBuffer[] buffers) { 123 | assert buffers.length == 1; 124 | assert buffers[0].capacity() == sourceWidth * sourceHeight * 4; 125 | PixelFormat pixelFormat = PixelFormat.getByteBgraPreInstance(); 126 | videoPixelBuffer = new PixelBuffer<>(sourceWidth, sourceHeight, buffers[0], pixelFormat); 127 | videoImage.set(new WritableImage(videoPixelBuffer)); 128 | } 129 | } 130 | 131 | private class FXRenderCallback implements RenderCallback { 132 | @Override 133 | public void display(MediaPlayer mediaPlayer, ByteBuffer[] nativeBuffers, BufferFormat bufferFormat) { 134 | Platform.runLater(() -> { 135 | videoPixelBuffer.updateBuffer(pb -> { 136 | return null; 137 | }); 138 | }); 139 | } 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /jfxtools-jarprocessor/doc/example_output_modulechecker.txt: -------------------------------------------------------------------------------- 1 | % java -jar ~/bin/mpJARProcessor.jar ModuleChecker `ls *.jar` 2 | Modular results: 3 | 4 | bcmail-jdk15on-1.64.jar : modular 5 | bcpkix-jdk15on-1.64.jar : modular 6 | bcprov-jdk15on-1.64.jar : modular 7 | >>> Inconsistent service declarations detected. <<< 8 | Services provided on module-path: 9 | Services provided on class-path: 10 | provides java.security.Provider with org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider 11 | provides java.security.Provider with org.bouncycastle.jce.provider.BouncyCastleProvider 12 | controlsfx-11.0.2.jar : modular 13 | provides org.controlsfx.glyphfont.GlyphFont with org.controlsfx.glyphfont.FontAwesome 14 | ikonli-core-12.0.0.jar : modular 15 | provides org.kordamp.ikonli.IkonProvider with org.kordamp.ikonli.IkonliIkonProvider 16 | provides org.kordamp.ikonli.IkonHandler with org.kordamp.ikonli.IkonliIkonResolver 17 | ikonli-fontawesome-pack-12.0.0.jar : modular 18 | provides org.kordamp.ikonli.IkonProvider with org.kordamp.ikonli.fontawesome.FontAwesomeIkonProvider 19 | provides org.kordamp.ikonli.IkonHandler with org.kordamp.ikonli.fontawesome.FontAwesomeIkonHandler 20 | ikonli-javafx-12.0.0.jar : modular 21 | jackson-annotations-2.10.2.jar : modular 22 | jackson-core-2.10.2.jar : modular 23 | >>> Inconsistent service declarations detected. <<< 24 | Services provided on module-path: 25 | Services provided on class-path: 26 | provides com.fasterxml.jackson.core.JsonFactory with com.fasterxml.jackson.core.JsonFactory 27 | jackson-databind-2.10.2.jar : modular 28 | provides com.fasterxml.jackson.core.ObjectCodec with com.fasterxml.jackson.databind.ObjectMapper 29 | jackson-datatype-jsr310-2.10.2.jar : modular 30 | >>> Inconsistent service declarations detected. <<< 31 | Services provided on module-path: 32 | provides com.fasterxml.jackson.databind.Module with com.fasterxml.jackson.datatype.jsr310.JSR310Module 33 | Services provided on class-path: 34 | provides com.fasterxml.jackson.databind.Module with com.fasterxml.jackson.datatype.jsr310.JavaTimeModule 35 | jakarta.activation-1.2.2.jar : modular 36 | jakarta.xml.bind-api-2.3.3.jar : modular 37 | javafx-base-16-ea+7-mac.jar : modular 38 | javafx-controls-16-ea+7-mac.jar : modular 39 | javafx-fxml-16-ea+7-mac.jar : modular 40 | javafx-graphics-16-ea+7-mac.jar : modular 41 | javafx-swing-16-ea+7-mac.jar : modular 42 | jaxb-impl-2.3.3.jar : modular 43 | >>> Inconsistent service declarations detected. <<< 44 | Services provided on module-path: 45 | provides javax.xml.bind.JAXBContextFactory with com.sun.xml.bind.v2.JAXBContextFactory 46 | Services provided on class-path: 47 | provides javax.xml.bind.JAXBContextFactory with com.sun.xml.bind.v2.JAXBContextFactory 48 | provides javax.xml.bind.JAXBContext with com.sun.xml.bind.v2.ContextFactory 49 | jfa-1.1.8.jar : modular 50 | jmetro-11.6.12.jar : modular 51 | nsmenufx-3.0.2.jar : modular 52 | 53 | Non-modular results: 54 | 55 | AnimateFX-1.2.1.jar : non-modular hasDefinedAutomaticModuleName = false 56 | GeographicLib-Java-1.49.jar : non-modular hasDefinedAutomaticModuleName = false 57 | annotations-16.0.3.jar : non-modular hasDefinedAutomaticModuleName = true 58 | commons-compress-1.20.jar : non-modular hasDefinedAutomaticModuleName = true 59 | commons-csv-1.6.jar : non-modular hasDefinedAutomaticModuleName = false 60 | commons-io-2.6.jar : non-modular hasDefinedAutomaticModuleName = true 61 | commons-lang3-3.9.jar : non-modular hasDefinedAutomaticModuleName = true 62 | commons-logging-1.2.jar : non-modular hasDefinedAutomaticModuleName = false 63 | commons-math3-3.6.1.jar : non-modular hasDefinedAutomaticModuleName = false 64 | commons-suncalc-2.4.jar : non-modular hasDefinedAutomaticModuleName = false 65 | earth-gravitational-model-1.0.0-SNAPSHOT.jar : non-modular hasDefinedAutomaticModuleName = false 66 | esri-geometry-api-2.2.3.jar : non-modular hasDefinedAutomaticModuleName = false 67 | flowless-0.6.1.jar : non-modular hasDefinedAutomaticModuleName = false 68 | fontbox-2.0.21.jar : non-modular hasDefinedAutomaticModuleName = true 69 | geojson-proto-1.1.0.jar : non-modular hasDefinedAutomaticModuleName = false 70 | jackson-datatype-jdk8-2.9.4.jar : non-modular hasDefinedAutomaticModuleName = true 71 | provides com.fasterxml.jackson.databind.Module with com.fasterxml.jackson.datatype.jdk8.Jdk8Module 72 | javafx-base-16-ea+7.jar : non-modular hasDefinedAutomaticModuleName = true 73 | javafx-controls-16-ea+7.jar : non-modular hasDefinedAutomaticModuleName = true 74 | javafx-fxml-16-ea+7.jar : non-modular hasDefinedAutomaticModuleName = true 75 | javafx-graphics-16-ea+7.jar : non-modular hasDefinedAutomaticModuleName = true 76 | javafx-swing-16-ea+7.jar : non-modular hasDefinedAutomaticModuleName = true 77 | javax.annotation-api-1.3.2.jar : non-modular hasDefinedAutomaticModuleName = true 78 | jfxtools-awtimage-1.0.0.jar : non-modular hasDefinedAutomaticModuleName = false 79 | jna-5.6.0.jar : non-modular hasDefinedAutomaticModuleName = true 80 | jsoup-1.11.3.jar : non-modular hasDefinedAutomaticModuleName = true 81 | jts-core-1.18.0.jar : non-modular hasDefinedAutomaticModuleName = true 82 | mbtiles4j-1.0.6-mod.jar : non-modular hasDefinedAutomaticModuleName = false 83 | metadata-extractor-2.13.0.jar : non-modular hasDefinedAutomaticModuleName = false 84 | miglayout-core-5.2.jar : non-modular hasDefinedAutomaticModuleName = false 85 | miglayout-javafx-5.2.jar : non-modular hasDefinedAutomaticModuleName = false 86 | named-regexp-0.2.5.jar : non-modular hasDefinedAutomaticModuleName = false 87 | ogn-client-java-1.1.0-SNAPSHOT.jar : non-modular hasDefinedAutomaticModuleName = false 88 | ogn-commons-java-1.0.0-SNAPSHOT.jar : non-modular hasDefinedAutomaticModuleName = false 89 | pcollections-3.0.3.jar : non-modular hasDefinedAutomaticModuleName = false 90 | pdfbox-2.0.21.jar : non-modular hasDefinedAutomaticModuleName = true 91 | protobuf-java-3.12.2.jar : non-modular hasDefinedAutomaticModuleName = true 92 | reactfx-2.0-M5.jar : non-modular hasDefinedAutomaticModuleName = false 93 | richtextfx-0.10.5.jar : non-modular hasDefinedAutomaticModuleName = true 94 | slf4j-api-1.7.30.jar : non-modular hasDefinedAutomaticModuleName = true 95 | slf4j-jdk14-1.7.30.jar : non-modular hasDefinedAutomaticModuleName = true 96 | sqlite-jdbc-3.32.3.2.jar : non-modular hasDefinedAutomaticModuleName = false 97 | provides java.sql.Driver with org.sqlite.JDBC 98 | time4j-base-5.2.jar : non-modular hasDefinedAutomaticModuleName = true 99 | timeshape-2020a.10.jar : non-modular hasDefinedAutomaticModuleName = true 100 | trove4j-3.0.3.jar : non-modular hasDefinedAutomaticModuleName = false 101 | undofx-2.1.0.jar : non-modular hasDefinedAutomaticModuleName = false 102 | wellbehavedfx-0.3.3.jar : non-modular hasDefinedAutomaticModuleName = false 103 | xmpcore-6.0.6.jar : non-modular hasDefinedAutomaticModuleName = false 104 | zstd-jni-1.4.4-9.jar : non-modular hasDefinedAutomaticModuleName = true 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JFXToolsAndDemos 2 | 3 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 4 | [![Maven Package](https://github.com/mipastgt/JFXToolsAndDemos/actions/workflows/maven-publish.yml/badge.svg)](https://github.com/mipastgt/JFXToolsAndDemos/actions/workflows/maven-publish.yml) 5 | 6 | 7 | A collection of tools and demos for JavaFX. (Now with SNAPSHOT packages) 8 | 9 | ## Build 10 | 11 | Attention: The current setup requires that you set JAVA_HOME to some Java 21 JDK or later! 12 | 13 | From the top-level project directory call: 14 | 15 | ``` 16 | mvn clean install 17 | ``` 18 | 19 | ## Modules 20 | 21 | ### JARProcessor 22 | 23 | A simple tool to analyze and process jar files. Currently two processors are implemented. 24 | 25 | #### Module checker 26 | 27 | This jar-processor analyzes a list of jar-files and lists all services provided 28 | by the contained code. 29 | 30 | It further checks whether the contained code is modular or non-modular. If it is 31 | modular, it checks if the service delarations for the module-path are consistent 32 | with the declarations for the classpath. 33 | 34 | Example: 35 | 36 | ``` 37 | mvn exec:java -pl jfxtools-jarprocessor -Dexec.args="ModuleChecker -v module1.jar module2.jar ..." 38 | ``` 39 | 40 | #### FXML checker 41 | 42 | This jar-processor analyzes a list of jar-files and lists all FXML files 43 | contained in each file. 44 | 45 | It further examines all import statements of the FXML files and finally 46 | prints out a combined list of all classes loaded by all the jar files. 47 | This is useful for tools like the maven shade plugin, ProGuard of GluonHQs 48 | client-maven-plugin for which you have to provide a list of all classes 49 | which have to be kept but which cannot be found by static analysis of the 50 | code. 51 | 52 | Example: 53 | 54 | ``` 55 | mvn exec:java -pl jfxtools-jarprocessor -Dexec.args="FXMLChecker -v -n module1.jar module2.jar ..." 56 | ``` 57 | 58 | #### Native artifact checker 59 | 60 | This jar-processor analyzes a list of jar-files and lists all native artifacts 61 | contained in each file. All files which end with ".o", ".a", ".so", ".dll", ".dylib" 62 | or ".jnilib" are listed. 63 | 64 | This is useful to know if you want to strip unneeded files from a build or if you want 65 | to check if all native artifacts in your build have been properly signed. This is, e.g, 66 | necessary to get a bundled app notarized by Apple. 67 | 68 | Example: 69 | 70 | ``` 71 | mvn exec:java -pl jfxtools-jarprocessor -Dexec.args="NativeArtifactChecker -v module1.jar module2.jar ..." 72 | ``` 73 | 74 | ### VLCJFXVideoPlayer 75 | 76 | This is a very simple example of a video player which uses the new WritableImage 77 | of JavaFX 13 with support for Buffers to improve performance. The idea is to let the 78 | well known [VLC media player](http://www.videolan.org/vlc/), which can play almost 79 | everything you throw at it, directly render into 80 | this buffer and use the image directly in an ImageView without any explicit rendering 81 | into a canvas or such thing. Only this brings the desired performance boost. 82 | 83 | What I have not considered yet is any kind of synchronization. 84 | I think an extension of the PixelBuffer to support some kind of double-buffering 85 | would be the right thing to do. 86 | 87 | In order to run the code, a recent version of the VLC player (3.0.x+) must be installed 88 | in a standard location on the system. 89 | 90 | This should work on macOS and Linux but there currently seem to be problems with the VLC code 91 | itself on Windows. Tested on macOS 10.14.6 and Linux. 92 | 93 | This software is based on the [VLCJ project](https://github.com/caprica/vlcj). 94 | Other dependencies can be found in the pom.xml. 95 | 96 | #### Run 97 | 98 | From the top-level project directory call: 99 | 100 | ``` 101 | mvn exec:java@vlcjfx-demo1 -pl jfxtools-vlcjfx-demos 102 | ``` 103 | 104 | or 105 | 106 | ``` 107 | mvn exec:java@vlcjfx-demo2 -pl jfxtools-vlcjfx-demos 108 | ``` 109 | 110 | ### AWTImage 111 | 112 | A simple wrapper arround an AWT image which utilizes the new WritableImage 113 | of JavaFX 13 with support for Buffers. Internally a JavaFX image is created 114 | which directly uses the same memory as the AWT image. So if you render 115 | into the AWT image with a AWT graphics context, the result will immediately 116 | appear on the screen. 117 | 118 | #### Run 119 | 120 | There are three examples for AWTImage. The first one just does some drawing and demonstrates the update behaviour 121 | and the second and third one utilizes AWTImage to build a little JavaFX PDF viewer based on Apache PDFBox. 122 | 123 | From the top-level project directory call: 124 | 125 | ``` 126 | mvn exec:java@AWTImageDemo -pl jfxtools-awtimage-demos 127 | ``` 128 | 129 | or 130 | 131 | ``` 132 | mvn exec:java@PDFViewerDemo -pl jfxtools-awtimage-demos 133 | ``` 134 | 135 | or 136 | 137 | ``` 138 | mvn exec:java@PDFViewerDemo2 -pl jfxtools-awtimage-demos 139 | ``` 140 | 141 | ### NativeRenderingCanvas 142 | 143 | An example to show how some native renderer can be integrated seemlessly into JavaFX. 144 | It uses the new WritableImage of JavaFX 13 with support for Buffers to improve performance. 145 | 146 | For more details see: [NativeRenderingCanvas](jfxtools-canvas/docs/NativeRenderingCanvas.adoc) 147 | 148 | **Attention 1: The event-handling in this example currently only works with JDK 11 and JFX 13+.** 149 | See [JDK-8236971](https://bugs.openjdk.java.net/browse/JDK-8236971). 150 | 151 | **Attention 2: Building the native part on anything else but macOS is completely untested.** 152 | **Building the native part on Windows requires MinGW.** 153 | 154 | #### Run 155 | 156 | From the top-level project directory call: 157 | 158 | ``` 159 | mvn -pl jfxtools-canvas-demos exec:exec 160 | ``` 161 | 162 | ### SkiaSurfaceFX 163 | 164 | This is a simple example of a wrapper for the Skia rendering engine. It uses the new WritableImage 165 | of JavaFX 13 with support for Buffers to improve performance. The idea is to let [Skia](https://skia.org/) 166 | directly render into this buffer and use the image directly in an ImageView without any explicit rendering 167 | into a canvas or such thing. Only this brings the desired performance boost. This code makes use 168 | of the [Skija](https://github.com/HumbleUI/Skija) java bindings originally provided by JetBrains. 169 | 170 | ![Demo1](jfxtools-skiafx-demos/demo1.png "SkiaSurfaceFXDemo1") 171 | 172 | What might this be good for? Primarily it is just a proof of concept but ... 173 | * Skia serves as the graphics engine for Google Chrome and Chrome OS, Android, Flutter, Mozilla Firefox 174 | and Firefox OS, Jetbrains Compose and many other products. So the Skia integration might help to 175 | make code written for Skia on these platforms easily available to JavaFX. 176 | * The Skia Canvas has a few features which you don't find in the JavaFX Canvas. 177 | * Its a real immediate mode renderer which is an advantage in some situations. 178 | * You can render on multiple threads which is an advantage, e.g., for offscreen rendering. 179 | * In JavaFX you have to take a snapshot of a Canvas if you need an image of your rendering. 180 | Here it is readily available. 181 | * Once you have code written for the Skia Canvas you can also use other render targets than Raster, e.g., 182 | OpenGL, PDF, XPS, SVG and Picture (for recording and then playing back into another Canvas) 183 | 184 | 185 | #### Run 186 | 187 | From the top-level project directory call: 188 | 189 | ``` 190 | mvn -pl jfxtools-skiafx-demos exec:exec 191 | ``` 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /jfxtools-awtimage-demos/src/main/java/de/mpmediasoft/jfxtools/awtimage/demo/PDFViewerDemo.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.awtimage.demo; 2 | 3 | import java.awt.Graphics2D; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.util.Locale; 7 | 8 | import javax.imageio.ImageIO; 9 | 10 | import org.apache.pdfbox.Loader; 11 | import org.apache.pdfbox.pdmodel.PDDocument; 12 | import org.apache.pdfbox.pdmodel.PDPage; 13 | import org.apache.pdfbox.pdmodel.common.PDRectangle; 14 | import org.apache.pdfbox.rendering.PDFRenderer; 15 | 16 | import de.mpmediasoft.jfxtools.awtimage.AWTImage; 17 | import javafx.application.Application; 18 | import javafx.application.Platform; 19 | import javafx.scene.Scene; 20 | import javafx.scene.control.Button; 21 | import javafx.scene.control.ColorPicker; 22 | import javafx.scene.control.ToolBar; 23 | import javafx.scene.control.Tooltip; 24 | import javafx.scene.image.ImageView; 25 | import javafx.scene.layout.BorderPane; 26 | import javafx.scene.paint.Color; 27 | import javafx.stage.FileChooser; 28 | import javafx.stage.Stage; 29 | 30 | /** 31 | * A simple demo to show how the AWTImage class can be used to build 32 | * a simple PDF viewer based on Apache PDFBox. 33 | * 34 | * @author Michael Paus 35 | */ 36 | public class PDFViewerDemo extends Application { 37 | 38 | private final static int IMAGE_WIDTH = 1280; 39 | private final static int IMAGE_HEIGHT = 720; 40 | 41 | private ImageView imageView; 42 | private AWTImage awtImage; 43 | private PDDocument document; 44 | private PDFRenderer pdfRenderer; 45 | private int pageIndex; 46 | private FileChooser fileChooser; 47 | private File pdfFile = null; 48 | private Color backgroundColor = Color.WHITE; 49 | 50 | @Override 51 | public void init() { 52 | System.out.println("java.runtime.version: " + System.getProperty("java.runtime.version", "(undefined)")); 53 | System.out.println("javafx.runtime.version: " + System.getProperty("javafx.runtime.version", "(undefined)")); 54 | } 55 | 56 | @Override 57 | public void start(Stage primaryStage) throws Exception { 58 | fileChooser = new FileChooser(); 59 | fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("PDF files (*.pdf)", "*.pdf")); 60 | Button selectPDFButton = new Button("Select PDF"); 61 | selectPDFButton.setTooltip(new Tooltip("Select a PDF file.")); 62 | selectPDFButton.setOnAction(e -> { 63 | pdfFile = fileChooser.showOpenDialog(primaryStage); 64 | if (pdfFile != null) { 65 | open(pdfFile); 66 | } 67 | }); 68 | 69 | Button pageBackwardButton = new Button("<"); 70 | pageBackwardButton.setTooltip(new Tooltip("Page backward.")); 71 | pageBackwardButton.setOnAction(e -> { 72 | pageIndex = Math.max(pageIndex - 1, 0); 73 | awtImage.update(); 74 | }); 75 | 76 | Button pageForwardButton = new Button(">"); 77 | pageForwardButton.setTooltip(new Tooltip("Page forward.")); 78 | pageForwardButton.setOnAction(e -> { 79 | pageIndex = Math.min(pageIndex + 1, document.getNumberOfPages() - 1); 80 | awtImage.update(); 81 | }); 82 | 83 | Button saveAsPNGButton = new Button("Save"); 84 | saveAsPNGButton.setTooltip(new Tooltip("Save rendered page as PNG image.")); 85 | saveAsPNGButton.setOnAction(e -> { 86 | saveAsPNG(pdfFile); 87 | }); 88 | 89 | ColorPicker colorPicker = new ColorPicker(); 90 | colorPicker.setTooltip(new Tooltip("Select background color.")); 91 | colorPicker.setValue(backgroundColor); 92 | colorPicker.setOnAction(a -> { 93 | backgroundColor = colorPicker.getValue(); 94 | render(); 95 | }); 96 | 97 | ToolBar toolbar = new ToolBar(selectPDFButton, pageBackwardButton, pageForwardButton, saveAsPNGButton, colorPicker); 98 | 99 | BorderPane root = new BorderPane(); 100 | root.setTop(toolbar); 101 | 102 | imageView = new ImageView(); 103 | imageView.setFitWidth(IMAGE_WIDTH); 104 | imageView.setFitHeight(IMAGE_HEIGHT); 105 | imageView.setPreserveRatio(true); 106 | root.setCenter(imageView); 107 | Scene scene = new Scene(root); 108 | primaryStage.setScene(scene); 109 | primaryStage.show(); 110 | 111 | Platform.runLater(() -> { 112 | // Load initial PDF if provided. 113 | Parameters params = getParameters(); 114 | if (params.getRaw().size() > 0) { 115 | String pdfFileName = params.getRaw().get(0); 116 | pdfFile = new File(pdfFileName); 117 | open(pdfFile); 118 | } 119 | }); 120 | } 121 | 122 | private void open(File pdfFile) { 123 | try { 124 | if (pdfFile != null && pdfFile.canRead()) { 125 | if (document != null) document.close(); 126 | document = Loader.loadPDF(pdfFile); // Version 3.0.x 127 | // document = PDDocument.load(pdfFile); // Version 2.0.x 128 | pdfRenderer = new PDFRenderer(document); 129 | pdfRenderer.setSubsamplingAllowed(true); 130 | 131 | render(); 132 | } else { 133 | System.err.println("No valid PDF document selected."); 134 | System.err.println("pdfFile: " + pdfFile); 135 | Platform.exit(); 136 | } 137 | } catch (IOException e) { 138 | e.printStackTrace(); 139 | } 140 | } 141 | 142 | private void render() { 143 | try { 144 | if (pdfRenderer != null) { 145 | pageIndex = 0; 146 | 147 | if (awtImage == null) { 148 | double renderScale = imageView.getScene().getWindow().getRenderScaleX(); 149 | awtImage = new AWTImage((int)(IMAGE_WIDTH * renderScale), (int)(IMAGE_HEIGHT * renderScale)); 150 | awtImage.setOnUpdate(g2d -> { 151 | try { 152 | PDPage page = document.getPage(pageIndex); 153 | 154 | PDRectangle cropBox = page.getCropBox(); 155 | 156 | float widthPt = cropBox.getWidth(); 157 | float widthPix = awtImage.getWidth(); 158 | float scaleX = widthPix / widthPt; 159 | 160 | float heightPt = cropBox.getHeight(); 161 | float heightPix = awtImage.getHeight(); 162 | float scaleY = heightPix / heightPt; 163 | 164 | float scale = Math.min(scaleX, scaleY); 165 | 166 | Graphics2D g2 = (Graphics2D) awtImage.getAWTImage().getGraphics(); 167 | // Set background for transparent pages 168 | java.awt.Color awtBackgroundColor = new java.awt.Color( 169 | (float)backgroundColor.getRed(), 170 | (float)backgroundColor.getGreen(), 171 | (float)backgroundColor.getBlue(), 172 | (float)backgroundColor.getOpacity()); 173 | g2.setBackground(awtBackgroundColor); 174 | g2.clearRect(0, 0, awtImage.getWidth(), awtImage.getHeight()); 175 | 176 | // Render the selected page 177 | pdfRenderer.renderPageToGraphics(pageIndex, g2, scale); 178 | g2.dispose(); 179 | } catch (IOException e) { 180 | e.printStackTrace(); 181 | } 182 | 183 | return null; 184 | }); 185 | imageView.setImage(awtImage.getFXImage()); 186 | } 187 | 188 | awtImage.update(); 189 | } 190 | } catch (Exception e) { 191 | e.printStackTrace(); 192 | } 193 | } 194 | 195 | private void saveAsPNG(File pdfFile) { 196 | try { 197 | if (pdfFile != null && pdfFile.canRead()) { 198 | File parent = pdfFile.getParentFile(); 199 | String pdfFileName = pdfFile.getName(); 200 | String baseFileName = pdfFileName.substring(0, pdfFileName.lastIndexOf('.')); 201 | File pngFile = new File(parent, baseFileName + ".png"); 202 | ImageIO.write(awtImage.getAWTImage(), "png", pngFile); 203 | System.out.println("PNG file written to: " + pngFile); 204 | } else { 205 | System.err.println("No valid PDF document selected."); 206 | } 207 | } catch (Exception e) { 208 | e.printStackTrace(); 209 | } 210 | } 211 | 212 | 213 | public static void main(String[] args) { 214 | Locale.setDefault(Locale.US); 215 | launch(args); 216 | } 217 | 218 | } 219 | 220 | class PDFViewerDemoLauncher {public static void main(String[] args) {PDFViewerDemo.main(args);}} 221 | -------------------------------------------------------------------------------- /jfxtools-awtimage-demos/src/main/java/de/mpmediasoft/jfxtools/awtimage/demo/PDFViewerDemo2.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.awtimage.demo; 2 | 3 | import java.awt.Graphics2D; 4 | import java.awt.Shape; 5 | import java.awt.geom.AffineTransform; 6 | import java.awt.geom.Rectangle2D; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.util.Locale; 10 | 11 | import javax.imageio.ImageIO; 12 | 13 | import org.apache.pdfbox.Loader; 14 | import org.apache.pdfbox.pdmodel.PDDocument; 15 | import org.apache.pdfbox.pdmodel.PDPage; 16 | import org.apache.pdfbox.pdmodel.common.PDRectangle; 17 | import org.apache.pdfbox.rendering.PDFRenderer; 18 | 19 | import de.mpmediasoft.jfxtools.awtimage.AWTImage; 20 | import javafx.application.Application; 21 | import javafx.application.Platform; 22 | import javafx.scene.Scene; 23 | import javafx.scene.control.Button; 24 | import javafx.scene.control.ColorPicker; 25 | import javafx.scene.control.TextField; 26 | import javafx.scene.control.ToolBar; 27 | import javafx.scene.control.Tooltip; 28 | import javafx.scene.image.ImageView; 29 | import javafx.scene.layout.BorderPane; 30 | import javafx.scene.paint.Color; 31 | import javafx.stage.FileChooser; 32 | import javafx.stage.Stage; 33 | 34 | /** 35 | * A simple demo to show how the AWTImage class can be used to build 36 | * a simple PDF viewer based on Apache PDFBox. 37 | * 38 | * @author Michael Paus 39 | */ 40 | public class PDFViewerDemo2 extends Application { 41 | 42 | private final static int WIDTH = 1000; 43 | private final static int HEIGHT = 800; 44 | 45 | private final static java.awt.Color CLEAR_COLOR = new java.awt.Color(0, 0, 0, 0); 46 | 47 | private ImageView imageView; 48 | private AWTImage awtImage; 49 | private PDDocument document; 50 | private PDFRenderer pdfRenderer; 51 | private FileChooser fileChooser; 52 | private File pdfFile = null; 53 | private int pageIndex = 0; 54 | private Color backgroundColor = Color.WHITE; 55 | private double rotAngleDeg = 0.0; 56 | private double pageScale = 1.0; 57 | 58 | @Override 59 | public void init() { 60 | System.out.println("java.runtime.version: " + System.getProperty("java.runtime.version", "(undefined)")); 61 | System.out.println("javafx.runtime.version: " + System.getProperty("javafx.runtime.version", "(undefined)")); 62 | } 63 | 64 | @Override 65 | public void start(Stage primaryStage) throws Exception { 66 | fileChooser = new FileChooser(); 67 | fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("PDF files (*.pdf)", "*.pdf")); 68 | Button selectPDFButton = new Button("Select PDF"); 69 | selectPDFButton.setTooltip(new Tooltip("Select a PDF file.")); 70 | selectPDFButton.setOnAction(e -> { 71 | pdfFile = fileChooser.showOpenDialog(primaryStage); 72 | if (pdfFile != null) { 73 | open(pdfFile); 74 | } 75 | }); 76 | 77 | Button pageBackwardButton = new Button("<"); 78 | pageBackwardButton.setTooltip(new Tooltip("Page backward.")); 79 | pageBackwardButton.setOnAction(e -> { 80 | pageIndex = Math.max(pageIndex - 1, 0); 81 | awtImage.update(); 82 | }); 83 | 84 | Button pageForwardButton = new Button(">"); 85 | pageForwardButton.setTooltip(new Tooltip("Page forward.")); 86 | pageForwardButton.setOnAction(e -> { 87 | pageIndex = Math.min(pageIndex + 1, document.getNumberOfPages() - 1); 88 | awtImage.update(); 89 | }); 90 | 91 | Button saveAsPNGButton = new Button("Save"); 92 | saveAsPNGButton.setTooltip(new Tooltip("Save rendered page as PNG image.")); 93 | saveAsPNGButton.setOnAction(e -> { 94 | saveAsPNG(pdfFile); 95 | }); 96 | 97 | ColorPicker colorPicker = new ColorPicker(); 98 | colorPicker.setTooltip(new Tooltip("Select background color.")); 99 | colorPicker.setValue(backgroundColor); 100 | colorPicker.setOnAction(a -> { 101 | backgroundColor = colorPicker.getValue(); 102 | render(pageScale, pageIndex, rotAngleDeg, backgroundColor); 103 | }); 104 | 105 | TextField rotDegInput = new TextField(); 106 | rotDegInput.setTooltip(new Tooltip("Select rotation angle in degrees [-180, 180].")); 107 | rotDegInput.setOnAction(a -> { 108 | try { 109 | rotAngleDeg = Math.min(Math.max(Double.parseDouble(rotDegInput.getText()), -180), +180); 110 | } catch (NumberFormatException e1) { 111 | rotAngleDeg = 0.0; 112 | } 113 | awtImage = null; 114 | render(pageScale, pageIndex, rotAngleDeg, backgroundColor); 115 | }); 116 | 117 | ToolBar toolbar = new ToolBar(selectPDFButton, pageBackwardButton, pageForwardButton, saveAsPNGButton, colorPicker, rotDegInput); 118 | 119 | BorderPane root = new BorderPane(); 120 | root.setTop(toolbar); 121 | 122 | imageView = new ImageView(); 123 | root.setCenter(imageView); 124 | Scene scene = new Scene(root, WIDTH, HEIGHT); 125 | primaryStage.setScene(scene); 126 | primaryStage.show(); 127 | 128 | Platform.runLater(() -> { 129 | // Load initial PDF if provided. 130 | Parameters params = getParameters(); 131 | if (params.getRaw().size() > 0) { 132 | String pdfFileName = params.getRaw().get(0); 133 | pdfFile = new File(pdfFileName); 134 | open(pdfFile); 135 | } 136 | }); 137 | } 138 | 139 | private void open(File pdfFile) { 140 | try { 141 | if (pdfFile != null && pdfFile.canRead()) { 142 | if (document != null) document.close(); 143 | document = Loader.loadPDF(pdfFile); // Version 3.0.x 144 | // document = PDDocument.load(pdfFile); // Version 2.0.x 145 | pdfRenderer = new PDFRenderer(document); 146 | pdfRenderer.setSubsamplingAllowed(true); 147 | 148 | render(pageScale, pageIndex, rotAngleDeg, backgroundColor); 149 | } else { 150 | System.err.println("No valid PDF document selected."); 151 | System.err.println("pdfFile: " + pdfFile); 152 | Platform.exit(); 153 | } 154 | } catch (IOException e) { 155 | e.printStackTrace(); 156 | } 157 | } 158 | 159 | private void render(double pageScale, int pageIndex, double rotAngleDeg, Color background) { 160 | try { 161 | if (pdfRenderer != null) { 162 | if (awtImage == null) { 163 | PDPage page = document.getPage(pageIndex); 164 | 165 | PDRectangle bBox = page.getBBox(); 166 | 167 | Rectangle2D pageRect = new Rectangle2D.Double(0.0, 0.0, bBox.getWidth() * pageScale, bBox.getHeight() * pageScale); 168 | 169 | final AffineTransform combTrafo; 170 | 171 | if (rotAngleDeg != 0.0) { 172 | AffineTransform rot = AffineTransform.getRotateInstance(Math.toRadians(rotAngleDeg)); 173 | Shape rotRect = rot.createTransformedShape(pageRect); 174 | Rectangle2D rotRectBnds = rotRect.getBounds2D(); 175 | 176 | AffineTransform trans = AffineTransform.getTranslateInstance(-rotRectBnds.getMinX(), -rotRectBnds.getMinY()); 177 | 178 | combTrafo = new AffineTransform(); 179 | combTrafo.concatenate(trans); 180 | combTrafo.concatenate(rot); 181 | 182 | Shape combTrafoPageRect = combTrafo.createTransformedShape(pageRect); 183 | System.out.println("combTrafoPageRect: " + combTrafoPageRect); 184 | 185 | awtImage = new AWTImage((int)rotRectBnds.getWidth(), (int)rotRectBnds.getHeight()); 186 | } else { 187 | combTrafo = null; 188 | awtImage = new AWTImage((int)pageRect.getWidth(), (int)pageRect.getHeight()); 189 | } 190 | 191 | awtImage.setOnUpdate(g2d -> { 192 | try { 193 | Graphics2D g2 = (Graphics2D) awtImage.getAWTImage().getGraphics(); 194 | g2.setBackground(CLEAR_COLOR); 195 | g2.clearRect(0, 0, awtImage.getWidth(), awtImage.getHeight()); 196 | 197 | java.awt.Color awtBackgroundColor = new java.awt.Color( 198 | (float)background.getRed(), 199 | (float)background.getGreen(), 200 | (float)background.getBlue(), 201 | (float)background.getOpacity()); 202 | 203 | g2.setBackground(awtBackgroundColor); 204 | 205 | if (combTrafo != null) { 206 | g2.setTransform(combTrafo); 207 | } 208 | 209 | // Render the selected page 210 | pdfRenderer.renderPageToGraphics(pageIndex, g2, (float)pageScale); 211 | 212 | g2.dispose(); 213 | } catch (IOException e) { 214 | e.printStackTrace(); 215 | } 216 | 217 | return null; 218 | }); 219 | imageView.setImage(awtImage.getFXImage()); 220 | } 221 | 222 | awtImage.update(); 223 | } 224 | } catch (Exception e) { 225 | e.printStackTrace(); 226 | } 227 | } 228 | 229 | private void saveAsPNG(File pdfFile) { 230 | try { 231 | if (pdfFile != null && pdfFile.canRead()) { 232 | File parent = pdfFile.getParentFile(); 233 | String pdfFileName = pdfFile.getName(); 234 | String baseFileName = pdfFileName.substring(0, pdfFileName.lastIndexOf('.')); 235 | File pngFile = new File(parent, baseFileName + ".png"); 236 | ImageIO.write(awtImage.getAWTImage(), "png", pngFile); 237 | System.out.println("PNG file written to: " + pngFile); 238 | } else { 239 | System.err.println("No valid PDF document selected."); 240 | } 241 | } catch (Exception e) { 242 | e.printStackTrace(); 243 | } 244 | } 245 | 246 | 247 | public static void main(String[] args) { 248 | Locale.setDefault(Locale.US); 249 | launch(args); 250 | } 251 | 252 | } 253 | 254 | class PDFViewerDemo2Launcher {public static void main(String[] args) {PDFViewerDemo2.main(args);}} 255 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /jfxtools-canvas/src/main/java/de/mpmediasoft/jfxtools/canvas/NativeRenderingCanvas.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.canvas; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.IntBuffer; 5 | import java.util.concurrent.ExecutorService; 6 | import java.util.concurrent.Executors; 7 | 8 | import javafx.application.Platform; 9 | import javafx.beans.property.ObjectProperty; 10 | import javafx.beans.property.SimpleObjectProperty; 11 | import javafx.beans.value.ChangeListener; 12 | import javafx.concurrent.Service; 13 | import javafx.concurrent.Task; 14 | import javafx.concurrent.WorkerStateEvent; 15 | import javafx.event.EventHandler; 16 | import javafx.geometry.Bounds; 17 | import javafx.geometry.Rectangle2D; 18 | import javafx.scene.Node; 19 | import javafx.scene.image.ImageView; 20 | import javafx.scene.image.PixelBuffer; 21 | import javafx.scene.image.PixelFormat; 22 | import javafx.scene.image.WritableImage; 23 | import javafx.scene.layout.Pane; 24 | 25 | /** 26 | * A native rendering canvas. The assumption is that some native renderer 27 | * produces an image provided as an IntBuffer or ByteBuffer. The PixelFormats 28 | * must be IntArgbPre or ByteBgraPre respectively. For the API see NativeRenderer. 29 | * 30 | * This buffer is then used to create an Image which is bound to an ImageView. 31 | * This class manages the direct display of this Image in a Pane and reacts to 32 | * user input via mouse input or gestures on touch devices. 33 | * 34 | * TODOs: 35 | * - Implement further user actions. 36 | * - Handle different render scales. 37 | * - Packaging of native part into jar file. 38 | * 39 | * @author Michael Paus 40 | */ 41 | public class NativeRenderingCanvas { 42 | 43 | // Configure this to use double-buffering [2] or not [1]. 44 | private final int numBuffers = 2; 45 | 46 | // Configure this to use an external thread or the JavaFX application thread for rendering. 47 | private final boolean doRenderingAsynchronously = false; // The resizing does not work perfectly yet !!! 48 | 49 | private final int MAX_THREADS = 1; // More than one thread does not make sense for this service setup! 50 | 51 | private final ExecutorService executorService = Executors.newFixedThreadPool(MAX_THREADS, runnable -> { 52 | Thread t = new Thread(runnable); 53 | t.setDaemon(true); 54 | t.setName("NativeRenderer"); 55 | return t ; 56 | }); 57 | 58 | private final PixelFormat pixelFormat; 59 | private final ObjectProperty fxImage; 60 | private final ImageView imageView; 61 | private final Pane canvasPane; 62 | private final NativeRenderer nativeRenderer; 63 | private final RenderingService renderingService; 64 | private final ChangeListener resizeListener; 65 | 66 | private ByteBuffer oldRawByteBuffer; 67 | private ByteBuffer newRawByteBuffer; 68 | private PixelBuffer pixelBuffer; 69 | 70 | // The native renderer viewport. Its width and height are multiples of nrViewIncrement 71 | // and thus will normally be larger than the canvasPanes width and height. 72 | private int nrViewIncrement = 64; 73 | private final Viewport emptyViewport = new Viewport(); 74 | private Viewport nrViewport = emptyViewport; 75 | 76 | private double mx = 0.0; 77 | private double my = 0.0; 78 | 79 | private boolean inScrollBrackets = false; 80 | 81 | /** 82 | * Create and initialize a NativeRenderingCanvas instance. 83 | */ 84 | public NativeRenderingCanvas() { 85 | nativeRenderer = new NativeRenderer(); 86 | renderingService = new RenderingService(); 87 | canvasPane = new Pane(); 88 | fxImage = new SimpleObjectProperty<>(); 89 | pixelFormat = PixelFormat.getIntArgbPreInstance(); 90 | 91 | imageView = new ImageView(); 92 | imageView.imageProperty().bind(fxImage); 93 | imageView.fitWidthProperty().bind(canvasPane.widthProperty()); 94 | imageView.fitHeightProperty().bind(canvasPane.heightProperty()); 95 | imageView.setManaged(false); // !!! 96 | imageView.setPreserveRatio(true); 97 | imageView.setPickOnBounds(true); 98 | 99 | canvasPane.getChildren().add(imageView); 100 | 101 | resizeListener = (v,o,n) -> { 102 | render(nrViewport.withSizeIncrement((int)canvasPane.getWidth(), (int)canvasPane.getHeight(), nrViewIncrement)); 103 | }; 104 | 105 | init(); 106 | } 107 | 108 | /** 109 | * Must be called before the NativeRenderingCanvas can be used again after dispose() has been called. 110 | */ 111 | public void init() { 112 | canvasPane.boundsInLocalProperty().addListener(resizeListener); 113 | 114 | imageView.setOnMousePressed(e -> { 115 | if (! e.isSynthesized()) { 116 | mx = e.getX(); 117 | my = e.getY(); 118 | e.consume(); 119 | } 120 | }); 121 | 122 | imageView.setOnMouseReleased(e -> { 123 | if (! e.isSynthesized()) { 124 | mx = 0.0; 125 | my = 0.0; 126 | e.consume(); 127 | } 128 | }); 129 | 130 | imageView.setOnMouseDragged(e -> { 131 | if (! e.isSynthesized()) { 132 | Viewport newViewport = nrViewport.withDeltaLocation((int)(mx - e.getX()), (int)(my - e.getY())); 133 | mx = e.getX(); 134 | my = e.getY(); 135 | e.consume(); 136 | 137 | render(newViewport); 138 | } 139 | }); 140 | 141 | imageView.setOnScrollStarted(e -> { 142 | inScrollBrackets = true; 143 | }); 144 | 145 | imageView.setOnScrollFinished(e -> { 146 | inScrollBrackets = false; 147 | }); 148 | 149 | imageView.setOnScroll(e -> { 150 | 151 | // According to the JavaFX documentation, scroll started/finished indicates that this gesture was 152 | // performed on a touch device and not the mouse wheel. But due to a bug (at least on macOS, see: 153 | // https://bugs.openjdk.java.net/browse/JDK-8236971 ) this mechanism currently does not work 154 | // for JDKs above 11 independent of the JFX version used. 155 | 156 | // This simple mechanism does not work due to above bug because the total-delta values are NOT zero for mouse-wheels. 157 | // ScrollAction scrollAction = (e.getTotalDeltaX() != 0 || e.getTotalDeltaY() != 0.0) ? ScrollAction.ZOOM : ScrollAction.PAN; 158 | 159 | // We need all these criteria to find out whether this event comes from a mouse wheel 160 | // and it remains to be tested whether this works on all platforms and all devices. 161 | // Also this workarround only works with JDKs <= 11. 162 | ScrollAction scrollAction; 163 | if (! inScrollBrackets && 164 | ! e.isInertia() && 165 | Math.abs(e.getDeltaX()) == 0.0 && 166 | e.getDeltaY() == e.getTotalDeltaY() && 167 | Math.abs(e.getDeltaY()) > 1.0000001) 168 | { 169 | scrollAction = ScrollAction.ZOOM; 170 | } else { 171 | scrollAction = ScrollAction.PAN; 172 | } 173 | 174 | Viewport newViewport; 175 | if (scrollAction == ScrollAction.ZOOM) { 176 | // TODO: Implement action. 177 | newViewport = nrViewport; 178 | } else { 179 | newViewport = nrViewport.withDeltaLocation((int)-e.getDeltaX(), (int)-e.getDeltaY()); 180 | } 181 | e.consume(); 182 | 183 | render(newViewport); 184 | }); 185 | 186 | imageView.setOnZoom(e -> { 187 | // TODO: Implement action. 188 | Viewport newViewport = nrViewport; 189 | e.consume(); 190 | 191 | render(newViewport); 192 | }); 193 | 194 | imageView.setOnRotate(e -> { 195 | // TODO: Implement action. 196 | Viewport newViewport = nrViewport; 197 | e.consume(); 198 | 199 | render(newViewport); 200 | }); 201 | } 202 | 203 | /** 204 | * Dispose all resources and disable all actions. Init() has to be called 205 | * before the NativeRenderingCanvas instance can be used again. 206 | */ 207 | public void dispose() { 208 | nrViewport = emptyViewport; 209 | inScrollBrackets = false; 210 | 211 | canvasPane.boundsInLocalProperty().removeListener(resizeListener); 212 | 213 | imageView.setOnMouseClicked(null); 214 | imageView.setOnMousePressed(null); 215 | imageView.setOnMouseReleased(null); 216 | imageView.setOnMouseDragged(null); 217 | imageView.setOnScrollStarted(null); 218 | imageView.setOnScrollFinished(null); 219 | imageView.setOnScroll(null); 220 | imageView.setOnZoom(null); 221 | imageView.setOnRotate(null); 222 | 223 | fxImage.set(null); 224 | nativeRenderer.dispose(); 225 | } 226 | 227 | /** 228 | * Return the root node of the NativeRenderingCanvas which can be directly 229 | * added to some layout-pane. 230 | * 231 | * @return the root node of the NativeRenderingCanvas. 232 | */ 233 | public Node getRoot() {return canvasPane;} 234 | 235 | private void render(Viewport viewport) { 236 | if (! viewport.isEmpty()) { 237 | if (doRenderingAsynchronously) { 238 | renderingService.renderIfIdle(viewport); 239 | } else { 240 | renderUpdate(renderAction(viewport, nrViewport), viewport); 241 | } 242 | nrViewport = viewport; 243 | } 244 | } 245 | 246 | // Can be called on any thread. 247 | private int renderAction(Viewport newViewport, Viewport oldViewport) { 248 | if (newViewport != oldViewport) { 249 | if (newViewport.getWidth() != oldViewport.getWidth() || newViewport.getHeight() != oldViewport.getHeight()) { 250 | newRawByteBuffer = nativeRenderer.createCanvas(newViewport.getWidth(), newViewport.getHeight(), numBuffers, NativeColorModel.INT_ARGB_PRE.ordinal()); 251 | } 252 | } 253 | nativeRenderer.moveTo(newViewport.getMinX(), newViewport.getMinY()); 254 | return nativeRenderer.render(); 255 | } 256 | 257 | // Must be called on JavaFX application thread. 258 | private void renderUpdate(int bufferIndex, Viewport viewport) { 259 | assert Platform.isFxApplicationThread() : "Not called on JavaFX application thread."; 260 | if (newRawByteBuffer != oldRawByteBuffer) { 261 | oldRawByteBuffer = newRawByteBuffer; 262 | final IntBuffer intBuffer = newRawByteBuffer.asIntBuffer(); 263 | pixelBuffer = new PixelBuffer<>(viewport.getWidth(), numBuffers * viewport.getHeight(), intBuffer, pixelFormat); 264 | fxImage.set(new WritableImage(pixelBuffer)); 265 | } 266 | pixelBuffer.updateBuffer(pb -> { 267 | final Rectangle2D renderedFrame = new Rectangle2D( 268 | 0, 269 | bufferIndex * viewport.getHeight(), 270 | Math.min(canvasPane.getWidth(), viewport.getWidth()), 271 | Math.min(canvasPane.getHeight(), viewport.getHeight())); 272 | imageView.setViewport(renderedFrame); 273 | return renderedFrame; 274 | }); 275 | } 276 | 277 | private class RenderingService extends Service { 278 | private Viewport oldViewport = emptyViewport; 279 | private Viewport newViewport = emptyViewport; 280 | private Viewport dirtyViewport = emptyViewport; 281 | 282 | RenderingService() { 283 | setExecutor(executorService); 284 | 285 | this.setOnSucceeded(new EventHandler() { 286 | @Override 287 | public void handle(WorkerStateEvent t) { 288 | renderUpdate((Integer) t.getSource().getValue(), newViewport); 289 | renderIfIdle(dirtyViewport); 290 | } 291 | }); 292 | } 293 | 294 | void renderIfIdle(Viewport viewport) { 295 | assert Platform.isFxApplicationThread() : "Not called on JavaFX application thread."; 296 | 297 | if(! viewport.isEmpty()) { 298 | dirtyViewport = viewport; 299 | State state = getState(); 300 | if (state != State.SCHEDULED && state != State.RUNNING) { 301 | restart(); 302 | } 303 | } 304 | } 305 | 306 | @Override 307 | protected Task createTask() { 308 | return new Task() { 309 | @Override 310 | protected Integer call() { 311 | oldViewport = newViewport; 312 | newViewport = dirtyViewport; 313 | dirtyViewport = emptyViewport; 314 | return renderAction(newViewport, oldViewport); 315 | } 316 | }; 317 | } 318 | } 319 | 320 | } 321 | -------------------------------------------------------------------------------- /jfxtools-jarprocessor/src/main/java/de/mpmediasoft/jfxtools/jarprocessor/processors/ModuleChecker.java: -------------------------------------------------------------------------------- 1 | package de.mpmediasoft.jfxtools.jarprocessor.processors; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.PrintStream; 8 | import java.net.URI; 9 | import java.nio.file.FileSystem; 10 | import java.nio.file.FileSystems; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.util.ArrayList; 14 | import java.util.Collections; 15 | import java.util.HashMap; 16 | import java.util.HashSet; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Scanner; 20 | import java.util.Set; 21 | import java.util.jar.Attributes; 22 | import java.util.jar.Attributes.Name; 23 | import java.util.jar.Manifest; 24 | import java.util.spi.ToolProvider; 25 | import java.util.stream.Collectors; 26 | 27 | import de.mpmediasoft.jfxtools.jarprocessor.AbstractJARProcessor; 28 | import de.mpmediasoft.jfxtools.jarprocessor.JARProcessorException; 29 | 30 | /** 31 | * This jar-processor analyzes a list of jar-files and lists all services provided 32 | * by the contained code. 33 | * 34 | * It further checks whether the contained code is modular or non-modular. If it is 35 | * modular, it checks if the service delarations for the module-path are consistent 36 | * with the declarations for the classpath. 37 | * 38 | * This code needs Java 11+. 39 | * 40 | * @author mpaus 41 | */ 42 | public class ModuleChecker extends AbstractJARProcessor { 43 | 44 | private final static String indent = " "; 45 | 46 | private final static Name AUTOMATIC_MODULE_NAME_KEY = new Name("Automatic-Module-Name"); 47 | 48 | private final List checkResults = new ArrayList<>(); 49 | 50 | private ToolProvider jar; 51 | 52 | private boolean verbose = false; 53 | 54 | private int errors = 0; 55 | 56 | private static class CheckResult { 57 | private final File jarFile; 58 | private final Map> modularServiceProviders = new HashMap<>(); 59 | private final Map> nonModularServiceProviders = new HashMap<>(); 60 | private boolean modular; 61 | private boolean consistent; 62 | private boolean definedAutomaticModuleName; 63 | 64 | public boolean hasDefinedAutomaticModuleName() { 65 | return definedAutomaticModuleName; 66 | } 67 | 68 | public void setDefinedAutomaticModuleName(boolean definedAutomaticModuleName) { 69 | this.definedAutomaticModuleName = definedAutomaticModuleName; 70 | } 71 | 72 | CheckResult(File jarFile) { 73 | this.jarFile = jarFile; 74 | } 75 | 76 | boolean isModular() { 77 | return modular; 78 | } 79 | 80 | void setModular(boolean modular) { 81 | this.modular = modular; 82 | } 83 | 84 | boolean isConsistent() { 85 | return consistent; 86 | } 87 | 88 | void setConsistent(boolean consistent) { 89 | this.consistent = consistent; 90 | } 91 | 92 | File getJarFile() { 93 | return jarFile; 94 | } 95 | 96 | Map> getModularServiceProviders() { 97 | return modularServiceProviders; 98 | } 99 | 100 | Map> getNonModularServiceProviders() { 101 | return nonModularServiceProviders; 102 | } 103 | 104 | } 105 | 106 | private void showResult(List checkResults, boolean modular) { 107 | checkResults.stream().filter(r -> r.isModular() == modular).forEach(r -> {compare(r); printResult(r);}); 108 | } 109 | 110 | private void check(CheckResult checkResult) throws JARProcessorException { 111 | processMetaInfManifest(checkResult); 112 | findModularServiceProviders(checkResult); 113 | findNonModularServiceProviders(checkResult); 114 | } 115 | 116 | private void findModularServiceProviders(CheckResult checkResult) throws JARProcessorException { 117 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 118 | PrintStream ps = new PrintStream(baos); 119 | 120 | String[] jarDescribeModuleArgs = { "--describe-module", "--file", checkResult.getJarFile().getAbsolutePath() }; 121 | 122 | int status = jar.run(ps, System.err, jarDescribeModuleArgs); 123 | 124 | if (status == 0) { 125 | String res = new String(baos.toByteArray()); 126 | checkResult.setModular(!res.startsWith("No module descriptor found")); 127 | if (checkResult.isModular()) { 128 | res.lines().filter(s -> s.startsWith("provides")).forEach(l -> { 129 | try { 130 | scanProvidesLine(checkResult, l); 131 | } catch (JARProcessorException e) { 132 | ++errors; 133 | if (verbose) e.printStackTrace(); 134 | } 135 | }); 136 | if (errors > 0) throw new JARProcessorException("findModularServiceProviders terminated with errors."); 137 | } 138 | } else { 139 | throw new JARProcessorException("jar tool terminated with errors."); 140 | } 141 | } 142 | 143 | private void scanProvidesLine(CheckResult checkResult, String line) throws JARProcessorException { 144 | try (Scanner scanner = new Scanner(line.trim())) { 145 | List list = scanner.tokens().filter(s -> { 146 | return !s.equalsIgnoreCase("provides") && !s.equalsIgnoreCase("with"); 147 | }).collect(Collectors.toList()); 148 | if (list.size() == 0) { 149 | return; 150 | } else if (list.size() == 2) { 151 | if (!checkResult.getModularServiceProviders().containsKey(list.get(0))) { 152 | checkResult.getModularServiceProviders().put(list.get(0), new HashSet()); 153 | } 154 | assert !list.get(1).contains(","); 155 | checkResult.getModularServiceProviders().get(list.get(0)).add(list.get(1)); 156 | } else { 157 | throw new JARProcessorException("Unexpected provides statement: " + line); 158 | } 159 | } 160 | } 161 | 162 | private void findNonModularServiceProviders(CheckResult checkResult) throws JARProcessorException { 163 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 164 | PrintStream ps = new PrintStream(baos); 165 | 166 | String[] jarListArgs = { "--list", "--file", checkResult.getJarFile().getAbsolutePath() }; 167 | 168 | int status = jar.run(ps, System.err, jarListArgs); 169 | 170 | if (status == 0) { 171 | String res = new String(baos.toByteArray()); 172 | res.lines().filter(s -> { 173 | return s.startsWith("META-INF/services/") && !s.equals("META-INF/services/"); 174 | }).forEach(l -> { 175 | try { 176 | processMetaInfServices(checkResult, l); 177 | } catch (JARProcessorException e) { 178 | ++errors; 179 | if (verbose) e.printStackTrace(); 180 | } 181 | }); 182 | if (errors > 0) throw new JARProcessorException("findNonModularServiceProviders terminated with errors."); 183 | } else { 184 | throw new JARProcessorException("jar tool terminated with errors."); 185 | } 186 | } 187 | 188 | private void processMetaInfServices(CheckResult checkResult, String line) throws JARProcessorException { 189 | String spi = line.replaceFirst("META-INF/services/", "").trim(); 190 | if (!spi.isEmpty()) { 191 | if (!checkResult.getNonModularServiceProviders().containsKey(spi)) { 192 | checkResult.getNonModularServiceProviders().put(spi, new HashSet()); 193 | } 194 | 195 | URI uri = URI.create(String.format("jar:" + checkResult.getJarFile().toURI())); 196 | try (FileSystem jfs = FileSystems.newFileSystem(uri, Collections.emptyMap())) { 197 | Path path = jfs.getPath(line); 198 | if (Files.isRegularFile(path)) { 199 | List lines = Files.readAllLines(path); 200 | for (String impl : lines) { 201 | if (!impl.startsWith("#")) 202 | checkResult.getNonModularServiceProviders().get(spi).add(impl); 203 | } 204 | } else { 205 | throw new JARProcessorException("No regular file: " + path); 206 | } 207 | } catch (IOException e) { 208 | throw new JARProcessorException("Error processing line: " + line); 209 | } 210 | } else { 211 | throw new JARProcessorException("Unexpected META-INF/services/ line: " + line); 212 | } 213 | } 214 | 215 | private void processMetaInfManifest(CheckResult checkResult) throws JARProcessorException { 216 | URI uri = URI.create(String.format("jar:" + checkResult.getJarFile().toURI())); 217 | try (FileSystem jfs = FileSystems.newFileSystem(uri, Collections.emptyMap())) { 218 | Path path = jfs.getPath("META-INF/MANIFEST.MF"); 219 | if (Files.isRegularFile(path)) { 220 | try (InputStream is = Files.newInputStream(path)) { 221 | Manifest mf = new Manifest(is); 222 | Attributes attributes = mf.getMainAttributes(); 223 | boolean definedAutomaticModuleName = attributes.containsKey(AUTOMATIC_MODULE_NAME_KEY); 224 | checkResult.setDefinedAutomaticModuleName(definedAutomaticModuleName); 225 | } catch (IOException e) { 226 | throw new JARProcessorException("Cannot read manifest from: " + checkResult.getJarFile()); 227 | } 228 | } else { 229 | throw new JARProcessorException("No regular file: " + path); 230 | } 231 | } catch (IOException e) { 232 | throw new JARProcessorException("Error processing manifest."); 233 | } 234 | } 235 | 236 | private void printResult(CheckResult checkResult) { 237 | System.out.print(checkResult.getJarFile() + " : " + (checkResult.isModular() ? "modular" : "non-modular")); 238 | System.out.println(checkResult.isModular() ? "" : " hasDefinedAutomaticModuleName = " + checkResult.hasDefinedAutomaticModuleName()); 239 | if (checkResult.isConsistent()) { 240 | printResult(null, checkResult.getNonModularServiceProviders()); 241 | } else { 242 | System.out.println(indent + ">>> Inconsistent service declarations detected. <<<"); 243 | printResult("Services provided on module-path:", checkResult.getModularServiceProviders()); 244 | printResult("Services provided on class-path:", checkResult.getNonModularServiceProviders()); 245 | } 246 | } 247 | 248 | private void printResult(String msg, Map> serviceProviders) { 249 | if (msg != null) System.out.println(indent + msg); 250 | if (!serviceProviders.isEmpty()) { 251 | for (String spi : serviceProviders.keySet()) { 252 | Set nonModImpls = serviceProviders.get(spi); 253 | for (String nonModImpl : nonModImpls) { 254 | System.out.println(indent + indent + "provides " + spi + " with " + nonModImpl); 255 | } 256 | } 257 | } 258 | } 259 | 260 | private void compare(CheckResult checkResult) { 261 | checkResult.setConsistent(true); 262 | if (checkResult.isModular()) { 263 | if (checkResult.getModularServiceProviders().size() == checkResult.getNonModularServiceProviders().size()) { 264 | for (String spi : checkResult.getModularServiceProviders().keySet()) { 265 | Set modImpls = checkResult.getModularServiceProviders().get(spi); 266 | Set nonModImpls = checkResult.getNonModularServiceProviders().get(spi); 267 | if (nonModImpls != null && modImpls.size() == nonModImpls.size()) { 268 | for (String impl : modImpls) { 269 | if (!nonModImpls.contains(impl)) { 270 | checkResult.setConsistent(false); 271 | } 272 | } 273 | } 274 | } 275 | } else { 276 | checkResult.setConsistent(false); 277 | } 278 | } 279 | } 280 | 281 | @Override 282 | public void initialize(ToolProvider jarToolProvider, boolean verbose) { 283 | this.jar = jarToolProvider; 284 | this.verbose = verbose; 285 | } 286 | 287 | @Override 288 | public void start() { 289 | checkResults.clear(); 290 | errors = 0; 291 | } 292 | 293 | @Override 294 | public void process(String arg) throws JARProcessorException { 295 | CheckResult checkResult = new CheckResult(jarFile(arg)); 296 | check(checkResult); 297 | checkResults.add(checkResult); 298 | } 299 | 300 | @Override 301 | public void finish() { 302 | System.out.println("Modular results:"); 303 | System.out.println(); 304 | showResult(checkResults, true); 305 | 306 | System.out.println(); 307 | 308 | System.out.println("Non-modular results:"); 309 | System.out.println(); 310 | showResult(checkResults, false); 311 | } 312 | 313 | } 314 | -------------------------------------------------------------------------------- /docs/articles/JFX-Native/JFX-Native.adoc: -------------------------------------------------------------------------------- 1 | = Building native, AOT compiled, real-world JavaFX applications 2 | Dr.-Ing. Michael Paus, mpMediaSoft GmbH 3 | Version 1.3.0, 2021-03-27 4 | :doctype: article 5 | :encoding: utf-8 6 | :lang: en 7 | :toc: left 8 | :numbered: 9 | 10 | [.lead] 11 | A summary of my lessons learned converting an existing JavaFX application to a native, 12 | AOT compiled, real-world application using the Gluon Client Maven plugin which is based 13 | on the GraalVM `native-image` toolchain. 14 | 15 | == Overview 16 | 17 | First of all I would like to thank the team at Gluon which made all the following possible. 18 | Without their continuous, hard work it would not be possible at all to compile a modern 19 | JavaFX application into a native platform application. 20 | My thanks of course also include all the people involved in the GraalVM project 21 | which laid the foundation for the work done for JavaFX by Gluon. 22 | 23 | This article summarizes the experiences which I collected while converting one of my 24 | existing Java applications to a native application using the Gluon Client Maven plugin. 25 | This is not one of the many demos you can find on the internet, but an application that 26 | is still relatively small and manageable but otherwise a real-world application 27 | with a lot of external dependencies and technical challenges. It was not always 28 | an easy ride but in the end it worked out nicely. 29 | 30 | Actually this exercise was just the prelude to the real goal which is to also run this, 31 | and other similar applications, on mobile platforms like iOS and Android. (My 32 | main target is Android though, just because I own an Android phone and tablet but 33 | not any iOS device.) Now that the first step has been taken, I now eagerly await 34 | the general availability of the Android support of course. 35 | 36 | .Screenshot of the application on the Mac. 37 | image::images/000_mac_DAeCAirspaceValidator.png[] 38 | 39 | == Requirements 40 | 41 | Just follow the instructions given on the project page at GitHub. 42 | https://github.com/gluonhq/client-maven-plugin 43 | Further documentation and some samples can be reached from this entry page too. 44 | There is also a Gradle plugin, but at the moment the focus of the development seems 45 | to be on the Maven plugin. 46 | 47 | == Build infrastructure 48 | 49 | The GraalVM `native-image` tool is very memory hungry. When your build times get longer 50 | and longer, you are probably running out of memory. My initial hello-world builds were 51 | mostly finished within 2 - 4 minutes but when I started to do bigger builds the times 52 | went up to 7, 22 and finally more than an hour. This turned out to be caused 53 | by the insuffcient amount of RAM in my old MacBook Pro, which only had 8 GB of RAM. 54 | Now I use a Mac mini with 16 GB of RAM and my build times, even for larger projects, 55 | are back in the 3 minutes range. So, using a development machine with enough memory 56 | is essential and having even 32 GB of RAM available certainly does not hurt. 57 | 58 | Having a fast multi-core CPU also does not hurt. I have seen CPU utilizations of up to 59 | 1200%, which is probably the best you can get from a 6 core CPU with hyperthreading. 60 | 61 | == Build configuration 62 | 63 | === Options 64 | 65 | It is advisable to use the following `native-image` options for the build: 66 | 67 | * -ea 68 | * --verbose 69 | 70 | The option `-ea` enables assertions in the resulting image and this is very helpful. 71 | It is not as easy as in Java to debug a native image and therefore it is helpful to use 72 | a lot of assertions in your code to be notified as early as possible about potential 73 | problems, e.g., resources which have not been loaded. 74 | 75 | The option `--verbose` makes the output of the build process more verbose and this helps in 76 | case something goes wrong. As a build takes a while, it makes sense to always use this option 77 | so that you do not have to repeat the build in case something goes wrong and you don't know why. 78 | 79 | === System properties 80 | 81 | When calling `native-image` you can define system properties but these are only 82 | visible to the VM during the build process but not later at run-time of the native 83 | application. This can cause some confusion because for classes which are initialized 84 | at build-time, these system properties would be defined, whereas for classes which are 85 | initialized at run-time they wouldn't. 86 | 87 | A concept, how they can be made visible at run-time too, is explained here 88 | https://github.com/oracle/graal/issues/779 but this does not seem to work anymore 89 | because classes are now initialized at run-time by default and not at build-time 90 | as in previous versions. In order to circumvent this problem I created a separate 91 | class for this and defined via the appropriate command line option `--initialize-at-build-time` that this 92 | particular class should be initialized at build time. This did the trick and it works now. 93 | 94 | NOTE: Don't try to be too smart when writing this class. Only write primitive code because 95 | otherwise `native-image` will refuse to initialize this class at build-time. 96 | 97 | == GraalVM/native-image limitations and issues 98 | 99 | GraalVM `native-image` still has several limitations which may bite you in real-world 100 | projects. So I strongly advise you to read the following document which 101 | summarizes most of these limitations. 102 | 103 | https://github.com/oracle/graal/blob/master/substratevm/Limitations.md 104 | 105 | The ones I stumbled over most often where: 106 | 107 | * Reflection configuration (Everywhere) 108 | * Method Handles not supported (Log4J, NSMenuFX) + 109 | (This issue seems to be mostly fixed now but NSMenuFX still does not work for other reasons.) 110 | * Serialization not supported (Disk cache) + 111 | (This issue seems to be fixed with GraalVM 21.0.0) 112 | * Soft-References not working as expected (RAM cache) + 113 | (This issue seems to be fixed now: https://github.com/oracle/graal/issues/2145) 114 | * Only a single (default) locale + 115 | (This issue is supposed to be fixed in version 21.1.0 (20. April 2021): 116 | https://github.com/oracle/graal/issues/911#issuecomment-745209431) 117 | * Media not supported on all platforms 118 | 119 | I'll go into more details in the following sections. 120 | 121 | === Reflection 122 | 123 | The use of reflection is ubiquitous in the Java world which poses a problem for any AOT 124 | (ahead of time) compilation of Java code because which classes are accessed via reflection 125 | is not always known at build time. Some uses can be detected automatically but for others 126 | a list of classes must be provided by the user at build time. 127 | 128 | One way to make this task less tedious and error prone, is to use the tracing agent. 129 | 130 | https://medium.com/graalvm/introducing-the-tracing-agent-simplifying-graalvm-native-image-configuration-c3b56c486271 131 | 132 | This agent collects relevant data by analyzing the software when executed via a standard 133 | Java virtual machine. It's a pity though that the output of this agent cannot yet be integrated 134 | directly into the configuration of the client-maven-plugin. 135 | 136 | See: https://github.com/gluonhq/client-gradle-plugin/issues/25 137 | 138 | (This issue seems to be mostly fixed now because you can use the tracing agent via the client-maven-plugin.) 139 | 140 | === Resources 141 | 142 | Resources can be delt with in a similar way as reflection. The nice thing is that you can 143 | specify which resources to load via wild cards. In my case it was enough to specify the 144 | following resource list: 145 | .... 146 | 147 | .*\\.properties$ 148 | .*\\.vert$ 149 | .*\\.wav$ 150 | .*\\.json$ 151 | .*\\.COF$ 152 | 153 | .... 154 | A special case of this are language resource bundles which are also properties but have to 155 | be specified in a separate list. It would be very tedious if you would have to explicitly 156 | differentiate between general properties and language bundles but in my case I found it 157 | to be ok to keep the properties wild card in the resource list and separately add the 158 | language bundles to the bundles list like this. 159 | .... 160 | 161 | com.mycompany.myproject.Main 162 | com.mycompany.myproject.airspaces.Airspaces 163 | com.mycompany.myproject.maps.Maps 164 | controlsfx 165 | 166 | .... 167 | 168 | === Method handles 169 | 170 | According to the documentation, method handles are not supported. 171 | 172 | See: https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md#invokedynamic-bytecode-and-method-handles 173 | 174 | This has severe consequences for several libraries and frameworks. 175 | 176 | ==== Logging 177 | 178 | Logging frameworks are notorious users of all kind of reflection magic (I still don't understand why) which 179 | falls onto your feet when you use `native-image`. The worst of all is Log4J. 180 | 181 | See: https://issues.apache.org/jira/browse/LOG4J2-2649?focusedCommentId=17005296&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-17005296 182 | 183 | I finally had to completely abandon Log4J (and in retrospect I wonder why I have ever used it at all). 184 | This switch was made easy for me by the fact that I have consistently used the SLF4J facade throughout all my 185 | software, so the only necessary change was the configuration of the logging framework and rewriting my own 186 | JFX logging handler. I finally ended up using the standard Java logging because that is supported out of the 187 | box with `native-image`. The simple variant of SLF4J also worked but it would have been more complicated to 188 | rewrite my JFX logging handler. 189 | 190 | One problem remains though. I simply can't get the FileHandler working. 191 | See: https://github.com/gluonhq/client-maven-plugin/issues/125 192 | 193 | ==== NSMenuFX 194 | 195 | Another library I used was NSMenuFX to get a decent system menu integration for the Mac, which JavaFX 196 | does not provide by default, but it failed with `native-image`. After a lot of research 197 | (thanks José https://github.com/gluonhq/substrate/issues/118 ) I finally learned that this is also due 198 | to the internal use of method handles. 199 | 200 | So I first created an issue https://github.com/codecentric/NSMenuFX/issues/31 on GitHub and 201 | finally fixed the problem myself and created a pull-request, which has now been integrated into the 202 | latest release of NSMenuFX. 203 | 204 | However, my frustration grew again when I finally realized that this was all in vain and NSMenuFX still 205 | did not work because the system menu bar is in general not yet supported. This isn't nice for the Mac version 206 | but as my real goal is the Android version it is not such a big problem because on Android I won't need the 207 | system menu bar anyway. 208 | 209 | === Serialization 210 | 211 | I used Java serialization for a temporary disk cache but serialization is currently not 212 | supported. So I now have to live without disk cache. (The issue was not serious enough 213 | to justify a switch to another fast serialization technique.) 214 | 215 | https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md#serialization 216 | 217 | === Soft references 218 | 219 | (This issue seems to be fixed now: https://github.com/oracle/graal/issues/2145) 220 | 221 | I used a temporary RAM cache in my code which was based on Javas soft-references. 222 | The result was that my native code felt slow and was not very responsive and I was 223 | actually very disappointed. Finally I found out that this happened because my cache 224 | was almost always empty and so my software had to load everything from disk over and over 225 | again. GraalVMs `native-image` handles references differently than the Java VM does, which 226 | has the effect that all soft-references are always immediately cleared and thus became 227 | useless to me. 228 | 229 | https://github.com/oracle/graal/blob/master/substratevm/LIMITATIONS.md#references 230 | 231 | There is only one small sentence in the documentation which hints at this deviation. 232 | 233 | NOTE: I learned from Laurent Bourgès that the MarlinFX renderer uses soft-references 234 | by default to hold its own renderer context. It should therefore be tuned for 235 | GraalVM `native-image` to use hard references instead: `-Dprism.marlin.useRef=hard` 236 | 237 | === Single locale 238 | 239 | A severe, not very well documented, limitation of `native-image` is the fact that 240 | currently only one locale is supported. You have to decide at build time which locale 241 | you want to use for your application. If you want to support more than one locale you 242 | have to build separate versions of your application. One for each supported locale. 243 | 244 | This is already a pain but it gets worse if you look at the possible side effects 245 | this can have. In fact you cannot even parse a simple string value which does not 246 | adhere to the conventions of your chosen built-in locale. 247 | 248 | See: https://github.com/oracle/graal/issues/2141 249 | 250 | (This issue is supposed to be fixed in version 21.1.0 (20. April 2021): 251 | https://github.com/oracle/graal/issues/911#issuecomment-745209431) 252 | 253 | === Media not supported on all platforms 254 | 255 | Saving images via javafx.embed.swing.SwingFXUtils.fromFXImage(SwingFXUtils.java:284) 256 | does not seem to work. + 257 | https://github.com/oracle/graal/issues/2842#issuecomment-808720795 258 | 259 | Playing audio clips also does not seem to work. Although the code compiled without problem 260 | and the AudioClip was instantiated directly without any reflection magic I got the following 261 | exception at runtime. 262 | .... 263 | Exception in thread "JavaFX Application Thread" java.lang.NoClassDefFoundError: javafx.scene.media.AudioClip 264 | .... 265 | 266 | 267 | == JavaFX/Substrate limitations and issues 268 | 269 | The JavaFX part of the native image creation currently also has some limitations. 270 | 271 | === System menus 272 | 273 | The system menu bar is currently not supported (see above). 274 | 275 | === AWT 276 | 277 | AWT is currently not supported. This would not be such a big deal if some features 278 | of JavaFX did not depend on it. 279 | 280 | * javafx.application.HostServices.showDocument (fails on Mac) 281 | 282 | See: https://github.com/gluonhq/substrate/issues/337 283 | 284 | Some other uses of AWT do work, e.g., image reading and writing. In order to save 285 | a JavaFX image it has to be converted to an AWT BufferedImage first, so that it can then 286 | be saved via ImageIO. That works although it is part of AWT. 287 | 288 | It would probably be a good idea in general to make JavaFX completely independent 289 | from AWT. 290 | 291 | === Audio 292 | 293 | Playing AudioClips currently does not seem to work because the glib-lite library is missing. 294 | 295 | See: https://github.com/gluonhq/substrate/issues/336 296 | 297 | === Image size 298 | 299 | The size of the created executable file currently seems to be quite large. In my case, of a 300 | still quite small application, the size is already 100 MB, which is more than the whole 301 | .app bundle created by jpackage, which has only 73.8 MB if I bundle everything or only 58.8 MB 302 | if I use the Maven shade plugin with the option minimizeJar switched on. 303 | 304 | If jlink would put a bit more effort into it, the size of the .app bundle could even be 305 | further reduced substantially by more selectively loading code and resources and not just 306 | doing so on a whole module basis. 307 | 308 | (This issue can be solved via UPX. See: https://upx.github.io/) 309 | 310 | === Performance 311 | 312 | The performance of the community editon of `native-image` sometimes seems to be 313 | much worse than the standard VM with HotSpot due to some missing code optimizations. 314 | See: https://github.com/bourgesl/perfFX 315 | 316 | == Special cases 317 | 318 | === SQLite 319 | 320 | It took me some time to get SQLite working but in the end all I had to do is to add the 321 | following items to the POM. 322 | 323 | .... 324 | 325 | org.sqlite.core.DB 326 | org.sqlite.core.NativeDB 327 | org.sqlite.BusyHandler 328 | org.sqlite.Function 329 | org.sqlite.ProgressHandler 330 | org.sqlite.Function$Aggregate 331 | org.sqlite.Function$Window 332 | org.sqlite.core.DB$ProgressObserver 333 | 334 | .... 335 | 336 | .... 337 | 338 | org/sqlite/native/Mac/${os.arch}/.* 339 | 340 | .... 341 | 342 | The last entry is tricky. The path contains the platform specifc shared library 343 | of the native part of SQLite. (Change `Mac` to the right one for your platform. 344 | Just ${os.name} does not work.) 345 | 346 | == Open issues 347 | 348 | === Fully or partially blank panes 349 | 350 | When something goes wrong during the initialization of a view, I often have the situation 351 | that I am just confronted with a blank stage or pane without any error message or stack trace. 352 | It is then very difficult to track down what the actual cause of the problem is. I mostly 353 | have this problem when initializing views via FXML. 354 | 355 | === FXML 356 | 357 | The use of FXML is a PITA. All classes are loaded via reflection and so must be 358 | present in the final reflection list. Some classes are already included in this list 359 | by default, others (most ?) must be added manually. I finally adopted the habbit to 360 | just copy the `import` section of each FXML file because there you already have a list 361 | of all classes used by this file if this file was created by SceneBuilder which luckily 362 | does not use the wildcard notation. 363 | 364 | In order to make this task at least a little bit less cumbersome, I have written a 365 | tool for myself to collect this information. I have published it on GitHub, just in case 366 | someone has the same need for such a tool like I had. It is not perfect but it helps a little. 367 | https://github.com/mipastgt/JFXToolsAndDemos#fxml-checker 368 | 369 | Another annoying problem is that sometimes it is not sufficient to just put the class you 370 | want to load into this list. E.g., if you want to load a ProgressBar and have put this 371 | class into the refection list, you will still get the following error: 372 | `ProgressBar Property "progress" does not exist or is read-only`. 373 | The reason is that the property "progress" is defined in the super-class of ProgressBar 374 | and so you have to specify ProgressIndicator as well. 375 | 376 | === UnsatisfiedLinkErrors 377 | 378 | Some native libraries seem to be missing from substrate and so you will get UnsatisfiedLinkErrors. 379 | 380 | * java.util.logging.FileHandler + 381 | See: https://github.com/gluonhq/client-maven-plugin/issues/125 382 | * com.sun.imageio.plugins.jpeg.JPEGImageReader + 383 | symbol: Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_initJPEGImageReader or Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_initJPEGImageReader__ 384 | * no jfxwebkit in java.library.path 385 | 386 | === Misleading error messages 387 | 388 | Very often the error messages you get are very misleading. 389 | At a first glance an error message like `java.lang.IllegalArgumentException: Unable to coerce CENTER to class javafx.geometry.HPos.` 390 | is very cunfusing because CENTER definitely is a valid member of HPos. The actual reason for this 391 | error message is that `javafx.geometry.HPos` is just missing in the reflection list. Error messages 392 | should give a more precise hint on the real cause for an error. 393 | 394 | === JAXB 395 | 396 | For me JAXB is the workhorse for dealing with XML files but this seems to be a hard problem 397 | for GraalVM/native-image. 398 | 399 | See: https://github.com/oracle/graal/issues/379 400 | 401 | I got this working in a separte test program for some GPX files by following the hints in the above link. 402 | I can now read and write such files. (At least the ones I have tested, but that is another issue.) 403 | 404 | However, this involves the use of the tracing agent which is currently not supported by the Client-Maven-Plugin 405 | and when I tried to transfer the results of the agent manually I got stuck because there is currently also 406 | no proxy list support. 407 | 408 | Until now I have not found a solution for this and thus cannot read or write any XML files in my real software, 409 | which limits its usability quite a bit. 410 | 411 | (This issue seems to be mostly fixed now because you can use the tracing agent via the client-maven-plugin.) 412 | 413 | == Java VM vs. GraalVM/native-image comparison 414 | 415 | This is a subjective comparison of a standard Java VM (Oracle OpenJDK 14 EA) 416 | versus the GraalVM/native-image community edition (20.0.0 utilized by GluonHQ/substrate via 417 | Client-Maven-Plugin). 418 | 419 | .Table Java VM vs. GraalVM/native-image comparison 420 | |=== 421 | |Feature |Java VM |GraalVM/native-image 422 | 423 | |Works on Mobile + 424 | (iOS, Android) 425 | |- 426 | |+ 427 | 428 | |Development experience 429 | |+ 430 | |- 431 | 432 | |Feature completeness 433 | |+ 434 | |0 435 | 436 | |Startup time 437 | |0 438 | |+ 439 | 440 | |Warmup time 441 | |0 442 | |+ 443 | 444 | |Peak performance 445 | |+ 446 | |0 447 | 448 | |Bundle size 449 | |0 450 | |- (pure), + (with UPX) 451 | |=== 452 | 453 | Some remarks on the table: 454 | 455 | * The startup time of the Java VM could be further reduced if AppCDS would also work for reduced 456 | runtime images created via jlink. The current advantage of AOT compilation could be reduced in 457 | this respect. 458 | * Also the warmup time of the Java VM could be further reduced via profile guided optimization. 459 | 460 | Taking all this into account, the real driver to use GraalVM/native-image is the promise that 461 | it will allow the use of the latest standard Java/JavaFX on mobile devices too and thus make 462 | it possible to cover the mobile, embedded and desktop sector with a single code base. 463 | For a pure desktop environment its usefulness is currently still questionable due to various limitations and the development overhead, but we are making progress. 464 | 465 | == Conclusion 466 | 467 | This is only a snapshot of my experiences so far in getting a real-world JavaFX 468 | application compiled into a native image. If I have missed something important or you think 469 | you can help me with one of the open issues, just drop me a line or create an issue here. 470 | 471 | Once you have circumvented all the mentioned problems, the resulting binary seems to be 472 | quite stable and the performance is also relatively good. So, I am looking forward to do the next 473 | step and compile the whole application as an Android app. 474 | 475 | --------------------------------------------------------------------------------