Added a PDF viewer

Also added the PoDoFo and Poppler libraries as dependencies
This commit is contained in:
Leon Styhre 2023-06-21 23:02:19 +02:00
parent 177dd23b7c
commit bd2c229476
14 changed files with 766 additions and 5 deletions

View file

@ -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 <pino@kde.org>
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)

View file

@ -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)

View file

@ -135,6 +135,7 @@ elseif(NOT EMSCRIPTEN)
find_package(FreeImage REQUIRED) find_package(FreeImage REQUIRED)
find_package(Freetype REQUIRED) find_package(Freetype REQUIRED)
find_package(Libgit2 REQUIRED) find_package(Libgit2 REQUIRED)
find_package(PoDoFo REQUIRED)
find_package(Pugixml REQUIRED) find_package(Pugixml REQUIRED)
find_package(SDL2 REQUIRED) find_package(SDL2 REQUIRED)
endif() endif()
@ -455,6 +456,7 @@ else()
${FreeImage_INCLUDE_DIRS} ${FreeImage_INCLUDE_DIRS}
${FREETYPE_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS}
${GIT2_INCLUDE_PATH} ${GIT2_INCLUDE_PATH}
${PoDoFo_INCLUDE_DIRS}
${PUGIXML_INCLUDE_DIRS} ${PUGIXML_INCLUDE_DIRS}
${SDL2_INCLUDE_DIR}) ${SDL2_INCLUDE_DIR})
endif() endif()
@ -571,6 +573,7 @@ else()
${FreeImage_LIBRARIES} ${FreeImage_LIBRARIES}
${FREETYPE_LIBRARIES} ${FREETYPE_LIBRARIES}
${GIT2_LIBRARY} ${GIT2_LIBRARY}
${PoDoFo_LIBRARIES}
${PUGIXML_LIBRARIES} ${PUGIXML_LIBRARIES}
${SDL2_LIBRARY}) ${SDL2_LIBRARY})
endif() 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) set(LIBRARY_OUTPUT_PATH ${dir} CACHE PATH "Build directory" FORCE)
# Add each component. # Add each component.
add_subdirectory(es-pdf-converter)
add_subdirectory(external) add_subdirectory(external)
add_subdirectory(es-core) add_subdirectory(es-core)
add_subdirectory(es-app) 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) add_dependencies(es-core rlottie)
set_target_properties(lottie2gif PROPERTIES EXCLUDE_FROM_ALL 1 EXCLUDE_FROM_DEFAULT_BUILD 1) set_target_properties(lottie2gif PROPERTIES EXCLUDE_FROM_ALL 1 EXCLUDE_FROM_DEFAULT_BUILD 1)

View file

@ -3,7 +3,7 @@
# EmulationStation Desktop Edition # EmulationStation Desktop Edition
# CMakeLists.txt (es-app) # CMakeLists.txt (es-app)
# #
# CMake configuration for es-app. # CMake configuration for es-app
# Also contains the application packaging configuration. # 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/MetaData.h
${CMAKE_CURRENT_SOURCE_DIR}/src/MiximageGenerator.h ${CMAKE_CURRENT_SOURCE_DIR}/src/MiximageGenerator.h
${CMAKE_CURRENT_SOURCE_DIR}/src/PlatformId.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/Screensaver.h
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.h
${CMAKE_CURRENT_SOURCE_DIR}/src/UIModeController.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/MetaData.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/MiximageGenerator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/MiximageGenerator.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/PlatformId.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/Screensaver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/UIModeController.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/UIModeController.cpp
@ -228,6 +230,7 @@ elseif(APPLE)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/licenses DESTINATION ../Resources) install(DIRECTORY ${CMAKE_SOURCE_DIR}/licenses DESTINATION ../Resources)
else() else()
install(TARGETS emulationstation RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin) 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) if(CMAKE_SYSTEM_NAME MATCHES Linux)
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/emulationstation.6.gz install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/assets/emulationstation.6.gz
DESTINATION ${CMAKE_INSTALL_PREFIX}/share/man/man6) DESTINATION ${CMAKE_INSTALL_PREFIX}/share/man/man6)

View file

@ -47,6 +47,14 @@ void MediaViewer::stopMediaViewer()
mImages.clear(); mImages.clear();
} }
void MediaViewer::launchPDFViewer()
{
if (mGame->getManualPath() != "") {
Window::getInstance()->stopMediaViewer();
Window::getInstance()->startPDFViewer(mGame);
}
}
void MediaViewer::update(int deltaTime) void MediaViewer::update(int deltaTime)
{ {
if (mVideo) if (mVideo)

View file

@ -21,6 +21,7 @@ public:
bool startMediaViewer(FileData* game) override; bool startMediaViewer(FileData* game) override;
void stopMediaViewer() override; void stopMediaViewer() override;
void launchPDFViewer() override;
void update(int deltaTime) override; void update(int deltaTime) override;
void render(const glm::mat4& parentTrans) override; void render(const glm::mat4& parentTrans) override;

268
es-app/src/PDFViewer.cpp Normal file
View file

@ -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<int>(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<float>(cropBox.GetWidth())};
float height {static_cast<float>(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<int>(std::round(textureSize.x))};
const int textureHeight {static_cast<int>(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<int>(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<char, 512> buffer {};
std::string imageData;
int returnValue;
if (!(commandPipe = reinterpret_cast<FILE*>(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<int>(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<ImageComponent>(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<const unsigned char*>(&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);
}

56
es-app/src/PDFViewer.h Normal file
View file

@ -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 <podofo/podofo.h>
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<char> imageData;
};
Renderer* mRenderer;
std::shared_ptr<TextureResource> mTexture;
std::unique_ptr<ImageComponent> mPageImage;
std::map<int, PageEntry> mPages;
float mScaleFactor;
int mCurrentPage;
int mPageCount;
std::string mManualPath;
};
#endif // ES_APP_PDF_VIEWER_H

View file

@ -25,6 +25,7 @@
#include "Log.h" #include "Log.h"
#include "MameNames.h" #include "MameNames.h"
#include "MediaViewer.h" #include "MediaViewer.h"
#include "PDFViewer.h"
#include "Screensaver.h" #include "Screensaver.h"
#include "Scripting.h" #include "Scripting.h"
#include "Settings.h" #include "Settings.h"
@ -725,6 +726,7 @@ int main(int argc, char* argv[])
CollectionSystemsManager::getInstance(); CollectionSystemsManager::getInstance();
Screensaver screensaver; Screensaver screensaver;
MediaViewer mediaViewer; MediaViewer mediaViewer;
PDFViewer pdfViewer;
GuiLaunchScreen guiLaunchScreen; GuiLaunchScreen guiLaunchScreen;
if (!window->init()) { if (!window->init()) {

View file

@ -3,7 +3,7 @@
# EmulationStation Desktop Edition # EmulationStation Desktop Edition
# CMakeLists.txt (es-core) # CMakeLists.txt (es-core)
# #
# CMake configuration for es-core. # CMake configuration for es-core
# #
project(core) project(core)

View file

@ -29,6 +29,7 @@ Window::Window() noexcept
, mBackgroundOverlayOpacity {1.0f} , mBackgroundOverlayOpacity {1.0f}
, mScreensaver {nullptr} , mScreensaver {nullptr}
, mMediaViewer {nullptr} , mMediaViewer {nullptr}
, mPDFViewer {nullptr}
, mLaunchScreen {nullptr} , mLaunchScreen {nullptr}
, mInfoPopup {nullptr} , mInfoPopup {nullptr}
, mListScrollOpacity {0.0f} , mListScrollOpacity {0.0f}
@ -40,6 +41,7 @@ Window::Window() noexcept
, mRenderScreensaver {false} , mRenderScreensaver {false}
, mRenderMediaViewer {false} , mRenderMediaViewer {false}
, mRenderLaunchScreen {false} , mRenderLaunchScreen {false}
, mRenderPDFViewer {false}
, mGameLaunchedState {false} , mGameLaunchedState {false}
, mAllowTextScrolling {true} , mAllowTextScrolling {true}
, mAllowFileAnimation {true} , mAllowFileAnimation {true}
@ -219,16 +221,35 @@ void Window::input(InputConfig* config, Input input)
} }
if (mMediaViewer && mRenderMediaViewer) { 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(); mMediaViewer->showNext();
else if (config->isMappedLike("left", input) && input.value != 0) else if (config->isMappedLike("left", input) && input.value != 0)
mMediaViewer->showPrevious(); mMediaViewer->showPrevious();
else if (input.value != 0) else if (input.value != 0)
// Any other input than left or right stops the media viewer. // Any other input stops the media viewer.
stopMediaViewer(); stopMediaViewer();
return; 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 (mGameLaunchedState && mLaunchScreen && mRenderLaunchScreen) {
if (input.value != 0) { if (input.value != 0) {
mLaunchScreen->closeLaunchScreen(); mLaunchScreen->closeLaunchScreen();
@ -654,6 +675,9 @@ void Window::render()
if (mRenderMediaViewer) if (mRenderMediaViewer)
mMediaViewer->render(trans); mMediaViewer->render(trans);
if (mRenderPDFViewer)
mPDFViewer->render(trans);
if (mRenderLaunchScreen) if (mRenderLaunchScreen)
mLaunchScreen->render(trans); mLaunchScreen->render(trans);
@ -858,6 +882,29 @@ void Window::stopMediaViewer()
mRenderMediaViewer = false; 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) void Window::displayLaunchScreen(FileData* game)
{ {
if (mLaunchScreen) { if (mLaunchScreen) {

View file

@ -56,6 +56,7 @@ public:
public: public:
virtual bool startMediaViewer(FileData* game) = 0; virtual bool startMediaViewer(FileData* game) = 0;
virtual void stopMediaViewer() = 0; virtual void stopMediaViewer() = 0;
virtual void launchPDFViewer() = 0;
virtual void showNext() = 0; virtual void showNext() = 0;
virtual void showPrevious() = 0; virtual void showPrevious() = 0;
@ -64,6 +65,20 @@ public:
virtual void render(const glm::mat4& parentTrans) = 0; 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 class GuiLaunchScreen
{ {
public: public:
@ -124,6 +139,11 @@ public:
void setMediaViewer(MediaViewer* mediaViewer) { mMediaViewer = mediaViewer; } void setMediaViewer(MediaViewer* mediaViewer) { mMediaViewer = mediaViewer; }
bool isMediaViewerActive() { return mRenderMediaViewer; } 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 displayLaunchScreen(FileData* game);
void closeLaunchScreen(); void closeLaunchScreen();
void setLaunchScreen(GuiLaunchScreen* launchScreen) { mLaunchScreen = launchScreen; } void setLaunchScreen(GuiLaunchScreen* launchScreen) { mLaunchScreen = launchScreen; }
@ -180,6 +200,7 @@ private:
Screensaver* mScreensaver; Screensaver* mScreensaver;
MediaViewer* mMediaViewer; MediaViewer* mMediaViewer;
PDFViewer* mPDFViewer;
GuiLaunchScreen* mLaunchScreen; GuiLaunchScreen* mLaunchScreen;
GuiInfoPopup* mInfoPopup; GuiInfoPopup* mInfoPopup;
@ -200,6 +221,7 @@ private:
bool mRenderScreensaver; bool mRenderScreensaver;
bool mRenderMediaViewer; bool mRenderMediaViewer;
bool mRenderLaunchScreen; bool mRenderLaunchScreen;
bool mRenderPDFViewer;
bool mGameLaunchedState; bool mGameLaunchedState;
bool mAllowTextScrolling; bool mAllowTextScrolling;
bool mAllowFileAnimation; bool mAllowFileAnimation;

View file

@ -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)

View file

@ -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 <cmath>
#include <iostream>
int main(int argc, char* argv[])
{
if (argc != 5) {
std::cout << "Usage: es-pdf-convert <filename> <page number> <horizontal resolution> "
"<vertical resolution>"
<< 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<double>(portraitOrientation ? height : width) /
pageHeight};
poppler::image image {pageRenderer.render_page(
page, static_cast<int>(std::round(72.0 * sizeFactor)),
static_cast<int>(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;
}