1//========================================================================
2//
3// cairo-thread-test.cc
4//
5// This file is licensed under the GPLv2 or later
6//
7// Copyright (C) 2022 Adrian Johnson <ajohnson@redneon.com>
8//
9//========================================================================
10
11#include "config.h"
12#include <poppler-config.h>
13#include <condition_variable>
14#include <cmath>
15#include <cstdio>
16#include <mutex>
17#include <queue>
18#include <thread>
19#include <vector>
20
21#include "goo/GooString.h"
22#include "CairoOutputDev.h"
23#include "CairoFontEngine.h"
24#include "GlobalParams.h"
25#include "PDFDoc.h"
26#include "PDFDocFactory.h"
27#include "../utils/numberofcharacters.h"
28
29#include <cairo.h>
30#include <cairo-pdf.h>
31#include <cairo-ps.h>
32#include <cairo-svg.h>
33
34static const int renderResolution = 150;
35
36enum OutputType
37{
38 png,
39 pdf,
40 ps,
41 svg
42};
43
44// Lazy creation of PDFDoc
45class Document
46{
47public:
48 explicit Document(const std::string &filenameA) : filename(filenameA) { std::call_once(once&: ftLibOnceFlag, f&: FT_Init_FreeType, args: &ftLib); }
49
50 std::shared_ptr<PDFDoc> getDoc()
51 {
52 std::call_once(once&: docOnceFlag, f: &Document::openDocument, args: this);
53 return doc;
54 }
55
56 const std::string &getFilename() { return filename; }
57 CairoFontEngine *getFontEngine() { return fontEngine.get(); }
58
59private:
60 void openDocument()
61 {
62 doc = PDFDocFactory().createPDFDoc(uri: GooString(filename));
63 if (!doc->isOk()) {
64 fprintf(stderr, format: "Error opening PDF file %s\n", filename.c_str());
65 exit(status: 1);
66 }
67 fontEngine = std::make_unique<CairoFontEngine>(args&: ftLib);
68 }
69
70 std::string filename;
71 std::shared_ptr<PDFDoc> doc;
72 std::once_flag docOnceFlag;
73 std::unique_ptr<CairoFontEngine> fontEngine;
74
75 static FT_Library ftLib;
76 static std::once_flag ftLibOnceFlag;
77};
78
79FT_Library Document::ftLib;
80std::once_flag Document::ftLibOnceFlag;
81
82struct Job
83{
84 Job(OutputType typeA, const std::shared_ptr<Document> &documentA, int pageNumA, const std::string &outputFileA) : type(typeA), document(documentA), pageNum(pageNumA), outputFile(outputFileA) { }
85 OutputType type;
86 std::shared_ptr<Document> document;
87 int pageNum;
88 std::string outputFile;
89};
90
91class JobQueue
92{
93public:
94 JobQueue() : shutdownFlag(false) { }
95
96 void pushJob(std::unique_ptr<Job> &job)
97 {
98 std::scoped_lock lock { mutex };
99 queue.push_back(x: std::move(job));
100 condition.notify_one();
101 }
102
103 // Wait for job. If shutdownFlag true, will return null if queue empty.
104 std::unique_ptr<Job> popJob()
105 {
106 std::unique_lock<std::mutex> lock(mutex);
107 condition.wait(lock&: lock, p: [this] { return !queue.empty() || shutdownFlag; });
108 std::unique_ptr<Job> job;
109 if (!queue.empty()) {
110 job = std::move(queue.front());
111 queue.pop_front();
112 } else {
113 condition.notify_all(); // notify waitUntilEmpty()
114 }
115 return job;
116 }
117
118 // When called, popJob() will not block on an empty queue instead returning nullptr
119 void shutdown()
120 {
121 shutdownFlag = true;
122 condition.notify_all();
123 }
124
125 // wait until queue is empty
126 void waitUntilEmpty()
127 {
128 std::unique_lock<std::mutex> lock(mutex);
129 condition.wait(lock&: lock, p: [this] { return queue.empty(); });
130 }
131
132private:
133 std::deque<std::unique_ptr<Job>> queue;
134 std::mutex mutex;
135 std::condition_variable condition;
136 bool shutdownFlag;
137};
138
139static cairo_status_t writeStream(void *closure, const unsigned char *data, unsigned int length)
140{
141 FILE *file = (FILE *)closure;
142
143 if (fwrite(ptr: data, size: length, n: 1, s: file) == 1) {
144 return CAIRO_STATUS_SUCCESS;
145 } else {
146 return CAIRO_STATUS_WRITE_ERROR;
147 }
148}
149
150// PDF/PS/SVG output
151static void renderDocument(const Job &job)
152{
153 FILE *f = openFile(path: job.outputFile.c_str(), mode: "wb");
154 if (!f) {
155 fprintf(stderr, format: "Error opening output file %s\n", job.outputFile.c_str());
156 exit(status: 1);
157 }
158
159 cairo_surface_t *surface = nullptr;
160
161 switch (job.type) {
162 case OutputType::pdf:
163 surface = cairo_pdf_surface_create_for_stream(write_func: writeStream, closure: f, width_in_points: 1, height_in_points: 1);
164 break;
165 case OutputType::ps:
166 surface = cairo_ps_surface_create_for_stream(write_func: writeStream, closure: f, width_in_points: 1, height_in_points: 1);
167 break;
168 case OutputType::svg:
169 surface = cairo_svg_surface_create_for_stream(write_func: writeStream, closure: f, width_in_points: 1, height_in_points: 1);
170 break;
171 case OutputType::png:
172 break;
173 }
174
175 cairo_surface_set_fallback_resolution(surface, x_pixels_per_inch: renderResolution, y_pixels_per_inch: renderResolution);
176
177 std::unique_ptr<CairoOutputDev> cairoOut = std::make_unique<CairoOutputDev>();
178
179 cairoOut->startDoc(docA: job.document->getDoc().get(), fontEngine: job.document->getFontEngine());
180
181 cairo_status_t status;
182 for (int pageNum = 1; pageNum <= job.document->getDoc()->getNumPages(); pageNum++) {
183 double width = job.document->getDoc()->getPageMediaWidth(page: pageNum);
184 double height = job.document->getDoc()->getPageMediaHeight(page: pageNum);
185
186 if (job.type == OutputType::pdf) {
187 cairo_pdf_surface_set_size(surface, width_in_points: width, height_in_points: height);
188 } else if (job.type == OutputType::ps) {
189 cairo_ps_surface_set_size(surface, width_in_points: width, height_in_points: height);
190 }
191
192 cairo_t *cr = cairo_create(target: surface);
193
194 cairoOut->setCairo(cr);
195 cairoOut->setPrinting(true);
196
197 cairo_save(cr);
198 job.document->getDoc()->displayPageSlice(out: cairoOut.get(), page: pageNum, hDPI: 72.0, vDPI: 72.0, rotate: 0, /* rotate */
199 useMediaBox: true, /* useMediaBox */
200 crop: false, /* Crop */
201 printing: true /*printing*/, sliceX: -1, sliceY: -1, sliceW: -1, sliceH: -1);
202 cairo_restore(cr);
203 cairoOut->setCairo(nullptr);
204
205 status = cairo_status(cr);
206 if (status) {
207 fprintf(stderr, format: "cairo error: %s\n", cairo_status_to_string(status));
208 }
209 cairo_destroy(cr);
210 }
211
212 cairo_surface_finish(surface);
213 status = cairo_surface_status(surface);
214 if (status) {
215 fprintf(stderr, format: "cairo error: %s\n", cairo_status_to_string(status));
216 }
217 cairo_surface_destroy(surface);
218 fclose(stream: f);
219}
220
221// PNG page output
222static void renderPage(const Job &job)
223{
224 double width = job.document->getDoc()->getPageMediaWidth(page: job.pageNum);
225 double height = job.document->getDoc()->getPageMediaHeight(page: job.pageNum);
226
227 // convert from points to pixels
228 width *= renderResolution / 72.0;
229 height *= renderResolution / 72.0;
230
231 cairo_surface_t *surface = cairo_image_surface_create(format: CAIRO_FORMAT_ARGB32, width: static_cast<int>(ceil(x: width)), height: static_cast<int>(ceil(x: height)));
232
233 std::unique_ptr<CairoOutputDev> cairoOut = std::make_unique<CairoOutputDev>();
234
235 cairoOut->startDoc(docA: job.document->getDoc().get(), fontEngine: job.document->getFontEngine());
236 cairo_t *cr = cairo_create(target: surface);
237 cairo_status_t status;
238
239 cairoOut->setCairo(cr);
240 cairoOut->setPrinting(false);
241
242 cairo_save(cr);
243 cairo_scale(cr, sx: renderResolution / 72.0, sy: renderResolution / 72.0);
244
245 job.document->getDoc()->displayPageSlice(out: cairoOut.get(), page: job.pageNum, hDPI: 72.0, vDPI: 72.0, rotate: 0, /* rotate */
246 useMediaBox: true, /* useMediaBox */
247 crop: false, /* Crop */
248 printing: false /*printing */, sliceX: -1, sliceY: -1, sliceW: -1, sliceH: -1);
249 cairo_restore(cr);
250
251 cairoOut->setCairo(nullptr);
252
253 // Blend onto white page
254 cairo_save(cr);
255 cairo_set_operator(cr, op: CAIRO_OPERATOR_DEST_OVER);
256 cairo_set_source_rgb(cr, red: 1, green: 1, blue: 1);
257 cairo_paint(cr);
258 cairo_restore(cr);
259
260 status = cairo_status(cr);
261 if (status) {
262 fprintf(stderr, format: "cairo error: %s\n", cairo_status_to_string(status));
263 }
264 cairo_destroy(cr);
265
266 FILE *f = openFile(path: job.outputFile.c_str(), mode: "wb");
267 if (!f) {
268 fprintf(stderr, format: "Error opening output file %s\n", job.outputFile.c_str());
269 exit(status: 1);
270 }
271 cairo_surface_write_to_png_stream(surface, write_func: writeStream, closure: f);
272 fclose(stream: f);
273
274 cairo_surface_finish(surface);
275 status = cairo_surface_status(surface);
276 if (status) {
277 fprintf(stderr, format: "cairo error: %s\n", cairo_status_to_string(status));
278 }
279 cairo_surface_destroy(surface);
280}
281
282static void runThread(const std::shared_ptr<JobQueue> &jobQueue)
283{
284 while (true) {
285 std::unique_ptr<Job> job = jobQueue->popJob();
286 if (!job) {
287 break;
288 }
289 switch (job->type) {
290 case OutputType::png:
291 renderPage(job: *job);
292 break;
293 case OutputType::pdf:
294 case OutputType::ps:
295 case OutputType::svg:
296 renderDocument(job: *job);
297 break;
298 }
299 }
300}
301
302static void printUsage()
303{
304 int default_threads = std::max(a: 1, b: (int)std::thread::hardware_concurrency());
305 printf(format: "cairo-thread-test [-j jobs] [-p priority] [<output option> <files>...]...\n");
306 printf(format: " -j num number of concurrent threads (default %d)\n", default_threads);
307 printf(format: " -p <priority> priority is one of:\n");
308 printf(format: " page one page at a time will be queued from each document in round-robin fashion (default).\n");
309 printf(format: " document all pages in the first document will be queued before processing to the next document.\n");
310 printf(format: " Note: documents with vector output will be handled in one job. They can not be parallelized.\n");
311 printf(format: " <output option> is one of -png, -pdf, -ps, -svg\n");
312 printf(format: " The output option will apply to all documents after the option until a different option is specified\n");
313}
314
315// Parse -j and -p options. These must appear before any other arguments
316static bool getThreadsAndPriority(int &argc, char **&argv, int &numThreads, bool &documentPriority)
317{
318 numThreads = std::max(a: 1, b: (int)std::thread::hardware_concurrency());
319 documentPriority = false;
320
321 while (argc > 0) {
322 std::string arg(*argv);
323 if (arg == "-j") {
324 argc--;
325 argv++;
326 if (argc == 0) {
327 return false;
328 }
329 numThreads = atoi(nptr: *argv);
330 if (numThreads == 0) {
331 return false;
332 }
333 argc--;
334 argv++;
335 } else if (arg == "-p") {
336 argc--;
337 argv++;
338 if (argc == 0) {
339 return false;
340 }
341 arg = *argv;
342 if (arg == "document") {
343 documentPriority = true;
344 } else if (arg == "page") {
345 documentPriority = false;
346
347 } else {
348 return false;
349 }
350 argc--;
351 argv++;
352 } else {
353 // file or output option
354 break;
355 }
356 }
357 return true;
358}
359
360// eg "-png doc1.pdf -ps doc2.pdf doc3.pdf -png doc4.pdf"
361static bool getOutputTypeAndDocument(int &argc, char **&argv, OutputType &outputType, std::string &filename)
362{
363 static OutputType type;
364 static bool typeInitialized = false;
365
366 while (argc > 0) {
367 std::string arg(*argv);
368 if (arg == "-png") {
369 argc--;
370 argv++;
371 type = OutputType::png;
372 typeInitialized = true;
373 } else if (arg == "-pdf") {
374 argc--;
375 argv++;
376 type = OutputType::pdf;
377 typeInitialized = true;
378 } else if (arg == "-ps") {
379 argc--;
380 argv++;
381 type = OutputType::ps;
382 typeInitialized = true;
383 } else if (arg == "-svg") {
384 argc--;
385 argv++;
386 type = OutputType::svg;
387 typeInitialized = true;
388 } else {
389 // filename
390 if (!typeInitialized) {
391 return false;
392 }
393 outputType = type;
394 filename = *argv;
395 argc--;
396 argv++;
397 return true;
398 }
399 }
400 return false;
401}
402
403// "../a/b/foo.pdf" => "foo"
404static std::string getBaseName(const std::string &filename)
405{
406 // strip everything up to last '/'
407 size_t slash_pos = filename.find_last_of(c: '/');
408 std::string basename;
409 if (slash_pos != std::string::npos) {
410 basename = filename.substr(pos: slash_pos + 1, n: std::string::npos);
411 } else {
412 basename = filename;
413 }
414
415 // remove .pdf extension
416 size_t dot_pos = basename.find_last_of(c: '.');
417 if (dot_pos != std::string::npos) {
418 if (basename.compare(pos: dot_pos, n1: std::string::npos, s: ".pdf") == 0) {
419 basename.erase(pos: dot_pos);
420 }
421 }
422 return basename;
423}
424
425// Represents an input file on the command line
426struct InputFile
427{
428 InputFile(const std::string &filename, OutputType typeA) : type(typeA)
429 {
430 document = std::make_shared<Document>(args: filename);
431 basename = getBaseName(filename);
432 currentPage = 0;
433 numPages = 0; // filled in later
434 numDigits = 0; // filled in later
435 }
436 std::shared_ptr<Document> document;
437 OutputType type;
438
439 // Used when creating jobs for this InputFile
440 int currentPage;
441 std::string basename;
442 int numPages;
443 int numDigits;
444};
445
446// eg "basename.out-123.png" or "basename.out.pdf"
447static std::string getOutputName(const InputFile &input)
448{
449 std::string output;
450 char buf[30];
451 switch (input.type) {
452 case OutputType::png:
453 std::snprintf(s: buf, maxlen: sizeof(buf), format: ".out-%0*d.png", input.numDigits, input.currentPage);
454 output = input.basename + buf;
455 break;
456 case OutputType::pdf:
457 output = input.basename + ".out.pdf";
458 break;
459 case OutputType::ps:
460 output = input.basename + ".out.ps";
461 break;
462 case OutputType::svg:
463 output = input.basename + ".out.svg";
464 break;
465 }
466 return output;
467}
468
469int main(int argc, char *argv[])
470{
471 if (argc < 3) {
472 printUsage();
473 exit(status: 1);
474 }
475
476 // skip program name
477 argc--;
478 argv++;
479
480 int numThreads;
481 bool documentPriority;
482 if (!getThreadsAndPriority(argc, argv, numThreads, documentPriority)) {
483 printUsage();
484 exit(status: 1);
485 }
486
487 globalParams = std::make_unique<GlobalParams>();
488
489 std::shared_ptr<JobQueue> jobQueue = std::make_shared<JobQueue>();
490 std::vector<std::thread> threads;
491 threads.reserve(n: 4);
492 for (int i = 0; i < numThreads; i++) {
493 threads.emplace_back(args&: runThread, args&: jobQueue);
494 }
495
496 std::vector<InputFile> inputFiles;
497
498 while (argc > 0) {
499 std::string filename;
500 OutputType type;
501 if (!getOutputTypeAndDocument(argc, argv, outputType&: type, filename)) {
502 printUsage();
503 exit(status: 1);
504 }
505 InputFile input(filename, type);
506 inputFiles.push_back(x: input);
507 }
508
509 if (documentPriority) {
510 while (true) {
511 bool jobAdded = false;
512 for (auto &input : inputFiles) {
513 if (input.numPages == 0) {
514 // first time seen
515 if (input.type == OutputType::png) {
516 input.numPages = input.document->getDoc()->getNumPages();
517 input.numDigits = numberOfCharacters(n: input.numPages);
518 } else {
519 input.numPages = 1; // Use 1 for vector output as there is only one output file
520 }
521 }
522 if (input.currentPage < input.numPages) {
523 input.currentPage++;
524 std::string output = getOutputName(input);
525 std::unique_ptr<Job> job = std::make_unique<Job>(args&: input.type, args&: input.document, args&: input.currentPage, args&: output);
526 jobQueue->pushJob(job);
527 jobAdded = true;
528 }
529 }
530 if (!jobAdded) {
531 break;
532 }
533 }
534 } else {
535 for (auto &input : inputFiles) {
536 if (input.type == OutputType::png) {
537 input.numPages = input.document->getDoc()->getNumPages();
538 input.numDigits = numberOfCharacters(n: input.numPages);
539 for (int i = 1; i <= input.numPages; i++) {
540 input.currentPage = i;
541 std::string output = getOutputName(input);
542 std::unique_ptr<Job> job = std::make_unique<Job>(args&: input.type, args&: input.document, args&: input.currentPage, args&: output);
543 jobQueue->pushJob(job);
544 }
545 } else {
546 std::string output = getOutputName(input);
547 std::unique_ptr<Job> job = std::make_unique<Job>(args&: input.type, args&: input.document, args: 1, args&: output);
548 jobQueue->pushJob(job);
549 }
550 }
551 }
552
553 jobQueue->shutdown();
554 jobQueue->waitUntilEmpty();
555
556 for (int i = 0; i < numThreads; i++) {
557 threads[i].join();
558 }
559
560 return 0;
561}
562

source code of poppler/test/cairo-thread-test.cc