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 | |
34 | static const int renderResolution = 150; |
35 | |
36 | enum OutputType |
37 | { |
38 | png, |
39 | pdf, |
40 | ps, |
41 | svg |
42 | }; |
43 | |
44 | // Lazy creation of PDFDoc |
45 | class Document |
46 | { |
47 | public: |
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 | |
59 | private: |
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 | |
79 | FT_Library Document::ftLib; |
80 | std::once_flag Document::ftLibOnceFlag; |
81 | |
82 | struct 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 | |
91 | class JobQueue |
92 | { |
93 | public: |
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 | |
132 | private: |
133 | std::deque<std::unique_ptr<Job>> queue; |
134 | std::mutex mutex; |
135 | std::condition_variable condition; |
136 | bool shutdownFlag; |
137 | }; |
138 | |
139 | static 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 |
151 | static 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 |
222 | static void (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 | |
282 | static 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 | |
302 | static 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 |
316 | static 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" |
361 | static 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" |
404 | static 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 |
426 | struct 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" |
447 | static 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 | |
469 | int 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 | |