diff options
| -rw-r--r-- | src/pdf_formatter.cc | 286 | ||||
| -rw-r--r-- | src/pdf_formatter.h | 81 | ||||
| -rw-r--r-- | src/wscript | 5 | ||||
| -rw-r--r-- | tools/dcpverify.cc | 24 | ||||
| -rw-r--r-- | tools/wscript | 2 | ||||
| -rw-r--r-- | wscript | 16 |
6 files changed, 411 insertions, 3 deletions
diff --git a/src/pdf_formatter.cc b/src/pdf_formatter.cc new file mode 100644 index 00000000..5eb1772c --- /dev/null +++ b/src/pdf_formatter.cc @@ -0,0 +1,286 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "exceptions.h" +#include "pdf_formatter.h" +#include <fmt/format.h> + + +using std::unique_ptr; +using namespace dcp; + + +int constexpr dpi = 72; + +/* These are in inches */ +float constexpr page_width = 8.27; +float constexpr horizontal_margin = 0.5; +float constexpr page_height = 11.69; +float constexpr vertical_margin = 1.0; + + +static +void +error_handler(HPDF_STATUS error, HPDF_STATUS detail, void*) +{ + throw MiscError(fmt::format("Could not create PDF {} {}", error, detail)); +} + + +PDFFormatter::PDFFormatter(boost::filesystem::path file) + : _file(file) + , _pdf(HPDF_New(error_handler, nullptr)) + , _normal_font(HPDF_GetFont(_pdf, "Helvetica", nullptr)) + , _fixed_font(HPDF_GetFont(_pdf, "Courier", nullptr)) + , _bold_font(HPDF_GetFont(_pdf, "Helvetica-Bold", nullptr)) +{ + add_page(); +} + + +PDFFormatter::~PDFFormatter() +{ + HPDF_Free(_pdf); +} + + +void +PDFFormatter::add_page() +{ + _page = HPDF_AddPage(_pdf); + HPDF_Page_Concat(_page, 1, 0, 0, 1, horizontal_margin * dpi, (page_height - vertical_margin) * dpi); + _y = 0; +} + + +void +PDFFormatter::heading(std::string const& text) +{ + move_down(16 * 1.4); + HPDF_Page_SetFontAndSize(_page, _bold_font, 20); + HPDF_Page_SetRGBFill(_page, 0, 0, 0); + HPDF_Page_BeginText(_page); + HPDF_Page_TextOut(_page, 0, _y, text.c_str()); + HPDF_Page_EndText(_page); + move_down(20 * 1.4); +} + + +void +PDFFormatter::subheading(std::string const& text) +{ + move_down(12 * 1.4); + HPDF_Page_SetFontAndSize(_page, _bold_font, 16); + HPDF_Page_SetRGBFill(_page, 0, 0, 0); + HPDF_Page_BeginText(_page); + HPDF_Page_TextOut(_page, 0, _y, text.c_str()); + HPDF_Page_EndText(_page); + move_down(16 * 1.4); +} + + +unique_ptr<Formatter::Wrap> +PDFFormatter::unordered_list() +{ + return unique_ptr<PDFFormatter::Wrap>(new PDFFormatter::Wrap(this)); +} + + +void +PDFFormatter::move_down(float spacing) +{ + _y -= spacing; + if (_y < ((-page_height + vertical_margin * 2) * dpi)) { + add_page(); + } +} + + +void +PDFFormatter::wrapped_text(float x, float first_line_indent, float font_size, dcp::Colour colour, std::string const& text, float width, float line_spacing) +{ + class Block + { + public: + enum class Style { + NORMAL, + FIXED + }; + + Block(std::string text_, Style style_) + : text(text_) + , style(style_) + {} + + std::string text; + Style style; + }; + + std::map<std::string, Block::Style> tags = { + { "[fixed]", Block::Style::FIXED }, + { "[/fixed]", Block::Style::NORMAL } + }; + + std::vector<Block> blocks; + Block block("", Block::Style::NORMAL); + auto work = text; + while (true) { + std::pair<std::string, Block::Style> found_tag; + auto found_start = std::string::npos; + + for (auto const& tag: tags) { + auto const start = work.find(tag.first); + if (start < found_start) { + found_tag = tag; + found_start = start; + } + } + + if (found_start == std::string::npos) { + break; + } + + if (found_start > 0) { + block.text += work.substr(0, found_start); + blocks.push_back(block); + block = Block("", found_tag.second); + } + work = work.substr(found_start + found_tag.first.length()); + } + + if (!work.empty()) { + block.text += work; + blocks.push_back(block); + } + + std::map<Block::Style, HPDF_Font*> fonts = { + { Block::Style::NORMAL, &_normal_font }, + { Block::Style::FIXED, &_fixed_font } + }; + + auto px = x + first_line_indent; + for (auto const& block: blocks) { + + int offset = 0; + int left = block.text.length(); + while (left) { + HPDF_Page_SetFontAndSize(_page, *fonts[block.style], font_size); + + HPDF_REAL text_width; + int fits = HPDF_Font_MeasureText( + *fonts[block.style], + reinterpret_cast<unsigned char const*>(block.text.substr(offset).c_str()), + block.text.length() - offset, + width - px, + font_size, + 0, + 0, + true, + &text_width); + + if (fits == 0) { + /* Try again without word-wrap */ + fits = HPDF_Font_MeasureText( + *fonts[block.style], + reinterpret_cast<unsigned char const*>(block.text.substr(offset).c_str()), + block.text.length() - offset, + width - px, + font_size, + 0, + 0, + false, + &text_width); + } + + HPDF_Page_SetRGBFill(_page, colour.r / 255.0, colour.g / 255.0, colour.b / 255.0); + HPDF_Page_BeginText(_page); + HPDF_Page_TextOut(_page, px, _y, block.text.substr(offset, fits).c_str()); + HPDF_Page_EndText(_page); + if (fits < static_cast<int>(block.text.length()) - offset) { + px = x; + move_down(line_spacing); + } else { + px += text_width; + } + + offset += fits; + left -= fits; + } + } + + move_down(line_spacing); +} + + +void +PDFFormatter::list_item(std::string const& text, boost::optional<std::string> type) +{ + float const indent = 16 * _indent; + float constexpr dot_radius = 1.5; + float constexpr font_size = 10; + + HPDF_Page_Circle(_page, indent + dot_radius, _y + font_size / 3, dot_radius); + HPDF_Page_Fill(_page); + + dcp::Colour colour(0, 0, 0); + if (type && *type == "ok") { + colour = dcp::Colour(0, 153, 0); + } else if (type && *type == "warning") { + colour = dcp::Colour(255, 127, 102); + } else if (type && (*type == "error" || *type == "bv21-error")) { + colour = dcp::Colour(153, 0, 0); + } + + wrapped_text(indent, dot_radius * 6, font_size, colour, text, (page_width - horizontal_margin * 2) * dpi, font_size * 1.2); +} + + +std::function<std::string (std::string)> +PDFFormatter::process_string() +{ + return [](std::string s) { + return s; + }; +} + + +std::function<std::string (std::string)> +PDFFormatter::fixed_width() +{ + return [](std::string s) { + return String::compose("[fixed]%1[/fixed]", s); + }; +} + + +void +PDFFormatter::finish() +{ + HPDF_SaveToFile(_pdf, _file.string().c_str()); +} + + +void +PDFFormatter::indent(int amount) +{ + _indent += amount; +} + diff --git a/src/pdf_formatter.h b/src/pdf_formatter.h new file mode 100644 index 00000000..b7d0213d --- /dev/null +++ b/src/pdf_formatter.h @@ -0,0 +1,81 @@ +/* + Copyright (C) 2025 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "verify_report.h" +#include <hpdf.h> + + +namespace dcp { + + +class PDFFormatter : public Formatter +{ +public: + PDFFormatter(boost::filesystem::path file); + ~PDFFormatter(); + + class Wrap : public Formatter::Wrap + { + public: + Wrap(PDFFormatter* formatter) + : _formatter(formatter) + { + _formatter->indent(1); + } + + ~Wrap() + { + _formatter->indent(-1); + } + + private: + PDFFormatter* _formatter; + }; + + void heading(std::string const& text) override; + void subheading(std::string const& text) override; + std::unique_ptr<Formatter::Wrap> unordered_list() override; + void list_item(std::string const& text, boost::optional<std::string> type = {}) override; + std::function<std::string (std::string)> process_string() override; + std::function<std::string (std::string)> fixed_width() override; + void finish() override; + + void indent(int amount); + +private: + void add_page(); + void move_down(float spacing); + void wrapped_text(float x, float first_line_indent, float font_size, dcp::Colour colour, std::string const& text, float width, float line_spacing); + + boost::filesystem::path _file; + HPDF_Doc _pdf; + HPDF_Page _page; + float _y; + HPDF_Font _normal_font; + HPDF_Font _fixed_font; + HPDF_Font _bold_font; + + int _indent = 0; +}; + + +} + diff --git a/src/wscript b/src/wscript index 3e3b75b8..6176ab53 100644 --- a/src/wscript +++ b/src/wscript @@ -265,6 +265,11 @@ def build(bld): uselib = 'BOOST_FILESYSTEM BOOST_SIGNALS2 BOOST_DATETIME OPENSSL SIGC++ LIBXML++ OPENJPEG CXML XMLSEC1 ASDCPLIB_DCPOMATIC XERCES AVCODEC AVUTIL FMT FAST_FLOAT' + if bld.env.LIBDCP_HAVE_HARU: + source += "pdf_formatter.cc " + headers += "pdf_formatter.h " + uselib += ' HARU' + # Main library if bld.env.STATIC: obj = bld(features='cxx cxxstlib') diff --git a/tools/dcpverify.cc b/tools/dcpverify.cc index bff3fde3..8966c3ca 100644 --- a/tools/dcpverify.cc +++ b/tools/dcpverify.cc @@ -36,6 +36,9 @@ #include "compose.hpp" #include "filesystem.h" #include "html_formatter.h" +#ifdef LIBDCP_HAVE_HARU +#include "pdf_formatter.h" +#endif #include "raw_convert.h" #include "text_formatter.h" #include "verify.h" @@ -72,7 +75,12 @@ help (string n) << " --no-asset-hash-check don't check asset hashes\n" << " --asset-hash-check-maximum-size <size-in-MB> only check hashes for assets smaller than this size (in MB)\n" << " --no-picture-details-check don't check details of picture assets (J2K bitstream etc.)\n" - << " -o <filename> write HTML report to filename\n" + << " -o <filename> write report to filename " +#ifdef LIBDCP_HAVE_HARU + " (.txt, .htm, .html or .pdf)\n" +#else + " (.txt, .htm or .html)\n" +#endif << " -q, --quiet don't report progress\n"; } @@ -228,8 +236,18 @@ main (int argc, char* argv[]) } if (report_filename) { - dcp::HTMLFormatter formatter(*report_filename); - dcp::verify_report({ result }, formatter); + if (report_filename->extension() == ".htm" || report_filename->extension() == ".html") { + dcp::HTMLFormatter formatter(*report_filename); + dcp::verify_report({ result }, formatter); +#ifdef LIBDCP_HAVE_HARU + } else if (report_filename->extension() == ".pdf") { + dcp::PDFFormatter formatter(*report_filename); + dcp::verify_report({ result }, formatter); +#endif + } else { + dcp::TextFormatter formatter(*report_filename); + dcp::verify_report({ result }, formatter); + } } exit (failed ? EXIT_FAILURE : EXIT_SUCCESS); diff --git a/tools/wscript b/tools/wscript index a5fcf545..10192f50 100644 --- a/tools/wscript +++ b/tools/wscript @@ -33,6 +33,8 @@ def build(bld): uselib = 'OPENJPEG CXML OPENMP ASDCPLIB_DCPOMATIC BOOST_FILESYSTEM LIBXML++ XMLSEC1 OPENSSL XERCES DL MAGICK AVCODEC AVUTIL FMT FAST_FLOAT' + if bld.env.LIBDCP_HAVE_HARU: + uselib += ' HARU' for f in ['diff', 'info', 'verify']: obj = bld(features='cxx cxxprogram') @@ -271,6 +271,20 @@ def configure(conf): conf.check_cfg(package='fmt', args='--cflags --libs', uselib_store='FMT', mandatory=True) conf.check_cxx(header_name="fast_float/fast_float.h", uselib_store='FAST_FLOAT', mandatory=True) + haru = conf.check_cc(fragment=""" + #include "hpdf.h"\n + int main() { HPDF_Doc pdf; return 0; }\n + """, + msg='Checking for haru library', + libpath='/usr/local/lib', + lib=['hpdf'], + uselib_store='HARU', + define_name='LIBDCP_HAVE_HARU', + mandatory=False) + + if haru: + conf.env.append_value('LIBDCP_HAVE_HARU', '1') + if not conf.env.DISABLE_TESTS: conf.recurse('test') if conf.options.enable_gcov: @@ -288,6 +302,8 @@ def build(bld): boost_lib_suffix = '' libs="-L${libdir} -ldcp%s -lcxml -lboost_system%s" % (bld.env.API_VERSION, boost_lib_suffix) + if bld.env.HAVE_HARU: + libs += " -lhpdf" if bld.env.TARGET_LINUX: libs += " -ldl" |
