lmi-commits
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[lmi-commits] [lmi] gwc-no-xslfo a78b402 2/2: Import remaining new files


From: Greg Chicares
Subject: [lmi-commits] [lmi] gwc-no-xslfo a78b402 2/2: Import remaining new files verbatim from vz-no-xslfo
Date: Sat, 27 Jan 2018 02:09:54 -0500 (EST)

branch: gwc-no-xslfo
commit a78b402a87d2954de9bc8bf98ecf0ccbe75cb294
Author: Gregory W. Chicares <address@hidden>
Commit: Gregory W. Chicares <address@hidden>

    Import remaining new files verbatim from vz-no-xslfo
---
 html.cpp                    |  128 ++
 html.hpp                    |  327 +++++
 interpolate_string.cpp      |  296 +++++
 interpolate_string.hpp      |   67 +
 interpolate_string_test.cpp |  233 ++++
 ledger_evaluator.cpp        |  864 +++++++++++++
 ledger_evaluator.hpp        |   59 +
 ledger_pdf.cpp              |   44 +
 ledger_pdf.hpp              |   35 +
 ledger_pdf_generator.cpp    |   46 +
 ledger_pdf_generator.hpp    |   63 +
 ledger_pdf_generator_wx.cpp | 2940 +++++++++++++++++++++++++++++++++++++++++++
 output_mode.hpp             |   35 +
 pdf_writer_wx.cpp           |  249 ++++
 pdf_writer_wx.hpp           |  100 ++
 15 files changed, 5486 insertions(+)

diff --git a/html.cpp b/html.cpp
new file mode 100644
index 0000000..95a68a9
--- /dev/null
+++ b/html.cpp
@@ -0,0 +1,128 @@
+// Utilities for representing and generating HTML.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "html.hpp"
+
+#include <cstring>
+
+namespace html
+{
+
+namespace attr
+{
+
+extern attribute const align        ("align");
+extern attribute const cellpadding  ("cellpadding");
+extern attribute const cellspacing  ("cellspacing");
+extern attribute const colspan      ("colspan");
+extern attribute const nowrap       ("nowrap");
+extern attribute const size         ("size");
+extern attribute const valign       ("valign");
+extern attribute const width        ("width");
+
+} // namespace attr
+
+namespace tag
+{
+
+extern element      const b         ("b");
+extern void_element const br        ("br");
+extern element      const font      ("font");
+extern element      const i         ("i");
+extern element      const p         ("p");
+extern element      const table     ("table");
+extern element      const td        ("td");
+extern element      const tr        ("tr");
+
+} // namespace tag
+
+std::string attribute::as_string() const
+{
+    std::string s(name_);
+    if(!value_.empty())
+        {
+        s += "=";
+        // TODO: Escape quotes.
+        s += value_;
+        }
+    return s;
+}
+
+namespace detail
+{
+
+std::string any_element::get_start() const
+{
+    std::string s("<");
+    // Extra +1 for the space before attributes, even if it's not needed.
+    s.reserve(1 + std::strlen(name_) + 1 + attributes_.length() + 1);
+    s += name_;
+    if(!attributes_.empty())
+        {
+        s += " ";
+        s += attributes_;
+        }
+    s += ">";
+    return s;
+}
+
+void any_element::update_attributes(attribute const& attr)
+{
+    if(attributes_.empty())
+        {
+        attributes_ = attr.as_string();
+        }
+    else
+        {
+        attributes_ += " ";
+        attributes_ += attr.as_string();
+        }
+}
+
+} // namespace detail
+
+void element::update_contents(std::string&& contents)
+{
+    if(contents_.empty())
+        {
+        contents_ = std::move(contents);
+        }
+    else
+        {
+        contents_ += contents;
+        }
+}
+
+element::operator text() const
+{
+    std::string s(get_start());
+    s.reserve(s.length() + contents_.length() + 2 + std::strlen(name_) + 1);
+    s += contents_;
+    s += "</";
+    s += name_;
+    s += ">";
+
+    return text::from_html(std::move(s));
+}
+
+} // namespace html
diff --git a/html.hpp b/html.hpp
new file mode 100644
index 0000000..552651b
--- /dev/null
+++ b/html.hpp
@@ -0,0 +1,327 @@
+// Utilities for representing and generating HTML.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef html_hpp
+#define html_hpp
+
+#include "config.hpp"
+
+#include <string>
+#include <utility>                      // std::move
+
+/// Namespace for helpers used for HTML generation.
+///
+/// Main idea is to avoid generating HTML using raw strings, which is error
+/// prone and difficult to read and maintain. One source of errors is
+/// forgetting to escape special characters, such as "<" or "&", and html::text
+/// class helps with this by providing from() method doing it automatically.
+///
+/// Another one is forgetting to close a tag (or closing a wrong one) and while
+/// html::text is too low level to help with this, html::element can be used
+/// for structured HTML generation, which guarantees that the result is
+/// well-formed. By using predefined constants in html::tag and html::attr
+/// namespaces, typos in the element names can also be automatically avoided.
+namespace html
+{
+
+/// Represents a piece of text containing HTML.
+///
+/// This is a separate type for type safety, e.g. to avoid passing raw,
+/// unescaped, strings to a function expecting HTML (or, less catastrophically,
+/// but still wrongly, passing already escaped HTML to a function doing
+/// escaping internally).
+///
+/// As it still needs to be converted to a string sooner or later to be really
+/// used, it does provide a conversion -- but it can be used only once.
+class text
+{
+  public:
+    // This type has value semantics.
+    text() = default;
+    text(text const&) = default;
+    text(text&&) = default;
+    text& operator=(text const&) = default;
+    text& operator=(text&&) = default;
+
+    /// Escape special XML characters in the given string, ensuring that it
+    /// appears correctly inside HTML element contents. Notice that we don't
+    /// need to escape quotes here as we never use the result of this function
+    /// inside an HTML attribute, only inside HTML elements.
+    static text from(std::string const& s)
+    {
+        std::string z;
+        z.reserve(s.length());
+        for(auto const& c : s)
+            {
+            switch(c)
+                {
+                case '<': z += "&lt;" ; break;
+                case '>': z += "&gt;" ; break;
+                case '&': z += "&amp;"; break;
+                default : z += c      ;
+                }
+            }
+
+        return text{std::move(z)};
+    }
+
+    /// Use the given string with HTML inside it directly. No escaping is done
+    /// by this ctor.
+    static text from_html(std::string s)
+    {
+        return text{std::move(s)};
+    }
+
+    /// Just a symbolic name for a non breaking space HTML entiry.
+    static text nbsp()
+    {
+        return text::from_html("&nbsp;");
+    }
+
+    /// Append another text fragment to this one.
+    ///
+    /// This method allows chained invocation for appending more than one
+    /// fragment at once.
+    text& operator+=(text const& t)
+    {
+        m_html += t.m_html;
+
+        return *this;
+    }
+
+    std::string const& as_html() const&
+    {
+        return m_html;
+    }
+
+    std::string&& as_html() &&
+    {
+        return std::move(m_html);
+    }
+
+  private:
+    // This move ctor is private and does not perform any escaping.
+    explicit text(std::string&& html)
+        :m_html{html}
+    {
+    }
+
+    std::string m_html;
+};
+
+/// Represents a single attribute of an HTML element.
+class attribute
+{
+  public:
+    explicit attribute(char const* name)
+        :name_{name}
+    {
+    }
+
+    attribute operator()(std::string value) const
+    {
+        return attribute(name_, std::move(value));
+    }
+
+    std::string as_string() const;
+
+  private:
+    attribute(char const* name, std::string&& value)
+        :name_{name}
+        ,value_{std::move(value)}
+    {
+    }
+
+    char const* const name_;
+    std::string const value_;
+};
+
+namespace detail
+{
+
+class any_element
+{
+  public:
+    /// Ctor should only be used with literal strings as argument.
+    explicit any_element(char const* name)
+        :name_(name)
+    {
+    }
+
+  protected:
+    // Return the opening tag of the element, with attributes, if any.
+    std::string get_start() const;
+
+    // Add the given attribute to our attributes string.
+    void update_attributes(attribute const& attr);
+
+    char const* const name_;
+
+  private:
+    std::string       attributes_;
+};
+
+} // namespace detail
+
+/// Represents a normal HTML element which can have content inside it.
+///
+/// This class uses the so called fluent API model in which calls to its
+/// different methods return the object itself and so can be chained together.
+/// For example (assuming an implicit "using namespace html"):
+///
+///     auto para_with_link =
+///         tag::p[attr::align("center")]
+///             (text("Link to "))
+///             (tag::a[attr::href("http://lmi.nongnu.org/";)]
+///                 (text::from("lmi project page"))
+///             )
+///         ;
+
+class element : private detail::any_element
+{
+  public:
+    /// Ctor should only be used with literal strings as argument.
+    explicit element(char const* name)
+        :detail::any_element(name)
+    {
+    }
+
+    element(element const&) = default;
+    element(element&&) = default;
+
+    /// Add an attribute.
+    element operator[](attribute const& attr) const&
+    {
+        element e{*this};
+        e.update_attributes(attr);
+        return e;
+    }
+
+    element&& operator[](attribute const& attr) &&
+    {
+        update_attributes(attr);
+        return std::move(*this);
+    }
+
+    /// Add inner contents.
+    element operator()(text contents) const&
+    {
+        element e{*this};
+        e.update_contents(std::move(contents).as_html());
+        return e;
+    }
+
+    element&& operator()(text contents) &&
+    {
+        update_contents(std::move(contents).as_html());
+        return std::move(*this);
+    }
+
+    /// Convert to HTML text with this element and its contents.
+    ///
+    /// This implicit conversion operator is not really dangerous as it is
+    /// normal to represent an HTML element as HTML text and it's very
+    /// convenient to have it as it allows to accept either another element or
+    /// text in our own operator() and also use operator+() defined below to
+    /// concatenate HTML elements without having to convert them to text
+    /// beforehand.
+    operator text() const;
+
+  private:
+    void update_contents(std::string&& contents);
+
+    std::string contents_;
+};
+
+/// Represents a void HTML element which can't have anything inside it.
+class void_element : private detail::any_element
+{
+  public:
+    explicit void_element(char const* name)
+        :detail::any_element(name)
+    {
+    }
+
+    void_element(void_element const&) = default;
+    void_element(void_element&&) = default;
+
+    void_element operator[](attribute const& attr) const&
+    {
+        void_element e{*this};
+        e.update_attributes(std::move(attr));
+        return e;
+    }
+
+    void_element&& operator[](attribute const& attr) &&
+    {
+        update_attributes(std::move(attr));
+        return std::move(*this);
+    }
+
+    operator text() const
+    {
+        return text::from_html(get_start());
+    }
+};
+
+/// Namespace for HTML attributes.
+
+namespace attr
+{
+
+extern attribute const align;
+extern attribute const cellpadding;
+extern attribute const cellspacing;
+extern attribute const colspan;
+extern attribute const nowrap;
+extern attribute const size;
+extern attribute const valign;
+extern attribute const width;
+
+} // namespace attr
+
+/// Namespace for HTML tags.
+
+namespace tag
+{
+
+extern element      const b;
+extern void_element const br;
+extern element      const font;
+extern element      const i;
+extern element      const p;
+extern element      const table;
+extern element      const td;
+extern element      const tr;
+
+} // namespace tag
+
+inline
+text operator+(text t1, text const& t2)
+{
+    text t{std::move(t1)};
+    t += t2;
+    return t;
+}
+
+} // namespace html
+
+#endif // html_hpp
diff --git a/interpolate_string.cpp b/interpolate_string.cpp
new file mode 100644
index 0000000..28f6f4e
--- /dev/null
+++ b/interpolate_string.cpp
@@ -0,0 +1,296 @@
+// Interpolate string containing embedded variable references.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "interpolate_string.hpp"
+
+#include "alert.hpp"
+
+#include <stack>
+#include <stdexcept>
+#include <vector>
+
+namespace
+{
+
+// Information about a single section that we're currently in.
+struct section_info
+{
+    section_info(std::string const& name, bool active)
+        :name_(name)
+        ,active_(active)
+    {
+    }
+
+    // Name of the section, i.e. the part after "#".
+    //
+    // TODO: In C++14 this could be replaced with string_view which would
+    // save on memory allocations without compromising safety, as we know
+    // that the input string doesn't change during this function execution.
+    std::string const name_;
+
+    // If true, output section contents, otherwise simply eat it.
+    bool const active_;
+
+    // Note: we could also store the position of the section start here to
+    // improve error reporting. Currently this is done as templates we use
+    // are small and errors shouldn't be difficult to find even without the
+    // exact position, but this could change in the future.
+};
+
+// The only context we need is the stack of sections entered so far.
+using context = std::stack<section_info, std::vector<section_info>>;
+
+// The real interpolation recursive function, called by the public one to do
+// all the work.
+void do_interpolate_string_in_context
+    (char const* s
+    ,lookup_function const& lookup
+    ,std::string& out
+    ,context& sections
+    ,std::string const& partial = std::string()
+    ,int recursion_level = 0
+    )
+{
+    // Guard against too deep recursion to avoid crashing on code using too
+    // many nested partials (either unintentionally, e.g. due to including a
+    // partial from itself, or maliciously).
+    //
+    // The maximum recursion level is chosen completely arbitrarily, the only
+    // criteria are that it shouldn't be too big to crash due to stack overflow
+    // before it is reached nor too small to break legitimate use cases.
+    if(recursion_level >= 100)
+        {
+        alarum()
+            << "Nesting level too deep while expanding the partial \""
+            << partial
+            << "\""
+            << std::flush
+            ;
+        }
+
+    // Check if the output is currently active or suppressed because we're
+    // inside an inactive section.
+    auto const is_active = [&sections]()
+        {
+            return sections.empty() || sections.top().active_;
+        };
+
+    for(char const* p = s; *p; ++p)
+        {
+        // As we know that the string is NUL-terminated, it is safe to check
+        // the next character.
+        if(p[0] == '{' && p[1] == '{')
+            {
+            std::string name;
+            auto const pos_start = p - s + 1;
+            for(p += 2;; ++p)
+                {
+                if(*p == '\0')
+                    {
+                    alarum()
+                        << "Unmatched opening brace at position "
+                        << pos_start
+                        << std::flush
+                        ;
+                    }
+
+                if(p[0] == '}' && p[1] == '}')
+                    {
+                    switch(name.empty() ? '\0' : name[0])
+                        {
+                        case '#':
+                        case '^':
+                            {
+                            auto const real_name = name.substr(1);
+                            // If we're inside a disabled section, it doesn't
+                            // matter whether this one is active or not.
+                            bool active = is_active();
+                            if(active)
+                                {
+                                auto const value = lookup
+                                    (real_name
+                                    ,interpolate_lookup_kind::section
+                                    );
+                                if(value == "1")
+                                    {
+                                    active = true;
+                                    }
+                                else if(value == "0")
+                                    {
+                                    active = false;
+                                    }
+                                else
+                                    {
+                                    alarum()
+                                        << "Invalid value '"
+                                        << value
+                                        << "' of section '"
+                                        << real_name
+                                        << "' at position "
+                                        << pos_start
+                                        << ", only \"0\" or \"1\" allowed"
+                                        << std::flush
+                                        ;
+                                    }
+
+                                if(name[0] == '^')
+                                    {
+                                    active = !active;
+                                    }
+                                }
+
+                            sections.emplace(real_name, active);
+                            }
+                            break;
+
+                        case '/':
+                            if(sections.empty())
+                                {
+                                alarum()
+                                    << "Unexpected end of section '"
+                                    << name.substr(1)
+                                    << "' at position "
+                                    << pos_start
+                                    << " without previous section start"
+                                    << std::flush
+                                    ;
+                                }
+                            if(name.compare(1, std::string::npos, 
sections.top().name_) != 0)
+                                {
+                                alarum()
+                                    << "Unexpected end of section '"
+                                    << name.substr(1)
+                                    << "' at position "
+                                    << pos_start
+                                    << " while inside the section '"
+                                    << sections.top().name_
+                                    << "'"
+                                    << std::flush
+                                    ;
+                                }
+                            sections.pop();
+                            break;
+
+                        case '>':
+                            if(is_active())
+                                {
+                                auto const& real_name = name.substr(1);
+
+                                do_interpolate_string_in_context
+                                    (lookup
+                                        (real_name
+                                        ,interpolate_lookup_kind::partial
+                                        ).c_str()
+                                    ,lookup
+                                    ,out
+                                    ,sections
+                                    ,real_name
+                                    ,recursion_level + 1
+                                    );
+                                }
+                            break;
+
+                        case '!':
+                            // This is a comment, we just ignore it completely.
+                            break;
+
+                        default:
+                            if(is_active())
+                                {
+                                // We don't check here if name is not empty, as
+                                // there is no real reason to do it. Empty
+                                // variable name may seem strange, but why not
+                                // allow using "{{}}" to insert something into
+                                // the interpolated string, after all?
+                                out += lookup
+                                    (name
+                                    ,interpolate_lookup_kind::variable
+                                    );
+                                }
+                        }
+
+                    // We consume two characters here ("}}"), not one, as in a
+                    // usual loop iteration.
+                    ++p;
+                    break;
+                    }
+
+                if(p[0] == '{' && p[1] == '{')
+                    {
+                    // We don't allow nested interpolations, so this can only
+                    // be result of an error, e.g. a forgotten "}}" somewhere.
+                    alarum()
+                        << "Unexpected nested interpolation at position "
+                        << pos_start
+                        << " (outer interpolation starts at "
+                        << (p - s - 1 - name.length())
+                        << ")"
+                        << std::flush
+                        ;
+                    }
+
+                // We don't impose any restrictions on the kind of characters
+                // that can occur in the variable names neither because there
+                // just doesn't seem to be anything to gain from it.
+                name += *p;
+                }
+            }
+        else if(is_active())
+            {
+            out += *p;
+            }
+        }
+}
+
+} // Unnamed namespace.
+
+std::string interpolate_string
+    (char const* s
+    ,lookup_function const& lookup
+    )
+{
+    std::string out;
+
+    // This is probably not going to be enough as replacements of the
+    // interpolated variables tend to be longer than the variables names
+    // themselves, but it's difficult to estimate the resulting string length
+    // any better than this.
+    out.reserve(strlen(s));
+
+    // The stack contains all the sections that we're currently in.
+    std::stack<section_info, std::vector<section_info>> sections;
+
+    do_interpolate_string_in_context(s, lookup, out, sections);
+
+    if(!sections.empty())
+        {
+        alarum()
+            << "Unclosed section '"
+            << sections.top().name_
+            << "'"
+            << std::flush
+            ;
+        }
+
+    return out;
+}
diff --git a/interpolate_string.hpp b/interpolate_string.hpp
new file mode 100644
index 0000000..e71d1ed
--- /dev/null
+++ b/interpolate_string.hpp
@@ -0,0 +1,67 @@
+// Interpolate string containing embedded variable references.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef interpolate_string_hpp
+#define interpolate_string_hpp
+
+#include "config.hpp"
+
+#include <functional>
+#include <string>
+
+enum class interpolate_lookup_kind
+{
+    variable,
+    section,
+    partial
+};
+
+using lookup_function
+    = std::function<std::string (std::string const&, interpolate_lookup_kind)>;
+
+/// Interpolate string containing embedded variable references.
+///
+/// Return the input string after replacing all {{variable}} references in it
+/// with the value of the variable as returned by the provided function. The
+/// syntax is a (strict) subset of Mustache templates, the following features
+/// are supported:
+///  - Simple variable expansion for {{variable}}.
+///  - Conditional expansion using {{#variable}}...{{/variable}}.
+///  - Negated checks of the form {{^variable}}...{{/variable}}.
+///  - Partials support, i.e. {{>filename}}.
+///
+/// The following features are explicitly _not_ supported:
+///  - HTML escaping: this is done by a separate html::text class.
+///  - Separate types: 0/1 is false/true, anything else is an error.
+///  - Lists/section iteration (not needed yet).
+///  - Lambdas, comments, delimiter changes: omitted for simplicity.
+///
+/// To allow embedding literal "{{" fragment into the returned string, create a
+/// pseudo-variable expanding to these characters as its expansion, there is no
+/// built-in way to escape them.
+///
+/// Throw if the lookup function throws or if the string uses invalid syntax.
+std::string interpolate_string
+    (char const* s
+    ,lookup_function const& lookup
+    );
+
+#endif // interpolate_string_hpp
diff --git a/interpolate_string_test.cpp b/interpolate_string_test.cpp
new file mode 100644
index 0000000..2cf81be
--- /dev/null
+++ b/interpolate_string_test.cpp
@@ -0,0 +1,233 @@
+// Interpolate string containing embedded variable references.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "interpolate_string.hpp"
+
+#include "test_tools.hpp"
+
+int test_main(int, char*[])
+{
+    auto const test_interpolate = [](char const* s)
+        {
+        return interpolate_string
+            (s
+            ,[](std::string const& k, interpolate_lookup_kind) { return k; }
+            );
+        };
+
+    // Check that basic interpolation works.
+    BOOST_TEST_EQUAL( test_interpolate(""),               ""        );
+    BOOST_TEST_EQUAL( test_interpolate("literal"),        "literal" );
+    BOOST_TEST_EQUAL( test_interpolate("{{foo}}"),        "foo"     );
+    BOOST_TEST_EQUAL( test_interpolate("{{foo}}bar"),     "foobar"  );
+    BOOST_TEST_EQUAL( test_interpolate("foo{{}}bar"),     "foobar"  );
+    BOOST_TEST_EQUAL( test_interpolate("foo{{bar}}"),     "foobar"  );
+    BOOST_TEST_EQUAL( test_interpolate("{{foo}}{{bar}}"), "foobar"  );
+
+    // Comments should be just ignored.
+    BOOST_TEST_EQUAL( test_interpolate("{{! ignore me}}"), ""       );
+    BOOST_TEST_EQUAL( test_interpolate("{{! too}}{{x}}"),  "x"      );
+    BOOST_TEST_EQUAL( test_interpolate("{{x}}{{!also}}"),  "x"      );
+
+    // Sections.
+    auto const section_test = [](char const* str)
+        {
+        return interpolate_string
+            (str
+            ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+                {
+                if(s == "var0") return "0";
+                if(s == "var1") return "1";
+                if(s == "var" ) return "" ;
+
+                throw std::runtime_error("no such variable '" + s + "'");
+                }
+            );
+        };
+
+    BOOST_TEST_EQUAL( section_test("x{{#var1}}y{{/var1}}z"),   "xyz"    );
+    BOOST_TEST_EQUAL( section_test("x{{#var0}}y{{/var0}}z"),   "xz"     );
+    BOOST_TEST_EQUAL( section_test("x{{^var0}}y{{/var0}}z"),   "xyz"    );
+    BOOST_TEST_EQUAL( section_test("x{{^var1}}y{{/var1}}z"),   "xz"     );
+
+    BOOST_TEST_EQUAL
+        (section_test("a{{#var1}}b{{#var1}}c{{/var1}}d{{/var1}}e")
+        ,"abcde"
+        );
+    BOOST_TEST_EQUAL
+        (section_test("a{{#var1}}b{{#var0}}c{{/var0}}d{{/var1}}e")
+        ,"abde"
+        );
+    BOOST_TEST_EQUAL
+        (section_test("a{{^var1}}b{{#var0}}c{{/var0}}d{{/var1}}e")
+        ,"ae"
+        );
+    BOOST_TEST_EQUAL
+        (section_test("a{{^var1}}b{{^var0}}c{{/var0}}d{{/var1}}e")
+        ,"ae"
+        );
+
+    // Partials.
+    auto const partial_test = [](char const* str)
+        {
+        return interpolate_string
+            (str
+            ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+                {
+                if(s == "header")       return "[header with {{var}}]";
+                if(s == "footer")       return "[footer with {{var}}]";
+                if(s == "nested")       return "[header with {{>footer}}]";
+                if(s == "recursive")    return "{{>recursive}}";
+                if(s == "sec" )         return "1" ;
+                if(s == "var" )         return "variable" ;
+
+                throw std::runtime_error("no such variable '" + s + "'");
+                }
+            );
+        };
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{>header}}")
+        ,"[header with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{>header}}{{var}} in body{{>footer}}")
+        ,"[header with variable]variable in body[footer with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{#sec}}{{>header}}{{/sec}}")
+        ,"[header with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("only{{^sec}}{{>header}}{{/sec}}{{>footer}}")
+        ,"only[footer with variable]"
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("{{>nested}}")
+        ,"[header with [footer with variable]]"
+        );
+
+    BOOST_TEST_THROW
+        (partial_test("{{>recursive}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Nesting level too deep")
+        );
+
+    BOOST_TEST_EQUAL
+        (partial_test("no {{^sec}}{{>recursive}}{{/sec}} problem")
+        ,"no  problem"
+        );
+
+    // Some special cases.
+    BOOST_TEST_EQUAL
+        (interpolate_string
+            ("{{expanded}}"
+            ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+                {
+                if(s == "expanded")
+                    {
+                    return "{{unexpanded}}";
+                    }
+                throw std::runtime_error("no such variable '" + s + "'");
+                }
+            )
+        ,"{{unexpanded}}"
+        );
+
+    // Check that the kind of variable being expanded is correct.
+    BOOST_TEST_EQUAL
+        (interpolate_string
+            ("{{>test}}"
+             "{{#section1}}{{^section0}}{{variable}}{{/section0}}{{/section1}}"
+             ,[](std::string const& s, interpolate_lookup_kind kind) -> 
std::string
+                {
+                switch(kind)
+                    {
+                    case interpolate_lookup_kind::variable:
+                        return "value of " + s;
+
+                    case interpolate_lookup_kind::section:
+                        // Get rid of the "section" prefix.
+                        return s.substr(7);
+
+                    case interpolate_lookup_kind::partial:
+                        return s + " partial included\n";
+                    }
+
+                throw std::runtime_error("invalid lookup kind");
+                }
+            )
+        ,"test partial included\nvalue of variable"
+        );
+
+    // Should throw if the input syntax is invalid.
+    BOOST_TEST_THROW
+        (test_interpolate("{{x")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unmatched opening brace")
+        );
+    BOOST_TEST_THROW
+        (test_interpolate("{{x{{y}}}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unexpected nested interpolation")
+        );
+    BOOST_TEST_THROW
+        (section_test("{{#var1}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unclosed section 'var1'")
+        );
+    BOOST_TEST_THROW
+        (section_test("{{^var0}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unclosed section 'var0'")
+        );
+    BOOST_TEST_THROW
+        (section_test("{{/var1}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unexpected end of section")
+        );
+    BOOST_TEST_THROW
+        (section_test("{{#var1}}{{/var0}}")
+        ,std::runtime_error
+        ,lmi_test::what_regex("Unexpected end of section")
+        );
+
+    // Or because the lookup function throws.
+    BOOST_TEST_THROW
+        (interpolate_string
+            ("{{x}}"
+            ,[](std::string const& s, interpolate_lookup_kind) -> std::string
+                {
+                throw std::runtime_error("no such variable '" + s + "'");
+                }
+            )
+            ,std::runtime_error
+        ,"no such variable 'x'"
+        );
+
+    return EXIT_SUCCESS;
+}
diff --git a/ledger_evaluator.cpp b/ledger_evaluator.cpp
new file mode 100644
index 0000000..3e5ab24
--- /dev/null
+++ b/ledger_evaluator.cpp
@@ -0,0 +1,864 @@
+// Ledger evaluator returning values of all ledger fields.
+//
+// Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 
2014, 2015, 2016, 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "ledger_evaluator.hpp"
+
+#include "alert.hpp"
+#include "authenticity.hpp"
+#include "calendar_date.hpp"
+#include "configurable_settings.hpp"
+#include "contains.hpp"
+#include "global_settings.hpp"
+#include "handle_exceptions.hpp"
+#include "ledger.hpp"
+#include "ledger_invariant.hpp"
+#include "ledger_text_formats.hpp"      // ledger_format()
+#include "ledger_variant.hpp"
+#include "map_lookup.hpp"
+#include "mc_enum_aux.hpp"              // mc_e_vector_to_string_vector()
+#include "miscellany.hpp"               // each_equal(), lmi_array_size()
+#include "oecumenic_enumerations.hpp"
+#include "value_cast.hpp"
+#include "version.hpp"
+
+#include <algorithm>                    // transform()
+#include <functional>                   // minus
+#include <unordered_map>
+#include <utility>                      // pair
+
+namespace
+{
+int const n = 7;
+
+char const* char_p_suffixes[n] =
+    {"_Current"        // mce_run_gen_curr_sep_full
+    ,"_Guaranteed"     // mce_run_gen_guar_sep_full
+    ,"_Midpoint"       // mce_run_gen_mdpt_sep_full
+    ,"_CurrentZero"    // mce_run_gen_curr_sep_zero
+    ,"_GuaranteedZero" // mce_run_gen_guar_sep_zero
+    ,"_CurrentHalf"    // mce_run_gen_curr_sep_half
+    ,"_GuaranteedHalf" // mce_run_gen_guar_sep_half
+    };
+
+std::vector<std::string> const suffixes
+    (char_p_suffixes
+    ,char_p_suffixes + n
+    );
+
+typedef std::unordered_map<std::string, std::pair<int,oenum_format_style>> 
format_map_t;
+typedef std::unordered_map<std::string, std::string> title_map_t;
+
+// For all numbers (so-called 'scalars' and 'vectors', but not
+// 'strings') grabbed from all ledgers, look for a format. If one
+// is found, use it to turn the number into a string. If not, and
+// the field is named in unavailable(), then it's ignored. Otherwise,
+// format_exists() displays a warning and ignores the field (because
+// throwing an exception would cause only the first warning to be
+// displayed).
+//
+// Rationale: Silently falling back on some default format can't be
+// right, because it masks defects that should be fixed: no default
+// can be universally appropriate.
+//
+// For names formed as
+//   basename + '_' + suffix
+// only the basename is used as a map key. Lookups in the format map
+// are strict, as they must be, else one key like "A" would match
+// anything beginning with that letter.
+//
+// Some of the unavailable fields could easily be made available
+// someday; perhaps others should be eliminated from class Ledger.
+
+bool unavailable(std::string const& s)
+{
+    static std::string const a[] =
+        {"DateOfBirthJdn"        // used by group quotes
+        ,"EffDateJdn"            // used by group quotes
+        ,"ListBillDateJdn"       // probably not needed
+        ,"InforceAsOfDateJdn"    // probably not needed
+        ,"InitDacTaxRate"        // used by PrintRosterTabDelimited(); not 
cents
+        ,"InitPremTaxRate"       // used by PrintRosterTabDelimited(); not 
cents
+        ,"SubstdTable"           // probably not needed
+        ,"InitMlyPolFee"         // used by PrintRosterTabDelimited()
+        ,"InitTgtPremHiLoadRate" // used by PrintRosterTabDelimited(); not 
cents
+        };
+    static std::vector<std::string> const v(a, a + lmi_array_size(a));
+    return contains(v, s);
+}
+
+bool format_exists
+    (std::string  const& s
+    ,std::string  const& suffix
+    ,format_map_t const& m
+    )
+{
+    if(contains(m, s))
+        {
+        return true;
+        }
+    else if(unavailable(s))
+        {
+        return false;
+        }
+    else
+        {
+        warning() << "No format found for " << s << suffix << LMI_FLUSH;
+        return false;
+        }
+}
+
+} // Unnamed namespace.
+
+std::string ledger_evaluator::operator()(std::string const& scalar) const
+{
+    return map_lookup(scalars_, scalar);
+}
+
+std::string ledger_evaluator::operator()
+    (std::string const& vector
+    ,std::size_t index
+    ) const
+{
+    return map_lookup(vectors_, vector).at(index);
+}
+
+ledger_evaluator Ledger::make_evaluator() const
+{
+    title_map_t title_map;
+
+// Can't seem to get a literal &nbsp; into the output.
+
+// Original:   title_map["AttainedAge"                     ] = " 
&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0; 
End of &#xA0;&#xA0;Year Age";
+// No good:    title_map["AttainedAge"                     ] = " 
&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;&&#xA0;
 End of &&#xA0;&&#xA0;Year Age";
+// No good:    title_map["AttainedAge"                     ] = " 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 
End of &nbsp;&nbsp;Year Age";
+// No good:    title_map["AttainedAge"                     ] = " 
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
 End of &amp;nbsp;&amp;nbsp;Year Age";
+// No good:    title_map["AttainedAge"                     ] = "<![CDATA[ 
&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0;&#xA0; 
End of &#xA0;&#xA0;Year Age]]>";
+// No good:    title_map["AttainedAge"                     ] = " ááááááááááááá 
End of ááYear Age";
+// No good:    title_map["AttainedAge"                     ] = " 
&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 
End of &#160;&#160;Year Age";
+
+//  Here are the columns to be listed in the user interface
+//  as well as their corresponding titles.
+
+    // Current and guaranteed variants are generally given for columns
+    // that vary by basis. Some offer only a current variant because
+    // they are defined only on a current basis--experience-rating
+    // columns, e.g.
+
+    title_map["AVGenAcct_CurrentZero"           ] = "Curr 
Charges\nAccount\nValue\nGen Acct";
+    title_map["AVGenAcct_GuaranteedZero"        ] = "Guar 
Charges\nAccount\nValue\nGen Acct";
+    title_map["AVRelOnDeath_Current"            ] = 
"Account\nValue\nReleased\non Death";
+    title_map["AVSepAcct_CurrentZero"           ] = "Curr Charges\n0% 
Account\nValue\nSep Acct";
+    title_map["AVSepAcct_GuaranteedZero"        ] = "Guar Charges\n0% 
Account\nValue\nSep Acct";
+    title_map["AcctVal_Current"                 ] = "Curr Account\nValue";
+    title_map["AcctVal_CurrentZero"             ] = "Curr Charges\n0% 
Account\nValue";
+    title_map["AcctVal_Guaranteed"              ] = "Guar Account\nValue";
+    title_map["AcctVal_GuaranteedZero"          ] = "Guar Charges\n0% 
Account\nValue";
+    title_map["AddonCompOnAssets"               ] = "Additional\nComp 
on\nAssets";
+    title_map["AddonCompOnPremium"              ] = "Additional\nComp 
on\nPremium";
+    title_map["AddonMonthlyFee"                 ] = "Additional\nMonthly\nFee";
+    title_map["AnnGAIntRate_Current"            ] = "Curr Ann\nGen Acct\nInt 
Rate";
+    title_map["AnnGAIntRate_Guaranteed"         ] = "Guar Ann\nGen Acct\nInt 
Rate";
+    title_map["AnnHoneymoonValueRate_Current"   ] = "Curr 
Ann\nHoneymoon\nValue Rate";
+    title_map["AnnHoneymoonValueRate_Guaranteed"] = "Guar 
Ann\nHoneymoon\nValue Rate";
+    title_map["AnnPostHoneymoonRate_Current"    ] = "Curr 
Post\nHoneymoon\nRate";
+    title_map["AnnPostHoneymoonRate_Guaranteed" ] = "Guar 
Post\nHoneymoon\nRate";
+    title_map["AnnSAIntRate_Current"            ] = "Curr Ann\nSep Acct\nInt 
Rate";
+    title_map["AnnSAIntRate_Guaranteed"         ] = "Guar Ann\nSep Acct\nInt 
Rate";
+    title_map["AttainedAge"                     ] = "End of\nYear Age";
+    title_map["AvgDeathBft_Current"             ] = "Curr Avg\nDeath\nBenefit";
+    title_map["AvgDeathBft_Guaranteed"          ] = "Guar Avg\nDeath\nBenefit";
+    title_map["BaseDeathBft_Current"            ] = "Curr 
Base\nDeath\nBenefit";
+    title_map["BaseDeathBft_Guaranteed"         ] = "Guar 
Base\nDeath\nBenefit";
+    title_map["COICharge_Current"               ] = "Curr COI\nCharge";
+    title_map["COICharge_Guaranteed"            ] = "Guar COI\nCharge";
+    title_map["CSVNet_Current"                  ] = "Curr Net\nCash\nSurr 
Value";
+    title_map["CSVNet_CurrentZero"              ] = "Curr Charges\n0% Net 
Cash\nSurr Value";
+    title_map["CSVNet_Guaranteed"               ] = "Guar Net\nCash\nSurr 
Value";
+    title_map["CSVNet_GuaranteedZero"           ] = "Guar Charges\n0% Net 
Cash\nSurr Value";
+    title_map["CV7702_Current"                  ] = "Curr 7702\nCash Value";
+    title_map["CV7702_Guaranteed"               ] = "Guar 7702\nCash Value";
+    title_map["ClaimsPaid_Current"              ] = "Curr\nClaims\nPaid";
+    title_map["ClaimsPaid_Guaranteed"           ] = "Guar\nClaims\nPaid";
+    title_map["CorpTaxBracket"                  ] = "Corp Tax\nBracket";
+    title_map["CorridorFactor"                  ] = "Corridor\nFactor";
+    title_map["CurrMandE"                       ] = 
"Mortality\nand\nExpense\nCharge";
+    title_map["DBOpt"                           ] = "Death\nBenefit\nOption";
+    title_map["DacTaxLoad_Current"              ] = "Curr DAC\nTax\nLoad";
+    title_map["DacTaxLoad_Guaranteed"           ] = "Guar DAC\nTax\nLoad";
+    title_map["DacTaxRsv_Current"               ] = "Curr DAC\nTax\nReserve";
+    title_map["DacTaxRsv_Guaranteed"            ] = "Guar DAC\nTax\nReserve";
+    title_map["DeathProceedsPaid_Current"       ] = "Curr 
Death\nProceeds\nPaid";
+    title_map["DeathProceedsPaid_Guaranteed"    ] = "Guar 
Death\nProceeds\nPaid";
+    title_map["EOYDeathBft_Current"             ] = "Curr EOY\nDeath\nBenefit";
+    title_map["EOYDeathBft_Guaranteed"          ] = "Guar EOY\nDeath\nBenefit";
+    title_map["EeGrossPmt"                      ] = "EE Gross\nPayment";
+    title_map["EeModalMinimumPremium"           ] = "EE 
Modal\nMinimum\nPremium";
+    title_map["EeMode"                          ] = "EE\nPayment\nMode";
+// TODO ?? This can't be a mode. I don't know how it differs from 'EeGrossPmt' 
above.
+    title_map["EePmt"                           ] = "EE\nPayment\nMode";
+    title_map["ErGrossPmt"                      ] = "ER Gross\nPayment";
+    title_map["ErModalMinimumPremium"           ] = "ER 
Modal\nMinimum\nPremium";
+    title_map["ErMode"                          ] = "ER\nPayment\nMode";
+// TODO ?? This can't be a mode. I don't know how it differs from 'ErGrossPmt' 
above.
+    title_map["ErPmt"                           ] = "ER\nPayment\nMode";
+    title_map["ExpenseCharges_Current"          ] = "Curr\nExpense\nCharge";
+    title_map["ExpenseCharges_Guaranteed"       ] = "Guar\nExpense\nCharge";
+    title_map["ExperienceReserve_Current"       ] = 
"Experience\nRating\nReserve";
+    title_map["GptForceout"                     ] = "Forceout";
+    title_map["GrossIntCredited_Current"        ] = "Curr 
Gross\nInt\nCredited";
+    title_map["GrossIntCredited_Guaranteed"     ] = "Guar 
Gross\nInt\nCredited";
+    title_map["GrossPmt"                        ] = "Premium\nOutlay";
+    title_map["HoneymoonValueSpread"            ] = "Honeymoon\nValue\nSpread";
+    title_map["IndvTaxBracket"                  ] = "EE Tax\nBracket";
+    title_map["InforceLives"                    ] = "BOY\nLives\nInforce";
+    title_map["IrrCsv_Current"                  ] = "Curr IRR\non CSV";
+    title_map["IrrCsv_Guaranteed"               ] = "Guar IRR\non CSV";
+    title_map["IrrDb_Current"                   ] = "Curr IRR\non DB";
+    title_map["IrrDb_Guaranteed"                ] = "Guar IRR\non DB";
+    title_map["KFactor_Current"                 ] = "Experience\nRating\nK 
Factor";
+    title_map["LoanIntAccrued_Current"          ] = "Curr Loan\nInt\nAccrued";
+    title_map["LoanIntAccrued_Guaranteed"       ] = "Guar Loan\nInt\nAccrued";
+    title_map["MlyGAIntRate_Current"            ] = "Curr Monthly\nGen 
Acct\nInt Rate";
+    title_map["MlyGAIntRate_Guaranteed"         ] = "Guar Monthly\nGen 
Acct\nInt Rate";
+    title_map["MlyHoneymoonValueRate_Current"   ] = "Curr 
Monthly\nHoneymoon\nValue Rate";
+    title_map["MlyHoneymoonValueRate_Guaranteed"] = "Guar 
Monthly\nHoneymoon\nValue Rate";
+    title_map["MlyPostHoneymoonRate_Current"    ] = "Curr 
Monthly\nPost\nHoneymoon\nRate";
+    title_map["MlyPostHoneymoonRate_Guaranteed" ] = "Guar 
Monthly\nPost\nHoneymoon\nRate";
+    title_map["MlySAIntRate_Current"            ] = "Curr Monthly\nSep 
Acct\nInt Rate";
+    title_map["MlySAIntRate_Guaranteed"         ] = "Guar Monthly\nSep 
Acct\nInt Rate";
+    title_map["ModalMinimumPremium"             ] = "Modal\nMinimum\nPremium";
+    title_map["AnnualFlatExtra"                 ] = "Annual\nFlat\nExtra";
+//    title_map["NaarForceout"                    ] = "Forced\nWithdrawal\ndue 
to\nNAAR Limit";
+    title_map["NetCOICharge_Current"            ] = "Experience\nRating\nNet 
COI\nCharge";
+    title_map["NetClaims_Current"               ] = "Curr Net\nClaims";
+    title_map["NetClaims_Guaranteed"            ] = "Guar Net\nClaims";
+    title_map["NetIntCredited_Current"          ] = "Curr Net\nInt\nCredited";
+    title_map["NetIntCredited_Guaranteed"       ] = "Guar Net\nInt\nCredited";
+    title_map["NetPmt_Current"                  ] = "Curr Net\nPayment";
+    title_map["NetPmt_Guaranteed"               ] = "Guar Net\nPayment";
+    title_map["NetWD"                           ] = "Withdrawal";
+    title_map["NewCashLoan"                     ] = "Annual Loan";
+    title_map["Outlay"                          ] = "Net Outlay";
+    title_map["PartMortTableMult"               ] = 
"Partial\nMortality\nMuliplier";
+    title_map["PolicyFee_Current"               ] = "Curr\nPolicy\nFee";
+    title_map["PolicyFee_Guaranteed"            ] = "Guar\nPolicy\nFee";
+    title_map["PolicyYear"                      ] = "Policy\nYear";
+    title_map["PrefLoanBalance_Current"         ] = "Curr\nPreferred\nLoan 
Bal";
+    title_map["PrefLoanBalance_Guaranteed"      ] = "Guar\nPreferred\nLoan 
Bal";
+    title_map["PremTaxLoad_Current"             ] = "Curr\nPremium\nTax Load";
+    title_map["PremTaxLoad_Guaranteed"          ] = "Guar\nPremium\nTax Load";
+// Excluded because it's unimplemented:
+//    title_map["ProducerCompensation"            ] = "Producer\nCompensation";
+    title_map["ProjectedCoiCharge_Current"      ] = 
"Experience\nRating\nProjected\nCOI Charge";
+    title_map["RefundableSalesLoad"             ] = "Refundable\nSales\nLoad";
+    title_map["RiderCharges_Current"            ] = "Curr Rider\nCharges";
+    title_map["Salary"                          ] = "Salary";
+    title_map["SepAcctCharges_Current"          ] = "Curr Sep\nAcct\nCharges";
+    title_map["SepAcctCharges_Guaranteed"       ] = "Guar Sep\nAcct\nCharges";
+    title_map["SpecAmt"                         ] = "Specified\nAmount";
+    title_map["SpecAmtLoad_Current"             ] = "Curr Spec\nAmt Load";
+    title_map["SpecAmtLoad_Guaranteed"          ] = "Guar Spec\nAmt Load";
+    title_map["SurrChg_Current"                 ] = "Curr Surr\nCharge";
+    title_map["SurrChg_Guaranteed"              ] = "Guar Surr\nCharge";
+    title_map["TermPurchased_Current"           ] = "Curr 
Term\nAmt\nPurchased";
+    title_map["TermPurchased_Guaranteed"        ] = "Guar 
Term\nAmt\nPurchased";
+    title_map["TermSpecAmt"                     ] = "Term\nSpecified\nAmount";
+    title_map["TgtPrem"                         ] = "Target\nPremium";
+    title_map["TotalIMF"                        ] = "Total\nInvestment\nMgt 
Fee";
+    title_map["TotalLoanBalance_Current"        ] = "Curr 
Total\nLoan\nBalance";
+    title_map["TotalLoanBalance_Guaranteed"     ] = "Guar 
Total\nLoan\nBalance";
+
+    // TODO ?? Titles ought to be read from an external file that
+    // permits flexible customization. Compliance might require that
+    // 'AcctVal_Current' be called "Cash Value" for one policy form,
+    // and "Account Value" for another, in order to match the terms
+    // used in the contract exactly. Therefore, these titles probably
+    // belong in the product database, which permits variation by
+    // product--though it does not accommodate strings as this is
+    // written in 2006-07. DATABASE !! So consider adding them there
+    // when the database is revamped.
+
+// Here's my top-level analysis of the formatting specification.
+//
+// Formats
+//
+// F0: zero decimals
+// F1: zero decimals, commas
+// F2: two decimals, commas
+// F3: scaled by 100, zero decimals, with '%' at end:
+// F4: scaled by 100, two decimals, with '%' at end:
+//
+// Presumably all use commas as thousands-separators, so that
+// an IRR of 12345.67% would be formatted as "12,345.67%".
+//
+// So the differences are:
+//   'precision' (number of decimal places)
+//   percentage (scaled by 100, '%' at end) or not
+// and therefore F0 is equivalent to F1
+
+    std::pair<int,oenum_format_style> f1(0, oe_format_normal);
+    std::pair<int,oenum_format_style> f2(2, oe_format_normal);
+    std::pair<int,oenum_format_style> f3(0, oe_format_percentage);
+    std::pair<int,oenum_format_style> f4(2, oe_format_percentage);
+
+    format_map_t format_map;
+
+// > Special Formatting for Scalar Items
+// >
+// F4: scaled by 100, two decimals, with '%' at end:
+// > Format as percentage "0.00%"
+// >
+    format_map["GuarMaxMandE"                      ] = f4;
+    format_map["InitAnnGenAcctInt"                 ] = f4;
+    format_map["InitAnnLoanCredRate"               ] = f4;
+    format_map["InitAnnLoanDueRate"                ] = f4;
+    format_map["InitAnnSepAcctCurrGross0Rate"      ] = f4;
+    format_map["InitAnnSepAcctCurrGrossHalfRate"   ] = f4;
+    format_map["InitAnnSepAcctCurrNet0Rate"        ] = f4;
+    format_map["InitAnnSepAcctCurrNetHalfRate"     ] = f4;
+    format_map["InitAnnSepAcctGrossInt"            ] = f4;
+    format_map["InitAnnSepAcctGuarGross0Rate"      ] = f4;
+    format_map["InitAnnSepAcctGuarGrossHalfRate"   ] = f4;
+    format_map["InitAnnSepAcctGuarNet0Rate"        ] = f4;
+    format_map["InitAnnSepAcctGuarNetHalfRate"     ] = f4;
+    format_map["InitAnnSepAcctNetInt"              ] = f4;
+    format_map["PostHoneymoonSpread"               ] = f4;
+    format_map["Preferred"                         ] = f4;
+    format_map["PremTaxRate"                       ] = f4;
+
+// F3: scaled by 100, zero decimals, with '%' at end:
+// > Format as percentage with no decimal places (##0%)
+    format_map["SalesLoadRefundRate0"              ] = f3;
+    format_map["SalesLoadRefundRate1"              ] = f3;
+    format_map["GenAcctAllocationPercent"          ] = f3;
+    format_map["GenAcctAllocationComplementPercent"] = f3;
+
+// >
+// F2: two decimals, commas
+// > Format as a number with thousand separators and two decimal places 
(#,###,###.00)
+// >
+    format_map["CurrentCoiMultiplier"              ] = f2;
+    format_map["EeListBillPremium"                 ] = f2;
+    format_map["ErListBillPremium"                 ] = f2;
+    format_map["GuarPrem"                          ] = f2;
+    format_map["InforceTaxBasis"                   ] = f2;
+    format_map["InforceUnloanedAV"                 ] = f2;
+    format_map["InitGLP"                           ] = f2;
+    format_map["InitGSP"                           ] = f2;
+    format_map["InitPrem"                          ] = f2;
+    format_map["InitSevenPayPrem"                  ] = f2;
+    format_map["InitTgtPrem"                       ] = f2;
+    format_map["InitMinPrem"                       ] = f2;
+    format_map["ListBillPremium"                   ] = f2;
+    format_map["ModalMinimumDumpin"                ] = f2;
+// >
+// F1: zero decimals, commas
+// > Format as a number with thousand separators and no decimal places 
(#,###,###)
+// >
+    format_map["Age"                               ] = f1;
+    format_map["AllowDbo3"                         ] = f1;
+    format_map["AvgFund"                           ] = f1;
+    format_map["ChildRiderAmount"                  ] = f1;
+    format_map["CustomFund"                        ] = f1;
+    format_map["Dumpin"                            ] = f1;
+    format_map["EndtAge"                           ] = f1;
+    format_map["External1035Amount"                ] = f1;
+    format_map["GenAcctAllocation"                 ] = f1;
+    format_map["GenderBlended"                     ] = f1;
+    format_map["GenderDistinct"                    ] = f1;
+    format_map["Has1035ExchCharge"                 ] = f1;
+    format_map["HasADD"                            ] = f1;
+    format_map["HasChildRider"                     ] = f1;
+    format_map["HasHoneymoon"                      ] = f1;
+    format_map["HasSalesLoadRefund"                ] = f1;
+    format_map["HasSpouseRider"                    ] = f1;
+    format_map["HasSupplSpecAmt"                   ] = f1;
+    format_map["HasTerm"                           ] = f1;
+    format_map["HasWP"                             ] = f1;
+    format_map["InforceIsMec"                      ] = f1;
+    format_map["InforceMonth"                      ] = f1;
+    format_map["InforceYear"                       ] = f1;
+    format_map["InitBaseSpecAmt"                   ] = f1;
+    format_map["InitTermSpecAmt"                   ] = f1;
+    format_map["InitTotalSA"                       ] = f1;
+    format_map["Internal1035Amount"                ] = f1;
+    format_map["IsInforce"                         ] = f1;
+    format_map["IsMec"                             ] = f1;
+    format_map["LapseMonth"                        ] = f1;
+    format_map["LapseYear"                         ] = f1;
+    format_map["MaxDuration"                       ] = f1;
+    format_map["MecMonth"                          ] = f1;
+    format_map["MecYear"                           ] = f1;
+    format_map["NoLapse"                           ] = f1;
+    format_map["NoLapseAlwaysActive"               ] = f1;
+    format_map["NoLapseMinAge"                     ] = f1;
+    format_map["NoLapseMinDur"                     ] = f1;
+    format_map["RetAge"                            ] = f1;
+    format_map["SmokerBlended"                     ] = f1;
+    format_map["SmokerDistinct"                    ] = f1;
+    format_map["SplitFundAllocation"               ] = f1;
+    format_map["SplitMinPrem"                      ] = f1;
+    format_map["SpouseIssueAge"                    ] = f1;
+    format_map["SupplementalReport"                ] = f1;
+    format_map["UseExperienceRating"               ] = f1;
+    format_map["GroupIndivSelection"               ] = f1;
+    format_map["UsePartialMort"                    ] = f1;
+
+// > Vector Formatting
+// >
+// > Here are the vectors enumerated
+// >
+// F3: scaled by 100, zero decimals, with '%' at end:
+// > Format as percentage with no decimal places (##0%)
+// >
+    format_map["CorridorFactor"                    ] = f3;
+    format_map["FundAllocations"                   ] = f3;
+    format_map["MaleProportion"                    ] = f3;
+    format_map["NonsmokerProportion"               ] = f3;
+    format_map["PartMortTableMult"                 ] = f3;
+
+// >
+// F4: scaled by 100, two decimals, with '%' at end:
+// > Format as percentage with two decimal places (##0.00%)
+// >
+    format_map["AnnGAIntRate"                      ] = f4;
+    format_map["AnnHoneymoonValueRate"             ] = f4;
+    format_map["AnnPostHoneymoonRate"              ] = f4;
+    format_map["AnnSAIntRate"                      ] = f4;
+    format_map["CashFlowIRR"                       ] = f4;
+    format_map["CorpTaxBracket"                    ] = f4;
+    format_map["CurrMandE"                         ] = f4;
+    format_map["HoneymoonValueSpread"              ] = f4;
+    format_map["IndvTaxBracket"                    ] = f4;
+    format_map["InforceHMVector"                   ] = f4;
+
+    format_map["IrrCsv_Current"                    ] = f4;
+    format_map["IrrCsv_CurrentZero"                ] = f4;
+    format_map["IrrCsv_Guaranteed"                 ] = f4;
+    format_map["IrrCsv_GuaranteedZero"             ] = f4;
+    format_map["IrrDb_Current"                     ] = f4;
+    format_map["IrrDb_CurrentZero"                 ] = f4;
+    format_map["IrrDb_Guaranteed"                  ] = f4;
+    format_map["IrrDb_GuaranteedZero"              ] = f4;
+
+    format_map["MlyGAIntRate"                      ] = f4;
+    format_map["MlyHoneymoonValueRate"             ] = f4;
+    format_map["MlyPostHoneymoonRate"              ] = f4;
+    format_map["MlySAIntRate"                      ] = f4;
+    format_map["TotalIMF"                          ] = f4;
+// >
+// F0: zero decimals
+// > Format as a number no thousand separator or decimal point (##0%)
+// >
+    format_map["AttainedAge"                       ] = f1;
+    format_map["Duration"                          ] = f1;
+    format_map["LapseYears"                        ] = f1;
+    format_map["PolicyYear"                        ] = f1;
+// >
+// F2: two decimals, commas
+// > Format as a number with thousand separators and two decimal places 
(#,###,###.00)
+// >
+    format_map["AddonMonthlyFee"                   ] = f2;
+// TODO ?? The precision of 'InforceLives' and 'KFactor' is inadequate.
+// Is every other format OK?
+    format_map["InforceLives"                      ] = f2;
+    format_map["KFactor"                           ] = f2;
+    format_map["AnnualFlatExtra"                   ] = f2;
+// >
+// F1: zero decimals, commas
+// > Format as a number with thousand separators and no decimal places 
(#,###,##0)
+// >
+    format_map["AcctVal"                           ] = f1;
+    format_map["AccumulatedPremium"                ] = f1;
+    format_map["AddonCompOnAssets"                 ] = f1;
+    format_map["AddonCompOnPremium"                ] = f1;
+    format_map["AvgDeathBft"                       ] = f1;
+    format_map["AVGenAcct"                         ] = f1;
+    format_map["AVRelOnDeath"                      ] = f1;
+    format_map["AVSepAcct"                         ] = f1;
+    format_map["BaseDeathBft"                      ] = f1;
+    format_map["BOYAssets"                         ] = f1;
+    format_map["ClaimsPaid"                        ] = f1;
+    format_map["COICharge"                         ] = f1;
+    format_map["Composite"                         ] = f1;
+    format_map["CSVNet"                            ] = f1;
+    format_map["CV7702"                            ] = f1;
+    format_map["DacTaxLoad"                        ] = f1;
+    format_map["DacTaxRsv"                         ] = f1;
+    format_map["DeathProceedsPaid"                 ] = f1;
+    format_map["EeGrossPmt"                        ] = f1;
+    format_map["EeModalMinimumPremium"             ] = f1;
+//    format_map["EeMode"                            ] = f1; // Not numeric.
+    format_map["EePmt"                             ] = f1;
+    format_map["EOYDeathBft"                       ] = f1;
+    format_map["ErGrossPmt"                        ] = f1;
+    format_map["ErModalMinimumPremium"             ] = f1;
+//    format_map["ErMode"                            ] = f1; // Not numeric.
+    format_map["ErPmt"                             ] = f1;
+    format_map["ExpenseCharges"                    ] = f1;
+    format_map["ExperienceReserve"                 ] = f1;
+    format_map["FundNumbers"                       ] = f1;
+    format_map["GptForceout"                       ] = f1;
+    format_map["GrossIntCredited"                  ] = f1;
+    format_map["GrossPmt"                          ] = f1;
+    format_map["Loads"                             ] = f1;
+    format_map["LoanInt"                           ] = f1;
+    format_map["LoanIntAccrued"                    ] = f1;
+    format_map["ModalMinimumPremium"               ] = f1;
+    format_map["NaarForceout"                      ] = f1;
+    format_map["NetClaims"                         ] = f1;
+    format_map["NetCOICharge"                      ] = f1;
+    format_map["NetIntCredited"                    ] = f1;
+    format_map["NetPmt"                            ] = f1;
+    format_map["NetWD"                             ] = f1;
+    format_map["NewCashLoan"                       ] = f1;
+    format_map["Outlay"                            ] = f1;
+    format_map["PolicyFee"                         ] = f1;
+    format_map["PrefLoanBalance"                   ] = f1;
+    format_map["PremTaxLoad"                       ] = f1;
+    format_map["ProducerCompensation"              ] = f1;
+    format_map["ProjectedCoiCharge"                ] = f1;
+    format_map["RefundableSalesLoad"               ] = f1;
+    format_map["RiderCharges"                      ] = f1;
+    format_map["Salary"                            ] = f1;
+    format_map["SepAcctCharges"                    ] = f1;
+    format_map["SpecAmt"                           ] = f1;
+    format_map["SpecAmtLoad"                       ] = f1;
+    format_map["SpouseRiderAmount"                 ] = f1;
+    format_map["SurrChg"                           ] = f1;
+    format_map["TermPurchased"                     ] = f1;
+    format_map["TermSpecAmt"                       ] = f1;
+    format_map["TgtPrem"                           ] = f1;
+    format_map["TotalLoanBalance"                  ] = f1;
+
+    // This is a little tricky. We have some stuff that
+    // isn't in the maps inside the ledger classes. We're going to
+    // stuff it into a copy of the invariant-ledger class's data.
+    // To avoid copying, we'll use pointers to the data. Most of
+    // this stuff is invariant anyway, so that's a reasonable
+    // place to put it.
+    //
+    // First we make a copy of the invariant ledger:
+
+    double_vector_map   vectors = ledger_invariant_->AllVectors;
+    scalar_map          scalars = ledger_invariant_->AllScalars;
+    string_map          strings = ledger_invariant_->Strings;
+
+    // Now we add the stuff that wasn't in the invariant
+    // ledger's class's maps (indexable by name). Because we're
+    // working with maps of pointers, we need pointers here.
+    //
+    // The IRRs are the worst of all.
+
+    if(!ledger_invariant_->IsInforce)
+        {
+        ledger_invariant_->CalculateIrrs(*this);
+        }
+    vectors["IrrCsv_GuaranteedZero" ] = &ledger_invariant_->IrrCsvGuar0    ;
+    vectors["IrrDb_GuaranteedZero"  ] = &ledger_invariant_->IrrDbGuar0     ;
+    vectors["IrrCsv_CurrentZero"    ] = &ledger_invariant_->IrrCsvCurr0    ;
+    vectors["IrrDb_CurrentZero"     ] = &ledger_invariant_->IrrDbCurr0     ;
+    vectors["IrrCsv_Guaranteed"     ] = &ledger_invariant_->IrrCsvGuarInput;
+    vectors["IrrDb_Guaranteed"      ] = &ledger_invariant_->IrrDbGuarInput ;
+    vectors["IrrCsv_Current"        ] = &ledger_invariant_->IrrCsvCurrInput;
+    vectors["IrrDb_Current"         ] = &ledger_invariant_->IrrDbCurrInput ;
+
+// GetMaxLength() is max *composite* length.
+//    int max_length = GetMaxLength();
+    double MaxDuration = ledger_invariant_->EndtAge - ledger_invariant_->Age;
+    scalars["MaxDuration"] = &MaxDuration;
+    int max_duration = static_cast<int>(MaxDuration);
+
+    std::vector<double> PolicyYear;
+    std::vector<double> AttainedAge;
+
+    PolicyYear .resize(max_duration);
+    AttainedAge.resize(max_duration);
+
+    int issue_age = static_cast<int>(ledger_invariant_->Age);
+    for(int j = 0; j < max_duration; ++j)
+        {
+        PolicyYear[j]  = 1 + j;
+        AttainedAge[j] = 1 + j + issue_age;
+        }
+
+// TODO ?? An attained-age column is meaningless in a composite. So
+// are several others--notably those affected by partial mortaility.
+    vectors["AttainedAge"] = &AttainedAge;
+    vectors["PolicyYear" ] = &PolicyYear ;
+
+    std::vector<double> InitAnnLoanDueRate(max_duration);
+    std::fill
+        (InitAnnLoanDueRate.begin()
+        ,InitAnnLoanDueRate.end()
+        ,ledger_invariant_->GetInitAnnLoanDueRate()
+        );
+    vectors["InitAnnLoanDueRate"] = &InitAnnLoanDueRate;
+
+    vectors["InforceLives"] = &ledger_invariant_->InforceLives;
+
+    vectors["FundNumbers"    ] = &ledger_invariant_->FundNumbers    ;
+    vectors["FundAllocations"] = &ledger_invariant_->FundAllocations;
+
+    // The Ledger object should contain a basic minimal set of columns
+    // from which others may be derived. It must be kept small because
+    // its size imposes a practical limit on the number of lives that
+    // can be run as part of a single census.
+    //
+    // TODO ?? A really good design would give users the power to
+    // define and store their own derived-column definitions. For now,
+    // however, code changes are required, and this is as appropriate
+    // a place as any to make them.
+
+    LedgerInvariant const& Invar = GetLedgerInvariant();
+    LedgerVariant   const& Curr_ = GetCurrFull();
+    LedgerVariant   const& Guar_ = GetGuarFull();
+
+    std::vector<double> PremiumLoads(max_duration);
+    std::vector<double> AdminCharges(max_duration);
+    for(int j = 0; j < max_duration; ++j)
+        {
+        PremiumLoads[j] = Invar.GrossPmt[j] - Curr_.NetPmt[j];
+        AdminCharges[j] = Curr_.SpecAmtLoad[j] + Curr_.PolicyFee[j];
+        }
+
+    vectors   ["PremiumLoads"] = &PremiumLoads;
+    format_map["PremiumLoads"] = f1;
+    vectors   ["AdminCharges"] = &AdminCharges;
+    format_map["AdminCharges"] = f1;
+
+    // ET !! Easier to write as
+    //   std::vector<double> NetDeathBenefit =
+    //     Curr_.EOYDeathBft - Curr_.TotalLoanBalance;
+    std::vector<double> NetDeathBenefit(Curr_.EOYDeathBft);
+    std::transform
+        (NetDeathBenefit.begin()
+        ,NetDeathBenefit.end()
+        ,Curr_.TotalLoanBalance.begin()
+        ,NetDeathBenefit.begin()
+        ,std::minus<double>()
+        );
+    vectors   ["NetDeathBenefit"] = &NetDeathBenefit;
+    title_map ["NetDeathBenefit"] = "Net\nDeath\nBenefit";
+    format_map["NetDeathBenefit"] = f1;
+
+    std::vector<double> SupplDeathBft_Current   (Curr_.TermPurchased);
+    std::vector<double> SupplDeathBft_Guaranteed(Guar_.TermPurchased);
+    vectors   ["SupplDeathBft_Current"   ] = &SupplDeathBft_Current;
+    vectors   ["SupplDeathBft_Guaranteed"] = &SupplDeathBft_Guaranteed;
+    title_map ["SupplDeathBft_Current"   ] = "Curr Suppl\nDeath\nBenefit";
+    title_map ["SupplDeathBft_Guaranteed"] = "Guar Suppl\nDeath\nBenefit";
+    format_map["SupplDeathBft_Current"   ] = f1;
+    format_map["SupplDeathBft_Guaranteed"] = f1;
+
+    std::vector<double> SupplSpecAmt(Invar.TermSpecAmt);
+    vectors   ["SupplSpecAmt"            ] = &SupplSpecAmt;
+    title_map ["SupplSpecAmt"            ] = "Suppl\nSpecified\nAmount";
+    format_map["SupplSpecAmt"            ] = f1;
+
+    // [End of derived columns.]
+
+    double Composite = is_composite();
+    scalars["Composite"] = &Composite;
+
+    double NoLapse =
+            0 != ledger_invariant_->NoLapseMinDur
+        ||  0 != ledger_invariant_->NoLapseMinAge
+        ;
+    scalars["NoLapse"] = &NoLapse;
+
+    std::string LmiVersion(LMI_VERSION);
+    calendar_date prep_date;
+
+    // Skip authentication for non-interactive regression testing.
+    if(!global_settings::instance().regression_testing())
+        {
+        authenticate_system();
+        }
+    else
+        {
+        // For regression tests,
+        //   - use an invariant string as version
+        //   - use EffDate as date prepared
+        // in order to avoid gratuitous failures.
+        LmiVersion = "Regression testing";
+        
prep_date.julian_day_number(static_cast<int>(ledger_invariant_->EffDateJdn));
+        }
+
+    strings["LmiVersion"] = &LmiVersion;
+
+    std::string PrepYear  = value_cast<std::string>(prep_date.year());
+    std::string PrepMonth = month_name(prep_date.month());
+    std::string PrepDay   = value_cast<std::string>(prep_date.day());
+
+    strings["PrepYear" ] = &PrepYear;
+    strings["PrepMonth"] = &PrepMonth;
+    strings["PrepDay"  ] = &PrepDay;
+
+    double HasSalesLoadRefund =
+        !each_equal(ledger_invariant_->RefundableSalesLoad, 0.0);
+    double SalesLoadRefundRate0 = ledger_invariant_->RefundableSalesLoad[0];
+    double SalesLoadRefundRate1 = ledger_invariant_->RefundableSalesLoad[1];
+
+    scalars["HasSalesLoadRefund"  ] = &HasSalesLoadRefund  ;
+    scalars["SalesLoadRefundRate0"] = &SalesLoadRefundRate0;
+    scalars["SalesLoadRefundRate1"] = &SalesLoadRefundRate1;
+
+    double GenAcctAllocation           = ledger_invariant_->GenAcctAllocation;
+    double GenAcctAllocationComplement = 1. - GenAcctAllocation;
+
+    scalars["GenAcctAllocationPercent"          ] = &GenAcctAllocation;
+    scalars["GenAcctAllocationComplementPercent"] = 
&GenAcctAllocationComplement;
+
+    std::string ScaleUnit = ledger_invariant_->ScaleUnit();
+    strings["ScaleUnit"] = &ScaleUnit;
+
+    double InitTotalSA =
+            ledger_invariant_->InitBaseSpecAmt
+        +   ledger_invariant_->InitTermSpecAmt
+        ;
+    scalars["InitTotalSA"] = &InitTotalSA;
+
+    // Maps to hold the results of formatting numeric data.
+
+    std::unordered_map<std::string, std::string> stringscalars;
+    std::unordered_map<std::string, std::vector<std::string>> stringvectors;
+
+    stringvectors["FundNames"] = ledger_invariant_->FundNames;
+
+    // Map the data, formatting it as necessary.
+
+    // First we'll get the invariant stuff--the copy we made,
+    // along with all the stuff we plugged into it above.
+    {
+    std::string suffix = "";
+    for(auto const& j : scalars)
+        {
+        if(format_exists(j.first, suffix, format_map))
+            stringscalars[j.first + suffix] = ledger_format(*j.second, 
format_map[j.first]);
+        }
+    for(auto const& j : strings)
+        {
+        stringscalars[j.first + suffix] = *j.second;
+        }
+    for(auto const& j : vectors)
+        {
+        if(format_exists(j.first, suffix, format_map))
+            stringvectors[j.first + suffix] = ledger_format(*j.second, 
format_map[j.first]);
+        }
+    }
+
+//    stringscalars["GuarMaxMandE"] = ledger_format(*scalars["GuarMaxMandE"], 
2, true);
+//    stringvectors["CorridorFactor"] = 
ledger_format(*vectors["CorridorFactor"], 0, true);
+//    stringscalars["InitAnnGenAcctInt_Current"] = 
ledger_format(*scalars["InitAnnGenAcctInt_Current"], 0, true);
+
+    // That was the tricky part. Now it's all downhill.
+
+    for(auto const& i : ledger_map_->held())
+        {
+        std::string suffix = suffixes[i.first];
+        for(auto const& j : i.second.AllScalars)
+            {
+//            scalars[j.first + suffix] = j.second;
+            if(format_exists(j.first, suffix, format_map))
+                stringscalars[j.first + suffix] = ledger_format(*j.second, 
format_map[j.first]);
+            }
+        for(auto const& j : i.second.Strings)
+            {
+            strings[j.first + suffix] = j.second;
+            }
+        for(auto const& j : i.second.AllVectors)
+            {
+//            vectors[j.first + suffix] = j.second;
+            if(format_exists(j.first, suffix, format_map))
+                stringvectors[j.first + suffix] = ledger_format(*j.second, 
format_map[j.first]);
+            }
+        }
+
+    stringvectors["EeMode"] = 
mc_e_vector_to_string_vector(ledger_invariant_->EeMode);
+    stringvectors["ErMode"] = 
mc_e_vector_to_string_vector(ledger_invariant_->ErMode);
+    stringvectors["DBOpt"]  = 
mc_e_vector_to_string_vector(ledger_invariant_->DBOpt );
+
+// TODO ?? Here I copied some stuff from the ledger class files: the
+// parts that speak of odd members that aren't in those class's
+// maps. This may reveal incomplete or incorrect systems analysis.
+
+// Invariant
+//
+//    // Special-case vectors (not <double>, or different length than others).
+//    EeMode              .reserve(Length);
+//    ErMode              .reserve(Length);
+//    DBOpt               .reserve(Length);
+//
+//    std::vector<int>            FundNumbers; [not handled yet]
+//    std::vector<std::string>    FundNames;   [not handled yet]
+//    std::vector<int>            FundAllocs;  [not handled yet]
+//
+//    std::vector<double> InforceLives;
+//
+//    // Special-case strings.
+//    std::string     EffDate; [furnished as PrepYear, PrepMonth, PrepDay]
+//
+// Variant
+//
+// [None of these are stored, and I think none is wanted.]
+//
+//    // special cases
+//    int              Length;
+//    mcenum_gen_basis GenBasis_;
+//    mcenum_sep_basis SepBasis_;
+//    bool             FullyInitialized;   // I.e. by Init(BasicValues const* 
b)
+
+    if(ledger_invariant_->SupplementalReport)
+        {
+        std::vector<std::string> SupplementalReportColumns;
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn00);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn01);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn02);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn03);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn04);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn05);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn06);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn07);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn08);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn09);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn10);
+        
SupplementalReportColumns.push_back(ledger_invariant_->SupplementalReportColumn11);
+
+        // Eventually customize the report name.
+        stringscalars["SupplementalReportTitle"] = "Supplemental Report";
+
+        std::vector<std::string> SupplementalReportColumnsTitles;
+        
SupplementalReportColumnsTitles.reserve(SupplementalReportColumns.size());
+
+        for(auto const& j : SupplementalReportColumns)
+            {
+            SupplementalReportColumnsTitles.push_back(title_map[j]);
+            }
+
+        stringvectors["SupplementalReportColumnsNames"] = 
std::move(SupplementalReportColumns);
+        stringvectors["SupplementalReportColumnsTitles"] = 
std::move(SupplementalReportColumnsTitles);
+        }
+
+    return ledger_evaluator(std::move(stringscalars), 
std::move(stringvectors));
+}
diff --git a/ledger_evaluator.hpp b/ledger_evaluator.hpp
new file mode 100644
index 0000000..07e86d8
--- /dev/null
+++ b/ledger_evaluator.hpp
@@ -0,0 +1,59 @@
+// Ledger evaluator returning values of all ledger fields.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef ledger_evaluator_hpp
+#define ledger_evaluator_hpp
+
+#include "config.hpp"
+
+#include "so_attributes.hpp"
+
+#include <cstddef>                      // size_t
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+/// Class allowing to retrieve the string representation of any scalar or
+/// vector stored in a ledger.
+class LMI_SO ledger_evaluator
+{
+  public:
+    std::string operator()(std::string const& scalar) const;
+    std::string operator()(std::string const& vector, std::size_t index) const;
+
+  private:
+    using all_scalars = std::unordered_map<std::string,            std::string 
>;
+    using all_vectors = 
std::unordered_map<std::string,std::vector<std::string>>;
+
+    // Objects of this class can only be created by Ledger::make_evaluator().
+    ledger_evaluator(all_scalars&& scalars, all_vectors&& vectors)
+        :scalars_(scalars)
+        ,vectors_(vectors)
+    {
+    }
+
+    all_scalars const scalars_;
+    all_vectors const vectors_;
+
+    friend class Ledger;
+};
+
+#endif // ledger_evaluator_hpp
diff --git a/ledger_pdf.cpp b/ledger_pdf.cpp
new file mode 100644
index 0000000..48b2a75
--- /dev/null
+++ b/ledger_pdf.cpp
@@ -0,0 +1,44 @@
+// Ledger PDF generation.
+//
+// Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 
2014, 2015, 2016, 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "ledger_pdf.hpp"
+
+#include "configurable_settings.hpp"
+#include "ledger.hpp"
+#include "ledger_pdf_generator.hpp"
+#include "path_utility.hpp"
+
+/// Write ledger as pdf.
+
+std::string write_ledger_as_pdf(Ledger const& ledger, fs::path const& filepath)
+{
+    throw_if_interdicted(ledger);
+
+    fs::path print_dir(configurable_settings::instance().print_directory());
+    fs::path pdf_out_file = unique_filepath(print_dir / filepath, ".pdf");
+
+    auto const pdf = ledger_pdf_generator::create();
+    pdf->write(ledger, pdf_out_file);
+
+    return pdf_out_file.string();
+}
diff --git a/ledger_pdf.hpp b/ledger_pdf.hpp
new file mode 100644
index 0000000..e0b0534
--- /dev/null
+++ b/ledger_pdf.hpp
@@ -0,0 +1,35 @@
+// Ledger PDF generation.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef ledger_pdf_hpp
+#define ledger_pdf_hpp
+
+#include "config.hpp"
+
+#include <boost/filesystem/path.hpp>
+
+#include <string>
+
+class Ledger;
+
+std::string write_ledger_as_pdf(Ledger const&, fs::path const&);
+
+#endif // ledger_pdf_hpp
diff --git a/ledger_pdf_generator.cpp b/ledger_pdf_generator.cpp
new file mode 100644
index 0000000..5061b67
--- /dev/null
+++ b/ledger_pdf_generator.cpp
@@ -0,0 +1,46 @@
+// Generate PDF files with ledger data.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile.hpp"
+
+#include "ledger_pdf_generator.hpp"
+
+#include "callback.hpp"
+
+namespace
+{
+callback<ledger_pdf_generator::creator_type>
+    group_quote_pdf_generator_create_callback;
+} // Unnamed namespace.
+
+typedef ledger_pdf_generator::creator_type FunctionPointer;
+template<> FunctionPointer callback<FunctionPointer>::function_pointer_ = 
nullptr;
+
+bool ledger_pdf_generator::set_creator(creator_type f)
+{
+    group_quote_pdf_generator_create_callback.initialize(f);
+    return true;
+}
+
+std::shared_ptr<ledger_pdf_generator> ledger_pdf_generator::create()
+{
+    return group_quote_pdf_generator_create_callback()();
+}
diff --git a/ledger_pdf_generator.hpp b/ledger_pdf_generator.hpp
new file mode 100644
index 0000000..3b81ff0
--- /dev/null
+++ b/ledger_pdf_generator.hpp
@@ -0,0 +1,63 @@
+// Generate PDF files with ledger data.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef ledger_pdf_generator_hpp
+#define ledger_pdf_generator_hpp
+
+#include "config.hpp"
+
+#include "so_attributes.hpp"
+
+#include <boost/filesystem/path.hpp>
+
+#include <memory>                       // std::shared_ptr
+
+class Ledger;
+
+/// Abstract base class for generating PDFs with ledger data.
+///
+/// Although there is currently only a single concrete implementation of this
+/// abstract base class and no other implementations are planned, splitting the
+/// PDF generation functionality into an abstract base and the concrete derived
+/// class is still needed because the former is part of liblmi while the latter
+/// uses wxPdfDocument and other wx facilities and is only part of libskeleton.
+
+class LMI_SO ledger_pdf_generator
+{
+  public:
+    typedef std::shared_ptr<ledger_pdf_generator> (*creator_type)();
+
+    static bool set_creator(creator_type);
+    static std::shared_ptr<ledger_pdf_generator> create();
+
+    virtual ~ledger_pdf_generator() = default;
+
+    virtual void write(Ledger const& ledger, fs::path const& output) = 0;
+
+  protected:
+    ledger_pdf_generator() = default;
+
+  private:
+    ledger_pdf_generator(ledger_pdf_generator const&) = delete;
+    ledger_pdf_generator& operator=(ledger_pdf_generator const&) = delete;
+};
+
+#endif // ledger_pdf_generator_hpp
diff --git a/ledger_pdf_generator_wx.cpp b/ledger_pdf_generator_wx.cpp
new file mode 100644
index 0000000..ae6d649
--- /dev/null
+++ b/ledger_pdf_generator_wx.cpp
@@ -0,0 +1,2940 @@
+// Generate PDF files with ledger data using wxPdfDocument library.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile_wx.hpp"
+
+#include "ledger_pdf_generator.hpp"
+
+#include "alert.hpp"
+#include "assert_lmi.hpp"
+#include "authenticity.hpp"
+#include "bourn_cast.hpp"
+#include "calendar_date.hpp"
+#include "data_directory.hpp"           // AddDataDir()
+#include "force_linking.hpp"
+#include "html.hpp"
+#include "interpolate_string.hpp"
+#include "istream_to_string.hpp"
+#include "ledger.hpp"
+#include "ledger_evaluator.hpp"
+#include "ledger_invariant.hpp"
+#include "ledger_variant.hpp"
+#include "miscellany.hpp"               // lmi_tolower()
+#include "pdf_writer_wx.hpp"
+#include "version.hpp"
+#include "wx_table_generator.hpp"
+
+#include <wx/pdfdc.h>
+
+#include <wx/image.h>
+#include <wx/log.h>
+
+#include <wx/html/m_templ.h>
+
+#include <cstdint>                      // SIZE_MAX
+#include <fstream>
+#include <map>
+#include <memory>
+#include <sstream>
+#include <stdexcept>
+#include <type_traits>                  // std::conditional
+#include <vector>
+
+LMI_FORCE_LINKING_IN_SITU(ledger_pdf_generator_wx)
+
+namespace
+{
+
+// Colour used for lines and border in the generated illustrations.
+const wxColour HIGHLIGHT_COL(0x00, 0x2f, 0x6c);
+
+// This function is also provided in <boost/algorithm/string/predicate.hpp>,
+// but it's arguably not worth adding dependency on this Boost library just for
+// this function.
+inline
+bool starts_with(std::string const& s, char const* prefix)
+{
+    return s.compare(0, strlen(prefix), prefix) == 0;
+}
+
+// Helper enums identifying the possible {Guaranteed,Current}{Zero,}
+// combinations.
+enum class base
+    {guaranteed
+    ,current
+    };
+
+enum class interest_rate
+    {zero
+    ,non_zero
+    };
+
+// And functions to retrieve their string representation.
+std::string base_suffix(base guar_or_curr)
+{
+    switch(guar_or_curr)
+        {
+        case base::guaranteed: return "Guaranteed";
+        case base::current:    return "Current"   ;
+        }
+    throw "Unreachable--unknown base value";
+}
+
+std::string ir_suffix(interest_rate zero_or_not)
+{
+    switch(zero_or_not)
+        {
+        case interest_rate::zero:     return "Zero";
+        case interest_rate::non_zero: return ""    ;
+        }
+    throw "Unreachable--unknown interest_rate value";
+}
+
+// Helper class grouping functions for dealing with interpolating strings
+// containing variable references.
+class html_interpolator
+{
+  public:
+    // Ctor takes the object used to interpolate the variables not explicitly
+    // defined using add_variable().
+    explicit html_interpolator(ledger_evaluator&& evaluator)
+        :evaluator_(evaluator)
+    {
+    }
+
+    // This function is provided to be able to delegate to it in custom
+    // interpolation functions, but usually shouldn't be called directly, just
+    // use operator() below instead.
+    std::string interpolation_func
+        (std::string const& s
+        ,interpolate_lookup_kind kind
+        ) const
+    {
+        switch(kind)
+            {
+            case interpolate_lookup_kind::variable:
+            case interpolate_lookup_kind::section:
+                return expand_html(s).as_html();
+
+            case interpolate_lookup_kind::partial:
+                return load_partial_from_file(s);
+            }
+
+        throw std::runtime_error("invalid lookup kind");
+    }
+
+    // A method which can be used to interpolate an HTML string containing
+    // references to the variables defined for this illustration. The general
+    // syntax is the same as in the global interpolate_string() function, i.e.
+    // variables are of the form "{{name}}" and section of the form
+    // "{{#name}}..{{/name}}" or "{{^name}}..{{/name}}" are also allowed and
+    // their contents is included in the expansion if and only if the variable
+    // with the given name has value "1" for the former or "0" for the latter.
+    //
+    // The variable names recognized by this function are either those defined
+    // by ledger_evaluator, i.e. scalar and vector fields of the ledger, or any
+    // variables explicitly defined by add_variable() calls.
+    html::text operator()(char const* s) const
+    {
+        return html::text::from_html
+            (interpolate_string
+                (s
+                ,[this]
+                    (std::string const& str
+                    ,interpolate_lookup_kind kind
+                    )
+                    {
+                        return interpolation_func(str, kind);
+                    }
+                )
+            );
+    }
+
+    html::text operator()(std::string const& s) const
+    {
+        return (*this)(s.c_str());
+    }
+
+    // Add a variable, providing either its raw text or already escaped HTML
+    // representation. Boolean values are converted to strings "0" or "1" as
+    // expected.
+    void add_variable(std::string const& name, html::text const& value)
+    {
+        vars_[name] = value;
+    }
+
+    void add_variable(std::string const& name, std::string const& value)
+    {
+        add_variable(name, html::text::from(value));
+    }
+
+    void add_variable(std::string const& name, int value)
+    {
+        std::ostringstream oss;
+        oss << value;
+        add_variable(name, oss.str());
+    }
+
+    void add_variable(std::string const& name, bool value)
+    {
+        add_variable(name, std::string(value ? "1" : "0"));
+    }
+
+    // Detect, at compile-time, mistaken attempts to add floating point
+    // variables: all those are only available from ledger_evaluator as they
+    // must be formatted correctly.
+    void add_variable(std::string const& name, double value) = delete;
+
+    // Test a boolean variable: the value must be "0" or "1", which is mapped
+    // to false or true respectively. Anything else results in an exception.
+    bool test_variable(std::string const& name) const
+    {
+        auto const z = expand_html(name).as_html();
+        return
+              z == "1" ? true
+            : z == "0" ? false
+            : throw std::runtime_error
+                ("Variable '" + name + "' has non-boolean value '" + z + "'"
+                )
+            ;
+    }
+
+    // Return the value of a single scalar variable.
+    std::string evaluate(std::string const& name) const
+    {
+        return evaluator_(name);
+    }
+
+    // Return a single value of a vector variable.
+    std::string evaluate(std::string const& name, std::size_t index) const
+    {
+        return evaluator_(name, index);
+    }
+
+    // Interpolate the contents of the given external template.
+    //
+    // This is exactly the same as interpolating "{{>template_name}}" string
+    // but a bit more convenient to use and simpler to read.
+    html::text expand_template(std::string const& template_name) const
+    {
+        return (*this)("{{>" + template_name + "}}");
+    }
+
+  private:
+    // The expansion function used with interpolate_string().
+    html::text expand_html(std::string const& s) const
+    {
+        // Check our own variables first:
+        auto const it = vars_.find(s);
+        if(it != vars_.end())
+            {
+            return it->second;
+            }
+
+        // Then look in the ledger, either as a scalar or a vector depending on
+        // whether it has "[index]" part or not.
+        if(!s.empty() && *s.rbegin() == ']')
+            {
+            auto const open_pos = s.find('[');
+            if(open_pos == std::string::npos)
+                {
+                throw std::runtime_error
+                    ("Variable '" + s + "' doesn't have the expected '['"
+                    );
+                }
+
+            char* stop = nullptr;
+            auto const index = std::strtoul(s.c_str() + open_pos + 1, &stop, 
10);
+
+            // Conversion must have stopped at the closing bracket character
+            // and also check for overflow (notice that index == SIZE_MAX
+            // doesn't, in theory, need to indicate overflow, but in practice
+            // we're never going to have valid indices close to this number).
+            if(stop != s.c_str() + s.length() - 1 || index >= SIZE_MAX)
+                {
+                throw std::runtime_error
+                    ("Index of vector variable '" + s + "' is not a valid 
number"
+                    );
+                }
+
+            // Cast below is valid because of the check for overflow above.
+            return html::text::from
+                (evaluator_
+                    (s.substr(0, open_pos)
+                    ,static_cast<std::size_t>(index)
+                    )
+                );
+            }
+
+        return html::text::from(evaluator_(s));
+    }
+
+    std::string load_partial_from_file(std::string const& file) const
+    {
+        std::ifstream ifs(AddDataDir(file + ".mst"));
+        if(!ifs)
+            {
+            alarum()
+                << "Template file \""
+                << file
+                << ".mst\" not found."
+                << std::flush
+                ;
+            }
+        std::string partial;
+        istream_to_string(ifs, partial);
+        return partial;
+    }
+
+    // Object used for variables expansion.
+    ledger_evaluator const evaluator_;
+
+    // Variables defined for all pages of this illustration.
+    std::map<std::string, html::text> vars_;
+};
+
+// A slightly specialized table generator for the tables used in the
+// illustrations.
+class illustration_table_generator : public wx_table_generator
+{
+  public:
+    static int const rows_per_group = 5;
+
+    explicit illustration_table_generator(pdf_writer_wx& writer)
+        :wx_table_generator
+            (writer.dc()
+            ,writer.get_horz_margin()
+            ,writer.get_page_width()
+            )
+    {
+        use_condensed_style();
+        align_right();
+    }
+
+    // Return the amount of vertical space taken by separator lines in the
+    // table headers.
+    int get_separator_line_height() const
+    {
+        // This is completely arbitrary and chosen just because it seems to
+        // look well.
+        return row_height() / 2;
+    }
+};
+
+// A helper mix-in class for pages using tables which is also reused by the
+// custom wxHtmlCell showing a table.
+//
+// Derived classes must provide get_table_columns() and may also override
+// should_show_column() to hide some of these columns dynamically and then can
+// use create_table_generator() to obtain the generator object that can be used
+// to render a table with the specified columns.
+class using_illustration_table
+{
+  protected:
+    // Description of a single table column.
+    struct illustration_table_column
+    {
+        std::string const variable_name;
+        std::string const label;
+        std::string const widest_text;
+    };
+
+    using illustration_table_columns = std::vector<illustration_table_column>;
+
+    // Must be overridden to return the description of the table columns.
+    virtual illustration_table_columns const& get_table_columns() const = 0;
+
+    // May be overridden to return false if the given column shouldn't be shown
+    // for the specific ledger values (currently used to exclude individual
+    // columns from composite illustrations).
+    virtual bool should_show_column(Ledger const& ledger, int column) const
+    {
+        stifle_warning_for_unused_value(ledger);
+        stifle_warning_for_unused_value(column);
+        return true;
+    }
+
+    // Useful helper for creating the table generator using the columns defined
+    // by the separate (and simpler to implement) get_table_columns() pure
+    // virtual method.
+    illustration_table_generator create_table_generator
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ) const
+    {
+        // Set the smaller font used for all tables before creating the table
+        // generator which uses the DC font for its measurements.
+        auto& dc = writer.dc();
+        auto font = dc.GetFont();
+        font.SetPointSize(9);
+        dc.SetFont(font);
+
+        illustration_table_generator table(writer);
+
+        // But set the highlight colour for drawing separator lines after
+        // creating it to override its default pen.
+        dc.SetPen(HIGHLIGHT_COL);
+
+        int column = 0;
+        for(auto const& i : get_table_columns())
+            {
+            std::string label;
+            if(should_show_column(ledger, column++))
+                {
+                label = i.label;
+                }
+            //else: Leave the label empty to avoid showing the column.
+
+            table.add_column(label, i.widest_text);
+            }
+
+        return table;
+    }
+};
+
+// Base class for our custom HTML cells providing a way to pass them
+// information about the PDF document being generated and the ledger used to
+// generate it.
+class html_cell_for_pdf_output : public wxHtmlCell
+{
+  public:
+    // Before using this class a pdf_context_setter object needs to be
+    // instantiated (and remain alive for as long as this class is used).
+    class pdf_context_setter
+    {
+      public:
+        // References passed to the ctor must have lifetime greater than that
+        // of this object itself.
+        explicit pdf_context_setter
+            (Ledger const& ledger
+            ,pdf_writer_wx& writer
+            ,html_interpolator const& interpolate_html
+            )
+        {
+            html_cell_for_pdf_output::pdf_context_for_html_output.set
+                (&ledger
+                ,&writer
+                ,&interpolate_html
+                );
+        }
+
+        ~pdf_context_setter()
+        {
+            html_cell_for_pdf_output::pdf_context_for_html_output.set
+                (nullptr
+                ,nullptr
+                ,nullptr
+                );
+        }
+    };
+
+  protected:
+    // This is ugly, but we have to use a global variable to make pdf_writer_wx
+    // and wxDC objects used by the main code accessible to this cell class,
+    // there is no way to pass them as parameters through wxHTML machinery.
+    //
+    // To at least make it a little bit safer to deal with this, the variable
+    // itself is private and a public pdf_context_setter class is provided to
+    // actually set it.
+    class pdf_context
+    {
+      public:
+        void set
+            (Ledger const* ledger
+            ,pdf_writer_wx* writer
+            ,html_interpolator const* interpolate_html
+            )
+        {
+            ledger_ = ledger;
+            writer_ = writer;
+            interpolate_html_ = interpolate_html;
+        }
+
+        Ledger const& ledger() const
+        {
+            LMI_ASSERT(ledger_);
+            return *ledger_;
+        }
+
+        pdf_writer_wx& writer() const
+        {
+            LMI_ASSERT(writer_);
+            return *writer_;
+        }
+
+        html_interpolator const& interpolate_html() const
+        {
+            LMI_ASSERT(interpolate_html_);
+            return *interpolate_html_;
+        }
+
+      private:
+        Ledger const* ledger_ = nullptr;
+        pdf_writer_wx* writer_ = nullptr;
+        html_interpolator const* interpolate_html_ = nullptr;
+    };
+
+    // Small helper to check that we're using the expected DC and, also, acting
+    // as a sink for the never used parameters of Draw().
+    void draw_check_precondition
+        (wxDC& dc
+        ,int view_y1
+        ,int view_y2
+        ,wxHtmlRenderingInfo& info
+        )
+    {
+        // The DC passed to this function is supposed to be the same as the one
+        // associated with the writer we will use for rendering, but check that
+        // this is really so in order to avoid unexpectedly drawing the table
+        // on something else.
+        LMI_ASSERT(&dc == &pdf_context_for_html_output.writer().dc());
+
+        // There is no need to optimize drawing by restricting it to the
+        // currently shown positions, we always render the cell entirely.
+        stifle_warning_for_unused_value(view_y1);
+        stifle_warning_for_unused_value(view_y2);
+
+        // We don't care about rendering state as we don't support interactive
+        // selection anyhow.
+        stifle_warning_for_unused_value(info);
+    }
+
+    static pdf_context pdf_context_for_html_output;
+
+    friend pdf_context_setter;
+};
+
+html_cell_for_pdf_output::pdf_context
+html_cell_for_pdf_output::pdf_context_for_html_output;
+
+// Define scaffolding for a custom HTML "scaled_image" tag which must be used
+// instead of the standard "a" in order to allow specifying the scaling factor
+// that we want to use for the image in the PDF. Unfortunately this can't be
+// achieved by simply using "width" and/or "height" attributes of the "a" tag
+// because their values can only be integers which is not precise enough to
+// avoid (slightly but noticeably) distorting the image due to the aspect ratio
+// being not quite right.
+
+class scaled_image_cell : public html_cell_for_pdf_output
+{
+  public:
+    scaled_image_cell
+        (wxImage const& image
+        ,wxString const& src
+        ,double scale_factor
+        )
+        :image_(image)
+        ,src_(src)
+        ,scale_factor_(scale_factor)
+    {
+        m_Width  = wxRound(image.GetWidth () / scale_factor);
+        m_Height = wxRound(image.GetHeight() / scale_factor);
+    }
+
+    // Override the base class method to actually render the image.
+    void Draw
+        (wxDC& dc
+        ,int x
+        ,int y
+        ,int view_y1
+        ,int view_y2
+        ,wxHtmlRenderingInfo& info
+        ) override
+    {
+        draw_check_precondition(dc, view_y1, view_y2, info);
+
+        auto& writer = pdf_context_for_html_output.writer();
+
+        x += m_PosX;
+
+        int pos_y = y + m_PosY;
+        writer.output_image(image_, src_.utf8_str(), scale_factor_, x, &pos_y);
+    }
+
+  private:
+    wxImage const image_;
+    wxString const src_;
+    double const scale_factor_;
+};
+
+TAG_HANDLER_BEGIN(scaled_image, "SCALED_IMAGE")
+    TAG_HANDLER_PROC(tag)
+    {
+        wxString src;
+        if (!tag.GetParamAsString("SRC", &src))
+            {
+            throw std::runtime_error
+                ("missing mandatory \"src\" attribute of \"scaled_image\" tag"
+                );
+            }
+
+        // The scale factor is optional.
+        double scale_factor = 1.;
+
+        // But if it is given, we currently specify its inverse in HTML just
+        // because it so happens that for the scale factors we use the inverse
+        // can be expressed exactly in decimal notation, while the factor
+        // itself can't. In principle, the converse could also happen and we
+        // might add support for "factor" attribute too in this case. Or we
+        // could use separate "numerator" and "denominator" attributes. But for
+        // now implement just the bare minimum of what we need.
+        wxString inv_factor_str;
+        if (tag.GetParamAsString("INV_FACTOR", &inv_factor_str))
+            {
+            double inv_factor = 0.;
+            if (!inv_factor_str.ToCDouble(&inv_factor) || inv_factor == 0.)
+                {
+                throw std::runtime_error
+                    ( "invalid value for \"inv_factor\" attribute of "
+                      "\"scaled_image\" tag: \""
+                    + inv_factor_str.ToStdString()
+                    + "\""
+                    );
+                }
+
+            scale_factor = 1./inv_factor;
+            }
+
+        wxImage image;
+        // Disable error logging, we'll simply ignore the tag if the image is
+        // not present.
+            {
+            wxLogNull noLog;
+            image.LoadFile(src);
+            }
+
+        if (image.IsOk())
+            {
+            m_WParser->GetContainer()->InsertCell
+                (new scaled_image_cell(image, src, scale_factor)
+                );
+            }
+
+        // This tag isn't supposed to have any inner contents, so return true
+        // to not even try parsing it.
+        return true;
+    }
+TAG_HANDLER_END(scaled_image)
+
+class pdf_illustration;
+
+// Base class for all logical illustration pages.
+//
+// A single logical page may result in multiple physical pages of output, e.g.
+// if it contains a table not fitting on one page, but mostly these page
+// objects correspond to a single physical page of the resulting illustration.
+class page
+{
+  public:
+    page() = default;
+
+    // Pages are not value-like objects, so prohibit copying them.
+    page(page const&) = delete;
+    page& operator=(page const&) = delete;
+
+    // Make base class dtor virtual.
+    virtual ~page() = default;
+
+    // Associate the illustration object using this page with it.
+    //
+    // This object is not passed as a ctor argument because it would be
+    // redundant, instead it is associated with the page when it's added to an
+    // illustration. This method is supposed to be called only once and only by
+    // pdf_illustration this page is being added to.
+    void illustration(pdf_illustration const& illustration)
+    {
+        LMI_ASSERT(!illustration_);
+
+        illustration_ = &illustration;
+    }
+
+    // Called before rendering any pages to prepare for doing this, e.g. by
+    // computing the number of pages needed.
+    //
+    // This method must not draw anything on the wxDC, it is provided only for
+    // measurement purposes.
+    virtual void pre_render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        )
+    {
+        stifle_warning_for_unused_value(ledger);
+        stifle_warning_for_unused_value(writer);
+        stifle_warning_for_unused_value(interpolate_html);
+    }
+
+    // Render this page contents.
+    virtual void render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) = 0;
+
+  protected:
+    // Helper method for rendering the contents of the given external template,
+    // which is expected to be found in the file with the provided name and
+    // ".mst" extension in the data directory.
+    //
+    // Return the height of the page contents.
+    int render_page_template
+        (std::string const& template_name
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        )
+    {
+        return writer.output_html
+            (writer.get_horz_margin()
+            ,writer.get_vert_margin()
+            ,writer.get_page_width()
+            ,interpolate_html.expand_template(template_name)
+            );
+    }
+
+    // The associated illustration, which will be non-null by the time our
+    // virtual methods such as pre_render() and render() are called.
+    pdf_illustration const* illustration_ = nullptr;
+};
+
+// Base class for the different kinds of illustrations.
+//
+// This object contains pages, added to it using its add() method, as well as
+// illustration-global data registered as variables with html_interpolator and
+// so available for the pages when expanding the external templates defining
+// their contents.
+class pdf_illustration : protected html_interpolator
+{
+  public:
+    pdf_illustration(Ledger const& ledger
+                    ,fs::path const& output
+                    )
+        :html_interpolator(ledger.make_evaluator())
+        ,writer_(output.string(), wxPORTRAIT, &html_font_sizes)
+        ,ledger_(ledger)
+    {
+        init_variables();
+    }
+
+    // Make base class dtor virtual.
+    virtual ~pdf_illustration() = default;
+
+    // Add a page.
+    //
+    // This is a template just in order to save on writing std::make_unique<>()
+    // in the code using it to make it slightly shorter.
+    template<typename T, typename... Args>
+    void add(Args&&... args)
+    {
+        auto page = std::make_unique<T>(std::forward<Args>(args)...);
+        page->illustration(*this);
+        pages_.emplace_back(std::move(page));
+    }
+
+    // Render all pages.
+    void render_all()
+    {
+        html_cell_for_pdf_output::pdf_context_setter
+            set_pdf_context(ledger_, writer_, *this);
+
+        for(auto const& page : pages_)
+            {
+            page->pre_render(ledger_, writer_, *this);
+            }
+
+        bool first = true;
+        for(auto const& page : pages_)
+            {
+            if(first)
+                {
+                // We shouldn't start a new page before the very first one.
+                first = false;
+                }
+            else
+                {
+                // Do start a new physical page before rendering all the
+                // subsequent pages (notice that a page is also free to call
+                // StartPage() from its render()).
+                writer_.dc().StartPage();
+                }
+
+            page->render(ledger_, writer_, *this);
+            }
+    }
+
+    // Methods to be implemented by the derived classes to indicate which
+    // templates should be used for the upper (above the separating line) and
+    // the lower parts of the footer. The upper template name may be empty if
+    // it is not used at all.
+    //
+    // Notice that the upper footer template name can be overridden at the page
+    // level, the methods here define the default for all illustration pages.
+    //
+    // These methods are used by the pages deriving from page_with_footer.
+    virtual std::string get_upper_footer_template_name() const = 0;
+    virtual std::string get_lower_footer_template_name() const = 0;
+
+  protected:
+    // Explicitly retrieve the base class.
+    html_interpolator const& get_interpolator() const {return *this;}
+
+    // Helper for abbreviating a string to at most the given length (in bytes).
+    static std::string abbreviate_if_necessary(std::string s, size_t len)
+    {
+        if(s.length() > len && len > 3)
+            {
+            s.replace(len - 3, std::string::npos, "...");
+            }
+
+        return s;
+    }
+
+    // Helper for creating abbreviated variables in the derived classes: such
+    // variables have the name based on the name of the original variable with
+    // "Abbrev" and "len" appended to it and their value is at most "len" bytes
+    // long.
+    void add_abbreviated_variable(std::string const& var, size_t len)
+    {
+        add_variable
+            (var + "Abbrev" + std::to_string(len)
+            ,abbreviate_if_necessary(evaluate(var), len)
+            );
+    }
+
+  private:
+    // Define variables that can be used when interpolating pages contents.
+    void init_variables()
+    {
+        // The variables defined here are used by all, or at least more than
+        // one, illustration kinds. Variables only used in the templates of a
+        // single illustration type should be defined in the corresponding
+        // derived pdf_illustration_xxx class instead.
+
+        add_variable
+            ("date_prepared"
+            , html::text::from(evaluate("PrepMonth"))
+            + html::text::nbsp()
+            + html::text::from(evaluate("PrepDay"))
+            + html::text::from(", ")
+            + html::text::from(evaluate("PrepYear"))
+            );
+
+        auto indent = html::text::nbsp();
+        add_variable("Space1", indent);
+
+        indent += indent;
+        add_variable("Space2", indent);
+
+        indent += indent;
+        add_variable("Space4", indent);
+
+        indent += indent;
+        add_variable("Space8", indent);
+
+        indent += indent;
+        add_variable("Space16", indent);
+
+        indent += indent;
+        add_variable("Space32", indent);
+
+        indent += indent;
+        add_variable("Space64", indent);
+
+        auto const& invar = ledger_.GetLedgerInvariant();
+
+        add_abbreviated_variable("CorpName", 60);
+        add_abbreviated_variable("Insured1", 30);
+
+        // Define the variables needed by contract_numbers template.
+        add_variable
+            ("HasMasterContract"
+            ,!invar.MasterContractNumber.empty()
+            );
+        add_variable
+            ("HasPolicyNumber"
+            ,!invar.ContractNumber.empty()
+            );
+
+        size_t const full_abbrev_length = 30;
+        add_abbreviated_variable("MasterContractNumber", full_abbrev_length);
+        add_abbreviated_variable("MasterContractNumber", full_abbrev_length / 
2);
+        add_abbreviated_variable("ContractNumber", full_abbrev_length);
+        add_abbreviated_variable("ContractNumber", full_abbrev_length / 2);
+
+        add_variable
+            ("HasComplianceTrackingNumber"
+            ,expand_template("imprimatur")
+                .as_html().find_first_not_of(" \n")
+                != std::string::npos
+            );
+
+        add_variable
+            ("HasScaleUnit"
+            ,!invar.ScaleUnit().empty()
+            );
+
+        add_variable
+            ("DefnLifeInsIsGPT"
+            ,invar.DefnLifeIns == "GPT"
+            );
+
+        add_variable
+            ("MecYearPlus1"
+            ,bourn_cast<int>(invar.MecYear) + 1
+            );
+
+        add_variable
+            ("UWTypeIsMedical"
+            ,invar.UWType == "Medical"
+            );
+
+        add_variable
+            ("UWClassIsRated"
+            ,invar.UWClass == "Rated"
+            );
+
+        auto const& policy_name = invar.PolicyLegalName;
+        add_variable
+            ("GroupCarveout"
+            ,policy_name == "Group Flexible Premium Adjustable Life Insurance 
Certificate"
+            );
+
+        auto const& state_abbrev = invar.GetStatePostalAbbrev();
+        add_variable
+            ("StateIsCarolina"
+            ,state_abbrev == "NC" || state_abbrev == "SC"
+            );
+
+        add_variable
+            ("StateIsMaryland"
+            ,state_abbrev == "MD"
+            );
+    }
+
+    // This array stores the non-default font sizes that are used to make it
+    // simpler to replicate the existing illustrations.
+    static std::array<int, 7> const html_font_sizes;
+
+    // Writer object used for the page metrics and higher level functions.
+    pdf_writer_wx writer_;
+
+    // Source of the data.
+    Ledger const& ledger_;
+
+    // All the pages of this illustration.
+    std::vector<std::unique_ptr<page>> pages_;
+};
+
+std::array<int, 7> const pdf_illustration::html_font_sizes
+    {
+    { 8
+    , 9
+    ,10
+    ,12
+    ,14
+    ,18
+    ,20
+    }
+    };
+
+// Cover page used by several different illustration kinds.
+class cover_page : public page
+{
+  public:
+    void render
+        (Ledger const& /* ledger */
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        int const height_contents = render_page_template
+            ("cover"
+            ,writer
+            ,interpolate_html
+            );
+
+        // There is no way to draw a border around the page contents in wxHTML
+        // currently, so do it manually.
+        auto& dc = writer.dc();
+
+        dc.SetPen(wxPen(HIGHLIGHT_COL, 2));
+        dc.SetBrush(*wxTRANSPARENT_BRUSH);
+
+        dc.DrawRectangle
+            (writer.get_horz_margin()
+            ,writer.get_vert_margin()
+            ,writer.get_page_width()
+            ,height_contents
+            );
+    }
+};
+
+// Base class for all pages with a footer.
+class page_with_footer : public page
+{
+  public:
+    // Override pre_render() to compute footer_top_ which is needed in the
+    // derived classes overridden get_extra_pages_needed().
+    void pre_render
+        (Ledger const& /* ledger */
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        auto const frame_horz_margin = writer.get_horz_margin();
+        auto const frame_width       = writer.get_page_width();
+
+        // We implicitly assume here that get_footer_lower_html() result
+        // doesn't materially depend on the exact value of the page number as
+        // we don't know its definitive value here yet. In theory, this doesn't
+        // need to be true, e.g. we may later discover that 10 pages are needed
+        // instead of 9 and the extra digit might result in a line wrapping on
+        // a new line and this increasing the footer height, but in practice
+        // this doesn't risk happening and taking into account this possibility
+        // wouldn't be simple at all, so just ignore this possibility.
+        auto footer_height = writer.output_html
+            (frame_horz_margin
+            ,0
+            ,frame_width
+            ,get_footer_lower_html(interpolate_html)
+            ,e_output_measure_only
+            );
+
+        auto const& upper_template = get_upper_footer_template_name();
+        if(!upper_template.empty())
+            {
+            footer_height += writer.output_html
+                (frame_horz_margin
+                ,0
+                ,frame_width
+                ,interpolate_html.expand_template(upper_template)
+                ,e_output_measure_only
+                );
+
+            // Leave a gap between the upper part of the footer and the main
+            // page contents to separate them in absence of a separator line
+            // which delimits the lower part.
+            footer_height += writer.dc().GetCharHeight();
+            }
+
+        footer_top_ = writer.get_page_bottom() - footer_height;
+    }
+
+    void render
+        (Ledger const& /* ledger */
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        auto const frame_horz_margin = writer.get_horz_margin();
+        auto const frame_width       = writer.get_page_width();
+
+        auto& dc = writer.dc();
+
+        auto y = footer_top_;
+
+        auto const& upper_template = get_upper_footer_template_name();
+        if(!upper_template.empty())
+            {
+            y += dc.GetCharHeight();
+
+            y += writer.output_html
+                (frame_horz_margin
+                ,y
+                ,frame_width
+                ,interpolate_html.expand_template(upper_template)
+                );
+            }
+
+        writer.output_html
+            (frame_horz_margin
+            ,y
+            ,frame_width
+            ,get_footer_lower_html(interpolate_html)
+            );
+
+        dc.SetPen(HIGHLIGHT_COL);
+        dc.DrawLine
+            (frame_horz_margin
+            ,y
+            ,frame_width + frame_horz_margin
+            ,y
+            );
+    }
+
+  protected:
+    // Helper for the derived pages to get the vertical position of the footer.
+    // Notice that it can only be used after calling our pre_render() method
+    // as this is where it is computed.
+    int get_footer_top() const
+    {
+        LMI_ASSERT(footer_top_ != 0);
+
+        return footer_top_;
+    }
+
+  private:
+    // Method to be overridden in the base class which should actually return
+    // the page number or equivalent string (e.g. "Appendix").
+    virtual std::string get_page_number() const = 0;
+
+    // This method forwards to the illustration by default, but can be
+    // overridden to define a page-specific footer if necessary.
+    virtual std::string get_upper_footer_template_name() const
+    {
+        return illustration_->get_upper_footer_template_name();
+    }
+
+    // This method uses get_page_number() and returns the HTML wrapping it
+    // and other fixed information appearing in the lower part of the footer.
+    html::text get_footer_lower_html(html_interpolator const& 
interpolate_html) const
+    {
+        auto const page_number_str = get_page_number();
+
+        auto const templ = illustration_->get_lower_footer_template_name();
+
+        // Use our own interpolation function to handle the special
+        // "page_number" variable that is replaced with the actual
+        // (possibly dynamic) page number.
+        return html::text::from_html
+            (interpolate_string
+                (("{{>" + templ + "}}").c_str()
+                ,[page_number_str, interpolate_html]
+                    (std::string const& s
+                    ,interpolate_lookup_kind kind
+                    ) -> std::string
+                    {
+                    if(s == "page_number")
+                        {
+                        return page_number_str;
+                        }
+
+                    return interpolate_html.interpolation_func(s, kind);
+                    }
+                )
+            );
+    }
+
+    int footer_top_ = 0;
+};
+
+// Base class for attachment pages.
+class attachment_page : public page_with_footer
+{
+  private:
+    std::string get_page_number() const override
+    {
+        return "Attachment";
+    }
+};
+
+// Base class for all pages showing the page number in the footer.
+//
+// In addition to actually providing page_with_footer with the correct string
+// to show in the footer, this class implicitly handles the page count by
+// incrementing it whenever a new object of this class is pre-rendered.
+class numbered_page : public page_with_footer
+{
+  public:
+    // Must be called before creating the first numbered page.
+    static void start_numbering()
+    {
+        last_page_number_ = 0;
+    }
+
+    numbered_page()
+    {
+        // This assert would fail if start_numbering() hadn't been called
+        // before creating a numbered page, as it should be.
+        LMI_ASSERT(last_page_number_ >= 0);
+    }
+
+    void pre_render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        page_with_footer::pre_render(ledger, writer, interpolate_html);
+
+        this_page_number_ = ++last_page_number_;
+
+        extra_pages_ = get_extra_pages_needed
+            (ledger
+            ,writer
+            ,interpolate_html
+            );
+
+        LMI_ASSERT(extra_pages_ >= 0);
+
+        last_page_number_ += extra_pages_;
+    }
+
+    ~numbered_page() override
+    {
+        // Check that next_page() was called the expected number of times.
+        // Unfortunately we can't use LMI_ASSERT() in the (noexcept) dtor, so
+        // use warning() instead.
+        if(extra_pages_)
+            {
+            warning()
+                << "Logic error: "
+                << extra_pages_
+                << " missing extra pages."
+                << LMI_FLUSH
+                ;
+            }
+    }
+
+  protected:
+    void next_page(pdf_writer_wx& writer)
+    {
+        // This method may only be called if we had reserved enough physical
+        // pages for this logical pages by overriding get_extra_pages_needed().
+        LMI_ASSERT(extra_pages_ > 0);
+
+        writer.dc().StartPage();
+
+        this_page_number_++;
+        extra_pages_--;
+    }
+
+  private:
+    // Derived classes may override this method if they may need more than one
+    // physical page to show their contents.
+    virtual int get_extra_pages_needed
+        (Ledger const&              ledger
+        ,pdf_writer_wx&             writer
+        ,html_interpolator const&   interpolate_html
+        ) const
+    {
+        stifle_warning_for_unused_value(ledger);
+        stifle_warning_for_unused_value(writer);
+        stifle_warning_for_unused_value(interpolate_html);
+
+        return 0;
+    }
+
+    std::string get_page_number() const override
+    {
+        std::ostringstream oss;
+        oss << "Page " << this_page_number_ << " of " << last_page_number_;
+        return oss.str();
+    }
+
+    static int last_page_number_;
+    int        this_page_number_     = 0;
+    int        extra_pages_          = 0;
+};
+
+// Initial value is invalid, use start_numbering() to change it.
+int numbered_page::last_page_number_ = -1;
+
+// Simplest possible page which is entirely defined by its external template
+// whose name must be specified when constructing it.
+class standard_page : public numbered_page
+{
+  public:
+    // Accept only string literals as template names, there should be no need
+    // to use anything else.
+    template<int N>
+    explicit standard_page(char const (&page_template_name)[N])
+        :page_template_name_(page_template_name)
+    {
+    }
+
+    void render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        numbered_page::render(ledger, writer, interpolate_html);
+
+        render_page_template(page_template_name_, writer, interpolate_html);
+    }
+
+  private:
+    char const* const page_template_name_;
+};
+
+// Helper classes used to show the numeric summary table. The approach used
+// here is to define a custom HTML tag (<numeric_summary_table>) and use the
+// existing illustration_table_generator to replace it with the actual table
+// when rendering.
+//
+// Notice that we currently make the simplifying assumption that this table is
+// always short enough so that everything fits on the same page as it would be
+// much more complicated to handle page breaks in the table in the middle of a
+// page (page_with_tabular_report below handles them only for the table at the
+// bottom of the page, after all the other contents, and this is already more
+// complicated and can't be done with just a custom HTML tag as we do it here).
+
+// An HTML cell showing the contents of the numeric summary table.
+class numeric_summary_table_cell
+    :public html_cell_for_pdf_output
+    ,private using_illustration_table
+{
+  public:
+    numeric_summary_table_cell()
+    {
+        m_Height = render_or_measure(0, e_output_measure_only);
+    }
+
+    // Override the base class method to actually render the table.
+    void Draw
+        (wxDC& dc
+        ,int x
+        ,int y
+        ,int view_y1
+        ,int view_y2
+        ,wxHtmlRenderingInfo& info
+        ) override
+    {
+        draw_check_precondition(dc, view_y1, view_y2, info);
+
+        // We ignore the horizontal coordinate which is always 0 for this cell
+        // anyhow.
+        stifle_warning_for_unused_value(x);
+
+        render_or_measure(y + m_PosY, e_output_normal);
+    }
+
+  private:
+    enum
+        {column_policy_year
+        ,column_premium_outlay
+        ,column_guar_account_value
+        ,column_guar_cash_surr_value
+        ,column_guar_death_benefit
+        ,column_separator_guar_non_guar
+        ,column_mid_account_value
+        ,column_mid_cash_surr_value
+        ,column_mid_death_benefit
+        ,column_separator_mid_curr
+        ,column_curr_account_value
+        ,column_curr_cash_surr_value
+        ,column_curr_death_benefit
+        ,column_max
+        };
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"             , "Policy\nYear"       ,       "999" }
+            ,{ "GrossPmt"               , "Premium\nOutlay"    ,   "999,999" }
+            ,{ "AcctVal_Guaranteed"     , "Account\nValue"     ,   "999,999" }
+            ,{ "CSVNet_Guaranteed"      , "Cash Surr\nValue"   ,   "999,999" }
+            ,{ "EOYDeathBft_Guaranteed" , "Death\nBenefit"     , "9,999,999" }
+            ,{ ""                       , " "                  ,         "-" }
+            ,{ "AcctVal_Midpoint"       , "Account\nValue"     ,   "999,999" }
+            ,{ "CSVNet_Midpoint"        , "Cash Surr\nValue"   ,   "999,999" }
+            ,{ "EOYDeathBft_Midpoint"   , "Death\nBenefit"     , "9,999,999" }
+            ,{ ""                       , " "                  ,         "-" }
+            ,{ "AcctVal_Current"        , "Account\nValue"     ,   "999,999" }
+            ,{ "CSVNet_Current"         , "Cash Surr\nValue"   ,   "999,999" }
+            ,{ "EOYDeathBft_Current"    , "Death\nBenefit"     , "9,999,999" }
+            };
+
+        return columns;
+    }
+
+    int render_or_measure(int pos_y, enum_output_mode output_mode)
+    {
+        auto const& ledger = pdf_context_for_html_output.ledger();
+        auto& writer = pdf_context_for_html_output.writer();
+
+        illustration_table_generator
+            table{create_table_generator(ledger, writer)};
+
+        // Output multiple rows of headers.
+
+        // Make a copy because we want pos_y to be modified only once, not
+        // twice, by both output_super_header() calls.
+        auto y_copy = pos_y;
+        table.output_super_header
+            ("Guaranteed Values"
+            ,column_guar_account_value
+            ,column_separator_guar_non_guar
+            ,&y_copy
+            ,output_mode
+            );
+        table.output_super_header
+            ("Non-Guaranteed Values"
+            ,column_mid_account_value
+            ,column_max
+            ,&pos_y
+            ,output_mode
+            );
+
+        pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_guar_account_value
+            ,column_separator_guar_non_guar
+            ,pos_y
+            ,output_mode
+            );
+        table.output_horz_separator
+            (column_mid_account_value
+            ,column_max
+            ,pos_y
+            ,output_mode
+            );
+
+        y_copy = pos_y;
+        table.output_super_header
+            ("Midpoint Values"
+            ,column_mid_account_value
+            ,column_separator_mid_curr
+            ,&y_copy
+            ,output_mode
+            );
+
+        table.output_super_header
+            ("Current Values"
+            ,column_curr_account_value
+            ,column_max
+            ,&pos_y
+            ,output_mode
+            );
+
+        pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_mid_account_value
+            ,column_separator_mid_curr
+            ,pos_y
+            ,output_mode
+            );
+
+        table.output_horz_separator
+            (column_curr_account_value
+            ,column_max
+            ,pos_y
+            ,output_mode
+            );
+
+        table.output_header(&pos_y, output_mode);
+
+        pos_y += table.get_separator_line_height();
+        table.output_horz_separator(0, column_max, pos_y, output_mode);
+
+        // And now the table values themselves.
+        auto const& columns = get_table_columns();
+        std::vector<std::string> values(columns.size());
+
+        auto const& invar = ledger.GetLedgerInvariant();
+        auto const& interpolate_html = 
pdf_context_for_html_output.interpolate_html();
+
+        int const year_max = 
pdf_context_for_html_output.ledger().GetMaxLength();
+        int const age_last = 70;
+        std::array<int, 4> const summary_years =
+            {{5, 10, 20, age_last - bourn_cast<int>(invar.Age)}
+            };
+        for(auto const& year : summary_years)
+            {
+            // Skip row if it doesn't exist. For instance, if the issue
+            // age is 85 and the contract remains in force until age 100,
+            // then there is no twentieth duration and no age-70 row.
+            if(!(0 < year && year <= year_max))
+                {
+                continue;
+                }
+
+            // Last row, showing the values for "Age 70" normally, needs to be
+            // handled specially.
+            bool const is_last_row = &year == &summary_years.back();
+
+            // For composite ledgers, "Age" doesn't make sense and so this row
+            // should be just skipped for them.
+            if(is_last_row && ledger.is_composite())
+                {
+                continue;
+                }
+
+            switch(output_mode)
+                {
+                case e_output_measure_only:
+                    pos_y += table.row_height();
+                    break;
+
+                case e_output_normal:
+                    for(std::size_t col = 0; col < columns.size(); ++col)
+                        {
+                        std::string const variable_name = 
columns[col].variable_name;
+
+                        // According to regulations, we need to replace the
+                        // policy year in the last row with the age.
+                        if(col == column_policy_year)
+                            {
+                            if(is_last_row)
+                                {
+                                std::ostringstream oss;
+                                oss << "Age " << age_last;
+                                values[col] = oss.str();
+                                continue;
+                                }
+                            }
+
+                        // Special hack for the dummy columns whose value is 
always
+                        // empty as it's used only as separator.
+                        values[col] = variable_name.empty()
+                            ? std::string{}
+                            : interpolate_html.evaluate(variable_name, year - 
1)
+                            ;
+                        }
+
+                    table.output_row(&pos_y, values.data());
+                    break;
+                }
+            }
+
+        return pos_y;
+    }
+};
+
+// Custom tag which is replaced by the numeric summary table.
+TAG_HANDLER_BEGIN(numeric_summary_table, "NUMERIC_SUMMARY_TABLE")
+    TAG_HANDLER_PROC(tag)
+    {
+        // The tag argument would be useful if we defined any parameters for
+        // it, but currently we don't.
+        stifle_warning_for_unused_value(tag);
+
+        m_WParser->GetContainer()->InsertCell(new 
numeric_summary_table_cell());
+
+        // This tag isn't supposed to have any inner contents, so return true
+        // to not even try parsing it.
+        return true;
+    }
+TAG_HANDLER_END(numeric_summary_table)
+
+// In wxWidgets versions prior to 3.1.1, there is an extra semicolon at the end
+// of TAGS_MODULE_BEGIN() expansion resulting in a warning with -pedantic used
+// by lmi, so suppress this warning here (this could be removed once 3.1.1 is
+// required).
+wxGCC_WARNING_SUPPRESS(pedantic)
+
+TAGS_MODULE_BEGIN(lmi_illustration)
+    TAGS_MODULE_ADD(scaled_image)
+    TAGS_MODULE_ADD(numeric_summary_table)
+TAGS_MODULE_END(lmi_illustration)
+
+wxGCC_WARNING_RESTORE(pedantic)
+
+// Numeric summary page appears twice, once as a normal page and once as an
+// attachment, with the only difference being that the base class is different,
+// so make it a template to avoid duplicating the code.
+
+// Just a helper alias.
+template<bool is_attachment>
+using numbered_or_attachment_base = typename std::conditional
+    <is_attachment
+    ,attachment_page
+    ,numbered_page
+    >::type;
+
+template<bool is_attachment>
+class reg_numeric_summary_or_attachment_page
+    : public numbered_or_attachment_base<is_attachment>
+{
+  public:
+    void render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        numbered_or_attachment_base<is_attachment>::render
+            (ledger
+            ,writer
+            ,interpolate_html
+            );
+
+        this->render_page_template
+            ("reg_numeric_summary"
+            ,writer
+            ,interpolate_html
+            );
+    }
+};
+
+// Helper base class for pages showing a table displaying values for all
+// contract years after some fixed content.
+class page_with_tabular_report
+    :public numbered_page
+    ,protected using_illustration_table
+{
+  public:
+    void render
+        (Ledger const& ledger
+        ,pdf_writer_wx& writer
+        ,html_interpolator const& interpolate_html
+        ) override
+    {
+        numbered_page::render(ledger, writer, interpolate_html);
+
+        illustration_table_generator
+            table{create_table_generator(ledger, writer)};
+
+        auto const& columns = get_table_columns();
+
+        // Just some cached values used inside the loop below.
+        auto const row_height = table.row_height();
+        auto const page_bottom = get_footer_top();
+        auto const rows_per_group = 
illustration_table_generator::rows_per_group;
+        std::vector<std::string> values(columns.size());
+
+        // The table may need several pages, loop over them.
+        int const year_max = ledger.GetMaxLength();
+        for(int year = 0; year < year_max; ++year)
+            {
+            int pos_y = render_or_measure_fixed_page_part
+                (table
+                ,writer
+                ,interpolate_html
+                ,e_output_normal
+                );
+
+            for(; year < year_max; ++year)
+                {
+                for(std::size_t col = 0; col < columns.size(); ++col)
+                    {
+                    std::string const variable_name = 
columns[col].variable_name;
+
+                    // Special hack for the dummy columns used in some reports,
+                    // whose value is always empty as it's used only as
+                    // separator.
+                    values[col] = variable_name.empty()
+                        ? std::string{}
+                        : interpolate_html.evaluate(variable_name, year)
+                        ;
+                    }
+
+                table.output_row(&pos_y, values.data());
+
+                if((year + 1) % rows_per_group == 0)
+                    {
+                    // We need a group break.
+                    pos_y += row_height;
+
+                    // And possibly a page break, which will be necessary if 
we don't
+                    // have enough space for another full group because we 
don't want
+                    // to have page breaks in the middle of a group.
+                    if(pos_y >= page_bottom - rows_per_group*row_height)
+                        {
+                        next_page(writer);
+                        numbered_page::render(ledger, writer, 
interpolate_html);
+                        break;
+                        }
+                    }
+                }
+            }
+    }
+
+  protected:
+    // Must be overridden to return the template containing the fixed page 
part.
+    virtual std::string get_fixed_page_contents_template_name() const = 0;
+
+    // May be overridden to render (only if output_mode is e_output_normal)
+    // the extra headers just above the regular table headers.
+    //
+    // If this function does anything, it must show the first super-header at
+    // pos_y and update it to account for the added lines. The base class
+    // version does nothing.
+    virtual void render_or_measure_extra_headers
+        (illustration_table_generator&  table
+        ,html_interpolator const&       interpolate_html
+        ,int*                           pos_y
+        ,enum_output_mode               output_mode
+        ) const
+    {
+        stifle_warning_for_unused_value(table);
+        stifle_warning_for_unused_value(interpolate_html);
+        stifle_warning_for_unused_value(pos_y);
+        stifle_warning_for_unused_value(output_mode);
+    }
+
+  private:
+    // Render (only if output_mode is e_output_normal) the fixed page part and
+    // (in any case) return the vertical coordinate of its bottom, where the
+    // tabular report starts.
+    int render_or_measure_fixed_page_part
+        (illustration_table_generator&  table
+        ,pdf_writer_wx&                 writer
+        ,html_interpolator const&       interpolate_html
+        ,enum_output_mode               output_mode
+        ) const
+    {
+        int pos_y = writer.get_vert_margin();
+
+        pos_y += writer.output_html
+            (writer.get_horz_margin()
+            ,pos_y
+            ,writer.get_page_width()
+            ,interpolate_html.expand_template
+                (get_fixed_page_contents_template_name()
+                )
+            ,output_mode
+            );
+
+        render_or_measure_extra_headers
+            (table
+            ,interpolate_html
+            ,&pos_y
+            ,output_mode
+            );
+
+        table.output_header(&pos_y, output_mode);
+
+        pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (0
+            ,table.columns_count()
+            ,pos_y
+            ,output_mode
+            );
+
+        return pos_y;
+    }
+
+    // Override the base class method as the table may overflow onto the next
+    // page(s).
+    int get_extra_pages_needed
+        (Ledger const&              ledger
+        ,pdf_writer_wx&             writer
+        ,html_interpolator const&   interpolate_html
+        ) const override
+    {
+        illustration_table_generator
+            table{create_table_generator(ledger, writer)};
+
+        int const pos_y = render_or_measure_fixed_page_part
+            (table
+            ,writer
+            ,interpolate_html
+            ,e_output_measure_only
+            );
+
+        int const rows_per_page = (get_footer_top() - pos_y) / 
table.row_height();
+
+        int const rows_per_group = 
illustration_table_generator::rows_per_group;
+
+        if(rows_per_page < rows_per_group)
+            {
+            // We can't afford to continue in this case as we can never output
+            // the table as the template simply doesn't leave enough space for
+            // it on the page.
+            throw std::runtime_error("no space left for tabular report");
+            }
+
+        // Each group actually takes rows_per_group+1 rows because of the
+        // separator row between groups, hence the second +1, but there is no
+        // need for the separator after the last group, hence the first +1.
+        int const groups_per_page = (rows_per_page + 1) / (rows_per_group + 1);
+
+        // But we are actually interested in the number of years per page and
+        // not the number of groups.
+        int const years_per_page = groups_per_page * rows_per_group;
+
+        // Finally determine how many pages we need to show all the years.
+        return ledger.GetMaxLength() / years_per_page;
+    }
+};
+
+class reg_tabular_detail_page : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_premium_outlay
+        ,column_guar_account_value
+        ,column_guar_cash_surr_value
+        ,column_guar_death_benefit
+        ,column_dummy_separator
+        ,column_curr_account_value
+        ,column_curr_cash_surr_value
+        ,column_curr_death_benefit
+        ,column_max
+        };
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_tabular_details";
+    }
+
+    std::string get_upper_footer_template_name() const override
+    {
+        return "reg_footer_disclaimer";
+    }
+
+    void render_or_measure_extra_headers
+        (illustration_table_generator&  table
+        ,html_interpolator const&       interpolate_html
+        ,int*                           pos_y
+        ,enum_output_mode               output_mode
+        ) const override
+    {
+        stifle_warning_for_unused_value(interpolate_html);
+
+        // Make a copy because we want the real pos_y to be modified only once,
+        // not twice, by both output_super_header() calls.
+        auto pos_y_copy = *pos_y;
+        table.output_super_header
+            ("Guaranteed Values"
+            ,column_guar_account_value
+            ,column_dummy_separator
+            ,&pos_y_copy
+            ,output_mode
+            );
+        table.output_super_header
+            ("Non-Guaranteed Values"
+            ,column_curr_account_value
+            ,column_max
+            ,pos_y
+            ,output_mode
+            );
+
+        *pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_guar_account_value
+            ,column_dummy_separator
+            ,*pos_y
+            ,output_mode
+            );
+        table.output_horz_separator
+            (column_curr_account_value
+            ,column_max
+            ,*pos_y
+            ,output_mode
+            );
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"             , "Policy\nYear"       ,       "999" }
+            ,{ "AttainedAge"            , "End of\nYear Age"   ,       "999" }
+            ,{ "GrossPmt"               , "Premium\nOutlay"    ,   "999,999" }
+            ,{ "AcctVal_Guaranteed"     , "Account\nValue"     ,   "999,999" }
+            ,{ "CSVNet_Guaranteed"      , "Cash Surr\nValue"   ,   "999,999" }
+            ,{ "EOYDeathBft_Guaranteed" , "Death\nBenefit"     , "9,999,999" }
+            ,{ ""                       , " "                  ,      "----" }
+            ,{ "AcctVal_Current"        , "Account\nValue"     ,   "999,999" }
+            ,{ "CSVNet_Current"         , "Cash Surr\nValue"   ,   "999,999" }
+            ,{ "EOYDeathBft_Current"    , "Death\nBenefit"     , "9,999,999" }
+            };
+
+        return columns;
+    }
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        // One column should be hidden for composite ledgers.
+        return column != column_end_of_year_age || !ledger.is_composite();
+    }
+};
+
+class reg_tabular_detail2_page : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_ill_crediting_rate
+        ,column_selected_face_amount
+        ,column_max
+        };
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_tabular_details2";
+    }
+
+    std::string get_upper_footer_template_name() const override
+    {
+        return "reg_footer_disclaimer";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"          , "Policy\nYear"               ,         
"999" }
+            ,{ "AttainedAge"         , "End of\nYear Age"           ,         
"999" }
+            ,{ "AnnGAIntRate_Current", "Illustrated\nCrediting Rate",      
"99.99%" }
+            ,{ "SpecAmt"             , "Selected\nFace Amount"      , 
"999,000,000" }
+            };
+
+        return columns;
+    }
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        // One column should be hidden for composite ledgers.
+        return column != column_end_of_year_age || !ledger.is_composite();
+    }
+};
+
+// Class for pages showing supplemental report after the fixed template
+// contents. It can be either used directly or further derived from, e.g. to
+// override some of its inherited virtual methods such as
+// get_upper_footer_template_name() as done below.
+class standard_supplemental_report : public page_with_tabular_report
+{
+  public:
+    explicit standard_supplemental_report
+        (html_interpolator const& interpolate_html
+        ,std::string       const& page_template
+        )
+        :columns_(build_columns(interpolate_html))
+        ,page_template_(page_template)
+    {
+    }
+
+  private:
+    illustration_table_columns const& get_table_columns() const override
+    {
+        return columns_;
+    }
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return page_template_;
+    }
+
+    // Helper function used by the ctor to initialize the const columns_ field.
+    illustration_table_columns build_columns
+        (html_interpolator const& interpolate_html
+        )
+    {
+        constexpr std::size_t max_columns = 12;
+        std::string const empty_column_name("[none]");
+
+        illustration_table_columns columns;
+        for(std::size_t i = 0; i < max_columns; ++i)
+            {
+            auto name = 
interpolate_html.evaluate("SupplementalReportColumnsNames", i);
+            if(name != empty_column_name)
+                {
+                // We currently don't have the field width information for
+                // arbitrary fields, so use fixed width that should be
+                // sufficient for almost all of them.
+                columns.emplace_back
+                    (illustration_table_column
+                        {std::move(name)
+                        
,interpolate_html.evaluate("SupplementalReportColumnsTitles", i)
+                        ,"999,999"
+                        }
+                    );
+                }
+            }
+
+        return columns;
+    }
+
+    illustration_table_columns const columns_      ;
+    std::string                const page_template_;
+};
+
+class reg_supplemental_report : public standard_supplemental_report
+{
+  public:
+    explicit reg_supplemental_report(html_interpolator const& interpolate_html)
+        :standard_supplemental_report(interpolate_html, "reg_supp_report")
+    {
+    }
+
+  private:
+    std::string get_upper_footer_template_name() const override
+    {
+        return "reg_footer_disclaimer";
+    }
+};
+
+// Regular illustration.
+class pdf_illustration_regular : public pdf_illustration
+{
+  public:
+    pdf_illustration_regular(Ledger const& ledger
+                            ,fs::path const& output
+                            )
+        :pdf_illustration(ledger, output)
+    {
+        auto const& invar = ledger.GetLedgerInvariant();
+        auto const& policy_name = invar.PolicyLegalName;
+        auto const& state_abbrev = invar.GetStatePostalAbbrev();
+
+        // Define variables specific to this illustration which doesn't use the
+        // standard 60/30 lengths for whatever reason.
+        add_abbreviated_variable("CorpName", 50);
+        add_abbreviated_variable("Insured1", 50);
+
+        add_variable
+            ("ModifiedSinglePremium"
+            ,starts_with(policy_name, "Single") && state_abbrev == "MA"
+            );
+
+        add_variable
+            ("ModifiedSinglePremium0"
+            ,starts_with(policy_name, "Modified")
+            );
+
+        add_variable
+            ("ModifiedSinglePremiumOrModifiedSinglePremium0"
+            , test_variable("ModifiedSinglePremium")
+            ||test_variable("ModifiedSinglePremium0")
+            );
+
+        add_variable
+            ("SinglePremium"
+            ,starts_with(policy_name, "Single") || starts_with(policy_name, 
"Modified")
+            );
+
+        add_variable
+            ("GroupExperienceRating"
+            ,policy_name == "Group Flexible Premium Adjustable Life Insurance 
Policy"
+            );
+
+        // Variable representing the premium payment frequency with the
+        // appropriate indefinite article preceding it, e.g. "an annual" or "a
+        // monthly".
+        auto const er_mode = invar.ErMode[0].str();
+        if(!er_mode.empty())
+            {
+            auto const er_mode_first = lmi_tolower(er_mode[0]);
+            add_variable
+                ("ErModeLCWithArticle"
+                ,(strchr("aeiou", er_mode_first) ? "an" : "a") + 
er_mode.substr(1)
+                );
+            }
+
+        add_variable
+            ("HasProducerCity"
+            ,invar.ProducerCity != "0"
+            );
+
+        add_variable
+            ("HasInterestDisclaimer"
+            ,!invar.InterestDisclaimer.empty()
+            );
+
+        add_variable
+            ("HasGuarPrem"
+            ,invar.GuarPrem != 0
+            );
+
+        add_variable
+            ("StateIsIllinois"
+            ,state_abbrev == "IL"
+            );
+
+        add_variable
+            ("StateIsTexas"
+            ,state_abbrev == "TX"
+            );
+
+        add_variable
+            ("StateIsIllinoisOrTexas"
+            ,state_abbrev == "IL" || state_abbrev == "TX"
+            );
+
+        add_variable
+            ("UltimateInterestRate"
+            ,evaluate("AnnGAIntRate_Current", invar.InforceYear + 1)
+            );
+
+        auto const max_duration = invar.EndtAge - invar.Age;
+        auto const lapse_year_guaruanteed = ledger.GetGuarFull().LapseYear;
+        auto const lapse_year_midpoint = ledger.GetMdptFull().LapseYear;
+        auto const lapse_year_current = ledger.GetCurrFull().LapseYear;
+
+        add_variable
+            ("LapseYear_Guaranteed_LT_MaxDuration"
+            ,lapse_year_guaruanteed < max_duration
+            );
+
+        add_variable
+            ("LapseYear_Guaranteed_Plus1"
+            ,bourn_cast<int>(lapse_year_guaruanteed) + 1
+            );
+
+        add_variable
+            ("LapseYear_Midpoint_LT_MaxDuration"
+            ,lapse_year_midpoint < max_duration
+            );
+
+        add_variable
+            ("LapseYear_Midpoint_Plus1"
+            ,bourn_cast<int>(lapse_year_midpoint) + 1
+            );
+
+        add_variable
+            ("LapseYear_Current_LT_MaxDuration"
+            ,lapse_year_current < max_duration
+            );
+
+        add_variable
+            ("LapseYear_Current_Plus1"
+            ,bourn_cast<int>(lapse_year_current) + 1
+            );
+
+        // Add all the pages.
+        add<cover_page>();
+        numbered_page::start_numbering();
+        add<standard_page>("reg_narr_summary");
+        add<standard_page>("reg_narr_summary2");
+        add<standard_page>("reg_column_headings");
+        if(!invar.IsInforce)
+            {
+            add<reg_numeric_summary_or_attachment_page<false>>();
+            }
+        add<reg_tabular_detail_page>();
+        add<reg_tabular_detail2_page>();
+        if(invar.SupplementalReport)
+            {
+            add<reg_supplemental_report>(get_interpolator());
+            }
+        if(!invar.IsInforce)
+            {
+            add<reg_numeric_summary_or_attachment_page<true>>();
+            }
+    }
+
+    std::string get_upper_footer_template_name() const override
+        { return {}; }
+    std::string get_lower_footer_template_name() const override
+        { return "reg_footer"; }
+};
+
+// Common base class for basic illustration pages using the same columns in
+// both NASD and private group placement illustrations.
+class page_with_basic_tabular_report : public page_with_tabular_report
+{
+  private:
+    // This method must be overridden to return the text of the super-header
+    // used for all pairs of "cash surrogate value" and "death benefit"
+    // columns. The return value is subject to HTML interpolation and so may
+    // contain {{variables}} and also can be multiline but, if so, it must have
+    // the same number of lines for all input arguments.
+    //
+    // The base and interest_rate arguments can be used to construct the full
+    // name of the variable appropriate for the current column pair, with the
+    // help of base_suffix() and ir_suffix() functions.
+    virtual std::string get_two_column_header
+        (base          guar_or_curr
+        ,interest_rate zero_or_not
+        ) const = 0;
+
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_premium_outlay
+        ,column_guar0_cash_surr_value
+        ,column_guar0_death_benefit
+        ,column_separator_guar0_guar
+        ,column_guar_cash_surr_value
+        ,column_guar_death_benefit
+        ,column_separator_guar_curr0
+        ,column_curr0_cash_surr_value
+        ,column_curr0_death_benefit
+        ,column_separator_curr0_curr
+        ,column_curr_cash_surr_value
+        ,column_curr_death_benefit
+        ,column_max
+        };
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"                 , "Policy\nYear"     ,       "999" 
}
+            ,{ "AttainedAge"                , "End of\nYear Age" ,       "999" 
}
+            ,{ "GrossPmt"                   , "Premium\nOutlay"  ,   "999,999" 
}
+            ,{ "CSVNet_GuaranteedZero"      , "Cash Surr\nValue" ,   "999,999" 
}
+            ,{ "EOYDeathBft_GuaranteedZero" , "Death\nBenefit"   , "9,999,999" 
}
+            ,{ ""                           , " "                ,         "-" 
}
+            ,{ "CSVNet_Guaranteed"          , "Cash Surr\nValue" ,   "999,999" 
}
+            ,{ "EOYDeathBft_Guaranteed"     , "Death\nBenefit"   , "9,999,999" 
}
+            ,{ ""                           , " "                ,         "-" 
}
+            ,{ "CSVNet_CurrentZero"         , "Cash Surr\nValue" ,   "999,999" 
}
+            ,{ "EOYDeathBft_CurrentZero"    , "Death\nBenefit"   , "9,999,999" 
}
+            ,{ ""                           , " "                ,         "-" 
}
+            ,{ "CSVNet_Current"             , "Cash Surr\nValue" ,   "999,999" 
}
+            ,{ "EOYDeathBft_Current"        , "Death\nBenefit"   , "9,999,999" 
}
+            };
+
+        return columns;
+    }
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        // One column should be hidden for composite ledgers.
+        return column != column_end_of_year_age || !ledger.is_composite();
+    }
+
+    void render_or_measure_extra_headers
+        (illustration_table_generator&  table
+        ,html_interpolator const&       interpolate_html
+        ,int*                           pos_y
+        ,enum_output_mode               output_mode
+        ) const override
+    {
+        // Output the first super header row.
+
+        auto pos_y_copy = *pos_y;
+        table.output_super_header
+            ("Using guaranteed charges"
+            ,column_guar0_cash_surr_value
+            ,column_separator_guar_curr0
+            ,pos_y
+            ,output_mode
+            );
+
+        *pos_y = pos_y_copy;
+        table.output_super_header
+            ("Using current charges"
+            ,column_curr0_cash_surr_value
+            ,column_max
+            ,pos_y
+            ,output_mode
+            );
+
+        *pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_guar0_cash_surr_value
+            ,column_separator_guar_curr0
+            ,*pos_y
+            ,output_mode
+            );
+        table.output_horz_separator
+            (column_curr0_cash_surr_value
+            ,column_max
+            ,*pos_y
+            ,output_mode
+            );
+
+        // Output the second super header row which is composed of three
+        // physical lines.
+
+        // This function outputs all lines of a single header, corresponding to
+        // the "Guaranteed" or "Current", "Zero" or not, column and returns the
+        // vertical position below the header.
+        auto const output_two_column_super_header = [=,&table]
+            (base           guar_or_curr
+            ,interest_rate  zero_or_not
+            ,std::size_t    begin_column
+            ) -> int
+            {
+                std::size_t end_column = begin_column + 2;
+                LMI_ASSERT(end_column <= column_max);
+
+                auto y = *pos_y;
+
+                auto const header = get_two_column_header
+                    (guar_or_curr
+                    ,zero_or_not
+                    );
+                table.output_super_header
+                    (interpolate_html(header).as_html()
+                    ,begin_column
+                    ,end_column
+                    ,&y
+                    ,output_mode
+                    );
+
+                y += table.get_separator_line_height();
+                table.output_horz_separator
+                    (begin_column
+                    ,end_column
+                    ,y
+                    ,output_mode
+                    );
+
+                return y;
+            };
+
+        output_two_column_super_header
+            (base::guaranteed
+            ,interest_rate::zero
+            ,column_guar0_cash_surr_value
+            );
+
+        output_two_column_super_header
+            (base::guaranteed
+            ,interest_rate::non_zero
+            ,column_guar_cash_surr_value
+            );
+
+        output_two_column_super_header
+            (base::current
+            ,interest_rate::zero
+            ,column_curr0_cash_surr_value
+            );
+
+        *pos_y = output_two_column_super_header
+            (base::current
+            ,interest_rate::non_zero
+            ,column_curr_cash_surr_value
+            );
+    }
+};
+
+class nasd_basic : public page_with_basic_tabular_report
+{
+  private:
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "nasd_basic";
+    }
+
+    std::string get_two_column_header
+        (base          guar_or_curr
+        ,interest_rate zero_or_not
+        ) const override
+    {
+        std::ostringstream oss;
+        oss
+            << "{{InitAnnSepAcctGrossInt_"
+            << base_suffix(guar_or_curr)
+            << ir_suffix(zero_or_not)
+            << "}} "
+            << "Assumed Sep Acct\n"
+            << "Gross Rate* "
+            << "({{InitAnnSepAcctNetInt_"
+            << base_suffix(guar_or_curr)
+            << ir_suffix(zero_or_not)
+            << "}} net)\n"
+            << "{{InitAnnGenAcctInt_"
+            << base_suffix(guar_or_curr)
+            << "}} GPA rate"
+            ;
+        return oss.str();
+    }
+};
+
+class nasd_supplemental : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_er_gross_payment
+        ,column_ee_gross_payment
+        ,column_premium_outlay
+        ,column_admin_charge
+        ,column_premium_tax_load
+        ,column_dac_tax_load
+        ,column_er_min_premium
+        ,column_ee_min_premium
+        ,column_net_premium
+        ,column_cost_of_insurance_charges
+        ,column_curr_account_value
+        ,column_curr_cash_surr_value
+        ,column_curr_death_benefit
+        ,column_max
+        };
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "nasd_supp";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"           , "Policy\nYear"               ,       
"999" }
+            ,{ "AttainedAge"          , "End of\nYear Age"           ,       
"999" }
+            ,{ "ErGrossPmt"           , "ER Gross\nPayment"          ,   
"999,999" }
+            ,{ "EeGrossPmt"           , "EE Gross\nPayment"          ,   
"999,999" }
+            ,{ "GrossPmt"             , "Premium\nOutlay"            ,   
"999,999" }
+            ,{ "PolicyFee_Current"    , "Admin\nCharge"              ,   
"999,999" }
+            ,{ "PremTaxLoad_Current"  , "Premium\nTax Load"          ,   
"999,999" }
+            ,{ "DacTaxLoad_Current"   , "DAC\nTax Load"              ,   
"999,999" }
+            ,{ "ErModalMinimumPremium", "ER Modal\nMinimum\nPremium" ,   
"999,999" }
+            ,{ "EeModalMinimumPremium", "EE Modal\nMinimum\nPremium" ,   
"999,999" }
+            ,{ "NetPmt_Current"       , "Net\nPremium"               ,   
"999,999" }
+            ,{ "COICharge_Current"    , "Cost of\nInsurance\nCharges",   
"999,999" }
+            ,{ "AcctVal_Current"      , "Current\nAccount\nValue"    ,   
"999,999" }
+            ,{ "CSVNet_Current"       , "Current\nCash Surr\nValue"  ,   
"999,999" }
+            ,{ "EOYDeathBft_Current"  , "Current\nDeath\nBenefit"    , 
"9,999,999" }
+            };
+
+        return columns;
+    }
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        auto const& invar = ledger.GetLedgerInvariant();
+
+        // The supplemental page in NASD illustrations exists in two versions:
+        // default one and one with split premiums. Hide columns that are not
+        // needed for the current illustration.
+        switch(column)
+            {
+            case column_end_of_year_age:
+                // This column doesn't make sense for composite ledgers.
+                return !ledger.is_composite();
+
+            case column_admin_charge:
+            case column_premium_tax_load:
+            case column_dac_tax_load:
+                // These columns only appear in non-split premiums case.
+                return invar.SplitMinPrem == 0.;
+
+            case column_er_gross_payment:
+            case column_ee_gross_payment:
+            case column_er_min_premium:
+            case column_ee_min_premium:
+                // While those only appear in split premiums case.
+                return invar.SplitMinPrem == 1.;
+
+            case column_policy_year:
+            case column_premium_outlay:
+            case column_net_premium:
+            case column_cost_of_insurance_charges:
+            case column_curr_account_value:
+            case column_curr_cash_surr_value:
+            case column_curr_death_benefit:
+            case column_max:
+                // These columns are common to both cases and never hidden.
+                break;
+            }
+
+        return true;
+    }
+};
+
+class nasd_assumption_detail : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_sep_acct_crediting_rate
+        ,column_gen_acct_crediting_rate
+        ,column_m_and_e
+        ,column_ee_payment_mode
+        ,column_er_payment_mode
+        ,column_assumed_loan_interest
+        ,column_max
+        };
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "nasd_assumption_detail";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"          , "Policy\nYear"                ,        
"999" }
+            ,{ "AttainedAge"         , "End of\nYear Age"            ,        
"999" }
+            ,{ "AnnSAIntRate_Current", "Sep Acct Net\nInv Rate"      ,     
"99.99%" }
+            ,{ "AnnGAIntRate_Current", "Gen Acct\nCurrent Rate"      ,     
"99.99%" }
+            ,{ "CurrMandE"           , "M&E"                         ,     
"99.99%" }
+            ,{ "EeMode"              , "Indiv\nPmt Mode"             , 
"Semiannual" }
+            ,{ "ErMode"              , "Corp\nPmt Mode"              , 
"Semiannual" }
+            ,{ "InitAnnLoanDueRate"  , "Assumed\nLoan Interest"      ,     
"99.99%" }
+            };
+
+        return columns;
+    }
+
+    // Notice that there is no need to override should_show_column() in this
+    // class as this page is not included in composite illustrations and hence
+    // all of its columns, including the "AttainedAge" one, are always shown.
+};
+
+// NASD illustration.
+class pdf_illustration_nasd : public pdf_illustration
+{
+  public:
+    pdf_illustration_nasd
+        (Ledger const& ledger
+        ,fs::path const& output
+        )
+        :pdf_illustration(ledger, output)
+    {
+        auto const& invar = ledger.GetLedgerInvariant();
+
+        // Define variables specific to this illustration.
+        if(!invar.ContractName.empty())
+            {
+            std::string s = invar.ContractName;
+            for(auto& c : s)
+                {
+                c = lmi_tolower(c);
+                }
+            s[0] = lmi_toupper(s[0]);
+
+            add_variable("ContractNameCap", s);
+            }
+
+        add_variable
+            ("UWTypeIsGuaranteedIssueInTexasWithFootnote"
+            ,invar.UWType == "Guaranteed issue"
+            );
+
+        add_variable
+            ("HasTermOrSupplSpecAmt"
+            ,test_variable("HasTerm") || test_variable("HasSupplSpecAmt")
+            );
+
+        auto const& state_abbrev = invar.GetStatePostalAbbrev();
+        add_variable
+            ("StateIsNewYork"
+            ,state_abbrev == "NY"
+            );
+
+        // Add all the pages.
+        add<cover_page>();
+        numbered_page::start_numbering();
+        add<nasd_basic>();
+        add<nasd_supplemental>();
+        add<standard_page>("nasd_column_headings");
+        add<standard_page>("nasd_notes1");
+        add<standard_page>("nasd_notes2");
+        if(!ledger.is_composite())
+            {
+            add<nasd_assumption_detail>();
+            }
+        if(invar.SupplementalReport)
+            {
+            add<standard_supplemental_report>
+                (get_interpolator()
+                ,"nasd_supp_report"
+                );
+            }
+    }
+
+    std::string get_upper_footer_template_name() const override
+    {
+        return "nasd_footer_upper";
+    }
+
+    std::string get_lower_footer_template_name() const override
+    {
+        return "nasd_footer_lower";
+    }
+};
+
+// Basic illustration page of the private group placement illustration.
+class reg_d_group_basic : public page_with_basic_tabular_report
+{
+  private:
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_d_group_basic";
+    }
+
+    std::string get_two_column_header
+        (base          guar_or_curr
+        ,interest_rate zero_or_not
+        ) const override
+    {
+        std::ostringstream oss;
+        oss
+            << "{{InitAnnSepAcctGrossInt_"
+            << base_suffix(guar_or_curr)
+            << ir_suffix(zero_or_not)
+            << "}} "
+            << "Hypothetical Gross\n"
+            << "Return ({{InitAnnSepAcctNetInt_"
+            << base_suffix(guar_or_curr)
+            << ir_suffix(zero_or_not)
+            << "}} net)"
+            ;
+        return oss.str();
+    }
+};
+
+// Private group placement illustration.
+class pdf_illustration_reg_d_group : public pdf_illustration
+{
+  public:
+    pdf_illustration_reg_d_group
+        (Ledger const& ledger
+        ,fs::path const& output
+        )
+        :pdf_illustration(ledger, output)
+    {
+        // Define variables specific to this illustration.
+        auto const& invar = ledger.GetLedgerInvariant();
+
+        add_variable
+            ("MecYearIs0"
+            ,invar.MecYear == 0
+            );
+
+        // Add all the pages.
+        add<cover_page>();
+        numbered_page::start_numbering();
+        add<reg_d_group_basic>();
+        add<standard_page>("reg_d_group_column_headings");
+        add<standard_page>("reg_d_group_narr_summary");
+        add<standard_page>("reg_d_group_narr_summary2");
+        if(invar.SupplementalReport)
+            {
+            add<standard_supplemental_report>
+                (get_interpolator()
+                ,"reg_d_group_supp_report"
+                );
+            }
+    }
+
+    std::string get_upper_footer_template_name() const override
+    {
+        return "reg_d_group_footer_upper";
+    }
+
+    std::string get_lower_footer_template_name() const override
+    {
+        return "reg_d_group_footer_lower";
+    }
+};
+
+// This page exists in two almost identical versions, one using guaranteed and
+// the other one using current values, use a base class to share the common
+// parts.
+class reg_d_individual_irr_base : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_premium_outlay
+        ,column_zero_cash_surr_value
+        ,column_zero_death_benefit
+        ,column_zero_irr_surr_value
+        ,column_zero_irr_death_benefit
+        ,column_separator
+        ,column_nonzero_cash_surr_value
+        ,column_nonzero_death_benefit
+        ,column_nonzero_irr_surr_value
+        ,column_nonzero_irr_death_benefit
+        ,column_max
+        };
+
+    // Must be overridden to return the base being used.
+    virtual base get_base() const = 0;
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        // One column should be hidden for composite ledgers.
+        return column != column_end_of_year_age || !ledger.is_composite();
+    }
+
+    void render_or_measure_extra_headers
+        (illustration_table_generator&  table
+        ,html_interpolator const&       interpolate_html
+        ,int*                           pos_y
+        ,enum_output_mode               output_mode
+        ) const override
+    {
+        std::ostringstream header_zero;
+        header_zero
+            << "{{InitAnnSepAcctGrossInt_"
+            << base_suffix(get_base())
+            << ir_suffix(interest_rate::zero)
+            << "}} Hypothetical Rate of\n"
+            << "Return*"
+            ;
+
+        auto pos_y_copy = *pos_y;
+        table.output_super_header
+            (interpolate_html(header_zero.str()).as_html()
+            ,column_zero_cash_surr_value
+            ,column_zero_irr_surr_value
+            ,pos_y
+            ,output_mode
+            );
+
+        std::ostringstream header_nonzero;
+        header_nonzero
+            << "{{InitAnnSepAcctGrossInt_"
+            << base_suffix(get_base())
+            << ir_suffix(interest_rate::non_zero)
+            << "}} Hypothetical Rate of\n"
+            << "Return*"
+            ;
+
+        *pos_y = pos_y_copy;
+        table.output_super_header
+            (interpolate_html(header_nonzero.str()).as_html()
+            ,column_nonzero_cash_surr_value
+            ,column_nonzero_irr_surr_value
+            ,pos_y
+            ,output_mode
+            );
+
+        *pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_zero_cash_surr_value
+            ,column_zero_irr_surr_value
+            ,*pos_y
+            ,output_mode
+            );
+        table.output_horz_separator
+            (column_nonzero_cash_surr_value
+            ,column_nonzero_irr_surr_value
+            ,*pos_y
+            ,output_mode
+            );
+    }
+};
+
+class reg_d_individual_guar_irr : public reg_d_individual_irr_base
+{
+  private:
+    base get_base() const override
+    {
+        return base::guaranteed;
+    }
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_d_indiv_guar_irr";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"                 , "Policy\nYear"       ,       
"999" }
+            ,{ "AttainedAge"                , "End of\nYear Age"   ,       
"999" }
+            ,{ "GrossPmt"                   , "Premium\nOutlay"    ,   
"999,999" }
+            ,{ "CSVNet_GuaranteedZero"      , "Cash Surr\nValue"   ,   
"999,999" }
+            ,{ "EOYDeathBft_GuaranteedZero" , "Death\nBenefit"     , 
"9,999,999" }
+            ,{ "IrrCsv_GuaranteedZero"      , "IRR on\nSurr Value" ,    
"99.99%" }
+            ,{ "IrrDb_GuaranteedZero"       , "IRR on\nDeath Bft"  ,    
"99.99%" }
+            ,{ ""                           , " "                  ,         
"-" }
+            ,{ "CSVNet_Guaranteed"          , "Cash Surr\nValue"   ,   
"999,999" }
+            ,{ "EOYDeathBft_Guaranteed"     , "Death\nBenefit"     , 
"9,999,999" }
+            ,{ "IrrCsv_Guaranteed"          , "IRR on\nSurr Value" ,    
"99.99%" }
+            ,{ "IrrDb_Guaranteed"           , "IRR on\nDeath Bft"  ,    
"99.99%" }
+            };
+
+        return columns;
+    }
+};
+
+class reg_d_individual_curr_irr : public reg_d_individual_irr_base
+{
+  private:
+    base get_base() const override
+    {
+        return base::current;
+    }
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_d_indiv_curr_irr";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"                 , "Policy\nYear"       ,       
"999" }
+            ,{ "AttainedAge"                , "End of\nYear Age"   ,       
"999" }
+            ,{ "GrossPmt"                   , "Premium\nOutlay"    ,   
"999,999" }
+            ,{ "CSVNet_CurrentZero"         , "Cash Surr\nValue"   ,   
"999,999" }
+            ,{ "EOYDeathBft_CurrentZero"    , "Death\nBenefit"     , 
"9,999,999" }
+            ,{ "IrrCsv_CurrentZero"         , "IRR on\nSurr Value" ,    
"99.99%" }
+            ,{ "IrrDb_CurrentZero"          , "IRR on\nDeath Bft"  ,    
"99.99%" }
+            ,{ ""                           , " "                  ,         
"-" }
+            ,{ "CSVNet_Current"             , "Cash Surr\nValue"   ,   
"999,999" }
+            ,{ "EOYDeathBft_Current"        , "Death\nBenefit"     , 
"9,999,999" }
+            ,{ "IrrCsv_Current"             , "IRR on\nSurr Value" ,    
"99.99%" }
+            ,{ "IrrDb_Current"              , "IRR on\nDeath Bft"  ,    
"99.99%" }
+            };
+
+        return columns;
+    }
+};
+
+class reg_d_individual_curr : public page_with_tabular_report
+{
+  private:
+    enum
+        {column_policy_year
+        ,column_end_of_year_age
+        ,column_premium_outlay
+        ,column_premium_loads
+        ,column_admin_charges
+        ,column_curr_mortality_charges
+        ,column_curr_asset_charges
+        ,column_curr_investment_income
+        ,column_curr_account_value
+        ,column_curr_cash_surr_value
+        ,column_curr_death_benefit
+        ,column_max
+        };
+
+    std::string get_fixed_page_contents_template_name() const override
+    {
+        return "reg_d_indiv_curr";
+    }
+
+    illustration_table_columns const& get_table_columns() const override
+    {
+        static illustration_table_columns const columns =
+            {{ "PolicyYear"              , "Policy\nYear"      ,       "999" }
+            ,{ "AttainedAge"             , "End of\nYear Age"  ,       "999" }
+            ,{ "GrossPmt"                , "Premium\nOutlay"   ,   "999,999" }
+            ,{ "PremiumLoads"            , "Premium\nLoads"    ,   "999,999" }
+            ,{ "AdminCharges"            , "Admin\nCharges"    ,   "999,999" }
+            ,{ "COICharge_Current"       , "Mortality\nCharges",   "999,999" }
+            ,{ "SepAcctCharges_Current"  , "Asset\nCharges"    ,   "999,999" }
+            ,{ "GrossIntCredited_Current", "Investment\nIncome",   "999,999" }
+            ,{ "AcctVal_Current"         , "Account\nValue"    ,   "999,999" }
+            ,{ "CSVNet_Current"          , "Cash\nSurr Value"  ,   "999,999" }
+            ,{ "EOYDeathBft_Current"     , "Death\nBenefit"    , "9,999,999" }
+            };
+
+        return columns;
+    }
+
+    bool should_show_column(Ledger const& ledger, int column) const override
+    {
+        // One column should be hidden for composite ledgers.
+        return column != column_end_of_year_age || !ledger.is_composite();
+    }
+
+    void render_or_measure_extra_headers
+        (illustration_table_generator&  table
+        ,html_interpolator const&       interpolate_html
+        ,int*                           pos_y
+        ,enum_output_mode               output_mode
+        ) const override
+    {
+        table.output_super_header
+            (interpolate_html
+                ("{{InitAnnSepAcctGrossInt_Guaranteed}} Hypothetical Rate of 
Return*"
+                ).as_html()
+            ,column_curr_investment_income
+            ,column_max
+            ,pos_y
+            ,output_mode
+            );
+
+        *pos_y += table.get_separator_line_height();
+        table.output_horz_separator
+            (column_curr_investment_income
+            ,column_max
+            ,*pos_y
+            ,output_mode
+            );
+    }
+};
+
+// Private individual placement illustration.
+class pdf_illustration_reg_d_individual : public pdf_illustration
+{
+  public:
+    pdf_illustration_reg_d_individual
+        (Ledger const& ledger
+        ,fs::path const& output
+        )
+        :pdf_illustration(ledger, output)
+    {
+        auto const& invar = ledger.GetLedgerInvariant();
+
+        // Define variables specific to this illustration.
+        add_abbreviated_variable("CorpName", 140);
+        add_abbreviated_variable("Insured1", 140);
+
+        // Add all the pages.
+        numbered_page::start_numbering();
+        add<standard_page>("reg_d_indiv_cover_page");
+        add<reg_d_individual_guar_irr>();
+        add<reg_d_individual_curr_irr>();
+        add<reg_d_individual_curr>();
+        add<standard_page>("reg_d_indiv_notes1");
+        add<standard_page>("reg_d_indiv_notes2");
+        add<standard_page>("reg_d_indiv_notes3");
+        if(invar.SupplementalReport)
+            {
+            add<standard_supplemental_report>
+                (get_interpolator()
+                ,"reg_d_indiv_supp_report"
+                );
+            }
+    }
+
+    std::string get_upper_footer_template_name() const override
+    {
+        return "reg_d_indiv_footer_upper";
+    }
+
+    std::string get_lower_footer_template_name() const override
+    {
+        return "reg_d_indiv_footer_lower";
+    }
+};
+
+class ledger_pdf_generator_wx : public ledger_pdf_generator
+{
+  public:
+    static std::shared_ptr<ledger_pdf_generator> do_create()
+        {
+        return std::make_shared<ledger_pdf_generator_wx>();
+        }
+
+    ledger_pdf_generator_wx() = default;
+    ledger_pdf_generator_wx(ledger_pdf_generator_wx const&) = delete;
+    ledger_pdf_generator_wx& operator=(ledger_pdf_generator_wx const&) = 
delete;
+
+    void write(Ledger const& ledger, fs::path const& output) override;
+
+  private:
+};
+
+void ledger_pdf_generator_wx::write
+    (Ledger const& ledger
+    ,fs::path const& output
+    )
+{
+    std::unique_ptr<pdf_illustration> pdf_ill;
+
+    auto const z = ledger.ledger_type();
+    switch(z)
+        {
+        case mce_ill_reg:
+            pdf_ill = std::make_unique<pdf_illustration_regular>(ledger, 
output);
+            break;
+        case mce_nasd:
+            pdf_ill = std::make_unique<pdf_illustration_nasd>(ledger, output);
+            break;
+        case mce_group_private_placement:
+            pdf_ill = std::make_unique<pdf_illustration_reg_d_group>(ledger, 
output);
+            break;
+        case mce_individual_private_placement:
+            pdf_ill = 
std::make_unique<pdf_illustration_reg_d_individual>(ledger, output);
+            break;
+        default:
+            alarum() << "Unknown ledger type '" << z << "'." << LMI_FLUSH;
+        }
+
+    pdf_ill->render_all();
+}
+
+volatile bool ensure_setup = ledger_pdf_generator::set_creator
+    (ledger_pdf_generator_wx::do_create
+    );
+
+} // Unnamed namespace.
diff --git a/output_mode.hpp b/output_mode.hpp
new file mode 100644
index 0000000..91c79e0
--- /dev/null
+++ b/output_mode.hpp
@@ -0,0 +1,35 @@
+// Output mode enum used in PDF generation helpers.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef output_mode_hpp
+#define output_mode_hpp
+
+#include "config.hpp"
+
+/// Convenient enum used with functions that can either actually render
+/// something or just pretend doing it in order to compute the space that would
+/// be taken by it, in the layout phase.
+enum enum_output_mode
+    {e_output_normal
+    ,e_output_measure_only
+    };
+
+#endif // output_mode_hpp
diff --git a/pdf_writer_wx.cpp b/pdf_writer_wx.cpp
new file mode 100644
index 0000000..5ae36c2
--- /dev/null
+++ b/pdf_writer_wx.cpp
@@ -0,0 +1,249 @@
+// PDF generation helpers.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#include "pchfile_wx.hpp"
+
+#include "pdf_writer_wx.hpp"
+
+#include "alert.hpp"
+#include "global_settings.hpp"
+#include "html.hpp"
+
+#include <wx/filesys.h>
+#include <wx/html/htmlcell.h>
+
+namespace
+{
+
+// These margins are arbitrary and can be changed to conform to subjective
+// preferences.
+constexpr int horz_margin = 24;
+constexpr int vert_margin = 36;
+
+wxPrintData make_print_data
+    (wxString const&    output_filename
+    ,wxPrintOrientation orientation
+    )
+{
+    wxPrintData print_data;
+    print_data.SetPaperId(wxPAPER_LETTER);
+    print_data.SetFilename(output_filename);
+    print_data.SetOrientation(orientation);
+    return print_data;
+}
+
+} // Unnamed namespace.
+
+pdf_writer_wx::pdf_writer_wx
+    (wxString const&    output_filename
+    ,wxPrintOrientation orientation
+    ,std::array<int, 7> const* html_font_sizes
+    )
+    :print_data_        {make_print_data(output_filename, orientation)}
+    ,pdf_dc_            {print_data_}
+    ,html_parser_       {nullptr}
+    ,total_page_size_   {pdf_dc_.GetSize()}
+{
+    // Ensure that the output is independent of the current display resolution:
+    // it seems that this is only the case with the PDF map mode and wxDC mode
+    // different from wxMM_TEXT.
+    pdf_dc_.SetMapModeStyle(wxPDF_MAPMODESTYLE_PDF);
+
+    // For simplicity, use points for everything: font sizers are expressed in
+    // them anyhow, so it's convenient to use them for everything else too.
+    pdf_dc_.SetMapMode(wxMM_POINTS);
+
+    pdf_dc_.StartDoc(wxString()); // Argument is not used.
+    pdf_dc_.StartPage();
+
+    // Use a standard PDF Helvetica font (without embedding any custom fonts in
+    // the generated file, the only other realistic choice is Times New Roman).
+    pdf_dc_.SetFont
+        (wxFontInfo
+            (html_font_sizes
+                ? html_font_sizes->at(2)
+                : 8
+            )
+            .Family(wxFONTFAMILY_SWISS)
+            .FaceName("Helvetica")
+        );
+
+    // Create an HTML parser to allow easily adding HTML contents to the 
output.
+    html_parser_.SetDC(&pdf_dc_);
+    if(html_font_sizes)
+        {
+        html_parser_.SetFonts
+            ("Helvetica"
+            ,"Courier"
+            ,html_font_sizes->data()
+            );
+        }
+    else
+        {
+        html_parser_.SetStandardFonts
+            (pdf_dc_.GetFont().GetPointSize()
+            ,"Helvetica"
+            ,"Courier"
+            );
+        }
+
+    // Create the virtual file system object for loading images referenced from
+    // HTML and interpret relative paths from the data directory.
+    html_vfs_.reset(new wxFileSystem());
+    html_vfs_->ChangePathTo
+        (global_settings::instance().data_directory().string()
+        ,true /* argument is a directory, not file path */
+        );
+    html_parser_.SetFS(html_vfs_.get());
+}
+
+/// Output an image at the given scale into the PDF.
+///
+/// The scale specifies how many times the image should be shrunk:
+/// scale > 1 makes the image smaller, while scale < 1 makes it larger.
+///
+/// Updates pos_y by increasing it by the height of the specified
+/// image at the given scale.
+
+void pdf_writer_wx::output_image
+    (wxImage const&   image
+    ,char const*      image_name
+    ,double           scale
+    ,int              x
+    ,int*             pos_y
+    ,enum_output_mode output_mode
+    )
+{
+    int const y = wxRound(image.GetHeight() / scale);
+
+    switch(output_mode)
+        {
+        case e_output_normal:
+            {
+            // Use wxPdfDocument API directly as wxDC doesn't provide a way to
+            // set the image scale at PDF level and also because passing via
+            // wxDC wastefully converts wxImage to wxBitmap only to convert it
+            // back to wxImage when embedding it into the PDF.
+            wxPdfDocument* const pdf_doc = pdf_dc_.GetPdfDocument();
+            LMI_ASSERT(pdf_doc);
+
+            pdf_doc->SetImageScale(scale);
+            pdf_doc->Image(image_name, image, x, *pos_y);
+            pdf_doc->SetImageScale(1);
+            }
+            break;
+        case e_output_measure_only:
+            // Do nothing.
+            break;
+        default:
+            {
+            alarum() << "Case " << output_mode << " not found." << LMI_FLUSH;
+            }
+        }
+
+    *pos_y += y;
+}
+
+/// Render, or just pretend rendering in order to measure it, the given HTML
+/// contents at the specified position wrapping it at the given width.
+/// Return the height of the output (using this width).
+
+int pdf_writer_wx::output_html
+    (int x
+    ,int y
+    ,int width
+    ,html::text const& html
+    ,enum_output_mode output_mode
+    )
+{
+    // We don't really want to change the font, but to preserve the current DC
+    // font which is changed by rendering the HTML contents.
+    wxDCFontChanger preserve_font(pdf_dc_, wxFont());
+
+    auto const html_str = wxString::FromUTF8(html.as_html());
+    std::unique_ptr<wxHtmlContainerCell> const cell
+        (static_cast<wxHtmlContainerCell*>(html_parser_.Parse(html_str))
+        );
+    LMI_ASSERT(cell);
+
+    cell->Layout(width);
+    switch(output_mode)
+        {
+        case e_output_normal:
+            {
+            wxHtmlRenderingInfo rendering_info;
+            cell->Draw
+                (pdf_dc_
+                ,x
+                ,y
+                ,0
+                ,std::numeric_limits<int>::max()
+                ,rendering_info
+                );
+            }
+            break;
+        case e_output_measure_only:
+            // Do nothing.
+            break;
+        default:
+            {
+            alarum() << "Case " << output_mode << " not found." << LMI_FLUSH;
+            }
+        }
+
+    return cell->GetHeight();
+}
+
+int pdf_writer_wx::get_horz_margin() const
+{
+    return horz_margin;
+}
+
+int pdf_writer_wx::get_vert_margin() const
+{
+    return vert_margin;
+}
+
+int pdf_writer_wx::get_page_width()  const
+{
+    return total_page_size_.x - 2 * horz_margin;
+}
+
+int pdf_writer_wx::get_total_width() const
+{
+    return total_page_size_.x;
+}
+
+int pdf_writer_wx::get_page_height() const
+{
+    return total_page_size_.y - 2 * vert_margin;
+}
+
+int pdf_writer_wx::get_page_bottom() const
+{
+    return total_page_size_.y - vert_margin;
+}
+
+pdf_writer_wx::~pdf_writer_wx()
+{
+    // This will finally generate the PDF file.
+    pdf_dc_.EndDoc();
+}
diff --git a/pdf_writer_wx.hpp b/pdf_writer_wx.hpp
new file mode 100644
index 0000000..4634f7c
--- /dev/null
+++ b/pdf_writer_wx.hpp
@@ -0,0 +1,100 @@
+// PDF generation helpers.
+//
+// Copyright (C) 2017 Gregory W. Chicares.
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+//
+// This program 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 this program; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
+//
+// http://savannah.nongnu.org/projects/lmi
+// email: <address@hidden>
+// snail: Chicares, 186 Belle Woods Drive, Glastonbury CT 06033, USA
+
+#ifndef pdf_writer_wx_hpp
+#define pdf_writer_wx_hpp
+
+#include "config.hpp"
+
+#include "assert_lmi.hpp"
+#include "output_mode.hpp"
+
+#include <wx/html/winpars.h>
+
+#include <wx/pdfdc.h>
+
+#include <array>
+#include <memory>                       // std::unique_ptr
+
+class wxFileSystem;
+
+namespace html { class text; }
+
+class pdf_writer_wx
+{
+  public:
+    // Optional html_font_sizes array allows to override default font sizes for
+    // the standard HTML3 fonts (1..7).
+    pdf_writer_wx
+        (wxString const&           output_filename
+        ,wxPrintOrientation        orientation
+        ,std::array<int, 7> const* html_font_sizes = nullptr
+        );
+
+    pdf_writer_wx(pdf_writer_wx const&) = delete;
+    pdf_writer_wx& operator=(pdf_writer_wx const&) = delete;
+
+    ~pdf_writer_wx();
+
+    // High level functions which should be preferably used if possible.
+    int output_html
+        (int x
+        ,int y
+        ,int width
+        ,html::text const& html
+        ,enum_output_mode output_mode = e_output_normal
+        );
+
+    void output_image
+        (wxImage const&   image
+        ,char const*      image_name
+        ,double           scale
+        ,int              x
+        ,int*             pos_y
+        ,enum_output_mode output_mode = e_output_normal
+        );
+
+    // Accessors allowing to use lower level wxDC API directly.
+    wxDC& dc() { return pdf_dc_; }
+
+    // Page metrics: the page width and height are the size of the page region
+    // reserved for the normal contents, excluding horizontal and vertical
+    // margins. Total width and height include the margins.
+    int get_horz_margin() const;
+    int get_vert_margin() const;
+    int get_page_width()  const;
+    int get_total_width() const;
+    int get_page_height() const;
+    int get_page_bottom() const;
+
+  private:
+    wxPrintData print_data_;
+    wxPdfDC pdf_dc_;
+
+    // Order is potentially important here: html_parser_ uses html_vfs_, so
+    // must be declared after it in order to be destroyed before it.
+    std::unique_ptr<wxFileSystem> html_vfs_;
+    wxHtmlWinParser html_parser_;
+
+    wxSize const total_page_size_;
+};
+
+#endif // pdf_writer_wx_hpp



reply via email to

[Prev in Thread] Current Thread [Next in Thread]