| /* Copyright Krzysztof Kowalczyk 2006-2007 |
| Copyright Hib Eris <hib@hiberis.nl> 2008, 2013 |
| Copyright 2018 Albert Astals Cid <aacid@kde.org> 2018 |
| Copyright 2019 Oliver Sander <oliver.sander@tu-dresden.de> |
| License: GPLv2 */ |
| /* |
| A tool to stress-test poppler rendering and measure rendering times for |
| very simplistic performance measuring. |
| |
| TODO: |
| * make it work with cairo output as well |
| * print more info about document like e.g. enumerate images, |
| streams, compression, encryption, password-protection. Each should have |
| a command-line arguments to turn it on/off |
| * never over-write file given as -out argument (optionally, provide -force |
| option to force writing the -out file). It's way too easy too lose results |
| of a previous run. |
| */ |
| |
| #ifdef _MSC_VER |
| // this sucks but I don't know any other way |
| #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"") |
| #endif |
| |
| #include <config.h> |
| |
| #ifdef _WIN32 |
| #include <windows.h> |
| #else |
| #include <strings.h> |
| #endif |
| |
| // Define COPY_FILE if you want the file to be copied to a local disk first |
| // before it's tested. This is desired if a file is on a slow drive. |
| // Currently copying only works on Windows. |
| // Not enabled by default. |
| //#define COPY_FILE 1 |
| |
| #include <assert.h> |
| #include <stdio.h> |
| #include <stdarg.h> |
| #include <ctype.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <errno.h> |
| #include <time.h> |
| |
| #ifdef HAVE_DIRENT_H |
| #include <dirent.h> |
| #endif |
| |
| #include "Error.h" |
| #include "ErrorCodes.h" |
| #include "goo/GooString.h" |
| #include "goo/GooTimer.h" |
| #include "GlobalParams.h" |
| #include "splash/SplashBitmap.h" |
| #include "Object.h" /* must be included before SplashOutputDev.h because of sloppiness in SplashOutputDev.h */ |
| #include "SplashOutputDev.h" |
| #include "TextOutputDev.h" |
| #include "PDFDoc.h" |
| #include "Link.h" |
| |
| #ifdef _MSC_VER |
| #define strdup _strdup |
| #define strcasecmp _stricmp |
| #endif |
| |
| #define dimof(X) (sizeof(X)/sizeof((X)[0])) |
| |
| #define INVALID_PAGE_NO -1 |
| |
| /* Those must be implemented in order to provide preview during execution. |
| They can be no-ops. An implementation for windows is in |
| perf-test-preview-win.cc |
| */ |
| extern void PreviewBitmapInit(void); |
| extern void PreviewBitmapDestroy(void); |
| extern void PreviewBitmapSplash(SplashBitmap *bmpSplash); |
| |
| class PdfEnginePoppler { |
| public: |
| PdfEnginePoppler(); |
| ~PdfEnginePoppler(); |
| |
| PdfEnginePoppler(const PdfEnginePoppler &) = delete; |
| PdfEnginePoppler& operator=(const PdfEnginePoppler &) = delete; |
| |
| const char *fileName(void) const { return _fileName; }; |
| |
| void setFileName(const char *fileName) { |
| assert(!_fileName); |
| _fileName = (char*)strdup(fileName); |
| } |
| |
| int pageCount(void) const { return _pageCount; } |
| |
| bool load(const char *fileName); |
| SplashBitmap *renderBitmap(int pageNo, double zoomReal, int rotation); |
| |
| SplashOutputDev * outputDevice(); |
| private: |
| char * _fileName; |
| int _pageCount; |
| |
| PDFDoc * _pdfDoc; |
| SplashOutputDev * _outputDev; |
| }; |
| |
| typedef struct StrList { |
| struct StrList *next; |
| char * str; |
| } StrList; |
| |
| /* List of all command-line arguments that are not switches. |
| We assume those are: |
| - names of PDF files |
| - names of a file with a list of PDF files |
| - names of directories with PDF files |
| */ |
| static StrList *gArgsListRoot = nullptr; |
| |
| /* Names of all command-line switches we recognize */ |
| #define TIMINGS_ARG "-timings" |
| #define RESOLUTION_ARG "-resolution" |
| #define RECURSIVE_ARG "-recursive" |
| #define OUT_ARG "-out" |
| #define PREVIEW_ARG "-preview" |
| #define SLOW_PREVIEW_ARG "-slowpreview" |
| #define LOAD_ONLY_ARG "-loadonly" |
| #define PAGE_ARG "-page" |
| #define TEXT_ARG "-text" |
| |
| /* Should we record timings? True if -timings command-line argument was given. */ |
| static bool gfTimings = false; |
| |
| /* If true, we use render each page at resolution 'gResolutionX'/'gResolutionY'. |
| If false, we render each page at its native resolution. |
| True if -resolution NxM command-line argument was given. */ |
| static bool gfForceResolution = false; |
| static int gResolutionX = 0; |
| static int gResolutionY = 0; |
| /* If NULL, we output the log info to stdout. If not NULL, should be a name |
| of the file to which we output log info. |
| Controlled by -out command-line argument. */ |
| static char * gOutFileName = nullptr; |
| /* FILE * corresponding to gOutFileName or stdout if gOutFileName is NULL or |
| was invalid name */ |
| static FILE * gOutFile = nullptr; |
| /* FILE * corresponding to gOutFileName or stderr if gOutFileName is NULL or |
| was invalid name */ |
| static FILE * gErrFile = nullptr; |
| |
| /* If True and a directory is given as a command-line argument, we'll process |
| pdf files in sub-directories as well. |
| Controlled by -recursive command-line argument */ |
| static bool gfRecursive = false; |
| |
| /* If true, preview rendered image. To make sure that they're being rendered correctly. */ |
| static bool gfPreview = false; |
| |
| /* 1 second (1000 milliseconds) */ |
| #define SLOW_PREVIEW_TIME 1000 |
| |
| /* If true, preview rendered image in a slow mode i.e. delay displaying for |
| SLOW_PREVIEW_TIME. This is so that a human has enough time to see if the |
| PDF renders ok. In release mode on fast processor pages take only ~100-200 ms |
| to render and they go away too quickly to be inspected by a human. */ |
| static bool gfSlowPreview = false; |
| |
| /* If true, we only dump the text, not render */ |
| static bool gfTextOnly = false; |
| |
| #define PAGE_NO_NOT_GIVEN -1 |
| |
| /* If equals PAGE_NO_NOT_GIVEN, we're in default mode where we render all pages. |
| If different, will only render this page */ |
| static int gPageNo = PAGE_NO_NOT_GIVEN; |
| /* If true, will only load the file, not render any pages. Mostly for |
| profiling load time */ |
| static bool gfLoadOnly = false; |
| |
| #define PDF_FILE_DPI 72 |
| |
| #define MAX_FILENAME_SIZE 1024 |
| |
| /* DOS is 0xd 0xa */ |
| #define DOS_NEWLINE "\x0d\x0a" |
| /* Mac is single 0xd */ |
| #define MAC_NEWLINE "\x0d" |
| /* Unix is single 0xa (10) */ |
| #define UNIX_NEWLINE "\x0a" |
| #define UNIX_NEWLINE_C 0xa |
| |
| #ifdef _WIN32 |
| #define DIR_SEP_CHAR '\\' |
| #define DIR_SEP_STR "\\" |
| #else |
| #define DIR_SEP_CHAR '/' |
| #define DIR_SEP_STR "/" |
| #endif |
| |
| static void memzero(void *data, size_t len) |
| { |
| memset(data, 0, len); |
| } |
| |
| static void *zmalloc(size_t len) |
| { |
| void *data = malloc(len); |
| if (data) |
| memzero(data, len); |
| return data; |
| } |
| |
| /* Concatenate 4 strings. Any string can be NULL. |
| Caller needs to free() memory. */ |
| static char *str_cat4(const char *str1, const char *str2, const char *str3, const char *str4) |
| { |
| char *str; |
| char *tmp; |
| size_t str1_len = 0; |
| size_t str2_len = 0; |
| size_t str3_len = 0; |
| size_t str4_len = 0; |
| |
| if (str1) |
| str1_len = strlen(str1); |
| if (str2) |
| str2_len = strlen(str2); |
| if (str3) |
| str3_len = strlen(str3); |
| if (str4) |
| str4_len = strlen(str4); |
| |
| str = (char*)zmalloc(str1_len + str2_len + str3_len + str4_len + 1); |
| if (!str) |
| return nullptr; |
| |
| tmp = str; |
| if (str1) { |
| memcpy(tmp, str1, str1_len); |
| tmp += str1_len; |
| } |
| if (str2) { |
| memcpy(tmp, str2, str2_len); |
| tmp += str2_len; |
| } |
| if (str3) { |
| memcpy(tmp, str3, str3_len); |
| tmp += str3_len; |
| } |
| if (str4) { |
| memcpy(tmp, str4, str1_len); |
| } |
| return str; |
| } |
| |
| static char *str_dup(const char *str) |
| { |
| return str_cat4(str, nullptr, nullptr, nullptr); |
| } |
| |
| static bool str_eq(const char *str1, const char *str2) |
| { |
| if (!str1 && !str2) |
| return true; |
| if (!str1 || !str2) |
| return false; |
| if (0 == strcmp(str1, str2)) |
| return true; |
| return false; |
| } |
| |
| static bool str_ieq(const char *str1, const char *str2) |
| { |
| if (!str1 && !str2) |
| return true; |
| if (!str1 || !str2) |
| return false; |
| if (0 == strcasecmp(str1, str2)) |
| return true; |
| return false; |
| } |
| |
| static bool str_endswith(const char *txt, const char *end) |
| { |
| size_t end_len; |
| size_t txt_len; |
| |
| if (!txt || !end) |
| return false; |
| |
| txt_len = strlen(txt); |
| end_len = strlen(end); |
| if (end_len > txt_len) |
| return false; |
| if (str_eq(txt+txt_len-end_len, end)) |
| return true; |
| return false; |
| } |
| |
| /* TODO: probably should move to some other file and change name to |
| sleep_milliseconds */ |
| static void sleep_milliseconds(int milliseconds) |
| { |
| #ifdef _WIN32 |
| Sleep((DWORD)milliseconds); |
| #else |
| struct timespec tv; |
| int secs, nanosecs; |
| secs = milliseconds / 1000; |
| nanosecs = (milliseconds - (secs * 1000)) * 1000; |
| tv.tv_sec = (time_t) secs; |
| tv.tv_nsec = (long) nanosecs; |
| while (1) |
| { |
| int rval = nanosleep(&tv, &tv); |
| if (rval == 0) |
| /* Completed the entire sleep time; all done. */ |
| return; |
| else if (errno == EINTR) |
| /* Interrupted by a signal. Try again. */ |
| continue; |
| else |
| /* Some other error; bail out. */ |
| return; |
| } |
| return; |
| #endif |
| } |
| |
| #ifndef HAVE_STRCPY_S |
| static void strcpy_s(char* dst, size_t dst_size, const char* src) |
| { |
| size_t src_size = strlen(src) + 1; |
| if (src_size <= dst_size) |
| memcpy(dst, src, src_size); |
| else { |
| if (dst_size > 0) { |
| memcpy(dst, src, dst_size); |
| dst[dst_size-1] = 0; |
| } |
| } |
| } |
| #endif |
| |
| #ifndef HAVE_STRCAT_S |
| static void strcat_s(char *dst, size_t dst_size, const char* src) |
| { |
| size_t dst_len = strlen(dst); |
| if (dst_len >= dst_size) { |
| if (dst_size > 0) |
| dst[dst_size-1] = 0; |
| return; |
| } |
| strcpy_s(dst+dst_len, dst_size - dst_len, src); |
| } |
| #endif |
| |
| static SplashColorMode gSplashColorMode = splashModeBGR8; |
| |
| static SplashColor splashColRed; |
| static SplashColor splashColGreen; |
| static SplashColor splashColBlue; |
| static SplashColor splashColWhite; |
| static SplashColor splashColBlack; |
| |
| #define SPLASH_COL_RED_PTR (SplashColorPtr)&(splashColRed[0]) |
| #define SPLASH_COL_GREEN_PTR (SplashColorPtr)&(splashColGreen[0]) |
| #define SPLASH_COL_BLUE_PTR (SplashColorPtr)&(splashColBlue[0]) |
| #define SPLASH_COL_WHITE_PTR (SplashColorPtr)&(splashColWhite[0]) |
| #define SPLASH_COL_BLACK_PTR (SplashColorPtr)&(splashColBlack[0]) |
| |
| static SplashColorPtr gBgColor = SPLASH_COL_WHITE_PTR; |
| |
| static void splashColorSet(SplashColorPtr col, unsigned char red, unsigned char green, unsigned char blue, unsigned char alpha) |
| { |
| switch (gSplashColorMode) |
| { |
| case splashModeBGR8: |
| col[0] = blue; |
| col[1] = green; |
| col[2] = red; |
| break; |
| case splashModeRGB8: |
| col[0] = red; |
| col[1] = green; |
| col[2] = blue; |
| break; |
| default: |
| assert(0); |
| break; |
| } |
| } |
| |
| static void SplashColorsInit(void) |
| { |
| splashColorSet(SPLASH_COL_RED_PTR, 0xff, 0, 0, 0); |
| splashColorSet(SPLASH_COL_GREEN_PTR, 0, 0xff, 0, 0); |
| splashColorSet(SPLASH_COL_BLUE_PTR, 0, 0, 0xff, 0); |
| splashColorSet(SPLASH_COL_BLACK_PTR, 0, 0, 0, 0); |
| splashColorSet(SPLASH_COL_WHITE_PTR, 0xff, 0xff, 0xff, 0); |
| } |
| |
| PdfEnginePoppler::PdfEnginePoppler() : |
| _fileName(nullptr) |
| , _pageCount(INVALID_PAGE_NO) |
| , _pdfDoc(nullptr) |
| , _outputDev(nullptr) |
| { |
| } |
| |
| PdfEnginePoppler::~PdfEnginePoppler() |
| { |
| free(_fileName); |
| delete _outputDev; |
| delete _pdfDoc; |
| } |
| |
| bool PdfEnginePoppler::load(const char *fileName) |
| { |
| setFileName(fileName); |
| /* note: don't delete fileNameStr since PDFDoc takes ownership and deletes them itself */ |
| GooString *fileNameStr = new GooString(fileName); |
| if (!fileNameStr) return false; |
| |
| _pdfDoc = new PDFDoc(fileNameStr, nullptr, nullptr, nullptr); |
| if (!_pdfDoc->isOk()) { |
| return false; |
| } |
| _pageCount = _pdfDoc->getNumPages(); |
| return true; |
| } |
| |
| SplashOutputDev * PdfEnginePoppler::outputDevice() { |
| if (!_outputDev) { |
| bool bitmapTopDown = true; |
| _outputDev = new SplashOutputDev(gSplashColorMode, 4, false, gBgColor, bitmapTopDown); |
| if (_outputDev) |
| _outputDev->startDoc(_pdfDoc); |
| } |
| return _outputDev; |
| } |
| |
| SplashBitmap *PdfEnginePoppler::renderBitmap(int pageNo, double zoomReal, int rotation) |
| { |
| assert(outputDevice()); |
| if (!outputDevice()) return nullptr; |
| |
| double hDPI = (double)PDF_FILE_DPI * zoomReal * 0.01; |
| double vDPI = (double)PDF_FILE_DPI * zoomReal * 0.01; |
| bool useMediaBox = false; |
| bool crop = true; |
| bool doLinks = true; |
| _pdfDoc->displayPage(_outputDev, pageNo, hDPI, vDPI, rotation, useMediaBox, |
| crop, doLinks, nullptr, nullptr); |
| |
| SplashBitmap* bmp = _outputDev->takeBitmap(); |
| return bmp; |
| } |
| |
| struct FindFileState { |
| char path[MAX_FILENAME_SIZE]; |
| char dirpath[MAX_FILENAME_SIZE]; /* current dir path */ |
| char pattern[MAX_FILENAME_SIZE]; /* search pattern */ |
| const char *bufptr; |
| #ifdef _WIN32 |
| WIN32_FIND_DATA fileinfo; |
| HANDLE dir; |
| #else |
| DIR *dir; |
| #endif |
| }; |
| |
| #ifdef _WIN32 |
| #include <sys/timeb.h> |
| #include <direct.h> |
| |
| __inline char *getcwd(char *buffer, int maxlen) |
| { |
| return _getcwd(buffer, maxlen); |
| } |
| |
| static int fnmatch(const char *pattern, const char *string, int flags) |
| { |
| int prefix_len; |
| const char *star_pos = strchr(pattern, '*'); |
| if (!star_pos) |
| return strcmp(pattern, string) != 0; |
| |
| prefix_len = (int)(star_pos-pattern); |
| if (0 == prefix_len) |
| return 0; |
| |
| if (0 == _strnicmp(pattern, string, prefix_len)) |
| return 0; |
| |
| return 1; |
| } |
| |
| #else |
| #include <fnmatch.h> |
| #endif |
| |
| #ifdef _WIN32 |
| /* on windows to query dirs we need foo\* to get files in this directory. |
| foo\ always fails and foo will return just info about foo directory, |
| not files in this directory */ |
| static void win_correct_path_for_FindFirstFile(char *path, int path_max_len) |
| { |
| int path_len = strlen(path); |
| if (path_len >= path_max_len-4) |
| return; |
| if (DIR_SEP_CHAR != path[path_len]) |
| path[path_len++] = DIR_SEP_CHAR; |
| path[path_len++] = '*'; |
| path[path_len] = 0; |
| } |
| #endif |
| |
| static FindFileState *find_file_open(const char *path, const char *pattern) |
| { |
| FindFileState *s; |
| |
| s = (FindFileState*)malloc(sizeof(FindFileState)); |
| if (!s) |
| return nullptr; |
| strcpy_s(s->path, sizeof(s->path), path); |
| strcpy_s(s->dirpath, sizeof(s->path), path); |
| #ifdef _WIN32 |
| win_correct_path_for_FindFirstFile(s->path, sizeof(s->path)); |
| #endif |
| strcpy_s(s->pattern, sizeof(s->pattern), pattern); |
| s->bufptr = s->path; |
| #ifdef _WIN32 |
| s->dir = INVALID_HANDLE_VALUE; |
| #else |
| s->dir = nullptr; |
| #endif |
| return s; |
| } |
| |
| #if 0 /* re-enable if we #define USE_OWN_GET_AUTH_DATA */ |
| void *StandardSecurityHandler::getAuthData() |
| { |
| return NULL; |
| } |
| #endif |
| |
| static char *makepath(char *buf, int buf_size, const char *path, |
| const char *filename) |
| { |
| strcpy_s(buf, buf_size, path); |
| int len = strlen(path); |
| if (len > 0 && path[len - 1] != DIR_SEP_CHAR && len + 1 < buf_size) { |
| buf[len++] = DIR_SEP_CHAR; |
| buf[len] = '\0'; |
| } |
| strcat_s(buf, buf_size, filename); |
| return buf; |
| } |
| |
| #ifdef _WIN32 |
| static int skip_matching_file(const char *filename) |
| { |
| if (0 == strcmp(".", filename)) |
| return 1; |
| if (0 == strcmp("..", filename)) |
| return 1; |
| return 0; |
| } |
| #endif |
| |
| static int find_file_next(FindFileState *s, char *filename, int filename_size_max) |
| { |
| #ifdef _WIN32 |
| int fFound; |
| if (INVALID_HANDLE_VALUE == s->dir) { |
| s->dir = FindFirstFile(s->path, &(s->fileinfo)); |
| if (INVALID_HANDLE_VALUE == s->dir) |
| return -1; |
| goto CheckFile; |
| } |
| |
| while (1) { |
| fFound = FindNextFile(s->dir, &(s->fileinfo)); |
| if (!fFound) |
| return -1; |
| CheckFile: |
| if (skip_matching_file(s->fileinfo.cFileName)) |
| continue; |
| if (0 == fnmatch(s->pattern, s->fileinfo.cFileName, 0) ) { |
| makepath(filename, filename_size_max, s->dirpath, s->fileinfo.cFileName); |
| return 0; |
| } |
| } |
| #else |
| struct dirent *dirent; |
| const char *p; |
| char *q; |
| |
| if (s->dir == nullptr) |
| goto redo; |
| |
| for (;;) { |
| dirent = readdir(s->dir); |
| if (dirent == nullptr) { |
| redo: |
| if (s->dir) { |
| closedir(s->dir); |
| s->dir = nullptr; |
| } |
| p = s->bufptr; |
| if (*p == '\0') |
| return -1; |
| /* CG: get_str(&p, s->dirpath, sizeof(s->dirpath), ":") */ |
| q = s->dirpath; |
| while (*p != ':' && *p != '\0') { |
| if ((q - s->dirpath) < (int)sizeof(s->dirpath) - 1) |
| *q++ = *p; |
| p++; |
| } |
| *q = '\0'; |
| if (*p == ':') |
| p++; |
| s->bufptr = p; |
| s->dir = opendir(s->dirpath); |
| if (!s->dir) |
| goto redo; |
| } else { |
| if (fnmatch(s->pattern, dirent->d_name, 0) == 0) { |
| makepath(filename, filename_size_max, |
| s->dirpath, dirent->d_name); |
| return 0; |
| } |
| } |
| } |
| #endif |
| } |
| |
| static void find_file_close(FindFileState *s) |
| { |
| #ifdef _WIN32 |
| if (INVALID_HANDLE_VALUE != s->dir) |
| FindClose(s->dir); |
| #else |
| if (s->dir) |
| closedir(s->dir); |
| #endif |
| free(s); |
| } |
| |
| static int StrList_Len(StrList **root) |
| { |
| int len = 0; |
| StrList * cur; |
| assert(root); |
| if (!root) |
| return 0; |
| cur = *root; |
| while (cur) { |
| ++len; |
| cur = cur->next; |
| } |
| return len; |
| } |
| |
| static int StrList_InsertAndOwn(StrList **root, char *txt) |
| { |
| StrList * el; |
| assert(root && txt); |
| if (!root || !txt) |
| return false; |
| |
| el = (StrList*)malloc(sizeof(StrList)); |
| if (!el) |
| return false; |
| el->str = txt; |
| el->next = *root; |
| *root = el; |
| return true; |
| } |
| |
| static int StrList_Insert(StrList **root, char *txt) |
| { |
| char *txtDup; |
| |
| assert(root && txt); |
| if (!root || !txt) |
| return false; |
| txtDup = str_dup(txt); |
| if (!txtDup) |
| return false; |
| |
| if (!StrList_InsertAndOwn(root, txtDup)) { |
| free((void*)txtDup); |
| return false; |
| } |
| return true; |
| } |
| |
| static StrList* StrList_RemoveHead(StrList **root) |
| { |
| StrList *tmp; |
| assert(root); |
| if (!root) |
| return nullptr; |
| |
| if (!*root) |
| return nullptr; |
| tmp = *root; |
| *root = tmp->next; |
| tmp->next = nullptr; |
| return tmp; |
| } |
| |
| static void StrList_FreeElement(StrList *el) |
| { |
| if (!el) |
| return; |
| free((void*)el->str); |
| free((void*)el); |
| } |
| |
| static void StrList_Destroy(StrList **root) |
| { |
| StrList * cur; |
| StrList * next; |
| |
| if (!root) |
| return; |
| cur = *root; |
| while (cur) { |
| next = cur->next; |
| StrList_FreeElement(cur); |
| cur = next; |
| } |
| *root = nullptr; |
| } |
| |
| static void my_error(void *, ErrorCategory, Goffset pos, const char *msg) { |
| #if 0 |
| char buf[4096], *p = buf; |
| |
| // NB: this can be called before the globalParams object is created |
| if (globalParams && globalParams->getErrQuiet()) { |
| return; |
| } |
| |
| if (pos >= 0) { |
| p += _snprintf(p, sizeof(buf)-1, "Error (%lld): ", (long long)pos); |
| *p = '\0'; |
| OutputDebugString(p); |
| } else { |
| OutputDebugString("Error: "); |
| } |
| |
| p = buf; |
| p += vsnprintf(p, sizeof(buf) - 1, msg, args); |
| while ( p > buf && isspace(p[-1]) ) |
| *--p = '\0'; |
| *p++ = '\r'; |
| *p++ = '\n'; |
| *p = '\0'; |
| OutputDebugString(buf); |
| |
| if (pos >= 0) { |
| p += _snprintf(p, sizeof(buf)-1, "Error (%lld): ", (long long)pos); |
| *p = '\0'; |
| OutputDebugString(buf); |
| if (gErrFile) |
| fprintf(gErrFile, buf); |
| } else { |
| OutputDebugString("Error: "); |
| if (gErrFile) |
| fprintf(gErrFile, "Error: "); |
| } |
| #endif |
| #if 0 |
| p = buf; |
| va_start(args, msg); |
| p += vsnprintf(p, sizeof(buf) - 3, msg, args); |
| while ( p > buf && isspace(p[-1]) ) |
| *--p = '\0'; |
| *p++ = '\r'; |
| *p++ = '\n'; |
| *p = '\0'; |
| OutputDebugString(buf); |
| if (gErrFile) |
| fprintf(gErrFile, buf); |
| va_end(args); |
| #endif |
| } |
| |
| static void LogInfo(const char *fmt, ...) GCC_PRINTF_FORMAT(1, 2); |
| |
| static void LogInfo(const char *fmt, ...) |
| { |
| va_list args; |
| char buf[4096], *p = buf; |
| |
| p = buf; |
| va_start(args, fmt); |
| p += vsnprintf(p, sizeof(buf) - 1, fmt, args); |
| *p = '\0'; |
| fprintf(gOutFile, "%s", buf); |
| va_end(args); |
| fflush(gOutFile); |
| } |
| |
| static void PrintUsageAndExit(int argc, char **argv) |
| { |
| printf("Usage: pdftest [-preview|-slowpreview] [-loadonly] [-timings] [-text] [-resolution NxM] [-recursive] [-page N] [-out out.txt] pdf-files-to-process\n"); |
| for (int i=0; i < argc; i++) { |
| printf("i=%d, '%s'\n", i, argv[i]); |
| } |
| exit(0); |
| } |
| |
| static bool ShowPreview(void) |
| { |
| if (gfPreview || gfSlowPreview) |
| return true; |
| return false; |
| } |
| |
| static void RenderPdfAsText(const char *fileName) |
| { |
| GooString * fileNameStr = nullptr; |
| PDFDoc * pdfDoc = nullptr; |
| GooString * txt = nullptr; |
| int pageCount; |
| double timeInMs; |
| |
| assert(fileName); |
| if (!fileName) |
| return; |
| |
| LogInfo("started: %s\n", fileName); |
| |
| TextOutputDev * textOut = new TextOutputDev(nullptr, true, 0, false, false); |
| if (!textOut->isOk()) { |
| delete textOut; |
| return; |
| } |
| |
| GooTimer msTimer; |
| /* note: don't delete fileNameStr since PDFDoc takes ownership and deletes them itself */ |
| fileNameStr = new GooString(fileName); |
| if (!fileNameStr) |
| goto Exit; |
| |
| pdfDoc = new PDFDoc(fileNameStr, nullptr, nullptr, nullptr); |
| if (!pdfDoc->isOk()) { |
| error(errIO, -1, "RenderPdfFile(): failed to open PDF file {0:s}\n", fileName); |
| goto Exit; |
| } |
| |
| msTimer.stop(); |
| timeInMs = msTimer.getElapsed(); |
| LogInfo("load: %.2f ms\n", timeInMs); |
| |
| pageCount = pdfDoc->getNumPages(); |
| LogInfo("page count: %d\n", pageCount); |
| |
| for (int curPage = 1; curPage <= pageCount; curPage++) { |
| if ((gPageNo != PAGE_NO_NOT_GIVEN) && (gPageNo != curPage)) |
| continue; |
| |
| msTimer.start(); |
| int rotate = 0; |
| bool useMediaBox = false; |
| bool crop = true; |
| bool doLinks = false; |
| pdfDoc->displayPage(textOut, curPage, 72, 72, rotate, useMediaBox, crop, doLinks); |
| txt = textOut->getText(0.0, 0.0, 10000.0, 10000.0); |
| msTimer.stop(); |
| timeInMs = msTimer.getElapsed(); |
| if (gfTimings) |
| LogInfo("page %d: %.2f ms\n", curPage, timeInMs); |
| printf("%s\n", txt->c_str()); |
| delete txt; |
| txt = nullptr; |
| } |
| |
| Exit: |
| LogInfo("finished: %s\n", fileName); |
| delete textOut; |
| delete pdfDoc; |
| } |
| |
| #ifdef _MSC_VER |
| #define POPPLER_TMP_NAME "c:\\poppler_tmp.pdf" |
| #else |
| #define POPPLER_TMP_NAME "/tmp/poppler_tmp.pdf" |
| #endif |
| |
| static void RenderPdf(const char *fileName) |
| { |
| const char * fileNameSplash = nullptr; |
| PdfEnginePoppler * engineSplash = nullptr; |
| int pageCount; |
| double timeInMs; |
| |
| #ifdef COPY_FILE |
| // TODO: fails if file already exists and has read-only attribute |
| CopyFile(fileName, POPPLER_TMP_NAME, false); |
| fileNameSplash = POPPLER_TMP_NAME; |
| #else |
| fileNameSplash = fileName; |
| #endif |
| LogInfo("started: %s\n", fileName); |
| |
| engineSplash = new PdfEnginePoppler(); |
| |
| GooTimer msTimer; |
| if (!engineSplash->load(fileNameSplash)) { |
| LogInfo("failed to load splash\n"); |
| goto Error; |
| } |
| msTimer.stop(); |
| timeInMs = msTimer.getElapsed(); |
| LogInfo("load splash: %.2f ms\n", timeInMs); |
| pageCount = engineSplash->pageCount(); |
| |
| LogInfo("page count: %d\n", pageCount); |
| if (gfLoadOnly) |
| goto Error; |
| |
| for (int curPage = 1; curPage <= pageCount; curPage++) { |
| if ((gPageNo != PAGE_NO_NOT_GIVEN) && (gPageNo != curPage)) |
| continue; |
| |
| SplashBitmap *bmpSplash = nullptr; |
| |
| GooTimer msRenderTimer; |
| bmpSplash = engineSplash->renderBitmap(curPage, 100.0, 0); |
| msRenderTimer.stop(); |
| timeInMs = msRenderTimer.getElapsed(); |
| if (gfTimings) { |
| if (!bmpSplash) |
| LogInfo("page splash %d: failed to render\n", curPage); |
| else |
| LogInfo("page splash %d (%dx%d): %.2f ms\n", curPage, bmpSplash->getWidth(), bmpSplash->getHeight(), timeInMs); |
| } |
| |
| if (ShowPreview()) { |
| PreviewBitmapSplash(bmpSplash); |
| if (gfSlowPreview) |
| sleep_milliseconds(SLOW_PREVIEW_TIME); |
| } |
| delete bmpSplash; |
| } |
| Error: |
| delete engineSplash; |
| LogInfo("finished: %s\n", fileName); |
| } |
| |
| static void RenderFile(const char *fileName) |
| { |
| if (gfTextOnly) { |
| RenderPdfAsText(fileName); |
| return; |
| } |
| |
| RenderPdf(fileName); |
| } |
| |
| static bool ParseInteger(const char *start, const char *end, int *intOut) |
| { |
| char numBuf[16]; |
| int digitsCount; |
| const char * tmp; |
| |
| assert(start && end && intOut); |
| assert(end >= start); |
| if (!start || !end || !intOut || (start > end)) |
| return false; |
| |
| digitsCount = 0; |
| tmp = start; |
| while (tmp <= end) { |
| if (isspace(*tmp)) { |
| /* do nothing, we allow whitespace */ |
| } else if (!isdigit(*tmp)) |
| return false; |
| numBuf[digitsCount] = *tmp; |
| ++digitsCount; |
| if (digitsCount == dimof(numBuf)-3) /* -3 to be safe */ |
| return false; |
| ++tmp; |
| } |
| if (0 == digitsCount) |
| return false; |
| numBuf[digitsCount] = 0; |
| *intOut = atoi(numBuf); |
| return true; |
| } |
| |
| /* Given 'resolutionString' in format NxM (e.g. "100x200"), parse the string and put N |
| into 'resolutionXOut' and M into 'resolutionYOut'. |
| Return false if there was an error (e.g. string is not in the right format */ |
| static bool ParseResolutionString(const char *resolutionString, int *resolutionXOut, int *resolutionYOut) |
| { |
| const char * posOfX; |
| |
| assert(resolutionString); |
| assert(resolutionXOut); |
| assert(resolutionYOut); |
| if (!resolutionString || !resolutionXOut || !resolutionYOut) |
| return false; |
| *resolutionXOut = 0; |
| *resolutionYOut = 0; |
| posOfX = strchr(resolutionString, 'X'); |
| if (!posOfX) |
| posOfX = strchr(resolutionString, 'x'); |
| if (!posOfX) |
| return false; |
| if (posOfX == resolutionString) |
| return false; |
| if (!ParseInteger(resolutionString, posOfX-1, resolutionXOut)) |
| return false; |
| if (!ParseInteger(posOfX+1, resolutionString+strlen(resolutionString)-1, resolutionYOut)) |
| return false; |
| return true; |
| } |
| |
| static void ParseCommandLine(int argc, char **argv) |
| { |
| char * arg; |
| |
| if (argc < 2) |
| PrintUsageAndExit(argc, argv); |
| |
| for (int i=1; i < argc; i++) { |
| arg = argv[i]; |
| assert(arg); |
| if ('-' == arg[0]) { |
| if (str_ieq(arg, TIMINGS_ARG)) { |
| gfTimings = true; |
| } else if (str_ieq(arg, RESOLUTION_ARG)) { |
| ++i; |
| if (i == argc) |
| PrintUsageAndExit(argc, argv); /* expect a file name after that */ |
| if (!ParseResolutionString(argv[i], &gResolutionX, &gResolutionY)) |
| PrintUsageAndExit(argc, argv); |
| gfForceResolution = true; |
| } else if (str_ieq(arg, RECURSIVE_ARG)) { |
| gfRecursive = true; |
| } else if (str_ieq(arg, OUT_ARG)) { |
| /* expect a file name after that */ |
| ++i; |
| if (i == argc) |
| PrintUsageAndExit(argc, argv); |
| gOutFileName = str_dup(argv[i]); |
| } else if (str_ieq(arg, PREVIEW_ARG)) { |
| gfPreview = true; |
| } else if (str_ieq(arg, TEXT_ARG)) { |
| gfTextOnly = true; |
| } else if (str_ieq(arg, SLOW_PREVIEW_ARG)) { |
| gfSlowPreview = true; |
| } else if (str_ieq(arg, LOAD_ONLY_ARG)) { |
| gfLoadOnly = true; |
| } else if (str_ieq(arg, PAGE_ARG)) { |
| /* expect an integer after that */ |
| ++i; |
| if (i == argc) |
| PrintUsageAndExit(argc, argv); |
| gPageNo = atoi(argv[i]); |
| if (gPageNo < 1) |
| PrintUsageAndExit(argc, argv); |
| } else { |
| /* unknown option */ |
| PrintUsageAndExit(argc, argv); |
| } |
| } else { |
| /* we assume that this is not an option hence it must be |
| a name of PDF/directory/file with PDF names */ |
| StrList_Insert(&gArgsListRoot, arg); |
| } |
| } |
| } |
| |
| #if 0 |
| void RenderFileList(char *pdfFileList) |
| { |
| char *data = NULL; |
| char *dataNormalized = NULL; |
| char *pdfFileName; |
| uint64_t fileSize; |
| |
| assert(pdfFileList); |
| if (!pdfFileList) |
| return; |
| data = file_read_all(pdfFileList, &fileSize); |
| if (!data) { |
| error(-1, "couldn't load file '%s'", pdfFileList); |
| return; |
| } |
| dataNormalized = str_normalize_newline(data, UNIX_NEWLINE); |
| if (!dataNormalized) { |
| error(-1, "couldn't normalize data of file '%s'", pdfFileList); |
| goto Exit; |
| } |
| for (;;) { |
| pdfFileName = str_split_iter(&dataNormalized, UNIX_NEWLINE_C); |
| if (!pdfFileName) |
| break; |
| str_strip_ws_both(pdfFileName); |
| if (str_empty(pdfFileName)) { |
| free((void*)pdfFileName); |
| continue; |
| } |
| RenderFile(pdfFileName); |
| free((void*)pdfFileName); |
| } |
| Exit: |
| free((void*)dataNormalized); |
| free((void*)data); |
| } |
| #endif |
| |
| #ifdef _WIN32 |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| |
| static bool IsDirectoryName(char *path) |
| { |
| struct _stat buf; |
| int result; |
| |
| result = _stat(path, &buf ); |
| if (0 != result) |
| return false; |
| |
| if (buf.st_mode & _S_IFDIR) |
| return true; |
| |
| return false; |
| } |
| |
| static bool IsFileName(char *path) |
| { |
| struct _stat buf; |
| int result; |
| |
| result = _stat(path, &buf ); |
| if (0 != result) |
| return false; |
| |
| if (buf.st_mode & _S_IFREG) |
| return true; |
| |
| return false; |
| } |
| #else |
| static bool IsDirectoryName(char *path) |
| { |
| /* TODO: implement me */ |
| return false; |
| } |
| |
| static bool IsFileName(char *path) |
| { |
| /* TODO: implement me */ |
| return true; |
| } |
| #endif |
| |
| static bool IsPdfFileName(char *path) |
| { |
| if (str_endswith(path, ".pdf")) |
| return true; |
| return false; |
| } |
| |
| static void RenderDirectory(char *path) |
| { |
| FindFileState * ffs; |
| char filename[MAX_FILENAME_SIZE]; |
| StrList * dirList = nullptr; |
| StrList * el; |
| |
| StrList_Insert(&dirList, path); |
| |
| while (0 != StrList_Len(&dirList)) { |
| el = StrList_RemoveHead(&dirList); |
| ffs = find_file_open(el->str, "*"); |
| while (!find_file_next(ffs, filename, sizeof(filename))) { |
| if (IsDirectoryName(filename)) { |
| if (gfRecursive) { |
| StrList_Insert(&dirList, filename); |
| } |
| } else if (IsFileName(filename)) { |
| if (IsPdfFileName(filename)) { |
| RenderFile(filename); |
| } |
| } |
| } |
| find_file_close(ffs); |
| StrList_FreeElement(el); |
| } |
| StrList_Destroy(&dirList); |
| } |
| |
| /* Render 'cmdLineArg', which can be: |
| - directory name |
| - name of PDF file |
| - name of text file with names of PDF files |
| */ |
| static void RenderCmdLineArg(char *cmdLineArg) |
| { |
| assert(cmdLineArg); |
| if (!cmdLineArg) |
| return; |
| if (IsDirectoryName(cmdLineArg)) { |
| RenderDirectory(cmdLineArg); |
| } else if (IsFileName(cmdLineArg)) { |
| if (IsPdfFileName(cmdLineArg)) |
| RenderFile(cmdLineArg); |
| #if 0 |
| else |
| RenderFileList(cmdLineArg); |
| #endif |
| } else { |
| error(errCommandLine, -1, "unexpected argument '{0:s}'", cmdLineArg); |
| } |
| } |
| |
| int main(int argc, char **argv) |
| { |
| setErrorCallback(my_error, nullptr); |
| ParseCommandLine(argc, argv); |
| if (0 == StrList_Len(&gArgsListRoot)) |
| PrintUsageAndExit(argc, argv); |
| assert(gArgsListRoot); |
| |
| SplashColorsInit(); |
| globalParams = new GlobalParams(); |
| if (!globalParams) |
| return 1; |
| globalParams->setErrQuiet(false); |
| |
| FILE * outFile = nullptr; |
| if (gOutFileName) { |
| outFile = fopen(gOutFileName, "wb"); |
| if (!outFile) { |
| printf("failed to open -out file %s\n", gOutFileName); |
| return 1; |
| } |
| gOutFile = outFile; |
| } |
| else |
| gOutFile = stdout; |
| |
| if (gOutFileName) |
| gErrFile = outFile; |
| else |
| gErrFile = stderr; |
| |
| PreviewBitmapInit(); |
| |
| StrList * curr = gArgsListRoot; |
| while (curr) { |
| RenderCmdLineArg(curr->str); |
| curr = curr->next; |
| } |
| if (outFile) |
| fclose(outFile); |
| PreviewBitmapDestroy(); |
| StrList_Destroy(&gArgsListRoot); |
| delete globalParams; |
| free(gOutFileName); |
| return 0; |
| } |
| |