diff --git a/include/xlnt/drawing/spreadsheet_drawing.hpp b/include/xlnt/drawing/spreadsheet_drawing.hpp new file mode 100644 index 00000000..6bb4bb3a --- /dev/null +++ b/include/xlnt/drawing/spreadsheet_drawing.hpp @@ -0,0 +1,59 @@ +// Copyright (c) 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 +#include + +namespace xml { + class parser; + class serializer; +} + +namespace xlnt { + +class worksheet; + +namespace drawing { + +/// +/// The spreadsheet_drawing class encapsulates the information +/// captured from objects within the spreadsheetDrawing schema. +/// +class XLNT_API spreadsheet_drawing +{ +public: + spreadsheet_drawing(xml::parser &parser); + void serialize(xml::serializer &serializer); + + std::vector get_embed_ids(); +private: + std::string serialized_value_; + std::vector embed_ids_; +}; + +} // namespace drawing +} // namespace xlnt + diff --git a/include/xlnt/workbook/workbook.hpp b/include/xlnt/workbook/workbook.hpp index 3ba26859..3fa7551f 100644 --- a/include/xlnt/workbook/workbook.hpp +++ b/include/xlnt/workbook/workbook.hpp @@ -50,7 +50,6 @@ class cell; class cell_style; class color; class const_worksheet_iterator; -class drawing; class fill; class font; class format; diff --git a/include/xlnt/worksheet/worksheet.hpp b/include/xlnt/worksheet/worksheet.hpp index ed460992..c706353e 100644 --- a/include/xlnt/worksheet/worksheet.hpp +++ b/include/xlnt/worksheet/worksheet.hpp @@ -745,6 +745,12 @@ public: /// void format_properties(const sheet_format_properties &properties); + /// + /// Returns true if this worksheet has a page setup. + /// + bool has_drawing() const; + + private: friend class cell; friend class const_range_iterator; diff --git a/source/detail/constants.cpp b/source/detail/constants.cpp index 9ccb4cd7..465de702 100644 --- a/source/detail/constants.cpp +++ b/source/detail/constants.cpp @@ -152,6 +152,9 @@ const std::unordered_map &constants::namespaces() {"xml", "http://www.w3.org/XML/1998/namespace"}, {"xsi", "http://www.w3.org/2001/XMLSchema-instance"}, + {"a", "http://schemas.openxmlformats.org/drawingml/2006/main"}, + {"xdr", "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"}, + {"loext", "http://schemas.libreoffice.org/"} }; diff --git a/source/detail/implementations/worksheet_impl.hpp b/source/detail/implementations/worksheet_impl.hpp index 7ae00ae4..2d629d62 100644 --- a/source/detail/implementations/worksheet_impl.hpp +++ b/source/detail/implementations/worksheet_impl.hpp @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -157,6 +158,9 @@ struct worksheet_impl optional sheet_properties_; optional extension_list_; + + std::string drawing_rel_id_; + optional drawing_; }; } // namespace detail diff --git a/source/detail/serialization/xlsx_consumer.cpp b/source/detail/serialization/xlsx_consumer.cpp index 3ac9eb0b..af54dcbe 100644 --- a/source/detail/serialization/xlsx_consumer.cpp +++ b/source/detail/serialization/xlsx_consumer.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -1188,7 +1189,11 @@ worksheet xlsx_consumer::read_worksheet_end(const std::string &rel_id) } else if (current_worksheet_element == qn("spreadsheetml", "drawing")) // CT_Drawing 0-1 { - skip_remaining_content(current_worksheet_element); + if (parser().attribute_present(qn("r", "id"))) + { + auto drawing_rel_id = parser().attribute(qn("r", "id")); + ws.d_->drawing_rel_id_ = drawing_rel_id; + } } else if (current_worksheet_element == qn("spreadsheetml", "legacyDrawing")) { @@ -1236,6 +1241,20 @@ worksheet xlsx_consumer::read_worksheet_end(const std::string &rel_id) } } + if (manifest.has_relationship(sheet_path, xlnt::relationship_type::drawings)) + { + auto drawings_part = manifest.canonicalize({workbook_rel, sheet_rel, + manifest.relationship(sheet_path, xlnt::relationship_type::drawings)}); + + auto receive = xml::parser::receive_default; + auto drawings_part_streambuf = archive_->open(drawings_part); + std::istream drawings_part_stream(drawings_part_streambuf.get()); + xml::parser parser(drawings_part_stream, drawings_part.string(), receive); + parser_ = &parser; + + read_drawings(ws, drawings_part); + } + return ws; } @@ -2695,8 +2714,26 @@ void xlsx_consumer::read_comments(worksheet ws) expect_end_element(qn("spreadsheetml", "comments")); } -void xlsx_consumer::read_drawings() +void xlsx_consumer::read_drawings(worksheet ws, const path &part) { + auto images = manifest().relationships(part, relationship_type::image); + + auto sd = drawing::spreadsheet_drawing(parser()); + + for (const auto image_rel_id : sd.get_embed_ids()) + { + auto image_rel = std::find_if(images.begin(), images.end(), + [&](const relationship &r) { return r.id() == image_rel_id; }); + + if (image_rel != images.end()) + { + const auto url = image_rel->target().path().resolve(part.parent()); + + read_image(url); + } + } + + ws.d_->drawing_ = sd; } // Unknown Parts diff --git a/source/detail/serialization/xlsx_consumer.hpp b/source/detail/serialization/xlsx_consumer.hpp index 25aa7898..fd6d70fb 100644 --- a/source/detail/serialization/xlsx_consumer.hpp +++ b/source/detail/serialization/xlsx_consumer.hpp @@ -232,7 +232,7 @@ private: /// /// /// - void read_drawings(); + void read_drawings(worksheet ws, const path &part); // Unknown Parts diff --git a/source/detail/serialization/xlsx_producer.cpp b/source/detail/serialization/xlsx_producer.cpp index f8172c16..3be332d4 100644 --- a/source/detail/serialization/xlsx_producer.cpp +++ b/source/detail/serialization/xlsx_producer.cpp @@ -2965,10 +2965,13 @@ void xlsx_producer::write_worksheet(const relationship &rel) write_start_element(xmlns, "legacyDrawing"); write_attribute(xml::qname(xmlns_r, "id"), child_rel.id()); write_end_element(xmlns, "legacyDrawing"); - - // todo: there's only one of these per sheet, right? - break; } + else if (child_rel.type() == xlnt::relationship_type::drawings) + { + write_start_element(xmlns, "drawing"); + write_attribute(xml::qname(xmlns_r, "id"), child_rel.id()); + write_end_element(xmlns, "drawing"); + } } } @@ -3016,6 +3019,10 @@ void xlsx_producer::write_worksheet(const relationship &rel) { write_vml_drawings(child_rel, ws, cells_with_comments); } + else if (child_rel.type() == relationship_type::drawings) + { + write_drawings(child_rel, ws); + } } } } @@ -3271,6 +3278,38 @@ void xlsx_producer::write_vml_drawings(const relationship &rel, worksheet ws, co write_end_element("xml"); } +void xlsx_producer::write_drawings(const relationship &drawing_rel, worksheet ws) +{ + const auto workbook_rel = source_.manifest().relationship(path("/"), relationship_type::office_document); + const auto worksheet_rel = ws.referring_relationship(); + const auto drawing_part = source_.manifest().canonicalize({workbook_rel, worksheet_rel, drawing_rel}); + const auto drawing_rels = source_.manifest().relationships(drawing_part); + + if (ws.d_->drawing_.is_set()) + { + ws.d_->drawing_.get().serialize(*current_part_serializer_); + } + + if (!drawing_rels.empty()) + { + write_relationships(drawing_rels, drawing_part); + + for (auto rel : drawing_rels) + { + if (rel.type() == relationship_type::image) + { + const auto image_path = source_.manifest().canonicalize({workbook_rel, worksheet_rel, rel}); + if (image_path.string().find("cid:") != std::string::npos) + { + // skip cid attachments + continue; + } + write_image(image_path); + } + } + } +} + // Other Parts void xlsx_producer::write_custom_property() diff --git a/source/detail/serialization/xlsx_producer.hpp b/source/detail/serialization/xlsx_producer.hpp index 96050942..6dd91d70 100644 --- a/source/detail/serialization/xlsx_producer.hpp +++ b/source/detail/serialization/xlsx_producer.hpp @@ -122,7 +122,8 @@ private: // Sheet Relationship Target Parts void write_comments(const relationship &rel, worksheet ws, const std::vector &cells); - void write_vml_drawings(const relationship &rel, worksheet ws, const std::vector &cells); + void write_vml_drawings(const relationship &rel, worksheet ws, const std::vector &cells); + void write_drawings(const relationship &rel, worksheet ws); // Other Parts @@ -137,8 +138,8 @@ private: /// Both are valid, but we can use this method to write either depending on the producer /// we're trying to match. /// - std::string write_bool(bool boolean) const; - + std::string write_bool(bool boolean) const; + void write_relationships(const std::vector &relationships, const path &part); void write_color(const xlnt::color &color); void write_border(const xlnt::border &b); @@ -188,12 +189,12 @@ private: current_part_serializer_->characters(characters); } - /// - /// A reference to the workbook which is the object of read/write operations. - /// - const workbook &source_; - - std::unique_ptr archive_; + /// + /// A reference to the workbook which is the object of read/write operations. + /// + const workbook &source_; + + std::unique_ptr archive_; std::unique_ptr current_part_serializer_; std::unique_ptr current_part_streambuf_; std::ostream current_part_stream_; diff --git a/source/drawing/spreadsheet_drawing.cpp b/source/drawing/spreadsheet_drawing.cpp new file mode 100644 index 00000000..c67c2405 --- /dev/null +++ b/source/drawing/spreadsheet_drawing.cpp @@ -0,0 +1,121 @@ +// Copyright (c) 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 + +#include + +namespace { + // copy elements to the serializer provided and extract the embed ids + // from blip elements + std::vector copy_and_extract(xml::parser& p, xml::serializer &s) + { + std::vector embed_ids; + int nest_level = 0; + while (nest_level > 0 || (p.peek() != xml::parser::event_type::end_element && p.peek() != xml::parser::event_type::eof)) + { + switch (p.next()) + { + case xml::parser::start_element: + { + ++nest_level; + auto attribs = p.attribute_map(); + auto current_ns = p.namespace_(); + s.start_element(p.qname()); + s.namespace_decl(current_ns, p.prefix()); + if (p.qname().name() == "blip") + { + embed_ids.push_back(attribs.at(xml::qname(xlnt::constants::ns("r"), "embed")).value); + } + p.peek(); + auto new_ns = p.namespace_(); + if (new_ns != current_ns) + { + auto pref = p.prefix(); + s.namespace_decl(new_ns, pref); + } + for (auto &ele : attribs) + { + s.attribute(ele.first, ele.second.value); + } + break; + } + case xml::parser::end_element: + { + --nest_level; + s.end_element(); + break; + } + case xml::parser::start_namespace_decl: + { + s.namespace_decl(p.namespace_(), p.prefix()); + break; + } + case xml::parser::end_namespace_decl: + { // nothing required here + break; + } + case xml::parser::characters: + { + s.characters(p.value()); + break; + } + case xml::parser::eof: + return embed_ids; + case xml::parser::start_attribute: + case xml::parser::end_attribute: + default: + break; + } + } + return embed_ids; + } +} + +namespace xlnt { +namespace drawing { + +spreadsheet_drawing::spreadsheet_drawing(xml::parser &parser) +{ + std::ostringstream serialization_stream; + xml::serializer s(serialization_stream, "", 0); + embed_ids_ = copy_and_extract(parser, s); + serialized_value_ = serialization_stream.str(); +} + +// void spreadsheet_drawing::serialize(xml::serializer &serializer, const std::string& ns) +void spreadsheet_drawing::serialize(xml::serializer &serializer) +{ + std::istringstream ser(serialized_value_); + xml::parser p(ser, "", xml::parser::receive_default); + copy_and_extract(p, serializer); +} + +std::vector spreadsheet_drawing::get_embed_ids() +{ + return embed_ids_; +} + +} // namespace drawing +} // namespace xlnt diff --git a/source/utils/path.cpp b/source/utils/path.cpp index 738d71f9..ca8f69cb 100644 --- a/source/utils/path.cpp +++ b/source/utils/path.cpp @@ -252,6 +252,12 @@ path path::resolve(const path &base_path) const for (const auto &part : split()) { + if (part == "..") + { + copy = copy.parent(); + continue; + } + copy = copy.append(part); } diff --git a/source/worksheet/worksheet.cpp b/source/worksheet/worksheet.cpp index 33386f84..e502267f 100644 --- a/source/worksheet/worksheet.cpp +++ b/source/worksheet/worksheet.cpp @@ -1121,4 +1121,9 @@ void worksheet::format_properties(const sheet_format_properties &properties) d_->format_properties_ = properties; } +bool worksheet::has_drawing() const +{ + return d_->drawing_.is_set(); +} + } // namespace xlnt diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 74c24352..4a442257 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,6 +15,7 @@ if(STATIC_CRT) endif() file(GLOB CELL_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/cell/*.cpp) +file(GLOB DRAWING_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/drawing/*.cpp) file(GLOB PACKAGING_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/packaging/*.cpp) file(GLOB STYLES_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/styles/*.cpp) file(GLOB UTILS_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/utils/*.cpp) @@ -23,6 +24,7 @@ file(GLOB WORKSHEET_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/worksheet/*.cpp) set(TESTS ${CELL_TESTS} + ${DRAWING_TESTS} ${PACKAGING_TESTS} ${STYLES_TESTS} ${UTILS_TESTS} diff --git a/tests/data/14_images.xlsx b/tests/data/14_images.xlsx new file mode 100644 index 00000000..89644bc1 Binary files /dev/null and b/tests/data/14_images.xlsx differ diff --git a/tests/drawing/drawing_test_suite.cpp b/tests/drawing/drawing_test_suite.cpp new file mode 100644 index 00000000..9242ac2f --- /dev/null +++ b/tests/drawing/drawing_test_suite.cpp @@ -0,0 +1,59 @@ +// 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 +// 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 +#include +#include +#include + +class drawing_test_suite : public test_suite +{ +public: + drawing_test_suite() + { + register_test(test_load_save); + } + + void test_load_save() + { + xlnt::workbook wb1; + wb1.load(path_helper::test_file("2_minimal.xlsx")); + auto ws1 = wb1.active_sheet(); + xlnt_assert_equals(ws1.has_drawing(), false); + + xlnt::workbook wb2; + wb2.load(path_helper::test_file("14_images.xlsx")); + auto ws2 = wb2.active_sheet(); + xlnt_assert_equals(ws2.has_drawing(), true); + wb2.save("temp_with_images.xlsx"); + + xlnt::workbook wb3; + wb3.load("temp_with_images.xlsx"); + auto ws3 = wb3.active_sheet(); + xlnt_assert_equals(ws3.has_drawing(), true); + } +}; +static drawing_test_suite x;