implement sheetFormatPr, x14ac:dyDescent, reordering workbook rels so sheets come first after insertions

This commit is contained in:
Thomas Fussell 2018-01-26 14:32:00 -05:00
parent 90633d0e8e
commit 0f0d3de75f
12 changed files with 370 additions and 27 deletions

View File

@ -839,6 +839,11 @@ private:
/// </summary>
void swap(workbook &other);
/// <summary>
/// Sheet 1 should be rId1, sheet 2 should be rId2, etc.
/// </summary>
void reorder_relationships();
/// <summary>
/// An opaque pointer to a structure that holds all of the data relating to this workbook.
/// </summary>

View File

@ -0,0 +1,53 @@
// Copyright (c) 2018 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 <xlnt/xlnt_config.hpp>
#include <xlnt/utils/optional.hpp>
namespace xlnt {
/// <summary>
/// General worksheet formatting properties.
/// </summary>
class XLNT_API sheet_format_properties
{
public:
/// <summary>
/// The base column width
/// </summary>
optional<double> base_col_width;
/// <summary>
/// The default row height
/// </summary>
optional<double> default_row_height;
/// <summary>
/// x14ac extension, dyDescent property
/// </summary>
optional<double> dy_descent;
};
} // namespace xlnt

View File

@ -54,6 +54,7 @@ class range_iterator;
class range_reference;
class relationship;
class row_properties;
class sheet_format_properties;
class workbook;
struct date;
@ -708,6 +709,26 @@ public:
/// </summary>
xlnt::conditional_format conditional_format(const range_reference &ref, const condition &when);
/// <summary>
/// Returns the path of this worksheet in the containing package.
/// </summary>
path path() const;
/// <summary>
/// Returns the relationship from the parent workbook to this worksheet.
/// </summary>
relationship referring_relationship() const;
/// <summary>
/// Returns the current formatting properties.
/// </summary>
sheet_format_properties format_properties() const;
/// <summary>
/// Sets the format properties to the given properties.
/// </summary>
void format_properties(const sheet_format_properties &properties);
private:
friend class cell;
friend class const_range_iterator;

View File

@ -34,6 +34,7 @@
#include <xlnt/cell/cell_reference.hpp>
#include <xlnt/cell/comment.hpp>
#include <xlnt/cell/rich_text.hpp>
#include <xlnt/packaging/manifest.hpp>
#include <xlnt/packaging/relationship.hpp>
#include <xlnt/styles/alignment.hpp>
#include <xlnt/styles/border.hpp>
@ -372,6 +373,23 @@ void cell::hyperlink(const std::string &hyperlink)
throw invalid_parameter();
}
auto ws = worksheet();
auto &manifest = ws.workbook().manifest();
bool existing = false;
for (const auto &rel : manifest.relationships(ws.path(), relationship_type::hyperlink))
{
if (rel.target().path().string() == hyperlink)
{
existing = true;
}
}
if (!existing) {
manifest.register_relationship(uri(ws.path().string()), relationship_type::hyperlink,
uri(hyperlink), target_mode::external);
}
d_->hyperlink_ = hyperlink;
}

View File

@ -29,12 +29,13 @@
#include <detail/implementations/cell_impl.hpp>
#include <xlnt/workbook/named_range.hpp>
#include <xlnt/worksheet/range.hpp>
#include <xlnt/worksheet/range_reference.hpp>
#include <xlnt/worksheet/sheet_view.hpp>
#include <xlnt/worksheet/column_properties.hpp>
#include <xlnt/worksheet/header_footer.hpp>
#include <xlnt/worksheet/range.hpp>
#include <xlnt/worksheet/range_reference.hpp>
#include <xlnt/worksheet/row_properties.hpp>
#include <xlnt/worksheet/sheet_format_properties.hpp>
#include <xlnt/worksheet/sheet_view.hpp>
namespace xlnt {
@ -62,18 +63,10 @@ struct worksheet_impl
id_ = other.id_;
title_ = other.title_;
format_properties_ = other.format_properties_;
column_properties_ = other.column_properties_;
row_properties_ = other.row_properties_;
cell_map_ = other.cell_map_;
for (auto &row : cell_map_)
{
for (auto &cell : row.second)
{
cell.second.parent_ = this;
}
}
page_setup_ = other.page_setup_;
auto_filter_ = other.auto_filter_;
page_margins_ = other.page_margins_;
@ -86,6 +79,14 @@ struct worksheet_impl
views_ = other.views_;
column_breaks_ = other.column_breaks_;
row_breaks_ = other.row_breaks_;
for (auto &row : cell_map_)
{
for (auto &cell : row.second)
{
cell.second.parent_ = this;
}
}
}
workbook *parent_;
@ -93,6 +94,8 @@ struct worksheet_impl
std::size_t id_;
std::string title_;
sheet_format_properties format_properties_;
std::unordered_map<column_t, column_properties> column_properties_;
std::unordered_map<row_t, row_properties> row_properties_;

View File

@ -518,7 +518,25 @@ std::string xlsx_consumer::read_worksheet_begin(const std::string &rel_id)
}
else if (current_worksheet_element == qn("spreadsheetml", "sheetFormatPr")) // CT_SheetFormatPr 0-1
{
skip_remaining_content(current_worksheet_element);
if (parser().attribute_present("baseColWidth"))
{
ws.d_->format_properties_.base_col_width =
parser().attribute<double>("baseColWidth");
}
if (parser().attribute_present("defaultRowHeight"))
{
ws.d_->format_properties_.default_row_height =
parser().attribute<double>("defaultRowHeight");
}
if (parser().attribute_present(qn("x14ac", "dyDescent")))
{
ws.d_->format_properties_.dy_descent =
parser().attribute<double>(qn("x14ac", "dyDescent"));
}
skip_attributes();
}
else if (current_worksheet_element == qn("spreadsheetml", "cols")) // CT_Cols 0+
{

View File

@ -1990,6 +1990,7 @@ void xlsx_producer::write_worksheet(const relationship &rel)
{
static const auto &xmlns = constants::ns("spreadsheetml");
static const auto &xmlns_r = constants::ns("r");
static const auto &xmlns_x14ac = constants::ns("x14ac");
auto worksheet_part = rel.source().path().parent().append(rel.target().path());
auto worksheet_rels = source_.manifest().relationships(worksheet_part);
@ -2005,6 +2006,21 @@ void xlsx_producer::write_worksheet(const relationship &rel)
write_namespace(xmlns, "");
write_namespace(xmlns_r, "r");
auto using_namespace = [&ws](const std::string &ns)
{
if (ns == "x14ac")
{
return ws.format_properties().dy_descent.is_set();
}
return false;
};
if (using_namespace("x14ac"))
{
write_namespace(xmlns_x14ac, "x14ac");
}
if (ws.has_page_setup())
{
write_start_element(xmlns, "sheetPr");
@ -2098,8 +2114,26 @@ void xlsx_producer::write_worksheet(const relationship &rel)
}
write_start_element(xmlns, "sheetFormatPr");
write_attribute("baseColWidth", "10");
write_attribute("defaultRowHeight", "16");
const auto &format_properties = ws.d_->format_properties_;
if (format_properties.base_col_width.is_set())
{
write_attribute("baseColWidth",
format_properties.base_col_width.get());
}
if (format_properties.default_row_height.is_set())
{
write_attribute("defaultRowHeight",
format_properties.default_row_height.get());
}
if (format_properties.dy_descent.is_set())
{
write_attribute(xml::qname(xmlns_x14ac, "dyDescent"),
format_properties.dy_descent.get());
}
write_end_element(xmlns, "sheetFormatPr");
bool has_column_properties = false;

View File

@ -63,8 +63,16 @@ relationship_type relationship::type() const
bool relationship::operator==(const relationship &rhs) const
{
return type_ == rhs.type_ && id_ == rhs.id_ && source_ == rhs.source_ && target_ == rhs.target_
return type_ == rhs.type_
&& id_ == rhs.id_
&& source_ == rhs.source_
&& target_ == rhs.target_
&& mode_ == rhs.mode_;
}
bool relationship::operator!=(const relationship &rhs) const
{
return !(*this == rhs);
}
} // namespace xlnt

View File

@ -429,6 +429,10 @@ workbook workbook::empty()
sheet_view view;
ws.add_view(view);
auto &format_properties = ws.d_->format_properties_;
format_properties.base_col_width = 10.0;
format_properties.default_row_height = 16.0;
wb.theme(xlnt::theme());
wb.d_->stylesheet_ = detail::stylesheet();
@ -719,6 +723,7 @@ worksheet workbook::create_sheet()
d_->sheet_title_rel_id_map_[title] = ws_rel;
update_sheet_properties();
reorder_relationships();
return worksheet(&d_->worksheets_.back());
}
@ -1496,4 +1501,70 @@ void workbook::update_sheet_properties()
}
}
void workbook::reorder_relationships()
{
const auto wb_rel = manifest().relationship(path("/"), relationship_type::office_document);
const auto wb_path = wb_rel.target().path();
const auto relationships = manifest().relationships(wb_path);
std::unordered_map<std::string, relationship> rel_map;
const auto titles = sheet_titles();
const auto title_rel_id_map = d_->sheet_title_rel_id_map_;
bool needs_reorder = false;
for (const auto &rel : relationships)
{
rel_map[rel.id()] = rel;
if (rel.type() == relationship_type::worksheet)
{
for (auto title_index = std::size_t(0); title_index < titles.size(); ++title_index)
{
const auto title = titles[title_index];
if (title_rel_id_map.at(title) == rel.id())
{
const auto expected_rel_id = "rId" + std::to_string(title_index + 1);
if (expected_rel_id != rel.id())
{
needs_reorder = true;
}
break;
}
}
}
}
if (!needs_reorder)
{
return;
}
for (const auto &rel : relationships)
{
manifest().unregister_relationship(uri(wb_path.string()), rel.id());
}
for (auto index = std::size_t(0); index < rel_map.size(); ++index)
{
auto rel_id = "rId" + std::to_string(index + 1);
auto old_rel_id = std::string();
if (index < titles.size())
{
auto title = titles[index];
old_rel_id = title_rel_id_map.at(title);
d_->sheet_title_rel_id_map_[title] = rel_id;
} else {
old_rel_id = "rId" + std::to_string(index - titles.size() + 2);
}
auto old_rel = rel_map[old_rel_id];
auto new_rel = relationship(rel_id, old_rel.type(),
old_rel.source(), old_rel.target(), old_rel.target_mode());
manifest().register_relationship(new_rel);
}
}
} // namespace xlnt

View File

@ -1110,4 +1110,30 @@ conditional_format worksheet::conditional_format(const range_reference &ref, con
return workbook().d_->stylesheet_.get().add_conditional_format_rule(d_, ref, when);
}
path worksheet::path() const
{
auto rel = referring_relationship();
return xlnt::path(rel.source().path().parent().append(rel.target().path()));
}
relationship worksheet::referring_relationship() const
{
auto &manifest = workbook().manifest();
auto wb_rel = manifest.relationship(xlnt::path("/"),
relationship_type::office_document);
auto ws_rel = manifest.relationship(wb_rel.target().path(),
workbook().d_->sheet_title_rel_id_map_.at(title()));
return ws_rel;
}
sheet_format_properties worksheet::format_properties() const
{
return d_->format_properties_;
}
void worksheet::format_properties(const sheet_format_properties &properties)
{
d_->format_properties_ = properties;
}
} // namespace xlnt

View File

@ -1,6 +1,7 @@
#pragma once
#include <sstream>
#include <unordered_set>
#include <detail/external/include_libstudxml.hpp>
#include <detail/serialization/vector_streambuf.hpp>
@ -15,9 +16,22 @@ public:
const std::string &right, const std::string &content_type)
{
// content types are stored in unordered maps, too complicated to compare
if (content_type == "[Content_Types].xml") return true;
if (content_type == "[Content_Types].xml")
{
return true;
}
// calcChain is optional
if (content_type == "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml") return true;
if (content_type == "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml")
{
return true;
}
// compared already
if (content_type == "application/vnd.openxmlformats-package.relationships+xml")
{
return true;
}
auto is_xml = (content_type.substr(0, 12) == "application/"
&& content_type.substr(content_type.size() - 4) == "+xml")
@ -206,6 +220,57 @@ public:
return !difference;
}
static bool compare_relationships(const xlnt::manifest &left,
const xlnt::manifest &right)
{
std::unordered_set<std::string> parts;
for (const auto &part : left.parts())
{
parts.insert(part.string());
auto left_rels = left.relationships(part);
auto right_rels = right.relationships(part);
if (left_rels.size() != right_rels.size())
{
return false;
}
std::unordered_map<std::string, xlnt::relationship> left_rels_map;
for (const auto &rel : left_rels)
{
left_rels_map[rel.id()] = rel;
}
for (const auto &right_rel : right_rels)
{
if (left_rels_map.count(right_rel.id()) != 1)
{
return false;
}
const auto &left_rel = left_rels_map.at(right_rel.id());
if (left_rel != right_rel)
{
return false;
}
}
}
for (const auto &part : right.parts())
{
if (parts.count(part.string()) != 1)
{
return false;
}
}
return true;
}
static bool xlsx_archives_match(const std::vector<std::uint8_t> &left,
const std::vector<std::uint8_t> &right)
{
@ -271,6 +336,11 @@ public:
auto &left_manifest = left_workbook.manifest();
auto &right_manifest = right_workbook.manifest();
if (!compare_relationships(left_manifest, right_manifest))
{
return false;
}
for (auto left_member : left_info)
{
if (!right_archive.has_file(left_member))

View File

@ -1,4 +1,4 @@
// Copyright (c) 2014-2018 Thomas Fussell
// Copyright (c) 2014-2018 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
@ -31,6 +31,7 @@
#include <helpers/test_suite.hpp>
#include <helpers/path_helper.hpp>
#include <helpers/xml_helper.hpp>
#include <xlnt/worksheet/sheet_format_properties.hpp>
#include <xlnt/workbook/streaming_workbook_reader.hpp>
#include <xlnt/workbook/streaming_workbook_writer.hpp>
#include <xlnt/workbook/workbook.hpp>
@ -78,6 +79,7 @@ public:
{
std::vector<std::uint8_t> wb_data;
wb.save(wb_data);
wb.save("temp.xlsx");
std::ifstream file_stream(file.string(), std::ios::binary);
auto file_data = xlnt::detail::to_vector(file_stream);
@ -178,8 +180,25 @@ public:
void test_write_comments_hyperlinks_formulae()
{
xlnt::workbook wb;
xlnt::sheet_format_properties format_properties;
format_properties.base_col_width = 10.0;
format_properties.default_row_height = 16.0;
format_properties.dy_descent = 0.2;
auto sheet1 = wb.active_sheet();
auto comment_font = xlnt::font().bold(true).size(10).color(xlnt::indexed_color(81)).name("Calibri");
sheet1.format_properties(format_properties);
auto comment_font = xlnt::font()
.bold(true)
.size(10)
.color(xlnt::indexed_color(81))
.name("Calibri");
sheet1.cell("A4").hyperlink("https://microsoft.com/", "hyperlink1");
sheet1.cell("A5").hyperlink("https://google.com/");
sheet1.cell("A6").hyperlink(sheet1.cell("A1"));
sheet1.cell("A7").hyperlink("mailto:invalid@example.com?subject=important");
sheet1.cell("A1").value("Sheet1!A1");
sheet1.cell("A1").comment("Sheet1 comment", comment_font, "Microsoft Office User");
@ -187,24 +206,21 @@ public:
sheet1.cell("A2").value("Sheet1!A2");
sheet1.cell("A2").comment("Sheet1 comment2", comment_font, "Microsoft Office User");
sheet1.cell("A4").hyperlink("https://microsoft.com", "hyperlink1");
sheet1.cell("A5").hyperlink("https://google.com");
sheet1.cell("A6").hyperlink(sheet1.cell("A1"));
sheet1.cell("A7").hyperlink("mailto:invalid@example.com?subject=important");
sheet1.cell("C1").formula("=CONCATENATE(C2,C3)");
sheet1.cell("C2").value("a");
sheet1.cell("C3").value("b");
auto sheet2 = wb.create_sheet();
sheet2.format_properties(format_properties);
sheet2.cell("A4").hyperlink("https://apple.com/", "hyperlink2");
sheet2.cell("A1").value("Sheet2!A1");
sheet2.cell("A2").comment("Sheet2 comment", comment_font, "Microsoft Office User");
sheet2.cell("A2").value("Sheet2!A2");
sheet2.cell("A2").comment("Sheet2 comment2", comment_font, "Microsoft Office User");
sheet2.cell("A4").hyperlink("https://apple.com", "hyperlink2");
sheet2.cell("C1").formula("=C2*C3");
sheet2.cell("C2").value(2);
sheet2.cell("C3").value(3);