initial commit

This commit is contained in:
uvos 2024-06-11 14:31:52 +02:00
commit b585be3679
9 changed files with 561 additions and 0 deletions

76
CMakeLists.txt Normal file
View File

@ -0,0 +1,76 @@
cmake_minimum_required(VERSION 3.5)
project(QImageTagger VERSION 0.1 LANGUAGES CXX)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
set(PROJECT_SOURCES
main.cpp
mainwindow.cpp
mainwindow.h
mainwindow.ui
imagemeta.h
imagemeta.cpp
imagewidget.h
imagewidget.cpp
)
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
qt_add_executable(QImageTagger
MANUAL_FINALIZATION
${PROJECT_SOURCES}
)
# Define target properties for Android with Qt 6 as:
# set_property(TARGET QImageTagger APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
# ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
else()
if(ANDROID)
add_library(QImageTagger SHARED
${PROJECT_SOURCES}
)
# Define properties for Android with Qt 5 after find_package() calls as:
# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
else()
add_executable(QImageTagger
${PROJECT_SOURCES}
)
endif()
endif()
target_link_libraries(QImageTagger PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
if(${QT_VERSION} VERSION_LESS 6.1.0)
set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.QImageTagger)
endif()
set_target_properties(QImageTagger PROPERTIES
${BUNDLE_ID_OPTION}
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)
include(GNUInstallDirs)
install(TARGETS QImageTagger
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
if(QT_VERSION_MAJOR EQUAL 6)
qt_finalize_executable(QImageTagger)
endif()

20
imagemeta.cpp Normal file
View File

@ -0,0 +1,20 @@
#include "imagemeta.h"
ImageMeta::ImageMeta(const std::filesystem::path& path, const QString& text):
path{path}, text{text}
{
}
ImageMeta::ImageMeta(const QJsonObject& json, const std::filesystem::path& dir)
{
QJsonValue filename = json["file_name"];
if(filename == QJsonValue::Undefined || !filename.isString())
throw ParseException("No or invalid file_name field found");
path = dir/filename.toString().toStdString();
QJsonValue textfrommeta = json["text"];
text = textfrommeta.toString("");
}

29
imagemeta.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef IMAGEMETA_H
#define IMAGEMETA_H
#include <filesystem>
#include <QString>
#include <QJsonObject>
#include <QException>
class ParseException : public QException
{
QString msg;
public:
ParseException(const QString& msg): msg{msg} {}
QString getMesg() {return msg;};
void raise() const override { throw *this; }
ParseException *clone() const override { return new ParseException(*this); }
};
class ImageMeta
{
public:
std::filesystem::path path;
QString text;
ImageMeta(const std::filesystem::path& path, const QString& text = "");
ImageMeta(const QJsonObject& json, const std::filesystem::path& dir);
};
#endif // IMAGEMETA_H

39
imagewidget.cpp Normal file
View File

@ -0,0 +1,39 @@
#include <QPainter>
#include "imagewidget.h"
void ImageWidget::paintEvent(QPaintEvent *event)
{
QLabel::paintEvent(event);
displayImage();
}
void ImageWidget::setPixmap(QPixmap image)
{
sourceImage = image;
currentImage = image;
repaint();
}
void ImageWidget::displayImage()
{
if (sourceImage.isNull())
return;
float cw = width(), ch = height();
float pw = currentImage.width(), ph = currentImage.height();
if(pw > cw && ph > ch && pw/cw > ph/ch ||
pw > cw && ph <= ch ||
pw < cw && ph < ch && cw/pw < ch/ph)
currentImage = sourceImage.scaledToWidth(cw, Qt::TransformationMode::FastTransformation);
else if(pw > cw && ph > ch && pw/cw <= ph/ch ||
ph > ch && pw <= cw ||
pw < cw && ph < ch && cw/pw > ch/ph)
currentImage = sourceImage.scaledToHeight(ch, Qt::TransformationMode::FastTransformation);
int x = (cw - currentImage.width())/2, y = (ch - currentImage.height())/2;
QPainter paint(this);
paint.drawPixmap(x, y, currentImage);
}

18
imagewidget.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#include <QPixmap>
#include <QLabel>
class ImageWidget: public QLabel
{
private:
QPixmap sourceImage;
QPixmap currentImage;
void displayImage();
public:
ImageWidget(QWidget* parent): QLabel(parent) { }
void setPixmap(QPixmap image);
void paintEvent(QPaintEvent *event);
};

11
main.cpp Normal file
View File

@ -0,0 +1,11 @@
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}

173
mainwindow.cpp Normal file
View File

@ -0,0 +1,173 @@
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QJsonDocument>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->actionOpen, &QAction::triggered, this, [this](bool){open();});
connect(ui->nextButton, &QPushButton::clicked, this, [this](bool){setImage(index+1);});
connect(ui->prevButton, &QPushButton::clicked, this, [this](bool){setImage(index-1);});
connect(ui->actionSave, &QAction::triggered, this, [this](bool){save();});
connect(ui->actionQuit, &QAction::triggered, this, [this](bool){close();});
ui->verticalLayout->addWidget(&status);
ui->imageLabel->setScaledContents( true );
ui->imageLabel->setSizePolicy( QSizePolicy::Ignored, QSizePolicy::Ignored );
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::open()
{
QString dirpath = QFileDialog::getExistingDirectory(this, "Choose a directory with images", "", QFlags<QFileDialog::Option>());
if(dirpath.isEmpty())
return;
openDir(dirpath.toStdString());
}
void MainWindow::openDir(const std::filesystem::path& path)
{
imageMetas.clear();
QDir dir(path);
bool hasMetadata = false;
std::vector<std::filesystem::path> imagePaths;
for(QString filename : dir.entryList(QDir::Files))
{
if(filename == "metadata.jsonl")
hasMetadata = true;
else if(filename.endsWith(".jpg") || filename.endsWith(".png"))
imagePaths.push_back(path/filename.toStdString());
}
if(imagePaths.empty())
{
QMessageBox::warning(this, "No images found", "No valid images found in this directory");
return;
}
if(hasMetadata)
{
if(!loadMetadata(path/"metadata.jsonl"))
QMessageBox::warning(this, "Could not read",
(std::string("Could not read ") + path.string() + std::string("/metadata.jsonl, file contains errors")).c_str());
}
for(const std::filesystem::path &path : imagePaths)
{
if(imageIsKnown(path))
continue;
qDebug()<<path.c_str()<<"not in existing metadata.jsonl";
imageMetas.push_back(ImageMeta(path));
}
this->dir = path;
ui->textEdit->document()->setPlainText(imageMetas[0].text);
setImage(0);
}
void MainWindow::setImage(ssize_t index)
{
if(imageMetas.empty())
return;
else if(index > imageMetas.size()-1)
return;
else if(index < 0)
return;
imageMetas[this->index].text = ui->textEdit->document()->toPlainText();
this->index = index;
ui->counterLabel->setText(QString::asprintf("%04zu/%04zu", index, imageMetas.size()-1));
ui->textEdit->document()->setPlainText(imageMetas[index].text);
QPixmap image;
bool ret = image.load(imageMetas[index].path.string().c_str());
if(!ret)
{
ui->imageLabel->setPixmap(QPixmap());
ui->imageLabel->setText(("Unable to load " + imageMetas[index].path.string()).c_str());
ui->imageLabel->setText("No image loaded");
}
else
{
ui->imageLabel->setPixmap(image);
}
ui->imageLabel->setText("");
}
bool MainWindow::loadMetadata(const std::filesystem::path& path)
{
QFile meta(path);
meta.open(QIODeviceBase::ReadOnly);
if(!meta.isOpen())
{
qWarning()<<("Could not open " + path.string()).c_str();
return false;
}
QByteArray line = meta.readLine();
while(!line.isEmpty())
{
QJsonParseError error;
QJsonDocument json = QJsonDocument::fromJson(line, &error);
if(json.isNull())
{
qWarning()<<error.errorString();
return false;
}
imageMetas.push_back(ImageMeta(json.object(), path.parent_path()));
line = meta.readLine();
}
return true;
}
bool MainWindow::imageIsKnown(const std::filesystem::path& path)
{
for(const ImageMeta& meta : imageMetas)
{
if(meta.path == path)
return true;
}
return false;
}
void MainWindow::save()
{
if(imageMetas.empty())
return;
QFile file(dir/"metadata.jsonl");
if(!file.open(QIODeviceBase::WriteOnly | QIODeviceBase::Truncate))
{
QMessageBox::warning(this, "Could not save",
("Could not save, can not open " + (dir/"metadata.jsonl").string() + " for writeing").c_str());
return;
}
for(const ImageMeta& meta : imageMetas)
{
QJsonObject obj;
obj["file_name"] = std::filesystem::relative(meta.path, dir).string().c_str();
obj["text"] = meta.text;
QJsonDocument document(obj);
QByteArray json = document.toJson(QJsonDocument::Compact);
json.removeIf([](char ch){return ch == '\n';});
json.append('\n');
file.write(json);
}
file.close();
QMessageBox::information(this, "Saved", ("Saved to " + (dir/"metadata.jsonl").string()).c_str());
}

42
mainwindow.h Normal file
View File

@ -0,0 +1,42 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <vector>
#include <QStatusBar>
#include "imagemeta.h"
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
bool modified = false;
std::vector<ImageMeta> imageMetas;
size_t index = 0;
std::filesystem::path dir;
QStatusBar status;
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void open();
void save();
private:
void openDir(const std::filesystem::path& path);
bool loadMetadata(const std::filesystem::path& path);
bool imageIsKnown(const std::filesystem::path& path);
void setImage(ssize_t index);
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

153
mainwindow.ui Normal file
View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1074</width>
<height>692</height>
</rect>
</property>
<property name="windowTitle">
<string>QImageTagger</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,0">
<property name="spacing">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="ImageWidget" name="imageLabel">
<property name="font">
<font>
<pointsize>24</pointsize>
</font>
</property>
<property name="text">
<string>No image loaded</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
<property name="margin">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="textEdit"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="prevButton">
<property name="text">
<string>Prev</string>
</property>
<property name="shortcut">
<string>Left</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="counterLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>120</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>0000/0000</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="nextButton">
<property name="text">
<string>Next</string>
</property>
<property name="shortcut">
<string>Right</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1074</width>
<height>25</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="actionQuit"/>
</widget>
<addaction name="menuFile"/>
</widget>
<action name="actionOpen">
<property name="text">
<string>Open</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionQuit">
<property name="text">
<string>Quit</string>
</property>
</action>
<action name="actionSave">
<property name="text">
<string>Save</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>ImageWidget</class>
<extends>QLabel</extends>
<header>imagewidget.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>