Compare commits

..

14 commits
v1.4 ... master

24 changed files with 285 additions and 93 deletions

View file

@ -1 +0,0 @@
ImageTagger-Solution

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.22" />
<option name="version" value="1.9.25" />
</component>
</project>

View file

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<list size="1">

3
flatpak/build.sh Executable file
View file

@ -0,0 +1,3 @@
# Script to debug the Flatpak application.
flatpak-builder --user --install --force-clean build-dir dev.nuculabs.ImageTagger.yaml
flatpak run dev.nuculabs.ImageTagger

View file

@ -1,3 +0,0 @@
# Script to debug the Flatpak application.
flatpak-builder --sandbox --user --install --force-clean build-dir dev.nuculabs.ImageTagger.yaml
flatpak run dev.nuculabs.ImageTagger

View file

@ -1,6 +1,6 @@
[Desktop Entry]
Name=ImageTagger
Exec=/app/bin/image/bin/ImageTagger
Exec=/app/bin/ImageTagger/bin/ImageTagger
Terminal=false
Type=Application
Icon=dev.nuculabs.ImageTagger

View file

@ -2,38 +2,47 @@ id: dev.nuculabs.ImageTagger
runtime: org.freedesktop.Platform
runtime-version: '23.08'
sdk: org.freedesktop.Sdk
command: /app/bin/image/bin/ImageTagger
sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk17
command: /app/bin/ImageTagger/bin/ImageTagger
finish-args:
# X11 + XShm access
- --share=ipc
- --socket=fallback-x11
# Wayland access
# - --socket=wayland
- --socket=wayland
# GPU acceleration if needed
- --device=dri
# Needs to save files locally
- --filesystem=xdg-pictures
- --filesystem=home
modules:
- name: ImageTagger
buildsystem: simple
build-options:
env:
PATH: /app/bin:/usr/bin:/usr/lib/sdk/openjdk17/bin
JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17
build-args:
- --share=network
build-commands:
- mkdir -p /app/bin/image/
- cp -R bin /app/bin/image/
- cp -R lib /app/bin/image/
- cp -R conf /app/bin/image/
- cp -R legal /app/bin/image/
- cp -R release /app/bin/image/
- cp prediction.onnx img-ai/src/main/resources/dev/nuculabs/imagetagger/ai/
- cp prediction_categories.txt img-ai/src/main/resources/dev/nuculabs/imagetagger/ai/
- gradle jpackageImage
- mkdir -p /app/bin/
# Copy ImageTagger folder to /app/bin
- ls img-ui/build/jpackage
- cp -R img-ui/build/jpackage/ImageTagger /app/bin/
- ls /app/bin/
# Desktop Integration
- chmod +x /app/bin/image/lib/jexec
- chmod +x /app/bin/image/lib/jspawnhelper
- mkdir -p bin /app/share/{applications,icons/hicolor/512x512/apps,metainfo}
- mv ${FLATPAK_ID}.desktop /app/share/applications/${FLATPAK_ID}.desktop
- mv ${FLATPAK_ID}.png /app/share/icons/hicolor/512x512/apps/${FLATPAK_ID}.png
sources:
- type: git
url: https://github.com/dnutiu/ImageTagger
- type: archive
path: ../img-ui/build/distributions/ImageTagger-linux.zip
# url: "https://github.com/dnutiu/ImageTagger/releases/download/v1.3/ImageTagger-linux-1.3.zip"
# sha256: 0f086e6a738b3d59e3d05cce9174316d95886e50278c03e5b452a67fd264ea40
url: https://github.com/dnutiu/ImageTagger/releases/download/v1/AIModels.zip
sha256: "bbe80bf135621897bc6186d8f17b889064717ed3b9951702b33be869e522321c"
- type: file
path: dev.nuculabs.ImageTagger.png
- type: file

View file

@ -2,17 +2,8 @@
This directory contains the flatpak build files.
If you are on Linux and would like to install this application as a Flatpak image then
build the application with gradle:
```
# in parrent folder:
./gradlew jlinkZip
```
And execute:
If you are on Linux and would like to install this application as a Flatpak then execute
```shell
cd flatpak
./debug.sh
./build.sh
```

View file

@ -1,6 +1,6 @@
plugins {
id("java")
kotlin("jvm") version "1.8.22"
kotlin("jvm") version "1.9.25"
id("org.javamodularity.moduleplugin") version "1.8.12"
}

View file

@ -1,6 +1,6 @@
plugins {
id("java")
kotlin("jvm") version "1.8.22"
kotlin("jvm") version "1.9.25"
id("org.javamodularity.moduleplugin") version "1.8.12"
}

View file

@ -1,7 +1,7 @@
plugins {
id("java")
id("application")
id("org.jetbrains.kotlin.jvm") version "1.8.22"
id("org.jetbrains.kotlin.jvm") version "1.9.25"
id("org.javamodularity.moduleplugin") version "1.8.12"
id("org.openjfx.javafxplugin") version "0.0.13"
id("org.beryx.jlink") version "2.25.0"
@ -63,6 +63,9 @@ jlink {
unixScriptTemplate = project.file("${layout.projectDirectory}/src/main/resources/unixExecutableScriptTemplate.txt")
name = "ImageTagger"
}
jpackage {
icon = "${layout.projectDirectory}/src/main/resources/dev/nuculabs/imagetagger/ui/image-analysis.png"
}
}
tasks.jlinkZip {

View file

@ -18,7 +18,7 @@ module dev.nuculabs.imagetagger.ui {
requires org.apache.commons.lang3;
opens dev.nuculabs.imagetagger.ui to javafx.fxml, javafx.graphics;
opens dev.nuculabs.imagetagger.ui.controls to javafx.fxml, javafx.graphics;
opens dev.nuculabs.imagetagger.ui.controls to javafx.fxml, javafx.graphics, javafx.base;
opens dev.nuculabs.imagetagger.ui.pages to javafx.fxml, javafx.graphics;
exports dev.nuculabs.imagetagger.ui;
}

View file

@ -43,16 +43,13 @@ class MainPage : Application() {
// Set up the stage.
stage.title = "Image Tagger"
stage.scene = scene
stage.minWidth = 640.0
stage.minWidth = 960.0
stage.minHeight = 760.0
// Whe the main window is hidden we exit the application.
stage.setOnHidden {
Platform.exit()
}
// Add menu bar
// (scene.root as BorderPane).children.add(ApplicationMenuBar(mainPageController))
stage.show()
}

View file

@ -1,11 +1,14 @@
package dev.nuculabs.imagetagger.ui
import dev.nuculabs.imagetagger.core.AnalyzedImage
import dev.nuculabs.imagetagger.ui.controls.ImageTagsDisplayMode
import dev.nuculabs.imagetagger.ui.controls.ImageTagsEntryControl
import dev.nuculabs.imagetagger.ui.controls.ImageTagsSessionHeader
import javafx.application.Platform
import javafx.collections.FXCollections
import javafx.fxml.FXML
import javafx.scene.control.Button
import javafx.scene.control.ChoiceBox
import javafx.scene.control.ProgressBar
import javafx.scene.control.Separator
import javafx.scene.layout.HBox
@ -58,6 +61,16 @@ class MainPageController {
*/
private var isCurrentTagsOperationCancelled: Boolean = false
/**
* Holds a list of predicted images controls that are rendered in the view.
*/
private var predictedImages: MutableList<ImageTagsEntryControl> = ArrayList()
/**
* Controls how image tags are displayed on the screen.
*/
private var tagsDisplayMode: ImageTagsDisplayMode = ImageTagsDisplayMode.Comma
@FXML
private lateinit var progressBar: ProgressBar
@ -68,7 +81,10 @@ class MainPageController {
private lateinit var cancelButton: Button
@FXML
lateinit var tagImagesButton: Button
private lateinit var tagImagesButton: Button
@FXML
private lateinit var tagsDisplayModeSelection: ChoiceBox<String>
/**
* Initializes the controller. Needs to be called after the dependencies have been injected.
@ -76,6 +92,28 @@ class MainPageController {
fun initialize() {
HBox.setHgrow(progressBar, Priority.ALWAYS)
HBox.setHgrow(cancelButton, Priority.ALWAYS)
initializeTagsDisplayMode()
}
/**
* Initializes the tags display mode.
*/
private fun initializeTagsDisplayMode() {
// Tags display mode
tagsDisplayModeSelection.items = FXCollections.observableArrayList(
ImageTagsDisplayMode.entries.map { it.toString() }
)
tagsDisplayModeSelection.value = ImageTagsDisplayMode.default().toString()
tagsDisplayModeSelection.selectionModel.selectedItemProperty().addListener { _, oldValue, newValue ->
if (oldValue != newValue && newValue != null) {
tagsDisplayMode = ImageTagsDisplayMode.fromString(newValue)
predictedImages.forEach {
it.setTagsDisplayMode(tagsDisplayMode)
}
}
}
}
/**
@ -148,7 +186,10 @@ class MainPageController {
fun addNewImagePredictionEntry(
analyzedImage: AnalyzedImage,
) {
verticalBox.children.add(ImageTagsEntryControl(analyzedImage))
val control = ImageTagsEntryControl(analyzedImage)
control.setTagsDisplayMode(tagsDisplayMode)
verticalBox.children.add(control)
predictedImages.add(control)
verticalBox.children.add(Separator())
}

View file

@ -0,0 +1,34 @@
package dev.nuculabs.imagetagger.ui.controls
/**
* Determines how tags are displayed
*/
enum class ImageTagsDisplayMode {
Comma,
HashTags,
Space;
companion object {
/**
* Builds the enum value from a given string.
*
* @param value - The string
* @throws IllegalArgumentException when an invalid value is provided.
*/
fun fromString(value: String): ImageTagsDisplayMode {
return when (value) {
"Comma" -> Comma
"Space" -> Space
"HashTags" -> HashTags
else -> throw IllegalArgumentException("Invalid argument $value")
}
}
/**
* Returns the default tags display mode.
*/
fun default(): ImageTagsDisplayMode {
return Comma
}
}
}

View file

@ -2,11 +2,13 @@ package dev.nuculabs.imagetagger.ui.controls
import dev.nuculabs.imagetagger.core.AnalyzedImage
import dev.nuculabs.imagetagger.ui.alerts.ErrorAlert
import org.apache.commons.lang3.SystemUtils
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import javafx.fxml.FXML
import javafx.fxml.FXMLLoader
import javafx.scene.control.Button
import javafx.scene.control.Label
import javafx.scene.control.TableView
import javafx.scene.control.TextArea
import javafx.scene.image.Image
import javafx.scene.image.ImageView
@ -15,6 +17,7 @@ import javafx.scene.input.ClipboardContent
import javafx.scene.input.MouseEvent
import javafx.scene.layout.HBox
import javafx.scene.layout.VBox
import org.apache.commons.lang3.SystemUtils
import java.awt.Desktop
import java.io.File
import java.io.IOException
@ -40,6 +43,16 @@ class ImageTagsEntryControl(private val image: AnalyzedImage) : HBox() {
@FXML
private lateinit var predictedImageTags: TextArea
/**
* Holds the tags.
*/
private var tags: List<String> = ArrayList()
/**
* Sets the default image tags display mode.
*/
private var tagsDisplayMode: ImageTagsDisplayMode = ImageTagsDisplayMode.Comma
/**
* The file name label.
*/
@ -59,22 +72,7 @@ class ImageTagsEntryControl(private val image: AnalyzedImage) : HBox() {
private lateinit var metadataVbox: VBox
@FXML
private lateinit var metadataAuthor: Label
@FXML
private lateinit var metadataCamera: Label
@FXML
private lateinit var metadataLens: Label
@FXML
private lateinit var metadataISO: Label
@FXML
private lateinit var metadataAperture: Label
@FXML
private lateinit var metadataShutterSpeed: Label
private lateinit var metadataTableView: TableView<ImageTagsEntryModel>
init {
val resource = ImageTagsEntryControl::class.java.getResource("image-tags-entry.fxml")
@ -89,10 +87,10 @@ class ImageTagsEntryControl(private val image: AnalyzedImage) : HBox() {
}
setImage(image)
if (image.hasError()) {
setText(listOf(image.errorMessage()))
setTags(listOf(image.errorMessage()))
metadataVbox.isVisible = false
} else {
setText(image.tags())
setTags(image.tags())
setMetadata()
}
@ -122,8 +120,38 @@ class ImageTagsEntryControl(private val image: AnalyzedImage) : HBox() {
*
* @param predictions The prediction list.
*/
private fun setText(predictions: List<String>) {
predictedImageTags.text = predictions.joinToString { it }
fun setTags(predictions: List<String>) {
tags = predictions
updateTags()
}
/**
* Sets the tags display mode.
*
* @param mode The image tags display mode.
*/
fun setTagsDisplayMode(mode: ImageTagsDisplayMode) {
tagsDisplayMode = mode
updateTags()
}
/**
* Updates the tags text.
*/
private fun updateTags() {
predictedImageTags.text = when (tagsDisplayMode) {
ImageTagsDisplayMode.Comma -> {
tags.joinToString { it }
}
ImageTagsDisplayMode.HashTags -> {
tags.joinToString(separator = " ") {
"#${it}"
}
}
ImageTagsDisplayMode.Space -> {
tags.joinToString(separator = " ") { it }
}
}
}
/**
@ -131,12 +159,24 @@ class ImageTagsEntryControl(private val image: AnalyzedImage) : HBox() {
*/
private fun setMetadata() {
val imageMetadata = image.metadata()
metadataAuthor.text = "Author: ${imageMetadata.artist}"
metadataCamera.text = "Camera: ${imageMetadata.cameraBrand} ${imageMetadata.cameraModel}"
metadataLens.text = "Lens: ${imageMetadata.lensModel}"
metadataISO.text = "ISO: ${imageMetadata.iso}"
metadataAperture.text = "Aperture: ${imageMetadata.aperture}"
metadataShutterSpeed.text = "Shutter Speed: ${imageMetadata.shutterSpeed}"
val metadataValues = listOf(
ImageTagsEntryModel("Author", imageMetadata.artist),
ImageTagsEntryModel("Brand", imageMetadata.cameraBrand),
ImageTagsEntryModel("Model", imageMetadata.cameraModel),
ImageTagsEntryModel("Lens", imageMetadata.lensModel),
ImageTagsEntryModel("ISO", imageMetadata.iso),
ImageTagsEntryModel("Aperture", imageMetadata.aperture),
ImageTagsEntryModel("Shutter Speed", imageMetadata.shutterSpeed),
).filterNot { it.getValue() == "Unknown" }
val data: ObservableList<ImageTagsEntryModel> = FXCollections.observableArrayList(
metadataValues
)
if (data.size == 0) {
metadataVbox.isVisible = false
return
}
metadataTableView.items = data
}

View file

@ -0,0 +1,38 @@
@file:Suppress("unused")
package dev.nuculabs.imagetagger.ui.controls
import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
/**
* ImageTagsEntryModel represents a metadata and it's associated value.
*/
class ImageTagsEntryModel(metadata: String, value: String) {
private val metadata: StringProperty = SimpleStringProperty(metadata)
private val value: StringProperty = SimpleStringProperty(value)
fun getMetadata(): String {
return metadata.get()
}
fun setMetadata(metadata: String) {
this.metadata.set(metadata)
}
fun metadataProperty(): StringProperty {
return metadata
}
fun getValue(): String {
return value.get()
}
fun setValue(value: String) {
this.value.set(value)
}
fun valueProperty(): StringProperty {
return value
}
}

View file

@ -65,7 +65,8 @@ class ImageTagsSessionHeader : HBox() {
// update header title
val shortDate = dateTimeProvider.getTodayShortDate()
this.title.text = "$shortDate ($numberOfImages Images)"
val shortTime = dateTimeProvider.getTodayTime()
this.title.text = "$shortDate - $shortTime - ($numberOfImages Images)"
}
/**

View file

@ -9,6 +9,8 @@ interface IDateTimeProvider {
* Returns today's short date. For example: 09 April 2024
*/
fun getTodayShortDate(): String
fun getTodayTime(): String
}
/**
@ -24,4 +26,13 @@ class DateTimeProvider : IDateTimeProvider {
val date = Date()
return dateFormat.format(date)
}
/**
* Returns today's short time. Example: 15:30
*/
override fun getTodayTime(): String {
val dateFormat: DateFormat = SimpleDateFormat("HH:mm", Locale.ENGLISH)
val date = Date()
return dateFormat.format(date)
}
}

View file

@ -6,11 +6,13 @@
<?import javafx.scene.image.ImageView?>
<?import org.kordamp.ikonli.javafx.FontIcon?>
<fx:root type="javafx.scene.layout.HBox" xmlns:fx="http://javafx.com/fxml" stylesheets="@image-tags-entry.css">
<StackPane prefWidth="244" prefHeight="244">
<?import javafx.scene.control.cell.PropertyValueFactory?>
<fx:root type="javafx.scene.layout.HBox" xmlns:fx="http://javafx.com/fxml" stylesheets="@image-tags-entry.css"
minHeight="250">
<StackPane minWidth="244" prefWidth="244" prefHeight="244">
<ImageView fx:id="imageView"/>
</StackPane>
<VBox>
<VBox minWidth="300" >
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
</padding>
@ -20,17 +22,6 @@
<Label text="Tags:"/>
<TextArea fx:id="predictedImageTags" editable="false" wrapText="true" prefColumnCount="20"/>
</VBox>
<VBox fx:id="metadataVbox">
<padding>
<Insets left="10.0" top="35.0"/>
</padding>
<Label fx:id="metadataAuthor" text="Author: " />
<Label fx:id="metadataCamera" text="Camera:" />
<Label fx:id="metadataLens" text="Lens:" />
<Label fx:id="metadataISO" text="ISO:" />
<Label fx:id="metadataAperture" text="Aperture" />
<Label fx:id="metadataShutterSpeed" text="Shutter Speed" />
</VBox>
</HBox>
<HBox>
<padding>
@ -43,5 +34,20 @@
</Button>
</HBox>
</VBox>
<VBox fx:id="metadataVbox" VBox.vgrow="ALWAYS">
<TableView fx:id="metadataTableView" minWidth="400" maxWidth="Infinity" VBox.vgrow="ALWAYS">
<columns>
<TableColumn text="Metadata">
<cellValueFactory>
<PropertyValueFactory property="metadata"/>
</cellValueFactory>
</TableColumn>
<TableColumn text="Value">
<cellValueFactory>
<PropertyValueFactory property="value"/>
</cellValueFactory>
</TableColumn>
</columns>
</TableView>
</VBox>
</fx:root>

View file

@ -14,7 +14,7 @@
<top>
<VBox>
<ApplicationMenuBar/>
<HBox alignment="CENTER_LEFT">
<HBox alignment="CENTER_LEFT" spacing="10">
<padding>
<Insets bottom="5" left="5" right="5"/>
</padding>
@ -24,6 +24,12 @@
</graphic>
</Button>
<Separator orientation="VERTICAL" style="-fx-padding: 10px"/>
<Region HBox.hgrow="ALWAYS"/>
<ChoiceBox fx:id="tagsDisplayModeSelection">
<tooltip>
<Tooltip text="Select how tags are displayed"/>
</tooltip>
</ChoiceBox>
</HBox>
</VBox>
</top>

View file

@ -12,7 +12,7 @@
fx:controller="dev.nuculabs.imagetagger.ui.pages.AboutPage"
stylesheets="@about-page.css"
>
<VBox prefHeight="250.0" maxWidth="350" spacing="10">
<VBox prefHeight="350.0" maxWidth="350" spacing="10">
<padding>
<Insets top="25" bottom="25" right="15" left="15"/>
</padding>

View file

@ -49,14 +49,30 @@ To run:
gradlew run
```
### Building the FlatPak
### Building the Flatpak
To build the Flatpak run the following commands:
```shell
gradlew jlinkZip
cd flatpak
flatpak-builder --sandbox --user --install --force-clean build-dir dev.nuculabs.ImageTagger.yaml
./build.sh
```
It will build the flatpak using the latest sources from this repo.
### Building a package (Fedora Example)
To build a package run
```shell
gradle jpackage <<< "--type rpm"
```
To install and run the application:
```shell
dnf install ./img-ui/build/jpackage/imagetagger-1.0-1.x86_64.rpm
/opt/imagetagger/bin/ImageTagger
```
# Blog
@ -66,4 +82,5 @@ You can visit my tech blog at [https://blog.nuculabs.dev](https://blog.nuculabs.
# Credits
- Icons: <a href="https://www.flaticon.com/free-icons/image-analysis" title="image analysis icons">Image analysis icons
created by Dewi Sari - Flaticon</a>
created by Dewi Sari - Flaticon</a>

View file

@ -1,7 +1,7 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "ImageTagger-Solution"
rootProject.name = "ImageTagger"
include("img-ui")
include("img-ai")
include("img-core")