From bd2c2294767893ea8bb99e9d236cadb1f6c2d36a Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Wed, 21 Jun 2023 23:02:19 +0200 Subject: [PATCH] Added a PDF viewer Also added the PoDoFo and Poppler libraries as dependencies --- CMake/Packages/FindPoDoFo.cmake | 66 ++++++++ CMake/Packages/FindPoppler.cmake | 178 ++++++++++++++++++++ CMakeLists.txt | 8 +- es-app/CMakeLists.txt | 5 +- es-app/src/MediaViewer.cpp | 8 + es-app/src/MediaViewer.h | 1 + es-app/src/PDFViewer.cpp | 268 +++++++++++++++++++++++++++++++ es-app/src/PDFViewer.h | 56 +++++++ es-app/src/main.cpp | 2 + es-core/CMakeLists.txt | 2 +- es-core/src/Window.cpp | 51 +++++- es-core/src/Window.h | 22 +++ es-pdf-converter/CMakeLists.txt | 16 ++ es-pdf-converter/src/main.cpp | 88 ++++++++++ 14 files changed, 766 insertions(+), 5 deletions(-) create mode 100644 CMake/Packages/FindPoDoFo.cmake create mode 100644 CMake/Packages/FindPoppler.cmake create mode 100644 es-app/src/PDFViewer.cpp create mode 100644 es-app/src/PDFViewer.h create mode 100644 es-pdf-converter/CMakeLists.txt create mode 100644 es-pdf-converter/src/main.cpp diff --git a/CMake/Packages/FindPoDoFo.cmake b/CMake/Packages/FindPoDoFo.cmake new file mode 100644 index 000000000..34c1422b4 --- /dev/null +++ b/CMake/Packages/FindPoDoFo.cmake @@ -0,0 +1,66 @@ +# - Try to find the PoDoFo library +# +# Windows users MUST set when building: +# +# PoDoFo_USE_SHARED - whether use PoDoFo as shared library +# +# Once done this will define: +# +# PoDoFo_FOUND - system has the PoDoFo library +# PoDoFo_INCLUDE_DIRS - the PoDoFo include directory +# PoDoFo_LIBRARIES - the libraries needed to use PoDoFo +# PoDoFo_DEFINITIONS - the definitions needed to use PoDoFo +# +# SPDX-License-Identifier: BSD-3-Clause +# SPDX-FileCopyrightText: 2016 Pino Toscano + + +find_path(PoDoFo_INCLUDE_DIRS + NAMES podofo/podofo.h +) +find_library(PoDoFo_LIBRARIES + NAMES libpodofo podofo +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(PoDoFo DEFAULT_MSG PoDoFo_LIBRARIES PoDoFo_INCLUDE_DIRS) + +set(PoDoFo_DEFINITIONS) +if(PoDoFo_FOUND) + if(WIN32) + if(NOT DEFINED PoDoFo_USE_SHARED) + message(SEND_ERROR "Win32 users MUST set PoDoFo_USE_SHARED") + message(SEND_ERROR "Set -DPoDoFo_USE_SHARED=0 if linking to a static library PoDoFo") + message(SEND_ERROR "or -DPoDoFo_USE_SHARED=1 if linking to a DLL build of PoDoFo") + message(FATAL_ERROR "PoDoFo_USE_SHARED unset on win32 build") + else() + if(PoDoFo_USE_SHARED) + set(PoDoFo_DEFINITIONS "${PoDoFo_DEFINITIONS} -DUSING_SHARED_PODOFO") + endif(PoDoFo_USE_SHARED) + endif() + endif() + + # PoDoFo-0.9.5 unconditionally includes openssl/opensslconf.h in a public + # header. The fix is in https://sourceforge.net/p/podofo/code/1830/ and will + # hopefully be released soon with 0.9.6. Note that krename doesn't use + # OpenSSL in any way. + file(STRINGS "${PoDoFo_INCLUDE_DIRS}/podofo/base/podofo_config.h" PoDoFo_MAJOR_VER_LINE REGEX "^#define[ \t]+PODOFO_VERSION_MAJOR[ \t]+[0-9]+$") + file(STRINGS "${PoDoFo_INCLUDE_DIRS}/podofo/base/podofo_config.h" PoDoFo_MINOR_VER_LINE REGEX "^#define[ \t]+PODOFO_VERSION_MINOR[ \t]+[0-9]+$") + file(STRINGS "${PoDoFo_INCLUDE_DIRS}/podofo/base/podofo_config.h" PoDoFo_PATCH_VER_LINE REGEX "^#define[ \t]+PODOFO_VERSION_PATCH[ \t]+[0-9]+$") + string(REGEX REPLACE "^#define[ \t]+PODOFO_VERSION_MAJOR[ \t]+([0-9]+)$" "\\1" PoDoFo_MAJOR_VER "${PoDoFo_MAJOR_VER_LINE}") + string(REGEX REPLACE "^#define[ \t]+PODOFO_VERSION_MINOR[ \t]+([0-9]+)$" "\\1" PoDoFo_MINOR_VER "${PoDoFo_MINOR_VER_LINE}") + string(REGEX REPLACE "^#define[ \t]+PODOFO_VERSION_PATCH[ \t]+([0-9]+)$" "\\1" PoDoFo_PATCH_VER "${PoDoFo_PATCH_VER_LINE}") + set(PoDoFo_VERSION "${PoDoFo_MAJOR_VER}.${PoDoFo_MINOR_VER}.${PoDoFo_PATCH_VER}") + if(PoDoFo_VERSION VERSION_EQUAL "0.9.5") + find_package(OpenSSL) + if (OpenSSL_FOUND) + message("OpenSSL found, which is required for this version of PoDofo (0.9.5)") + set(PoDoFo_INCLUDE_DIRS ${PoDoFo_INCLUDE_DIRS} ${OPENSSL_INCLUDE_DIR}) + else() + unset(PoDoFo_FOUND) + message("OpenSSL NOT found, which is required for this version of PoDofo (0.9.5)") + endif() + endif() +endif() + +mark_as_advanced(PoDoFo_INCLUDE_DIRS PoDoFo_LIBRARIES PoDoFo_DEFINITIONS) diff --git a/CMake/Packages/FindPoppler.cmake b/CMake/Packages/FindPoppler.cmake new file mode 100644 index 000000000..513c65b95 --- /dev/null +++ b/CMake/Packages/FindPoppler.cmake @@ -0,0 +1,178 @@ +# - Try to find Poppler and specified components: {cpp, Qt4, Qt5} +# Once done this will define: +# +# POPPLER_FOUND - system has Poppler and specified components +# POPPLER_INCLUDE_DIRS - The include directories for Poppler headers +# POPPLER_LIBRARIES - Link these to use Poppler +# POPPLER_NEEDS_FONTCONFIG - A boolean indicating if libpoppler depends on libfontconfig +# POPPLER_HAS_XPDF - A boolean indicating if libpoppler headers are available +# POPPLER_INCLUDE_DIR - the include directory for libpoppler XPDF headers +# +# Redistribution and use of this file is allowed according to the terms of the +# MIT license. For details see the file COPYING-CMAKE-MODULES. + +if( POPPLER_LIBRARIES ) + # in cache already + set( Poppler_FIND_QUIETLY TRUE ) +endif( POPPLER_LIBRARIES ) + +# Check which components we need to find +list(FIND Poppler_FIND_COMPONENTS "cpp" FIND_POS) +if(${FIND_POS} EQUAL -1) + set(FIND_CPP FALSE) +else() + set(FIND_CPP TRUE) +endif() + +list(FIND Poppler_FIND_COMPONENTS "Qt4" FIND_POS) +if(${FIND_POS} EQUAL -1) + set(FIND_QT4 FALSE) +else() + set(FIND_QT4 TRUE) +endif() + +list(FIND Poppler_FIND_COMPONENTS "Qt5" FIND_POS) +if(${FIND_POS} EQUAL -1) + set(FIND_QT5 FALSE) +else() + set(FIND_QT5 TRUE) +endif() + +# Default values +set(POPPLER_FOUND FALSE) +set(POPPLER_INCLUDE_DIRS) +set(POPPLER_LIBRARIES) +set(POPPLER_REQUIRED "POPPLER_LIBRARY") + +# use pkg-config to get the directories and then use these values +# in the find_path() and find_library() calls +if( NOT WIN32 ) + find_package(PkgConfig) + + pkg_check_modules(POPPLER_PKG QUIET poppler) + if( FIND_CPP ) + pkg_check_modules(POPPLER_CPP_PKG QUIET poppler-cpp) + endif() + if( FIND_QT4 ) + pkg_check_modules(POPPLER_QT4_PKG QUIET poppler-qt4) + endif() + if( FIND_QT5 ) + pkg_check_modules(POPPLER_QT5_PKG QUIET poppler-qt5) + endif() +endif( NOT WIN32 ) + +# Check for Poppler headers (optional) +find_path( POPPLER_INCLUDE_DIR NAMES poppler-config.h PATH_SUFFIXES poppler ) +if( NOT( POPPLER_INCLUDE_DIR ) ) + #if( NOT Poppler_FIND_QUIETLY ) + # message( STATUS "Could not find poppler-config.h, recompile Poppler with " + # "ENABLE_XPDF_HEADERS to link against libpoppler directly." ) + #endif() + set( POPPLER_HAS_XPDF FALSE ) +else() + set( POPPLER_HAS_XPDF TRUE ) + list(APPEND POPPLER_INCLUDE_DIRS ${POPPLER_INCLUDE_DIR}) +endif() + +# Find libpoppler (Required) +find_library( POPPLER_LIBRARY NAMES poppler ${POPPLER_CPP_PKG_LIBRARIES} + HINTS ${POPPLER_PKG_LIBDIR} ${POPPLER_CPP_PKG_LIBDIR} ) +if( NOT(POPPLER_LIBRARY) ) + if( NOT Poppler_FIND_QUIETLY ) + message(STATUS "Could not find libpoppler." ) + endif( NOT Poppler_FIND_QUIETLY ) +else( NOT(POPPLER_LIBRARY) ) + list(APPEND POPPLER_LIBRARIES ${POPPLER_LIBRARY}) + + # Scan poppler libraries for dependencies on Fontconfig + include(GetPrerequisites) + mark_as_advanced(gp_cmd) + GET_PREREQUISITES("${POPPLER_LIBRARY}" POPPLER_PREREQS 1 0 "" "") + if("${POPPLER_PREREQS}" MATCHES "fontconfig") + set(POPPLER_NEEDS_FONTCONFIG TRUE) + else() + set(POPPLER_NEEDS_FONTCONFIG FALSE) + endif() + + # cpp Component + if( FIND_CPP ) + list(APPEND POPPLER_REQUIRED POPPLER_CPP_INCLUDE_DIR POPPLER_CPP_LIBRARY) + find_path( POPPLER_CPP_INCLUDE_DIR NAMES poppler-version.h + HINTS ${POPPLER_PKG_INCLUDEDIR} ${POPPLER_CPP_PKG_INCLUDEDIR} + PATH_SUFFIXES cpp poppler/cpp ) + if( NOT(POPPLER_CPP_INCLUDE_DIR) ) + if( NOT Poppler_FIND_QUIETLY ) + message(STATUS "Could not find Poppler cpp wrapper headers." ) + endif( NOT Poppler_FIND_QUIETLY ) + else() + list(APPEND POPPLER_INCLUDE_DIRS ${POPPLER_CPP_INCLUDE_DIR}) + endif() + find_library( + POPPLER_CPP_LIBRARY NAMES poppler-cpp ${POPPLER_CPP_PKG_LIBRARIES} + HINTS ${POPPLER_PKG_LIBDIR} ${POPPLER_CPP_PKG_LIBDIR} ) + if( NOT(POPPLER_CPP_LIBRARY) ) + if( NOT Poppler_FIND_QUIETLY ) + message(STATUS "Could not find libpoppler-cpp." ) + endif( NOT Poppler_FIND_QUIETLY ) + else() + list(APPEND POPPLER_LIBRARIES ${POPPLER_CPP_LIBRARY}) + endif() + endif() + + # Qt4 Component + if( FIND_QT4 ) + list(APPEND POPPLER_REQUIRED POPPLER_QT4_INCLUDE_DIR POPPLER_QT4_LIBRARY) + find_path(POPPLER_QT4_INCLUDE_DIR NAMES poppler-qt4.h poppler-link.h + HINTS ${POPPLER_PKG_INCLUDEDIR} ${POPPLER_CPP_QT4_INCLUDEDIR} + PATH_SUFFIXES qt4 poppler/qt4 ) + if( NOT(POPPLER_QT4_INCLUDE_DIR) ) + if( NOT Poppler_FIND_QUIETLY ) + message(STATUS "Could not find Poppler-Qt4 headers." ) + endif( NOT Poppler_FIND_QUIETLY ) + else() + list(APPEND POPPLER_INCLUDE_DIRS ${POPPLER_QT4_INCLUDE_DIR}) + endif() + find_library( + POPPLER_QT4_LIBRARY NAMES poppler-qt4 ${POPPLER_QT4_PKG_LIBRARIES} + HINTS ${POPPLER_PKG_LIBDIR} ${POPPLER_QT4_PKG_LIBDIR} ) + if( NOT(POPPLER_QT4_LIBRARY) ) + if( NOT Poppler_FIND_QUIETLY ) + message(STATUS "Could not find libpoppler-qt4." ) + endif( NOT Poppler_FIND_QUIETLY ) + else() + list(APPEND POPPLER_LIBRARIES ${POPPLER_QT4_LIBRARY}) + endif() + endif() + + # Qt5 Component + if( FIND_QT5 ) + list(APPEND POPPLER_REQUIRED POPPLER_QT5_INCLUDE_DIR POPPLER_QT5_LIBRARY) + find_path(POPPLER_QT5_INCLUDE_DIR NAMES poppler-qt5.h poppler-link.h + HINTS ${POPPLER_QT5_INCLUDEDIR} ${POPPLER_QT5_PKG_INCLUDEDIR} + PATH_SUFFIXES qt5 poppler/qt5 ) + if( NOT(POPPLER_QT5_INCLUDE_DIR) ) + if( NOT Poppler_FIND_QUIETLY ) + message( STATUS "Could not find Poppler-Qt5 headers." ) + endif( NOT Poppler_FIND_QUIETLY ) + else() + list(APPEND POPPLER_INCLUDE_DIRS ${POPPLER_QT5_INCLUDE_DIR}) + endif() + find_library( + POPPLER_QT5_LIBRARY NAMES poppler-qt5 ${POPPLER_QT5_PKG_LIBRARIES} + HINTS ${POPPLER_PKG_LIBDIR} ${POPPLER_QT5_PKG_LIBDIR} ) + if( NOT(POPPLER_QT5_LIBRARY) ) + if( NOT Poppler_FIND_QUIETLY ) + message(STATUS "Could not find libpoppler-qt5." ) + endif( NOT Poppler_FIND_QUIETLY ) + else() + list(APPEND POPPLER_LIBRARIES ${POPPLER_QT5_LIBRARY}) + endif() + endif() +endif( NOT(POPPLER_LIBRARY) ) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Poppler DEFAULT_MSG ${POPPLER_REQUIRED}) + +mark_as_advanced(POPPLER_CPP_INCLUDE_DIR POPPLER_QT4_INCLUDE_DIR + POPPLER_QT5_INCLUDE_DIR POPPLER_LIBRARIES POPPLER_CPP_LIBRARY + POPPLER_QT4_LIBRARY POPPLER_QT5_LIBRARY) diff --git a/CMakeLists.txt b/CMakeLists.txt index b65f91bdf..4a15adee6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -135,6 +135,7 @@ elseif(NOT EMSCRIPTEN) find_package(FreeImage REQUIRED) find_package(Freetype REQUIRED) find_package(Libgit2 REQUIRED) + find_package(PoDoFo REQUIRED) find_package(Pugixml REQUIRED) find_package(SDL2 REQUIRED) endif() @@ -455,6 +456,7 @@ else() ${FreeImage_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS} ${GIT2_INCLUDE_PATH} + ${PoDoFo_INCLUDE_DIRS} ${PUGIXML_INCLUDE_DIRS} ${SDL2_INCLUDE_DIR}) endif() @@ -571,6 +573,7 @@ else() ${FreeImage_LIBRARIES} ${FREETYPE_LIBRARIES} ${GIT2_LIBRARY} + ${PoDoFo_LIBRARIES} ${PUGIXML_LIBRARIES} ${SDL2_LIBRARY}) endif() @@ -626,10 +629,13 @@ set(EXECUTABLE_OUTPUT_PATH ${dir} CACHE PATH "Build directory" FORCE) set(LIBRARY_OUTPUT_PATH ${dir} CACHE PATH "Build directory" FORCE) # Add each component. +add_subdirectory(es-pdf-converter) add_subdirectory(external) add_subdirectory(es-core) add_subdirectory(es-app) -# Make sure rlottie is built before es-core and set lottie2gif to not be built. +# Make sure that es-pdf-convert is built first, and then that rlottie is built before es-core. +# Also set lottie2gif to not be built. +add_dependencies(lunasvg es-pdf-convert) add_dependencies(es-core rlottie) set_target_properties(lottie2gif PROPERTIES EXCLUDE_FROM_ALL 1 EXCLUDE_FROM_DEFAULT_BUILD 1) diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index b8cf81e0c..3ed6edb66 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -3,7 +3,7 @@ # EmulationStation Desktop Edition # CMakeLists.txt (es-app) # -# CMake configuration for es-app. +# CMake configuration for es-app # Also contains the application packaging configuration. # @@ -21,6 +21,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/MetaData.h ${CMAKE_CURRENT_SOURCE_DIR}/src/MiximageGenerator.h ${CMAKE_CURRENT_SOURCE_DIR}/src/PlatformId.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/PDFViewer.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Screensaver.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.h ${CMAKE_CURRENT_SOURCE_DIR}/src/UIModeController.h @@ -70,6 +71,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/MetaData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MiximageGenerator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/PlatformId.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/PDFViewer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Screensaver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/UIModeController.cpp @@ -228,6 +230,7 @@ elseif(APPLE) install(DIRECTORY ${CMAKE_SOURCE_DIR}/licenses DESTINATION ../Resources) else() install(TARGETS emulationstation RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) + install(TARGETS es-pdf-convert RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) if(CMAKE_SYSTEM_NAME MATCHES Linux) install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/emulationstation.6.gz DESTINATION ${CMAKE_INSTALL_PREFIX}/share/man/man6) diff --git a/es-app/src/MediaViewer.cpp b/es-app/src/MediaViewer.cpp index f0abb7c51..311322b2a 100644 --- a/es-app/src/MediaViewer.cpp +++ b/es-app/src/MediaViewer.cpp @@ -47,6 +47,14 @@ void MediaViewer::stopMediaViewer() mImages.clear(); } +void MediaViewer::launchPDFViewer() +{ + if (mGame->getManualPath() != "") { + Window::getInstance()->stopMediaViewer(); + Window::getInstance()->startPDFViewer(mGame); + } +} + void MediaViewer::update(int deltaTime) { if (mVideo) diff --git a/es-app/src/MediaViewer.h b/es-app/src/MediaViewer.h index 2ca73a38b..bbadaa4ca 100644 --- a/es-app/src/MediaViewer.h +++ b/es-app/src/MediaViewer.h @@ -21,6 +21,7 @@ public: bool startMediaViewer(FileData* game) override; void stopMediaViewer() override; + void launchPDFViewer() override; void update(int deltaTime) override; void render(const glm::mat4& parentTrans) override; diff --git a/es-app/src/PDFViewer.cpp b/es-app/src/PDFViewer.cpp new file mode 100644 index 000000000..9cc36f17c --- /dev/null +++ b/es-app/src/PDFViewer.cpp @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// PDFViewer.cpp +// +// Parses PDF documents using the PoDoFo library and renders pages using the Poppler +// library via the external es-pdf-convert binary. +// + +#include "PDFViewer.h" + +#include "Log.h" +#include "Sound.h" +#include "utils/FileSystemUtil.h" + +#define DEBUG_PDF_CONVERSION false + +PDFViewer::PDFViewer() + : mRenderer {Renderer::getInstance()} +{ + Window::getInstance()->setPDFViewer(this); + mTexture = TextureResource::get(""); + mPages.clear(); + mPageImage.reset(); +} + +bool PDFViewer::startPDFViewer(FileData* game) +{ + mManualPath = game->getManualPath(); + + if (!Utils::FileSystem::exists(mManualPath)) { + LOG(LogError) << "No PDF manual found for game \"" << game->getName() << "\""; + return false; + } + + LOG(LogDebug) << "PDFViewer::startPDFViewer(): Opening document \"" << mManualPath << "\""; + + PoDoFo::PdfMemDocument pdf; + mPages.clear(); + mPageCount = 0; + mCurrentPage = 0; + mScaleFactor = 1.0f; + + try { + pdf.Load(mManualPath.c_str()); + } + catch (PoDoFo::PdfError& e) { + LOG(LogError) << "PDFViewer: Couldn't load file \"" << mManualPath << "\", PoDoFo error \"" + << e.what() << ": " << e.ErrorMessage(e.GetError()) << "\""; + return false; + } + +#if (DEBUG_PDF_CONVERSION) + PoDoFo::EPdfVersion versionEPdf {pdf.GetPdfVersion()}; + std::string version {"unknown"}; + + switch (versionEPdf) { + case 0: + version = "1.0"; + break; + case 1: + version = "1.1"; + break; + case 2: + version = "1.2"; + break; + case 3: + version = "1.3"; + break; + case 4: + version = "1.4"; + break; + case 5: + version = "1.5"; + break; + case 6: + version = "1.6"; + break; + case 7: + version = "1.7"; + break; + default: + version = "unknown"; + }; + + LOG(LogDebug) << "PDF version: " << version; + LOG(LogDebug) << "Page count: " << pdf.GetPageCount(); +#endif + + mPageCount = static_cast(pdf.GetPageCount()); + + for (int i {0}; i < mPageCount; ++i) { + const int rotation {pdf.GetPage(i)->GetRotation()}; + const PoDoFo::PdfRect cropBox {pdf.GetPage(i)->GetCropBox()}; + float width {static_cast(cropBox.GetWidth())}; + float height {static_cast(cropBox.GetHeight())}; + + if (rotation != 0 && rotation != 180) + std::swap(width, height); + + // Maintain page aspect ratio. + glm::vec2 textureSize {glm::vec2 {width, height}}; + const glm::vec2 targetSize {glm::vec2 {mRenderer->getScreenWidth() * mScaleFactor, + mRenderer->getScreenHeight() * mScaleFactor}}; + glm::vec2 resizeScale {targetSize.x / textureSize.x, targetSize.y / textureSize.y}; + + if (resizeScale.x < resizeScale.y) { + textureSize.x *= resizeScale.x; + textureSize.y = std::min(textureSize.y * resizeScale.x, targetSize.y); + } + else { + textureSize.y *= resizeScale.y; + textureSize.x = std::min((textureSize.y / height) * width, targetSize.x); + } + + const int textureWidth {static_cast(std::round(textureSize.x))}; + const int textureHeight {static_cast(std::round(textureSize.y))}; + +#if (DEBUG_PDF_CONVERSION) + LOG(LogDebug) << "Page " << i + 1 << ": Rotation: " << rotation << " degrees / " + << "Crop box width: " << width << " / " + << "Crop box height: " << height << " / " + << "Size ratio: " << width / height << " / " + << "Texture size: " << textureWidth << "x" << textureHeight; +#endif + + mPages[i + 1] = PageEntry {textureWidth, textureHeight, {}}; + } + + mCurrentPage = 1; + convertPage(mCurrentPage); + return true; +} + +void PDFViewer::stopPDFViewer() +{ + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + mPages.clear(); + mPageImage.reset(); +} + +void PDFViewer::convertPage(int pageNum) +{ + assert(pageNum <= static_cast(mPages.size())); + + const std::string esConvertPath {Utils::FileSystem::getExePath() + "/es-pdf-convert"}; + if (!Utils::FileSystem::exists(esConvertPath)) { + LOG(LogError) << "Couldn't find PDF conversion binary es-pdf-convert"; + return; + } + + std::string command {Utils::FileSystem::getEscapedPath(esConvertPath)}; + command.append(" ") + .append(Utils::FileSystem::getEscapedPath(mManualPath)) + .append(" ") + .append(std::to_string(pageNum)) + .append(" ") + .append(std::to_string(mPages[pageNum].width)) + .append(" ") + .append(std::to_string(mPages[pageNum].height)); + + if (mPages[pageNum].imageData.empty()) { +#if (DEBUG_PDF_CONVERSION) + LOG(LogDebug) << "Converting page: " << mCurrentPage; + LOG(LogDebug) << command; +#endif + FILE* commandPipe; + std::array buffer {}; + std::string imageData; + int returnValue; + + if (!(commandPipe = reinterpret_cast(popen(command.c_str(), "r")))) { + LOG(LogError) << "Couldn't open pipe to es-pdf-convert"; + return; + } + + while (fread(buffer.data(), 1, 512, commandPipe)) { + mPages[pageNum].imageData.insert(mPages[pageNum].imageData.end(), + std::make_move_iterator(buffer.begin()), + std::make_move_iterator(buffer.end())); + } + + returnValue = pclose(commandPipe); + size_t imageDataSize {mPages[pageNum].imageData.size()}; + + if (returnValue != 0 || (static_cast(imageDataSize) < + mPages[pageNum].width * mPages[pageNum].height * 4)) { + LOG(LogError) << "Error reading PDF file"; + mPages[pageNum].imageData.clear(); + return; + } + } + else { +#if (DEBUG_PDF_CONVERSION) + LOG(LogDebug) << "Using cached texture for page: " << mCurrentPage; +#endif + } + + mPageImage.reset(); + mPageImage = std::make_unique(false, false); + mPageImage->setOrigin(0.5f, 0.5f); + mPageImage->setPosition(mRenderer->getScreenWidth() / 2.0f, + mRenderer->getScreenHeight() / 2.0f); + + mPageImage->setFlipY(true); + mPageImage->setMaxSize( + glm::vec2 {mPages[pageNum].width / mScaleFactor, mPages[pageNum].height / mScaleFactor}); + mPageImage->setRawImage(reinterpret_cast(&mPages[pageNum].imageData[0]), + mPages[pageNum].width, mPages[pageNum].height); + +#if (DEBUG_PDF_CONVERSION) + LOG(LogDebug) << "ABGR32 data stream size: " << mPages[pageNum].imageData.size(); +#endif +} + +void PDFViewer::render(const glm::mat4& /*parentTrans*/) +{ + glm::mat4 trans {Renderer::getIdentity()}; + mRenderer->setMatrix(trans); + + // Render a black background below the document. + mRenderer->drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), + 0x000000FF, 0x000000FF); + + if (mPageImage != nullptr) { + mPageImage->render(trans); + } +} + +void PDFViewer::showNextPage() +{ + if (mCurrentPage == mPageCount) + return; + + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + ++mCurrentPage; + convertPage(mCurrentPage); +} + +void PDFViewer::showPreviousPage() +{ + if (mCurrentPage == 1) + return; + + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + --mCurrentPage; + convertPage(mCurrentPage); +} + +void PDFViewer::showFirstPage() +{ + if (mCurrentPage == 1) + return; + + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + mCurrentPage = 1; + convertPage(mCurrentPage); +} + +void PDFViewer::showLastPage() +{ + if (mCurrentPage == mPageCount) + return; + + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + mCurrentPage = mPageCount; + convertPage(mCurrentPage); +} diff --git a/es-app/src/PDFViewer.h b/es-app/src/PDFViewer.h new file mode 100644 index 000000000..9fe89b952 --- /dev/null +++ b/es-app/src/PDFViewer.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// PDFViewer.h +// +// Parses PDF documents using the PoDoFo library and renders pages using the Poppler +// library via the external es-pdf-convert binary. +// + +#ifndef ES_APP_PDF_VIEWER_H +#define ES_APP_PDF_VIEWER_H + +#include "FileData.h" +#include "Window.h" +#include "components/ImageComponent.h" + +#include + +class PDFViewer : public Window::PDFViewer +{ +public: + PDFViewer(); + ~PDFViewer() { stopPDFViewer(); } + + bool startPDFViewer(FileData* game) override; + void stopPDFViewer() override; + + void convertPage(int pageNum); + + void render(const glm::mat4& parentTrans) override; + +private: + void showNextPage() override; + void showPreviousPage() override; + void showFirstPage() override; + void showLastPage() override; + + struct PageEntry { + int width; + int height; + std::vector imageData; + }; + + Renderer* mRenderer; + std::shared_ptr mTexture; + std::unique_ptr mPageImage; + std::map mPages; + + float mScaleFactor; + int mCurrentPage; + int mPageCount; + + std::string mManualPath; +}; + +#endif // ES_APP_PDF_VIEWER_H diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 9a67aedfc..8b62809d2 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -25,6 +25,7 @@ #include "Log.h" #include "MameNames.h" #include "MediaViewer.h" +#include "PDFViewer.h" #include "Screensaver.h" #include "Scripting.h" #include "Settings.h" @@ -725,6 +726,7 @@ int main(int argc, char* argv[]) CollectionSystemsManager::getInstance(); Screensaver screensaver; MediaViewer mediaViewer; + PDFViewer pdfViewer; GuiLaunchScreen guiLaunchScreen; if (!window->init()) { diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index 59b07a8cf..91c91604a 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -3,7 +3,7 @@ # EmulationStation Desktop Edition # CMakeLists.txt (es-core) # -# CMake configuration for es-core. +# CMake configuration for es-core # project(core) diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 820bbea30..bb7135c8f 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -29,6 +29,7 @@ Window::Window() noexcept , mBackgroundOverlayOpacity {1.0f} , mScreensaver {nullptr} , mMediaViewer {nullptr} + , mPDFViewer {nullptr} , mLaunchScreen {nullptr} , mInfoPopup {nullptr} , mListScrollOpacity {0.0f} @@ -40,6 +41,7 @@ Window::Window() noexcept , mRenderScreensaver {false} , mRenderMediaViewer {false} , mRenderLaunchScreen {false} + , mRenderPDFViewer {false} , mGameLaunchedState {false} , mAllowTextScrolling {true} , mAllowFileAnimation {true} @@ -219,16 +221,35 @@ void Window::input(InputConfig* config, Input input) } if (mMediaViewer && mRenderMediaViewer) { - if (config->isMappedLike("right", input) && input.value != 0) + if (config->isMappedLike("y", input) && input.value != 0) { + mMediaViewer->launchPDFViewer(); + return; + } + else if (config->isMappedLike("right", input) && input.value != 0) mMediaViewer->showNext(); else if (config->isMappedLike("left", input) && input.value != 0) mMediaViewer->showPrevious(); else if (input.value != 0) - // Any other input than left or right stops the media viewer. + // Any other input stops the media viewer. stopMediaViewer(); return; } + if (mPDFViewer && mRenderPDFViewer) { + if (config->isMappedLike("right", input) && input.value != 0) + mPDFViewer->showNextPage(); + else if (config->isMappedLike("left", input) && input.value != 0) + mPDFViewer->showPreviousPage(); + else if (config->isMappedLike("righttrigger", input) && input.value != 0) + mPDFViewer->showLastPage(); + else if (config->isMappedLike("lefttrigger", input) && input.value != 0) + mPDFViewer->showFirstPage(); + else if (input.value != 0) + // Any other input stops the PDF viewer. + stopPDFViewer(); + return; + } + if (mGameLaunchedState && mLaunchScreen && mRenderLaunchScreen) { if (input.value != 0) { mLaunchScreen->closeLaunchScreen(); @@ -654,6 +675,9 @@ void Window::render() if (mRenderMediaViewer) mMediaViewer->render(trans); + if (mRenderPDFViewer) + mPDFViewer->render(trans); + if (mRenderLaunchScreen) mLaunchScreen->render(trans); @@ -858,6 +882,29 @@ void Window::stopMediaViewer() mRenderMediaViewer = false; } +void Window::startPDFViewer(FileData* game) +{ + if (mPDFViewer) { + if (mPDFViewer->startPDFViewer(game)) { + setAllowTextScrolling(false); + setAllowFileAnimation(false); + + mRenderPDFViewer = true; + } + } +} + +void Window::stopPDFViewer() +{ + if (mPDFViewer) { + mPDFViewer->stopPDFViewer(); + setAllowTextScrolling(true); + setAllowFileAnimation(true); + } + + mRenderPDFViewer = false; +} + void Window::displayLaunchScreen(FileData* game) { if (mLaunchScreen) { diff --git a/es-core/src/Window.h b/es-core/src/Window.h index d47424a2a..afbaafd9c 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -56,6 +56,7 @@ public: public: virtual bool startMediaViewer(FileData* game) = 0; virtual void stopMediaViewer() = 0; + virtual void launchPDFViewer() = 0; virtual void showNext() = 0; virtual void showPrevious() = 0; @@ -64,6 +65,20 @@ public: virtual void render(const glm::mat4& parentTrans) = 0; }; + class PDFViewer + { + public: + virtual bool startPDFViewer(FileData* game) = 0; + virtual void stopPDFViewer() = 0; + + virtual void showNextPage() = 0; + virtual void showPreviousPage() = 0; + virtual void showFirstPage() = 0; + virtual void showLastPage() = 0; + + virtual void render(const glm::mat4& parentTrans) = 0; + }; + class GuiLaunchScreen { public: @@ -124,6 +139,11 @@ public: void setMediaViewer(MediaViewer* mediaViewer) { mMediaViewer = mediaViewer; } bool isMediaViewerActive() { return mRenderMediaViewer; } + void startPDFViewer(FileData* game); + void stopPDFViewer(); + void setPDFViewer(PDFViewer* pdfViewer) { mPDFViewer = pdfViewer; } + bool isPDFViewerActive() { return mRenderPDFViewer; } + void displayLaunchScreen(FileData* game); void closeLaunchScreen(); void setLaunchScreen(GuiLaunchScreen* launchScreen) { mLaunchScreen = launchScreen; } @@ -180,6 +200,7 @@ private: Screensaver* mScreensaver; MediaViewer* mMediaViewer; + PDFViewer* mPDFViewer; GuiLaunchScreen* mLaunchScreen; GuiInfoPopup* mInfoPopup; @@ -200,6 +221,7 @@ private: bool mRenderScreensaver; bool mRenderMediaViewer; bool mRenderLaunchScreen; + bool mRenderPDFViewer; bool mGameLaunchedState; bool mAllowTextScrolling; bool mAllowFileAnimation; diff --git a/es-pdf-converter/CMakeLists.txt b/es-pdf-converter/CMakeLists.txt new file mode 100644 index 000000000..53eb0eb5e --- /dev/null +++ b/es-pdf-converter/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: MIT +# +# EmulationStation Desktop Edition +# CMakeLists.txt (es-pdf-converter) +# +# CMake configuration for es-pdf-convert +# + +project(es-pdf-convert) + +find_package(Poppler REQUIRED COMPONENTS cpp) + +include_directories(${POPPLER_CPP_INCLUDE_DIR}) +add_executable(es-pdf-convert ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp) +target_link_libraries(es-pdf-convert ${POPPLER_CPP_LIBRARY}) +set_target_properties(es-pdf-convert PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE) diff --git a/es-pdf-converter/src/main.cpp b/es-pdf-converter/src/main.cpp new file mode 100644 index 000000000..3ca158401 --- /dev/null +++ b/es-pdf-converter/src/main.cpp @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-only +// +// EmulationStation Desktop Edition (ES-DE) PDF converter +// main.cpp +// +// Converts PDF document pages to raw ARGB32 image data for maximum performance. +// This needs to be separated into its own binary to get around the restrictive GPL +// license used by the Poppler PDF rendering library. +// +// The column limit is 100 characters. +// All ES-DE C++ source code is formatted using clang-format. +// + +#include "poppler-document.h" +#include "poppler-image.h" +#include "poppler-page-renderer.h" +#include "poppler-page.h" + +#include +#include + +int main(int argc, char* argv[]) +{ + if (argc != 5) { + std::cout << "Usage: es-pdf-convert " + "" + << std::endl; + exit(-1); + } + + const std::string path {argv[1]}; + const int pageNum {atoi(argv[2])}; + const int width {atoi(argv[3])}; + const int height {atoi(argv[4])}; + + if (width < 1 || width > 7680) { + std::cerr << "Invalid horizontal resolution defined: " << argv[3] << std::endl; + exit(-1); + } + + if (height < 1 || height > 7680) { + std::cerr << "Invalid vertical resolution defined: " << argv[4] << std::endl; + exit(-1); + } + + // std::cerr << "Converting file \"" << path << "\", page " << pageNum << " to resolution " + // << width << "x" << height << " pixels" << std::endl; + + const poppler::document* document {poppler::document::load_from_file(path)}; + + if (document == nullptr) + exit(-1); + + if (pageNum < 1 || pageNum > document->pages()) { + std::cerr << "Error: Requested page " << pageNum << " does not exist in document" + << std::endl; + exit(-1); + } + + const poppler::page* page {document->create_page(pageNum - 1)}; + poppler::page_renderer pageRenderer; + + pageRenderer.set_render_hint(poppler::page_renderer::text_antialiasing); + pageRenderer.set_render_hint(poppler::page_renderer::antialiasing); + // pageRenderer.set_render_hint(poppler::page_renderer::text_hinting); + + const poppler::rectf pageRect {page->page_rect()}; + const bool portraitOrientation {page->orientation() == poppler::page::portrait}; + const double pageHeight {pageRect.height()}; + const double sizeFactor {static_cast(portraitOrientation ? height : width) / + pageHeight}; + + poppler::image image {pageRenderer.render_page( + page, static_cast(std::round(72.0 * sizeFactor)), + static_cast(std::round(72.0 * sizeFactor)), 0, 0, width, height)}; + + if (!image.is_valid()) { + std::cerr << "Rendered image is invalid" << std::endl; + exit(-1); + } + + // Necessary as the image data stream may contain null characters. + std::string imageARGB32; + imageARGB32.insert(0, std::move(image.data()), width * height * 4); + + std::cout << imageARGB32; + return 0; +}