Compare commits

..

56 commits

Author SHA1 Message Date
bf031ded63 Update readme.md 2025-01-07 11:38:23 +00:00
d6328b9089 Add time to ImageTagsSessionHeader 2024-11-19 22:02:29 +02:00
69e1bf4d3c implement image metadata display using a table 2024-11-19 21:58:28 +02:00
8f424f81dc broaden flatpak access to home directory 2024-11-17 22:14:55 +02:00
b4ae206fbf update flatpak readme.md 2024-11-17 22:12:23 +02:00
fbbf698aea update flatpak 2024-11-17 22:11:29 +02:00
1d4e451d09 update settings.gradle.kts 2024-11-17 17:22:57 +02:00
bd3cf72931 Add custom icon to jpackage 2024-11-17 17:07:36 +02:00
535f437ba4 Update readme.md 2024-11-17 16:29:30 +02:00
a54bd586d4 Implement space tags display mode 2024-11-17 14:42:20 +02:00
a150952990 Implement settings for tags display mode 📸 2024-11-17 14:37:37 +02:00
76f2ea7ce7 update to kotlin 1.9.25 2024-11-16 21:11:09 +02:00
af578f09f4 implement tag display mode 2024-11-16 19:57:54 +02:00
87737a5261 fix about page text not displaying properly 2024-11-16 19:27:51 +02:00
a3fa9590d1 Change tags label in image-tags-entry.fxml 2024-06-02 10:08:18 +03:00
facea33735 Update dependencies to 1.4 2024-06-02 10:05:07 +03:00
Denis Nuțiu
9ac8b808bf
Merge pull request #4 from dnutiu/metadata-feature
Metadata feature
2024-06-02 10:00:40 +03:00
7e1d4ef8c2 Display metadata in image row 2024-06-02 10:00:04 +03:00
b56057f2f9 Add image metadata placeholders 2024-06-01 23:39:13 +03:00
3ecdf21296 [Feature] [ALL] Implement image metadata extraction 2024-05-24 22:07:13 +03:00
f17b8ff262 [Feature] [ALL] Abstract ImageMetadata 2024-05-19 12:14:34 +03:00
7539d3dadf [Feature] [ALL] Implement image metadata extraction for field Artist 2024-05-19 12:07:09 +03:00
bf5e122af3 [Bugfix] [Linux] Cannot open images that contain spaces 2024-05-19 11:53:49 +03:00
ef1c4b67ea Disable wayland access 2024-05-11 20:59:03 +03:00
52aee3a8ca Fix Flatpak build 2024-05-11 15:40:58 +03:00
65a433ab61 Update application icon, increase size to 512px 2024-05-11 15:34:44 +03:00
7e6abef0e5 List release archive as potential source in flatpak build 2024-05-11 15:30:31 +03:00
0a3ad79c4f Change flatpak build steps: remove chmod 2024-05-11 15:21:38 +03:00
2fb74dc552 fix xdg-open permissions on Linux 2024-05-11 15:18:41 +03:00
09af9fccb5 convert img-ui build.gradle to Kotlin DSL 2024-05-11 14:25:19 +03:00
Denis-Cosmin NUTIU
cc62cc9fce Disable Tag Images button when prediction is ongoing 2024-05-04 16:37:32 +03:00
470491d9ac convert settings.gradle to kotlin syntax 2024-05-03 19:10:46 +03:00
7e36b73480 update flatpak readme 2024-05-03 19:09:18 +03:00
7bf4a9dced add flatpak dir 2024-05-03 18:39:30 +03:00
75c2f078a3 add flatpak 2024-05-03 18:36:28 +03:00
c492ea59c4 reformat code 2024-05-03 13:02:24 +03:00
96f4cb6683 Fix opening links in AboutPage on Linux 2024-05-03 13:00:57 +03:00
0f6b4ce4ff update FUNDING.yml 2024-04-28 12:20:19 +03:00
Denis Nuțiu
df2e4a04c1
Create FUNDING.yml 2024-04-28 12:19:01 +03:00
1de91e8f0c Bump version to 1.3 2024-04-28 12:16:35 +03:00
c1ca8b2234 Fix image opening and folder opening on Linux. 2024-04-28 12:13:40 +03:00
48076c69a5 display an error when image fails to be analyzed 2024-04-27 23:50:33 +03:00
cb00500705 implement img-core a core module 2024-04-27 15:33:37 +03:00
13d3c97d4f synchronize updateProgressBar 2024-04-27 14:54:07 +03:00
e934c28919 update application image 2024-04-13 21:43:48 +03:00
0e16af34ed suppress unused warnings when annotated with FXML 2024-04-13 19:07:53 +03:00
ebee2f24b7 remove unused imports from ApplicationMenuBar 2024-04-13 19:07:36 +03:00
c8f446295a Define ApplicationMenuBar using FXML 2024-04-13 19:06:09 +03:00
db5609bfc6 update build.gradle 2024-04-13 17:11:40 +03:00
6a710da083 make ApplicationMenuBar work on Windows 2024-04-13 17:10:08 +03:00
9ffe3d844f update gitignore 2024-04-13 16:18:46 +03:00
6daa7f5820 ignore workspace.xml 2024-04-13 16:11:02 +03:00
efc8343ce9 delete workspace.xml 2024-04-13 16:10:13 +03:00
d2292c3a22 update readme.md 2024-04-13 16:09:48 +03:00
Denis-Cosmin NUTIU
402130811e ImageTagsSessionHeader: set on action dynamically for show open directory button & change text to open folder 2024-04-10 00:24:21 +03:00
Denis Nuțiu
3ecebd0033
Merge pull request #2 from dnutiu/session-header
Session header
2024-04-10 00:18:07 +03:00
48 changed files with 903 additions and 577 deletions

14
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,14 @@
# 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,3 +40,5 @@ bin/
### Mac OS ###
.DS_Store
/.idea/workspace.xml
/.idea/copilot

View file

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

View file

@ -9,6 +9,7 @@
<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.8.22" />
<option name="version" value="1.9.25" />
</component>
</project>

View file

@ -1,5 +1,9 @@
<?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$" />

View file

@ -1,334 +0,0 @@
<?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: 316 KiB

After

Width:  |  Height:  |  Size: 327 KiB

3
flatpak/.gitignore vendored Normal file
View file

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

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

@ -0,0 +1,9 @@
[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.

After

Width:  |  Height:  |  Size: 42 KiB

View file

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

9
flatpak/readme.md Normal file
View file

@ -0,0 +1,9 @@
# 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,19 +1,20 @@
plugins {
id("java")
kotlin("jvm") version "1.8.22"
kotlin("jvm") version "1.9.25"
id("org.javamodularity.moduleplugin") version "1.8.12"
}
var junitVersion = "5.10.0"
group = "dev.nuculabs.imagetagger.ai"
version = "1.1"
version = "1.4"
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,5 +3,6 @@ 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

@ -3,6 +3,7 @@ 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

21
img-core/build.gradle.kts Normal file
View file

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

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

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

@ -0,0 +1,111 @@
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')"
}
}

View file

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

View file

@ -1,69 +0,0 @@
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'
}

73
img-ui/build.gradle.kts Normal file
View file

@ -0,0 +1,73 @@
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,9 +14,11 @@ 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;
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

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

View file

@ -1,14 +1,12 @@
package dev.nuculabs.imagetagger.ui
import dev.nuculabs.imagetagger.ai.IImageTagsPrediction
import dev.nuculabs.imagetagger.core.abstractions.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
@ -34,25 +32,24 @@ class MainPage : Application() {
setUpApplicationIcon()
// Load the FXML.
val scene = Scene(fxmlLoader.load(), 640.0, 760.0)
val scene = Scene(fxmlLoader.load(), 740.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 = 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,23 +1,25 @@
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 {
@ -34,7 +36,7 @@ class MainPageController {
private val maxImagesPredictionInProgress = Runtime.getRuntime().availableProcessors()
/**
* Semaphore to limit the maximum amount of predictions submitted to the tread pool.
* Semaphore to limit the maximum number of predictions submitted to the tread pool.
*/
private val workerSemaphore: Semaphore = Semaphore(maxImagesPredictionInProgress)
@ -54,11 +56,21 @@ 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
@ -68,12 +80,40 @@ 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)
}
}
}
}
/**
@ -84,7 +124,10 @@ 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
@ -112,19 +155,12 @@ class MainPageController {
return@submit
}
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()
}
val analyzedImage = AnalyzedImage(filePath, this.imageTagsPrediction)
workerSemaphore.release()
Platform.runLater {
// Add image and prediction to the view.
addNewImagePredictionEntry(analyzedImage)
updateProgressBar()
}
}
}
@ -142,38 +178,18 @@ 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 imagePath - The image path.
* @param imageTags - The image's tags.
* @param analyzedImage - The analyzed image instance.
*/
fun addNewImagePredictionEntry(
imagePath: String,
imageTags: List<String>,
analyzedImage: AnalyzedImage,
) {
verticalBox.children.add(ImageTagsEntryControl(imagePath, imageTags))
val control = ImageTagsEntryControl(analyzedImage)
control.setTagsDisplayMode(tagsDisplayMode)
verticalBox.children.add(control)
predictedImages.add(control)
verticalBox.children.add(Separator())
}
@ -191,13 +207,16 @@ class MainPageController {
* Updates the progress bar of the UI.
*/
fun updateProgressBar() {
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.")
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.")
}
}
}

View file

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

@ -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

@ -1,10 +1,14 @@
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
@ -12,16 +16,19 @@ 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 imagePath: String, predictions: List<String>) : HBox() {
class ImageTagsEntryControl(private val image: AnalyzedImage) : HBox() {
private val logger: Logger = Logger.getLogger("ImageTagsEntryControl")
/**
@ -36,6 +43,16 @@ class ImageTagsEntryControl(private val imagePath: String, predictions: List<Str
@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.
*/
@ -48,6 +65,15 @@ class ImageTagsEntryControl(private val imagePath: String, predictions: List<Str
@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")
@ -59,8 +85,15 @@ class ImageTagsEntryControl(private val imagePath: String, predictions: List<Str
} catch (exception: IOException) {
throw RuntimeException(exception)
}
setImage(imagePath)
setText(predictions)
setImage(image)
if (image.hasError()) {
setTags(listOf(image.errorMessage()))
metadataVbox.isVisible = false
} else {
setTags(image.tags())
setMetadata()
}
setupEventHandlers()
}
@ -87,20 +120,83 @@ class ImageTagsEntryControl(private val imagePath: String, predictions: List<Str
*
* @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 }
}
}
}
/**
* 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(imagePath: String) {
val file = File(imagePath)
file.inputStream().use {
imageView.image = Image(it, 244.0, 244.0, true, true)
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"))
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}"
}
/**
@ -108,14 +204,22 @@ class ImageTagsEntryControl(private val imagePath: String, predictions: List<Str
* If the operation fails it will display an error alert.
*/
fun onOpenImageClick() {
if (Desktop.isDesktopSupported()) {
val desktop = Desktop.getDesktop()
if (desktop.isSupported(Desktop.Action.OPEN)) {
desktop.open(File(imagePath))
}
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 {
logger.severe("Cannot open image $imagePath. Desktop action not supported!")
ErrorAlert("Can't open file: $imagePath\nOperation is not supported!")
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!")
}
}
}

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

@ -8,6 +8,7 @@ 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
@ -45,6 +46,9 @@ class ImageTagsSessionHeader : HBox() {
}
updateHeader(0)
openDirectoryButton.setOnAction {
openDirectory()
}
}
/**
@ -61,25 +65,30 @@ 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)"
}
/**
* Opens the directory in the user's Desktop.
*/
fun openDirectory() {
this.directoryPath?.let {
if (Desktop.isDesktopSupported()) {
val desktop = Desktop.getDesktop()
if (desktop.isSupported(Desktop.Action.OPEN)) {
desktop.open(File(it))
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!")
}
} 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

@ -1,38 +0,0 @@
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,6 +7,7 @@ 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
@ -16,12 +17,20 @@ class AboutPage {
@FXML
fun openBlog() {
Desktop.getDesktop().browse(URL("https://blog.nuculabs.dev").toURI())
if (SystemUtils.IS_OS_LINUX) {
Runtime.getRuntime().exec("xdg-open https://blog.nuculabs.dev")
} else {
Desktop.getDesktop().browse(URL("https://blog.nuculabs.dev").toURI())
}
}
@FXML
fun openGithub() {
Desktop.getDesktop().browse(URL("https://github.com/dnutiu/ImageTagger").toURI())
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())
}
}
@FXML

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

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

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

View file

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -5,31 +5,40 @@
<?import javafx.scene.layout.*?>
<?import org.kordamp.ikonli.javafx.FontIcon?>
<BorderPane prefHeight="537.0" prefWidth="725.0"
xmlns:fx="http://javafx.com/fxml/1"
<?import dev.nuculabs.imagetagger.ui.controls.ApplicationMenuBar?>
<BorderPane 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>
<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>
<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>
</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="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>
@ -25,6 +25,8 @@
<Image
url="@../image-analysis.png"
backgroundLoading="true"
requestedWidth="64"
requestedHeight="64"
/>
</ImageView>
</HBox>

View file

@ -0,0 +1,21 @@
#!/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,6 +1,7 @@
# ![](./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. 📸
@ -10,38 +11,68 @@ 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
gradle build
gradlew build
```
To run:
```bash
gradle run
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
```
# Blog
@ -50,4 +81,6 @@ 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>

View file

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

8
settings.gradle.kts Normal file
View file

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