//======================================================================== // // cairo-thread-test.cc // // This file is licensed under the GPLv2 or later // // Copyright (C) 2022, 2024 Adrian Johnson // //======================================================================== #include "config.h" #include #include #include #include #include #include #include #include #include "goo/GooString.h" #include "CairoOutputDev.h" #include "CairoFontEngine.h" #include "GlobalParams.h" #include "PDFDoc.h" #include "PDFDocFactory.h" #include "../utils/numberofcharacters.h" #include #include #include #include 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 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(ftLib); } std::string filename; std::shared_ptr doc; std::once_flag docOnceFlag; std::unique_ptr 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 &documentA, int pageNumA, const std::string &outputFileA) : type(typeA), document(documentA), pageNum(pageNumA), outputFile(outputFileA) { } OutputType type; std::shared_ptr document; int pageNum; std::string outputFile; }; class JobQueue { public: JobQueue() : shutdownFlag(false) { } void pushJob(std::unique_ptr &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 popJob() { std::unique_lock lock(mutex); condition.wait(lock, [this] { return !queue.empty() || shutdownFlag; }); std::unique_ptr 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 lock(mutex); condition.wait(lock, [this] { return queue.empty(); }); } private: std::deque> 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 cairoOut = std::make_unique(); 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(ceil(width)), static_cast(ceil(height))); std::unique_ptr cairoOut = std::make_unique(); 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) { while (true) { std::unique_ptr 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] [ ...]...\n"); printf(" -j num number of concurrent threads (default %d)\n", default_threads); printf(" -p 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(" 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(filename); basename = getBaseName(filename); currentPage = 0; numPages = 0; // filled in later numDigits = 0; // filled in later } std::shared_ptr 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(); std::shared_ptr jobQueue = std::make_shared(); std::vector threads; threads.reserve(4); for (int i = 0; i < numThreads; i++) { threads.emplace_back(runThread, jobQueue); } std::vector 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 = std::make_unique(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 = std::make_unique(input.type, input.document, input.currentPage, output); jobQueue->pushJob(job); } } else { std::string output = getOutputName(input); std::unique_ptr job = std::make_unique(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; }