1 | /* Copyright Krzysztof Kowalczyk 2006-2007 |
2 | Copyright Hib Eris <hib@hiberis.nl> 2008, 2013 |
3 | Copyright 2018, 2020, 2022 Albert Astals Cid <aacid@kde.org> 2018 |
4 | Copyright 2019 Oliver Sander <oliver.sander@tu-dresden.de> |
5 | Copyright 2020 Adam Reichold <adam.reichold@t-online.de> |
6 | License: GPLv2 */ |
7 | /* |
8 | A tool to stress-test poppler rendering and measure rendering times for |
9 | very simplistic performance measuring. |
10 | |
11 | TODO: |
12 | * make it work with cairo output as well |
13 | * print more info about document like e.g. enumerate images, |
14 | streams, compression, encryption, password-protection. Each should have |
15 | a command-line arguments to turn it on/off |
16 | * never over-write file given as -out argument (optionally, provide -force |
17 | option to force writing the -out file). It's way too easy too lose results |
18 | of a previous run. |
19 | */ |
20 | |
21 | #ifdef _MSC_VER |
22 | // this sucks but I don't know any other way |
23 | # pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"") |
24 | #endif |
25 | |
26 | #include <config.h> |
27 | |
28 | #ifdef _WIN32 |
29 | # include <windows.h> |
30 | #else |
31 | # include <strings.h> |
32 | #endif |
33 | |
34 | // Define COPY_FILE if you want the file to be copied to a local disk first |
35 | // before it's tested. This is desired if a file is on a slow drive. |
36 | // Currently copying only works on Windows. |
37 | // Not enabled by default. |
38 | // #define COPY_FILE 1 |
39 | |
40 | #include <cassert> |
41 | #include <cstdio> |
42 | #include <cstdarg> |
43 | #include <cctype> |
44 | #include <cstdlib> |
45 | #include <cstring> |
46 | #include <cerrno> |
47 | #include <ctime> |
48 | |
49 | #ifdef HAVE_DIRENT_H |
50 | # include <dirent.h> |
51 | #endif |
52 | |
53 | #include "Error.h" |
54 | #include "ErrorCodes.h" |
55 | #include "goo/GooString.h" |
56 | #include "goo/GooTimer.h" |
57 | #include "GlobalParams.h" |
58 | #include "splash/SplashBitmap.h" |
59 | #include "Object.h" /* must be included before SplashOutputDev.h because of sloppiness in SplashOutputDev.h */ |
60 | #include "SplashOutputDev.h" |
61 | #include "TextOutputDev.h" |
62 | #include "PDFDoc.h" |
63 | #include "Link.h" |
64 | |
65 | #ifdef _MSC_VER |
66 | # define strdup _strdup |
67 | # define strcasecmp _stricmp |
68 | #endif |
69 | |
70 | #define dimof(X) (sizeof(X) / sizeof((X)[0])) |
71 | |
72 | #define INVALID_PAGE_NO -1 |
73 | |
74 | /* Those must be implemented in order to provide preview during execution. |
75 | They can be no-ops. An implementation for windows is in |
76 | perf-test-preview-win.cc |
77 | */ |
78 | extern void PreviewBitmapInit(); |
79 | extern void PreviewBitmapDestroy(); |
80 | extern void PreviewBitmapSplash(SplashBitmap *bmpSplash); |
81 | |
82 | class PdfEnginePoppler |
83 | { |
84 | public: |
85 | PdfEnginePoppler(); |
86 | ~PdfEnginePoppler(); |
87 | |
88 | PdfEnginePoppler(const PdfEnginePoppler &) = delete; |
89 | PdfEnginePoppler &operator=(const PdfEnginePoppler &) = delete; |
90 | |
91 | const char *fileName() const { return _fileName; }; |
92 | |
93 | void setFileName(const char *fileName) |
94 | { |
95 | assert(!_fileName); |
96 | _fileName = (char *)strdup(s: fileName); |
97 | } |
98 | |
99 | int pageCount() const { return _pageCount; } |
100 | |
101 | bool load(const char *fileName); |
102 | SplashBitmap *renderBitmap(int pageNo, double zoomReal, int rotation); |
103 | |
104 | SplashOutputDev *outputDevice(); |
105 | |
106 | private: |
107 | char *_fileName; |
108 | int _pageCount; |
109 | |
110 | PDFDoc *_pdfDoc; |
111 | SplashOutputDev *_outputDev; |
112 | }; |
113 | |
114 | typedef struct StrList |
115 | { |
116 | struct StrList *next; |
117 | char *str; |
118 | } StrList; |
119 | |
120 | /* List of all command-line arguments that are not switches. |
121 | We assume those are: |
122 | - names of PDF files |
123 | - names of a file with a list of PDF files |
124 | - names of directories with PDF files |
125 | */ |
126 | static StrList *gArgsListRoot = nullptr; |
127 | |
128 | /* Names of all command-line switches we recognize */ |
129 | #define TIMINGS_ARG "-timings" |
130 | #define RESOLUTION_ARG "-resolution" |
131 | #define RECURSIVE_ARG "-recursive" |
132 | #define OUT_ARG "-out" |
133 | #define PREVIEW_ARG "-preview" |
134 | #define SLOW_PREVIEW_ARG "-slowpreview" |
135 | #define LOAD_ONLY_ARG "-loadonly" |
136 | #define PAGE_ARG "-page" |
137 | #define TEXT_ARG "-text" |
138 | |
139 | /* Should we record timings? True if -timings command-line argument was given. */ |
140 | static bool gfTimings = false; |
141 | |
142 | /* If true, we use render each page at resolution 'gResolutionX'/'gResolutionY'. |
143 | If false, we render each page at its native resolution. |
144 | True if -resolution NxM command-line argument was given. */ |
145 | static bool gfForceResolution = false; |
146 | static int gResolutionX = 0; |
147 | static int gResolutionY = 0; |
148 | /* If NULL, we output the log info to stdout. If not NULL, should be a name |
149 | of the file to which we output log info. |
150 | Controlled by -out command-line argument. */ |
151 | static char *gOutFileName = nullptr; |
152 | /* FILE * corresponding to gOutFileName or stdout if gOutFileName is NULL or |
153 | was invalid name */ |
154 | static FILE *gOutFile = nullptr; |
155 | /* FILE * corresponding to gOutFileName or stderr if gOutFileName is NULL or |
156 | was invalid name */ |
157 | static FILE *gErrFile = nullptr; |
158 | |
159 | /* If True and a directory is given as a command-line argument, we'll process |
160 | pdf files in sub-directories as well. |
161 | Controlled by -recursive command-line argument */ |
162 | static bool gfRecursive = false; |
163 | |
164 | /* If true, preview rendered image. To make sure that they're being rendered correctly. */ |
165 | static bool gfPreview = false; |
166 | |
167 | /* 1 second (1000 milliseconds) */ |
168 | #define SLOW_PREVIEW_TIME 1000 |
169 | |
170 | /* If true, preview rendered image in a slow mode i.e. delay displaying for |
171 | SLOW_PREVIEW_TIME. This is so that a human has enough time to see if the |
172 | PDF renders ok. In release mode on fast processor pages take only ~100-200 ms |
173 | to render and they go away too quickly to be inspected by a human. */ |
174 | static bool gfSlowPreview = false; |
175 | |
176 | /* If true, we only dump the text, not render */ |
177 | static bool gfTextOnly = false; |
178 | |
179 | #define PAGE_NO_NOT_GIVEN -1 |
180 | |
181 | /* If equals PAGE_NO_NOT_GIVEN, we're in default mode where we render all pages. |
182 | If different, will only render this page */ |
183 | static int gPageNo = PAGE_NO_NOT_GIVEN; |
184 | /* If true, will only load the file, not render any pages. Mostly for |
185 | profiling load time */ |
186 | static bool gfLoadOnly = false; |
187 | |
188 | #define PDF_FILE_DPI 72 |
189 | |
190 | #define MAX_FILENAME_SIZE 1024 |
191 | |
192 | /* DOS is 0xd 0xa */ |
193 | #define DOS_NEWLINE "\x0d\x0a" |
194 | /* Mac is single 0xd */ |
195 | #define MAC_NEWLINE "\x0d" |
196 | /* Unix is single 0xa (10) */ |
197 | #define UNIX_NEWLINE "\x0a" |
198 | #define UNIX_NEWLINE_C 0xa |
199 | |
200 | static void memzero(void *data, size_t len) |
201 | { |
202 | memset(s: data, c: 0, n: len); |
203 | } |
204 | |
205 | static void *zmalloc(size_t len) |
206 | { |
207 | void *data = malloc(size: len); |
208 | if (data) { |
209 | memzero(data, len); |
210 | } |
211 | return data; |
212 | } |
213 | |
214 | /* Concatenate 4 strings. Any string can be NULL. |
215 | Caller needs to free() memory. */ |
216 | static char *str_cat4(const char *str1, const char *str2, const char *str3, const char *str4) |
217 | { |
218 | char *str; |
219 | char *tmp; |
220 | size_t str1_len = 0; |
221 | size_t str2_len = 0; |
222 | size_t str3_len = 0; |
223 | size_t str4_len = 0; |
224 | |
225 | if (str1) { |
226 | str1_len = strlen(s: str1); |
227 | } |
228 | if (str2) { |
229 | str2_len = strlen(s: str2); |
230 | } |
231 | if (str3) { |
232 | str3_len = strlen(s: str3); |
233 | } |
234 | if (str4) { |
235 | str4_len = strlen(s: str4); |
236 | } |
237 | |
238 | str = (char *)zmalloc(len: str1_len + str2_len + str3_len + str4_len + 1); |
239 | if (!str) { |
240 | return nullptr; |
241 | } |
242 | |
243 | tmp = str; |
244 | if (str1) { |
245 | memcpy(dest: tmp, src: str1, n: str1_len); |
246 | tmp += str1_len; |
247 | } |
248 | if (str2) { |
249 | memcpy(dest: tmp, src: str2, n: str2_len); |
250 | tmp += str2_len; |
251 | } |
252 | if (str3) { |
253 | memcpy(dest: tmp, src: str3, n: str3_len); |
254 | tmp += str3_len; |
255 | } |
256 | if (str4) { |
257 | memcpy(dest: tmp, src: str4, n: str1_len); |
258 | } |
259 | return str; |
260 | } |
261 | |
262 | static char *str_dup(const char *str) |
263 | { |
264 | return str_cat4(str1: str, str2: nullptr, str3: nullptr, str4: nullptr); |
265 | } |
266 | |
267 | static bool str_eq(const char *str1, const char *str2) |
268 | { |
269 | if (!str1 && !str2) { |
270 | return true; |
271 | } |
272 | if (!str1 || !str2) { |
273 | return false; |
274 | } |
275 | if (0 == strcmp(s1: str1, s2: str2)) { |
276 | return true; |
277 | } |
278 | return false; |
279 | } |
280 | |
281 | static bool str_ieq(const char *str1, const char *str2) |
282 | { |
283 | if (!str1 && !str2) { |
284 | return true; |
285 | } |
286 | if (!str1 || !str2) { |
287 | return false; |
288 | } |
289 | if (0 == strcasecmp(s1: str1, s2: str2)) { |
290 | return true; |
291 | } |
292 | return false; |
293 | } |
294 | |
295 | static bool str_endswith(const char *txt, const char *end) |
296 | { |
297 | size_t end_len; |
298 | size_t txt_len; |
299 | |
300 | if (!txt || !end) { |
301 | return false; |
302 | } |
303 | |
304 | txt_len = strlen(s: txt); |
305 | end_len = strlen(s: end); |
306 | if (end_len > txt_len) { |
307 | return false; |
308 | } |
309 | if (str_eq(str1: txt + txt_len - end_len, str2: end)) { |
310 | return true; |
311 | } |
312 | return false; |
313 | } |
314 | |
315 | /* TODO: probably should move to some other file and change name to |
316 | sleep_milliseconds */ |
317 | static void sleep_milliseconds(int milliseconds) |
318 | { |
319 | #ifdef _WIN32 |
320 | Sleep((DWORD)milliseconds); |
321 | #else |
322 | struct timespec tv; |
323 | int secs, nanosecs; |
324 | secs = milliseconds / 1000; |
325 | nanosecs = (milliseconds - (secs * 1000)) * 1000; |
326 | tv.tv_sec = (time_t)secs; |
327 | tv.tv_nsec = (long)nanosecs; |
328 | while (true) { |
329 | int rval = nanosleep(requested_time: &tv, remaining: &tv); |
330 | if (rval == 0) { |
331 | /* Completed the entire sleep time; all done. */ |
332 | return; |
333 | } else if (errno == EINTR) { |
334 | /* Interrupted by a signal. Try again. */ |
335 | continue; |
336 | } else { |
337 | /* Some other error; bail out. */ |
338 | return; |
339 | } |
340 | } |
341 | return; |
342 | #endif |
343 | } |
344 | |
345 | static SplashColorMode gSplashColorMode = splashModeBGR8; |
346 | |
347 | static SplashColor splashColRed; |
348 | static SplashColor splashColGreen; |
349 | static SplashColor splashColBlue; |
350 | static SplashColor splashColWhite; |
351 | static SplashColor splashColBlack; |
352 | |
353 | #define SPLASH_COL_RED_PTR (SplashColorPtr) & (splashColRed[0]) |
354 | #define SPLASH_COL_GREEN_PTR (SplashColorPtr) & (splashColGreen[0]) |
355 | #define SPLASH_COL_BLUE_PTR (SplashColorPtr) & (splashColBlue[0]) |
356 | #define SPLASH_COL_WHITE_PTR (SplashColorPtr) & (splashColWhite[0]) |
357 | #define SPLASH_COL_BLACK_PTR (SplashColorPtr) & (splashColBlack[0]) |
358 | |
359 | static SplashColorPtr gBgColor = SPLASH_COL_WHITE_PTR; |
360 | |
361 | static void splashColorSet(SplashColorPtr col, unsigned char red, unsigned char green, unsigned char blue, unsigned char alpha) |
362 | { |
363 | switch (gSplashColorMode) { |
364 | case splashModeBGR8: |
365 | col[0] = blue; |
366 | col[1] = green; |
367 | col[2] = red; |
368 | break; |
369 | case splashModeRGB8: |
370 | col[0] = red; |
371 | col[1] = green; |
372 | col[2] = blue; |
373 | break; |
374 | default: |
375 | assert(0); |
376 | break; |
377 | } |
378 | } |
379 | |
380 | static void SplashColorsInit() |
381 | { |
382 | splashColorSet(SPLASH_COL_RED_PTR, red: 0xff, green: 0, blue: 0, alpha: 0); |
383 | splashColorSet(SPLASH_COL_GREEN_PTR, red: 0, green: 0xff, blue: 0, alpha: 0); |
384 | splashColorSet(SPLASH_COL_BLUE_PTR, red: 0, green: 0, blue: 0xff, alpha: 0); |
385 | splashColorSet(SPLASH_COL_BLACK_PTR, red: 0, green: 0, blue: 0, alpha: 0); |
386 | splashColorSet(SPLASH_COL_WHITE_PTR, red: 0xff, green: 0xff, blue: 0xff, alpha: 0); |
387 | } |
388 | |
389 | PdfEnginePoppler::PdfEnginePoppler() : _fileName(nullptr), _pageCount(INVALID_PAGE_NO), _pdfDoc(nullptr), _outputDev(nullptr) { } |
390 | |
391 | PdfEnginePoppler::~PdfEnginePoppler() |
392 | { |
393 | free(ptr: _fileName); |
394 | delete _outputDev; |
395 | delete _pdfDoc; |
396 | } |
397 | |
398 | bool PdfEnginePoppler::load(const char *fileName) |
399 | { |
400 | setFileName(fileName); |
401 | |
402 | _pdfDoc = new PDFDoc(std::make_unique<GooString>(args&: fileName)); |
403 | if (!_pdfDoc->isOk()) { |
404 | return false; |
405 | } |
406 | _pageCount = _pdfDoc->getNumPages(); |
407 | return true; |
408 | } |
409 | |
410 | SplashOutputDev *PdfEnginePoppler::outputDevice() |
411 | { |
412 | if (!_outputDev) { |
413 | bool bitmapTopDown = true; |
414 | _outputDev = new SplashOutputDev(gSplashColorMode, 4, false, gBgColor, bitmapTopDown); |
415 | if (_outputDev) { |
416 | _outputDev->startDoc(docA: _pdfDoc); |
417 | } |
418 | } |
419 | return _outputDev; |
420 | } |
421 | |
422 | SplashBitmap *PdfEnginePoppler::renderBitmap(int pageNo, double zoomReal, int rotation) |
423 | { |
424 | assert(outputDevice()); |
425 | if (!outputDevice()) { |
426 | return nullptr; |
427 | } |
428 | |
429 | double hDPI = (double)PDF_FILE_DPI * zoomReal * 0.01; |
430 | double vDPI = (double)PDF_FILE_DPI * zoomReal * 0.01; |
431 | bool useMediaBox = false; |
432 | bool crop = true; |
433 | bool doLinks = true; |
434 | _pdfDoc->displayPage(out: _outputDev, page: pageNo, hDPI, vDPI, rotate: rotation, useMediaBox, crop, printing: doLinks, abortCheckCbk: nullptr, abortCheckCbkData: nullptr); |
435 | |
436 | SplashBitmap *bmp = _outputDev->takeBitmap(); |
437 | return bmp; |
438 | } |
439 | |
440 | static int StrList_Len(StrList **root) |
441 | { |
442 | int len = 0; |
443 | StrList *cur; |
444 | assert(root); |
445 | if (!root) { |
446 | return 0; |
447 | } |
448 | cur = *root; |
449 | while (cur) { |
450 | ++len; |
451 | cur = cur->next; |
452 | } |
453 | return len; |
454 | } |
455 | |
456 | static int StrList_InsertAndOwn(StrList **root, char *txt) |
457 | { |
458 | StrList *el; |
459 | assert(root && txt); |
460 | if (!root || !txt) { |
461 | return false; |
462 | } |
463 | |
464 | el = (StrList *)malloc(size: sizeof(StrList)); |
465 | if (!el) { |
466 | return false; |
467 | } |
468 | el->str = txt; |
469 | el->next = *root; |
470 | *root = el; |
471 | return true; |
472 | } |
473 | |
474 | static int StrList_Insert(StrList **root, char *txt) |
475 | { |
476 | char *txtDup; |
477 | |
478 | assert(root && txt); |
479 | if (!root || !txt) { |
480 | return false; |
481 | } |
482 | txtDup = str_dup(str: txt); |
483 | if (!txtDup) { |
484 | return false; |
485 | } |
486 | |
487 | if (!StrList_InsertAndOwn(root, txt: txtDup)) { |
488 | free(ptr: (void *)txtDup); |
489 | return false; |
490 | } |
491 | return true; |
492 | } |
493 | |
494 | static void StrList_FreeElement(StrList *el) |
495 | { |
496 | if (!el) { |
497 | return; |
498 | } |
499 | free(ptr: (void *)el->str); |
500 | free(ptr: (void *)el); |
501 | } |
502 | |
503 | static void StrList_Destroy(StrList **root) |
504 | { |
505 | StrList *cur; |
506 | StrList *next; |
507 | |
508 | if (!root) { |
509 | return; |
510 | } |
511 | cur = *root; |
512 | while (cur) { |
513 | next = cur->next; |
514 | StrList_FreeElement(el: cur); |
515 | cur = next; |
516 | } |
517 | *root = nullptr; |
518 | } |
519 | |
520 | static void my_error(ErrorCategory, Goffset pos, const char *msg) |
521 | { |
522 | #if 0 |
523 | char buf[4096], *p = buf; |
524 | |
525 | // NB: this can be called before the globalParams object is created |
526 | if (globalParams && globalParams->getErrQuiet()) { |
527 | return; |
528 | } |
529 | |
530 | if (pos >= 0) { |
531 | p += _snprintf(p, sizeof(buf)-1, "Error (%lld): " , (long long)pos); |
532 | *p = '\0'; |
533 | OutputDebugString(p); |
534 | } else { |
535 | OutputDebugString("Error: " ); |
536 | } |
537 | |
538 | p = buf; |
539 | p += vsnprintf(p, sizeof(buf) - 1, msg, args); |
540 | while ( p > buf && isspace(p[-1]) ) |
541 | *--p = '\0'; |
542 | *p++ = '\r'; |
543 | *p++ = '\n'; |
544 | *p = '\0'; |
545 | OutputDebugString(buf); |
546 | |
547 | if (pos >= 0) { |
548 | p += _snprintf(p, sizeof(buf)-1, "Error (%lld): " , (long long)pos); |
549 | *p = '\0'; |
550 | OutputDebugString(buf); |
551 | if (gErrFile) |
552 | fprintf(gErrFile, buf); |
553 | } else { |
554 | OutputDebugString("Error: " ); |
555 | if (gErrFile) |
556 | fprintf(gErrFile, "Error: " ); |
557 | } |
558 | #endif |
559 | #if 0 |
560 | p = buf; |
561 | va_start(args, msg); |
562 | p += vsnprintf(p, sizeof(buf) - 3, msg, args); |
563 | while ( p > buf && isspace(p[-1]) ) |
564 | *--p = '\0'; |
565 | *p++ = '\r'; |
566 | *p++ = '\n'; |
567 | *p = '\0'; |
568 | OutputDebugString(buf); |
569 | if (gErrFile) |
570 | fprintf(gErrFile, buf); |
571 | va_end(args); |
572 | #endif |
573 | } |
574 | |
575 | static void LogInfo(const char *fmt, ...) GCC_PRINTF_FORMAT(1, 2); |
576 | |
577 | static void LogInfo(const char *fmt, ...) |
578 | { |
579 | va_list args; |
580 | char buf[4096], *p = buf; |
581 | |
582 | p = buf; |
583 | va_start(args, fmt); |
584 | p += vsnprintf(s: p, maxlen: sizeof(buf) - 1, format: fmt, arg: args); |
585 | *p = '\0'; |
586 | fprintf(stream: gOutFile, format: "%s" , buf); |
587 | va_end(args); |
588 | fflush(stream: gOutFile); |
589 | } |
590 | |
591 | static void PrintUsageAndExit(int argc, char **argv) |
592 | { |
593 | printf(format: "Usage: pdftest [-preview|-slowpreview] [-loadonly] [-timings] [-text] [-resolution NxM] [-recursive] [-page N] [-out out.txt] pdf-files-to-process\n" ); |
594 | for (int i = 0; i < argc; i++) { |
595 | printf(format: "i=%d, '%s'\n" , i, argv[i]); |
596 | } |
597 | exit(status: 0); |
598 | } |
599 | |
600 | static bool ShowPreview() |
601 | { |
602 | if (gfPreview || gfSlowPreview) { |
603 | return true; |
604 | } |
605 | return false; |
606 | } |
607 | |
608 | static void RenderPdfAsText(const char *fileName) |
609 | { |
610 | PDFDoc *pdfDoc = nullptr; |
611 | GooString *txt = nullptr; |
612 | int pageCount; |
613 | double timeInMs; |
614 | |
615 | assert(fileName); |
616 | if (!fileName) { |
617 | return; |
618 | } |
619 | |
620 | LogInfo(fmt: "started: %s\n" , fileName); |
621 | |
622 | TextOutputDev *textOut = new TextOutputDev(nullptr, true, 0, false, false); |
623 | if (!textOut->isOk()) { |
624 | delete textOut; |
625 | return; |
626 | } |
627 | |
628 | GooTimer msTimer; |
629 | pdfDoc = new PDFDoc(std::make_unique<GooString>(args&: fileName)); |
630 | if (!pdfDoc->isOk()) { |
631 | error(category: errIO, pos: -1, msg: "RenderPdfFile(): failed to open PDF file {0:s}\n" , fileName); |
632 | goto Exit; |
633 | } |
634 | |
635 | msTimer.stop(); |
636 | timeInMs = msTimer.getElapsed(); |
637 | LogInfo(fmt: "load: %.2f ms\n" , timeInMs); |
638 | |
639 | pageCount = pdfDoc->getNumPages(); |
640 | LogInfo(fmt: "page count: %d\n" , pageCount); |
641 | |
642 | for (int curPage = 1; curPage <= pageCount; curPage++) { |
643 | if ((gPageNo != PAGE_NO_NOT_GIVEN) && (gPageNo != curPage)) { |
644 | continue; |
645 | } |
646 | |
647 | msTimer.start(); |
648 | int rotate = 0; |
649 | bool useMediaBox = false; |
650 | bool crop = true; |
651 | bool doLinks = false; |
652 | pdfDoc->displayPage(out: textOut, page: curPage, hDPI: 72, vDPI: 72, rotate, useMediaBox, crop, printing: doLinks); |
653 | txt = textOut->getText(xMin: 0.0, yMin: 0.0, xMax: 10000.0, yMax: 10000.0); |
654 | msTimer.stop(); |
655 | timeInMs = msTimer.getElapsed(); |
656 | if (gfTimings) { |
657 | LogInfo(fmt: "page %d: %.2f ms\n" , curPage, timeInMs); |
658 | } |
659 | printf(format: "%s\n" , txt->c_str()); |
660 | delete txt; |
661 | txt = nullptr; |
662 | } |
663 | |
664 | Exit: |
665 | LogInfo(fmt: "finished: %s\n" , fileName); |
666 | delete textOut; |
667 | delete pdfDoc; |
668 | } |
669 | |
670 | #ifdef _MSC_VER |
671 | # define POPPLER_TMP_NAME "c:\\poppler_tmp.pdf" |
672 | #else |
673 | # define POPPLER_TMP_NAME "/tmp/poppler_tmp.pdf" |
674 | #endif |
675 | |
676 | static void RenderPdf(const char *fileName) |
677 | { |
678 | const char *fileNameSplash = nullptr; |
679 | PdfEnginePoppler *engineSplash = nullptr; |
680 | int pageCount; |
681 | double timeInMs; |
682 | |
683 | #ifdef COPY_FILE |
684 | // TODO: fails if file already exists and has read-only attribute |
685 | CopyFile(fileName, POPPLER_TMP_NAME, false); |
686 | fileNameSplash = POPPLER_TMP_NAME; |
687 | #else |
688 | fileNameSplash = fileName; |
689 | #endif |
690 | LogInfo(fmt: "started: %s\n" , fileName); |
691 | |
692 | engineSplash = new PdfEnginePoppler(); |
693 | |
694 | GooTimer msTimer; |
695 | if (!engineSplash->load(fileName: fileNameSplash)) { |
696 | LogInfo(fmt: "failed to load splash\n" ); |
697 | goto Error; |
698 | } |
699 | msTimer.stop(); |
700 | timeInMs = msTimer.getElapsed(); |
701 | LogInfo(fmt: "load splash: %.2f ms\n" , timeInMs); |
702 | pageCount = engineSplash->pageCount(); |
703 | |
704 | LogInfo(fmt: "page count: %d\n" , pageCount); |
705 | if (gfLoadOnly) { |
706 | goto Error; |
707 | } |
708 | |
709 | for (int curPage = 1; curPage <= pageCount; curPage++) { |
710 | if ((gPageNo != PAGE_NO_NOT_GIVEN) && (gPageNo != curPage)) { |
711 | continue; |
712 | } |
713 | |
714 | SplashBitmap *bmpSplash = nullptr; |
715 | |
716 | GooTimer msRenderTimer; |
717 | bmpSplash = engineSplash->renderBitmap(pageNo: curPage, zoomReal: 100.0, rotation: 0); |
718 | msRenderTimer.stop(); |
719 | timeInMs = msRenderTimer.getElapsed(); |
720 | if (gfTimings) { |
721 | if (!bmpSplash) { |
722 | LogInfo(fmt: "page splash %d: failed to render\n" , curPage); |
723 | } else { |
724 | LogInfo(fmt: "page splash %d (%dx%d): %.2f ms\n" , curPage, bmpSplash->getWidth(), bmpSplash->getHeight(), timeInMs); |
725 | } |
726 | } |
727 | |
728 | if (ShowPreview()) { |
729 | PreviewBitmapSplash(bmpSplash); |
730 | if (gfSlowPreview) { |
731 | sleep_milliseconds(SLOW_PREVIEW_TIME); |
732 | } |
733 | } |
734 | delete bmpSplash; |
735 | } |
736 | Error: |
737 | delete engineSplash; |
738 | LogInfo(fmt: "finished: %s\n" , fileName); |
739 | } |
740 | |
741 | static void RenderFile(const char *fileName) |
742 | { |
743 | if (gfTextOnly) { |
744 | RenderPdfAsText(fileName); |
745 | return; |
746 | } |
747 | |
748 | RenderPdf(fileName); |
749 | } |
750 | |
751 | static bool ParseInteger(const char *start, const char *end, int *intOut) |
752 | { |
753 | char numBuf[16]; |
754 | int digitsCount; |
755 | const char *tmp; |
756 | |
757 | assert(start && end && intOut); |
758 | assert(end >= start); |
759 | if (!start || !end || !intOut || (start > end)) { |
760 | return false; |
761 | } |
762 | |
763 | digitsCount = 0; |
764 | tmp = start; |
765 | while (tmp <= end) { |
766 | if (isspace(*tmp)) { |
767 | /* do nothing, we allow whitespace */ |
768 | } else if (!isdigit(*tmp)) { |
769 | return false; |
770 | } |
771 | numBuf[digitsCount] = *tmp; |
772 | ++digitsCount; |
773 | if (digitsCount == dimof(numBuf) - 3) { /* -3 to be safe */ |
774 | return false; |
775 | } |
776 | ++tmp; |
777 | } |
778 | if (0 == digitsCount) { |
779 | return false; |
780 | } |
781 | numBuf[digitsCount] = 0; |
782 | *intOut = atoi(nptr: numBuf); |
783 | return true; |
784 | } |
785 | |
786 | /* Given 'resolutionString' in format NxM (e.g. "100x200"), parse the string and put N |
787 | into 'resolutionXOut' and M into 'resolutionYOut'. |
788 | Return false if there was an error (e.g. string is not in the right format */ |
789 | static bool ParseResolutionString(const char *resolutionString, int *resolutionXOut, int *resolutionYOut) |
790 | { |
791 | const char *posOfX; |
792 | |
793 | assert(resolutionString); |
794 | assert(resolutionXOut); |
795 | assert(resolutionYOut); |
796 | if (!resolutionString || !resolutionXOut || !resolutionYOut) { |
797 | return false; |
798 | } |
799 | *resolutionXOut = 0; |
800 | *resolutionYOut = 0; |
801 | posOfX = strchr(s: resolutionString, c: 'X'); |
802 | if (!posOfX) { |
803 | posOfX = strchr(s: resolutionString, c: 'x'); |
804 | } |
805 | if (!posOfX) { |
806 | return false; |
807 | } |
808 | if (posOfX == resolutionString) { |
809 | return false; |
810 | } |
811 | if (!ParseInteger(start: resolutionString, end: posOfX - 1, intOut: resolutionXOut)) { |
812 | return false; |
813 | } |
814 | if (!ParseInteger(start: posOfX + 1, end: resolutionString + strlen(s: resolutionString) - 1, intOut: resolutionYOut)) { |
815 | return false; |
816 | } |
817 | return true; |
818 | } |
819 | |
820 | static void ParseCommandLine(int argc, char **argv) |
821 | { |
822 | char *arg; |
823 | |
824 | if (argc < 2) { |
825 | PrintUsageAndExit(argc, argv); |
826 | } |
827 | |
828 | for (int i = 1; i < argc; i++) { |
829 | arg = argv[i]; |
830 | assert(arg); |
831 | if ('-' == arg[0]) { |
832 | if (str_ieq(str1: arg, TIMINGS_ARG)) { |
833 | gfTimings = true; |
834 | } else if (str_ieq(str1: arg, RESOLUTION_ARG)) { |
835 | ++i; |
836 | if (i == argc) { |
837 | PrintUsageAndExit(argc, argv); /* expect a file name after that */ |
838 | } |
839 | if (!ParseResolutionString(resolutionString: argv[i], resolutionXOut: &gResolutionX, resolutionYOut: &gResolutionY)) { |
840 | PrintUsageAndExit(argc, argv); |
841 | } |
842 | gfForceResolution = true; |
843 | } else if (str_ieq(str1: arg, RECURSIVE_ARG)) { |
844 | gfRecursive = true; |
845 | } else if (str_ieq(str1: arg, OUT_ARG)) { |
846 | /* expect a file name after that */ |
847 | ++i; |
848 | if (i == argc) { |
849 | PrintUsageAndExit(argc, argv); |
850 | } |
851 | gOutFileName = str_dup(str: argv[i]); |
852 | } else if (str_ieq(str1: arg, PREVIEW_ARG)) { |
853 | gfPreview = true; |
854 | } else if (str_ieq(str1: arg, TEXT_ARG)) { |
855 | gfTextOnly = true; |
856 | } else if (str_ieq(str1: arg, SLOW_PREVIEW_ARG)) { |
857 | gfSlowPreview = true; |
858 | } else if (str_ieq(str1: arg, LOAD_ONLY_ARG)) { |
859 | gfLoadOnly = true; |
860 | } else if (str_ieq(str1: arg, PAGE_ARG)) { |
861 | /* expect an integer after that */ |
862 | ++i; |
863 | if (i == argc) { |
864 | PrintUsageAndExit(argc, argv); |
865 | } |
866 | gPageNo = atoi(nptr: argv[i]); |
867 | if (gPageNo < 1) { |
868 | PrintUsageAndExit(argc, argv); |
869 | } |
870 | } else { |
871 | /* unknown option */ |
872 | PrintUsageAndExit(argc, argv); |
873 | } |
874 | } else { |
875 | /* we assume that this is not an option hence it must be |
876 | a name of PDF/directory/file with PDF names */ |
877 | StrList_Insert(root: &gArgsListRoot, txt: arg); |
878 | } |
879 | } |
880 | } |
881 | |
882 | static bool IsPdfFileName(char *path) |
883 | { |
884 | if (str_endswith(txt: path, end: ".pdf" )) { |
885 | return true; |
886 | } |
887 | return false; |
888 | } |
889 | |
890 | /* Render 'cmdLineArg', which can be: |
891 | - name of PDF file |
892 | */ |
893 | static void RenderCmdLineArg(char *cmdLineArg) |
894 | { |
895 | assert(cmdLineArg); |
896 | if (!cmdLineArg) { |
897 | return; |
898 | } |
899 | if (IsPdfFileName(path: cmdLineArg)) { |
900 | RenderFile(fileName: cmdLineArg); |
901 | } else { |
902 | error(category: errCommandLine, pos: -1, msg: "unexpected argument '{0:s}'" , cmdLineArg); |
903 | } |
904 | } |
905 | |
906 | int main(int argc, char **argv) |
907 | { |
908 | setErrorCallback(my_error); |
909 | ParseCommandLine(argc, argv); |
910 | if (0 == StrList_Len(root: &gArgsListRoot)) { |
911 | PrintUsageAndExit(argc, argv); |
912 | } |
913 | assert(gArgsListRoot); |
914 | |
915 | SplashColorsInit(); |
916 | globalParams = std::make_unique<GlobalParams>(); |
917 | if (!globalParams) { |
918 | return 1; |
919 | } |
920 | globalParams->setErrQuiet(false); |
921 | |
922 | FILE *outFile = nullptr; |
923 | if (gOutFileName) { |
924 | outFile = fopen(filename: gOutFileName, modes: "wb" ); |
925 | if (!outFile) { |
926 | printf(format: "failed to open -out file %s\n" , gOutFileName); |
927 | return 1; |
928 | } |
929 | gOutFile = outFile; |
930 | } else { |
931 | gOutFile = stdout; |
932 | } |
933 | |
934 | if (gOutFileName) { |
935 | gErrFile = outFile; |
936 | } else { |
937 | gErrFile = stderr; |
938 | } |
939 | |
940 | PreviewBitmapInit(); |
941 | |
942 | StrList *curr = gArgsListRoot; |
943 | while (curr) { |
944 | RenderCmdLineArg(cmdLineArg: curr->str); |
945 | curr = curr->next; |
946 | } |
947 | if (outFile) { |
948 | fclose(stream: outFile); |
949 | } |
950 | PreviewBitmapDestroy(); |
951 | StrList_Destroy(root: &gArgsListRoot); |
952 | free(ptr: gOutFileName); |
953 | return 0; |
954 | } |
955 | |