From 6bf8c5af461170c33a7dfdd27244943a2a50dbcc Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Thu, 22 Jun 2023 21:15:35 +0200 Subject: [PATCH] Added Windows support for the PDF viewer --- .gitignore | 2 + es-app/src/PDFViewer.cpp | 186 ++++++++++++++++++++++++++++- es-core/src/utils/PlatformUtil.cpp | 8 +- es-pdf-converter/CMakeLists.txt | 14 ++- es-pdf-converter/src/main.cpp | 74 +++++++++++- 5 files changed, 268 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 7ab39b163..39b9c1561 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,8 @@ TODO.md # MSVC EmulationStation.ilk EmulationStation.pdb +es-pdf-convert.ilk +es-pdf-convert.pdb lunasvg.exp lunasvg.ilk lunasvg.pdb diff --git a/es-app/src/PDFViewer.cpp b/es-app/src/PDFViewer.cpp index 7a2692403..34c8d7df3 100644 --- a/es-app/src/PDFViewer.cpp +++ b/es-app/src/PDFViewer.cpp @@ -13,6 +13,12 @@ #include "utils/FileSystemUtil.h" #include "utils/StringUtil.h" +#include + +#if defined(_WIN64) +#include +#endif + #define DEBUG_PDF_CONVERSION false PDFViewer::PDFViewer() @@ -24,9 +30,18 @@ PDFViewer::PDFViewer() bool PDFViewer::startPDFViewer(FileData* game) { - mESConvertPath = Utils::FileSystem::getExePath() + "/es-pdf-convert"; +#if defined(_WIN64) + const std::string convertBinary {"/es-pdf-converter/es-pdf-convert.exe"}; +#else + const std::string convertBinary {"/es-pdf-convert"}; +#endif + mESConvertPath = Utils::FileSystem::getExePath() + convertBinary; if (!Utils::FileSystem::exists(mESConvertPath)) { +#if defined(_WIN64) + LOG(LogError) << "Couldn't find PDF conversion binary es-pdf-convert.exe"; +#else LOG(LogError) << "Couldn't find PDF conversion binary es-pdf-convert"; +#endif return false; } @@ -37,6 +52,10 @@ bool PDFViewer::startPDFViewer(FileData* game) return false; } +#if defined(_WIN64) + mManualPath = Utils::String::replace(mManualPath, "/", "\\"); +#endif + LOG(LogDebug) << "PDFViewer::startPDFViewer(): Opening document \"" << mManualPath << "\""; mPages.clear(); @@ -50,7 +69,7 @@ bool PDFViewer::startPDFViewer(FileData* game) return false; } - mPageCount = mPages.size(); + mPageCount = static_cast(mPages.size()); for (int i {1}; i <= mPageCount; ++i) { if (mPages.find(i) == mPages.end()) { @@ -106,9 +125,80 @@ void PDFViewer::stopPDFViewer() bool PDFViewer::getDocumentInfo() { + std::string commandOutput; + +#if defined(_WIN64) + std::wstring command { + Utils::String::stringToWideString(Utils::FileSystem::getEscapedPath(mESConvertPath))}; + command.append(L" -fileinfo ") + .append(Utils::String::stringToWideString(Utils::FileSystem::getEscapedPath(mManualPath))); + + STARTUPINFOW si {}; + PROCESS_INFORMATION pi; + HANDLE childStdoutRead {nullptr}; + HANDLE childStdoutWrite {nullptr}; + SECURITY_ATTRIBUTES saAttr {}; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = true; + saAttr.lpSecurityDescriptor = nullptr; + + CreatePipe(&childStdoutRead, &childStdoutWrite, &saAttr, 0); + SetHandleInformation(childStdoutRead, HANDLE_FLAG_INHERIT, 0); + + si.cb = sizeof(STARTUPINFOW); + si.hStdOutput = childStdoutWrite; + si.dwFlags |= STARTF_USESTDHANDLES; + + bool processReturnValue {true}; + + // clang-format off + processReturnValue = CreateProcessW( + nullptr, // No application name (use command line). + const_cast(command.c_str()), // Command line. + nullptr, // Process attributes. + nullptr, // Thread attributes. + TRUE, // Handles inheritance. + 0, // Creation flags. + nullptr, // Use parent's environment block. + nullptr, // Starting directory, possibly the same as parent. + &si, // Pointer to the STARTUPINFOW structure. + &pi); // Pointer to the PROCESS_INFORMATION structure. + // clang-format on + + if (!processReturnValue) { + LOG(LogError) << "Couldn't read PDF document information"; + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return false; + } + + // Close process and thread handles. + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + CloseHandle(childStdoutWrite); + + std::array buffer {}; + DWORD dwRead; + bool readValue {true}; + + while (readValue) { + readValue = ReadFile(childStdoutRead, &buffer[0], 512, &dwRead, nullptr); + if (readValue) { + for (int i {0}; i < 512; ++i) { + if (buffer[i] == '\0') + break; + commandOutput.append(1, buffer[i]); + } + buffer.fill('\0'); + } + } + + CloseHandle(childStdoutRead); + WaitForSingleObject(pi.hThread, INFINITE); + WaitForSingleObject(pi.hProcess, INFINITE); +#else FILE* commandPipe; std::array buffer {}; - std::string commandOutput; std::string command {Utils::FileSystem::getEscapedPath(mESConvertPath)}; command.append(" -fileinfo ").append(Utils::FileSystem::getEscapedPath(mManualPath)); @@ -129,6 +219,7 @@ bool PDFViewer::getDocumentInfo() if (pclose(commandPipe) != 0) return false; +#endif const std::vector pageRows { Utils::String::delimitedStringToVector(commandOutput, "\n")}; @@ -150,6 +241,18 @@ void PDFViewer::convertPage(int pageNum) { assert(pageNum <= static_cast(mPages.size())); +#if defined(_WIN64) + std::wstring command { + Utils::String::stringToWideString(Utils::FileSystem::getEscapedPath(mESConvertPath))}; + command.append(L" -convert ") + .append(Utils::String::stringToWideString(Utils::FileSystem::getEscapedPath(mManualPath))) + .append(L" ") + .append(std::to_wstring(pageNum)) + .append(L" ") + .append(std::to_wstring(static_cast(mPages[pageNum].width))) + .append(L" ") + .append(std::to_wstring(static_cast(mPages[pageNum].height))); +#else std::string command {Utils::FileSystem::getEscapedPath(mESConvertPath)}; command.append(" -convert ") .append(Utils::FileSystem::getEscapedPath(mManualPath)) @@ -159,15 +262,81 @@ void PDFViewer::convertPage(int pageNum) .append(std::to_string(static_cast(mPages[pageNum].width))) .append(" ") .append(std::to_string(static_cast(mPages[pageNum].height))); +#endif if (mPages[pageNum].imageData.empty()) { #if (DEBUG_PDF_CONVERSION) LOG(LogDebug) << "Converting page: " << mCurrentPage; +#if defined(_WIN64) + LOG(LogDebug) << Utils::String::wideStringToString(command); +#else LOG(LogDebug) << command; #endif +#endif + std::string imageData; +#if defined(_WIN64) + STARTUPINFOW si {}; + PROCESS_INFORMATION pi; + HANDLE childStdoutRead {nullptr}; + HANDLE childStdoutWrite {nullptr}; + SECURITY_ATTRIBUTES saAttr {}; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = true; + saAttr.lpSecurityDescriptor = nullptr; + + CreatePipe(&childStdoutRead, &childStdoutWrite, &saAttr, 0); + SetHandleInformation(childStdoutRead, HANDLE_FLAG_INHERIT, 0); + + si.cb = sizeof(STARTUPINFOW); + si.hStdOutput = childStdoutWrite; + si.dwFlags |= STARTF_USESTDHANDLES; + + bool processReturnValue {true}; + + // clang-format off + processReturnValue = CreateProcessW( + nullptr, // No application name (use command line). + const_cast(command.c_str()), // Command line. + nullptr, // Process attributes. + nullptr, // Thread attributes. + TRUE, // Handles inheritance. + 0, // Creation flags. + nullptr, // Use parent's environment block. + nullptr, // Starting directory, possibly the same as parent. + &si, // Pointer to the STARTUPINFOW structure. + &pi); // Pointer to the PROCESS_INFORMATION structure. + // clang-format on + + if (!processReturnValue) { + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return; + } + + // Close process and thread handles. + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + CloseHandle(childStdoutWrite); + + std::array buffer {}; + DWORD dwRead; + bool readValue {true}; + + while (readValue) { + readValue = ReadFile(childStdoutRead, &buffer[0], 512, &dwRead, nullptr); + if (readValue) { + mPages[pageNum].imageData.insert(mPages[pageNum].imageData.end(), + std::make_move_iterator(buffer.begin()), + std::make_move_iterator(buffer.end())); + } + } + + CloseHandle(childStdoutRead); + WaitForSingleObject(pi.hThread, INFINITE); + WaitForSingleObject(pi.hProcess, INFINITE); +#else FILE* commandPipe; std::array buffer {}; - std::string imageData; int returnValue; if (!(commandPipe = reinterpret_cast(popen(command.c_str(), "r")))) { @@ -182,10 +351,15 @@ void PDFViewer::convertPage(int pageNum) } returnValue = pclose(commandPipe); - size_t imageDataSize {mPages[pageNum].imageData.size()}; - +#endif + const size_t imageDataSize {mPages[pageNum].imageData.size()}; +#if defined(_WIN64) + if (!processReturnValue || (static_cast(imageDataSize) < + mPages[pageNum].width * mPages[pageNum].height * 4)) { +#else if (returnValue != 0 || (static_cast(imageDataSize) < mPages[pageNum].width * mPages[pageNum].height * 4)) { +#endif LOG(LogError) << "Error reading PDF file"; mPages[pageNum].imageData.clear(); return; diff --git a/es-core/src/utils/PlatformUtil.cpp b/es-core/src/utils/PlatformUtil.cpp index 8535deabc..4af714ceb 100644 --- a/es-core/src/utils/PlatformUtil.cpp +++ b/es-core/src/utils/PlatformUtil.cpp @@ -162,8 +162,8 @@ namespace Utils si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; } - bool processReturnValue = true; - DWORD errorCode = 0; + bool processReturnValue {true}; + DWORD errorCode {0}; std::wstring startDirectoryTemp {startDirectory}; wchar_t* startDir {startDirectory == L"" ? nullptr : &startDirectoryTemp[0]}; @@ -206,7 +206,7 @@ namespace Utils // If the return value is false, then something failed. if (!processReturnValue) { - LPWSTR pBuffer = nullptr; + LPWSTR pBuffer {nullptr}; FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ALLOCATE_BUFFER, nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), @@ -214,7 +214,7 @@ namespace Utils errorCode = GetLastError(); - std::string errorMessage = Utils::String::wideStringToString(pBuffer); + std::string errorMessage {Utils::String::wideStringToString(pBuffer)}; // Remove trailing newline from the error message. if (errorMessage.size()) { if (errorMessage.back() == '\n') diff --git a/es-pdf-converter/CMakeLists.txt b/es-pdf-converter/CMakeLists.txt index 53eb0eb5e..ce0b6425a 100644 --- a/es-pdf-converter/CMakeLists.txt +++ b/es-pdf-converter/CMakeLists.txt @@ -8,9 +8,19 @@ project(es-pdf-convert) -find_package(Poppler REQUIRED COMPONENTS cpp) +if(WIN32) + set(POPPLER_CPP_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../external/poppler-23.05.0/Library/include/poppler/cpp) + set(POPPLER_CPP_LIBRARY ${CMAKE_CURRENT_SOURCE_DIR}/poppler-cpp.lib) + else() + find_package(Poppler REQUIRED COMPONENTS cpp) +endif() 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) + +if(WIN32) + set_target_properties(es-pdf-convert PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/es-pdf-converter" INSTALL_RPATH_USE_LINK_PATH TRUE) +else() + set_target_properties(es-pdf-convert PROPERTIES INSTALL_RPATH_USE_LINK_PATH TRUE) +endif() diff --git a/es-pdf-converter/src/main.cpp b/es-pdf-converter/src/main.cpp index e5a68ebf7..e6fcec3cd 100644 --- a/es-pdf-converter/src/main.cpp +++ b/es-pdf-converter/src/main.cpp @@ -3,7 +3,7 @@ // EmulationStation Desktop Edition (ES-DE) PDF converter // main.cpp // -// Converts PDF document pages to raw ARGB32 image data for maximum performance. +// Converts PDF document pages to raw ARGB32 pixel 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. // @@ -17,8 +17,55 @@ #include "poppler-page.h" #include +#include #include +#if defined(_WIN64) +#include +#include +#include + +int wmain(int argc, wchar_t* argv[]) +{ + HANDLE stdoutHandle {GetStdHandle(STD_OUTPUT_HANDLE)}; + + if (stdoutHandle == INVALID_HANDLE_VALUE) { + std::cerr << "Error: Invalid stdout handle" << std::endl; + exit(-1); + } + + // This is required as Windows is braindead and will otherwise add carriage return characters + // to the stream when it encounters newline characters, which breaks binary output. + _setmode(_fileno(stdout), O_BINARY); + + bool validArguments {true}; + std::wstring mode; + + if (argc < 3) + validArguments = false; + else + mode = argv[1]; + + if ((mode == L"-fileinfo" && argc != 3) || (mode == L"-convert" && argc != 6)) + validArguments = false; + + if (!validArguments) { + std::cout << "This binary is only intended to be executed by EmulationStation.exe (ES-DE)" + << std::endl; + exit(-1); + } + + const std::wstring path {argv[2]}; + + int pageNum {0}; + int width {0}; + int height {0}; + + if (mode == L"-convert") { + pageNum = _wtoi(argv[3]); + width = _wtoi(argv[4]); + height = _wtoi(argv[5]); +#else int main(int argc, char* argv[]) { bool validArguments {true}; @@ -47,7 +94,7 @@ int main(int argc, char* argv[]) pageNum = atoi(argv[3]); width = atoi(argv[4]); height = atoi(argv[5]); - +#endif if (width < 1 || width > 7680) { std::cerr << "Invalid horizontal resolution defined: " << argv[3] << std::endl; exit(-1); @@ -62,7 +109,23 @@ int main(int argc, char* argv[]) // << width << "x" << height << " pixels" << std::endl; } - const poppler::document* document {poppler::document::load_from_file(path)}; + std::ifstream file; + + file.open(path, std::ifstream::binary); + if (file.fail()) { + std::cerr << "Error: Couldn't open PDF file, permission problems?" << std::endl; + exit(-1); + } + + file.seekg(0, std::ios::end); + const long fileLength {static_cast(file.tellg())}; + file.seekg(0, std::ios::beg); + std::vector fileData(fileLength); + file.read(&fileData[0], fileLength); + file.close(); + + const poppler::document* document { + poppler::document::load_from_raw_data(&fileData[0], fileLength)}; if (document == nullptr) { std::cerr << "Error: Couldn't open document, invalid PDF file?" << std::endl; @@ -70,8 +133,11 @@ int main(int argc, char* argv[]) } const int pageCount {document->pages()}; - +#if defined(_WIN64) + if (mode == L"-fileinfo") { +#else if (mode == "-fileinfo") { +#endif std::vector pageInfo; for (int i {0}; i < pageCount; ++i) { std::string pageRow;