1

I'm struggling for quite some time now to save a small image in a database and load it to be used as a javafx.scene.image.Image. 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.

I'm getting this exception: java.lang.NoClassDefFoundError: javafx/embed/swing/SwingFXUtils when trying to use SwingFXUtils.

When I use the module system, it breaks another library that I'm using (itextpdf). So I'd like to avoid that and just find another way to save and load a JavaFX image from DB. Any suggestions?

3
  • 2
    "but it appears that the class requires an entry in the module-info.java file" – This is not true. Nothing in the JavaFX library requires your code to be modular. Commented Nov 19, 2023 at 21:16
  • Ok. but I'm getting java.lang.NoClassDefFoundError: javafx/embed/swing/SwingFXUtils, so I assumed it must be this. Why am I getting this error then? Commented Nov 19, 2023 at 21:21
  • 3
    You must be failing to include javafx.swing on 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. Commented Nov 19, 2023 at 21:37

1 Answer 1

3

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.

  • Java 21.0.1 (Eclipse Adoptium / Temurin)

  • JavaFX 21.0.1

  • H2 2.2.224

  • Gradle 8.4

  • JavaFX Gradle Plugin 0.1.0

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.