From 698b40c54c122214b8f7fcd5992d41f5b8f13fa5 Mon Sep 17 00:00:00 2001 From: Kostas Dizas <254960+kostasdizas@users.noreply.github.com> Date: Fri, 5 Oct 2018 14:30:54 +0100 Subject: [PATCH] Added basic support for embedded images --- include/xlnt/drawing/spreadsheet_drawing.hpp | 59 +++++++++ include/xlnt/workbook/workbook.hpp | 1 - include/xlnt/worksheet/worksheet.hpp | 6 + source/detail/constants.cpp | 3 + .../detail/implementations/worksheet_impl.hpp | 4 + source/detail/serialization/xlsx_consumer.cpp | 41 +++++- source/detail/serialization/xlsx_consumer.hpp | 2 +- source/detail/serialization/xlsx_producer.cpp | 45 ++++++- source/detail/serialization/xlsx_producer.hpp | 19 +-- source/drawing/spreadsheet_drawing.cpp | 121 ++++++++++++++++++ source/utils/path.cpp | 6 + source/worksheet/worksheet.cpp | 5 + tests/CMakeLists.txt | 2 + tests/data/14_images.xlsx | Bin 0 -> 21723 bytes tests/drawing/drawing_test_suite.cpp | 59 +++++++++ 15 files changed, 357 insertions(+), 16 deletions(-) create mode 100644 include/xlnt/drawing/spreadsheet_drawing.hpp create mode 100644 source/drawing/spreadsheet_drawing.cpp create mode 100644 tests/data/14_images.xlsx create mode 100644 tests/drawing/drawing_test_suite.cpp 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 0000000000000000000000000000000000000000..89644bc1708fe818a0ec09543f77e411a921d056 GIT binary patch literal 21723 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%0DKNzi!VbEm z>c2&b+wyx1SrBW&6$pSB4@5<_o`;wq0LrO~D|njQxd_@X87;>H#dJkWRUb+h1KznvjN8GCNo+5J zz{?6|+mpHTimeOE8HhM^X&QlKXDMu}j>@m07cG6nLV9NvhrW@ZNqDR_w4Mrnh2gLPe zIYy@xOhPRS1=N9Fp&SgKtfee-hO9gm7#HjvMYlAI>tqPHTILIRqqDWCP$KYtk9I2w zF#fVAxLa|l;>7+Hp}M3f0o5no>zB0UFkhRoz3u2mWpS-PL7B%|Lb*7hMK^s8Y@JS- zHotticZ2C3a&e_5aTtzUJK(p77(j(DPkvrQSTJoRY=)j+>RK0qaFRyOHdvddU8;_a z3iz9CQ;CFn$R9<9wW!snRvziNAd@8=7k0a{LBZgeTd0TT{VEF{-dZC~w~!TFB3(n) zT1oN1zN>**;nuT;U&AP6S$7q%J{q!1usDlWc}7^T&X}WGz{6xDHWi^T4`0`=Yh>?f z$qwrLdJ`*tB6eW8XvIH0iBDw6v)3dDkff1j-0bp9Q99Blpr#<-QEjer;$&|er|iRb zQ-`U&sQ=6Wx66Z6u2bJW1NH55QOPTg?r38#cMtDl4jvvq@nQe*R;VMFj(v5B_$~I$ zw7!>qPIkO%4=V=6B`V?NWX#}4SWLFI(sD8xyTn*$G@5ix(Tiy6PLbma;V0N0{*pZ* zn_%?7qytvSdRw|zZB}x*Y9nv~#`BEp;vNZ9UX1S4P{>#NJ|*9jK@lDlu+RkT{g{wu zdn+jGIxfdGEperh@G%1cCkc#*t<<*S*DH>eo%o9n9lZ?mKW>&lDk5*mmn0lood^h; zW*kVnTgo;<*8!f%@mE5+R9|K!i-25iQ7|p`OSb2)d1GefgVcry69E?$_R^2E10RK* za$2G^stU3%lr&wa$S0qUjWLzRZ(#w)PA_opVJX*Nj)Q3Z*{Go)j}}mV?;4u>6$%zk zcq5z=fmZ78ytp+etrNDN+}vbOAOBqGeV_NK^%0!VqI%>V@AvTz?=w8|Ngtg^ZsT$4 zu20uip+>_B!*nDYSF&cMvk)4rmx+RKyu^qmx7d3Tp>G{F^4Ef7RaX|53T8gLAVVa_ zH_prDjE0tNP~&m`EM1pbrA0|<=`K@u;Lg%TyL+AcuXexnp8wcf!ZmvG2!5m>_(0rBbz?hegK8OTL@c6GpEdLVU#Hdv1gr z+~aw#$ky4+pAtw%1lyjw6)9C-n;R;ShrOchfH1YPmWmO2ntNwqqs2>m3P?&wuS}0k zo;EMP*tBY+7s@ad@~SJgNS#_jKMIVF9t`*$U-8#?{yP2(ZPD0Zx4~}u+b`j_V-9t~ z-NAyR749Zt?G)mv45Z)lv1om0$w}Kz6V&J_4M&MqOpC197US5yi~kj1L+e7z7}<8w z*t^^1H^dQILt1Lbw&5u%9O~yY{Xd2WtsyN4W7|-T>$@Q>7K7G+Hp;tg@Px{ eHxN72zb0m5114$_e@i`c04`9!gEPo)Xa56J5-XMf literal 0 HcmV?d00001 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;