initial commit
This commit is contained in:
commit
b585be3679
76
CMakeLists.txt
Normal file
76
CMakeLists.txt
Normal 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
20
imagemeta.cpp
Normal 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
29
imagemeta.h
Normal 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
39
imagewidget.cpp
Normal 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
18
imagewidget.h
Normal 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
11
main.cpp
Normal 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
173
mainwindow.cpp
Normal 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
42
mainwindow.h
Normal 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
153
mainwindow.ui
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user