Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a220bc3ed | |||
| f67539a3a1 | |||
| 468bcb5abb | |||
| fd49ba9a44 | |||
| 22e3b06fdd | |||
| 01febbf421 | |||
| 7690496cf5 | |||
| 743a5f50c4 | |||
| eac534352f | |||
| 57bdc74ef6 | |||
| fd1fd7ff08 | |||
| c1aca3ca65 | |||
| 5cc8fdd83d | |||
| 66e13529be | |||
| e1bed8e1cb | |||
| 151f521688 | |||
| 926647e1a7 | |||
| 71fc1f2bd1 | |||
| f1ff04382b | |||
| ad91adf1d9 | |||
| b7369c2501 | |||
| 79ed6b2059 | |||
| 380974a088 | |||
| b1ad56ca1f | |||
| 58abef5a72 | |||
| 5427ff57cb | |||
| 6a2fa3f656 | |||
| c62ec7db8c | |||
| d5eb0fdce5 | |||
| 6d25919e1f | |||
| 26d1af6077 | |||
| 44d8a8b856 | |||
| dab6c1f79d | |||
| ce6a4cc40c | |||
| 0368c1f1dc | |||
| a1e98d818b | |||
| f3f194bcef | |||
| efd36f96c3 | |||
| 37923b37b3 | |||
| 900453577e | |||
| 34d466c3e0 | |||
| 9e98127084 | |||
| ba6062b925 | |||
| 6411b7cd15 | |||
| 223f7cd0ea | |||
| f8f9ee08b3 | |||
| af5aed7ef8 | |||
| 8f5249b142 | |||
| a7dc942c62 | |||
| 1a1399434b | |||
| be567841bf | |||
| 62d2671112 | |||
| 1f8923512e | |||
| 455c3b2d64 | |||
| 4fe546f0e5 | |||
| 95c6fc5343 | |||
| 2bc54ea0cc | |||
| be6e472081 | |||
| 9746f8f653 | |||
| b51a305c63 | |||
| 39775b5e98 | |||
| 93b56e2966 | |||
| 2e41464ff4 |
+20
-8
@@ -25,6 +25,7 @@ set(TENMON_SRC
|
||||
about.cpp
|
||||
database.cpp
|
||||
databaseview.cpp
|
||||
delete.cpp
|
||||
filesystemwidget.cpp
|
||||
imageinfo.cpp
|
||||
imageringlist.cpp
|
||||
@@ -35,12 +36,22 @@ set(TENMON_SRC
|
||||
mainwindow.cpp
|
||||
markedfiles.cpp
|
||||
rawimage.cpp
|
||||
settingsdialog.cpp
|
||||
starfit.cpp
|
||||
statusbar.cpp
|
||||
stfslider.cpp
|
||||
stretchtoolbar.cpp
|
||||
)
|
||||
|
||||
option(COLOR_MANAGMENT "Enable sRGB framebuffer support for gamma correct images and color profiles support" ON)
|
||||
if(${Qt5Core_VERSION_STRING} VERSION_LESS "5.14")
|
||||
set(COLOR_MANAGMENT OFF)
|
||||
endif(${Qt5Core_VERSION_STRING} VERSION_LESS "5.14")
|
||||
|
||||
if(COLOR_MANAGMENT)
|
||||
add_compile_definitions("COLOR_MANAGMENT")
|
||||
endif(COLOR_MANAGMENT)
|
||||
|
||||
qt5_add_resources(TENMON_SRC resources.qrc)
|
||||
if(WIN32)
|
||||
list(APPEND TENMON_SRC icon.rc)
|
||||
@@ -53,6 +64,8 @@ elseif(APPLE)
|
||||
else()
|
||||
add_compile_definitions("__PCL_LINUX")
|
||||
set(tenmon_ICON "")
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_search_module(GIO REQUIRED gio-2.0)
|
||||
endif()
|
||||
|
||||
add_executable(tenmon WIN32 MACOSX_BUNDLE ${tenmon_ICON} ${TENMON_SRC})
|
||||
@@ -66,13 +79,14 @@ elseif(APPLE)
|
||||
target_link_directories(tenmon PRIVATE 3rdparty/lib/MacOS)
|
||||
else()
|
||||
target_link_directories(tenmon PRIVATE 3rdparty/lib/Linux)
|
||||
target_include_directories(tenmon PRIVATE ${GIO_INCLUDE_DIRS})
|
||||
endif()
|
||||
|
||||
target_link_libraries(tenmon Qt5::Widgets Qt5::Sql ${OpenCV_LIBS} ${GSL_LIB} ${GSLCBLAS_LIB} ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB})
|
||||
if(APPLE)
|
||||
target_link_libraries(tenmon PCL-pxi lcms-pxi lz4-pxi RFC6234-pxi zlib-pxi "-framework CoreFoundation")
|
||||
else()
|
||||
target_link_libraries(tenmon PCL lcms lz4 RFC6234 zlib)
|
||||
target_link_libraries(tenmon PCL lcms lz4 RFC6234 zlib ${GIO_LDFLAGS})
|
||||
endif(APPLE)
|
||||
|
||||
if(LIBRAW_STATIC)
|
||||
@@ -82,18 +96,16 @@ endif()
|
||||
|
||||
install(TARGETS tenmon BUNDLE DESTINATION .)
|
||||
if(UNIX AND NOT APPLE)
|
||||
include(GNUInstallDirs)
|
||||
find_program(XDG-DESKTOP-MENU_EXECUTABLE xdg-desktop-menu)
|
||||
if(XDG-DESKTOP-MENU_EXECUTABLE)
|
||||
install(SCRIPT install.cmake)
|
||||
else()
|
||||
if(DEFINED ENV{FLATPAK_DEST})
|
||||
install(FILES org.nou.tenmon.desktop DESTINATION "$ENV{FLATPAK_DEST}/share/applications")
|
||||
install(FILES org.nou.tenmon.png DESTINATION "$ENV{FLATPAK_DEST}/share/icons/hicolor/32x32/apps")
|
||||
else()
|
||||
install(FILES org.nou.tenmon.desktop DESTINATION "/usr/share/applications")
|
||||
install(FILES org.nou.tenmon.png DESTINATION "/usr/share/icons/hicolor/32x32/apps")
|
||||
endif()
|
||||
install(FILES space.nouspiro.tenmon.desktop DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
|
||||
install(FILES space.nouspiro.tenmon.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/64x64/apps")
|
||||
install(FILES space.nouspiro.tenmon_128.png DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps" RENAME space.nouspiro.tenmon.png)
|
||||
endif()
|
||||
install(FILES space.nouspiro.tenmon.metainfo.xml DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo")
|
||||
endif(UNIX AND NOT APPLE)
|
||||
|
||||
option(RELEASE_BUILD "Release build" OFF)
|
||||
|
||||
@@ -2,10 +2,23 @@ FITS/XISF image viewer with multithreaded image loading
|
||||
|
||||
To get all dependencies install these packages
|
||||
|
||||
sudo apt install qtbase5-dev libraw-dev libexif-dev libcfitsio-dev libgsl-dev cmake
|
||||
sudo apt install qtbase5-dev libraw-dev libexif-dev libcfitsio-dev libgsl-dev wcslib-dev libopencv-dev cmake
|
||||
|
||||
on OpenSUSE
|
||||
|
||||
sudo zypper install opencv-devel gsl-devel exif-devel libraw-devel wcslib-devel libqt5-qtbase-devel
|
||||
|
||||
MacOS X
|
||||
|
||||
To compile on MacOS install XCode first. Then install homebrew in x86_64 mode
|
||||
with "arch -i x86_64". Building on native ARM is not supported.
|
||||
|
||||
homebrew install qt5 libraw cfitsio libexif libgsl wcslib opencv
|
||||
|
||||
You may need to set CMAKE_PREFIX_PATH for Qt5 and OpenCV so CMake can find them.
|
||||
|
||||
Then to build run standard cmake
|
||||
|
||||
cmake -B build -S .
|
||||
make
|
||||
./tenmon
|
||||
cmake --build build
|
||||
./build/tenmon
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ img { margin: 5px; }
|
||||
<ul>
|
||||
<li>FITS 8, 16 bit integer and 32 bit float</li>
|
||||
<li>XISF 8, 16 bit integer and 32 bit float</li>
|
||||
<li>JPEG and PNG images</li>
|
||||
<li>JPEG, PNG, BMP, GIF, PBM, PGM, PPM and SVG images</li>
|
||||
<li>CR2, NEF, DNG raw images</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ img { margin: 5px; }
|
||||
<ul>
|
||||
<li>FITS 8, 16 bit entier et 32 bit point flottant</li>
|
||||
<li>XISF 8, 16 bit entier et 32 bit point flottant</li>
|
||||
<li>images JPEG et PNG</li>
|
||||
<li>images JPEG, PNG, BMP, GIF, PBM, PGM, PPM et SVG</li>
|
||||
<li>images RAW CR2, NEF, DNG</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ p { padding:0px; margin:5px 5px 10px 5px; }
|
||||
<ul>
|
||||
<li>FITS 8, 16 bitové celočíselné a 32 bitové s plávajúcou čiarkou</li>
|
||||
<li>XISF 8, 16 bitové celočíselné a 32 bitové s plávajúcou čiarkou</li>
|
||||
<li>JPEG a PNG obrázky</li>
|
||||
<li>JPEG, PNG, BMP, GIF, PBM, PGM, PPM a SVG obrázky</li>
|
||||
<li>CR2, NEF, DNG raw obrázky</li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<table><tr>
|
||||
<td style="padding-right:10px"><img src=":/org.nou.tenmon.png"></td>
|
||||
<td style="padding-right:10px"><img src=":/space.nouspiro.tenmon.png"></td>
|
||||
<td><h3>Tenmon</h3>
|
||||
Tenmon is FITS/XISF image viewer and converter. It also index FITS keywords.<br>
|
||||
v@GITVERSION@ Copyright © 2022 Dušan Poizl<br><br>
|
||||
|
||||
+4
-6
@@ -125,7 +125,6 @@ QStringList Database::getMarkedFiles()
|
||||
files << markedFiles.value("file").toString();
|
||||
}
|
||||
|
||||
qDebug() << files.size();
|
||||
return files;
|
||||
}
|
||||
|
||||
@@ -226,7 +225,6 @@ QStringList Database::getFitsKeywords()
|
||||
return keywords;
|
||||
}
|
||||
|
||||
|
||||
bool Database::indexDir2(const QDir &dir, QProgressDialog *progress)
|
||||
{
|
||||
QFileInfoList files = dir.entryInfoList(nameFilters, QDir::Files);
|
||||
@@ -288,7 +286,7 @@ bool Database::indexFile(const QFileInfo &file)
|
||||
m_insertFileWcs.bindValue(7, crVal2);
|
||||
if(!m_insertFileWcs.exec())
|
||||
{
|
||||
qDebug() << m_insertFileWcs.lastError();
|
||||
qDebug() << "Database error" << m_insertFileWcs.lastError();
|
||||
return false;
|
||||
}
|
||||
last_id = m_insertFileWcs.lastInsertId().toLongLong();
|
||||
@@ -299,7 +297,7 @@ bool Database::indexFile(const QFileInfo &file)
|
||||
m_insertFile.bindValue(1, mtime);
|
||||
if(!m_insertFile.exec())
|
||||
{
|
||||
qDebug() << m_insertFile.lastError();
|
||||
qDebug() << "Database error" << m_insertFile.lastError();
|
||||
return false;
|
||||
}
|
||||
last_id = m_insertFile.lastInsertId().toLongLong();
|
||||
@@ -319,10 +317,10 @@ bool Database::indexFile(const QFileInfo &file)
|
||||
m_insertFitsHeader.bindValue(3, comments);
|
||||
if(!m_insertFitsHeader.execBatch())
|
||||
{
|
||||
qDebug() << m_insertFitsHeader.lastError();
|
||||
qDebug() << "Database error" << m_insertFitsHeader.lastError();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
qDebug() << filePath << last_id;
|
||||
qDebug() << "Indexed" << filePath << last_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
+40
-5
@@ -8,7 +8,7 @@
|
||||
#include <QDebug>
|
||||
#include <QMenu>
|
||||
#include <QContextMenuEvent>
|
||||
#include <QRegExp>
|
||||
#include <QRegularExpression>
|
||||
#include <iostream>
|
||||
|
||||
const QStringList DEFAULT_COLUMNS = {"EXPTIME", "OBJECT", "RA", "DEC"};
|
||||
@@ -20,7 +20,7 @@ double RA(const QString &ra)
|
||||
double h = match.captured(1).toDouble();
|
||||
double m = match.captured(2).toDouble();
|
||||
double s = match.captured(3).toDouble();
|
||||
qDebug() << match.capturedTexts() << h << m << s;
|
||||
qDebug() << "RA" << match.capturedTexts() << h << m << s;
|
||||
return h*15 + m*0.25 + s*15/3600;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ double DEC(const QString &dec)
|
||||
double d = match.captured(2).toDouble();
|
||||
double m = match.captured(3).toDouble();
|
||||
double s = match.captured(4).toDouble();
|
||||
qDebug() << match.capturedTexts() << sign << d << m << s;
|
||||
qDebug() << "DEC" << match.capturedTexts() << sign << d << m << s;
|
||||
return sign * (d + m/60 + s/3600);
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ void FITSFileModel::prepareQuery()
|
||||
QString cols;
|
||||
QString join;
|
||||
QStringList where;
|
||||
QString sql = "SELECT f.file,";
|
||||
QString sql = m_columns.size() ? "SELECT f.file," : "SELECT f.file";
|
||||
for(int i=0; i<m_value.size(); i++)
|
||||
{
|
||||
if(m_key[i] == "file")
|
||||
@@ -195,7 +195,7 @@ void FITSFileModel::prepareQuery()
|
||||
}
|
||||
std::cout << sql.toStdString() << std::endl;
|
||||
if(lastError().type() != QSqlError::NoError)
|
||||
qDebug() << lastError();
|
||||
qDebug() << "Database error" << lastError();
|
||||
|
||||
m_markedFiles = m_database->getMarkedFiles().toSet();
|
||||
}
|
||||
@@ -362,3 +362,38 @@ void DataBaseView::applyFilter()
|
||||
}
|
||||
m_model->setFilter(keys, values, limits);
|
||||
}
|
||||
|
||||
bool DataBaseView::exportCSV(const QString &path)
|
||||
{
|
||||
QFile csv(path);
|
||||
if(!csv.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
return false;
|
||||
|
||||
QSqlQuery sql = m_model->query();
|
||||
int colCount = m_model->columnCount();
|
||||
QStringList header;
|
||||
for(int i=0; i<colCount; i++)
|
||||
header.append(m_model->headerData(i, Qt::Horizontal).toString());
|
||||
|
||||
csv.write(header.join(",").toUtf8());
|
||||
csv.write("\n");
|
||||
|
||||
while(sql.next())
|
||||
{
|
||||
QStringList columns;
|
||||
for(int i=0; i<colCount; i++)
|
||||
{
|
||||
QString val = sql.value(i).toString();
|
||||
val.replace("\"", "\"\"");
|
||||
if(val.contains('"') || val.contains(','))
|
||||
{
|
||||
val.prepend('"');
|
||||
val.append('"');
|
||||
}
|
||||
columns.append(val);
|
||||
}
|
||||
csv.write(columns.join(",").toUtf8());
|
||||
csv.write("\n");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ public slots:
|
||||
void loadDatabase();
|
||||
void itemActivated(const QModelIndex &index);
|
||||
void applyFilter();
|
||||
bool exportCSV(const QString &path);
|
||||
signals:
|
||||
void loadFile(QString file);
|
||||
};
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
#ifdef __linux__
|
||||
|
||||
#define QT_NO_KEYWORDS
|
||||
#include <QString>
|
||||
#include <iostream>
|
||||
#include <gio/gio.h>
|
||||
|
||||
//flatpak bug prevent to use QFile::moveToTrash
|
||||
bool moveToTrash(const QString &path)
|
||||
{
|
||||
GFile *gfile = g_file_new_for_path(path.toLocal8Bit().data());
|
||||
GError *error = nullptr;
|
||||
g_file_trash(gfile, nullptr, &error);
|
||||
if(error)std::cerr << "failed to trash file " << error->code << " " << error->message << std::endl;
|
||||
g_clear_error(&error);
|
||||
g_object_unref(gfile);
|
||||
return true;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
#include <QFile>
|
||||
#include <QString>
|
||||
|
||||
bool moveToTrash(const QString &path)
|
||||
{
|
||||
return QFile::moveToTrash(path);
|
||||
}
|
||||
|
||||
#endif
|
||||
+14
-6
@@ -20,12 +20,6 @@ FilesystemWidget::FilesystemWidget(QAbstractItemModel *model, QWidget *parent) :
|
||||
connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FilesystemWidget::fileClicked);
|
||||
}
|
||||
|
||||
void FilesystemWidget::setDir(const QString &dir)
|
||||
{
|
||||
//m_model->setRootPath(dir);
|
||||
//m_treeView->setRootIndex(m_model->index(m_model->rootPath()));
|
||||
}
|
||||
|
||||
void FilesystemWidget::selectFile(int row)
|
||||
{
|
||||
QModelIndex index = m_model->index(row, 0);
|
||||
@@ -47,6 +41,9 @@ Filetree::Filetree(QWidget *parent) : QTreeView(parent)
|
||||
m_fileSystemModel->setRootPath(m_rootDir);
|
||||
m_fileSystemModel->setNameFilters({"*.fits", "*.fit", "*.xisf", "*.jpg", "*.jpeg", "*.png", "*.cr2", "*.nef", "*.dng"});
|
||||
m_fileSystemModel->setNameFilterDisables(false);
|
||||
if(settings.value("filetree/showHidden", false).toBool())
|
||||
m_fileSystemModel->setFilter(m_fileSystemModel->filter() | QDir::Hidden);
|
||||
|
||||
setModel(m_fileSystemModel);
|
||||
setRootIndex(m_fileSystemModel->index(m_rootDir));
|
||||
header()->restoreState(settings.value("filetree/header").toByteArray());
|
||||
@@ -57,6 +54,7 @@ Filetree::~Filetree()
|
||||
QSettings settings;
|
||||
settings.setValue("filetree/rootDir", m_rootDir);
|
||||
settings.setValue("filetree/header", header()->saveState());
|
||||
settings.setValue("filetree/showHidden", (bool)(m_fileSystemModel->filter() & QDir::Hidden));
|
||||
}
|
||||
|
||||
void Filetree::contextMenuEvent(QContextMenuEvent *event)
|
||||
@@ -85,6 +83,9 @@ void Filetree::contextMenuEvent(QContextMenuEvent *event)
|
||||
|
||||
QAction *resetRoot = menu.addAction(tr("Reset root"));
|
||||
QAction *goUp = menu.addAction(tr("Go up"));
|
||||
QAction *showHidden = menu.addAction(tr("Show hidden files"));
|
||||
showHidden->setCheckable(true);
|
||||
showHidden->setChecked(m_fileSystemModel->filter() & QDir::Hidden);
|
||||
|
||||
QAction *a = menu.exec(event->globalPos());
|
||||
if(a == nullptr)
|
||||
@@ -121,6 +122,13 @@ void Filetree::contextMenuEvent(QContextMenuEvent *event)
|
||||
{
|
||||
emit indexDirectory(m_fileSystemModel->filePath(index));
|
||||
}
|
||||
else if(a == showHidden)
|
||||
{
|
||||
auto filter = m_fileSystemModel->filter();
|
||||
filter ^= QDir::Hidden;
|
||||
m_fileSystemModel->setFilter(filter);
|
||||
m_fileSystemModel->setRootPath(m_rootDir);
|
||||
}
|
||||
}
|
||||
|
||||
void Filetree::mouseDoubleClickEvent(QMouseEvent *event)
|
||||
|
||||
@@ -13,7 +13,6 @@ class FilesystemWidget : public QWidget
|
||||
QAbstractItemModel *m_model;
|
||||
public:
|
||||
explicit FilesystemWidget(QAbstractItemModel *model, QWidget *parent = nullptr);
|
||||
void setDir(const QString &dir);
|
||||
private slots:
|
||||
void selectFile(int row);
|
||||
void fileClicked(const QModelIndex &index, const QModelIndex &);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <wcslib/wcshdr.h>
|
||||
#include <wcslib/wcsfix.h>
|
||||
#include "pcl/FITSHeaderKeyword.h"
|
||||
#include "pcl/Property.h"
|
||||
|
||||
static const QVector<QByteArray> noEditableKey = {"SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE"};
|
||||
|
||||
@@ -43,6 +44,38 @@ FITSRecord::FITSRecord(const pcl::FITSHeaderKeyword &record)
|
||||
value = string;
|
||||
}
|
||||
|
||||
FITSRecord::FITSRecord(const pcl::Property &property)
|
||||
{
|
||||
key = property.Identifier().c_str();
|
||||
|
||||
const pcl::Variant &pclval = property.Value();
|
||||
switch(pclval.Type())
|
||||
{
|
||||
case pcl::VariantType::Bool:
|
||||
value = pclval.ToBool();
|
||||
break;
|
||||
case pcl::VariantType::Int8:
|
||||
case pcl::VariantType::Int16:
|
||||
case pcl::VariantType::Int32:
|
||||
case pcl::VariantType::Int64:
|
||||
value = pclval.ToInt64();
|
||||
break;
|
||||
case pcl::VariantType::UInt8:
|
||||
case pcl::VariantType::UInt16:
|
||||
case pcl::VariantType::UInt32:
|
||||
case pcl::VariantType::UInt64:
|
||||
value = pclval.ToUInt64();
|
||||
break;
|
||||
case pcl::VariantType::Float:
|
||||
case pcl::VariantType::Double:
|
||||
value = pclval.ToDouble();
|
||||
break;
|
||||
default:
|
||||
value = pclval.ToIsoString().c_str();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray FITSRecord::valueToByteArray() const
|
||||
{
|
||||
if(value.type() == QVariant::Bool)
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
namespace pcl { class FITSHeaderKeyword; }
|
||||
namespace pcl { class FITSHeaderKeyword; class Property; }
|
||||
|
||||
struct FITSRecord
|
||||
{
|
||||
@@ -17,6 +17,7 @@ struct FITSRecord
|
||||
FITSRecord(){}
|
||||
FITSRecord(const QByteArray &key, const QVariant &value, const QByteArray &comment);
|
||||
FITSRecord(const pcl::FITSHeaderKeyword &record);
|
||||
FITSRecord(const pcl::Property &property);
|
||||
QByteArray valueToByteArray() const;
|
||||
};
|
||||
|
||||
|
||||
+46
-6
@@ -7,7 +7,7 @@
|
||||
|
||||
using namespace std;
|
||||
|
||||
const int DEFAULT_WIDTH = 2;
|
||||
int DEFAULT_WIDTH = 2;
|
||||
|
||||
Image::Image(const QString name, int number, ImageRingList *ringList) :
|
||||
m_loading(false),
|
||||
@@ -76,6 +76,11 @@ int Image::number() const
|
||||
return m_number;
|
||||
}
|
||||
|
||||
void Image::clearThumbnail()
|
||||
{
|
||||
m_thumbnail.reset();
|
||||
}
|
||||
|
||||
void Image::imageLoaded(void *rawImage, ImageInfoData info)
|
||||
{
|
||||
m_loading = false;
|
||||
@@ -98,12 +103,14 @@ void Image::thumbnailLoadFinish(void *rawImage)
|
||||
emit thumbnailLoaded(this);
|
||||
}
|
||||
|
||||
ImageRingList::ImageRingList(Database *database, QObject *parent) : QAbstractItemModel(parent)
|
||||
ImageRingList::ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent) : QAbstractItemModel(parent)
|
||||
, m_liveMode(false)
|
||||
, m_analyzeLevel(None)
|
||||
, m_database(database)
|
||||
, m_nameFilter(nameFilter)
|
||||
{
|
||||
connect(&m_fileSystemWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(dirChanged(QString)));
|
||||
m_nameFilter.replaceInStrings(QRegularExpression("^"), "*.");
|
||||
m_thumbPool = new QThreadPool(this);
|
||||
}
|
||||
|
||||
@@ -122,10 +129,7 @@ bool ImageRingList::setDir(const QString path, const QString ¤tFile)
|
||||
|
||||
if(dir.exists())
|
||||
{
|
||||
QStringList nameFilter;
|
||||
nameFilter << "*.jpg" << "*.jpeg" << "*.png" << "*.cr2" << "*.nef" << "*.dng" << "*.fit" << "*.fits" << "*.xisf";
|
||||
|
||||
QStringList list = dir.entryList(nameFilter, QDir::Files | QDir::Readable, m_liveMode ? QDir::Time : QDir::Name | QDir::IgnoreCase);
|
||||
QStringList list = dir.entryList(m_nameFilter, QDir::Files | QDir::Readable, m_liveMode ? QDir::Time : QDir::Name | QDir::IgnoreCase);
|
||||
QStringList absolutePaths;
|
||||
foreach(const QString &file, list)
|
||||
{
|
||||
@@ -260,6 +264,7 @@ void ImageRingList::loadThumbnails()
|
||||
void ImageRingList::stopLoading()
|
||||
{
|
||||
m_thumbPool->clear();
|
||||
m_thumbPool->waitForDone();
|
||||
}
|
||||
|
||||
int ImageRingList::imageCount() const
|
||||
@@ -284,6 +289,12 @@ void ImageRingList::updateMark()
|
||||
}
|
||||
}
|
||||
|
||||
void ImageRingList::clearThumbnails()
|
||||
{
|
||||
for(auto &img : m_images)
|
||||
img->clearThumbnail();
|
||||
}
|
||||
|
||||
QModelIndex ImageRingList::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
return createIndex(row, column, m_images.at(row).get());
|
||||
@@ -337,6 +348,35 @@ QVariant ImageRingList::headerData(int section, Qt::Orientation orientation, int
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void ImageRingList::setPreload(int width)
|
||||
{
|
||||
DEFAULT_WIDTH = width;
|
||||
if(m_images.size() == 0)return;
|
||||
|
||||
int newWidth = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
|
||||
if(newWidth > m_width)
|
||||
{
|
||||
for(int i = newWidth - m_width; i>0; i--)
|
||||
{
|
||||
m_firstImage = decrement(m_firstImage);
|
||||
(*m_firstImage)->load();
|
||||
m_lastImage = increment(m_lastImage);
|
||||
(*m_lastImage)->load();
|
||||
}
|
||||
}
|
||||
if(newWidth < m_width)
|
||||
{
|
||||
for(int i = m_width - newWidth; i>0; i--)
|
||||
{
|
||||
(*m_firstImage)->release();
|
||||
m_firstImage = increment(m_firstImage);
|
||||
(*m_lastImage)->release();
|
||||
m_lastImage = decrement(m_lastImage);
|
||||
}
|
||||
}
|
||||
m_width = newWidth;
|
||||
}
|
||||
|
||||
void ImageRingList::setFiles(const QStringList files, const QString ¤tFile)
|
||||
{
|
||||
QThreadPool::globalInstance()->clear();
|
||||
|
||||
+6
-1
@@ -35,6 +35,7 @@ public:
|
||||
ImageInfoData info() const;
|
||||
bool isCurrent() const;
|
||||
int number() const;
|
||||
void clearThumbnail();
|
||||
signals:
|
||||
void pixmapLoaded(Image *ptr);
|
||||
void thumbnailLoaded(Image *ptr);
|
||||
@@ -60,8 +61,9 @@ class ImageRingList : public QAbstractItemModel
|
||||
AnalyzeLevel m_analyzeLevel;
|
||||
QThreadPool *m_thumbPool;
|
||||
Database *m_database;
|
||||
QStringList m_nameFilter;
|
||||
public:
|
||||
explicit ImageRingList(Database *database, QObject *parent = 0);
|
||||
explicit ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent = 0);
|
||||
~ImageRingList() override;
|
||||
bool setDir(const QString path, const QString ¤tFile = QString());
|
||||
void setFile(const QString &file);
|
||||
@@ -79,6 +81,7 @@ public:
|
||||
int imageCount() const;
|
||||
QStringList imageNames() const;
|
||||
void updateMark();
|
||||
void clearThumbnails();
|
||||
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
QModelIndex parent(const QModelIndex &child) const override;
|
||||
@@ -86,6 +89,8 @@ public:
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
public slots:
|
||||
void setPreload(int width);
|
||||
protected:
|
||||
void setFiles(const QStringList files, const QString ¤tFile = QString());
|
||||
QList<ImagePtr>::iterator increment(QList<ImagePtr>::iterator iter);
|
||||
|
||||
+129
-10
@@ -11,6 +11,7 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QPainter>
|
||||
#include <QFileInfo>
|
||||
#include <cmath>
|
||||
|
||||
struct RawImageType
|
||||
{
|
||||
@@ -24,13 +25,20 @@ const RawImageType rawImageTypes[] = {
|
||||
{QOpenGLTexture::Red, QOpenGLTexture::R8_UNorm, QOpenGLTexture::UInt8, true},
|
||||
{QOpenGLTexture::Red, QOpenGLTexture::R16_UNorm, QOpenGLTexture::UInt16, true},
|
||||
{QOpenGLTexture::Red, QOpenGLTexture::R32F, QOpenGLTexture::Float32, true},
|
||||
#ifdef COLOR_MANAGMENT
|
||||
{QOpenGLTexture::RGB, QOpenGLTexture::SRGB8, QOpenGLTexture::UInt8, false},
|
||||
{QOpenGLTexture::RGBA,QOpenGLTexture::SRGB8_Alpha8, QOpenGLTexture::UInt8, false},
|
||||
#else
|
||||
{QOpenGLTexture::RGB, QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::UInt8, false},
|
||||
{QOpenGLTexture::RGBA,QOpenGLTexture::RGB8_UNorm, QOpenGLTexture::UInt8, false},
|
||||
{QOpenGLTexture::RGBA,QOpenGLTexture::RGBA8_UNorm, QOpenGLTexture::UInt8, false},
|
||||
#endif
|
||||
{QOpenGLTexture::RGB, QOpenGLTexture::RGB16_UNorm, QOpenGLTexture::UInt16, false},
|
||||
{QOpenGLTexture::RGBA, QOpenGLTexture::RGB16_UNorm, QOpenGLTexture::UInt16, false},
|
||||
{QOpenGLTexture::RGB, QOpenGLTexture::RGB32F, QOpenGLTexture::Float32, false}
|
||||
};
|
||||
|
||||
static bool MANUAL_MIPMAP_GEN = false;
|
||||
|
||||
void setScrollRange(QScrollBar *scrollBar, int newRange)
|
||||
{
|
||||
int page = scrollBar->pageStep();
|
||||
@@ -89,16 +97,21 @@ ImageWidget::~ImageWidget()
|
||||
void ImageWidget::setImage(std::shared_ptr<RawImage> image, int index)
|
||||
{
|
||||
if(image == nullptr)return;
|
||||
|
||||
makeCurrent();
|
||||
m_rawImage = image;
|
||||
|
||||
m_imgWidth = image->width();
|
||||
m_imgHeight = image->height();
|
||||
m_currentImg = index;
|
||||
|
||||
if(!m_image)return;
|
||||
|
||||
const RawImageType &rawImageType = rawImageTypes[image->type()];
|
||||
m_srgb = rawImageType.textureFormat == QOpenGLTexture::SRGB8 || rawImageType.textureFormat == QOpenGLTexture::SRGB8_Alpha8;
|
||||
m_bwImg = rawImageType.bw;
|
||||
|
||||
m_image->destroy();
|
||||
m_image->setAutoMipMapGenerationEnabled(false);
|
||||
m_image->setFormat(rawImageType.textureFormat);
|
||||
m_image->setSize(image->width(), image->height());
|
||||
m_image->setMipLevels([&](){ int c = 0; int s = std::min(m_imgWidth, m_imgHeight); while(s>>=1)c++; return c; }());
|
||||
@@ -107,9 +120,46 @@ void ImageWidget::setImage(std::shared_ptr<RawImage> image, int index)
|
||||
m_image->setWrapMode(QOpenGLTexture::ClampToEdge);
|
||||
m_image->setBorderColor(0, 0, 0, 0);
|
||||
m_image->setData(0, rawImageType.pixelFormat, rawImageType.dataType, (const void*)image->data(), m_transferOptions.get());
|
||||
m_image->setLevelOfDetailRange(m_superpixel ? 1 : 0, m_image->mipMaxLevel());
|
||||
m_image->generateMipMaps();
|
||||
m_bwImg = rawImageType.bw;
|
||||
|
||||
auto sRGB_linear = [](cv::Point3f &pixel, const int *pos)
|
||||
{
|
||||
pixel.x = pixel.x <= 0.04045f ? pixel.x / 12.92f : std::pow((pixel.x + 0.055) / 1.055f, 2.4f);
|
||||
pixel.y = pixel.y <= 0.04045f ? pixel.y / 12.92f : std::pow((pixel.y + 0.055) / 1.055f, 2.4f);
|
||||
pixel.z = pixel.z <= 0.04045f ? pixel.z / 12.92f : std::pow((pixel.z + 0.055) / 1.055f, 2.4f);
|
||||
};
|
||||
|
||||
auto linear_sRGB = [](cv::Point3f &pixel, const int *pos)
|
||||
{
|
||||
pixel.x = pixel.x <= 0.0031308f ? pixel.x * 12.92f : 1.055f * std::pow(pixel.x , 1/2.4f) - 0.055f;
|
||||
pixel.y = pixel.y <= 0.0031308f ? pixel.y * 12.92f : 1.055f * std::pow(pixel.y , 1/2.4f) - 0.055f;
|
||||
pixel.z = pixel.z <= 0.0031308f ? pixel.z * 12.92f : 1.055f * std::pow(pixel.z , 1/2.4f) - 0.055f;
|
||||
};
|
||||
|
||||
//AMD OpenGL driver on Windows doesn't generate mipmaps for sRGB textures correctly
|
||||
if(m_srgb && MANUAL_MIPMAP_GEN)
|
||||
{
|
||||
cv::Mat img = image->mat();
|
||||
img.convertTo(img, CV_32FC3, 1/255.0);
|
||||
img.forEach<cv::Point3f>(sRGB_linear);
|
||||
cv::Size size(img.cols, img.rows);
|
||||
for(int i=1; i<m_image->mipLevels(); i++)
|
||||
{
|
||||
cv::Mat mip;
|
||||
size /= 2;
|
||||
cv::resize(img, mip, size);
|
||||
mip.copyTo(img);
|
||||
mip.forEach<cv::Point3f>(linear_sRGB);
|
||||
mip.convertTo(mip, CV_8UC3, 255);
|
||||
m_image->setData(i, rawImageType.pixelFormat, rawImageType.dataType, (const void*)mip.ptr(), m_transferOptions.get());
|
||||
}
|
||||
}
|
||||
else m_image->generateMipMaps();
|
||||
|
||||
if(m_debayerTex)
|
||||
{
|
||||
f->glDeleteTextures(1, &m_debayerTex);
|
||||
m_debayerTex = 0;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -168,7 +218,6 @@ void ImageWidget::setOffset(int dx, int dy)
|
||||
void ImageWidget::superPixel(bool enable)
|
||||
{
|
||||
m_superpixel = enable;
|
||||
m_image->setLevelOfDetailRange(enable ? 1 : 0, m_image->mipMaxLevel());
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -192,7 +241,10 @@ QImage ImageWidget::renderToImage()
|
||||
m_program->setUniformValue("offset", 0.0f, 0.0f);
|
||||
m_program->setUniformValue("zoom", 1.0f);
|
||||
|
||||
m_image->bind(0);
|
||||
if(m_superpixel && m_debayerTex)
|
||||
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
|
||||
else
|
||||
m_image->bind(0);
|
||||
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
fbo.bindDefault();
|
||||
@@ -249,6 +301,7 @@ void ImageWidget::paintGL()
|
||||
}
|
||||
m_thumbnailProgram->bind();
|
||||
f->glUniform3i(m_thumbnailProgram->uniformLocation("viewport_row"), width(), height(), width()/THUMB_SIZE_BORDER);
|
||||
f->glUniform3i(m_thumbnailProgram->uniformLocation("thumb_size"), THUMB_SIZE_BORDER/2, THUMB_SIZE_BORDER, THUMB_SIZE_BORDER_Y);
|
||||
m_thumbnailProgram->setUniformValue("mtf_param", m_low, m_mid, m_high);
|
||||
m_thumbnailProgram->setUniformValue("invert", m_invert);
|
||||
m_thumbnailProgram->setUniformValue("offset", 0, m_dy);
|
||||
@@ -287,15 +340,26 @@ void ImageWidget::paintGL()
|
||||
}
|
||||
else
|
||||
{
|
||||
debayer();
|
||||
|
||||
m_vao->bind();
|
||||
m_image->bind(0);
|
||||
if(m_superpixel && m_debayerTex)
|
||||
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
|
||||
else
|
||||
m_image->bind(0);
|
||||
|
||||
m_program->bind();
|
||||
m_program->setUniformValue("viewport", (float)width(), (float)height());
|
||||
m_program->setUniformValue("offset", dx, dy);
|
||||
m_program->setUniformValue("mtf_param", m_low, m_mid, m_high);
|
||||
m_program->setUniformValue("zoom", 1.0f/m_scale);
|
||||
m_program->setUniformValue("bw", m_bwImg);
|
||||
m_program->setUniformValue("bw", m_bwImg && !m_superpixel);
|
||||
m_program->setUniformValue("invert", m_invert);
|
||||
if(m_superpixel)m_program->setUniformValue("whiteBalance", m_whiteBalance[0], m_whiteBalance[1], m_whiteBalance[2]);
|
||||
else m_program->setUniformValue("whiteBalance", 1.0f, 1.0f, 1.0f);
|
||||
#ifdef COLOR_MANAGMENT
|
||||
m_program->setUniformValue("srgb", m_srgb);
|
||||
#endif
|
||||
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
|
||||
@@ -335,6 +399,8 @@ void ImageWidget::initializeGL()
|
||||
qDebug() << (char*)f->glGetString(GL_RENDERER);
|
||||
qDebug() << (char*)f->glGetString(GL_VERSION);
|
||||
|
||||
//MANUAL_MIPMAP_GEN = QString((const char*)f->glGetString(GL_VENDOR)).startsWith("ATI Technologies Inc", Qt::CaseInsensitive);
|
||||
|
||||
qDebug() << context()->format();
|
||||
|
||||
// each vertex is x,y 2D position and s,t texture coordinates
|
||||
@@ -366,12 +432,27 @@ void ImageWidget::initializeGL()
|
||||
m_program->setUniformValue("qt_Texture0", (GLuint)0);
|
||||
m_program->setUniformValue("scale", 1.0f, 0.0f);
|
||||
|
||||
m_debayerProgram = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
|
||||
m_debayerProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/debayer.vert");
|
||||
m_debayerProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/debayer.frag");
|
||||
if(f3)f3->glBindFragDataLocation(m_debayerProgram->programId(), 0, "color");
|
||||
m_debayerProgram->bind();
|
||||
m_debayerProgram->enableAttributeArray("qt_Vertex");
|
||||
m_debayerProgram->setAttributeBuffer("qt_Vertex", GL_FLOAT, 0, 2, sizeof(float)*4);
|
||||
m_debayerProgram->enableAttributeArray("qt_MultiTexCoord0");
|
||||
m_debayerProgram->setAttributeBuffer("qt_MultiTexCoord0", GL_FLOAT, sizeof(float)*2, 2, sizeof(float)*4);
|
||||
m_debayerProgram->setUniformValue("qt_Texture0", (GLuint)0);
|
||||
if(!m_debayerProgram->link())
|
||||
{
|
||||
qDebug() << "Link failed" << m_debayerProgram->log();
|
||||
}
|
||||
|
||||
m_vaoThumb->bind();
|
||||
|
||||
m_thumbnailProgram = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram);
|
||||
m_thumbnailProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/thumb.vert");
|
||||
m_thumbnailProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/thumb.frag");
|
||||
if(f3)f3->glBindFragDataLocation(m_program->programId(), 0, "color");
|
||||
if(f3)f3->glBindFragDataLocation(m_thumbnailProgram->programId(), 0, "color");
|
||||
m_thumbnailProgram->bind();
|
||||
m_thumbnailProgram->enableAttributeArray("qt_Vertex");
|
||||
m_thumbnailProgram->setAttributeBuffer("qt_Vertex", GL_FLOAT, 0, 2, sizeof(float)*4);
|
||||
@@ -410,6 +491,9 @@ void ImageWidget::initializeGL()
|
||||
|
||||
m_transferOptions = std::unique_ptr<QOpenGLPixelTransferOptions>(new QOpenGLPixelTransferOptions);
|
||||
m_transferOptions->setAlignment(1);
|
||||
|
||||
if(m_rawImage)
|
||||
setImage(m_rawImage, m_currentImg);
|
||||
}
|
||||
|
||||
void ImageWidget::dragEnterEvent(QDragEnterEvent *event)
|
||||
@@ -545,6 +629,41 @@ void ImageWidget::thumbSelect(QMouseEvent *event)
|
||||
}
|
||||
}
|
||||
|
||||
void ImageWidget::debayer()
|
||||
{
|
||||
if(m_debayerTex > 0 || !m_superpixel || !m_bwImg || m_imgWidth < 0)return;
|
||||
|
||||
QOpenGLFramebufferObject fbo(m_imgWidth, m_imgHeight, QOpenGLFramebufferObject::NoAttachment, GL_TEXTURE_2D, GL_RGBA16);
|
||||
fbo.bind();
|
||||
|
||||
f->glViewport(0, 0, m_imgWidth, m_imgHeight);
|
||||
|
||||
m_debayerProgram->bind();
|
||||
m_image->bind(0);
|
||||
f->glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
|
||||
fbo.release();
|
||||
f->glViewport(0, 0, m_width, m_height);
|
||||
m_debayerTex = fbo.takeTexture();
|
||||
f->glBindTexture(GL_TEXTURE_2D, m_debayerTex);
|
||||
f->glGenerateMipmap(GL_TEXTURE_2D);
|
||||
f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
f->glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||
|
||||
int size = std::max(m_imgWidth, m_imgHeight);
|
||||
int level = 0;
|
||||
while(size >>= 1)level++;
|
||||
int w,h;
|
||||
f3->glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &w);
|
||||
f3->glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_HEIGHT, &h);
|
||||
uint16_t pixel[w*h*4];
|
||||
f3->glGetTexImage(GL_TEXTURE_2D, level, GL_RGBA, GL_UNSIGNED_SHORT, pixel);
|
||||
float maxRGB = std::max(std::max(pixel[0], pixel[1]), pixel[2]);
|
||||
m_whiteBalance[0] = maxRGB / pixel[0];
|
||||
m_whiteBalance[1] = maxRGB / pixel[1];
|
||||
m_whiteBalance[2] = maxRGB / pixel[2];
|
||||
}
|
||||
|
||||
ImageScrollAreaGL::ImageScrollAreaGL(Database *database, QWidget *parent) : QWidget(parent)
|
||||
{
|
||||
QGridLayout *layout = new QGridLayout(this);
|
||||
|
||||
@@ -32,6 +32,7 @@ class ImageWidget : public QOpenGLWidget
|
||||
QTimer *m_updateTimer;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_program;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_thumbnailProgram;
|
||||
std::unique_ptr<QOpenGLShaderProgram> m_debayerProgram;
|
||||
std::unique_ptr<QOpenGLBuffer> m_buffer;
|
||||
std::unique_ptr<QOpenGLBuffer> m_bufferSizes;
|
||||
std::unique_ptr<QOpenGLTexture> m_image;
|
||||
@@ -39,6 +40,7 @@ class ImageWidget : public QOpenGLWidget
|
||||
std::unique_ptr<QOpenGLVertexArrayObject> m_vaoThumb;
|
||||
std::unique_ptr<QOpenGLPixelTransferOptions> m_transferOptions;
|
||||
std::unique_ptr<QOpenGLTexture> m_thumbnailTexture;
|
||||
GLuint m_debayerTex = 0;
|
||||
std::shared_ptr<RawImage> m_rawImage;
|
||||
std::shared_ptr<WCSData> m_wcs;
|
||||
int m_width, m_height;
|
||||
@@ -50,6 +52,7 @@ class ImageWidget : public QOpenGLWidget
|
||||
float m_range;
|
||||
float m_dx, m_dy;
|
||||
float m_scale;
|
||||
float m_whiteBalance[3];
|
||||
bool m_blockRepaint;
|
||||
bool m_bwImg;
|
||||
bool m_invert;
|
||||
@@ -57,6 +60,7 @@ class ImageWidget : public QOpenGLWidget
|
||||
bool m_showThumbnails;
|
||||
bool m_selecting;
|
||||
bool m_sizesDirty;
|
||||
bool m_srgb;
|
||||
int m_thumbnailCount;
|
||||
QVector<ImageThumb> m_thumnails;
|
||||
Database *m_database;
|
||||
@@ -87,6 +91,7 @@ protected:
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void thumbSelect(QMouseEvent *event);
|
||||
void debayer();
|
||||
signals:
|
||||
void fileDropped(const QString &path);
|
||||
void status(const QString &value, const QString &pixelCoords, const QString &celestialCoords);
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
find_program(XDG-DESKTOP-MENU_EXECUTABLE xdg-desktop-menu)
|
||||
find_program(XDG-ICON-RESOURCE_EXECUTABLE xdg-icon-resource)
|
||||
execute_process(COMMAND ${XDG-DESKTOP-MENU_EXECUTABLE} install --novendor org.nou.tenmon.desktop WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 32 org.nou.tenmon.png org.nou.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-DESKTOP-MENU_EXECUTABLE} install --novendor space.nouspiro.tenmon.desktop WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 64 space.nouspiro.tenmon.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
execute_process(COMMAND ${XDG-ICON-RESOURCE_EXECUTABLE} install --novendor --size 128 space.nouspiro.tenmon_128.png space.nouspiro.tenmon WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR})
|
||||
|
||||
+66
-21
@@ -14,6 +14,11 @@
|
||||
#include "starfit.h"
|
||||
#include "wcslib/wcshdr.h"
|
||||
|
||||
#ifdef COLOR_MANAGMENT
|
||||
#include <QColorSpace>
|
||||
static pcl::ICCProfile sRgbIccProfile((void*)QColorSpace(QColorSpace::SRgb).iccProfile().data());
|
||||
#endif
|
||||
|
||||
LoadRunable::LoadRunable(const QString &file, Image *receiver, AnalyzeLevel level, bool thumbnail) :
|
||||
m_file(file),
|
||||
m_receiver(receiver),
|
||||
@@ -83,17 +88,17 @@ bool loadRAW(const QString path, ImageInfoData &info, RawImage **image)
|
||||
if(!image)
|
||||
return false;
|
||||
|
||||
LibRaw raw;
|
||||
raw.open_file(path.toLocal8Bit().data());
|
||||
raw.imgdata.params.half_size = true;
|
||||
raw.imgdata.params.use_camera_wb = true;
|
||||
raw.imgdata.params.user_flip = 0;
|
||||
if(raw.unpack())
|
||||
std::unique_ptr<LibRaw> raw = std::make_unique<LibRaw>();
|
||||
raw->open_file(path.toLocal8Bit().data());
|
||||
raw->imgdata.params.half_size = true;
|
||||
raw->imgdata.params.use_camera_wb = true;
|
||||
raw->imgdata.params.user_flip = 0;
|
||||
if(raw->unpack())
|
||||
return false;
|
||||
|
||||
if(image)
|
||||
{
|
||||
libraw_rawdata_t rawdata = raw.imgdata.rawdata;
|
||||
libraw_rawdata_t rawdata = raw->imgdata.rawdata;
|
||||
size_t size = rawdata.sizes.width*rawdata.sizes.height;
|
||||
|
||||
std::vector<uint16_t> out;
|
||||
@@ -115,14 +120,14 @@ bool loadRAW(const QString path, ImageInfoData &info, RawImage **image)
|
||||
memcpy((*image)->data(), &out[0], sizeof(uint16_t)*d);
|
||||
}
|
||||
|
||||
QString shutterSpeed = QString::number(raw.imgdata.other.shutter);
|
||||
if(raw.imgdata.other.shutter < 1)
|
||||
QString shutterSpeed = QString::number(raw->imgdata.other.shutter);
|
||||
if(raw->imgdata.other.shutter < 1)
|
||||
{
|
||||
shutterSpeed = QString("1/%1s").arg(1.0f/raw.imgdata.other.shutter);
|
||||
shutterSpeed = QString("1/%1s").arg(1.0f/raw->imgdata.other.shutter);
|
||||
}
|
||||
//info.append(StringPair(QObject::tr("Width"), QString::number(rawImg->width)));
|
||||
//info.append(StringPair(QObject::tr("Height"), QString::number(rawImg->height)));
|
||||
info.info.append({QObject::tr("ISO"), QString::number(raw.imgdata.other.iso_speed)});
|
||||
info.info.append({QObject::tr("ISO"), QString::number(raw->imgdata.other.iso_speed)});
|
||||
info.info.append({QObject::tr("Shutter speed"), shutterSpeed});
|
||||
#if LIBRAW_MINOR_VERSION>=19
|
||||
// info.append(StringPair(QObject::tr("Camera temperature"), QString::number(raw.imgdata.other.CameraTemperature)));
|
||||
@@ -275,17 +280,32 @@ bool loadFITS(const QString path, ImageInfoData &info, RawImage **image)
|
||||
char err[100];
|
||||
fits_get_errstatus(status, err);
|
||||
info.info.append({QObject::tr("Error"), QString(err)});
|
||||
qDebug() << err;
|
||||
qDebug() << "Failed to load FITS file" << err;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#include "pcl/ICCProfileTransformation.h"
|
||||
|
||||
template<typename T, typename PCLtype, int CVtype>
|
||||
bool loadPCLImage(pcl::XISFReader &xisf, RawImage **image)
|
||||
{
|
||||
PCLtype pclImage;
|
||||
xisf.ReadImage(pclImage);
|
||||
pclImage.Status().DisableInitialization();
|
||||
|
||||
#ifdef COLOR_MANAGMENT
|
||||
pcl::ICCProfile iccProfile = xisf.ReadICCProfile();
|
||||
if(iccProfile.IsProfile() && iccProfile != sRgbIccProfile)
|
||||
{
|
||||
pcl::ICCProfileTransformation iccTran;
|
||||
iccTran.DisableParallelProcessing();
|
||||
iccTran.Add(iccProfile);
|
||||
iccTran.Add(sRgbIccProfile);
|
||||
iccTran >> pclImage;
|
||||
}
|
||||
#endif
|
||||
|
||||
int numChannels = pclImage.NumberOfChannels();
|
||||
cv::Mat cvImg[numChannels];
|
||||
@@ -315,6 +335,7 @@ bool loadXISF(const QString &path, ImageInfoData &info, RawImage **image)
|
||||
{
|
||||
try
|
||||
{
|
||||
pcl::XISF::EnsurePTLUTInitialized();
|
||||
pcl::String pclPath = path.utf16();
|
||||
pcl::XISFReader xisf;
|
||||
xisf.Open(pclPath);
|
||||
@@ -328,7 +349,15 @@ bool loadXISF(const QString &path, ImageInfoData &info, RawImage **image)
|
||||
{
|
||||
info.fitsHeader.append(fits);
|
||||
}
|
||||
auto imageproperties = xisf.ReadImageProperties();
|
||||
for(auto prop : imageproperties)
|
||||
{
|
||||
info.fitsHeader.append(prop);
|
||||
}
|
||||
|
||||
info.wcs = std::make_shared<WCSData>(xisf.ImageInfo().width, xisf.ImageInfo().height, info.fitsHeader);
|
||||
info.info.append({QObject::tr("Width"), QString::number(xisf.ImageInfo().width)});
|
||||
info.info.append({QObject::tr("Height"), QString::number(xisf.ImageInfo().height)});
|
||||
if(!info.wcs->valid())info.wcs.reset();
|
||||
|
||||
if(floatType && bps == 32)
|
||||
@@ -347,7 +376,7 @@ bool loadXISF(const QString &path, ImageInfoData &info, RawImage **image)
|
||||
catch (pcl::Error err)
|
||||
{
|
||||
info.info.append(QPair<QString, QString>("Error", err.FormatInfo().ToUTF8().c_str()));
|
||||
qDebug() << err.FormatInfo().ToUTF8().c_str();
|
||||
qDebug() << "Failed to load XISF" << err.FormatInfo().ToUTF8().c_str();
|
||||
}
|
||||
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
|
||||
return false;
|
||||
@@ -366,24 +395,31 @@ void LoadRunable::run()
|
||||
|
||||
RawImage *rawImage = nullptr;
|
||||
bool raw = false;
|
||||
timer.start();
|
||||
if(m_file.endsWith(".CR2", Qt::CaseInsensitive) || m_file.endsWith(".NEF", Qt::CaseInsensitive) || m_file.endsWith(".DNG", Qt::CaseInsensitive))
|
||||
{
|
||||
timer.start();
|
||||
loadRAW(m_file, info, &rawImage);
|
||||
raw = true;
|
||||
qDebug() << "LoadRaw" << timer.elapsed();
|
||||
qDebug() << "LoadRAW" << timer.elapsed();
|
||||
}
|
||||
else if(m_file.endsWith(".FIT", Qt::CaseInsensitive) || m_file.endsWith(".FITS", Qt::CaseInsensitive))
|
||||
{
|
||||
loadFITS(m_file, info, &rawImage);
|
||||
qDebug() << "LoadFITS" << timer.elapsed();
|
||||
}
|
||||
else if(m_file.endsWith(".XISF", Qt::CaseInsensitive))
|
||||
{
|
||||
loadXISF(m_file, info, &rawImage);
|
||||
qDebug() << "LoadXISF" << timer.elapsed();
|
||||
}
|
||||
else
|
||||
{
|
||||
QImage img(m_file);
|
||||
#ifdef COLOR_MANAGMENT
|
||||
if(img.colorSpace().isValid() && img.colorSpace() != QColorSpace::SRgb)
|
||||
img.convertToColorSpace(QColorSpace::SRgb);
|
||||
#endif
|
||||
|
||||
ExifData *exif = exif_data_new_from_file(m_file.toLocal8Bit().constData());
|
||||
info.info.append({QObject::tr("Width"), QString::number(img.width())});
|
||||
info.info.append({QObject::tr("Height"), QString::number(img.height())});
|
||||
@@ -400,8 +436,9 @@ void LoadRunable::run()
|
||||
{
|
||||
double mean, median, min, max, mad;
|
||||
double stdDev;
|
||||
uint32_t saturated;
|
||||
timer.start();
|
||||
rawImage->imageStats(&mean, &stdDev, &median, &min, &max, &mad);
|
||||
rawImage->imageStats(&mean, &stdDev, &median, &min, &max, &mad, &saturated);
|
||||
qDebug() << "image stats" << timer.restart();
|
||||
info.info.append({QObject::tr("Mean"), QString::number(mean)});
|
||||
info.info.append({QObject::tr("Standart deviation"), QString::number(stdDev)});
|
||||
@@ -409,6 +446,7 @@ void LoadRunable::run()
|
||||
info.info.append({QObject::tr("Minimum"), QString::number(min)});
|
||||
info.info.append({QObject::tr("Maximum"), QString::number(max)});
|
||||
info.info.append({QObject::tr("MAD"), QString::number(mad)});
|
||||
info.info.append({QObject::tr("Saturated"), QString::number(100.0 * saturated / rawImage->size()) + "%"});
|
||||
|
||||
if(m_analyzeLevel >= Peaks)
|
||||
{
|
||||
@@ -426,7 +464,7 @@ void LoadRunable::run()
|
||||
// drawPeaks(img, peaks);
|
||||
qDebug() << "draw peaks" << timer.restart();
|
||||
info.info.append({QObject::tr("Peaks"), QString::number(numPeaks)});
|
||||
info.info.append({QObject::tr("Peaks draw"), QString::number(peaks.size())});
|
||||
//info.info.append({QObject::tr("Peaks draw"), QString::number(peaks.size())});
|
||||
|
||||
if(m_analyzeLevel>= Stars)
|
||||
{
|
||||
@@ -504,6 +542,12 @@ bool readXISFHeader(const QString &path, ImageInfoData &info)
|
||||
{
|
||||
info.fitsHeader.append(fits);
|
||||
}
|
||||
|
||||
auto imageproperties = xisf.ReadImageProperties();
|
||||
for(auto prop : imageproperties)
|
||||
{
|
||||
info.fitsHeader.append(prop);
|
||||
}
|
||||
info.wcs = std::make_shared<WCSData>(xisf.ImageInfo().width, xisf.ImageInfo().height, info.fitsHeader);
|
||||
if(!info.wcs->valid())info.wcs.reset();
|
||||
}
|
||||
@@ -515,9 +559,10 @@ bool readXISFHeader(const QString &path, ImageInfoData &info)
|
||||
return true;
|
||||
}
|
||||
|
||||
ConvertRunable::ConvertRunable(const QString &in, const QString &out) :
|
||||
ConvertRunable::ConvertRunable(const QString &in, const QString &out, const QString &format) :
|
||||
m_infile(in),
|
||||
m_outfile(out)
|
||||
m_outfile(out),
|
||||
m_format(format)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -622,7 +667,7 @@ void ConvertRunable::run()
|
||||
|
||||
if(rawimage)
|
||||
{
|
||||
if(m_outfile.endsWith(".XISF", Qt::CaseInsensitive))
|
||||
if(m_format == "XISF")
|
||||
{
|
||||
pcl::XISFOptions options;
|
||||
pcl::FITSKeywordArray fitskeywords;
|
||||
@@ -649,7 +694,7 @@ void ConvertRunable::run()
|
||||
|
||||
}
|
||||
|
||||
if(m_outfile.endsWith(".FITS", Qt::CaseInsensitive) || m_outfile.endsWith(".FIT", Qt::CaseInsensitive))
|
||||
if(m_format == "FITS")
|
||||
{
|
||||
int status = 0;
|
||||
fitsfile *fw;
|
||||
|
||||
+2
-1
@@ -25,8 +25,9 @@ class ConvertRunable : public QRunnable
|
||||
{
|
||||
QString m_infile;
|
||||
QString m_outfile;
|
||||
QString m_format;
|
||||
public:
|
||||
ConvertRunable(const QString &in, const QString &out);
|
||||
ConvertRunable(const QString &in, const QString &out, const QString &format);
|
||||
void run() override;
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ int main(int argc, char *argv[])
|
||||
QApplication a(argc, argv);
|
||||
a.setOrganizationName("nou");
|
||||
a.setApplicationName("Tenmon");
|
||||
a.setWindowIcon(QIcon(":/org.nou.tenmon.png"));
|
||||
a.setWindowIcon(QIcon(":/space.nouspiro.tenmon.png"));
|
||||
|
||||
QTranslator translator;
|
||||
QTranslator translator2;
|
||||
|
||||
+142
-42
@@ -13,13 +13,16 @@
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <QSettings>
|
||||
#include <QCoreApplication>
|
||||
#include <QGuiApplication>
|
||||
#include <QThreadPool>
|
||||
#include <QStatusBar>
|
||||
#include <QImageReader>
|
||||
#include <QMimeDatabase>
|
||||
#include "loadrunable.h"
|
||||
#include "markedfiles.h"
|
||||
#include "about.h"
|
||||
#include "statusbar.h"
|
||||
#include "settingsdialog.h"
|
||||
|
||||
#ifdef __linux__
|
||||
#include <sys/ioctl.h>
|
||||
@@ -27,6 +30,8 @@
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
bool moveToTrash(const QString &path);
|
||||
|
||||
int MainWindow::socketPair[2] = {0, 0};
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
@@ -34,6 +39,27 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
qRegisterMetaType<ImageInfoData>("ImageInfoData");
|
||||
qRegisterMetaType<RawImage*>("RawImage");
|
||||
|
||||
SettingsDialog::loadSettings();
|
||||
|
||||
QStringList nameFilter;
|
||||
_saveFilter = tr("FITS (*.fits *.fit);;XISF (*.xisf);;");
|
||||
_openFilter = tr("Images (");
|
||||
QMimeDatabase db;
|
||||
auto supportedFormats = QImageReader::supportedMimeTypes();
|
||||
QStringList filters;
|
||||
for(auto format : supportedFormats)
|
||||
{
|
||||
QMimeType mimeType = db.mimeTypeForName(format);
|
||||
_saveFilter.append(mimeType.filterString() + ";;");
|
||||
_openFilter.append("*.");
|
||||
_openFilter.append(mimeType.suffixes().join(" *."));
|
||||
_openFilter.append(" ");
|
||||
nameFilter.append(mimeType.suffixes());
|
||||
}
|
||||
_openFilter.append("*.fit *.fits *.xisf *.cr2 *.nef *.dng)");
|
||||
_openFilter.append(tr(";;All files (*)"));
|
||||
nameFilter.append({"fit", "fits", "xisf", "cr2", "nef", "dng"});
|
||||
|
||||
m_info = new ImageInfo(this);
|
||||
QDockWidget *infoDock = new QDockWidget(tr("Image info"), this);
|
||||
infoDock->setWidget(m_info);
|
||||
@@ -59,7 +85,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
connect(m_stretchPanel, &StretchToolbar::invert, m_imageGL->imageWidget(), &ImageWidget::invert);
|
||||
connect(m_stretchPanel, &StretchToolbar::superPixel, m_imageGL->imageWidget(), &ImageWidget::superPixel);
|
||||
|
||||
m_ringList = new ImageRingList(m_database, this);
|
||||
m_ringList = new ImageRingList(m_database, nameFilter, this);
|
||||
m_filesystem = new FilesystemWidget(m_ringList, this);
|
||||
connect(m_filesystem, SIGNAL(fileSelected(int)), this, SLOT(loadFile(int)));
|
||||
|
||||
@@ -105,11 +131,13 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
fileMenu->addAction(tr("Open"), this, SLOT(loadFile()), QKeySequence::Open);
|
||||
fileMenu->addAction(tr("Save as"), this, SLOT(saveAs()), QKeySequence::Save);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(tr("Copy marked files"), this, SLOT(copyMarked()));
|
||||
fileMenu->addAction(tr("Move marked files"), this, SLOT(moveMarked()));
|
||||
fileMenu->addAction(tr("Copy marked files"), this, SLOT(copyMarked()), Qt::Key_F5);
|
||||
fileMenu->addAction(tr("Move marked files"), this, SLOT(moveMarked()), Qt::Key_F6);
|
||||
fileMenu->addAction(tr("Move marked files to trash"), this, &MainWindow::deleteMarked, QKeySequence::Delete);
|
||||
fileMenu->addSeparator();
|
||||
fileMenu->addAction(tr("Index directory"), this, SLOT(indexDir()));
|
||||
fileMenu->addAction(tr("Reindex files"), this, SLOT(reindex()));
|
||||
fileMenu->addAction(tr("Export database to CSV"), this, &MainWindow::exportCSV);
|
||||
fileMenu->addSeparator();
|
||||
QAction *liveModeAction = fileMenu->addAction(tr("Live mode"), this, SLOT(liveMode(bool)));
|
||||
liveModeAction->setCheckable(true);
|
||||
@@ -117,13 +145,17 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
exitAction->setShortcut(QKeySequence::Quit);
|
||||
menuBar()->addMenu(fileMenu);
|
||||
|
||||
QMenu *editMenu = new QMenu(tr("Edit"), this);
|
||||
editMenu->addAction(tr("Settings"), this, &MainWindow::showSettingsDialog);
|
||||
menuBar()->addMenu(editMenu);
|
||||
|
||||
QMenu *viewMenu = new QMenu(tr("View"), this);
|
||||
viewMenu->addAction(tr("Zoom In"), m_imageGL, SLOT(zoomIn()), QKeySequence::ZoomIn);
|
||||
viewMenu->addAction(tr("Zoom Out"), m_imageGL, SLOT(zoomOut()), QKeySequence::ZoomOut);
|
||||
viewMenu->addAction(tr("Best Fit"), m_imageGL, SLOT(bestFit()), QKeySequence("Ctrl+1"));
|
||||
viewMenu->addAction(tr("100%"), m_imageGL, SLOT(oneToOne()));
|
||||
viewMenu->addAction(tr("Fullscreen"), this, SLOT(toggleFullScreen()), Qt::CTRL + Qt::Key_F11);
|
||||
QAction *thumbnailsAction = viewMenu->addAction(tr("Thumbnails"), [this](bool checked){
|
||||
if(SettingsDialog::loadThumbsizes())m_ringList->clearThumbnails();
|
||||
m_imageGL->imageWidget()->allocateThumbnails(m_ringList->imageNames());
|
||||
m_imageGL->imageWidget()->showThumbnail(checked);
|
||||
m_imageGL->setThumbnails(checked ? m_ringList->imageCount() : 0);
|
||||
@@ -134,7 +166,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
menuBar()->addMenu(viewMenu);
|
||||
|
||||
QMenu *selectMenu = new QMenu(tr("Select"), this);
|
||||
selectMenu->addAction(tr("Mark"), this, SLOT(markImage()), Qt::Key_F5);
|
||||
selectMenu->addAction(tr("Mark"), this, SLOT(markImage()), Qt::Key_F7);
|
||||
selectMenu->addAction(tr("Unmark"), this, SLOT(unmarkImage()), Qt::Key_F8);
|
||||
selectMenu->addSeparator();
|
||||
selectMenu->addAction(tr("Mark and next"), this, SLOT(markAndNext()), Qt::Key_M);
|
||||
@@ -190,7 +222,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
_lastDir = standardLocations.first();
|
||||
|
||||
_lastDir = settings.value("mainwindow/lastdir", _lastDir).toString();
|
||||
m_filesystem->setDir(_lastDir);
|
||||
|
||||
QStringList args = QCoreApplication::arguments();
|
||||
args.removeFirst();
|
||||
@@ -208,6 +239,16 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
|
||||
}
|
||||
|
||||
m_imageGL->setFocus();
|
||||
|
||||
// workaround for nasty wayland backend bug https://bugreports.qt.io/browse/QTBUG-87332
|
||||
if(static_cast<QGuiApplication*>(QCoreApplication::instance())->platformName() == "wayland")
|
||||
{
|
||||
infoDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
filesystemDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
databaseViewDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
filetreeDock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable);
|
||||
m_stretchPanel->setFloatable(false);
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
@@ -277,7 +318,10 @@ void MainWindow::closeEvent(QCloseEvent *event)
|
||||
|
||||
void MainWindow::copyOrMove(bool copy)
|
||||
{
|
||||
QString dest = QFileDialog::getExistingDirectory(this, tr("Select destination"), _lastDir);
|
||||
QString dest = QFileDialog::getExistingDirectory(this,
|
||||
tr("Select destination"),
|
||||
_lastDir,
|
||||
QFileDialog::ShowDirsOnly);
|
||||
copyOrMove(copy, dest);
|
||||
}
|
||||
|
||||
@@ -291,8 +335,9 @@ void MainWindow::copyOrMove(bool copy, const QString &dest)
|
||||
QProgressDialog progress(copy ? tr("Copying") : tr("Moving"), tr("Cancel"), 0, files.size(), this);
|
||||
progress.setWindowModality(Qt::WindowModal);
|
||||
progress.show();
|
||||
foreach(const QString &file, files)
|
||||
for(const QString &file : files)
|
||||
{
|
||||
bool result = false;
|
||||
QFileInfo info(file);
|
||||
QFile srcFile(file);
|
||||
QFile dstFile(dir.absoluteFilePath(info.fileName()));
|
||||
@@ -301,7 +346,7 @@ void MainWindow::copyOrMove(bool copy, const QString &dest)
|
||||
continue;
|
||||
|
||||
if(progress.wasCanceled())
|
||||
break;
|
||||
return;
|
||||
#ifdef __linux__
|
||||
if(copy)
|
||||
{
|
||||
@@ -311,20 +356,29 @@ void MainWindow::copyOrMove(bool copy, const QString &dest)
|
||||
{
|
||||
dstFile.remove();
|
||||
dstFile.close();
|
||||
qDebug() << dstFile.fileName();
|
||||
srcFile.copy(dstFile.fileName());
|
||||
result = srcFile.copy(dstFile.fileName());
|
||||
}
|
||||
else result = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
srcFile.rename(dstFile.fileName());
|
||||
result = srcFile.rename(dstFile.fileName());
|
||||
}
|
||||
#else
|
||||
if(copy)
|
||||
srcFile.copy(dstFile.fileName());
|
||||
result = srcFile.copy(dstFile.fileName());
|
||||
else
|
||||
srcFile.rename(dstFile.fileName());
|
||||
result = srcFile.rename(dstFile.fileName());
|
||||
#endif
|
||||
if(!result)
|
||||
{
|
||||
QString t = copy ? tr("Failed to copy") : tr("Failed to move");
|
||||
QString m = copy ? tr("Failed to copy from %1 to %2") : tr("Failed to move from %1 to %2");
|
||||
QMessageBox::StandardButton button = QMessageBox::warning(this, t,
|
||||
m.arg(srcFile.fileName()).arg(dir.absolutePath()),
|
||||
QMessageBox::Ignore | QMessageBox::Abort);
|
||||
if(button == QMessageBox::Abort)return;
|
||||
}
|
||||
progress.setValue(i++);
|
||||
}
|
||||
m_database->clearMarkedFiles();
|
||||
@@ -350,7 +404,10 @@ void MainWindow::pixmapLoaded(Image *image)
|
||||
|
||||
void MainWindow::loadFile()
|
||||
{
|
||||
QString file = QFileDialog::getOpenFileName(this, tr("Open file"), _lastDir, tr("Images (*.jpg *.jpeg *.png *.cr2 *.nef *.dng *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.NEF *.DNG *.FIT *.FITS *.XISF)"));
|
||||
QString file = QFileDialog::getOpenFileName(this,
|
||||
tr("Open file"),
|
||||
_lastDir,
|
||||
_openFilter);
|
||||
loadFile(file);
|
||||
}
|
||||
|
||||
@@ -367,7 +424,6 @@ void MainWindow::loadFile(const QString &path)
|
||||
_lastDir = info.canonicalPath();
|
||||
QSettings settings;
|
||||
settings.setValue("mainwindow/lastdir", _lastDir);
|
||||
m_filesystem->setDir(_lastDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,7 +434,7 @@ void MainWindow::loadFile(int row)
|
||||
|
||||
void MainWindow::indexDir()
|
||||
{
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Index directory"), _lastDir);
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Index directory"), _lastDir, QFileDialog::ShowDirsOnly);
|
||||
indexDir(dir);
|
||||
}
|
||||
|
||||
@@ -402,35 +458,45 @@ void MainWindow::reindex()
|
||||
void MainWindow::saveAs()
|
||||
{
|
||||
QString selectedFilter;
|
||||
QString file = QFileDialog::getSaveFileName(this, tr("Save as"), _lastDir, tr("JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)"), &selectedFilter);
|
||||
QString file = QFileDialog::getSaveFileName(this,
|
||||
tr("Save as"),
|
||||
_lastDir,
|
||||
_saveFilter,
|
||||
&selectedFilter);
|
||||
auto filterToFormat = [](const QString &file, const QString &filter) -> const char*
|
||||
{
|
||||
QString suffix = QFileInfo(file).suffix();
|
||||
if(!suffix.compare("jpg", Qt::CaseInsensitive) || !suffix.compare("jpeg", Qt::CaseInsensitive))return "JPEG";
|
||||
if(!suffix.compare("png", Qt::CaseInsensitive))return "PNG";
|
||||
if(!suffix.compare("fits", Qt::CaseInsensitive) || !suffix.compare("fit", Qt::CaseInsensitive))return "FITS";
|
||||
if(!suffix.compare("xisf", Qt::CaseInsensitive))return "XISF";
|
||||
if(filter.contains("png"))return "PNG";
|
||||
if(filter.contains("fits"))return "FITS";
|
||||
if(filter.contains("xisf"))return "XISF";
|
||||
return "JPEG";
|
||||
};
|
||||
|
||||
if(!file.isEmpty())
|
||||
{
|
||||
QFileInfo info(file);
|
||||
if(info.suffix().isEmpty())
|
||||
{
|
||||
if(selectedFilter.contains("jpg"))file += ".jpg";
|
||||
if(selectedFilter.contains("png"))file += ".png";
|
||||
if(selectedFilter.contains("fits"))file += ".fits";
|
||||
if(selectedFilter.contains("xisf"))file += ".xisf";
|
||||
}
|
||||
QString format = filterToFormat(file, selectedFilter);
|
||||
|
||||
if(file.endsWith(".fits") || file.endsWith(".xisf"))
|
||||
if(format == "FITS" || format == "XISF")
|
||||
{
|
||||
convert(file);
|
||||
convert(file, format);
|
||||
}
|
||||
else
|
||||
{
|
||||
QImage img = m_imageGL->imageWidget()->renderToImage();
|
||||
if(!img.isNull())
|
||||
img.save(file);
|
||||
img.save(file, filterToFormat(file, selectedFilter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::convert(const QString &outfile)
|
||||
void MainWindow::convert(const QString &outfile, const QString &format)
|
||||
{
|
||||
QString file = m_ringList->currentImage()->name();
|
||||
QThreadPool::globalInstance()->start(new ConvertRunable(file, outfile));
|
||||
QThreadPool::globalInstance()->start(new ConvertRunable(file, outfile, format));
|
||||
}
|
||||
|
||||
void MainWindow::markImage()
|
||||
@@ -489,18 +555,32 @@ void MainWindow::moveMarked()
|
||||
copyOrMove(false);
|
||||
}
|
||||
|
||||
void MainWindow::toggleFullScreen()
|
||||
void MainWindow::deleteMarked()
|
||||
{
|
||||
if(isFullScreen())
|
||||
QStringList files = m_database->getMarkedFiles();
|
||||
if(QMessageBox::question(this, tr("Move files to trash?"), tr("Do you want to move %1 files to trash?").arg(files.size())) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
QProgressDialog progress(tr("Moving marked files to trash"), tr("Cancel"), 0, files.size(), this);
|
||||
progress.setWindowModality(Qt::WindowModal);
|
||||
progress.show();
|
||||
int i = 0;
|
||||
for(const QString &file : files)
|
||||
{
|
||||
showNormal();
|
||||
if(_maximized)showMaximized();
|
||||
}
|
||||
else
|
||||
{
|
||||
_maximized = isMaximized();
|
||||
showFullScreen();
|
||||
if(!QFile::exists(file))
|
||||
continue;
|
||||
|
||||
if(progress.wasCanceled())
|
||||
return;
|
||||
|
||||
if(!moveToTrash(file))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Failed to move file to trash"), tr("Failed to move file to trash %1").arg(file));
|
||||
return;
|
||||
}
|
||||
progress.setValue(i++);
|
||||
}
|
||||
m_database->clearMarkedFiles();
|
||||
}
|
||||
|
||||
void MainWindow::liveMode(bool active)
|
||||
@@ -525,10 +605,30 @@ void MainWindow::starFinder(bool findStars)
|
||||
|
||||
void MainWindow::showMarkFilesDialog()
|
||||
{
|
||||
MarkedFiles markedFiles;
|
||||
MarkedFiles markedFiles(this);
|
||||
markedFiles.exec();
|
||||
}
|
||||
|
||||
void MainWindow::showSettingsDialog()
|
||||
{
|
||||
SettingsDialog settingsDialog(this);
|
||||
connect(&settingsDialog, &SettingsDialog::preloadChanged, m_ringList, &ImageRingList::setPreload);
|
||||
settingsDialog.exec();
|
||||
}
|
||||
|
||||
void MainWindow::exportCSV()
|
||||
{
|
||||
QStringList documentDir = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation);
|
||||
if(documentDir.empty())documentDir.append("");
|
||||
QString file = QFileDialog::getSaveFileName(this,
|
||||
tr("Save as"),
|
||||
documentDir.first(),
|
||||
tr("CSV file (*.csv)"));
|
||||
|
||||
if(!file.isEmpty())
|
||||
m_databaseView->exportCSV(file);
|
||||
}
|
||||
|
||||
void MainWindow::updateWindowTitle()
|
||||
{
|
||||
ImagePtr ptr = m_ringList->currentImage();
|
||||
|
||||
+6
-2
@@ -28,6 +28,8 @@ class MainWindow : public QMainWindow
|
||||
QSocketNotifier *socketNotifier;
|
||||
QString _lastDir;
|
||||
bool _maximized;
|
||||
QString _openFilter;
|
||||
QString _saveFilter;
|
||||
public:
|
||||
MainWindow(QWidget *parent = 0);
|
||||
~MainWindow() override;
|
||||
@@ -50,19 +52,21 @@ protected slots:
|
||||
void indexDir(const QString &dir);
|
||||
void reindex();
|
||||
void saveAs();
|
||||
void convert(const QString &outfile);
|
||||
void convert(const QString &outfile, const QString &format);
|
||||
void markImage();
|
||||
void unmarkImage();
|
||||
void markAndNext();
|
||||
void unmarkAndNext();
|
||||
void copyMarked();
|
||||
void moveMarked();
|
||||
void toggleFullScreen();
|
||||
void deleteMarked();
|
||||
void liveMode(bool active);
|
||||
void imageStats(bool imageStats);
|
||||
void peakFinder(bool findPeaks);
|
||||
void starFinder(bool findStars);
|
||||
void showMarkFilesDialog();
|
||||
void showSettingsDialog();
|
||||
void exportCSV();
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
|
||||
+30
-1
@@ -1,5 +1,10 @@
|
||||
#include "rawimage.h"
|
||||
|
||||
int THUMB_SIZE = 128;
|
||||
int THUMB_SIZE_BORDER = 138;
|
||||
int THUMB_SIZE_BORDER_Y = 158;
|
||||
double SATURATION = 0.95;
|
||||
|
||||
RawImage::ImgType CV2Type(int cvtype)
|
||||
{
|
||||
switch (cvtype)
|
||||
@@ -67,6 +72,7 @@ RawImage::RawImage(cv::Mat &img)
|
||||
{
|
||||
m_img = img;
|
||||
m_stats = false;
|
||||
scaleToUnit();
|
||||
}
|
||||
|
||||
RawImage::RawImage(const RawImage &d)
|
||||
@@ -79,6 +85,7 @@ RawImage::RawImage(const RawImage &d)
|
||||
m_max = d.m_max;
|
||||
m_mad = d.m_mad;
|
||||
m_stats = d.m_stats;
|
||||
m_saturated = d.m_saturated;
|
||||
}
|
||||
|
||||
RawImage::RawImage(const QImage &img)
|
||||
@@ -121,7 +128,7 @@ RawImage::RawImage(const QImage &img)
|
||||
m_stats = false;
|
||||
}
|
||||
|
||||
bool RawImage::imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad)
|
||||
bool RawImage::imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad, uint32_t *saturated)
|
||||
{
|
||||
if(!m_stats)calcStats();
|
||||
if(mean)*mean = m_mean;
|
||||
@@ -130,6 +137,7 @@ bool RawImage::imageStats(double *mean, double *stdDev, double *median, double *
|
||||
if(min)*min = m_min;
|
||||
if(max)*max = m_max;
|
||||
if(mad)*mad = m_mad;
|
||||
if(saturated)*saturated = m_saturated;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -171,6 +179,12 @@ void RawImage::calcStats()
|
||||
}
|
||||
}
|
||||
if(img.type() == CV_32F)m_median /= histSize;
|
||||
|
||||
int threshold = SATURATION * histSize;
|
||||
m_saturated = 0;
|
||||
for(int i = histSize-1; i >= threshold; i--)
|
||||
m_saturated += hist.at<float>(0, i);
|
||||
|
||||
cv::Mat absDev;
|
||||
img.convertTo(absDev, CV_32F, 1, -m_median);
|
||||
absDev = cv::abs(absDev);
|
||||
@@ -378,3 +392,18 @@ bool RawImage::pixel(int x, int y, QVector3D &rgb) const
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RawImage::scaleToUnit()
|
||||
{
|
||||
if(CV_MAT_DEPTH(m_img.type()) == CV_32F)
|
||||
{
|
||||
double min, max;
|
||||
cv::minMaxIdx(m_img, &min, &max);
|
||||
if(min < 0 || max > 1)
|
||||
{
|
||||
float scale = 1.0 / (max - min);
|
||||
float zero = min * scale;
|
||||
m_img = m_img * scale - zero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -10,9 +10,9 @@
|
||||
#include <QImage>
|
||||
#include <QVector3D>
|
||||
|
||||
const int THUMB_SIZE = 128;
|
||||
const int THUMB_SIZE_BORDER = 138;
|
||||
const int THUMB_SIZE_BORDER_Y = 158;
|
||||
extern int THUMB_SIZE;
|
||||
extern int THUMB_SIZE_BORDER;
|
||||
extern int THUMB_SIZE_BORDER_Y;
|
||||
|
||||
class Peak
|
||||
{
|
||||
@@ -48,6 +48,7 @@ protected:
|
||||
double m_max;
|
||||
double m_mad;
|
||||
float m_thumbAspect;
|
||||
uint32_t m_saturated;
|
||||
public:
|
||||
enum ImgType
|
||||
{
|
||||
@@ -66,7 +67,7 @@ public:
|
||||
RawImage(cv::Mat &img);
|
||||
RawImage(const RawImage &d);
|
||||
RawImage(const QImage &img);
|
||||
bool imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad);
|
||||
bool imageStats(double *mean, double *stdDev, double *median, double *min, double *max, double *mad, uint32_t *saturated);
|
||||
void calcStats();
|
||||
void rect(int &x, int &y, int w, int h, std::vector<double> &r) const;
|
||||
int findPeaks(double background, double distance, std::vector<Peak> &peaks) const;
|
||||
@@ -84,6 +85,7 @@ public:
|
||||
float thumbAspect() const;
|
||||
const cv::Mat& mat() const;
|
||||
bool pixel(int x, int y, QVector3D &rgb) const;
|
||||
void scaleToUnit();
|
||||
};
|
||||
|
||||
#endif // RAWIMAGE_H
|
||||
|
||||
+3
-1
@@ -3,7 +3,7 @@
|
||||
<file>invert.png</file>
|
||||
<file>nuke.png</file>
|
||||
<file>bayer.png</file>
|
||||
<file>org.nou.tenmon.png</file>
|
||||
<file>space.nouspiro.tenmon.png</file>
|
||||
<file>nuke_a.png</file>
|
||||
<file>about/tenmon</file>
|
||||
<file>about/pcl</file>
|
||||
@@ -16,6 +16,8 @@
|
||||
<file>shaders/image.vert</file>
|
||||
<file>shaders/thumb.frag</file>
|
||||
<file>shaders/thumb.vert</file>
|
||||
<file>shaders/debayer.frag</file>
|
||||
<file>shaders/debayer.vert</file>
|
||||
</qresource>
|
||||
<qresource lang="en" prefix="/">
|
||||
<file alias="help">about/help_en</file>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "settingsdialog.h"
|
||||
#include <QFormLayout>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLabel>
|
||||
#include <QSettings>
|
||||
#include <QApplication>
|
||||
#include "rawimage.h"
|
||||
|
||||
extern int DEFAULT_WIDTH;
|
||||
extern double SATURATION;
|
||||
|
||||
class EvenNumber : public QSpinBox
|
||||
{
|
||||
public:
|
||||
explicit EvenNumber(QWidget *parent) : QSpinBox(parent){}
|
||||
protected:
|
||||
QValidator::State validate(QString &text, int &) const
|
||||
{
|
||||
bool ok;
|
||||
int val = text.toInt(&ok);
|
||||
if(ok && (val & 1) == 0)return QValidator::Acceptable;
|
||||
if(ok)return QValidator::Intermediate;
|
||||
return QValidator::Invalid;
|
||||
}
|
||||
void fixup(QString &input) const
|
||||
{
|
||||
bool ok;
|
||||
int val = input.toInt(&ok);
|
||||
val -= val & 1;
|
||||
input = QString::number(val);
|
||||
}
|
||||
};
|
||||
|
||||
SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
|
||||
{
|
||||
QFormLayout *layout = new QFormLayout(this);
|
||||
setWindowTitle(tr("Settings"));
|
||||
|
||||
QSettings settings;
|
||||
|
||||
m_preloadImages = new QSpinBox(this);
|
||||
m_preloadImages->setRange(0, 8);
|
||||
m_preloadImages->setValue(settings.value("settings/preloadimagecount", DEFAULT_WIDTH).toInt());
|
||||
m_preloadImages->setToolTip(tr("How many images are preloaded before and after current image."));
|
||||
|
||||
m_thumSize = new EvenNumber(this);
|
||||
m_thumSize->setRange(64, 512);
|
||||
m_thumSize->setSingleStep(2);
|
||||
m_thumSize->setValue(settings.value("settings/thumnailsize", THUMB_SIZE).toInt());
|
||||
m_thumSize->setToolTip(tr("Thumbnail size in pixels"));
|
||||
|
||||
m_saturation = new QDoubleSpinBox(this);
|
||||
m_saturation->setMinimum(0);
|
||||
m_saturation->setMaximum(100);
|
||||
m_saturation->setSuffix(" %");
|
||||
m_saturation->setValue(settings.value("settings/saturation", SATURATION * 100.0).toDouble());
|
||||
m_saturation->setToolTip(tr("Set threshold value that is considered saturated when showing statistics.\nFor RAW files you may set 22%"));
|
||||
|
||||
m_useNativeDialog = new QCheckBox(tr("Don't use native file dialog"), this);
|
||||
m_useNativeDialog->setChecked(QApplication::testAttribute(Qt::AA_DontUseNativeDialogs));
|
||||
|
||||
layout->addRow(tr("Image preload count"), m_preloadImages);
|
||||
layout->addRow(tr("Thumbnails size"), m_thumSize);
|
||||
layout->addRow(tr("Saturation"), m_saturation);
|
||||
layout->addRow(m_useNativeDialog);
|
||||
//layout->addRow(new QLabel(tr("Changes in settings will take effect after program restart.")));
|
||||
|
||||
QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
|
||||
buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(this, &QDialog::accepted, this, &SettingsDialog::saveSettings);
|
||||
|
||||
layout->addRow(buttonBox);
|
||||
}
|
||||
|
||||
void SettingsDialog::loadSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
THUMB_SIZE = settings.value("settings/thumbnailsize", THUMB_SIZE).toInt();
|
||||
THUMB_SIZE_BORDER = THUMB_SIZE + 10;
|
||||
THUMB_SIZE_BORDER_Y = THUMB_SIZE + 30;
|
||||
DEFAULT_WIDTH = settings.value("settings/preloadimagecount", DEFAULT_WIDTH).toInt();
|
||||
QApplication::setAttribute(Qt::AA_DontUseNativeDialogs, settings.value("settings/dontusenativedialogs", false).toBool());
|
||||
}
|
||||
|
||||
bool SettingsDialog::loadThumbsizes()
|
||||
{
|
||||
QSettings settings;
|
||||
int OLD_THUMB_SIZE = THUMB_SIZE;
|
||||
THUMB_SIZE = settings.value("settings/thumbnailsize", THUMB_SIZE).toInt();
|
||||
THUMB_SIZE_BORDER = THUMB_SIZE + 10;
|
||||
THUMB_SIZE_BORDER_Y = THUMB_SIZE + 30;
|
||||
SATURATION = settings.value("settings/saturation", 95.0).toDouble() / 100.0;
|
||||
return OLD_THUMB_SIZE != THUMB_SIZE;
|
||||
}
|
||||
|
||||
void SettingsDialog::saveSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue("settings/thumbnailsize", m_thumSize->value());
|
||||
settings.setValue("settings/preloadimagecount", m_preloadImages->value());
|
||||
settings.setValue("settings/dontusenativedialogs", m_useNativeDialog->isChecked());
|
||||
settings.setValue("settings/saturation", m_saturation->value());
|
||||
SATURATION = m_saturation->value() / 100.0;
|
||||
QApplication::setAttribute(Qt::AA_DontUseNativeDialogs, m_useNativeDialog->isChecked());
|
||||
if(DEFAULT_WIDTH != m_preloadImages->value())
|
||||
emit preloadChanged(m_preloadImages->value());
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef SETTINGSDIALOG_H
|
||||
#define SETTINGSDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QSpinBox>
|
||||
#include <QCheckBox>
|
||||
|
||||
class SettingsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SettingsDialog(QWidget *parent = nullptr);
|
||||
static void loadSettings();
|
||||
static bool loadThumbsizes();
|
||||
signals:
|
||||
void preloadChanged(int witdth);
|
||||
private:
|
||||
void saveSettings();
|
||||
|
||||
QSpinBox *m_preloadImages;
|
||||
QSpinBox *m_thumSize;
|
||||
QDoubleSpinBox *m_saturation;
|
||||
QCheckBox *m_useNativeDialog;
|
||||
};
|
||||
|
||||
#endif // SETTINGSDIALOG_H
|
||||
@@ -0,0 +1,41 @@
|
||||
#version 330
|
||||
|
||||
uniform sampler2D qt_Texture0;
|
||||
in vec2 qt_TexCoord0;
|
||||
in vec2 center;
|
||||
out vec4 color;
|
||||
|
||||
#define f(x, y) texelFetch(qt_Texture0, icenter + ivec2(x, y), 0).r
|
||||
|
||||
void main(void)
|
||||
{
|
||||
ivec2 texSize = textureSize(qt_Texture0, 0);
|
||||
ivec2 icenter = ivec2(center);
|
||||
ivec2 alternate = icenter % 2;
|
||||
|
||||
// cross, checker, theta, phi
|
||||
const vec4 kA = vec4(-1.0, -1.5, 0.5, -1.0) / 8.0;
|
||||
const vec4 kB = vec4( 2.0, 0.0, 0.0, 4.0) / 8.0;
|
||||
const vec4 kC = vec4( 4.0, 6.0, 5.0, 5.0) / 8.0;
|
||||
const vec4 kD = vec4( 0.0, 2.0, -1.0, -1.0) / 8.0;
|
||||
const vec4 kE = vec4(-1.0, -1.5, -1.0, 0.5) / 8.0;
|
||||
const vec4 kF = vec4( 2.0, 0.0, 4.0, 0.0) / 8.0;
|
||||
|
||||
float A = f(0,2) + f(0,-2);
|
||||
float B = f(0,1) + f(0,-1);
|
||||
float C = f(0,0);
|
||||
float D = f(1,1) + f(-1,1) + f(1,-1) + f(-1,-1);
|
||||
float E = f(2,0) + f(-2,0);
|
||||
float F = f(1,0) + f(-1,0);
|
||||
|
||||
vec4 P = kA*A + kB*B + kC*C + kD*D + kE*E + kF*F;
|
||||
|
||||
color.rgb = alternate.y == 0 ?
|
||||
(alternate.x == 0 ? vec3(C, P.xy) : // even row even col
|
||||
vec3(P.z, C, P.w)) : // even row odd col
|
||||
(alternate.x == 0 ? vec3(P.w, C, P.z) : // odd row even col
|
||||
vec3(P.yx, C)); // odd row odd col
|
||||
|
||||
color.a = 1.0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#version 330
|
||||
|
||||
uniform sampler2D qt_Texture0;
|
||||
in vec2 qt_Vertex;
|
||||
in vec2 qt_MultiTexCoord0;
|
||||
out vec2 qt_TexCoord0;
|
||||
out vec2 center;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
vec2 texSize = vec2(textureSize(qt_Texture0, 0));
|
||||
gl_Position = vec4(qt_Vertex, 0.0, 1.0);
|
||||
qt_TexCoord0 = qt_MultiTexCoord0;
|
||||
qt_TexCoord0.y = 1.0 - qt_TexCoord0.y;
|
||||
center = qt_TexCoord0 * texSize.xy;
|
||||
}
|
||||
+22
-1
@@ -4,9 +4,18 @@ uniform sampler2D qt_Texture0;
|
||||
uniform vec3 mtf_param;
|
||||
uniform bool bw;
|
||||
uniform bool invert;
|
||||
uniform bool srgb;
|
||||
uniform vec3 whiteBalance;
|
||||
in vec2 qt_TexCoord0;
|
||||
out vec4 color;
|
||||
|
||||
vec3 Linear2sRGB(vec3 color)
|
||||
{
|
||||
return mix(12.92 * color.rgb,
|
||||
1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055,
|
||||
greaterThan(color, vec3(0.0031308)));
|
||||
}
|
||||
|
||||
vec4 MTF(vec4 x, vec3 m)
|
||||
{
|
||||
x = (x - m.x) / (m.z - m.x);
|
||||
@@ -14,13 +23,25 @@ vec4 MTF(vec4 x, vec3 m)
|
||||
return ((m.y - 1) * x) / ((2 * m.y - 1) * x - m.y);
|
||||
}
|
||||
|
||||
vec3 checker()
|
||||
{
|
||||
vec2 pattern = fract(gl_FragCoord.xy * 0.0625) - 0.5;
|
||||
return vec3(step(pattern.x * pattern.y, 0.0) * 0.25 + 0.25);
|
||||
}
|
||||
|
||||
void main(void)
|
||||
{
|
||||
color = texture(qt_Texture0, qt_TexCoord0);
|
||||
if(bw)color = color.rrra;
|
||||
color = MTF(color, mtf_param);
|
||||
|
||||
if(invert)color = vec4(1.0) - color;
|
||||
if(invert)color.rgb = vec3(1.0) - color.rgb;
|
||||
|
||||
color.rgb = mix(checker(), color.rgb, color.a);
|
||||
|
||||
if(srgb)color.rgb = Linear2sRGB(color.rgb);
|
||||
|
||||
color.rgb *= whiteBalance;
|
||||
|
||||
if(any(lessThan(qt_TexCoord0, vec2(0.0))) || any(greaterThan(qt_TexCoord0, vec2(1.0))))
|
||||
color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
+3
-2
@@ -7,13 +7,14 @@ out vec3 qt_TexCoord0;
|
||||
uniform ivec3 viewport_row;
|
||||
uniform mat4 mvp;
|
||||
uniform vec2 offset;
|
||||
uniform ivec3 thumb_size;
|
||||
|
||||
void main(void)
|
||||
{
|
||||
vec2 pos = qt_Vertex * 0.5;
|
||||
pos.y *= -1.0;
|
||||
pos = pos * imageSize_num.xy + 69;
|
||||
ivec2 off = ivec2(imageSize_num.z % viewport_row.z, imageSize_num.z / viewport_row.z) * ivec2(138, 158);
|
||||
pos = pos * imageSize_num.xy + thumb_size.x;
|
||||
ivec2 off = ivec2(imageSize_num.z % viewport_row.z, imageSize_num.z / viewport_row.z) * thumb_size.yz;
|
||||
|
||||
gl_Position = mvp * vec4(pos - offset + off, 0.0, 1.0);
|
||||
qt_TexCoord0 = vec3(qt_MultiTexCoord0, imageSize_num.z + 0.1);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Exec=tenmon %U
|
||||
Icon=org.nou.tenmon
|
||||
Icon=space.nouspiro.tenmon
|
||||
Comment=FITS Image viewer
|
||||
Name=Tenmon
|
||||
Categories=Graphics;2DGraphics;RasterGraphics;Viewer;
|
||||
Categories=Graphics;2DGraphics;RasterGraphics;Viewer;Science;Astronomy
|
||||
MimeType=image/fits;image/x-xisf;
|
||||
Terminal=false
|
||||
@@ -0,0 +1,79 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop">
|
||||
<id>space.nouspiro.tenmon</id>
|
||||
<launchable type="desktop-id">space.nouspiro.tenmon.desktop</launchable>
|
||||
<name>Tenmon</name>
|
||||
<summary>FITS/XISF image viewer, converter, index and search</summary>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>GPL-3.0</project_license>
|
||||
<description>
|
||||
<p>It is intended primarily for viewing astro photos and images. It supports the following formats:</p>
|
||||
<ul>
|
||||
<li>FITS 8, 16 bit integer and 32 bit float</li>
|
||||
<li>XISF 8, 16 bit integer and 32 bit float</li>
|
||||
<li>RAW CR2, DNG, NEF</li>
|
||||
<li>JPEG, PNG, BMP, GIF, PBM, PGM, PPM and SVG images</li>
|
||||
</ul>
|
||||
<p>Features of application:</p>
|
||||
<ul>
|
||||
<li>Using same stretch function as PixInsight</li>
|
||||
<li>OpenGL accelerated drawing</li>
|
||||
<li>Index and search FITS XISF header data</li>
|
||||
<li>Quick mark images and then copy/move marked files</li>
|
||||
<li>Convert FITS <-> XISF</li>
|
||||
<li>Convert FITS/XISF -> JPEG/PNG</li>
|
||||
<li>Image statistics mean, media, min, max</li>
|
||||
<li>Support for WCS</li>
|
||||
<li>Thumbnails</li>
|
||||
<li>Convert CFA images to colour - debayer</li>
|
||||
<li>Color space aware</li>
|
||||
</ul>
|
||||
</description>
|
||||
<categories>
|
||||
<category>Graphics</category>
|
||||
<category>Viewer</category>
|
||||
<category>Science</category>
|
||||
<category>Astronomy</category>
|
||||
</categories>
|
||||
<keywords>
|
||||
<keyword>astronomy</keyword>
|
||||
</keywords>
|
||||
<url type="homepage">https://nouspiro.space/?page_id=206</url>
|
||||
<url type="bugtracker">https://github.com/flathub/space.nouspiro.tenmon/issues</url>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://nouspiro.space/wp-content/uploads/2022/04/tenmon-1024x579.png</image>
|
||||
<image>https://nouspiro.space/wp-content/uploads/2022/12/tenmon2-1024x645.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<releases>
|
||||
<release version="20221228" date="2022-12-28">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Add gray world white balance</li>
|
||||
<li>Export database to CSV</li>
|
||||
<li>Fine tune of STF when Shift key is pressed</li>
|
||||
<li>Add saturation statistic</li>
|
||||
<li>Various fixes</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="20221215" date="2022-12-15">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Add debayer support</li>
|
||||
<li>Move to trash action</li>
|
||||
<li>Change copy and move shortcuts to F5 and F6</li>
|
||||
<li>Change mark and unmark key shortcuts to F7 and F8</li>
|
||||
<li>Fix not refreshing file tree</li>
|
||||
<li>Add option to not use native file dialog</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="20221209" date="2022-12-09"/>
|
||||
<release version="20221126" date="2022-11-26"/>
|
||||
<release version="20221121" date="2022-11-11"/>
|
||||
<release version="20221023" date="2022-10-23"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 33.866666 33.866668"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
sodipodi:docname="space.nouspiro.tenmon.svg"
|
||||
inkscape:export-filename="/home/nou/c++/tenmon/space.nouspiro.tenmon_128.png"
|
||||
inkscape:export-xdpi="96.000008"
|
||||
inkscape:export-ydpi="96.000008"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
units="px"
|
||||
width="128px"
|
||||
inkscape:zoom="2.6547419"
|
||||
inkscape:cx="54.430903"
|
||||
inkscape:cy="78.162024"
|
||||
inkscape:window-width="1862"
|
||||
inkscape:window-height="1136"
|
||||
inkscape:window-x="58"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs2" />
|
||||
<g
|
||||
inkscape:label="Vrstva 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#000000;stroke:#ffffff;stroke-width:0;stroke-linejoin:round"
|
||||
id="rect1196"
|
||||
width="33.866665"
|
||||
height="33.866665"
|
||||
x="5e-07"
|
||||
y="5e-07" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:17.276px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.359917"
|
||||
x="-0.41414261"
|
||||
y="23.331123"
|
||||
id="text8592"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan8590"
|
||||
style="fill:#ffffff;stroke-width:0.359917"
|
||||
x="-0.41414261"
|
||||
y="23.331123">天文</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
+26
-3
@@ -21,6 +21,8 @@ STFSlider::STFSlider(QWidget *parent) : QWidget(parent)
|
||||
m_midPoint = 0.5;
|
||||
m_whitePoint = 1;
|
||||
m_grabbed = -1;
|
||||
m_fineTune = false;
|
||||
setToolTip(tr("Press Shift for fine tuning"));
|
||||
}
|
||||
|
||||
float STFSlider::blackPoint() const
|
||||
@@ -94,20 +96,34 @@ void STFSlider::mouseMoveEvent(QMouseEvent *event)
|
||||
else
|
||||
unsetCursor();
|
||||
|
||||
qreal x = (qreal)event->x()/width();
|
||||
if(event->modifiers() & Qt::ShiftModifier && !m_fineTune)
|
||||
{
|
||||
m_fineTune = true;
|
||||
m_fineTuneX = x;
|
||||
}
|
||||
if(!(event->modifiers() & Qt::ShiftModifier) && m_fineTune)
|
||||
m_fineTune = false;
|
||||
|
||||
if(m_fineTune)
|
||||
{
|
||||
x = m_fineTuneX + (x - m_fineTuneX) * 0.2;
|
||||
}
|
||||
|
||||
switch(m_grabbed)
|
||||
{
|
||||
case 0:
|
||||
m_blackPoint = clamp((qreal)event->x()/width());
|
||||
m_blackPoint = clamp(x);
|
||||
m_whitePoint = std::max(m_whitePoint, m_blackPoint);
|
||||
QToolTip::showText(event->globalPos(), QString::number(m_blackPoint), this);
|
||||
break;
|
||||
case 1:
|
||||
m_midPoint = ((qreal)event->x()/width() - m_blackPoint) / (m_whitePoint - m_blackPoint);
|
||||
m_midPoint = (x - m_blackPoint) / (m_whitePoint - m_blackPoint);
|
||||
m_midPoint = clamp(m_midPoint);
|
||||
QToolTip::showText(event->globalPos(), QString::number(m_midPoint), this);
|
||||
break;
|
||||
case 2:
|
||||
m_whitePoint = clamp((qreal)event->x()/width());
|
||||
m_whitePoint = clamp(x);
|
||||
m_blackPoint = std::min(m_blackPoint, m_whitePoint);
|
||||
QToolTip::showText(event->globalPos(), QString::number(m_whitePoint), this);
|
||||
break;
|
||||
@@ -121,6 +137,12 @@ void STFSlider::mouseMoveEvent(QMouseEvent *event)
|
||||
|
||||
void STFSlider::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if(event->modifiers() & Qt::ShiftModifier)
|
||||
{
|
||||
m_fineTune = true;
|
||||
m_fineTuneX = (qreal)event->x()/width();
|
||||
}
|
||||
|
||||
if(std::abs((m_blackPoint + (m_whitePoint - m_blackPoint) * m_midPoint)*width() - event->x()) < 5)
|
||||
m_grabbed = 1;
|
||||
else if(std::abs(m_blackPoint*width() - event->x()) < 5)
|
||||
@@ -134,5 +156,6 @@ void STFSlider::mousePressEvent(QMouseEvent *event)
|
||||
void STFSlider::mouseReleaseEvent(QMouseEvent *)
|
||||
{
|
||||
m_grabbed = -1;
|
||||
m_fineTune = false;
|
||||
emit paramChanged(m_blackPoint, midPoint(), m_whitePoint);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ class STFSlider : public QWidget
|
||||
float m_midPoint;
|
||||
float m_whitePoint;
|
||||
int m_grabbed;
|
||||
bool m_fineTune;
|
||||
float m_fineTuneX;
|
||||
public:
|
||||
explicit STFSlider(QWidget *parent = nullptr);
|
||||
float blackPoint() const;
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ StretchToolbar::StretchToolbar(QWidget *parent) : QToolBar(tr("Stretch toolbar")
|
||||
invertButton->setCheckable(true);
|
||||
connect(invertButton, SIGNAL(toggled(bool)), this, SIGNAL(invert(bool)));
|
||||
|
||||
QAction *superPixelButton = addAction(QIcon(":/bayer.png"), tr("Superpixel CFA draw 2x2 pixel as one"));
|
||||
QAction *superPixelButton = addAction(QIcon(":/bayer.png"), tr("Debayer CFA"));
|
||||
superPixelButton->setCheckable(true);
|
||||
connect(superPixelButton, SIGNAL(toggled(bool)), this, SIGNAL(superPixel(bool)));
|
||||
|
||||
@@ -49,7 +49,7 @@ void StretchToolbar::stretchImage(Image *img)
|
||||
if(img->rawImage())
|
||||
{
|
||||
double median, mad, max;
|
||||
img->rawImage()->imageStats(nullptr, nullptr, &median, nullptr, &max, &mad);
|
||||
img->rawImage()->imageStats(nullptr, nullptr, &median, nullptr, &max, &mad, nullptr);
|
||||
median /= img->rawImage()->norm();
|
||||
mad /= img->rawImage()->norm();
|
||||
max /= img->rawImage()->norm();
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Exec=Tenmon %U
|
||||
Icon=org.nou.tenmon
|
||||
Comment=FITS Image viewer
|
||||
Name=Tenmon
|
||||
Categories=Graphics;2DGraphics;RasterGraphics;Viewer;
|
||||
MimeType=image/fits;image/x-xisf;
|
||||
Terminal=false
|
||||
Binary file not shown.
+132
-20
@@ -71,6 +71,10 @@
|
||||
<source>Go up</source>
|
||||
<translation>Go up</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show hidden files</source>
|
||||
<translation>Show hidden files</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>HelpDialog</name>
|
||||
@@ -150,10 +154,6 @@
|
||||
<source>Filesystem</source>
|
||||
<translation>File system</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FITS files database</source>
|
||||
<translation type="vanished">FITS files database</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Tenmon</source>
|
||||
<translation>Tenmon</translation>
|
||||
@@ -204,7 +204,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Fullscreen</source>
|
||||
<translation>Fullscreen</translation>
|
||||
<translation type="vanished">Fullscreen</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Select</source>
|
||||
@@ -290,18 +290,10 @@
|
||||
<source>Moving</source>
|
||||
<translation>Moving</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS *.XISF)</source>
|
||||
<translation type="vanished">Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS *.XISF)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Indexing FITS files</source>
|
||||
<translation>Indexing FITS files</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)</source>
|
||||
<translation>JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Reindex files</source>
|
||||
<translation>Reindex files</translation>
|
||||
@@ -319,8 +311,76 @@
|
||||
<translation>Star finder</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Images (*.jpg *.jpeg *.png *.cr2 *.nef *.dng *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.NEF *.DNG *.FIT *.FITS *.XISF)</source>
|
||||
<translation>Images (*.jpg *.jpeg *.png *.cr2 *.nef *.dng *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.NEF *.DNG *.FIT *.FITS *.XISF)</translation>
|
||||
<source>Edit</source>
|
||||
<translation>Edit</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FITS header editor</source>
|
||||
<translation type="vanished">FITS header editor</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Settings</source>
|
||||
<translation>Settings</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Images (</source>
|
||||
<translation>Images (</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FITS (*.fits *.fit);;XISF (*.xisf);;</source>
|
||||
<translation>FITS image (*.fits *.fit);;XISF image (*.xisf);;</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to copy</source>
|
||||
<translation>Failed to copy</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move</source>
|
||||
<translation>Failed to move</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move from %1 to %2</source>
|
||||
<translation>Failed to move from %1 to %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to copy from %1 to %2</source>
|
||||
<translation>Failed to copy from %1 to %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>;;All files (*)</source>
|
||||
<translation>;;All files (*)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Move files to trash?</source>
|
||||
<translation>Move files to trash?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Do you want to move %1 files to trash?</source>
|
||||
<translation>Do you want to move %1 files to trash?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move file to trash</source>
|
||||
<translation>Failed to move file to trash</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move file to trash %1</source>
|
||||
<translation>Failed to move file to trash %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Move marked files to trash</source>
|
||||
<translation>Move marked files to trash</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Moving marked files to trash</source>
|
||||
<translation>Moving marked files to trash</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Export database to CSV</source>
|
||||
<translation>Export database to CSV file</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>CSV file (*.csv)</source>
|
||||
<translation>CSV files (*.csv)</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -398,7 +458,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Peaks draw</source>
|
||||
<translation>Peaks draw</translation>
|
||||
<translation type="vanished">Peaks draw</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FWHM X</source>
|
||||
@@ -412,6 +472,17 @@
|
||||
<source>Unsupported sample format</source>
|
||||
<translation>Unsupported sample format</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Saturated</source>
|
||||
<translation>Saturated</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>STFSlider</name>
|
||||
<message>
|
||||
<source>Press Shift for fine tuning</source>
|
||||
<translation>Press Shift for fine tuning</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SelectColumnsDialog</name>
|
||||
@@ -420,6 +491,47 @@
|
||||
<translation>Select columns</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SettingsDialog</name>
|
||||
<message>
|
||||
<source>Settings</source>
|
||||
<translation>Settings</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>How many images are preloaded before and after current image.</source>
|
||||
<translation>How many images are preloaded before and after current image.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Thumbnail size in pixels</source>
|
||||
<translation>Thumbnail size in pixels</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Image preload count</source>
|
||||
<translation>Image preload count</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Thumbnails size</source>
|
||||
<translation>Thumbnails size</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Changes in settings will take effect after program restart.</source>
|
||||
<translation>Changes in settings will take effect after program restart.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Don't use native file dialog</source>
|
||||
<translation>Don't use native file dialog</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Set threshold value that is considered saturated when showing statistics.
|
||||
For RAW files you may set 22%</source>
|
||||
<translation>Set threshold value that is considered saturated when showing statistics.
|
||||
For RAW files you may set 22%</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Saturation</source>
|
||||
<translation>Saturated</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>StretchToolbar</name>
|
||||
<message>
|
||||
@@ -438,13 +550,13 @@
|
||||
<source>Invert colors</source>
|
||||
<translation>Invert colors</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Superpixel CFA draw 2x2 pixel as one</source>
|
||||
<translation>Superpixel CFA draw 2x2 pixel as one</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Apply auto stretch on load</source>
|
||||
<translation>Apply auto stretch on load</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Debayer CFA</source>
|
||||
<translation>Debayer CFA</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
||||
Binary file not shown.
+132
-20
@@ -71,6 +71,10 @@
|
||||
<source>Go up</source>
|
||||
<translation>Monter</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show hidden files</source>
|
||||
<translation>Afficher les fichiers cachés</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>HelpDialog</name>
|
||||
@@ -150,10 +154,6 @@
|
||||
<source>Filesystem</source>
|
||||
<translation>Système de fichier</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FITS files database</source>
|
||||
<translation type="vanished">FITS files database</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Tenmon</source>
|
||||
<translation>Tenmon</translation>
|
||||
@@ -204,7 +204,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Fullscreen</source>
|
||||
<translation>Plein écran</translation>
|
||||
<translation type="vanished">Plein écran</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Select</source>
|
||||
@@ -290,18 +290,10 @@
|
||||
<source>Moving</source>
|
||||
<translation>Déplacement</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS *.XISF)</source>
|
||||
<translation type="vanished">Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS *.XISF)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Indexing FITS files</source>
|
||||
<translation>Indexation des fichiers FITS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)</source>
|
||||
<translation>JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Reindex files</source>
|
||||
<translation>Ré-indexer les fichiers</translation>
|
||||
@@ -319,8 +311,76 @@
|
||||
<translation>Détecteur d'étoiles</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Images (*.jpg *.jpeg *.png *.cr2 *.nef *.dng *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.NEF *.DNG *.FIT *.FITS *.XISF)</source>
|
||||
<translation>Images (*.jpg *.jpeg *.png *.cr2 *.nef *.dng *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.NEF *.DNG *.FIT *.FITS *.XISF)</translation>
|
||||
<source>Edit</source>
|
||||
<translation>Éditer</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FITS header editor</source>
|
||||
<translation type="vanished">Éditeur d'en-tête FITS</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Settings</source>
|
||||
<translation>Réglages</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Images (</source>
|
||||
<translation>Images (</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FITS (*.fits *.fit);;XISF (*.xisf);;</source>
|
||||
<translation>FITS image (*.fits *.fit);;XISF image (*.xisf);;</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to copy</source>
|
||||
<translation>Échec de la copie</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move</source>
|
||||
<translation>Échec du déplacement</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move from %1 to %2</source>
|
||||
<translation>Échec du déplacement de %1 vers %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to copy from %1 to %2</source>
|
||||
<translation>Échec de la copie de %1 vers %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>;;All files (*)</source>
|
||||
<translation>;;Tout les fichiers (*)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Move files to trash?</source>
|
||||
<translation>Déplacer les fichiers dans la corbeille?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Do you want to move %1 files to trash?</source>
|
||||
<translation>Voulez-vous déplacer le fichier %1 dans la corbeille?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move file to trash</source>
|
||||
<translation>Echec du déplacement dans la corbeille</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move file to trash %1</source>
|
||||
<translation>Echec du déplacement de %1 dans la corbeille</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Move marked files to trash</source>
|
||||
<translation>Déplacer les fichiers marqués dans la corbeille</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Moving marked files to trash</source>
|
||||
<translation>Déplacement des fichiers marqués dans la corbeille</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Export database to CSV</source>
|
||||
<translation>Exporter la base de données vers un fichier CSV</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>CSV file (*.csv)</source>
|
||||
<translation>Fichiers CSV (*.csv)</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -398,7 +458,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Peaks draw</source>
|
||||
<translation>Dessin des pic</translation>
|
||||
<translation type="vanished">Dessin des pic</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FWHM X</source>
|
||||
@@ -412,6 +472,17 @@
|
||||
<source>Unsupported sample format</source>
|
||||
<translation>Format non pris en charge</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Saturated</source>
|
||||
<translation>Saturé</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>STFSlider</name>
|
||||
<message>
|
||||
<source>Press Shift for fine tuning</source>
|
||||
<translation>Appuyez sur Shift pour un réglage fin</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SelectColumnsDialog</name>
|
||||
@@ -420,6 +491,47 @@
|
||||
<translation>Choix des colonnes</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SettingsDialog</name>
|
||||
<message>
|
||||
<source>Settings</source>
|
||||
<translation>Réglages</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>How many images are preloaded before and after current image.</source>
|
||||
<translation>Combien d'images sont préchargées avant et après l'image courante.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Thumbnail size in pixels</source>
|
||||
<translation>Taille des vignettes en pixels</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Image preload count</source>
|
||||
<translation>Nombre d'images préchargées</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Thumbnails size</source>
|
||||
<translation>Taille des vignette</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Changes in settings will take effect after program restart.</source>
|
||||
<translation>Les changements de paramètres prendront effet après le redémarrage du programme.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Don't use native file dialog</source>
|
||||
<translation>N'utilisez pas la boîte de dialogue de fichier natif</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Set threshold value that is considered saturated when showing statistics.
|
||||
For RAW files you may set 22%</source>
|
||||
<translation>Définissez la valeur seuil qui est considérée comme saturée lors de l'affichage des statistiques.
|
||||
Pour les fichiers RAW, vous pouvez définir 22 %</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Saturation</source>
|
||||
<translation>Saturé</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>StretchToolbar</name>
|
||||
<message>
|
||||
@@ -438,13 +550,13 @@
|
||||
<source>Invert colors</source>
|
||||
<translation>Inverser les couleurs</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Superpixel CFA draw 2x2 pixel as one</source>
|
||||
<translation>Superpixel CFA dessine 2x2 pixels comme un seul</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Apply auto stretch on load</source>
|
||||
<translation>Appliquer la luminosité automatiquement au chargement</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Debayer CFA</source>
|
||||
<translation>Débayeriser CFA</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
||||
Binary file not shown.
+132
-32
@@ -72,6 +72,10 @@
|
||||
<source>Go up</source>
|
||||
<translation>O úroveň vyššie</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show hidden files</source>
|
||||
<translation>Zobraz skryté súbory</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>HelpDialog</name>
|
||||
@@ -151,14 +155,6 @@
|
||||
<source>Filesystem</source>
|
||||
<translation>Zoznam súborov</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FITS Editor</source>
|
||||
<translation type="vanished">FITS editor</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FITS files database</source>
|
||||
<translation type="vanished">Databáza FITS súborov</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Tenmon</source>
|
||||
<translation>Tenmon</translation>
|
||||
@@ -205,7 +201,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Fullscreen</source>
|
||||
<translation>Celá obrazovka</translation>
|
||||
<translation type="vanished">Celá obrazovka</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Select</source>
|
||||
@@ -243,14 +239,6 @@
|
||||
<source>Open file</source>
|
||||
<translation>Otvoriť súbor</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS)</source>
|
||||
<translation type="vanished">Obrázky (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Images (*.jpg *.png *.JPG *.PNG)</source>
|
||||
<translation type="vanished">Obrázky (*.jpg *.png *.JPG *.PNG)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Select destination</source>
|
||||
<translation>Vybrať cieľ</translation>
|
||||
@@ -303,18 +291,10 @@
|
||||
<source>Moving</source>
|
||||
<translation>Presúvanie</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Images (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS *.XISF)</source>
|
||||
<translation type="vanished">Obrázky (*.jpg *.jpeg *.png *.cr2 *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.FIT *.FITS *.XISF)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Indexing FITS files</source>
|
||||
<translation>Indexovanie FITS/XISF súborov</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)</source>
|
||||
<translation>JPEG (*.jpg *.JPG);; PNG (*.png *.PNG);;FITS (*.fits *.FITS);;XISF (*.xisf *.XISF)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Reindex files</source>
|
||||
<translation>Reindexuj súbory</translation>
|
||||
@@ -332,8 +312,76 @@
|
||||
<translation>Vyhľadávač hviezd</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Images (*.jpg *.jpeg *.png *.cr2 *.nef *.dng *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.NEF *.DNG *.FIT *.FITS *.XISF)</source>
|
||||
<translation>Obrázky (*.jpg *.jpeg *.png *.cr2 *.nef *.dng *.fit *.fits *.xisf *.JPG *.JPEG *.PNG *.CR2 *.NEF *.DNG *.FIT *.FITS *.XISF)</translation>
|
||||
<source>Edit</source>
|
||||
<translation>Upraviť</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FITS header editor</source>
|
||||
<translation type="vanished">Editor FITS hlavičky</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Settings</source>
|
||||
<translation>Nastavenia</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Images (</source>
|
||||
<translation>Obrázky (</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FITS (*.fits *.fit);;XISF (*.xisf);;</source>
|
||||
<translation>Obrázok FITS (*.fits *.fit);;Obrázok XISF (*.xisf);;</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to copy</source>
|
||||
<translation>Zlyhalo kopírovanie</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move</source>
|
||||
<translation>Zlyhalo presúvanie</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move from %1 to %2</source>
|
||||
<translation>Zlyhalo presúvanie z %1 do %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to copy from %1 to %2</source>
|
||||
<translation>Zlyhalo kopírovanie z %1 do %2</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>;;All files (*)</source>
|
||||
<translation>;;Všetky súbory (*)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Move files to trash?</source>
|
||||
<translation>Presunúť súbory do koša?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Do you want to move %1 files to trash?</source>
|
||||
<translation>Presunúť %1 súborov do koša?</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move file to trash</source>
|
||||
<translation>Zlyhalo presunutie súbora do koša</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Failed to move file to trash %1</source>
|
||||
<translation>Zlyhalo presunutie súbora do koša %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Move marked files to trash</source>
|
||||
<translation>Presunúť označené súbory do koša</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Moving marked files to trash</source>
|
||||
<translation>Presúvanie do koša</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Export database to CSV</source>
|
||||
<translation>Exportovať databázu do CSV súboru</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>CSV file (*.csv)</source>
|
||||
<translation>Súbory CSV (*.csv)</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
@@ -415,7 +463,7 @@
|
||||
</message>
|
||||
<message>
|
||||
<source>Peaks draw</source>
|
||||
<translation>Vykreslené vrcholky</translation>
|
||||
<translation type="vanished">Vykreslené vrcholky</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>FWHM X</source>
|
||||
@@ -425,6 +473,17 @@
|
||||
<source>FWHM Y</source>
|
||||
<translation>FWHM Y</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Saturated</source>
|
||||
<translation>Saturované</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>STFSlider</name>
|
||||
<message>
|
||||
<source>Press Shift for fine tuning</source>
|
||||
<translation>Stlačte Shift pre jemné ladenie</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SelectColumnsDialog</name>
|
||||
@@ -433,6 +492,47 @@
|
||||
<translation>Výber stĺpcov</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>SettingsDialog</name>
|
||||
<message>
|
||||
<source>Settings</source>
|
||||
<translation>Nastavenia</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>How many images are preloaded before and after current image.</source>
|
||||
<translation>Koľko obrázkov sa prednačíta pred a za aktuálnym obrázkom.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Thumbnail size in pixels</source>
|
||||
<translation>Veľkosť náhľadu v pixeloch</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Image preload count</source>
|
||||
<translation>Počet prednačítaných obrázkov</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Thumbnails size</source>
|
||||
<translation>Veľkosť náhľadu</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Changes in settings will take effect after program restart.</source>
|
||||
<translation>Zmeny v nastaveniach sa prejavia po reštarte programu.</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Don't use native file dialog</source>
|
||||
<translation>Nepoužívať natívny súborový dialóg</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Set threshold value that is considered saturated when showing statistics.
|
||||
For RAW files you may set 22%</source>
|
||||
<translation>Nastavuje prahovú hodnotu ktorá sa považuje za saturovanú.
|
||||
Pre RAW súbory možno treba nastaviť 22%</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Saturation</source>
|
||||
<translation>Saturované</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>StretchToolbar</name>
|
||||
<message>
|
||||
@@ -451,13 +551,13 @@
|
||||
<source>Invert colors</source>
|
||||
<translation>Invertuj farby</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Superpixel CFA draw 2x2 pixel as one</source>
|
||||
<translation>Super pixel CFA kreslenie 2x2 pixelov ako jeden</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Apply auto stretch on load</source>
|
||||
<translation>Aplikuj automatické natiahnutie pri načítaní</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Debayer CFA</source>
|
||||
<translation>Preveď CFA na farbu</translation>
|
||||
</message>
|
||||
</context>
|
||||
</TS>
|
||||
|
||||
Reference in New Issue
Block a user