From ff59857ef4f2a2d7fa7cc885a0015bb21c34705e Mon Sep 17 00:00:00 2001 From: Ryan Morlok Date: Wed, 27 Feb 2013 23:59:07 -0600 Subject: [PATCH 1/6] Added ability to explicitly specify page name format when splitting output across multiple pages. Can include page number position in the output file name. Examples: pdf2htmlEX --split-pages 1 foo.pdf bar%d.html pdf2htmlEX --split-pages 1 foo.pdf bar%02d.page --- src/HTMLRenderer/general.cc | 3 ++- src/pdf2htmlEX.cc | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/HTMLRenderer/general.cc b/src/HTMLRenderer/general.cc index cda2d5d..68038c3 100644 --- a/src/HTMLRenderer/general.cc +++ b/src/HTMLRenderer/general.cc @@ -101,7 +101,8 @@ void HTMLRenderer::process(PDFDoc *doc) if(param->split_pages) { - auto page_fn = str_fmt("%s/%s%d.page", param->dest_dir.c_str(), param->output_filename.c_str(), i); + auto page_template_fn = str_fmt("%s/%s", param->dest_dir.c_str(), param->output_filename.c_str()); + auto page_fn = str_fmt(page_template_fn, i); f_pages.fs.open((char*)page_fn, ofstream::binary); if(!f_pages.fs) throw string("Cannot open ") + (char*)page_fn + " for writing"; diff --git a/src/pdf2htmlEX.cc b/src/pdf2htmlEX.cc index 806e91c..a54d5bf 100644 --- a/src/pdf2htmlEX.cc +++ b/src/pdf2htmlEX.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -213,7 +214,7 @@ int main(int argc, char **argv) if(get_suffix(param.input_filename) == ".pdf") { if(param.split_pages) - param.output_filename = s.substr(0, s.size() - 4); + param.output_filename = s.substr(0, s.size() - 4) + "%d.page"; else param.output_filename = s.substr(0, s.size() - 4) + ".html"; @@ -221,11 +222,16 @@ int main(int argc, char **argv) else { if(param.split_pages) - param.output_filename = s; + param.output_filename = s + "%d.page"; else param.output_filename = s + ".html"; } + } + else if(param.split_pages && !std::regex_match(param.output_filename, std::regex("^.*%[0-9]*d.*$"))) + { + const string suffix = get_suffix(param.output_filename); + param.output_filename = param.output_filename.substr(0, param.output_filename.size() - suffix.size()) + "%d" + suffix; } if(param.css_filename.empty()) { From 83c947462a2f5b599003c36a182f634a7f7e92ee Mon Sep 17 00:00:00 2001 From: Ryan Morlok Date: Sun, 17 Mar 2013 00:08:06 -0500 Subject: [PATCH 2/6] added input sanitation for split page generation when accepting a custom outfile file name format. Added unit tests for various file name generation scenarios. --- pdf2htmlEX.1.in | 6 +- src/HTMLRenderer/general.cc | 4 +- src/pdf2htmlEX.cc | 20 +++- src/util/path.cc | 16 +++ src/util/path.h | 11 ++ test/test_data/1-page.pdf | Bin 0 -> 12378 bytes test/test_data/2-pages.pdf | Bin 0 -> 13114 bytes test/test_data/3-pages.pdf | Bin 0 -> 13901 bytes test/test_naming.py | 227 ++++++++++++++++++++++++++++++++++++ 9 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 test/test_data/1-page.pdf create mode 100644 test/test_data/2-pages.pdf create mode 100644 test/test_data/3-pages.pdf create mode 100644 test/test_naming.py diff --git a/pdf2htmlEX.1.in b/pdf2htmlEX.1.in index 14d8917..b0bddfc 100644 --- a/pdf2htmlEX.1.in +++ b/pdf2htmlEX.1.in @@ -65,9 +65,9 @@ You need to modify the manifest if you do not want outline embedded. .TP .B --split-pages <0|1> (Default: 0) -If turned on, pages will be stored into separated files named as 0.page, 1.page, ... +If turned on, pages will be stored into separated files. By defualt, these files will be named as 0.page, 1.page, ..., however the name of the files can be customized by adding a %d marker in the to specify how the page should be used to generate the name. E.g. p%d.page yeilding p1.page, p2.page ... or p%03d.page yielding p001.page, p002.page etc. Only %d may be used, no other formatting markers. -Also the css and outline will be stored into separated files, and the will be no .html generated. +Also the css and outline will be stored into separated files, and there will be no .html generated. This switch is useful if you want pages to be loaded separately & dynamically -- in which case you need to compose the page yourself, and a supporting backend might be necessary. @@ -83,7 +83,7 @@ If it's empty, the file name will be determined automatically. .TP .B --outline-filename (Default: ) -Specify the filename of the generated outline file, if not embedded. +Specify the filename of the generated outline file, if not embedded. If it's empty, the file name will be determined automatically. diff --git a/src/HTMLRenderer/general.cc b/src/HTMLRenderer/general.cc index d23e1f0..bd6b196 100644 --- a/src/HTMLRenderer/general.cc +++ b/src/HTMLRenderer/general.cc @@ -101,8 +101,8 @@ void HTMLRenderer::process(PDFDoc *doc) if(param->split_pages) { - auto page_template_fn = str_fmt("%s/%s", param->dest_dir.c_str(), param->output_filename.c_str()); - auto page_fn = str_fmt(page_template_fn, i); + auto filled_template_filename = str_fmt(param->output_filename.c_str(), i); + auto page_fn = str_fmt("%s/%s", param->dest_dir.c_str(), string((char*)filled_template_filename).c_str()); f_pages.fs.open((char*)page_fn, ofstream::binary); if(!f_pages.fs) throw string("Cannot open ") + (char*)page_fn + " for writing"; diff --git a/src/pdf2htmlEX.cc b/src/pdf2htmlEX.cc index 01ae39f..54f79bb 100644 --- a/src/pdf2htmlEX.cc +++ b/src/pdf2htmlEX.cc @@ -216,7 +216,7 @@ int main(int argc, char **argv) if(get_suffix(param.input_filename) == ".pdf") { if(param.split_pages) - param.output_filename = s.substr(0, s.size() - 4) + "%d.page"; + param.output_filename = sanitize_filename(s.substr(0, s.size() - 4) + "%d.page", true); else param.output_filename = s.substr(0, s.size() - 4) + ".html"; @@ -224,16 +224,26 @@ int main(int argc, char **argv) else { if(param.split_pages) - param.output_filename = s + "%d.page"; + param.output_filename = sanitize_filename(s + "%d.page", true); else param.output_filename = s + ".html"; } } - else if(param.split_pages && !std::regex_match(param.output_filename, std::regex("^.*%[0-9]*d.*$"))) + else if(param.split_pages) { - const string suffix = get_suffix(param.output_filename); - param.output_filename = param.output_filename.substr(0, param.output_filename.size() - suffix.size()) + "%d" + suffix; + // Need to make sure we have a page number placeholder in the filename + if(!std::regex_match(param.output_filename, std::regex("^.*%[0-9]*d.*$"))) + { + // Inject the placeholder just before the file extension + const string suffix = get_suffix(param.output_filename); + param.output_filename = sanitize_filename(param.output_filename.substr(0, param.output_filename.size() - suffix.size()) + "%d" + suffix, true); + } + else + { + // Already have the placeholder, just make sure the name is safe. + param.output_filename = sanitize_filename(param.output_filename, true); + } } if(param.css_filename.empty()) { diff --git a/src/util/path.cc b/src/util/path.cc index ce80a4f..5c8e1d6 100644 --- a/src/util/path.cc +++ b/src/util/path.cc @@ -6,6 +6,7 @@ */ #include +#include #include #include @@ -39,6 +40,21 @@ void create_directories(const string & path) } } +string sanitize_filename(const string & filename, bool allow_single_format_number) +{ + // First, escape all %'s to make safe for use in printf. + string sanitized = std::regex_replace(filename, std::regex("%"), "%%"); + + if(allow_single_format_number) + { + // A single %d or %0xd is allowed in the input. + sanitized = std::regex_replace(sanitized, std::regex("%%([0-9]*)d"), "%$1d", std::regex_constants::format_first_only); + } + + return sanitized; +} + + bool is_truetype_suffix(const string & suffix) { return (suffix == ".ttf") || (suffix == ".ttc") || (suffix == ".otf"); diff --git a/src/util/path.h b/src/util/path.h index 4f82a8e..c16fb91 100644 --- a/src/util/path.h +++ b/src/util/path.h @@ -19,5 +19,16 @@ bool is_truetype_suffix(const std::string & suffix); std::string get_filename(const std::string & path); std::string get_suffix(const std::string & path); +/** + * Function to sanitize a filename so that it can be eventually safely used in a printf statement. + * + * @param filename the filename to be sanitized. + * @param allow_single_form_number boolean flag indicatin if a single format (e.g. %d) should be allowed + * in the filename for use in templating of pages. e.g. page%02d.html is ok. + * + * @return the sanitized filename. + */ +std::string sanitize_filename(const std::string & filename, bool allow_single_format_number); + } //namespace pdf2htmlEX #endif //PATH_H__ diff --git a/test/test_data/1-page.pdf b/test/test_data/1-page.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f5228186f3a942d9a77530e9747bbc9473421c97 GIT binary patch literal 12378 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 sys.stderr, "Command return code %d: %s" % (return_code, ' '.join(cmd)) + + return return_code + +def execute_pdf2htmlex_and_get_files(args): + """ + Execute the pdf2htmlEX with the specified arguments, and get the names of the output files. Will automatically create + a temporary directory for the output, pass that as the output dir to pdf2htmlEX, determine the files generated, and + clean up the temporary directory afterwards. + + :type args: list of values + :param args: list of arguments to pass to executable. First part of each tuple is the argument, second part is the value. + + :rtype: list of str + :return: List of the file names that were generated as output in alphabetical order. None if the command does not execute successfully. + """ + temp_dir = tempfile.mkdtemp() + + try: + if execute_pdf2htmlex_with_args(['--dest-dir', temp_dir] + args) != 0: + return None + + files = os.listdir(temp_dir) + files.sort() + return files + finally: + shutil.rmtree(path=temp_dir, ignore_errors=True) + +def path_to_test_file(filename): + """ + Retrieve an absolute path to the specified test file. + + :type filename: + :param filename: the name of the test file to get the path to + + :rtype: str + :returns: the full path to the test file + """ + return os.path.abspath(os.path.join(os.path.dirname(__file__), TEST_DATA_DIR, filename)) + +class OutputNamingTests(unittest.TestCase): + def test_generate_single_html_default_name_single_page_pdf(self): + files = execute_pdf2htmlex_and_get_files([ + path_to_test_file('1-page.pdf') + ]) + self.assertEquals(files, ['1-page.html']) + + def test_generate_single_html_default_name_multiple_page_pdf(self): + files = execute_pdf2htmlex_and_get_files([ + path_to_test_file('2-pages.pdf') + ]) + self.assertEquals(files, ['2-pages.html']) + + def test_generate_single_html_specify_name_single_page_pdf(self): + files = execute_pdf2htmlex_and_get_files([ + path_to_test_file('1-page.pdf'), + 'foo.html' + ]) + self.assertEquals(files, ['foo.html']) + + def test_generate_single_html_specify_name_multiple_page_pdf(self): + files = execute_pdf2htmlex_and_get_files([ + path_to_test_file('2-pages.pdf'), + 'foo.html' + ]) + self.assertEquals(files, ['foo.html']) + + def test_generate_split_pages_default_name_single_page(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('1-page.pdf') + ]) + self.assertEquals(files, sorted(['1-page.css', '1-page.outline', '1-page1.page'])) + + def test_generate_split_pages_default_name_multiple_pages(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf') + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', '3-pages1.page', '3-pages2.page', '3-pages3.page'])) + + def test_generate_split_pages_specify_name_single_page(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('1-page.pdf'), + 'foo.xyz' + ]) + self.assertEquals(files, sorted(['1-page.css', '1-page.outline', 'foo1.xyz'])) + + def test_generate_split_pages_specify_name_multiple_pages(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf'), + 'foo.xyz' + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'foo1.xyz', 'foo2.xyz', 'foo3.xyz'])) + + def test_generate_split_pages_specify_name_formatter_multiple_pages(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf'), + 'fo%do.xyz' + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'fo1o.xyz', 'fo2o.xyz', 'fo3o.xyz'])) + + def test_generate_split_pages_specify_name_formatter_with_padded_zeros_multiple_pages(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf'), + 'fo%03do.xyz' + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'fo001o.xyz', 'fo002o.xyz', 'fo003o.xyz'])) + + def test_generate_split_pages_specify_name_only_first_formatter_gets_taken(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf'), + 'f%do%do.xyz' + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'f1o%do.xyz', 'f2o%do.xyz', 'f3o%do.xyz'])) + + def test_generate_split_pages_specify_name_only_percent_d_is_used_percent_s(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf'), + 'f%soo.xyz' + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'f%soo1.xyz', 'f%soo2.xyz', 'f%soo3.xyz'])) + + def test_generate_split_pages_specify_name_only_percent_d_is_used_percent_p(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf'), + 'f%poo.xyz' + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'f%poo1.xyz', 'f%poo2.xyz', 'f%poo3.xyz'])) + + + def test_generate_split_pages_specify_name_only_percent_d_is_used_percent_n(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf'), + 'f%noo.xyz' + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'f%noo1.xyz', 'f%noo2.xyz', 'f%noo3.xyz'])) + + def test_generate_split_pages_specify_name_only_percent_d_is_used_percent_percent(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf'), + 'f%%oo.xyz' + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'f%%oo1.xyz', 'f%%oo2.xyz', 'f%%oo3.xyz'])) + + def test_generate_single_html_name_specified_format_characters_percent_d(self): + files = execute_pdf2htmlex_and_get_files([ + path_to_test_file('2-pages.pdf'), + 'foo%d.html' + ]) + self.assertEquals(files, ['foo%d.html']) + + def test_generate_single_html_name_specified_format_characters_percent_p(self): + files = execute_pdf2htmlex_and_get_files([ + path_to_test_file('2-pages.pdf'), + 'foo%p.html' + ]) + self.assertEquals(files, ['foo%p.html']) + + def test_generate_single_html_name_specified_format_characters_percent_n(self): + files = execute_pdf2htmlex_and_get_files([ + path_to_test_file('2-pages.pdf'), + 'foo%n.html' + ]) + self.assertEquals(files, ['foo%n.html']) + + def test_generate_single_html_name_specified_format_characters_percent_percent(self): + files = execute_pdf2htmlex_and_get_files([ + path_to_test_file('2-pages.pdf'), + 'foo%%.html' + ]) + self.assertEquals(files, ['foo%%.html']) + +if __name__=="__main__": + if not os.path.isfile(PDF2HTMLEX_PATH) or not os.access(PDF2HTMLEX_PATH, os.X_OK): + print >> sys.stderr, "Cannot locate pdf2htmlEX executable. Make sure source was built before running this test." + exit(1) + + suite = unittest.loader.TestLoader().loadTestsFromTestCase(OutputNamingTests) + unittest.TextTestRunner(verbosity=2).run(suite) \ No newline at end of file From 3cafb540c62b53e93b5e5c07fa54c96ab6ddd929 Mon Sep 17 00:00:00 2001 From: Ryan Morlok Date: Sun, 17 Mar 2013 23:31:43 -0500 Subject: [PATCH 3/6] updated split page filename formatting to not rely on regex to be compatible with older compilers. added several test cases to account for new implementation. updated documenation to more accurately reflect how split page filenames are generated. --- pdf2htmlEX.1.in | 26 ++++++++++++- src/pdf2htmlEX.cc | 11 +++--- src/util/path.cc | 92 +++++++++++++++++++++++++++++++++++++++++---- src/util/path.h | 17 +++++++-- test/test_naming.py | 18 ++++++++- 5 files changed, 144 insertions(+), 20 deletions(-) diff --git a/pdf2htmlEX.1.in b/pdf2htmlEX.1.in index b0bddfc..c9f7c53 100644 --- a/pdf2htmlEX.1.in +++ b/pdf2htmlEX.1.in @@ -65,12 +65,34 @@ You need to modify the manifest if you do not want outline embedded. .TP .B --split-pages <0|1> (Default: 0) -If turned on, pages will be stored into separated files. By defualt, these files will be named as 0.page, 1.page, ..., however the name of the files can be customized by adding a %d marker in the to specify how the page should be used to generate the name. E.g. p%d.page yeilding p1.page, p2.page ... or p%03d.page yielding p001.page, p002.page etc. Only %d may be used, no other formatting markers. +If turned on, the pages, css, and outline will be stored into separated files and no consolidated .html will be generated. -Also the css and outline will be stored into separated files, and there will be no .html generated. + may be used to specify the format for the filenames for individual pages. may contain a %d placeholder to indicate where the page number should be placed. + +If does not contain a placeholder for the page number, the page number will be inserted directly before the file extension. If the filename does not have an extension, the page number will be placed at the end of the file name. + +If is not specified, will be used for the output filename, replacing the extension with .page and adding the page number directly before the extension. This switch is useful if you want pages to be loaded separately & dynamically -- in which case you need to compose the page yourself, and a supporting backend might be necessary. +.B Examples + +.B pdf2htmlEX --split-pages 1 foo.pdf + + Yields page files foo1.page, foo2.page, etc. + +.B pdf2htmlEX --split-pages 1 foo.pdf bar.baz + + Yields page files bar1.baz, bar2.baz, etc. + +.B pdf2htmlEX --split-pages 1 foo.pdf page%dbar.baz + + Yields page files page1bar.baz, page2bar.baz, etc. + +.B pdf2htmlEX --split-pages 1 foo.pdf bar%03d.baz + + Yields page files bar001.baz, bar002.baz, etc. + .TP .B --dest-dir (Default: .) Specify destination folder diff --git a/src/pdf2htmlEX.cc b/src/pdf2htmlEX.cc index 54f79bb..7c6c7b3 100644 --- a/src/pdf2htmlEX.cc +++ b/src/pdf2htmlEX.cc @@ -10,7 +10,6 @@ #include #include #include -#include #include #include @@ -216,7 +215,7 @@ int main(int argc, char **argv) if(get_suffix(param.input_filename) == ".pdf") { if(param.split_pages) - param.output_filename = sanitize_filename(s.substr(0, s.size() - 4) + "%d.page", true); + param.output_filename = sanitize_filename(s.substr(0, s.size() - 4) + "%d.page"); else param.output_filename = s.substr(0, s.size() - 4) + ".html"; @@ -224,7 +223,7 @@ int main(int argc, char **argv) else { if(param.split_pages) - param.output_filename = sanitize_filename(s + "%d.page", true); + param.output_filename = sanitize_filename(s + "%d.page"); else param.output_filename = s + ".html"; @@ -233,16 +232,16 @@ int main(int argc, char **argv) else if(param.split_pages) { // Need to make sure we have a page number placeholder in the filename - if(!std::regex_match(param.output_filename, std::regex("^.*%[0-9]*d.*$"))) + if(!contains_integer_placeholder(param.output_filename)) { // Inject the placeholder just before the file extension const string suffix = get_suffix(param.output_filename); - param.output_filename = sanitize_filename(param.output_filename.substr(0, param.output_filename.size() - suffix.size()) + "%d" + suffix, true); + param.output_filename = sanitize_filename(param.output_filename.substr(0, param.output_filename.size() - suffix.size()) + "%d" + suffix); } else { // Already have the placeholder, just make sure the name is safe. - param.output_filename = sanitize_filename(param.output_filename, true); + param.output_filename = sanitize_filename(param.output_filename); } } if(param.css_filename.empty()) diff --git a/src/util/path.cc b/src/util/path.cc index 5c8e1d6..eacc16c 100644 --- a/src/util/path.cc +++ b/src/util/path.cc @@ -6,7 +6,6 @@ */ #include -#include #include #include @@ -40,20 +39,99 @@ void create_directories(const string & path) } } -string sanitize_filename(const string & filename, bool allow_single_format_number) +string sanitize_filename(const string & filename) { - // First, escape all %'s to make safe for use in printf. - string sanitized = std::regex_replace(filename, std::regex("%"), "%%"); + string sanitized = string(); + bool format_specifier_found = false; - if(allow_single_format_number) + for(int i = 0; i < filename.size(); i++) { - // A single %d or %0xd is allowed in the input. - sanitized = std::regex_replace(sanitized, std::regex("%%([0-9]*)d"), "%$1d", std::regex_constants::format_first_only); + if('%' == filename[i]) + { + if(format_specifier_found) + { + sanitized.push_back('%'); + sanitized.push_back('%'); + } + else + { + // We haven't found the format specifier yet, so see if we can use this one as a valid formatter + int original_i = i; + string tmp(""); + tmp.push_back('%'); + while(++i < filename.size()) + { + tmp.push_back(filename[i]); + + // If we aren't still in option specifiers, stop looking + if(!strchr("+-#0123456789.", filename[i])) + { + break; + } + } + + // Check to see if we yielded a valid format speifier + if('d' == tmp.back()) + { + // Found a valid integer format + sanitized.append(tmp); + format_specifier_found = true; + } + else + { + // Not a valid format specifier. Just append the protected % + // and keep looking from where we left of in the search + sanitized.push_back('%'); + sanitized.push_back('%'); + i = original_i; + } + } + } + else + { + sanitized.push_back(filename[i]); + } } return sanitized; } +bool contains_integer_placeholder(const string & filename) +{ + for(int i = 0; i < filename.size(); i++) + { + if('%' == filename[i]) + { + int original_i = i; + char last_char = '%'; + while(++i < filename.size()) + { + last_char = filename[i]; + + // If we aren't still in option specifiers, stop looking + if(!strchr("+-#0123456789.", last_char)) + { + break; + } + } + + // Check to see if we yielded a valid format speifier + if('d' == last_char) + { + // Yep. + return true; + } + else + { + // Nope. Resume looking where we left off. + i = original_i; + } + } + } + + return false; +} + bool is_truetype_suffix(const string & suffix) { diff --git a/src/util/path.h b/src/util/path.h index c16fb91..0eccb0c 100644 --- a/src/util/path.h +++ b/src/util/path.h @@ -20,15 +20,24 @@ std::string get_filename(const std::string & path); std::string get_suffix(const std::string & path); /** - * Function to sanitize a filename so that it can be eventually safely used in a printf statement. + * Function to sanitize a filename so that it can be eventually safely used in a printf + * statement. Allows a single %d placeholder, but no other format specifiers. * * @param filename the filename to be sanitized. - * @param allow_single_form_number boolean flag indicatin if a single format (e.g. %d) should be allowed - * in the filename for use in templating of pages. e.g. page%02d.html is ok. * * @return the sanitized filename. */ -std::string sanitize_filename(const std::string & filename, bool allow_single_format_number); +std::string sanitize_filename(const std::string & filename); + +/** + * Function to check if a filename contains at least one %d integer placeholder + * for use in a printf statement. + * + * @param filename the filename to check + * + * @return true if the filename contains an integer placeholder, false otherwise. + */ +bool contains_integer_placeholder(const std::string & filename); } //namespace pdf2htmlEX #endif //PATH_H__ diff --git a/test/test_naming.py b/test/test_naming.py index b56b217..0acbc23 100644 --- a/test/test_naming.py +++ b/test/test_naming.py @@ -190,6 +190,22 @@ class OutputNamingTests(unittest.TestCase): ]) self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'f%%oo1.xyz', 'f%%oo2.xyz', 'f%%oo3.xyz'])) + def test_generate_split_pages_specify_name_only_formatter_starts_part_way_through_invalid_formatter(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf'), + 'f%02%doo.xyz' + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'f%021oo.xyz', 'f%022oo.xyz', 'f%023oo.xyz'])) + + def test_generate_split_pages_specify_output_filename_no_formatter_no_extension(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('1-page.pdf'), + 'foo' + ]) + self.assertEquals(files, sorted(['1-page.css', '1-page.outline', 'foo1'])) + def test_generate_single_html_name_specified_format_characters_percent_d(self): files = execute_pdf2htmlex_and_get_files([ path_to_test_file('2-pages.pdf'), @@ -217,7 +233,7 @@ class OutputNamingTests(unittest.TestCase): 'foo%%.html' ]) self.assertEquals(files, ['foo%%.html']) - + if __name__=="__main__": if not os.path.isfile(PDF2HTMLEX_PATH) or not os.access(PDF2HTMLEX_PATH, os.X_OK): print >> sys.stderr, "Cannot locate pdf2htmlEX executable. Make sure source was built before running this test." From c0972e1baa336792d86aec698d19d113b1c3f7c0 Mon Sep 17 00:00:00 2001 From: Ryan Morlok Date: Sun, 17 Mar 2013 23:51:14 -0500 Subject: [PATCH 4/6] fixed missing include causing ci build to fail; cleaned up warnings about signed/unsigned comparison; cleaned up local string declarations --- src/util/path.cc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/util/path.cc b/src/util/path.cc index eacc16c..7324385 100644 --- a/src/util/path.cc +++ b/src/util/path.cc @@ -8,6 +8,7 @@ #include #include #include +#include #include "path.h" @@ -41,10 +42,10 @@ void create_directories(const string & path) string sanitize_filename(const string & filename) { - string sanitized = string(); + string sanitized; bool format_specifier_found = false; - for(int i = 0; i < filename.size(); i++) + for(uint i = 0; i < filename.size(); i++) { if('%' == filename[i]) { @@ -57,7 +58,7 @@ string sanitize_filename(const string & filename) { // We haven't found the format specifier yet, so see if we can use this one as a valid formatter int original_i = i; - string tmp(""); + string tmp; tmp.push_back('%'); while(++i < filename.size()) { From ccc8ff4761446a10d9946f49456a158ec007755e Mon Sep 17 00:00:00 2001 From: Ryan Morlok Date: Mon, 18 Mar 2013 00:29:30 -0500 Subject: [PATCH 5/6] updated sanitize filename code to use size_t instead of raw number types --- src/util/path.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/util/path.cc b/src/util/path.cc index 7324385..b50de01 100644 --- a/src/util/path.cc +++ b/src/util/path.cc @@ -45,7 +45,7 @@ string sanitize_filename(const string & filename) string sanitized; bool format_specifier_found = false; - for(uint i = 0; i < filename.size(); i++) + for(size_t i = 0; i < filename.size(); i++) { if('%' == filename[i]) { @@ -57,7 +57,7 @@ string sanitize_filename(const string & filename) else { // We haven't found the format specifier yet, so see if we can use this one as a valid formatter - int original_i = i; + size_t original_i = i; string tmp; tmp.push_back('%'); while(++i < filename.size()) @@ -99,11 +99,11 @@ string sanitize_filename(const string & filename) bool contains_integer_placeholder(const string & filename) { - for(int i = 0; i < filename.size(); i++) + for(size_t i = 0; i < filename.size(); i++) { if('%' == filename[i]) { - int original_i = i; + size_t original_i = i; char last_char = '%'; while(++i < filename.size()) { From 6298c19a3b4667d2a775c709749d96a6e77ebc25 Mon Sep 17 00:00:00 2001 From: Ryan Morlok Date: Mon, 18 Mar 2013 01:39:02 -0500 Subject: [PATCH 6/6] combined the sanitize and validate funcitons into a single function; limited the format characters supported to avoid validation complexity; updated documentation; feature implemented by Ryan Morlok (ryan.morlok@morlok.com) on behalf of Docalytics (http://www.docalytics.com/) --- pdf2htmlEX.1.in | 2 +- src/pdf2htmlEX.cc | 24 ++++++++++++++--------- src/util/path.cc | 47 ++++++++------------------------------------- src/util/path.h | 20 +++++-------------- test/test_naming.py | 16 +++++++++++++++ 5 files changed, 45 insertions(+), 64 deletions(-) diff --git a/pdf2htmlEX.1.in b/pdf2htmlEX.1.in index c9f7c53..1452b81 100644 --- a/pdf2htmlEX.1.in +++ b/pdf2htmlEX.1.in @@ -67,7 +67,7 @@ You need to modify the manifest if you do not want outline embedded. .B --split-pages <0|1> (Default: 0) If turned on, the pages, css, and outline will be stored into separated files and no consolidated .html will be generated. - may be used to specify the format for the filenames for individual pages. may contain a %d placeholder to indicate where the page number should be placed. + may be used to specify the format for the filenames for individual pages. may contain a %d placeholder to indicate where the page number should be placed. The placeholder supports a limited subset of normal numerical placeholders, including specified width and zero padding. If does not contain a placeholder for the page number, the page number will be inserted directly before the file extension. If the filename does not have an extension, the page number will be placed at the end of the file name. diff --git a/src/pdf2htmlEX.cc b/src/pdf2htmlEX.cc index 7c6c7b3..64d27b2 100644 --- a/src/pdf2htmlEX.cc +++ b/src/pdf2htmlEX.cc @@ -215,33 +215,39 @@ int main(int argc, char **argv) if(get_suffix(param.input_filename) == ".pdf") { if(param.split_pages) - param.output_filename = sanitize_filename(s.substr(0, s.size() - 4) + "%d.page"); + { + param.output_filename = s.substr(0, s.size() - 4) + "%d.page"; + sanitize_filename(param.output_filename); + } else + { param.output_filename = s.substr(0, s.size() - 4) + ".html"; + } } else { if(param.split_pages) - param.output_filename = sanitize_filename(s + "%d.page"); + { + param.output_filename = s + "%d.page"; + sanitize_filename(param.output_filename); + } else + { param.output_filename = s + ".html"; + } } } else if(param.split_pages) { // Need to make sure we have a page number placeholder in the filename - if(!contains_integer_placeholder(param.output_filename)) + if(!sanitize_filename(param.output_filename)) { // Inject the placeholder just before the file extension const string suffix = get_suffix(param.output_filename); - param.output_filename = sanitize_filename(param.output_filename.substr(0, param.output_filename.size() - suffix.size()) + "%d" + suffix); - } - else - { - // Already have the placeholder, just make sure the name is safe. - param.output_filename = sanitize_filename(param.output_filename); + param.output_filename = param.output_filename.substr(0, param.output_filename.size() - suffix.size()) + "%d" + suffix; + sanitize_filename(param.output_filename); } } if(param.css_filename.empty()) diff --git a/src/util/path.cc b/src/util/path.cc index b50de01..bc71128 100644 --- a/src/util/path.cc +++ b/src/util/path.cc @@ -40,7 +40,7 @@ void create_directories(const string & path) } } -string sanitize_filename(const string & filename) +bool sanitize_filename(string & filename) { string sanitized; bool format_specifier_found = false; @@ -65,13 +65,13 @@ string sanitize_filename(const string & filename) tmp.push_back(filename[i]); // If we aren't still in option specifiers, stop looking - if(!strchr("+-#0123456789.", filename[i])) + if(!strchr("0123456789", filename[i])) { break; } } - // Check to see if we yielded a valid format speifier + // Check to see if we yielded a valid format specifier if('d' == tmp.back()) { // Found a valid integer format @@ -93,46 +93,15 @@ string sanitize_filename(const string & filename) sanitized.push_back(filename[i]); } } - - return sanitized; -} -bool contains_integer_placeholder(const string & filename) -{ - for(size_t i = 0; i < filename.size(); i++) + // Only sanitize if it is a valid format. + if(format_specifier_found) { - if('%' == filename[i]) - { - size_t original_i = i; - char last_char = '%'; - while(++i < filename.size()) - { - last_char = filename[i]; - - // If we aren't still in option specifiers, stop looking - if(!strchr("+-#0123456789.", last_char)) - { - break; - } - } - - // Check to see if we yielded a valid format speifier - if('d' == last_char) - { - // Yep. - return true; - } - else - { - // Nope. Resume looking where we left off. - i = original_i; - } - } + filename.assign(sanitized); } - - return false; -} + return format_specifier_found; +} bool is_truetype_suffix(const string & suffix) { diff --git a/src/util/path.h b/src/util/path.h index 0eccb0c..2a2a685 100644 --- a/src/util/path.h +++ b/src/util/path.h @@ -20,24 +20,14 @@ std::string get_filename(const std::string & path); std::string get_suffix(const std::string & path); /** - * Function to sanitize a filename so that it can be eventually safely used in a printf - * statement. Allows a single %d placeholder, but no other format specifiers. + * Sanitize all occurrences of '%' except for the first valid format specifier. Filename + * is only sanitized if a formatter is found, and the function returns true. * - * @param filename the filename to be sanitized. + * @param filename the filename to be sanitized. Value will be modified. * - * @return the sanitized filename. + * @return true if a format specifier was found, false otherwise. */ -std::string sanitize_filename(const std::string & filename); - -/** - * Function to check if a filename contains at least one %d integer placeholder - * for use in a printf statement. - * - * @param filename the filename to check - * - * @return true if the filename contains an integer placeholder, false otherwise. - */ -bool contains_integer_placeholder(const std::string & filename); +bool sanitize_filename(std::string & filename); } //namespace pdf2htmlEX #endif //PATH_H__ diff --git a/test/test_naming.py b/test/test_naming.py index 0acbc23..d061e94 100644 --- a/test/test_naming.py +++ b/test/test_naming.py @@ -190,6 +190,22 @@ class OutputNamingTests(unittest.TestCase): ]) self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'f%%oo1.xyz', 'f%%oo2.xyz', 'f%%oo3.xyz'])) + def test_generate_split_pages_specify_name_only_percent_d_is_used_percent_percent_with_actual_placeholder(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf'), + 'f%%o%do.xyz' + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'f%%o1o.xyz', 'f%%o2o.xyz', 'f%%o3o.xyz'])) + + def test_generate_split_pages_specify_name_only_percent_d_is_used_percent_percent_with_actual_placeholder(self): + files = execute_pdf2htmlex_and_get_files([ + '--split-pages', 1, + path_to_test_file('3-pages.pdf'), + 'fo%do%%.xyz' + ]) + self.assertEquals(files, sorted(['3-pages.css', '3-pages.outline', 'fo1o%%.xyz', 'fo2o%%.xyz', 'fo3o%%.xyz'])) + def test_generate_split_pages_specify_name_only_formatter_starts_part_way_through_invalid_formatter(self): files = execute_pdf2htmlex_and_get_files([ '--split-pages', 1,