From 82311c96e4c826f0e2c2872bc6a6d79064d7e663 Mon Sep 17 00:00:00 2001 From: Thomas Fussell Date: Sat, 29 Oct 2016 10:23:04 -0400 Subject: [PATCH] implement reading of cell comments --- include/xlnt/cell/cell.hpp | 22 +++ include/xlnt/cell/comment.hpp | 53 +++--- .../cell/{text.hpp => formatted_text.hpp} | 24 +-- include/xlnt/cell/text_run.hpp | 14 +- include/xlnt/packaging/relationship.hpp | 1 + include/xlnt/styles/color.hpp | 3 + include/xlnt/workbook/workbook.hpp | 32 ++-- include/xlnt/xlnt.hpp | 2 +- source/cell/cell.cpp | 59 +++++-- source/cell/comment.cpp | 37 ++-- source/cell/{text.cpp => formatted_text.cpp} | 35 ++-- source/cell/tests/test_cell.hpp | 15 ++ source/cell/tests/test_text.hpp | 22 +-- source/cell/text_run.cpp | 22 ++- source/detail/cell_impl.cpp | 11 +- source/detail/cell_impl.hpp | 15 +- source/detail/comment_impl.cpp | 46 ----- source/detail/comment_impl.hpp | 44 ----- source/detail/custom_value_traits.cpp | 4 + source/detail/custom_value_traits.hpp | 1 + source/detail/workbook_impl.hpp | 2 +- source/detail/xlsx_consumer.cpp | 161 +++++++++++++++++- source/detail/xlsx_consumer.hpp | 3 +- source/detail/xlsx_crypto.cpp | 4 +- source/detail/xlsx_producer.cpp | 10 +- source/styles/color.cpp | 16 ++ source/workbook/tests/test_consume_xlsx.hpp | 16 ++ source/workbook/workbook.cpp | 6 +- tests/data/18_basic_comments.xlsx | Bin 0 -> 26858 bytes 29 files changed, 430 insertions(+), 250 deletions(-) rename include/xlnt/cell/{text.hpp => formatted_text.hpp} (77%) rename source/cell/{text.cpp => formatted_text.cpp} (82%) delete mode 100644 source/detail/comment_impl.cpp delete mode 100644 source/detail/comment_impl.hpp create mode 100644 tests/data/18_basic_comments.xlsx diff --git a/include/xlnt/cell/cell.hpp b/include/xlnt/cell/cell.hpp index 0dc2dbd5..2a25509d 100644 --- a/include/xlnt/cell/cell.hpp +++ b/include/xlnt/cell/cell.hpp @@ -435,6 +435,28 @@ public: /// std::string check_string(const std::string &to_check); + // comment + + /// + /// Return true if this cell has a comment applied. + /// + bool has_comment(); + + /// + /// Delete the comment applied to this cell if it exists. + /// + void clear_comment(); + + /// + /// Get the comment applied to this cell. + /// + comment comment(); + + /// + /// Apply the comment provided as the ony argument to the cell. + /// + void comment(const class comment &new_comment); + // operators /// diff --git a/include/xlnt/cell/comment.hpp b/include/xlnt/cell/comment.hpp index 8ad6a706..2326e139 100644 --- a/include/xlnt/cell/comment.hpp +++ b/include/xlnt/cell/comment.hpp @@ -1,5 +1,4 @@ // Copyright (c) 2014-2016 Thomas Fussell -// Copyright (c) 2010-2015 openpyxl // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -26,64 +25,54 @@ #include #include +#include namespace xlnt { -class cell; -namespace detail { -struct comment_impl; -} - /// -/// A comment can be applied to a cell to provide extra information. +/// A comment can be applied to a cell to provide extra information about its contents. /// class XLNT_API comment { public: /// - /// The default constructor makes an invalid comment without a parent cell. + /// Constructs a new blank comment. /// comment(); /// - /// Constructs a comment applied to the given cell, parent, and with the comment - /// text and author set to the provided respective values. - comment(cell parent, const std::string &text, const std::string &auth); + /// Constructs a new comment with the given text and author. + /// + comment(const formatted_text &text, const std::string &author); - ~comment(); + /// + /// Constructs a new comment with the given unformatted text and author. + /// + comment(const std::string &text, const std::string &author); /// /// Return the text that will be displayed for this comment. /// - std::string get_text() const; + formatted_text text() const; + + /// + /// Return the plain text that will be displayed for this comment without formatting information. + /// + std::string plain_text() const; /// /// Return the author of this comment. /// - std::string get_author() const; + std::string author() const; /// - /// True if the comments point to the same sell (false if - /// they are different cells but identical comments). Note - /// that a cell can only have one comment and a comment - /// can only be applied to one cell. + /// Return true if both comments are equivalent. /// - bool operator==(const comment &other) const; + friend bool operator==(const comment &left, const comment &right); private: - friend class cell; // cell needs access to private constructor - - /// - /// Construct a comment from an implementation of a comment. - /// - comment(detail::comment_impl *d); - - /// - /// Pointer to the implementation of this comment. - /// This allows comments to be passed by value while - /// retaining the ability to modify the parent cell. - /// - detail::comment_impl *d_; + formatted_text text_; + std::string author_; }; } // namespace xlnt diff --git a/include/xlnt/cell/text.hpp b/include/xlnt/cell/formatted_text.hpp similarity index 77% rename from include/xlnt/cell/text.hpp rename to include/xlnt/cell/formatted_text.hpp index 335a3da3..e83afe55 100644 --- a/include/xlnt/cell/text.hpp +++ b/include/xlnt/cell/formatted_text.hpp @@ -25,27 +25,27 @@ #include #include -#include // for XLNT_API, XLNT_API +#include #include namespace xlnt { -class text_run; - -class XLNT_API text +class XLNT_API formatted_text { public: - void clear(); - void set_plain_string(const std::string &s); - std::string get_plain_string() const; - std::vector get_runs() const; - void add_run(const text_run &t); - void set_run(const std::vector &parts); + void clear(); + + void plain_text(const std::string &s); + std::string plain_text() const; + + std::vector runs() const; + void runs(const std::vector &new_runs); + void add_run(const text_run &t); - bool operator==(const text &rhs) const; + bool operator==(const formatted_text &rhs) const; private: - std::vector runs_; + std::vector runs_; }; } // namespace xlnt diff --git a/include/xlnt/cell/text_run.hpp b/include/xlnt/cell/text_run.hpp index 10f87c6a..a5403039 100644 --- a/include/xlnt/cell/text_run.hpp +++ b/include/xlnt/cell/text_run.hpp @@ -24,7 +24,8 @@ #include -#include // for XLNT_API, XLNT_API +#include +#include #include namespace xlnt { @@ -45,8 +46,8 @@ public: void set_size(std::size_t size); bool has_color() const; - std::string get_color() const; - void set_color(const std::string &color); + color get_color() const; + void set_color(const color &new_color); bool has_font() const; std::string get_font() const; @@ -60,14 +61,19 @@ public: std::string get_scheme() const; void set_scheme(const std::string &scheme); + bool bold_set() const; + bool is_bold() const; + void set_bold(bool bold); + private: std::string string_; optional size_; - optional color_; + optional color_; optional font_; optional family_; optional scheme_; + optional bold_; }; } // namespace xlnt diff --git a/include/xlnt/packaging/relationship.hpp b/include/xlnt/packaging/relationship.hpp index b2704d93..fb2b51fe 100644 --- a/include/xlnt/packaging/relationship.hpp +++ b/include/xlnt/packaging/relationship.hpp @@ -85,6 +85,7 @@ enum class XLNT_API relationship_type single_cell_table_definitions, styles, table_definition, + vml_drawing, volatile_dependencies, worksheet, diff --git a/include/xlnt/styles/color.hpp b/include/xlnt/styles/color.hpp index c22dce44..ced7d0a5 100644 --- a/include/xlnt/styles/color.hpp +++ b/include/xlnt/styles/color.hpp @@ -132,6 +132,9 @@ public: double get_tint() const; void set_tint(double tint); + + bool operator==(const color &other) const; + bool operator!=(const color &other) const { return !(*this == other); } protected: std::string to_hash_string() const override; diff --git a/include/xlnt/workbook/workbook.hpp b/include/xlnt/workbook/workbook.hpp index df4585e5..811dd028 100644 --- a/include/xlnt/workbook/workbook.hpp +++ b/include/xlnt/workbook/workbook.hpp @@ -45,6 +45,7 @@ class drawing; class fill; class font; class format; +class formatted_text; class manifest; class named_range; class number_format; @@ -56,7 +57,6 @@ class range_reference; class relationship; class style; class style_serializer; -class text; class theme; class workbook_view; class worksheet; @@ -429,10 +429,10 @@ public: // shared strings - void add_shared_string(const text &shared, bool allow_duplicates=false); - std::vector &get_shared_strings(); - const std::vector &get_shared_strings() const; - + void add_shared_string(const formatted_text &shared, bool allow_duplicates=false); + std::vector &get_shared_strings(); + const std::vector &get_shared_strings() const; + // thumbnail void set_thumbnail(const std::vector &thumbnail, @@ -470,30 +470,30 @@ public: bool operator!=(const workbook &rhs) const; private: - friend class worksheet; - friend class detail::xlsx_consumer; - friend class detail::xlsx_producer; + friend class worksheet; + friend class detail::xlsx_consumer; + friend class detail::xlsx_producer; - workbook(detail::workbook_impl *impl); + workbook(detail::workbook_impl *impl); - detail::workbook_impl &impl(); + detail::workbook_impl &impl(); - const detail::workbook_impl &impl() const; + const detail::workbook_impl &impl() const; /// /// Apply the function "f" to every cell in every worksheet in this workbook. /// void apply_to_cells(std::function f); - void register_app_properties_in_manifest(); + void register_app_properties_in_manifest(); - void register_core_properties_in_manifest(); + void register_core_properties_in_manifest(); - void register_shared_string_table_in_manifest(); + void register_shared_string_table_in_manifest(); - void register_stylesheet_in_manifest(); + void register_stylesheet_in_manifest(); - void register_theme_in_manifest(); + void register_theme_in_manifest(); /// /// An opaque pointer to a structure that holds all of the data relating to this workbook. diff --git a/include/xlnt/xlnt.hpp b/include/xlnt/xlnt.hpp index 0cc5310c..243171a6 100644 --- a/include/xlnt/xlnt.hpp +++ b/include/xlnt/xlnt.hpp @@ -30,8 +30,8 @@ #include #include #include +#include #include -#include #include // packaging diff --git a/source/cell/cell.cpp b/source/cell/cell.cpp index f084ac5a..034e3105 100644 --- a/source/cell/cell.cpp +++ b/source/cell/cell.cpp @@ -28,7 +28,7 @@ #include #include #include -#include +#include #include #include #include @@ -44,8 +44,6 @@ #include #include -#include - namespace { @@ -316,13 +314,13 @@ XLNT_API void cell::set_value(std::string s) } else { - d_->type_ = type::string; - d_->value_text_.set_plain_string(s); + d_->type_ = type::string; + d_->value_text_.plain_text(s); - if (s.size() > 0) - { - get_workbook().add_shared_string(d_->value_text_); - } + if (s.size() > 0) + { + get_workbook().add_shared_string(d_->value_text_); + } } if (get_workbook().get_guess_types()) @@ -332,17 +330,17 @@ XLNT_API void cell::set_value(std::string s) } template <> -XLNT_API void cell::set_value(text t) +XLNT_API void cell::set_value(formatted_text text) { - if (t.get_runs().size() == 1 && !t.get_runs().front().has_formatting()) + if (text.runs().size() == 1 && !text.runs().front().has_formatting()) { - set_value(t.get_plain_string()); + set_value(text.plain_text()); } else { d_->type_ = type::string; - d_->value_text_ = t; - get_workbook().add_shared_string(t); + d_->value_text_ = text; + get_workbook().add_shared_string(text); } } @@ -518,7 +516,7 @@ void cell::set_error(const std::string &error) throw invalid_data_type(); } - d_->value_text_.set_plain_string(error); + d_->value_text_.plain_text(error); d_->type_ = type::error; } @@ -804,11 +802,11 @@ void cell::set_protection(const xlnt::protection &protection_) template <> XLNT_API std::string cell::get_value() const { - return d_->value_text_.get_plain_string(); + return d_->value_text_.plain_text(); } template <> -XLNT_API text cell::get_value() const +XLNT_API formatted_text cell::get_value() const { return d_->value_text_; } @@ -1026,4 +1024,31 @@ bool cell::has_hyperlink() const return d_->hyperlink_; } +// comment + +bool cell::has_comment() +{ + return (bool)d_->comment_; +} + +void cell::clear_comment() +{ + d_->comment_.clear(); +} + +comment cell::comment() +{ + if (!has_comment()) + { + throw xlnt::exception("cell has no comment"); + } + + return d_->comment_.get(); +} + +void cell::comment(const class comment &new_comment) +{ + d_->comment_.set(new_comment); +} + } // namespace xlnt diff --git a/source/cell/comment.cpp b/source/cell/comment.cpp index f98e8fdd..ee557c92 100644 --- a/source/cell/comment.cpp +++ b/source/cell/comment.cpp @@ -21,44 +21,45 @@ // // @license: http://www.opensource.org/licenses/mit-license.php // @author: see AUTHORS file -#include #include -#include "detail/comment_impl.hpp" - namespace xlnt { -comment::comment(detail::comment_impl *d) : d_(d) +comment::comment() : comment("", "") { } -comment::comment(cell parent, const std::string &text, const std::string &author) : d_(nullptr) -{ - /*d_->text_ = text; - d_->author_ = author;*/ -} - -comment::comment() : d_(nullptr) +comment::comment(const formatted_text &text, const std::string &author) + : text_(text), + author_(author) { } -comment::~comment() +comment::comment(const std::string &text, const std::string &author) + : text_(), + author_(author) { + text_.plain_text(text); } -std::string comment::get_author() const +formatted_text comment::text() const { - return d_->author_; + return text_; } -std::string comment::get_text() const +std::string comment::plain_text() const { - return d_->text_; + return text_.plain_text(); } -bool comment::operator==(const xlnt::comment &other) const +std::string comment::author() const { - return d_ == other.d_; + return author_; +} + +bool operator==(const comment &left, const comment &right) +{ + return left.text_ == right.text_ && left.author_ == right.author_; } } // namespace xlnt diff --git a/source/cell/text.cpp b/source/cell/formatted_text.cpp similarity index 82% rename from source/cell/text.cpp rename to source/cell/formatted_text.cpp index bfb99fab..b81e71e7 100644 --- a/source/cell/text.cpp +++ b/source/cell/formatted_text.cpp @@ -21,46 +21,41 @@ // // @license: http://www.opensource.org/licenses/mit-license.php // @author: see AUTHORS file +#include -#include +#include #include namespace xlnt { -void text::clear() +void formatted_text::clear() { - runs_.clear(); + runs_.clear(); } -void text::set_plain_string(const std::string &s) +void formatted_text::plain_text(const std::string &s) { - clear(); - add_run(text_run(s)); + clear(); + add_run(text_run(s)); } -std::string text::get_plain_string() const +std::string formatted_text::plain_text() const { - std::string plain_string; - - for (const auto &run : runs_) - { - plain_string.append(run.get_string()); - } - - return plain_string; + return std::accumulate(runs_.begin(), runs_.end(), std::string(), + [](const std::string &a, const text_run &run) { return a + run.get_string(); }); } -std::vector text::get_runs() const +std::vector formatted_text::runs() const { - return runs_; + return runs_; } -void text::add_run(const text_run &t) +void formatted_text::add_run(const text_run &t) { - runs_.push_back(t); + runs_.push_back(t); } -bool text::operator==(const text &rhs) const +bool formatted_text::operator==(const formatted_text &rhs) const { if (runs_.size() != rhs.runs_.size()) return false; diff --git a/source/cell/tests/test_cell.hpp b/source/cell/tests/test_cell.hpp index 14f89bff..c6b3f283 100644 --- a/source/cell/tests/test_cell.hpp +++ b/source/cell/tests/test_cell.hpp @@ -612,4 +612,19 @@ public: TS_ASSERT(cell.has_hyperlink()); TS_ASSERT_EQUALS(cell.get_hyperlink(), "http://example.com"); } + + void test_comment() + { + xlnt::workbook wb; + auto ws = wb.get_active_sheet(); + auto cell = ws.get_cell("A1"); + TS_ASSERT(!cell.has_comment()); + TS_ASSERT_THROWS(cell.comment(), xlnt::exception); + cell.comment(xlnt::comment("comment", "author")); + TS_ASSERT(cell.has_comment()); + TS_ASSERT_EQUALS(cell.comment(), xlnt::comment("comment", "author")); + cell.clear_comment(); + TS_ASSERT(!cell.has_comment()); + TS_ASSERT_THROWS(cell.comment(), xlnt::exception); + } }; diff --git a/source/cell/tests/test_text.hpp b/source/cell/tests/test_text.hpp index cf96e3f0..10b2c672 100644 --- a/source/cell/tests/test_text.hpp +++ b/source/cell/tests/test_text.hpp @@ -7,13 +7,13 @@ #include -class test_text : public CxxTest::TestSuite +class test_formatted_text : public CxxTest::TestSuite { public: void test_operators() { - xlnt::text text1; - xlnt::text text2; + xlnt::formatted_text text1; + xlnt::formatted_text text2; TS_ASSERT_EQUALS(text1, text2); xlnt::text_run run_default; text1.add_run(run_default); @@ -22,42 +22,42 @@ public: TS_ASSERT_EQUALS(text1, text2); xlnt::text_run run_formatted; - run_formatted.set_color("maroon"); + run_formatted.set_color(xlnt::color::green()); run_formatted.set_font("Cambria"); run_formatted.set_scheme("ascheme"); run_formatted.set_size(40); run_formatted.set_family(17); - xlnt::text text_formatted; + xlnt::formatted_text text_formatted; text_formatted.add_run(run_formatted); xlnt::text_run run_color_differs = run_formatted; - run_color_differs.set_color("mauve"); - xlnt::text text_color_differs; + run_color_differs.set_color(xlnt::color::red()); + xlnt::formatted_text text_color_differs; text_color_differs.add_run(run_color_differs); TS_ASSERT_DIFFERS(text_formatted, text_color_differs); xlnt::text_run run_font_differs = run_formatted; run_font_differs.set_font("Calibri"); - xlnt::text text_font_differs; + xlnt::formatted_text text_font_differs; text_font_differs.add_run(run_font_differs); TS_ASSERT_DIFFERS(text_formatted, text_font_differs); xlnt::text_run run_scheme_differs = run_formatted; run_scheme_differs.set_scheme("bscheme"); - xlnt::text text_scheme_differs; + xlnt::formatted_text text_scheme_differs; text_scheme_differs.add_run(run_scheme_differs); TS_ASSERT_DIFFERS(text_formatted, text_scheme_differs); xlnt::text_run run_size_differs = run_formatted; run_size_differs.set_size(41); - xlnt::text text_size_differs; + xlnt::formatted_text text_size_differs; text_size_differs.add_run(run_size_differs); TS_ASSERT_DIFFERS(text_formatted, text_size_differs); xlnt::text_run run_family_differs = run_formatted; run_family_differs.set_family(18); - xlnt::text text_family_differs; + xlnt::formatted_text text_family_differs; text_family_differs.add_run(run_family_differs); TS_ASSERT_DIFFERS(text_formatted, text_family_differs); } diff --git a/source/cell/text_run.cpp b/source/cell/text_run.cpp index dc6df7ae..71be04d9 100644 --- a/source/cell/text_run.cpp +++ b/source/cell/text_run.cpp @@ -22,6 +22,7 @@ // @author: see AUTHORS file #include +#include namespace xlnt { @@ -68,14 +69,14 @@ bool text_run::has_color() const return (bool)color_; } -std::string text_run::get_color() const +color text_run::get_color() const { return *color_; } -void text_run::set_color(const std::string &color) +void text_run::set_color(const color &new_color) { - color_ = color; + color_ = new_color; } bool text_run::has_font() const @@ -123,4 +124,19 @@ void text_run::set_scheme(const std::string &scheme) scheme_ = scheme; } +bool text_run::bold_set() const +{ + return (bool)bold_; +} + +bool text_run::is_bold() const +{ + return *bold_; +} + +void text_run::set_bold(bool bold) +{ + bold_ = bold; +} + } // namespace xlnt diff --git a/source/detail/cell_impl.cpp b/source/detail/cell_impl.cpp index 6cb0a20d..ee115c26 100644 --- a/source/detail/cell_impl.cpp +++ b/source/detail/cell_impl.cpp @@ -24,10 +24,19 @@ #include #include "cell_impl.hpp" -#include "comment_impl.hpp" namespace xlnt { namespace detail { +cell_impl::cell_impl() + : type_(cell_type::null), + parent_(nullptr), + column_(1), + row_(1), + is_merged_(false), + value_numeric_(0) +{ +} + } // namespace detail } // namespace xlnt diff --git a/source/detail/cell_impl.hpp b/source/detail/cell_impl.hpp index eb9ee89d..266c3999 100644 --- a/source/detail/cell_impl.hpp +++ b/source/detail/cell_impl.hpp @@ -26,19 +26,20 @@ #include #include +#include +#include #include -#include #include namespace xlnt { - namespace detail { -struct comment_impl; struct worksheet_impl; struct cell_impl { + cell_impl(); + cell_type type_; worksheet_impl *parent_; @@ -46,18 +47,16 @@ struct cell_impl column_t column_; row_t row_; - bool is_merged_; + bool is_merged_; - text value_text_; + formatted_text value_text_; long double value_numeric_; optional formula_; - optional hyperlink_; - optional format_id_; - optional style_name_; + optional comment_; }; } // namespace detail diff --git a/source/detail/comment_impl.cpp b/source/detail/comment_impl.cpp deleted file mode 100644 index 420acad4..00000000 --- a/source/detail/comment_impl.cpp +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2014-2016 Thomas Fussell -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, WRISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE -// -// @license: http://www.opensource.org/licenses/mit-license.php -// @author: see AUTHORS file -#include "comment_impl.hpp" - -namespace xlnt { -namespace detail { - -comment_impl::comment_impl() -{ -} - -comment_impl::comment_impl(const comment_impl &rhs) -{ - *this = rhs; -} - -comment_impl &comment_impl::operator=(const xlnt::detail::comment_impl &rhs) -{ - text_ = rhs.text_; - author_ = rhs.author_; - - return *this; -} - -} // namespace detail -} // namespace xlnt diff --git a/source/detail/comment_impl.hpp b/source/detail/comment_impl.hpp deleted file mode 100644 index 77abae18..00000000 --- a/source/detail/comment_impl.hpp +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2014-2016 Thomas Fussell -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, WRISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE -// -// @license: http://www.opensource.org/licenses/mit-license.php -// @author: see AUTHORS file -#pragma once - -#include - -namespace xlnt { -namespace detail { - -struct cell_impl; - -struct comment_impl -{ - comment_impl(); - comment_impl(cell_impl *parent, const std::string &text, const std::string &author); - comment_impl(const comment_impl &rhs); - comment_impl &operator=(const comment_impl &rhs); - - std::string text_; - std::string author_; -}; - -} // namespace detail -} // namespace xlnt diff --git a/source/detail/custom_value_traits.cpp b/source/detail/custom_value_traits.cpp index e883090a..44db4b08 100644 --- a/source/detail/custom_value_traits.cpp +++ b/source/detail/custom_value_traits.cpp @@ -49,6 +49,10 @@ std::string to_string(relationship::type t) return "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"; case relationship::type::chartsheet: return "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"; + case relationship::type::comments: + return "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"; + case relationship::type::vml_drawing: + return "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"; default: return default_case("?"); } diff --git a/source/detail/custom_value_traits.hpp b/source/detail/custom_value_traits.hpp index 7c328f16..6c19842a 100644 --- a/source/detail/custom_value_traits.hpp +++ b/source/detail/custom_value_traits.hpp @@ -124,6 +124,7 @@ struct value_traits relationship_type::theme, relationship_type::thumbnail, relationship_type::unknown, + relationship_type::vml_drawing, relationship_type::volatile_dependencies, relationship_type::worksheet }; diff --git a/source/detail/workbook_impl.hpp b/source/detail/workbook_impl.hpp index 313043f6..092f4f6b 100644 --- a/source/detail/workbook_impl.hpp +++ b/source/detail/workbook_impl.hpp @@ -179,7 +179,7 @@ struct workbook_impl std::size_t active_sheet_index_; std::list worksheets_; - std::vector shared_strings_; + std::vector shared_strings_; bool guess_types_; bool data_only_; diff --git a/source/detail/xlsx_consumer.cpp b/source/detail/xlsx_consumer.cpp index 456a6199..46ae2785 100644 --- a/source/detail/xlsx_consumer.cpp +++ b/source/detail/xlsx_consumer.cpp @@ -21,6 +21,7 @@ // @license: http://www.opensource.org/licenses/mit-license.php // @author: see AUTHORS file #include +#include // for std::accumulate #include #include @@ -532,7 +533,7 @@ void xlsx_consumer::read_workbook() static const auto xmlns_mx = constants::get_namespace("mx"); static const auto xmlns_r = constants::get_namespace("r"); static const auto xmlns_s = constants::get_namespace("worksheet"); - static const auto xmlns_x15 = constants::get_namespace("x15"); + static const auto xmlns_x15 = constants::get_namespace("x15"); static const auto xmlns_x15ac = constants::get_namespace("x15ac"); parser().next_expect(xml::parser::event_type::start_element, xmlns, "workbook"); @@ -844,14 +845,14 @@ void xlsx_consumer::read_shared_string_table() parser().content(xml::content::complex); parser().next_expect(xml::parser::event_type::start_element); - text t; + formatted_text t; parser().attribute_map(); if (parser().name() == "t") { parser().next_expect(xml::parser::event_type::characters); - t.set_plain_string(parser().value()); + t.plain_text(parser().value()); } else if (parser().name() == "r") // possible multiple text entities. { @@ -884,7 +885,7 @@ void xlsx_consumer::read_shared_string_table() } else if (parser().qname() == xml::qname(xmlns, "color")) { - run.set_color(parser().attribute("rgb")); + run.set_color(read_color(parser())); } else if (parser().qname() == xml::qname(xmlns, "family")) { @@ -1589,6 +1590,7 @@ void xlsx_consumer::read_worksheet(const std::string &rel_id) { static const auto xmlns = constants::get_namespace("worksheet"); static const auto xmlns_mc = constants::get_namespace("mc"); + static const auto xmlns_r = constants::get_namespace("r"); static const auto xmlns_x14ac = constants::get_namespace("x14ac"); auto title = std::find_if(target_.d_->sheet_title_rel_id_map_.begin(), @@ -1984,15 +1986,164 @@ void xlsx_consumer::read_worksheet(const std::string &rel_id) parser().next(); } + else if (parser().qname() == xml::qname(xmlns, "legacyDrawing")) + { + parser().attribute(xml::qname(xmlns_r, "id")); + parser().next_expect(xml::parser::event_type::end_element, xmlns, "legacyDrawing"); + } } parser().next_expect(xml::parser::event_type::end_element, xmlns, "worksheet"); + + auto &manifest = target_.get_manifest(); + const auto workbook_rel = manifest.get_relationship(path("/"), relationship::type::office_document); + const auto sheet_rel = manifest.get_relationship(workbook_rel.get_target().get_path(), rel_id); + path sheet_path(sheet_rel.get_source().get_path().parent().append(sheet_rel.get_target().get_path())); + + for (const auto &rel : manifest.get_relationships(sheet_path)) + { + path part_path(sheet_path.parent().append(rel.get_target().get_path())); + auto split_part_path = part_path.split(); + auto part_path_iter = split_part_path.begin(); + while (part_path_iter != split_part_path.end()) + { + if (*part_path_iter == "..") + { + part_path_iter = split_part_path.erase(part_path_iter - 1, part_path_iter + 1); + continue; + } + + ++part_path_iter; + } + part_path = std::accumulate(split_part_path.begin(), split_part_path.end(), path(""), + [](const path &a, const std::string &b) { return a.append(b); }); + std::istringstream parser_stream(source_.read(part_path)); + auto receive = xml::parser::receive_default; + xml::parser parser(parser_stream, rel.get_target().get_path().string(), receive); + parser_ = &parser; + + switch (rel.get_type()) + { + case relationship::type::comments: + read_comments(ws); + break; + + default: + break; + } + + parser_ = nullptr; + } } // Sheet Relationship Target Parts -void xlsx_consumer::read_comments() +void xlsx_consumer::read_comments(worksheet ws) { + static const auto xmlns = xlnt::constants::get_namespace("worksheet"); + + std::vector authors; + + parser().next_expect(xml::parser::event_type::start_element, xmlns, "comments"); + parser().next_expect(xml::parser::event_type::start_element, xmlns, "authors"); + + for (;;) + { + if (parser().peek() == xml::parser::event_type::end_element) break; + + parser().next_expect(xml::parser::event_type::start_element, xmlns, "author"); + parser().next_expect(xml::parser::event_type::characters); + authors.push_back(parser().value()); + parser().next_expect(xml::parser::event_type::end_element, xmlns, "author"); + } + + parser().next_expect(xml::parser::event_type::end_element, xmlns, "authors"); + parser().next_expect(xml::parser::event_type::start_element, xmlns, "commentList"); + + for (;;) + { + if (parser().peek() == xml::parser::event_type::end_element) break; + + parser().next_expect(xml::parser::event_type::start_element, xmlns, "comment"); + + auto cell_ref = parser().attribute("ref"); + auto author_id = parser().attribute("authorId"); + + parser().next_expect(xml::parser::event_type::start_element, xmlns, "text"); + + // todo: this is duplicated from shared strings + formatted_text text; + + for (;;) + { + if (parser().peek() == xml::parser::event_type::end_element) break; + + parser().next_expect(xml::parser::event_type::start_element, xmlns, "r"); + text_run run; + + for (;;) + { + if (parser().peek() == xml::parser::event_type::end_element) break; + + parser().next_expect(xml::parser::event_type::start_element); + + if (parser().name() == "t") + { + parser().next_expect(xml::parser::event_type::characters); + run.set_string(parser().value()); + parser().next_expect(xml::parser::event_type::end_element, xmlns, "t"); + } + else if (parser().name() == "rPr") + { + for (;;) + { + if (parser().peek() == xml::parser::event_type::end_element) break; + + parser().next_expect(xml::parser::event_type::start_element); + + if (parser().qname() == xml::qname(xmlns, "sz")) + { + run.set_size(string_to_size_t(parser().attribute("val"))); + } + else if (parser().qname() == xml::qname(xmlns, "rFont")) + { + run.set_font(parser().attribute("val")); + } + else if (parser().qname() == xml::qname(xmlns, "color")) + { + run.set_color(read_color(parser())); + } + else if (parser().qname() == xml::qname(xmlns, "family")) + { + run.set_family(string_to_size_t(parser().attribute("val"))); + } + else if (parser().qname() == xml::qname(xmlns, "scheme")) + { + run.set_scheme(parser().attribute("val")); + } + else if (parser().qname() == xml::qname(xmlns, "b")) + { + run.set_bold(true); + } + + parser().next_expect(xml::parser::event_type::end_element); + } + + parser().next_expect(xml::parser::event_type::end_element, xmlns, "rPr"); + } + } + + text.add_run(run); + parser().next_expect(xml::parser::event_type::end_element, xmlns, "r"); + } + + ws.get_cell(cell_ref).comment(comment(text, authors.at(author_id))); + parser().next_expect(xml::parser::event_type::end_element, xmlns, "text"); + parser().next_expect(xml::parser::event_type::end_element, xmlns, "comment"); + } + + parser().next_expect(xml::parser::event_type::end_element, xmlns, "commentList"); + parser().next_expect(xml::parser::event_type::end_element, xmlns, "comments"); } void xlsx_consumer::read_drawings() diff --git a/source/detail/xlsx_consumer.hpp b/source/detail/xlsx_consumer.hpp index 2ea3e9c7..995336b5 100644 --- a/source/detail/xlsx_consumer.hpp +++ b/source/detail/xlsx_consumer.hpp @@ -37,6 +37,7 @@ namespace xlnt { class path; class relationship; class workbook; +class worksheet; namespace detail { @@ -200,7 +201,7 @@ private: /// /// /// - void read_comments(); + void read_comments(worksheet ws); /// /// diff --git a/source/detail/xlsx_crypto.cpp b/source/detail/xlsx_crypto.cpp index f20c6de5..59b87509 100644 --- a/source/detail/xlsx_crypto.cpp +++ b/source/detail/xlsx_crypto.cpp @@ -154,8 +154,8 @@ std::vector decrypt_xlsx_standard(const std::vector auto header_length = read_int(offset, encryption_info); auto index_at_start = offset; - auto skip_flags = read_int(offset, encryption_info); - auto size_extra = read_int(offset, encryption_info); + /*auto skip_flags = */read_int(offset, encryption_info); + /*auto size_extra = */read_int(offset, encryption_info); auto alg_id = read_int(offset, encryption_info); if (alg_id == 0 || alg_id == 0x0000660E || alg_id == 0x0000660F || alg_id == 0x00006610) diff --git a/source/detail/xlsx_producer.cpp b/source/detail/xlsx_producer.cpp index 2aea3861..b9d63648 100644 --- a/source/detail/xlsx_producer.cpp +++ b/source/detail/xlsx_producer.cpp @@ -655,10 +655,10 @@ void xlsx_producer::write_shared_string_table(const relationship &rel) for (const auto &string : source_.get_shared_strings()) { - if (string.get_runs().size() == 1 && !string.get_runs().at(0).has_formatting()) + if (string.runs().size() == 1 && !string.runs().at(0).has_formatting()) { serializer().start_element(xmlns, "si"); - serializer().element(xmlns, "t", string.get_plain_string()); + serializer().element(xmlns, "t", string.plain_text()); serializer().end_element(xmlns, "si"); continue; @@ -666,7 +666,7 @@ void xlsx_producer::write_shared_string_table(const relationship &rel) serializer().start_element(xmlns, "si"); - for (const auto &run : string.get_runs()) + for (const auto &run : string.runs()) { serializer().start_element(xmlns, "r"); @@ -684,7 +684,7 @@ void xlsx_producer::write_shared_string_table(const relationship &rel) if (run.has_color()) { serializer().start_element(xmlns, "color"); - serializer().attribute("val", run.get_color()); + write_color(run.get_color()); serializer().end_element(xmlns, "color"); } @@ -1974,7 +1974,7 @@ void xlsx_producer::write_worksheet(const relationship &rel) for (std::size_t i = 0; i < shared_strings.size(); i++) { - if (shared_strings[i] == cell.get_value()) + if (shared_strings[i] == cell.get_value()) { match_index = static_cast(i); break; diff --git a/source/styles/color.cpp b/source/styles/color.cpp index 96bdc236..d91c66a9 100644 --- a/source/styles/color.cpp +++ b/source/styles/color.cpp @@ -240,4 +240,20 @@ void color::assert_type(type t) const } } +bool color::operator==(const xlnt::color &other) const +{ + if (type_ != other.type_ || tint_ != other.tint_) return false; + switch(type_) + { + case type::auto_: + case type::indexed : + return indexed_.get_index() == other.indexed_.get_index(); + case type::theme: + return theme_.get_index() == other.theme_.get_index(); + case type::rgb: + return rgb_.get_hex_string() == other.rgb_.get_hex_string(); + } + return false; +} + } // namespace xlnt diff --git a/source/workbook/tests/test_consume_xlsx.hpp b/source/workbook/tests/test_consume_xlsx.hpp index dccc9aa8..30889d5e 100644 --- a/source/workbook/tests/test_consume_xlsx.hpp +++ b/source/workbook/tests/test_consume_xlsx.hpp @@ -34,4 +34,20 @@ public: xlnt::workbook wb; wb.load(path_helper::get_data_directory("17_encrypted_numbers.xlsx"), "secret"); } + + void test_comments() + { + xlnt::workbook wb; + wb.load("data/18_basic_comments.xlsx"); + + auto sheet1 = wb[0]; + TS_ASSERT_EQUALS(sheet1.get_cell("A1").get_value(), "Sheet1!A1"); + TS_ASSERT_EQUALS(sheet1.get_cell("A1").comment().plain_text(), "Sheet1 comment"); + TS_ASSERT_EQUALS(sheet1.get_cell("A1").comment().author(), "Microsoft Office User"); + + auto sheet2 = wb[1]; + TS_ASSERT_EQUALS(sheet2.get_cell("A1").get_value(), "Sheet2!A1"); + TS_ASSERT_EQUALS(sheet2.get_cell("A1").comment().plain_text(), "Sheet2 comment"); + TS_ASSERT_EQUALS(sheet2.get_cell("A1").comment().author(), "Microsoft Office User"); + } }; diff --git a/source/workbook/workbook.cpp b/source/workbook/workbook.cpp index b3aa575f..8886012e 100644 --- a/source/workbook/workbook.cpp +++ b/source/workbook/workbook.cpp @@ -954,17 +954,17 @@ const manifest &workbook::get_manifest() const return d_->manifest_; } -std::vector &workbook::get_shared_strings() +std::vector &workbook::get_shared_strings() { return d_->shared_strings_; } -const std::vector &workbook::get_shared_strings() const +const std::vector &workbook::get_shared_strings() const { return d_->shared_strings_; } -void workbook::add_shared_string(const text &shared, bool allow_duplicates) +void workbook::add_shared_string(const formatted_text &shared, bool allow_duplicates) { register_shared_string_table_in_manifest(); diff --git a/tests/data/18_basic_comments.xlsx b/tests/data/18_basic_comments.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..731e748c4e5c57c017e2829c09ea64bbfdcf1d3a GIT binary patch literal 26858 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