diff --git a/.gitmodules b/.gitmodules index 301e753..ffc48c4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "external/vtflib"] path = external/vtflib url = https://github.com/JJL772/VTFLib +[submodule "external/fmtlib"] + path = external/fmtlib + url = https://github.com/fmtlib/fmt diff --git a/CMakeLists.txt b/CMakeLists.txt index 1faec2d..798fbbc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,41 +6,66 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_CXX_STANDARD 20) # Project settings - -# Set to enable Qt for this project -set(USING_QT OFF) +option(BUILD_GUI "Build the VTFViewer GUI" ON) # Global flags, mainly for UNIX. Use $ORIGIN rpath & -fPIC set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_BUILD_RPATH_USE_ORIGIN ON) +# Build vtflib as static lib +set(VTFLIB_STATIC ON) +add_subdirectory(external/vtflib) +add_subdirectory(external/fmtlib) + +############################## +# Common code +############################## +set(COMMON_SRC + src/common/image.cpp + src/common/enums.cpp) + +add_library(libcom STATIC ${COMMON_SRC}) + +############################## +# CLI +############################## + # Sources set(CLI_SRC src/cli/main.cpp src/cli/action_extract.cpp - src/cli/action_info.cpp - src/common/enums.cpp - src/common/image.cpp) + src/cli/action_info.cpp) add_executable(vtex2 ${CLI_SRC}) -# Qt support -if (USING_QT) +############################## +# GUI +############################## + +if (BUILD_GUI) include(cmake_scripts/Qt.cmake) - find_package(Qt5 REQUIRED COMPONENTS Widgets Core Gui) - target_link_libraries(${PROJECTNAME} PRIVATE Qt5::Widgets Qt5::Core Qt5::Gui) - target_include_directories(${PROJECTNAME} PRIVATE ${QT_INCLUDE} ${QT_INCLUDE}/QtWidgets ${QT_INCLUDE}/QtGui ${QT_INCLUDE}/QtCore) -endif() + set(VIEWER_SRC + src/gui/main.cpp + src/gui/viewer.cpp) + + add_executable(vtfview ${VIEWER_SRC}) +endif() # Set up the debugger so it can run the program without copying a million dlls if(WIN32) - set_target_properties(vtex2 PROPERTIES VS_DEBUGGER_ENVIRONMENT "PATH=%PATH%;${QT_BASEDIR}/bin;") + set_target_properties(vtfview PROPERTIES VS_DEBUGGER_ENVIRONMENT "PATH=%PATH%;${QT_BASEDIR}/bin;") endif() -# Build vtflib as static lib -set(VTFLIB_STATIC ON) -add_subdirectory(external/vtflib) - -target_link_libraries(vtex2 vtflib) +target_link_libraries(vtex2 PRIVATE vtflib libcom fmt::fmt) target_include_directories(vtex2 PRIVATE src external) +target_include_directories(libcom PRIVATE src external external/vtflib/lib) + +if (BUILD_GUI) + target_link_libraries(vtfview PRIVATE vtflib libcom fmt::fmt) + target_include_directories(vtfview PRIVATE src external) + + find_package(Qt5 REQUIRED COMPONENTS Widgets Core Gui) + target_link_libraries(vtfview PRIVATE Qt5::Widgets Qt5::Core Qt5::Gui) + target_include_directories(vtfview PRIVATE ${QT_INCLUDE} ${QT_INCLUDE}/QtWidgets ${QT_INCLUDE}/QtGui ${QT_INCLUDE}/QtCore) +endif() diff --git a/external/fmtlib b/external/fmtlib new file mode 160000 index 0000000..91abfcd --- /dev/null +++ b/external/fmtlib @@ -0,0 +1 @@ +Subproject commit 91abfcd6cfde3b083612bcef08be110246dd2883 diff --git a/external/vtflib b/external/vtflib index 55c69ee..3ba35fa 160000 --- a/external/vtflib +++ b/external/vtflib @@ -1 +1 @@ -Subproject commit 55c69ee5e4a65ce03d1742f99185359aec332015 +Subproject commit 3ba35fa02145e2481031d30044ba05ac8b3165bf diff --git a/src/common/enums.cpp b/src/common/enums.cpp index fba0fe5..f39d627 100644 --- a/src/common/enums.cpp +++ b/src/common/enums.cpp @@ -141,3 +141,16 @@ std::vector TextureFlagsToStringVector(std::uint32_t flags) { return ret; } + +const char* GetResourceName(vlUInt resource) { + switch(resource) { + case VTF_LEGACY_RSRC_LOW_RES_IMAGE: return "Low-res Image (Legacy"; + case VTF_LEGACY_RSRC_IMAGE: return "Image (Legacy)"; + case VTF_RSRC_SHEET: return "Sheet"; + case VTF_RSRC_CRC: return "CRC"; + case VTF_RSRC_TEXTURE_LOD_SETTINGS: return "Texture LOD Settings"; + case VTF_RSRC_TEXTURE_SETTINGS_EX: return "Texture Settings Extended"; + case VTF_RSRC_KEY_VALUE_DATA: return "KeyValue Data"; + default: return ""; + } +} diff --git a/src/common/enums.hpp b/src/common/enums.hpp index 6cc11e0..7b94fe7 100644 --- a/src/common/enums.hpp +++ b/src/common/enums.hpp @@ -24,3 +24,8 @@ VTFImageFormat ImageFormatFromString(const char* arg); * Convert VTF flags to a user friendly list of strings */ std::vector TextureFlagsToStringVector(std::uint32_t flags); + +/** + * Get a human readable name for the resource + */ +const char* GetResourceName(vlUInt resource); diff --git a/src/common/util.hpp b/src/common/util.hpp index 1f9d231..2da088d 100644 --- a/src/common/util.hpp +++ b/src/common/util.hpp @@ -57,4 +57,9 @@ namespace util { private: T m_func; }; + + template + constexpr std::size_t ArraySize(T(&arr)[N]) { + return N; + } } diff --git a/src/gui/main.cpp b/src/gui/main.cpp new file mode 100644 index 0000000..8c0a47c --- /dev/null +++ b/src/gui/main.cpp @@ -0,0 +1,26 @@ + +#include + +#include +#include + +#include "viewer.hpp" +#include "VTFLib.h" + +int main(int argc, char** argv) { + QApplication app(argc, argv); + + std::string file; + if (argc > 1) { + file = argv[1]; + } + + auto* pMainWindow = new vtfview::ViewerMainWindow(nullptr); + if (file.length() && !pMainWindow->load_file(file.c_str())) { + fprintf(stderr, "Could not open vtf '%s'!\n", file.c_str()); + return 1; + } + pMainWindow->show(); + + return app.exec(); +} diff --git a/src/gui/viewer.cpp b/src/gui/viewer.cpp new file mode 100644 index 0000000..a1519ad --- /dev/null +++ b/src/gui/viewer.cpp @@ -0,0 +1,292 @@ + +#include +#include +#include +#include +#include +#include +#include + +#include "fmt/format.h" + +#include "viewer.hpp" + +#include "common/util.hpp" +#include "common/enums.hpp" + +using namespace vtfview; +using namespace VTFLib; + +ViewerMainWindow::ViewerMainWindow(QWidget* pParent) : + QMainWindow(pParent) { + setup_ui(); +} + + +bool ViewerMainWindow::load_file(const char* path) { + std::uint8_t* buffer; + auto numRead = util::read_file(path, buffer); + if (!numRead) + return false; + + bool ok = load_file(buffer, numRead); + delete buffer; + return ok; +} + +bool ViewerMainWindow::load_file(const void* data, size_t size) { + file_ = new VTFLib::CVTFFile(); + if (!file_->Load(data, size)) { + delete file_; + file_ = nullptr; + return false; + } + return load_file(file_); +} + +bool ViewerMainWindow::load_file(VTFLib::CVTFFile* file) { + emit vtfFileChanged(file); + file_ = file; + return true; +} + +void ViewerMainWindow::unload_file() { + if (!file_) + return; + emit vtfFileChanged(nullptr); + delete file_; + file_ = nullptr; +} + +void ViewerMainWindow::setup_ui() { + + setTabPosition(Qt::LeftDockWidgetArea, QTabWidget::North); + + // Info widget + auto* infoDock = new QDockWidget(tr("Info"), this); + + auto* infoWidget = new InfoWidget(this); + connect(this, &ViewerMainWindow::vtfFileChanged, infoWidget, &InfoWidget::update_info); + + infoDock->setWidget(infoWidget); + addDockWidget(Qt::LeftDockWidgetArea, infoDock); + + // Resource list + auto* resDock = new QDockWidget(tr("Resources"), this); + + auto* resList = new ResourceWidget(this); + connect(this, &ViewerMainWindow::vtfFileChanged, resList, &ResourceWidget::set_vtf); + + resDock->setWidget(resList); + addDockWidget(Qt::LeftDockWidgetArea, resDock); + + tabifyDockWidget(infoDock, resDock); + + // Main image viewer + auto* imageView = new ImageViewWidget(this); + + connect(this, &ViewerMainWindow::vtfFileChanged, [imageView](VTFLib::CVTFFile* file) { + imageView->set_vtf(file); + }); + setCentralWidget(imageView); +} + +void ViewerMainWindow::reset_state() { + dirty_ = false; +} + +////////////////////////////////////////////////////////////////////////////////// +// InfoWidget +////////////////////////////////////////////////////////////////////////////////// + +static inline constexpr const char* INFO_FIELDS[] = { + "Width", "Height", "Depth", + "Frames", "Faces", "Mips", + "Image format", + "Reflectivity" +}; + +static inline constexpr const char* FILE_FIELDS[] = { + "Size", "Version" +}; + +InfoWidget::InfoWidget(QWidget* pParent) : + QWidget(pParent) { + setup_ui(); +} + +void InfoWidget::update_info(VTFLib::CVTFFile* file) { + find("Width")->setText(QString::number(file->GetWidth())); + find("Height")->setText(QString::number(file->GetHeight())); + find("Depth")->setText(QString::number(file->GetDepth())); + find("Frames")->setText(QString::number(file->GetFrameCount())); + find("Faces")->setText(QString::number(file->GetFaceCount())); + find("Mips")->setText(QString::number(file->GetMipmapCount())); + find("Image format")->setText(ImageFormatToString(file->GetFormat())); + + find("Version")->setText(QString::number(file->GetMajorVersion()) + "." + QString::number(file->GetMinorVersion())); + auto size = file->GetSize(); + find("Size")->setText( + fmt::format(FMT_STRING("{:.2f} MiB ({:.2f} KiB)"), size / (1024.f*1024.f), size / 1024.f).c_str() + ); + + vlSingle x, y, z; + file->GetReflectivity(x, y, z); + find("Reflectivity")->setText( + fmt::format(FMT_STRING("{:.3f} {:.3f} {:.3f}"), x, y, z).c_str() + ); +} + +void InfoWidget::setup_ui() { + auto* layout = new QVBoxLayout(this); + auto* fileGroupBox = new QGroupBox(tr("File Metadata"), this); + auto* imageGroupBox = new QGroupBox(tr("Image Info"), this); + + auto* fileGroupLayout = new QGridLayout(fileGroupBox); + auto* imageGroupLayout = new QGridLayout(imageGroupBox); + fileGroupLayout->setColumnStretch(1, 1); + imageGroupLayout->setColumnStretch(1, 1); + + // Prevent rows from expanding on resize + fileGroupLayout->setRowStretch(util::ArraySize(FILE_FIELDS), 1); + imageGroupLayout->setRowStretch(util::ArraySize(INFO_FIELDS), 1); + + // File meta info + int row = 0; + for (auto& f : FILE_FIELDS) { + auto* label = new QLabel(QString(f) + ":", fileGroupBox); + auto* edit = new QLineEdit(this); + edit->setReadOnly(true); + + fileGroupLayout->addWidget(label, row, 0); + fileGroupLayout->addWidget(edit, row, 1); + ++row; + + fields_.insert({f, edit}); + } + + // Image contents info + row = 0; + for (auto& f : INFO_FIELDS) { + auto* label = new QLabel(QString(f) + ":", imageGroupBox); + auto* edit = new QLineEdit(this); + edit->setReadOnly(true); + + imageGroupLayout->addWidget(label, row, 0); + imageGroupLayout->addWidget(edit, row, 1); + ++row; + + fields_.insert({f, edit}); + } + + layout->addWidget(fileGroupBox); + layout->addWidget(imageGroupBox); + + // Prevent space being added to the bottom of the file metadata group box + layout->addStretch(1); +} + +////////////////////////////////////////////////////////////////////////////////// +// ImageViewWidget +////////////////////////////////////////////////////////////////////////////////// + +ImageViewWidget::ImageViewWidget(QWidget* pParent) : + QWidget(pParent) { + setMinimumSize(256, 256); +} + +void ImageViewWidget::set_pixmap(const QImage& pixmap) { + image_ = pixmap; +} + +void ImageViewWidget::set_vtf(VTFLib::CVTFFile* file) { + file_ = file; + // Force refresh of data + currentFrame_ = -1; + currentFace_ = -1; + currentMip_ = -1; + + zoom_ = 1.f; + pos_ = {0,0}; +} + +void ImageViewWidget::paintEvent(QPaintEvent* event) { + QPainter painter(this); + + // Compute draw size for this mip, frame, etc + vlUInt imageWidth, imageHeight, imageDepth; + CVTFFile::ComputeMipmapDimensions(file_->GetWidth(), file_->GetHeight(), file_->GetDepth(), mip_, imageWidth, imageHeight, imageDepth); + + // Needs decode + if (frame_ != currentFrame_ || mip_ != currentMip_ || face_ != currentFace_) { + + const bool hasAlpha = CVTFFile::GetImageFormatInfo(file_->GetFormat()).uiAlphaBitsPerPixel > 0; + const VTFImageFormat format = hasAlpha ? IMAGE_FORMAT_RGBA8888 : IMAGE_FORMAT_RGB888; + auto size = file_->ComputeMipmapSize(file_->GetWidth(), file_->GetHeight(), 1, mip_, format); + + auto* data = static_cast(malloc(size)); + bool ok = CVTFFile::Convert(file_->GetData(frame_, face_, 0, mip_), data, imageWidth, imageHeight, file_->GetFormat(), format); + + if (!ok) { + fprintf(stderr, "Could not convert image for display.\n"); + return; + } + + image_ = QImage(data, imageWidth, imageHeight, hasAlpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888); + + currentFace_ = face_; + currentFrame_ = frame_; + currentMip_ = mip_; + } + + QPoint center = QPoint(width()/2, height()/2) - QPoint(imageWidth/2, imageHeight/2); + painter.drawImage(center + pos_, image_); +} + +////////////////////////////////////////////////////////////////////////////////// +// ResourceWidget +////////////////////////////////////////////////////////////////////////////////// + +ResourceWidget::ResourceWidget(QWidget* parent) : + QWidget(parent) { + setup_ui(); +} + +void ResourceWidget::set_vtf(VTFLib::CVTFFile* file) { + table_->clear(); + + auto resources = file->GetResourceCount(); + table_->setRowCount(resources); + for (vlUInt i = 0; i < resources; ++i) { + auto type = file->GetResourceType(i); + vlUInt size; + auto data = file->GetResourceData(type, size); + + table_->setItem(i, 0, new QTableWidgetItem(GetResourceName(type))); + + auto typeItem = new QTableWidgetItem( + fmt::format(FMT_STRING("0x{:X}"), type).c_str() + ); + table_->setItem(i, 1, typeItem); + + auto sizeItem = new QTableWidgetItem( + fmt::format(FMT_STRING("{:d} bytes ({:.2f} KiB)"), size, size / 1024.f).c_str() + ); + table_->setItem(i, 2, sizeItem); + } +} + +void ResourceWidget::setup_ui() { + auto* layout = new QVBoxLayout(this); + + table_ = new QTableWidget(this); + table_->setSelectionBehavior(QAbstractItemView::SelectRows); + table_->verticalHeader()->hide(); + table_->setColumnCount(3); + table_->horizontalHeader()->setStretchLastSection(true); + table_->setHorizontalHeaderItem(0, new QTableWidgetItem("Resource Name")); + table_->setHorizontalHeaderItem(1, new QTableWidgetItem("Resource Type")); + table_->setHorizontalHeaderItem(2, new QTableWidgetItem("Data Size")); + + layout->addWidget(table_); +} diff --git a/src/gui/viewer.hpp b/src/gui/viewer.hpp new file mode 100644 index 0000000..590c45f --- /dev/null +++ b/src/gui/viewer.hpp @@ -0,0 +1,117 @@ + +#include +#include +#include +#include + +#include +#include + +#include "VTFLib.h" + +#pragma once + +namespace vtfview { + + /** + * Main window container for the VTF viewer + */ + class ViewerMainWindow : public QMainWindow { + Q_OBJECT; + + public: + ViewerMainWindow(QWidget* pParent); + + bool load_file(const char* path); + bool load_file(const void* data, size_t size); + bool load_file(VTFLib::CVTFFile* file); + void unload_file(); + + inline auto* file() { return file_; } + inline const auto* file() const { return file_; } + + protected: + void setup_ui(); + void reset_state(); + + signals: + /** + * Invoked whenever the vtf changes + * file may be nullptr if the file is fully unloaded + */ + void vtfFileChanged(VTFLib::CVTFFile* file); + + private: + VTFLib::CVTFFile* file_; + bool dirty_; + }; + + /** + * General VTF info widget + */ + class InfoWidget : public QWidget { + Q_OBJECT; + public: + InfoWidget(QWidget* pParent = nullptr); + + /** + * Update the widget with info from the specified VTF file + */ + void update_info(VTFLib::CVTFFile* file); + + private: + void setup_ui(); + inline QLineEdit* find(const std::string& l) { return fields_.find(l)->second; } + + std::unordered_map fields_; + }; + + /** + * Simple image viewer widget + */ + class ImageViewWidget : public QWidget { + Q_OBJECT; + public: + ImageViewWidget(QWidget* pParent = nullptr); + + void set_pixmap(const QImage& pixmap); + void set_vtf(VTFLib::CVTFFile* file); + + inline const QImage& pixmap() const { return image_; }; + inline QImage& pixmap() { return image_; }; + + void paintEvent(QPaintEvent* event) override; + + private: + QImage image_; + VTFLib::CVTFFile* file_; + + float zoom_ = 1.0f; + QPoint pos_; + + int frame_ = 0; + int face_ = 0; + int mip_ = 0; + + int currentFrame_ = 0; + int currentFace_ = 0; + int currentMip_ = 0; + }; + + /** + * Resource list widget + */ + class ResourceWidget : public QWidget { + Q_OBJECT; + public: + ResourceWidget(QWidget* parent); + + void set_vtf(VTFLib::CVTFFile* file); + + private: + void setup_ui(); + + QTableWidget* table_; + }; + +}