summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2025-09-16 17:39:08 +0200
committerCarl Hetherington <cth@carlh.net>2025-09-19 13:37:28 +0200
commit5f090b58fc2a7742c3a924387416fa628f39477d (patch)
tree7d9706650ab49666290e49d5cce5048a6b537afe
parentbf59fbc84f5b156c9b42511f2075bf6112a46e4e (diff)
Add PDF formatter for verification reports (#1823).v1.10.28
-rw-r--r--src/pdf_formatter.cc286
-rw-r--r--src/pdf_formatter.h81
-rw-r--r--src/wscript5
-rw-r--r--tools/dcpverify.cc24
-rw-r--r--tools/wscript2
-rw-r--r--wscript16
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')
diff --git a/wscript b/wscript
index c162011b..052bb014 100644
--- a/wscript
+++ b/wscript
@@ -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"