You mention:
Several solutions that I tried involved using SwingFXUtils.fromFXImage and SwingFXUtils.toFxImage, but it appears that the class requires an entry in the module-info.java file, which I didn't even intend to use
This is not true. Although JavaFX only supports being loaded as named modules1, nothing about JavaFX requires that your own code be modular. The only way you can be getting a NoClassDefFoundError is by failing to include the class on the module-path/class-path at run-time (it must be there at compile-time, otherwise your code would fail to compile). I recommend reading Getting Started with JavaFX to see how to set up a basic JavaFX application with one of the major Java IDEs and/or build tools. But without knowing how you have configured your project and how you are running the project, we will not be able to tell you what specifically is wrong with your setup.
That said, what you want to accomplish in general is indeed possible. Here is an example that allows you to browse images on your computer and save them to an in-memory H2 database. The ID and name of the images are put into a TableView which includes a column with an "Open" button that allows you to view an image loaded from the in-memory database. The images are stored as blobs in the database in PNG format, regardless of their original format. The SwingFXUtils and ImageIO classes are used to convert the JavaFX image to a PNG "file"2.
The example does not show you how to deploy the application (e.g., via jpackage).
Versions
Here are the versions of libraries and tools I used to build and run the example.
In a comment to your previous question2, you stated you are using Gradle, so that is what I used for the example.
Source Code
There is no module-info.java file.
ImageRecord.java
package com.example;
public record ImageRecord(int id, String name) {}
Main.java
package com.example;
import java.io.File;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import javafx.application.Application;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.stage.Window;
public class Main extends Application {
private final Executor executor = Executors.newVirtualThreadPerTaskExecutor();
private final ImagesDatabase db = new ImagesDatabase("test");
private final ImageRepository imageRepo = new ImageRepository(db);
private File lastDirectory;
@Override
public void start(Stage primaryStage) {
var table = createTable(record -> displayImage(primaryStage, record));
var chooseBtn = new Button("Choose image...");
chooseBtn.setOnAction(
e -> {
e.consume();
var image = chooseImage(primaryStage);
if (image != null) {
executor.execute(createSaveImageTask(image, table.getItems()::add));
}
});
var root = new BorderPane();
root.setTop(chooseBtn);
root.setCenter(table);
BorderPane.setAlignment(chooseBtn, Pos.CENTER);
BorderPane.setMargin(chooseBtn, new Insets(10));
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
}
@Override
public void stop() throws Exception {
db.close();
}
private Image chooseImage(Window owner) {
var chooser = new FileChooser();
chooser.setTitle("Choose Image File");
chooser
.getExtensionFilters()
.add(new FileChooser.ExtensionFilter("Image Files", "*.jpeg", "*.jpg", "*.png"));
if (lastDirectory != null) {
chooser.setInitialDirectory(lastDirectory);
}
var file = chooser.showOpenDialog(owner);
if (file != null) {
lastDirectory = file.getParentFile();
return new Image(file.toURI().toString());
}
return null;
}
private void displayImage(Window owner, ImageRecord record) {
var view = new ImageView();
var task = createGetImageTask(record, view::setImage);
executor.execute(task);
var sp = new ScrollPane(view);
sp.setPannable(true);
var window = new Stage(StageStyle.UTILITY);
window.initOwner(owner);
window.setTitle(record.name());
window.setScene(new Scene(sp, 500, 300));
window.setOnHiding(e -> task.cancel());
window.show();
}
private TableView<ImageRecord> createTable(Consumer<ImageRecord> onOpen) {
var table = new TableView<ImageRecord>();
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
var idCol = new TableColumn<ImageRecord, Number>("ID");
idCol.setCellValueFactory(data -> new SimpleIntegerProperty(data.getValue().id()));
table.getColumns().add(idCol);
var nameCol = new TableColumn<ImageRecord, String>("Name");
nameCol.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().name()));
table.getColumns().add(nameCol);
var openBtnCol = new TableColumn<ImageRecord, ImageRecord>();
openBtnCol.setCellValueFactory(data -> new SimpleObjectProperty<>(data.getValue()));
openBtnCol.setCellFactory(tc -> createOpenButtonCell(onOpen));
table.getColumns().add(openBtnCol);
return table;
}
private TableCell<ImageRecord, ImageRecord> createOpenButtonCell(Consumer<ImageRecord> onOpen) {
return new TableCell<>() {
final HBox container = new HBox();
final Button openButton = new Button("Open");
{
container.getChildren().add(openButton);
container.setAlignment(Pos.CENTER);
openButton.setOnAction(
e -> {
e.consume();
var item = isEmpty() ? null : getItem();
if (item != null) {
onOpen.accept(item);
}
});
}
@Override
protected void updateItem(ImageRecord item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
setGraphic(container);
}
}
};
}
private Task<?> createSaveImageTask(Image image, Consumer<ImageRecord> onSuccess) {
return new Task<ImageRecord>() {
@Override
protected ImageRecord call() throws Exception {
return imageRepo.insertImage(image);
}
@Override
protected void succeeded() {
onSuccess.accept(getValue());
}
@Override
protected void failed() {
getException().printStackTrace();
}
};
}
private Task<?> createGetImageTask(ImageRecord record, Consumer<Image> onSuccess) {
return new Task<Image>() {
@Override
protected Image call() throws Exception {
return imageRepo.getImage(record).orElseThrow();
}
@Override
protected void succeeded() {
onSuccess.accept(getValue());
}
@Override
protected void failed() {
getException().printStackTrace();
}
};
}
}
ImageRepository.java
package com.example;
import static java.sql.Statement.RETURN_GENERATED_KEYS;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import javax.imageio.ImageIO;
public class ImageRepository {
private static final String SELECT_ALL_RECORDS_SQL = "SELECT id, name FROM Images";
private static final String SELECT_IMAGE_SQL = "SELECT image FROM Images WHERE id = ?";
private static final String INSERT_SQL = "INSERT INTO Images (name, image) VALUES (?, ?)";
private final AtomicInteger generatedNameCount = new AtomicInteger();
private final ImagesDatabase db;
public ImageRepository(ImagesDatabase db) {
this.db = db;
}
public List<ImageRecord> getRecords() throws SQLException {
return db.execute(
conn -> {
try (var stat = conn.createStatement()) {
var result = stat.executeQuery(SELECT_ALL_RECORDS_SQL);
var records = new ArrayList<ImageRecord>();
while (result.next()) {
int id = result.getInt(1);
var name = result.getString(2);
records.add(new ImageRecord(id, name));
}
return records;
}
});
}
public Optional<Image> getImage(ImageRecord record) throws SQLException {
return getImage(record.id());
}
public Optional<Image> getImage(int recordId) throws SQLException {
if (recordId <= 0) {
throw new IllegalArgumentException("recordId <= 0: " + recordId);
}
return db.execute(
conn -> {
try (var stat = conn.prepareStatement(SELECT_IMAGE_SQL)) {
stat.setInt(1, recordId);
var result = stat.executeQuery();
if (result.next()) {
var image = new Image(result.getBinaryStream(1));
return Optional.of(image);
} else {
return Optional.empty();
}
}
});
}
public ImageRecord insertImage(Image image) throws SQLException {
Objects.requireNonNull(image);
return db.execute(
conn -> {
try (var stat = conn.prepareStatement(INSERT_SQL, RETURN_GENERATED_KEYS)) {
var name = getImageName(image);
stat.setString(1, name);
stat.setBinaryStream(2, imageToInputStream(image));
stat.executeUpdate();
var keys = stat.getGeneratedKeys();
if (keys.next()) {
int id = keys.getInt(1);
return new ImageRecord(id, name);
} else {
throw new IllegalStateException("generated key not returned");
}
}
});
}
private String getImageName(Image image) {
var source = image.getUrl();
return source == null ? generateImageName() : source;
}
private String generateImageName() {
return "Generated Image Name " + generatedNameCount.incrementAndGet();
}
private InputStream imageToInputStream(Image image) {
var out = new ByteArrayOutputStream();
try {
ImageIO.write(SwingFXUtils.fromFXImage(image, null), "png", out);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return new ByteArrayInputStream(out.toByteArray());
}
}
ImagesDatabase.java
package com.example;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.sql.DataSource;
import org.h2.jdbcx.JdbcDataSource;
public class ImagesDatabase implements AutoCloseable {
private static final String CREATE_TABLE_SQL =
"CREATE TABLE Images (id IDENTITY, name VARCHAR(255), image BLOB)";
@FunctionalInterface
public interface SQLFunction<T> {
T execute(Connection connection) throws SQLException;
}
private final Lock mutex = new ReentrantLock();
private final DataSource source;
private Connection connection;
private boolean open = true;
private boolean initialized;
public ImagesDatabase(String name) {
if (name.isBlank()) {
throw new IllegalArgumentException("blank name");
}
var source = new JdbcDataSource();
source.setUrl("jdbc:h2:mem:" + name + ";DB_CLOSE_DELAY=-1");
this.source = source;
}
public <T> T execute(SQLFunction<T> function) throws SQLException {
Objects.requireNonNull(function);
mutex.lock();
try {
checkOpen();
return function.execute(getOrOpenConnection());
} finally {
mutex.unlock();
}
}
private Connection getOrOpenConnection() throws SQLException {
if (connection == null || connection.isClosed()) {
connection = source.getConnection();
initialize(connection);
}
return connection;
}
private void initialize(Connection conn) throws SQLException {
if (!initialized) {
try (var stat = conn.createStatement()) {
stat.executeUpdate(CREATE_TABLE_SQL);
}
initialized = true;
}
}
private void shutdown() throws SQLException {
if (initialized) {
try (var conn = getOrOpenConnection();
var stat = conn.createStatement()) {
stat.execute("SHUTDOWN");
}
connection = null;
}
}
private void checkOpen() {
if (!open) {
throw new IllegalStateException("closed");
}
}
@Override
public void close() throws SQLException {
mutex.lock();
try {
if (open) {
open = false;
shutdown();
}
} finally {
mutex.unlock();
}
}
}
Gradle Files
I used the Kotlin DSL, but you can use the Groovy DSL if you want.
settings.gradle.kts
rootProject.name = "h2images-example"
build.gradle.kts
plugins {
id("org.openjfx.javafxplugin") version "0.1.0"
application
}
group = "com.example"
version = "1.0"
javafx {
modules("javafx.controls", "javafx.swing")
version = "21.0.1"
}
application {
mainClass.set("com.example.Main")
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.h2database:h2:2.2.224")
}
Execution
You would execute the above with:
./gradlew run
Note ./gradlew invokes the Gradle Wrapper. If you have a version of Gradle installed on your computer, then you can generate a wrapper for version 8.4 via:
gradle wrapper --gradle-version 8.4
1. JavaFX does not technically support being loaded from the class-path. That means ideally the JavaFX modules should be on the module-path and resolved as named modules, even if your own code and other dependencies are loaded form the class-path. However, I am not aware of anything that breaks if JavaFX is on the class-path (at least as of JavaFX 21), except that your main class can no longer be a subclass of javafx.application.Application (you would need a separate "launcher class" as the main class). Just know that anything that breaks specifically because JavaFX is on the class-path is unlikely to be fixed by the JavaFX team.
Note that the Gradle and Maven plugins provided by OpenJFX configure those build tools to put JavaFX on the module-path.
2. Based on context from your two questions, the example converts Image objects to PNG bytes. However, if you already receive the images as bytes (i.e., as a file, either locally or remotely), then it would probably be easier and more efficient to just put those bytes directly into the database.
javafx.swingon the module-path/class-path at run-time (it must be there at compile-time, or your code wouldn't compile). You can read Getting Started with JavaFX to see how to set up a basic JavaFX project using one of the big Java IDEs and/or build tools. But without a minimal reproducible example no one will be able to point out what specifically is wrong with your setup.