test/CMakeLists.txt | 21 + test/cairo-thread-test.cc | 561 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 582 insertions(+)
New commits: commit a8ffdc092dff15a7729d4ec2e5a1476dc1030172 Author: Adrian Johnson <ajohn...@redneon.com> Date: Tue May 24 19:12:48 2022 +0930 Add cairo thread test diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9de32ec5..258ef20c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -44,6 +44,27 @@ if (GTK_FOUND) endif () +if (HAVE_CAIRO) + include(CheckCXXSymbolExists) + set (CMAKE_REQUIRED_INCLUDES ${CAIRO_INCLUDE_DIRS}) + check_cxx_symbol_exists(CAIRO_HAS_PNG_FUNCTIONS "cairo.h" HAVE_CAIRO_PNG) + check_cxx_symbol_exists(CAIRO_HAS_PDF_SURFACE "cairo.h" HAVE_CAIRO_PDF) + check_cxx_symbol_exists(CAIRO_HAS_PS_SURFACE "cairo.h" HAVE_CAIRO_PS) + check_cxx_symbol_exists(CAIRO_HAS_SVG_SURFACE "cairo.h" HAVE_CAIRO_SVG) + + if (HAVE_CAIRO_PNG AND HAVE_CAIRO_PDF AND HAVE_CAIRO_PS AND HAVE_CAIRO_SVG) + find_package(Threads) + set(cairo_thread_test_SRCS + cairo-thread-test.cc + ${CMAKE_SOURCE_DIR}/poppler/CairoFontEngine.cc + ${CMAKE_SOURCE_DIR}/poppler/CairoOutputDev.cc + ${CMAKE_SOURCE_DIR}/poppler/CairoRescaleBox.cc + ) + add_executable(cairo-thread-test ${cairo_thread_test_SRCS}) + target_link_libraries(cairo-thread-test ${CAIRO_LIBRARIES} Freetype::Freetype Threads::Threads poppler) + endif () +endif () + set (pdf_fullrewrite_SRCS pdf-fullrewrite.cc ../utils/parseargs.cc diff --git a/test/cairo-thread-test.cc b/test/cairo-thread-test.cc new file mode 100644 index 00000000..46e08ed3 --- /dev/null +++ b/test/cairo-thread-test.cc @@ -0,0 +1,561 @@ +//======================================================================== +// +// cairo-thread-test.cc +// +// This file is licensed under the GPLv2 or later +// +// Copyright (C) 2022 Adrian Johnson <ajohn...@redneon.com> +// +//======================================================================== + +#include "config.h" +#include <poppler-config.h> +#include <condition_variable> +#include <cmath> +#include <cstdio> +#include <mutex> +#include <queue> +#include <thread> +#include <vector> + +#include "goo/GooString.h" +#include "CairoOutputDev.h" +#include "CairoFontEngine.h" +#include "GlobalParams.h" +#include "PDFDoc.h" +#include "PDFDocFactory.h" +#include "../utils/numberofcharacters.h" + +#include <cairo.h> +#include <cairo-pdf.h> +#include <cairo-ps.h> +#include <cairo-svg.h> + +static const int renderResolution = 150; + +enum OutputType +{ + png, + pdf, + ps, + svg +}; + +// Lazy creation of PDFDoc +class Document +{ +public: + explicit Document(const std::string &filenameA) : filename(filenameA) { std::call_once(ftLibOnceFlag, FT_Init_FreeType, &ftLib); } + + std::shared_ptr<PDFDoc> getDoc() + { + std::call_once(docOnceFlag, &Document::openDocument, this); + return doc; + } + + const std::string &getFilename() { return filename; } + CairoFontEngine *getFontEngine() { return fontEngine.get(); } + +private: + void openDocument() + { + doc = PDFDocFactory().createPDFDoc(GooString(filename)); + if (!doc->isOk()) { + fprintf(stderr, "Error opening PDF file %s\n", filename.c_str()); + exit(1); + } + fontEngine = std::make_unique<CairoFontEngine>(ftLib); + } + + std::string filename; + std::shared_ptr<PDFDoc> doc; + std::once_flag docOnceFlag; + std::unique_ptr<CairoFontEngine> fontEngine; + + static FT_Library ftLib; + static std::once_flag ftLibOnceFlag; +}; + +FT_Library Document::ftLib; +std::once_flag Document::ftLibOnceFlag; + +struct Job +{ + Job(OutputType typeA, const std::shared_ptr<Document> &documentA, int pageNumA, const std::string &outputFileA) : type(typeA), document(documentA), pageNum(pageNumA), outputFile(outputFileA) { } + OutputType type; + std::shared_ptr<Document> document; + int pageNum; + std::string outputFile; +}; + +class JobQueue +{ +public: + JobQueue() : shutdownFlag(false) { } + + void pushJob(std::unique_ptr<Job> &job) + { + std::scoped_lock lock { mutex }; + queue.push_back(std::move(job)); + condition.notify_one(); + } + + // Wait for job. If shutdownFlag true, will return null if queue empty. + std::unique_ptr<Job> popJob() + { + std::unique_lock<std::mutex> lock(mutex); + condition.wait(lock, [this] { return !queue.empty() || shutdownFlag; }); + std::unique_ptr<Job> job; + if (!queue.empty()) { + job = std::move(queue.front()); + queue.pop_front(); + } else { + condition.notify_all(); // notify waitUntilEmpty() + } + return job; + } + + // When called, popJob() will not block on an empty queue instead returning nullptr + void shutdown() + { + shutdownFlag = true; + condition.notify_all(); + } + + // wait until queue is empty + void waitUntilEmpty() + { + std::unique_lock<std::mutex> lock(mutex); + condition.wait(lock, [this] { return queue.empty(); }); + } + +private: + std::deque<std::unique_ptr<Job>> queue; + std::mutex mutex; + std::condition_variable condition; + bool shutdownFlag; +}; + +static cairo_status_t writeStream(void *closure, const unsigned char *data, unsigned int length) +{ + FILE *file = (FILE *)closure; + + if (fwrite(data, length, 1, file) == 1) { + return CAIRO_STATUS_SUCCESS; + } else { + return CAIRO_STATUS_WRITE_ERROR; + } +} + +// PDF/PS/SVG output +static void renderDocument(const Job &job) +{ + FILE *f = openFile(job.outputFile.c_str(), "wb"); + if (!f) { + fprintf(stderr, "Error opening output file %s\n", job.outputFile.c_str()); + exit(1); + } + + cairo_surface_t *surface = nullptr; + + switch (job.type) { + case OutputType::pdf: + surface = cairo_pdf_surface_create_for_stream(writeStream, f, 1, 1); + break; + case OutputType::ps: + surface = cairo_ps_surface_create_for_stream(writeStream, f, 1, 1); + break; + case OutputType::svg: + surface = cairo_svg_surface_create_for_stream(writeStream, f, 1, 1); + break; + case OutputType::png: + break; + } + + cairo_surface_set_fallback_resolution(surface, renderResolution, renderResolution); + + std::unique_ptr<CairoOutputDev> cairoOut = std::make_unique<CairoOutputDev>(); + + cairoOut->startDoc(job.document->getDoc().get(), job.document->getFontEngine()); + + cairo_status_t status; + for (int pageNum = 1; pageNum <= job.document->getDoc()->getNumPages(); pageNum++) { + double width = job.document->getDoc()->getPageMediaWidth(pageNum); + double height = job.document->getDoc()->getPageMediaHeight(pageNum); + + if (job.type == OutputType::pdf) { + cairo_pdf_surface_set_size(surface, width, height); + } else if (job.type == OutputType::ps) { + cairo_ps_surface_set_size(surface, width, height); + } + + cairo_t *cr = cairo_create(surface); + + cairoOut->setCairo(cr); + cairoOut->setPrinting(true); + + cairo_save(cr); + job.document->getDoc()->displayPageSlice(cairoOut.get(), pageNum, 72.0, 72.0, 0, /* rotate */ + true, /* useMediaBox */ + false, /* Crop */ + true /*printing*/, -1, -1, -1, -1); + cairo_restore(cr); + cairoOut->setCairo(nullptr); + + status = cairo_status(cr); + if (status) { + fprintf(stderr, "cairo error: %s\n", cairo_status_to_string(status)); + } + cairo_destroy(cr); + } + + cairo_surface_finish(surface); + status = cairo_surface_status(surface); + if (status) { + fprintf(stderr, "cairo error: %s\n", cairo_status_to_string(status)); + } + cairo_surface_destroy(surface); + fclose(f); +} + +// PNG page output +static void renderPage(const Job &job) +{ + double width = job.document->getDoc()->getPageMediaWidth(job.pageNum); + double height = job.document->getDoc()->getPageMediaHeight(job.pageNum); + + // convert from points to pixels + width *= renderResolution / 72.0; + height *= renderResolution / 72.0; + + cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, static_cast<int>(ceil(width)), static_cast<int>(ceil(height))); + + std::unique_ptr<CairoOutputDev> cairoOut = std::make_unique<CairoOutputDev>(); + + cairoOut->startDoc(job.document->getDoc().get(), job.document->getFontEngine()); + cairo_t *cr = cairo_create(surface); + cairo_status_t status; + + cairoOut->setCairo(cr); + cairoOut->setPrinting(false); + + cairo_save(cr); + cairo_scale(cr, renderResolution / 72.0, renderResolution / 72.0); + + job.document->getDoc()->displayPageSlice(cairoOut.get(), job.pageNum, 72.0, 72.0, 0, /* rotate */ + true, /* useMediaBox */ + false, /* Crop */ + false /*printing */, -1, -1, -1, -1); + cairo_restore(cr); + + cairoOut->setCairo(nullptr); + + // Blend onto white page + cairo_save(cr); + cairo_set_operator(cr, CAIRO_OPERATOR_DEST_OVER); + cairo_set_source_rgb(cr, 1, 1, 1); + cairo_paint(cr); + cairo_restore(cr); + + status = cairo_status(cr); + if (status) { + fprintf(stderr, "cairo error: %s\n", cairo_status_to_string(status)); + } + cairo_destroy(cr); + + FILE *f = openFile(job.outputFile.c_str(), "wb"); + if (!f) { + fprintf(stderr, "Error opening output file %s\n", job.outputFile.c_str()); + exit(1); + } + cairo_surface_write_to_png_stream(surface, writeStream, f); + fclose(f); + + cairo_surface_finish(surface); + status = cairo_surface_status(surface); + if (status) { + fprintf(stderr, "cairo error: %s\n", cairo_status_to_string(status)); + } + cairo_surface_destroy(surface); +} + +static void runThread(const std::shared_ptr<JobQueue> &jobQueue) +{ + while (true) { + std::unique_ptr<Job> job = jobQueue->popJob(); + if (!job) { + break; + } + switch (job->type) { + case OutputType::png: + renderPage(*job); + break; + case OutputType::pdf: + case OutputType::ps: + case OutputType::svg: + renderDocument(*job); + break; + } + } +} + +static void printUsage() +{ + int default_threads = std::max(1, (int)std::thread::hardware_concurrency()); + printf("cairo-thread-test [-j jobs] [-p priority] [<output option> <files>...]...\n"); + printf(" -j num number of concurrent threads (default %d)\n", default_threads); + printf(" -p <priority> priority is one of:\n"); + printf(" page one page at a time will be queued from each document in round-robin fashion (default).\n"); + printf(" document all pages in the first document will be queued before processing to the next document.\n"); + printf(" Note: documents with vector output will be handled in one job. They can not be parallelized.\n"); + printf(" <output option> is one of -png, -pdf, -ps, -svg\n"); + printf(" The output option will apply to all documents after the option until a different option is specified\n"); +} + +// Parse -j and -p options. These must appear before any other arguments +static bool getThreadsAndPriority(int &argc, char **&argv, int &numThreads, bool &documentPriority) +{ + numThreads = std::max(1, (int)std::thread::hardware_concurrency()); + documentPriority = false; + + while (argc > 0) { + std::string arg(*argv); + if (arg == "-j") { + argc--; + argv++; + if (argc == 0) { + return false; + } + numThreads = atoi(*argv); + if (numThreads == 0) { + return false; + } + argc--; + argv++; + } else if (arg == "-p") { + argc--; + argv++; + if (argc == 0) { + return false; + } + arg = *argv; + if (arg == "document") { + documentPriority = true; + } else if (arg == "page") { + documentPriority = false; + + } else { + return false; + } + argc--; + argv++; + } else { + // file or output option + break; + } + } + return true; +} + +// eg "-png doc1.pdf -ps doc2.pdf doc3.pdf -png doc4.pdf" +static bool getOutputTypeAndDocument(int &argc, char **&argv, OutputType &outputType, std::string &filename) +{ + static OutputType type; + static bool typeInitialized = false; + + while (argc > 0) { + std::string arg(*argv); + if (arg == "-png") { + argc--; + argv++; + type = OutputType::png; + typeInitialized = true; + } else if (arg == "-pdf") { + argc--; + argv++; + type = OutputType::pdf; + typeInitialized = true; + } else if (arg == "-ps") { + argc--; + argv++; + type = OutputType::ps; + typeInitialized = true; + } else if (arg == "-svg") { + argc--; + argv++; + type = OutputType::svg; + typeInitialized = true; + } else { + // filename + if (!typeInitialized) { + return false; + } + outputType = type; + filename = *argv; + argc--; + argv++; + return true; + } + } + return false; +} + +// "../a/b/foo.pdf" => "foo" +static std::string getBaseName(const std::string &filename) +{ + // strip everything up to last '/' + size_t slash_pos = filename.find_last_of('/'); + std::string basename; + if (slash_pos != std::string::npos) { + basename = filename.substr(slash_pos + 1, std::string::npos); + } else { + basename = filename; + } + + // remove .pdf extension + size_t dot_pos = basename.find_last_of('.'); + if (dot_pos != std::string::npos) { + if (basename.compare(dot_pos, std::string::npos, ".pdf") == 0) { + basename.erase(dot_pos); + } + } + return basename; +} + +// Represents an input file on the command line +struct InputFile +{ + InputFile(const std::string &filename, OutputType typeA) : type(typeA) + { + document = std::make_shared<Document>(filename); + basename = getBaseName(filename); + currentPage = 0; + numPages = 0; // filled in later + numDigits = 0; // filled in later + } + std::shared_ptr<Document> document; + OutputType type; + + // Used when creating jobs for this InputFile + int currentPage; + std::string basename; + int numPages; + int numDigits; +}; + +// eg "basename.out-123.png" or "basename.out.pdf" +static std::string getOutputName(const InputFile &input) +{ + std::string output; + char buf[30]; + switch (input.type) { + case OutputType::png: + std::snprintf(buf, sizeof(buf), ".out-%0*d.png", input.numDigits, input.currentPage); + output = input.basename + buf; + break; + case OutputType::pdf: + output = input.basename + ".out.pdf"; + break; + case OutputType::ps: + output = input.basename + ".out.ps"; + break; + case OutputType::svg: + output = input.basename + ".out.svg"; + break; + } + return output; +} + +int main(int argc, char *argv[]) +{ + if (argc < 3) { + printUsage(); + exit(1); + } + + // skip program name + argc--; + argv++; + + int numThreads; + bool documentPriority; + if (!getThreadsAndPriority(argc, argv, numThreads, documentPriority)) { + printUsage(); + exit(1); + } + + globalParams = std::make_unique<GlobalParams>(); + + std::shared_ptr<JobQueue> jobQueue = std::make_shared<JobQueue>(); + std::vector<std::thread> threads; + threads.reserve(4); + for (int i = 0; i < numThreads; i++) { + threads.emplace_back(std::thread(runThread, jobQueue)); + } + + std::vector<InputFile> inputFiles; + + while (argc > 0) { + std::string filename; + OutputType type; + if (!getOutputTypeAndDocument(argc, argv, type, filename)) { + printUsage(); + exit(1); + } + InputFile input(filename, type); + inputFiles.push_back(input); + } + + if (documentPriority) { + while (true) { + bool jobAdded = false; + for (auto &input : inputFiles) { + if (input.numPages == 0) { + // first time seen + if (input.type == OutputType::png) { + input.numPages = input.document->getDoc()->getNumPages(); + input.numDigits = numberOfCharacters(input.numPages); + } else { + input.numPages = 1; // Use 1 for vector output as there is only one output file + } + } + if (input.currentPage < input.numPages) { + input.currentPage++; + std::string output = getOutputName(input); + std::unique_ptr<Job> job = std::make_unique<Job>(input.type, input.document, input.currentPage, output); + jobQueue->pushJob(job); + jobAdded = true; + } + } + if (!jobAdded) { + break; + } + } + } else { + for (auto &input : inputFiles) { + if (input.type == OutputType::png) { + input.numPages = input.document->getDoc()->getNumPages(); + input.numDigits = numberOfCharacters(input.numPages); + for (int i = 1; i <= input.numPages; i++) { + input.currentPage = i; + std::string output = getOutputName(input); + std::unique_ptr<Job> job = std::make_unique<Job>(input.type, input.document, input.currentPage, output); + jobQueue->pushJob(job); + } + } else { + std::string output = getOutputName(input); + std::unique_ptr<Job> job = std::make_unique<Job>(input.type, input.document, 1, output); + jobQueue->pushJob(job); + } + } + } + + jobQueue->shutdown(); + jobQueue->waitUntilEmpty(); + + for (int i = 0; i < numThreads; i++) { + threads[i].join(); + } + + return 0; +}