| //======================================================================== |
| // |
| // cairo-thread-test.cc |
| // |
| // This file is licensed under the GPLv2 or later |
| // |
| // Copyright (C) 2022, 2024 Adrian Johnson <ajohnson@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(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(); |
| } |
| |
| #ifndef NDEBUG |
| // Clear the cairo font cache. If all references to font faces or |
| // scaled fonts have not been released this function will |
| // assert. If this occurs we have found a memory leak. |
| cairo_debug_reset_static_data(); |
| #endif |
| |
| return 0; |
| } |