Compare commits

..

No commits in common. "master" and "session-header" have entirely different histories.

48 changed files with 577 additions and 903 deletions

14
.github/FUNDING.yml vendored
View file

@ -1,14 +0,0 @@
# These are supported funding model platforms
github: # dnutiu
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: nuculabs
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

2
.gitignore vendored
View file

@ -40,5 +40,3 @@ bin/
### Mac OS ###
.DS_Store
/.idea/workspace.xml
/.idea/copilot

1
.idea/.name Normal file
View file

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

View file

@ -9,7 +9,6 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/img-ai" />
<option value="$PROJECT_DIR$/img-core" />
<option value="$PROJECT_DIR$/img-ui" />
</set>
</option>

View file

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

View file

@ -1,9 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EntryPointsManager">
<list size="1">
<item index="0" class="java.lang.String" itemvalue="javafx.fxml.FXML" />
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />

334
.idea/workspace.xml Normal file
View file

@ -0,0 +1,334 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="dac8f85c-5bd6-4e05-9520-9e6a91c7f78e" name="Changes" comment="add module-info.java">
<change beforePath="$PROJECT_DIR$/readme.md" beforeDir="false" afterPath="$PROJECT_DIR$/readme.md" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ExternalProjectsData">
<projectState path="$PROJECT_DIR$">
<ProjectState />
</projectState>
<projectState path="$PROJECT_DIR$/img-ui">
<ProjectState />
</projectState>
</component>
<component name="ExternalProjectsManager">
<system id="GRADLE">
<state>
<task path="$PROJECT_DIR$/img-ui">
<activation />
</task>
<task path="$PROJECT_DIR$">
<activation />
</task>
<task path="$PROJECT_DIR$/ImageTagger">
<activation />
</task>
<task path="$PROJECT_DIR$/img-ai">
<activation />
</task>
<projects_view>
<tree_state>
<expand>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="ImageTagger-Solution" type="f1a62948:ProjectNode" />
</path>
</expand>
<select />
</tree_state>
</projects_view>
</state>
</system>
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Class" />
<option value="module-info" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 5
}</component>
<component name="ProjectId" id="2eOxGB0t00dWfHWsgfDybOaUUBb" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Gradle.Build img-ui.executor": "Run",
"Gradle.ImageTagger [build].executor": "Run",
"Gradle.ImageTagger:img-ai [test].executor": "Run",
"Gradle.ImageTagger:img-ui [clean].executor": "Run",
"Gradle.ImageTagger:img-ui [jlinkZip].executor": "Run",
"Gradle.ImageTagsPredictionTests.executor": "Run",
"Gradle.img-ui [build].executor": "Run",
"Gradle.img-ui [run].executor": "Run",
"Kotlin.MainPageKt.executor": "Debug",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"dart.analysis.tool.window.visible": "false",
"git-widget-placeholder": "multi-project",
"jdk.selected.JAVA_MODULE": "17",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "/Users/dnutiu/Projects/ImageTagger/img-ai/src/test/resources/dev/nuculabs/imagetagger/ai",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"project.structure.last.edited": "Modules",
"project.structure.proportion": "0.15",
"project.structure.side.proportion": "0.29070836",
"settings.editor.selected.configurable": "reference.settingsdialog.project.gradle",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/img-ai/src/test/resources/dev/nuculabs/imagetagger/ai" />
<recent name="$PROJECT_DIR$/img-ai/src/main/resources/dev.nuculabs.imagetagger.ai" />
<recent name="$PROJECT_DIR$/img-ui" />
<recent name="$PROJECT_DIR$" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/img-ai/src/main/resources/dev/nuculabs/imagetagger/ai" />
<recent name="$PROJECT_DIR$" />
</key>
<key name="MoveKotlinTopLevelDeclarationsDialog.RECENTS_KEY">
<recent name="dev.nuculabs.imagetagger.ai" />
</key>
<key name="CopyKotlinDeclarationDialog.RECENTS_KEY">
<recent name="" />
</key>
</component>
<component name="RunManager" selected="Gradle.ImageTagger:img-ui [jlinkZip]">
<configuration name="ImageTagger:img-ui [clean]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/img-ui" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="clean" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
<configuration name="ImageTagger:img-ui [jlinkZip]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/img-ui" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="jlinkZip" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
<configuration name="ImageTagsPredictionTests" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":img-ai:test" />
<option value="--tests" />
<option value="&quot;dev.nuculabs.imagetagger.ai.ImageTagsPredictionTests&quot;" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>true</RunAsTest>
<method v="2" />
</configuration>
<configuration name="img-ui [build]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/img-ui" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="build" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
<configuration name="img-ui [run]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/img-ui" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="run" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<RunAsTest>false</RunAsTest>
<method v="2" />
</configuration>
<recent_temporary>
<list>
<item itemvalue="Gradle.ImageTagger:img-ui [jlinkZip]" />
<item itemvalue="Gradle.ImageTagger:img-ui [clean]" />
<item itemvalue="Gradle.img-ui [run]" />
<item itemvalue="Gradle.ImageTagsPredictionTests" />
<item itemvalue="Gradle.img-ui [build]" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="jdk-17.0.10-corretto-17.0.10-4caba194b151-53826d6a" />
<option value="jdk-17.0.9-corretto-17.0.9-4caba194b151-92e50af3" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="dac8f85c-5bd6-4e05-9520-9e6a91c7f78e" name="Changes" comment="" />
<created>1711789328334</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1711789328334</updated>
<workItem from="1711789330084" duration="2109000" />
<workItem from="1711791444105" duration="4705000" />
</task>
<task id="LOCAL-00001" summary="split ImageTagger into subproject img-ui">
<option name="closed" value="true" />
<created>1711790450344</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1711790450344</updated>
</task>
<task id="LOCAL-00002" summary="update readme.md">
<option name="closed" value="true" />
<created>1711790617285</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1711790617285</updated>
</task>
<task id="LOCAL-00003" summary="update gradle config">
<option name="closed" value="true" />
<created>1711791812195</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1711791812195</updated>
</task>
<task id="LOCAL-00004" summary="extract common ai-related functionality into img-ai">
<option name="closed" value="true" />
<created>1711793501074</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1711793501074</updated>
</task>
<task id="LOCAL-00005" summary="add module-info.java">
<option name="closed" value="true" />
<created>1711796896478</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1711796896478</updated>
</task>
<option name="localTasksCounter" value="6" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="split ImageTagger into subproject img-ui" />
<MESSAGE value="update readme.md" />
<MESSAGE value="update gradle config" />
<MESSAGE value="extract common ai-related functionality into img-ai" />
<MESSAGE value="add module-info.java" />
<option name="LAST_COMMIT_MESSAGE" value="add module-info.java" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<breakpoints>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/img-ui/src/main/kotlin/dev/nuculabs/imagetagger/ui/MainPage.kt</url>
<line>70</line>
<option name="timeStamp" value="1" />
</line-breakpoint>
</breakpoints>
</breakpoint-manager>
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
<select />
</component>
</project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 KiB

After

Width:  |  Height:  |  Size: 316 KiB

3
flatpak/.gitignore vendored
View file

@ -1,3 +0,0 @@
build-dir
.idea
.flatpak-builder

View file

@ -1,3 +0,0 @@
# 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,9 +0,0 @@
[Desktop Entry]
Name=ImageTagger
Exec=/app/bin/ImageTagger/bin/ImageTagger
Terminal=false
Type=Application
Icon=dev.nuculabs.ImageTagger
StartupWMClass=ImageTagger
Comment=Image Tagger
Categories=Utility;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View file

@ -1,49 +0,0 @@
id: dev.nuculabs.ImageTagger
runtime: org.freedesktop.Platform
runtime-version: '23.08'
sdk: org.freedesktop.Sdk
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
# GPU acceleration if needed
- --device=dri
# Needs to save files locally
- --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:
- 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
- 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
url: https://github.com/dnutiu/ImageTagger/releases/download/v1/AIModels.zip
sha256: "bbe80bf135621897bc6186d8f17b889064717ed3b9951702b33be869e522321c"
- type: file
path: dev.nuculabs.ImageTagger.png
- type: file
path: dev.nuculabs.ImageTagger.desktop

View file

@ -1,9 +0,0 @@
# Flatpak
This directory contains the flatpak build files.
If you are on Linux and would like to install this application as a Flatpak then execute
```shell
./build.sh
```

View file

@ -1,20 +1,19 @@
plugins {
id("java")
kotlin("jvm") version "1.9.25"
kotlin("jvm") version "1.8.22"
id("org.javamodularity.moduleplugin") version "1.8.12"
}
var junitVersion = "5.10.0"
group = "dev.nuculabs.imagetagger.ai"
version = "1.4"
version = "1.1"
repositories {
mavenCentral()
}
dependencies {
implementation(project(":img-core"))
implementation("com.microsoft.onnxruntime:onnxruntime:1.17.1")
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")

View file

@ -3,6 +3,5 @@ module dev.nuculabs.imagetagger.ai {
requires java.desktop;
requires java.logging;
requires kotlin.stdlib;
requires dev.nuculabs.imagetagger.core;
exports dev.nuculabs.imagetagger.ai;
}

View file

@ -1,4 +1,4 @@
package dev.nuculabs.imagetagger.core.abstractions
package dev.nuculabs.imagetagger.ai
import java.awt.image.BufferedImage
import java.io.InputStream

View file

@ -3,7 +3,6 @@ package dev.nuculabs.imagetagger.ai
import ai.onnxruntime.OnnxTensor
import ai.onnxruntime.OrtEnvironment
import ai.onnxruntime.OrtSession
import dev.nuculabs.imagetagger.core.abstractions.IImageTagsPrediction
import java.awt.image.BufferedImage
import java.io.Closeable
import java.io.IOException

View file

@ -1,21 +0,0 @@
plugins {
id("java")
kotlin("jvm") version "1.9.25"
id("org.javamodularity.moduleplugin") version "1.8.12"
}
group = "dev.nuculabs.imagetagger.core"
version = "1.4"
repositories {
mavenCentral()
}
dependencies {
testImplementation(kotlin("test"))
implementation("com.drewnoakes:metadata-extractor:2.19.0")
}
tasks.test {
useJUnitPlatform()
}

View file

@ -1,8 +0,0 @@
module dev.nuculabs.imagetagger.core {
requires kotlin.stdlib;
requires java.desktop;
requires java.logging;
requires metadata.extractor;
exports dev.nuculabs.imagetagger.core;
exports dev.nuculabs.imagetagger.core.abstractions;
}

View file

@ -1,80 +0,0 @@
package dev.nuculabs.imagetagger.core
import dev.nuculabs.imagetagger.core.abstractions.IImageTagsPrediction
import java.awt.image.BufferedImage
import java.io.File
import java.util.logging.Logger
import javax.imageio.ImageIO
/**
* AnalyzedImage represents an Analyzed Image inside the ImageTagger application.
*/
class AnalyzedImage(private val file: File, imageTagsPrediction: IImageTagsPrediction) {
private var imageFile: File = file
private var bufferedImage: BufferedImage? = null
private var predictedTags: List<String> = emptyList()
private val logger: Logger = Logger.getLogger("AnalyzedImage")
private lateinit var imageMetadata: IImageMetadata
private var error: String = ""
private var hasError: Boolean = false
/**
* Initializes the analyzed image and predicts its tags.
*/
init {
try {
imageFile = File(file.absolutePath)
bufferedImage = ImageIO.read(imageFile)
predictedTags = imageTagsPrediction.predictTags(bufferedImage!!)
imageMetadata = ImageMetadata(imageFile)
} catch (e: NullPointerException) {
val message = "Error while predicting image: invalid image type or type not supported."
logger.warning(message)
hasError = true
error = message
} catch (e: Exception) {
val message = "Error loading image $e"
logger.warning(message)
hasError = true
error = e.message.toString()
}
}
/**
* Returns an image's metadata
*/
fun metadata(): IImageMetadata {
return imageMetadata
}
/**
* Returns a boolean indicating if the image analysis has errors.
* The flag is `True` if it has errors, `False` otherwise.
*/
fun hasError(): Boolean {
return hasError
}
/**
* Returns the prediction error
*/
fun errorMessage(): String {
return error
}
/**
* Returns the absolute file path of the image.
*/
fun absolutePath(): String {
return file.absolutePath
}
/**
* Returns the predicted tags.
*/
fun tags(): List<String> {
return predictedTags
}
}

View file

@ -1,111 +0,0 @@
package dev.nuculabs.imagetagger.core
import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.Metadata
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.exif.ExifSubIFDDirectory
import java.io.File
/**
* Interface that holds common image metadata objects used by this application.
*/
interface IImageMetadata {
val artist: String
val aperture: String
val shutterSpeed: String
val iso: String
val lensModel: String
val cameraModel: String
val cameraBrand: String
}
/**
* An image metadata provider that uses
*/
class ImageMetadata internal constructor(file: File) : IImageMetadata {
private var metadata: Metadata? = null
private lateinit var _cameraBrand: String
private lateinit var _cameraModel: String
private lateinit var _lensModel: String
private lateinit var _artist: String
private lateinit var _aperture: String
private lateinit var _iso: String
private lateinit var _shutterSpeed: String
init {
metadata = ImageMetadataReader.readMetadata(file)
if (metadata != null) {
val exifDirectory = metadata?.getFirstDirectoryOfType(ExifIFD0Directory::class.java)
val exifSubDirectory = metadata?.getFirstDirectoryOfType(ExifSubIFDDirectory::class.java)
_cameraBrand = exifDirectory?.getString(ExifIFD0Directory.TAG_MAKE) ?: "Unknown"
_cameraModel = exifDirectory?.getString(ExifIFD0Directory.TAG_MODEL) ?: "Unknown"
_lensModel = exifSubDirectory?.getString(ExifIFD0Directory.TAG_LENS_MODEL) ?: "Unknown"
_artist = exifDirectory?.getString(ExifIFD0Directory.TAG_ARTIST) ?: "Unknown"
_aperture = exifSubDirectory?.getString(ExifIFD0Directory.TAG_FNUMBER) ?: "Unknown"
_iso = exifSubDirectory?.getString(ExifIFD0Directory.TAG_ISO_EQUIVALENT) ?: "Unknown"
_shutterSpeed = exifSubDirectory?.getString(ExifIFD0Directory.TAG_EXPOSURE_TIME) ?: "Unknown"
}
}
override val cameraBrand: String
get() {
if (metadata == null) {
return "Unknown"
}
return _cameraBrand
}
override val cameraModel: String
get() {
if (metadata == null) {
return "Unknown"
}
return _cameraModel
}
override val lensModel: String
get() {
if (metadata == null) {
return "Unknown"
}
return _lensModel
}
override val artist: String
get() {
if (metadata == null) {
return "Unknown"
}
return _artist
}
override val aperture: String
get() {
if (metadata == null) {
return "Unknown"
}
return _aperture
}
override val iso: String
get() {
if (metadata == null) {
return "Unknown"
}
return _iso
}
override val shutterSpeed: String
get() {
if (metadata == null) {
return "Unknown"
}
return _shutterSpeed
}
override fun toString(): String {
return "ImageMetadata(cameraBrand='$cameraBrand', cameraModel='$cameraModel', lensModel='$lensModel', artist='$artist', aperture='$aperture', iso='$iso', shutterSpeed='$shutterSpeed')"
}
}

69
img-ui/build.gradle Normal file
View file

@ -0,0 +1,69 @@
plugins {
id 'java'
id 'application'
id 'org.jetbrains.kotlin.jvm' version '1.8.22'
id 'org.javamodularity.moduleplugin' version '1.8.12'
id 'org.openjfx.javafxplugin' version '0.0.13'
id 'org.beryx.jlink' version '2.25.0'
}
group 'com.nuculabs.dev'
version '1.1'
repositories {
mavenCentral()
}
ext {
junitVersion = '5.10.0'
}
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
application {
mainModule = 'dev.nuculabs.imagetagger.ui'
mainClass = 'dev.nuculabs.imagetagger.ui.MainPage'
}
kotlin {
jvmToolchain( 17 )
}
javafx {
version = '21'
modules = ['javafx.controls', 'javafx.fxml']
}
dependencies {
implementation(project(":img-ai"))
implementation('org.controlsfx:controlsfx:11.1.2')
implementation('com.dlsc.formsfx:formsfx-core:11.6.0') {
exclude(group: 'org.openjfx')
}
implementation('net.synedra:validatorfx:0.4.0') {
exclude(group: 'org.openjfx')
}
implementation('org.kordamp.ikonli:ikonli-javafx:12.3.1')
implementation('org.kordamp.ikonli:ikonli-fontawesome5-pack:12.3.1')
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1-Beta")
testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
}
test {
useJUnitPlatform()
}
jlink {
imageZip = project.file("${buildDir}/distributions/ImageTagger-${javafx.platform.classifier}-${version}.zip")
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
launcher {
name = 'ImageTagger'
}
}
jlinkZip {
group = 'distribution'
}

View file

@ -1,73 +0,0 @@
plugins {
id("java")
id("application")
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"
}
group "com.nuculabs.dev"
version "1.4"
repositories {
mavenCentral()
}
tasks.withType(JavaCompile::class.java) {
options.encoding = "UTF-8"
}
application {
mainModule = "dev.nuculabs.imagetagger.ui"
mainClass = "dev.nuculabs.imagetagger.ui.MainPage"
}
kotlin {
jvmToolchain(17)
}
javafx {
version = "21"
modules = listOf("javafx.controls", "javafx.fxml")
}
dependencies {
implementation(project(":img-ai"))
implementation(project(":img-core"))
implementation("org.controlsfx:controlsfx:11.1.2")
implementation("com.dlsc.formsfx:formsfx-core:11.6.0") {
exclude("org.openjfx")
}
implementation("net.synedra:validatorfx:0.4.0") {
exclude("org.openjfx")
}
implementation("org.kordamp.ikonli:ikonli-javafx:12.3.1")
implementation("org.kordamp.ikonli:ikonli-fontawesome5-pack:12.3.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1-Beta")
implementation("org.apache.commons:commons-lang3:3.14.0")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0")
}
tasks.test {
useJUnitPlatform()
}
jlink {
val buildDirectory = layout.buildDirectory.asFile.get().absolutePath
imageZip = project.file("$buildDirectory/distributions/ImageTagger-${javafx.platform.classifier}.zip")
options = listOf("--strip-debug", "--compress", "2", "--no-header-files", "--no-man-pages")
launcher {
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 {
group = "distribution"
}

View file

@ -14,11 +14,9 @@ module dev.nuculabs.imagetagger.ui {
requires org.kordamp.ikonli.fontawesome5;
requires kotlinx.coroutines.core;
requires dev.nuculabs.imagetagger.ai;
requires dev.nuculabs.imagetagger.core;
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, javafx.base;
opens dev.nuculabs.imagetagger.ui.controls to javafx.fxml, javafx.graphics;
opens dev.nuculabs.imagetagger.ui.pages to javafx.fxml, javafx.graphics;
exports dev.nuculabs.imagetagger.ui;
}

View file

@ -1,6 +1,6 @@
package dev.nuculabs.imagetagger.ui
import dev.nuculabs.imagetagger.core.abstractions.IImageTagsPrediction
import dev.nuculabs.imagetagger.ai.IImageTagsPrediction
/**
* BasicServiceLocator is implemented to avoid polluting the apps with singletons.
@ -10,7 +10,6 @@ import dev.nuculabs.imagetagger.core.abstractions.IImageTagsPrediction
*/
class BasicServiceLocator private constructor() {
internal lateinit var imageTagsPrediction: IImageTagsPrediction
internal lateinit var mainPageController: MainPageController
// Singleton Pattern
companion object {

View file

@ -1,12 +1,14 @@
package dev.nuculabs.imagetagger.ui
import dev.nuculabs.imagetagger.core.abstractions.IImageTagsPrediction
import dev.nuculabs.imagetagger.ai.IImageTagsPrediction
import dev.nuculabs.imagetagger.ai.ImageTagsPrediction
import dev.nuculabs.imagetagger.ui.controls.programatic.ApplicationMenuBar
import javafx.application.Application
import javafx.application.Platform
import javafx.fxml.FXMLLoader
import javafx.scene.Scene
import javafx.scene.image.Image
import javafx.scene.layout.BorderPane
import javafx.stage.Stage
import java.awt.Taskbar
import java.awt.Toolkit
@ -32,24 +34,25 @@ class MainPage : Application() {
setUpApplicationIcon()
// Load the FXML.
val scene = Scene(fxmlLoader.load(), 740.0, 760.0)
val scene = Scene(fxmlLoader.load(), 640.0, 760.0)
// Initialize the controller.
val mainPageController = fxmlLoader.getController<MainPageController>()
mainPageController.initialize()
// Set MainPage controller.
serviceLocator.mainPageController = fxmlLoader.getController()
// Set up the stage.
stage.title = "Image Tagger"
stage.scene = scene
stage.minWidth = 960.0
stage.minWidth = 640.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,25 +1,23 @@
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
import javafx.scene.layout.Priority
import javafx.scene.layout.VBox
import javafx.stage.FileChooser
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.concurrent.Semaphore
import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Logger
import javax.imageio.ImageIO
class MainPageController {
@ -36,7 +34,7 @@ class MainPageController {
private val maxImagesPredictionInProgress = Runtime.getRuntime().availableProcessors()
/**
* Semaphore to limit the maximum number of predictions submitted to the tread pool.
* Semaphore to limit the maximum amount of predictions submitted to the tread pool.
*/
private val workerSemaphore: Semaphore = Semaphore(maxImagesPredictionInProgress)
@ -56,21 +54,11 @@ class MainPageController {
private val imageTagsPrediction = BasicServiceLocator.getInstance().imageTagsPrediction
/**
* A boolean that when set to, true it will stop the current image tagging operation.
* When a new operation is started, the boolean is reset to false.
* A boolean that when set to true it will stop the current image tagging operation.
* When a new operation is started the boolean is reset to false.
*/
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
@ -80,40 +68,12 @@ class MainPageController {
@FXML
private lateinit var cancelButton: Button
@FXML
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.
*/
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)
}
}
}
}
/**
@ -124,10 +84,7 @@ class MainPageController {
synchronized(this) {
val fileChooser = FileChooser().apply { title = "Choose images" }
val filePaths = fileChooser.showOpenMultipleDialog(null) ?: return
if (tagImagesButton.isDisable) {
return
}
tagImagesButton.isDisable = true
isCurrentTagsOperationCancelled = false
progressBar.isVisible = true
cancelButton.isVisible = true
@ -155,12 +112,19 @@ class MainPageController {
return@submit
}
val analyzedImage = AnalyzedImage(filePath, this.imageTagsPrediction)
workerSemaphore.release()
Platform.runLater {
// Add image and prediction to the view.
addNewImagePredictionEntry(analyzedImage)
updateProgressBar()
predictImageTags(
filePath,
onError = {
workerSemaphore.release()
}
) { imagePath, imageTags ->
// Add newly predicted tags to UI.
Platform.runLater {
// Add image and prediction to the view.
addNewImagePredictionEntry(imagePath, imageTags)
updateProgressBar()
workerSemaphore.release()
}
}
}
}
@ -178,18 +142,38 @@ class MainPageController {
cancelButton.isVisible = false
}
/**
* Predicts an image tags and executes an action with it.
*
* @param filePath - The image file's absolute path.
*/
fun predictImageTags(
filePath: File,
onError: (Exception) -> Unit,
onSuccess: (String, List<String>) -> Unit
) {
try {
// Get predictions for the image.
val imageFile = ImageIO.read(File(filePath.absolutePath))
val tags: List<String> = imageTagsPrediction.predictTags(imageFile)
onSuccess(filePath.absolutePath, tags)
} catch (e: Exception) {
logger.warning("Error while predicting images $e")
onError(e)
}
}
/**
* Updates the UI with a new ImagePredictionEntry.
*
* @param analyzedImage - The analyzed image instance.
* @param imagePath - The image path.
* @param imageTags - The image's tags.
*/
fun addNewImagePredictionEntry(
analyzedImage: AnalyzedImage,
imagePath: String,
imageTags: List<String>,
) {
val control = ImageTagsEntryControl(analyzedImage)
control.setTagsDisplayMode(tagsDisplayMode)
verticalBox.children.add(control)
predictedImages.add(control)
verticalBox.children.add(ImageTagsEntryControl(imagePath, imageTags))
verticalBox.children.add(Separator())
}
@ -207,16 +191,13 @@ class MainPageController {
* Updates the progress bar of the UI.
*/
fun updateProgressBar() {
synchronized(this) {
logger.info("Progress ${processedImageFilesCount.get()}/${imageFilesTotal} ${progressBar.progress}")
val processedImages = processedImageFilesCount.incrementAndGet()
progressBar.progress = ((processedImages * 100) / imageFilesTotal).toDouble() / 100.0
if (processedImageFilesCount.get() == imageFilesTotal) {
progressBar.isVisible = false
cancelButton.isVisible = false
tagImagesButton.isDisable = false
logger.info("Finished processing images.")
}
logger.info("Progress ${processedImageFilesCount.get()}/${imageFilesTotal} ${progressBar.progress}")
progressBar.progress =
((processedImageFilesCount.incrementAndGet() * 100) / imageFilesTotal).toDouble() / 100.0
if (processedImageFilesCount.get() == imageFilesTotal) {
progressBar.isVisible = false
cancelButton.isVisible = false
logger.info("Finished processing images.")
}
}

View file

@ -1,51 +0,0 @@
package dev.nuculabs.imagetagger.ui.controls
import dev.nuculabs.imagetagger.ui.BasicServiceLocator
import dev.nuculabs.imagetagger.ui.MainPageController
import dev.nuculabs.imagetagger.ui.pages.AboutPage
import javafx.fxml.FXML
import javafx.fxml.FXMLLoader
import javafx.scene.control.MenuBar
import java.io.IOException
/**
* Used as the application menu bar.
*/
class ApplicationMenuBar : MenuBar() {
private val mainPageController: MainPageController by lazy {
BasicServiceLocator.getInstance().mainPageController
}
init {
// Load component
val fxmlLoader = FXMLLoader(
ImageTagsSessionHeader::class.java.getResource("application-menu-bar.fxml")
)
fxmlLoader.setRoot(this)
fxmlLoader.setController(this)
try {
fxmlLoader.load()
} catch (exception: IOException) {
throw RuntimeException(exception)
}
useSystemMenuBarProperty().set(false)
}
/**
* Calls the tag images functionality from the main controller.
* The controller is retrieved in a lazy-like fashion.
*/
@FXML
private fun fileMenuTagImages() {
mainPageController.onTagImagesButtonClick()
}
/**
* Shows the about dialog.
*/
@FXML
private fun aboutMenuShowAbout() {
AboutPage.show()
}
}

View file

@ -1,34 +0,0 @@
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

@ -1,14 +1,10 @@
package dev.nuculabs.imagetagger.ui.controls
import dev.nuculabs.imagetagger.core.AnalyzedImage
import dev.nuculabs.imagetagger.ui.alerts.ErrorAlert
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
@ -16,19 +12,16 @@ import javafx.scene.input.Clipboard
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
import java.io.InputStreamReader
import java.util.logging.Logger
/**
* This class is used to create a custom control for the image prediction entry.
*/
class ImageTagsEntryControl(private val image: AnalyzedImage) : HBox() {
class ImageTagsEntryControl(private val imagePath: String, predictions: List<String>) : HBox() {
private val logger: Logger = Logger.getLogger("ImageTagsEntryControl")
/**
@ -43,16 +36,6 @@ 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.
*/
@ -65,15 +48,6 @@ class ImageTagsEntryControl(private val image: AnalyzedImage) : HBox() {
@FXML
private lateinit var copyTagsButton: Button
/**
* Metadata related UI fields.
*/
@FXML
private lateinit var metadataVbox: VBox
@FXML
private lateinit var metadataTableView: TableView<ImageTagsEntryModel>
init {
val resource = ImageTagsEntryControl::class.java.getResource("image-tags-entry.fxml")
logger.fine("Using resource URL: $resource")
@ -85,15 +59,8 @@ class ImageTagsEntryControl(private val image: AnalyzedImage) : HBox() {
} catch (exception: IOException) {
throw RuntimeException(exception)
}
setImage(image)
if (image.hasError()) {
setTags(listOf(image.errorMessage()))
metadataVbox.isVisible = false
} else {
setTags(image.tags())
setMetadata()
}
setImage(imagePath)
setText(predictions)
setupEventHandlers()
}
@ -120,83 +87,20 @@ class ImageTagsEntryControl(private val image: AnalyzedImage) : HBox() {
*
* @param predictions The prediction list.
*/
fun setTags(predictions: List<String>) {
tags = predictions
updateTags()
private fun setText(predictions: List<String>) {
predictedImageTags.text = predictions.joinToString { it }
}
/**
* 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 }
}
}
}
/**
* Sets and displays the image metadata.
*/
private fun setMetadata() {
val imageMetadata = image.metadata()
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
}
/**
* Setter for setting the image.
*/
private fun setImage(analyzedImage: AnalyzedImage) {
val file = File(analyzedImage.absolutePath())
if (analyzedImage.hasError()) {
fileNameLabel.text = "Failed: ${file.name}"
fileNameLabel.styleClass.add("errorLabel")
imageView.image = Image(this.javaClass.getResourceAsStream("images/failed.png"))
private fun setImage(imagePath: String) {
val file = File(imagePath)
file.inputStream().use {
imageView.image = Image(it, 244.0, 244.0, true, true)
imageView.isCache = true
} else {
file.inputStream().use {
imageView.image = Image(it, 244.0, 244.0, true, true)
imageView.isCache = true
}
fileNameLabel.text = "File: ${file.name}"
}
fileNameLabel.text = "File: ${file.name}"
}
/**
@ -204,22 +108,14 @@ class ImageTagsEntryControl(private val image: AnalyzedImage) : HBox() {
* If the operation fails it will display an error alert.
*/
fun onOpenImageClick() {
if (image.hasError()) {
return
}
if (SystemUtils.IS_OS_LINUX) {
val status = Runtime.getRuntime().exec(arrayOf("xdg-open", image.absolutePath()))
logger.fine(InputStreamReader(status.errorStream).readText())
} else {
if (Desktop.isDesktopSupported()) {
val desktop = Desktop.getDesktop()
if (desktop.isSupported(Desktop.Action.OPEN)) {
desktop.open(File(image.absolutePath()))
}
} else {
logger.severe("Cannot open image ${image.absolutePath()}. Desktop action not supported!")
ErrorAlert("Can't open file: ${image.absolutePath()}\nOperation is not supported!")
if (Desktop.isDesktopSupported()) {
val desktop = Desktop.getDesktop()
if (desktop.isSupported(Desktop.Action.OPEN)) {
desktop.open(File(imagePath))
}
} else {
logger.severe("Cannot open image $imagePath. Desktop action not supported!")
ErrorAlert("Can't open file: $imagePath\nOperation is not supported!")
}
}

View file

@ -1,38 +0,0 @@
@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

@ -8,7 +8,6 @@ import javafx.fxml.FXMLLoader
import javafx.scene.control.Button
import javafx.scene.control.Label
import javafx.scene.layout.HBox
import org.apache.commons.lang3.SystemUtils
import java.awt.Desktop
import java.io.File
import java.io.IOException
@ -46,9 +45,6 @@ class ImageTagsSessionHeader : HBox() {
}
updateHeader(0)
openDirectoryButton.setOnAction {
openDirectory()
}
}
/**
@ -65,30 +61,25 @@ class ImageTagsSessionHeader : HBox() {
// update header title
val shortDate = dateTimeProvider.getTodayShortDate()
val shortTime = dateTimeProvider.getTodayTime()
this.title.text = "$shortDate - $shortTime - ($numberOfImages Images)"
this.title.text = "$shortDate ($numberOfImages Images)"
}
/**
* Opens the directory in the user's Desktop.
*/
fun openDirectory() {
directoryPath?.let {
if (SystemUtils.IS_OS_LINUX) {
Runtime.getRuntime().exec("xdg-open $directoryPath")
} else {
if (Desktop.isDesktopSupported()) {
val desktop = Desktop.getDesktop()
if (desktop.isSupported(Desktop.Action.OPEN)) {
desktop.open(File(it))
} else {
logger.severe("Cannot open image directory $it. Desktop action not supported!")
ErrorAlert("Can't open file: $it\nOperation is not supported!")
}
this.directoryPath?.let {
if (Desktop.isDesktopSupported()) {
val desktop = Desktop.getDesktop()
if (desktop.isSupported(Desktop.Action.OPEN)) {
desktop.open(File(it))
} else {
logger.severe("Cannot open image directory $it. Desktop action not supported!")
ErrorAlert("Can't open file: $it\nOperation is not supported!")
}
} else {
logger.severe("Cannot open image directory $it. Desktop action not supported!")
ErrorAlert("Can't open file: $it\nOperation is not supported!")
}
}
}

View file

@ -0,0 +1,38 @@
package dev.nuculabs.imagetagger.ui.controls.programatic
import dev.nuculabs.imagetagger.ui.MainPageController
import dev.nuculabs.imagetagger.ui.pages.AboutPage
import javafx.scene.control.Menu
import javafx.scene.control.MenuBar
import javafx.scene.control.MenuItem
/**
* Used as the application menu bar.
*/
class ApplicationMenuBar(private val mainPageController: MainPageController) : MenuBar() {
private val fileMenu = Menu("File")
private val aboutMenu = Menu("About")
init {
useSystemMenuBarProperty().set(true)
menus.addAll(fileMenu, aboutMenu)
setupFileMenu()
setupAboutMenu()
}
private fun setupAboutMenu() {
val aboutMenuItem = MenuItem("About")
aboutMenuItem.setOnAction {
AboutPage.show()
}
aboutMenu.items.add(aboutMenuItem)
}
private fun setupFileMenu() {
val tagImagesMenuItem = MenuItem("Tag Images")
tagImagesMenuItem.setOnAction {
mainPageController.onTagImagesButtonClick()
}
fileMenu.items.add(tagImagesMenuItem)
}
}

View file

@ -7,7 +7,6 @@ import javafx.scene.Parent
import javafx.scene.Scene
import javafx.scene.control.Button
import javafx.stage.Stage
import org.apache.commons.lang3.SystemUtils
import java.awt.Desktop
import java.net.URL
@ -17,20 +16,12 @@ class AboutPage {
@FXML
fun openBlog() {
if (SystemUtils.IS_OS_LINUX) {
Runtime.getRuntime().exec("xdg-open https://blog.nuculabs.dev")
} else {
Desktop.getDesktop().browse(URL("https://blog.nuculabs.dev").toURI())
}
Desktop.getDesktop().browse(URL("https://blog.nuculabs.dev").toURI())
}
@FXML
fun openGithub() {
if (SystemUtils.IS_OS_LINUX) {
Runtime.getRuntime().exec("xdg-open https://github.com/dnutiu/ImageTagger")
} else {
Desktop.getDesktop().browse(URL("https://github.com/dnutiu/ImageTagger").toURI())
}
Desktop.getDesktop().browse(URL("https://github.com/dnutiu/ImageTagger").toURI())
}
@FXML

View file

@ -9,8 +9,6 @@ interface IDateTimeProvider {
* Returns today's short date. For example: 09 April 2024
*/
fun getTodayShortDate(): String
fun getTodayTime(): String
}
/**
@ -26,13 +24,4 @@ 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

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<fx:root type="javafx.scene.control.MenuBar" xmlns:fx="http://javafx.com/fxml">
<Menu text="File">
<MenuItem text="Tag Images" onAction="#fileMenuTagImages"/>
</Menu>
<Menu text="About">
<MenuItem text="About" onAction="#aboutMenuShowAbout"/>
</Menu>
</fx:root>

View file

@ -1,5 +0,0 @@
.errorLabel {
-fx-text-background-color: red;
-fx-font-family: "Inter Bold"
}

View file

@ -6,23 +6,17 @@
<?import javafx.scene.image.ImageView?>
<?import org.kordamp.ikonli.javafx.FontIcon?>
<?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">
<fx:root type="javafx.scene.layout.HBox" xmlns:fx="http://javafx.com/fxml">
<StackPane style="-fx-background-color: lightgray;" prefWidth="244" prefHeight="244">
<ImageView fx:id="imageView"/>
</StackPane>
<VBox minWidth="300" >
<VBox>
<padding>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0"/>
</padding>
<HBox>
<VBox>
<Label fx:id="fileNameLabel"/>
<Label text="Tags:"/>
<TextArea fx:id="predictedImageTags" editable="false" wrapText="true" prefColumnCount="20"/>
</VBox>
</HBox>
<Label fx:id="fileNameLabel"/>
<Label text="Predicted tags:"/>
<TextArea fx:id="predictedImageTags" editable="false" wrapText="true" prefColumnCount="20"/>
<HBox>
<padding>
<Insets top="5.0"/>
@ -34,20 +28,5 @@
</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

@ -12,7 +12,7 @@
</padding>
<Label fx:id="title" text="9 Apr. 2024" styleClass="sessionTitle"/>
<Pane HBox.hgrow="ALWAYS"/>
<Button fx:id="openDirectoryButton" text="Open Folder">
<Button fx:id="openDirectoryButton" onAction="#openDirectory" text="Open Directory">
<tooltip>
<Tooltip text="Open directory of the images"/>
</tooltip>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -5,40 +5,31 @@
<?import javafx.scene.layout.*?>
<?import org.kordamp.ikonli.javafx.FontIcon?>
<?import dev.nuculabs.imagetagger.ui.controls.ApplicationMenuBar?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1"
<BorderPane prefHeight="537.0" prefWidth="725.0"
xmlns:fx="http://javafx.com/fxml/1"
xmlns="http://javafx.com/javafx/17.0.2-ea"
fx:controller="dev.nuculabs.imagetagger.ui.MainPageController"
stylesheets="@main-window-view.css"
>
<padding>
<Insets bottom="20.0" left="30.0" right="30.0" top="20.0"/>
</padding>
<top>
<VBox>
<ApplicationMenuBar/>
<HBox alignment="CENTER_LEFT" spacing="10">
<padding>
<Insets bottom="5" left="5" right="5"/>
</padding>
<Button fx:id="tagImagesButton" onAction="#onTagImagesButtonClick" text="Tag Images">
<graphic>
<FontIcon iconLiteral="far-images" iconSize="16"/>
</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>
<HBox alignment="CENTER_LEFT">
<padding>
<Insets bottom="5" left="5" right="5"/>
</padding>
<Button onAction="#onTagImagesButtonClick" text="Tag Images">
<graphic>
<FontIcon iconLiteral="far-images" iconSize="16"/>
</graphic>
</Button>
<Separator orientation="VERTICAL" style="-fx-padding: 10px"/>
</HBox>
</top>
<center>
<ScrollPane fitToWidth="true" fitToHeight="true">
<VBox fx:id="verticalBox">
<padding>
<Insets left="10" right="10"/>
</padding>
</VBox>
</ScrollPane>
</center>

View file

@ -12,7 +12,7 @@
fx:controller="dev.nuculabs.imagetagger.ui.pages.AboutPage"
stylesheets="@about-page.css"
>
<VBox prefHeight="350.0" maxWidth="350" spacing="10">
<VBox prefHeight="250.0" maxWidth="350" spacing="10">
<padding>
<Insets top="25" bottom="25" right="15" left="15"/>
</padding>
@ -25,8 +25,6 @@
<Image
url="@../image-analysis.png"
backgroundLoading="true"
requestedWidth="64"
requestedHeight="64"
/>
</ImageView>
</HBox>

View file

@ -1,21 +0,0 @@
#!/bin/sh
# NucuLabs.dev's Image Tagger Application
SCRIPT_NAME=\$(basename "\$0")
APP_NAME=\${SCRIPT_NAME%.sh}
DIR="\${0%/*}"
<% if ( System.properties['BADASS_CDS_ARCHIVE_FILE_LINUX'] ) { %>
CDS_ARCHIVE_FILE="<%= System.properties['BADASS_CDS_ARCHIVE_FILE_LINUX'] %>"
CDS_JVM_OPTS="-XX:ArchiveClassesAtExit=\$CDS_ARCHIVE_FILE"
if [ -f "\$CDS_ARCHIVE_FILE" ]; then
CDS_JVM_OPTS="-XX:SharedArchiveFile=\$CDS_ARCHIVE_FILE"
fi
<% } %>
# Make jexec and jspawnhelper executable otherwise they won't work properly.
chmod +x \$DIR/../lib/jexec
chmod +x \$DIR/../lib/jspawnhelper
"\$DIR/java" \$CDS_JVM_OPTS ${jvmArgs} -p "\$DIR/../app" -m ${moduleName}/${mainClassName} ${args} "\$@"

View file

@ -1,7 +1,6 @@
# ![](./docs/image-analysis.png) Image Tagger
# ![](./docs/image-analysis.png) Image Tagger
Image Tagger is a simple software application for predicting an image's keywords using a deep learning model based on
resnet.
Image Tagger is a simple software application for predicting an image's keywords using a deep learning model based on resnet.
It allows photographers to automate the image tagging process. 📸
@ -11,68 +10,38 @@ It allows photographers to automate the image tagging process. 📸
1. Download a release from the release page.
2. Unzip the release.
3. Run `ImageTagger\image\bin\ImageTagger`.
3. Run `ImageTagger\image\bin\ImageTagger`.
![./docs/application.png](./docs/application.png)
Photo credit: [https://unsplash.com/@ndcphoto](https://unsplash.com/@ndcphoto)
Alternatively see [Flatpak](./flatpak/readme.md) installation instructions.
## Development
If you want to build the application yourself, you will need Java 17 JDK and the
If you want to build the application yourself, you will need Java 17 JDK and the
AI models available in the AIModels release.
The release archive is in the [releases page](https://github.com/dnutiu/ImageTagger/releases).
Note: On Linux desktop related features (opening images, folders) are handled
via [xdg-open](https://linux.die.net/man/1/xdg-open).
### Building and Running from source
To build from source you will need Java 17 JDK and Gradle.
Due to some GitHub limitations that do not allow me to upload large files, you'll need to download the AIModels
zip file which contains the deep learning models and place them into the
zip file which contains the deep learning models and place them into the
`ImageTagger/img-ai/src/main/resources/dev/nuculabs/imagetagger/ai/` path.
To build the project run:
```bash
gradlew build
gradle build
```
To run:
```bash
gradlew run
```
### Building the Flatpak
To build the Flatpak run the following commands:
```shell
cd flatpak
./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
gradle run
```
# Blog
@ -81,6 +50,4 @@ 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>
- Icons: <a href="https://www.flaticon.com/free-icons/image-analysis" title="image analysis icons">Image analysis icons created by Dewi Sari - Flaticon</a>

7
settings.gradle Normal file
View file

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

View file

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