diff --git a/include/xlnt/cell/cell.hpp b/include/xlnt/cell/cell.hpp index 9ff5fb93..3670bd1e 100644 --- a/include/xlnt/cell/cell.hpp +++ b/include/xlnt/cell/cell.hpp @@ -54,6 +54,7 @@ class workbook; class worksheet; class xlsx_consumer; class xlsx_producer; +class phonetic_pr; struct date; struct datetime; @@ -501,6 +502,18 @@ public: /// void merged(bool merged); + // phonetics + + /// + /// Returns true if this cell is set to show phonetic information. + /// + bool phonetics_visible() const; + + /// + /// Enables the display of phonetic information on this cell. + /// + void show_phonetics(bool phonetics); + /// /// Returns the error string that is stored in this cell. /// diff --git a/include/xlnt/cell/phonetic_run.hpp b/include/xlnt/cell/phonetic_run.hpp new file mode 100644 index 00000000..e5eee8cd --- /dev/null +++ b/include/xlnt/cell/phonetic_run.hpp @@ -0,0 +1,46 @@ +// Copyright (c) 2016-2018 +// +// 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 + +#include + +namespace xlnt { + +/// +/// Encapsulates a run of text that +/// +struct XLNT_API phonetic_run +{ + std::string text; + uint32_t start; + uint32_t end; + bool preserve_space; + + bool operator==(const phonetic_run &other) const; + bool operator!=(const phonetic_run &other) const; +}; + +} // namespace xlnt diff --git a/include/xlnt/cell/rich_text.hpp b/include/xlnt/cell/rich_text.hpp index 31cb2790..c9cdcd6c 100644 --- a/include/xlnt/cell/rich_text.hpp +++ b/include/xlnt/cell/rich_text.hpp @@ -28,6 +28,8 @@ #include #include +#include +#include namespace xlnt { @@ -94,6 +96,36 @@ public: /// void add_run(const rich_text_run &t); + /// + /// Returns a copy of the individual runs that comprise this text. + /// + std::vector phonetic_runs() const; + + /// + /// Sets the runs of this text all at once. + /// + void phonetic_runs(const std::vector &new_phonetic_runs); + + /// + /// Adds a new run to the end of the set of runs. + /// + void add_phonetic_run(const phonetic_run &t); + + /// + /// Returns true if this text has phonetic properties + /// + bool has_phonetic_properties() const; + + /// + /// Returns the phonetic properties of this text. + /// + const phonetic_pr &phonetic_properties() const; + + /// + /// Sets the phonetic properties of this text to phonetic_props + /// + void phonetic_properties(const phonetic_pr& phonetic_props); + /// /// Copies rich text object from other /// @@ -124,6 +156,8 @@ private: /// The runs that make up this rich text. /// std::vector runs_; + std::vector phonetic_runs_; + optional phonetic_properties_; }; class XLNT_API rich_text_hash diff --git a/include/xlnt/worksheet/row_properties.hpp b/include/xlnt/worksheet/row_properties.hpp index d78c4313..a2e92e25 100644 --- a/include/xlnt/worksheet/row_properties.hpp +++ b/include/xlnt/worksheet/row_properties.hpp @@ -63,6 +63,13 @@ public: /// The index to the style used by all cells in this row /// optional style; + + /// + /// The row column span, used as part of the row block optimisation. + /// This used for loading this attribute from existing excel files mainly for inspecting + /// and not used when saving, it is calculated in the xlsx_producer. + /// + optional spans; }; inline bool operator==(const row_properties &lhs, const row_properties &rhs) @@ -72,7 +79,8 @@ inline bool operator==(const row_properties &lhs, const row_properties &rhs) && lhs.custom_height == rhs.custom_height && lhs.hidden == rhs.hidden && lhs.custom_format == rhs.custom_format - && lhs.style == rhs.style; + && lhs.style == rhs.style + && lhs.spans == rhs.spans; } } // namespace xlnt diff --git a/source/cell/cell.cpp b/source/cell/cell.cpp index 0f1ba819..25007f55 100644 --- a/source/cell/cell.cpp +++ b/source/cell/cell.cpp @@ -56,6 +56,7 @@ #include #include #include +#include namespace { @@ -198,7 +199,7 @@ cell::cell(detail::cell_impl *d) bool cell::garbage_collectible() const { - return !(has_value() || is_merged() || has_formula() || has_format() || has_hyperlink()); + return !(has_value() || is_merged() || phonetics_visible() || has_formula() || has_format() || has_hyperlink()); } void cell::value(std::nullptr_t) @@ -329,6 +330,16 @@ bool cell::is_merged() const return d_->is_merged_; } +bool cell::phonetics_visible() const +{ + return d_->phonetics_visible_; +} + +void cell::show_phonetics(bool phonetics) +{ + d_->phonetics_visible_ = phonetics; +} + bool cell::is_date() const { return data_type() == type::number diff --git a/source/cell/phonetic_run.cpp b/source/cell/phonetic_run.cpp new file mode 100644 index 00000000..65da5a9a --- /dev/null +++ b/source/cell/phonetic_run.cpp @@ -0,0 +1,40 @@ +// Copyright (c) 2014-2018 +// +// 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 +#include + +namespace xlnt { + +bool phonetic_run::operator==(const phonetic_run &other) const +{ + return std::tie(text, start, end, preserve_space) == + std::tie(other.text, other.start, other.end, other.preserve_space); +} + +bool phonetic_run::operator!=(const phonetic_run &other) const +{ + return !(*this == other); +} + +} // namespace xlnt diff --git a/source/cell/rich_text.cpp b/source/cell/rich_text.cpp index 3cf13290..86b5e67e 100644 --- a/source/cell/rich_text.cpp +++ b/source/cell/rich_text.cpp @@ -52,8 +52,10 @@ rich_text::rich_text(const rich_text &other) rich_text &rich_text::operator=(const rich_text &rhs) { - runs_.clear(); + clear(); runs_ = rhs.runs_; + phonetic_runs_ = rhs.phonetic_runs_; + phonetic_properties_ = rhs.phonetic_properties_; return *this; } @@ -65,6 +67,8 @@ rich_text::rich_text(const rich_text_run &single_run) void rich_text::clear() { runs_.clear(); + phonetic_runs_.clear(); + phonetic_properties_.clear(); } void rich_text::plain_text(const std::string &s, bool preserve_space = false) @@ -99,6 +103,36 @@ void rich_text::add_run(const rich_text_run &t) runs_.push_back(t); } +std::vector rich_text::phonetic_runs() const +{ + return phonetic_runs_; +} + +void rich_text::phonetic_runs(const std::vector &new_phonetic_runs) +{ + phonetic_runs_ = new_phonetic_runs; +} + +void rich_text::add_phonetic_run(const phonetic_run &r) +{ + phonetic_runs_.push_back(r); +} + +bool rich_text::has_phonetic_properties() const +{ + return phonetic_properties_.is_set(); +} + +const phonetic_pr& rich_text::phonetic_properties() const +{ + return phonetic_properties_.get(); +} + +void rich_text::phonetic_properties(const phonetic_pr& phonetic_props) +{ + phonetic_properties_.set(phonetic_props); +} + bool rich_text::operator==(const rich_text &rhs) const { if (runs_.size() != rhs.runs_.size()) return false; @@ -108,6 +142,15 @@ bool rich_text::operator==(const rich_text &rhs) const if (runs_[i] != rhs.runs_[i]) return false; } + if (phonetic_runs_.size() != rhs.phonetic_runs_.size()) return false; + + for (std::size_t i = 0; i < phonetic_runs_.size(); i++) + { + if (phonetic_runs_[i] != rhs.phonetic_runs_[i]) return false; + } + + if (phonetic_properties_ != rhs.phonetic_properties_) return false; + return true; } diff --git a/source/detail/implementations/cell_impl.cpp b/source/detail/implementations/cell_impl.cpp index 8be6324b..b5ac9ef4 100644 --- a/source/detail/implementations/cell_impl.cpp +++ b/source/detail/implementations/cell_impl.cpp @@ -35,6 +35,7 @@ cell_impl::cell_impl() column_(1), row_(1), is_merged_(false), + phonetics_visible_(false), value_numeric_(0) { } diff --git a/source/detail/implementations/cell_impl.hpp b/source/detail/implementations/cell_impl.hpp index 64756770..4d7bfc63 100644 --- a/source/detail/implementations/cell_impl.hpp +++ b/source/detail/implementations/cell_impl.hpp @@ -55,6 +55,7 @@ struct cell_impl row_t row_; bool is_merged_; + bool phonetics_visible_; rich_text value_text_; double value_numeric_; @@ -72,6 +73,7 @@ inline bool operator==(const cell_impl &lhs, const cell_impl &rhs) && lhs.column_ == rhs.column_ && lhs.row_ == rhs.row_ && lhs.is_merged_ == rhs.is_merged_ + && lhs.phonetics_visible_ == rhs.phonetics_visible_ && lhs.value_text_ == rhs.value_text_ && lhs.value_numeric_ == rhs.value_numeric_ && lhs.formula_ == rhs.formula_ diff --git a/source/detail/serialization/xlsx_consumer.cpp b/source/detail/serialization/xlsx_consumer.cpp index 9a05603f..c27e92ea 100644 --- a/source/detail/serialization/xlsx_consumer.cpp +++ b/source/detail/serialization/xlsx_consumer.cpp @@ -221,9 +221,13 @@ cell xlsx_consumer::read_cell() row_properties.dy_descent = parser().attribute(qn("x14ac", "dyDescent")); } + if (parser().attribute_present("spans")) { + row_properties.spans = parser().attribute("spans"); + } + skip_attributes({"customFormat", "s", "customFont", "outlineLevel", "collapsed", "thickTop", "thickBot", - "ph", "spans"}); + "ph"}); } if (!in_element(qn("spreadsheetml", "row"))) @@ -241,6 +245,11 @@ cell xlsx_consumer::read_cell() cell.d_->column_ = reference.column_index(); cell.d_->row_ = reference.row(); + if (parser().attribute_present("ph")) + { + cell.d_->phonetics_visible_ = parser().attribute("ph"); + } + auto has_type = parser().attribute_present("t"); auto type = has_type ? parser().attribute("t") : "n"; @@ -713,9 +722,13 @@ void xlsx_consumer::read_worksheet_sheetdata() row_properties.custom_format.set(parser().attribute("customFormat")); } + if (parser().attribute_present("spans")) { + row_properties.spans = parser().attribute("spans"); + } + skip_attributes({"customFont", "outlineLevel", "collapsed", "thickTop", "thickBot", - "ph", "spans"}); + "ph"}); while (in_element(qn("spreadsheetml", "row"))) { @@ -730,6 +743,11 @@ void xlsx_consumer::read_worksheet_sheetdata() cell.format(target_.format(static_cast(std::stoull(parser().attribute("s"))))); } + if (parser().attribute_present("ph")) + { + cell.d_->phonetics_visible_ = parser().attribute("ph"); + } + auto has_value = false; auto value_string = std::string(); @@ -2989,11 +3007,34 @@ rich_text xlsx_consumer::read_rich_text(const xml::qname &parent) } else if (text_element == xml::qname(xmlns, "rPh")) { - skip_remaining_content(text_element); + phonetic_run pr; + pr.start = parser().attribute("sb"); + pr.end = parser().attribute("eb"); + + expect_start_element(xml::qname(xmlns, "t"), xml::content::simple); + pr.text = read_text(); + + if (parser().attribute_present(xml_space)) + { + pr.preserve_space = parser().attribute(xml_space) == "preserve"; + } + + expect_end_element(xml::qname(xmlns, "t")); + + t.add_phonetic_run(pr); } else if (text_element == xml::qname(xmlns, "phoneticPr")) { - skip_remaining_content(text_element); + phonetic_pr ph(parser().attribute("fontId")); + if (parser().attribute_present("type")) + { + ph.type(phonetic_pr::type_from_string(parser().attribute("type"))); + } + if (parser().attribute_present("alignment")) + { + ph.alignment(phonetic_pr::alignment_from_string(parser().attribute("alignment"))); + } + t.phonetic_properties(ph); } else { diff --git a/source/detail/serialization/xlsx_producer.cpp b/source/detail/serialization/xlsx_producer.cpp index f8172c16..eaa60302 100644 --- a/source/detail/serialization/xlsx_producer.cpp +++ b/source/detail/serialization/xlsx_producer.cpp @@ -862,6 +862,36 @@ void xlsx_producer::write_shared_string_table(const relationship & /*rel*/) write_characters(string.second.plain_text(), string.second.runs().front().preserve_space); write_end_element(xmlns, "t"); + + for (const auto &run : string.second.phonetic_runs()) + { + write_start_element(xmlns, "rPh"); + write_attribute("eb", run.end); + write_attribute("sb", run.start); + + write_start_element(xmlns, "t"); + write_characters(run.text, run.preserve_space); + write_end_element(xmlns, "t"); + + write_end_element(xmlns, "rPh"); + } + + if (string.second.has_phonetic_properties()) + { + write_start_element(xmlns, phonetic_pr::Serialised_ID()); + const auto &ph_props = string.second.phonetic_properties(); + write_attribute("fontId", ph_props.font_id()); + if (ph_props.has_type()) + { + write_attribute("type", phonetic_pr::type_as_string(ph_props.type())); + } + if (ph_props.has_alignment()) + { + write_attribute("alignment", phonetic_pr::alignment_as_string(ph_props.alignment())); + } + write_end_element(xmlns, phonetic_pr::Serialised_ID()); + } + write_end_element(xmlns, "si"); continue; @@ -927,6 +957,35 @@ void xlsx_producer::write_shared_string_table(const relationship & /*rel*/) write_end_element(xmlns, "r"); } + for (const auto &run : string.second.phonetic_runs()) + { + write_start_element(xmlns, "rPh"); + write_attribute("sb", run.start); + write_attribute("eb", run.end); + + write_start_element(xmlns, "t"); + write_characters(run.text, run.preserve_space); + write_end_element(xmlns, "t"); + + write_end_element(xmlns, "rPh"); + } + + if (string.second.has_phonetic_properties()) + { + write_start_element(xmlns, phonetic_pr::Serialised_ID()); + const auto &ph_props = string.second.phonetic_properties(); + write_attribute("fontId", ph_props.font_id()); + if (ph_props.has_type()) + { + write_attribute("type", phonetic_pr::type_as_string(ph_props.type())); + } + if (ph_props.has_alignment()) + { + write_attribute("alignment", phonetic_pr::alignment_as_string(ph_props.alignment())); + } + write_end_element(xmlns, phonetic_pr::Serialised_ID()); + } + write_end_element(xmlns, "si"); } @@ -2439,6 +2498,10 @@ void xlsx_producer::write_worksheet(const relationship &rel) // See note for CT_Row, span attribute about block optimization if (first_row_in_block) { + // reset block column range + first_block_column = constants::max_column(); + last_block_column = constants::min_column(); + first_check_row = row; // round up to the next multiple of 16 last_check_row = ((row / 16) + 1) * 16; @@ -2448,8 +2511,8 @@ void xlsx_producer::write_worksheet(const relationship &rel) { for (auto column = dimension.top_left().column(); column <= dimension.bottom_right().column(); ++column) { - if (!ws.has_cell(cell_reference(column, row))) continue; - auto cell = ws.cell(cell_reference(column, row)); + if (!ws.has_cell(cell_reference(column, check_row))) continue; + auto cell = ws.cell(cell_reference(column, check_row)); if (cell.garbage_collectible()) continue; first_block_column = std::min(first_block_column, cell.column()); @@ -2534,6 +2597,11 @@ void xlsx_producer::write_worksheet(const relationship &rel) write_attribute("r", cell.reference().to_string()); + if (cell.phonetics_visible()) + { + write_attribute("ph", write_bool(true)); + } + if (cell.has_format()) { write_attribute("s", cell.format().d_->id); diff --git a/source/worksheet/worksheet.cpp b/source/worksheet/worksheet.cpp index 4f74aa92..59403528 100644 --- a/source/worksheet/worksheet.cpp +++ b/source/worksheet/worksheet.cpp @@ -578,7 +578,7 @@ column_t worksheet::highest_column_or_props() const range_reference worksheet::calculate_dimension() const { - return range_reference(lowest_column(), lowest_row(), + return range_reference(lowest_column(), lowest_row_or_props(), highest_column(), highest_row_or_props()); } diff --git a/tests/cell/cell_test_suite.cpp b/tests/cell/cell_test_suite.cpp index edfc6ba2..73de12cf 100644 --- a/tests/cell/cell_test_suite.cpp +++ b/tests/cell/cell_test_suite.cpp @@ -82,6 +82,7 @@ public: register_test(test_hyperlink); register_test(test_comment); register_test(test_copy_and_compare); + register_test(test_cell_phonetic_properties); } private: @@ -811,6 +812,19 @@ private: cell3 = cell2; xlnt_assert_equals(cell2, cell3); } + + void test_cell_phonetic_properties() + { + xlnt::workbook wb; + auto ws = wb.active_sheet(); + auto cell1 = ws.cell("A1"); + + xlnt_assert_equals(cell1.phonetics_visible(), false); + cell1.show_phonetics(true); + xlnt_assert_equals(cell1.phonetics_visible(), true); + cell1.show_phonetics(false); + xlnt_assert_equals(cell1.phonetics_visible(), false); + } }; static cell_test_suite x{}; \ No newline at end of file diff --git a/tests/cell/rich_text_test_suite.cpp b/tests/cell/rich_text_test_suite.cpp index 3af26383..af866c4e 100644 --- a/tests/cell/rich_text_test_suite.cpp +++ b/tests/cell/rich_text_test_suite.cpp @@ -36,6 +36,8 @@ public: { register_test(test_operators); register_test(test_runs); + register_test(test_phonetic_runs); + register_test(test_phonetic_properties); } void test_operators() @@ -115,5 +117,30 @@ public: rt.runs(test_runs); xlnt_assert_equals(test_runs, rt.runs()); } + + void test_phonetic_runs() + { + xlnt::rich_text rt; + rt.plain_text("取引", true); + rt.add_phonetic_run({"トリヒキ", 0, 2}); + + xlnt_assert_equals(rt.phonetic_runs().size(), 1); + xlnt_assert_equals(rt.phonetic_runs()[0].text, "トリヒキ"); + xlnt_assert_equals(rt.phonetic_runs()[0].start, 0); + xlnt_assert_equals(rt.phonetic_runs()[0].end, 2); + } + + void test_phonetic_properties() + { + xlnt::rich_text rt; + xlnt::phonetic_pr ph(1); + ph.type(xlnt::phonetic_pr::type_from_string("fullwidthKatakana")); + ph.alignment(xlnt::phonetic_pr::alignment_from_string("Center")); + rt.phonetic_properties(ph); + + xlnt_assert_equals(rt.has_phonetic_properties(), true); + xlnt_assert_equals(rt.phonetic_properties().has_type(), true); + xlnt_assert_equals(rt.phonetic_properties().has_alignment(), true); + } }; static rich_text_test_suite x{}; \ No newline at end of file diff --git a/tests/data/15_phonetics.xlsx b/tests/data/15_phonetics.xlsx new file mode 100644 index 00000000..2af2e5b3 Binary files /dev/null and b/tests/data/15_phonetics.xlsx differ diff --git a/tests/data/Issue353_first_row_empty_w_properties.xlsx b/tests/data/Issue353_first_row_empty_w_properties.xlsx new file mode 100644 index 00000000..65cdbe87 Binary files /dev/null and b/tests/data/Issue353_first_row_empty_w_properties.xlsx differ diff --git a/tests/workbook/workbook_test_suite.cpp b/tests/workbook/workbook_test_suite.cpp index 9f155087..f883e359 100644 --- a/tests/workbook/workbook_test_suite.cpp +++ b/tests/workbook/workbook_test_suite.cpp @@ -67,6 +67,7 @@ public: register_test(test_id_gen); register_test(test_load_file); register_test(test_Issue279); + register_test(test_Issue353); } void test_active_sheet() @@ -495,5 +496,18 @@ public: //save a copy file wb.save("temp.xlsx"); } + + void test_Issue353() + { + xlnt::workbook wb1; + wb1.load(path_helper::test_file("Issue353_first_row_empty_w_properties.xlsx")); + wb1.save("temp_issue353.xlsx"); + + xlnt::workbook wb2; + wb2.load("temp_issue353.xlsx"); + auto ws = wb2.active_sheet(); + xlnt_assert_equals(ws.row_properties(1).spans.get(), "1:8"); + xlnt_assert_equals(ws.row_properties(17).spans.get(), "2:7"); + } }; static workbook_test_suite x; \ No newline at end of file diff --git a/tests/worksheet/worksheet_test_suite.cpp b/tests/worksheet/worksheet_test_suite.cpp index ed6f8eeb..bc80bfe8 100644 --- a/tests/worksheet/worksheet_test_suite.cpp +++ b/tests/worksheet/worksheet_test_suite.cpp @@ -105,6 +105,7 @@ public: register_test(test_clear_row); register_test(test_set_title); register_test(test_set_title_unicode); + register_test(test_phonetics); register_test(test_insert_rows); register_test(test_insert_columns); register_test(test_delete_rows); @@ -1279,6 +1280,29 @@ public: xlnt::exception); } + void test_phonetics() + { + xlnt::workbook wb; + wb.load(path_helper::test_file("15_phonetics.xlsx")); + auto ws = wb.active_sheet(); + + xlnt_assert_equals(ws.cell("A1").phonetics_visible(), true); + xlnt_assert_equals(ws.cell("A1").value().phonetic_runs()[0].text, "シュウ "); + xlnt_assert_equals(ws.cell("B1").phonetics_visible(), true); + xlnt_assert_equals(ws.cell("C1").phonetics_visible(), false); + + wb.save("temp.xlsx"); + + xlnt::workbook wb2; + wb2.load("temp.xlsx"); + auto ws2 = wb2.active_sheet(); + + xlnt_assert_equals(ws2.cell("A1").phonetics_visible(), true); + xlnt_assert_equals(ws2.cell("A1").value().phonetic_runs()[0].text, "シュウ "); + xlnt_assert_equals(ws2.cell("B1").phonetics_visible(), true); + xlnt_assert_equals(ws2.cell("C1").phonetics_visible(), false); + } + void test_insert_rows() { xlnt::workbook wb;