From 1301027696c9f0e8ac2fa38d5757ba0f1de1bfd0 Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Wed, 24 Sep 2025 15:28:49 -0700 Subject: [PATCH 01/13] silence compiler warnings --- Examples/Demo1.cpp | 4 ++-- OpenXLSX/headers/XLCell.hpp | 12 ++++++++++++ OpenXLSX/headers/XLCellValue.hpp | 4 ++-- OpenXLSX/headers/XLRowData.hpp | 6 +++--- OpenXLSX/headers/XLSharedStrings.hpp | 2 +- OpenXLSX/sources/XLCell.cpp | 10 ++++++++++ OpenXLSX/sources/XLCellIterator.cpp | 2 +- OpenXLSX/sources/XLCellReference.cpp | 2 +- OpenXLSX/sources/XLCellValue.cpp | 2 +- OpenXLSX/sources/XLComments.cpp | 2 +- OpenXLSX/sources/XLDocument.cpp | 2 +- OpenXLSX/sources/XLDrawing.cpp | 2 +- OpenXLSX/sources/XLRelationships.cpp | 2 +- OpenXLSX/sources/XLRow.cpp | 2 +- OpenXLSX/sources/XLRowData.cpp | 2 +- Tests/testXLRow.cpp | 10 +++++----- 16 files changed, 44 insertions(+), 22 deletions(-) diff --git a/Examples/Demo1.cpp b/Examples/Demo1.cpp index 8c0f244f..b3c51447 100644 --- a/Examples/Demo1.cpp +++ b/Examples/Demo1.cpp @@ -13,7 +13,7 @@ using namespace OpenXLSX; void printAllDocumentComments(XLDocument const & doc) { for( size_t i = 1; i <= doc.workbook().worksheetCount(); ++i ) { - auto wks = doc.workbook().worksheet(i); + auto wks = doc.workbook().worksheet(static_cast(i)); if( wks.hasComments() ) { std::cout << "worksheet(" << i << ") with name \"" << wks.name() << "\" has comments" << std::endl; XLComments wksComments = wks.comments(); @@ -114,7 +114,7 @@ int main() // date/time data. See https://en.cppreference.com/w/cpp/chrono/c/tm for more information. // An XLDateTime object can be created from a std::tm object: - std::tm tm; + std::tm tm = {}; // Initialize all members to zero tm.tm_year = 121; tm.tm_mon = 8; tm.tm_mday = 1; diff --git a/OpenXLSX/headers/XLCell.hpp b/OpenXLSX/headers/XLCell.hpp index 6500c42e..a9e5711f 100644 --- a/OpenXLSX/headers/XLCell.hpp +++ b/OpenXLSX/headers/XLCell.hpp @@ -267,6 +267,18 @@ namespace OpenXLSX */ XLCellAssignable (XLCell && other); + /** + * @brief Copy constructor for XLCellAssignable + * @param other the XLCellAssignable to construct from + */ + XLCellAssignable (const XLCellAssignable& other); + + /** + * @brief Move constructor for XLCellAssignable + * @param other the XLCellAssignable to construct from + */ + XLCellAssignable (XLCellAssignable&& other) noexcept; + // /** // * @brief Inherit all constructors with parameters from XLCell // */ diff --git a/OpenXLSX/headers/XLCellValue.hpp b/OpenXLSX/headers/XLCellValue.hpp index dc7e8205..9e70273d 100644 --- a/OpenXLSX/headers/XLCellValue.hpp +++ b/OpenXLSX/headers/XLCellValue.hpp @@ -84,9 +84,9 @@ namespace OpenXLSX struct VisitXLCellValueTypeToDouble { std::string packageName = "VisitXLCellValueTypeToDouble"; - double operator()(int64_t v) const { return v; } + double operator()(int64_t v) const { return static_cast(v); } double operator()(double v) const { return v; } - double operator()(bool v) const { return v; } + double operator()(bool v) const { return static_cast(v); } // double operator()( struct timestamp v ) { /* to be implemented if this type ever gets supported */ } double operator()(std::string v) const { throw XLValueTypeError("string is not convertible to double."); // disable if implicit conversion of string to double shall be allowed diff --git a/OpenXLSX/headers/XLRowData.hpp b/OpenXLSX/headers/XLRowData.hpp index 6c489ea8..416b9dbb 100644 --- a/OpenXLSX/headers/XLRowData.hpp +++ b/OpenXLSX/headers/XLRowData.hpp @@ -283,12 +283,12 @@ namespace OpenXLSX // ===== If the container value_type is XLCellValue, the values can be copied directly. if constexpr (std::is_same_v) { // ===== First, delete the values in the first N columns. - deleteCellValues(values.size()); // 2024-04-30: whitespace support + deleteCellValues(static_cast(values.size())); // 2024-04-30: whitespace support // ===== Then, prepend new cell nodes to current row node auto colNo = values.size(); for (auto value = values.rbegin(); value != values.rend(); ++value) { // NOLINT - prependCellValue(*value, colNo); // 2024-04-30: whitespace support: this is safe because only prependCellValue (with + prependCellValue(*value, static_cast(colNo)); // 2024-04-30: whitespace support: this is safe because only prependCellValue (with // whitespace support) touches the row data --colNo; } @@ -296,7 +296,7 @@ namespace OpenXLSX // ===== If the container value_type is a POD type, use the overloaded operator= on each cell. else { - auto range = XLRowDataRange(*m_rowNode, 1, values.size(), getSharedStrings()); + auto range = XLRowDataRange(*m_rowNode, 1, static_cast(values.size()), getSharedStrings()); auto dst = range.begin(); // 2024-04-30: whitespace support: safe because XLRowDataRange::begin invokes whitespace-safe // getCellNode for column 1 auto src = values.begin(); diff --git a/OpenXLSX/headers/XLSharedStrings.hpp b/OpenXLSX/headers/XLSharedStrings.hpp index 9f695bde..8e87d805 100644 --- a/OpenXLSX/headers/XLSharedStrings.hpp +++ b/OpenXLSX/headers/XLSharedStrings.hpp @@ -133,7 +133,7 @@ namespace OpenXLSX * @brief return the amount of shared string entries currently in the cache * @return */ - int32_t stringCount() const { return m_stringCache->size(); } + int32_t stringCount() const { return static_cast( m_stringCache->size() ); } /** * @brief diff --git a/OpenXLSX/sources/XLCell.cpp b/OpenXLSX/sources/XLCell.cpp index 7b1fdd49..5f955505 100644 --- a/OpenXLSX/sources/XLCell.cpp +++ b/OpenXLSX/sources/XLCell.cpp @@ -242,6 +242,16 @@ XLCellAssignable::XLCellAssignable (XLCell const & other) : XLCell(other) {} */ XLCellAssignable::XLCellAssignable (XLCell && other) : XLCell(std::move(other)) {} +/** + * @details + */ +XLCellAssignable::XLCellAssignable (const XLCellAssignable& other) : XLCell(other) {} + +/** + * @details + */ +XLCellAssignable::XLCellAssignable (XLCellAssignable&& other) noexcept : XLCell(std::move(other)) {} + /** * @details */ diff --git a/OpenXLSX/sources/XLCellIterator.cpp b/OpenXLSX/sources/XLCellIterator.cpp index 04914b45..e9ddac4d 100644 --- a/OpenXLSX/sources/XLCellIterator.cpp +++ b/OpenXLSX/sources/XLCellIterator.cpp @@ -283,7 +283,7 @@ void XLCellIterator::updateCurrentCell(bool createIfMissing) XMLNode rowNode = m_hintNode.parent().next_sibling_of_type(pugi::node_element); uint32_t rowNo = 0; while (not rowNode.empty()) { - rowNo = rowNode.attribute("r").as_ullong(); + rowNo = static_cast( rowNode.attribute("r").as_ullong() ); if (rowNo >= m_currentRow) break; // if desired row was reached / passed, break before incrementing rowNode rowNode = rowNode.next_sibling_of_type(pugi::node_element); } diff --git a/OpenXLSX/sources/XLCellReference.cpp b/OpenXLSX/sources/XLCellReference.cpp index 214ecaf0..0e72c2a2 100644 --- a/OpenXLSX/sources/XLCellReference.cpp +++ b/OpenXLSX/sources/XLCellReference.cpp @@ -382,7 +382,7 @@ XLCoordinates XLCellReference::coordinatesFromAddress(const std::string& address for (; pos < address.length() && std::isdigit(address[pos]); ++pos) // check digits rowNo = rowNo * 10 + (address[pos] - '0'); if (pos == address.length() && rowNo <= MAX_ROWS) // full address was < 4 letters + only digits - return std::make_pair(rowNo, colNo); + return XLCoordinates(static_cast(rowNo), static_cast(colNo)); } throw XLInputError("XLCellReference::coordinatesFromAddress - address \"" + address + "\" is invalid"); diff --git a/OpenXLSX/sources/XLCellValue.cpp b/OpenXLSX/sources/XLCellValue.cpp index 5701e093..c5ff6d07 100644 --- a/OpenXLSX/sources/XLCellValue.cpp +++ b/OpenXLSX/sources/XLCellValue.cpp @@ -494,7 +494,7 @@ XLCellValue XLCellValueProxy::getValue() const int32_t XLCellValueProxy::stringIndex() const { if (strcmp(m_cellNode->attribute("t").value(), "s") != 0) return -1; // cell value is not a shared string - return m_cellNode->child("v").text().as_ullong(-1); // return the shared string index stored for this cell + return static_cast( m_cellNode->child("v").text().as_ullong(-1) );// return the shared string index stored for this cell /**/ // if, for whatever reason, the underlying XML has no reference stored, also return -1 } diff --git a/OpenXLSX/sources/XLComments.cpp b/OpenXLSX/sources/XLComments.cpp index 4b396967..62498911 100644 --- a/OpenXLSX/sources/XLComments.cpp +++ b/OpenXLSX/sources/XLComments.cpp @@ -468,7 +468,7 @@ bool XLComments::set(std::string const& cellRef, std::string const& commentText, try { cShape = shape(cellRef); // for existing comments, try to access existing shape } - catch (XLException const& e) { + catch (XLException const& ) { newShapeNeeded = true; // not found: create fresh } } diff --git a/OpenXLSX/sources/XLDocument.cpp b/OpenXLSX/sources/XLDocument.cpp index 5e26fe1f..648c64f6 100644 --- a/OpenXLSX/sources/XLDocument.cpp +++ b/OpenXLSX/sources/XLDocument.cpp @@ -1459,7 +1459,7 @@ void XLDocument::setSavingDeclaration(XLXmlSavingDeclaration const& savingDeclar */ void XLDocument::cleanupSharedStrings() { - int32_t oldStringCount = m_sharedStringCache.size(); + int32_t oldStringCount = static_cast( m_sharedStringCache.size() ); std::vector< int32_t > indexMap(oldStringCount, -1); // indexMap[ oldIndex ] :== newIndex, -1 = not yet assigned int32_t newStringCount = 1; // reserve index 0 for empty string, count here +1 for each unique shared string index that is in use in the worksheet diff --git a/OpenXLSX/sources/XLDrawing.cpp b/OpenXLSX/sources/XLDrawing.cpp index 1befa63e..8bbf6281 100644 --- a/OpenXLSX/sources/XLDrawing.cpp +++ b/OpenXLSX/sources/XLDrawing.cpp @@ -278,7 +278,7 @@ int16_t XLShapeStyle::attributeOrderIndex(std::string const& attributeName) cons auto attributeIterator = std::find(m_nodeOrder.begin(), m_nodeOrder.end(), attributeName); if (attributeIterator == m_nodeOrder.end()) return -1; - return attributeIterator - m_nodeOrder.begin(); + return static_cast( attributeIterator - m_nodeOrder.begin() ); } /** diff --git a/OpenXLSX/sources/XLRelationships.cpp b/OpenXLSX/sources/XLRelationships.cpp index a0f18c41..fe37deb1 100644 --- a/OpenXLSX/sources/XLRelationships.cpp +++ b/OpenXLSX/sources/XLRelationships.cpp @@ -99,7 +99,7 @@ namespace OpenXLSX { // anonymous namespace: do not export these symbols std::random_device rd; rdSeed = rd(); } - Rand32.seed(rdSeed); + Rand32.seed(static_cast(rdSeed)); RandomizerInitialized = true; } } // namespace OpenXLSX diff --git a/OpenXLSX/sources/XLRow.cpp b/OpenXLSX/sources/XLRow.cpp index 4f47e3b4..ded0bd2a 100644 --- a/OpenXLSX/sources/XLRow.cpp +++ b/OpenXLSX/sources/XLRow.cpp @@ -486,7 +486,7 @@ namespace OpenXLSX XMLNode rowNode = m_hintRow.next_sibling_of_type(pugi::node_element); uint32_t rowNo = 0; while (not rowNode.empty()) { - rowNo = rowNode.attribute("r").as_ullong(); + rowNo = static_cast(rowNode.attribute("r").as_ullong()); if (rowNo >= m_currentRowNumber) break; // if desired row was reached / passed, break before incrementing rowNode rowNode = rowNode.next_sibling_of_type(pugi::node_element); } diff --git a/OpenXLSX/sources/XLRowData.cpp b/OpenXLSX/sources/XLRowData.cpp index 0787eee6..37d9affe 100644 --- a/OpenXLSX/sources/XLRowData.cpp +++ b/OpenXLSX/sources/XLRowData.cpp @@ -383,7 +383,7 @@ namespace OpenXLSX // ===== prepend new cell nodes to current row node XMLNode curNode{}; - uint16_t colNo = values.size(); + uint16_t colNo = static_cast(values.size()); for (auto value = values.rbegin(); value != values.rend(); ++value) { // NOLINT curNode = m_rowNode->prepend_child("c"); setDefaultCellAttributes(curNode, XLCellReference(static_cast(m_row->rowNumber()), colNo).address(), *m_rowNode, colNo); diff --git a/Tests/testXLRow.cpp b/Tests/testXLRow.cpp index c8876b20..513b03f2 100644 --- a/Tests/testXLRow.cpp +++ b/Tests/testXLRow.cpp @@ -32,9 +32,9 @@ TEST_CASE("XLRow Tests", "[XLRow]") XLRow copy3; copy3 = copy2; - copy3.setHeight(height * 3); + copy3.setHeight(static_cast(height * 3)); REQUIRE(copy3.height() == height * 3); - copy3.setHeight(height * 4); + copy3.setHeight(static_cast(height * 4)); REQUIRE(copy3.height() == height * 4); XLRow copy4; @@ -162,7 +162,7 @@ TEST_CASE("XLRowData Tests", "[XLRowData]") const auto row1c = row1; - auto val1sum = 0; + std::string::size_type val1sum = 0; const auto val1results1 = static_cast>(row1c.values()); for (const auto v : val1results1) val1sum += v.size(); REQUIRE(val1sum == 12); @@ -294,7 +294,7 @@ TEST_CASE("XLRowData Tests", "[XLRowData]") const auto row1c = row1; - auto val1sum = 0; + std::string::size_type val1sum = 0; const auto val1results1 = static_cast>(row1c.values()); for (const auto v : val1results1) val1sum += v.size(); REQUIRE(val1sum == 12); @@ -426,7 +426,7 @@ TEST_CASE("XLRowData Tests", "[XLRowData]") const auto row1c = row1; - auto val1sum = 0; + std::string::size_type val1sum = 0; const auto val1results1 = static_cast>(row1c.values()); for (const auto v : val1results1) val1sum += v.size(); REQUIRE(val1sum == 12); From 763ab8a52ca34839572ad3a51c6164f5bd3a6cc1 Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Wed, 1 Oct 2025 16:04:16 -0700 Subject: [PATCH 02/13] Embedded Image Support -- phase I --- Examples/CMakeLists.txt | 6 + Examples/Demo11.cpp | 580 +++++++++++++++++++++ OpenXLSX/CMakeLists.txt | 2 + OpenXLSX/OpenXLSX.hpp | 1 + OpenXLSX/headers/XLContentTypes.hpp | 14 + OpenXLSX/headers/XLDocument.hpp | 40 ++ OpenXLSX/headers/XLDrawingML.hpp | 114 ++++ OpenXLSX/headers/XLImage.hpp | 300 +++++++++++ OpenXLSX/headers/XLSheet.hpp | 161 ++++++ OpenXLSX/sources/XLContentTypes.cpp | 16 + OpenXLSX/sources/XLDocument.cpp | 80 +++ OpenXLSX/sources/XLDrawingML.cpp | 296 +++++++++++ OpenXLSX/sources/XLImage.cpp | 425 +++++++++++++++ OpenXLSX/sources/XLSheet.cpp | 337 ++++++++++++ OpenXLSX/sources/utilities/XLUtilities.hpp | 4 + 15 files changed, 2376 insertions(+) create mode 100644 Examples/Demo11.cpp create mode 100644 OpenXLSX/headers/XLDrawingML.hpp create mode 100644 OpenXLSX/headers/XLImage.hpp create mode 100644 OpenXLSX/sources/XLDrawingML.cpp create mode 100644 OpenXLSX/sources/XLImage.cpp diff --git a/Examples/CMakeLists.txt b/Examples/CMakeLists.txt index 29ad43ad..9c1ccdfd 100644 --- a/Examples/CMakeLists.txt +++ b/Examples/CMakeLists.txt @@ -87,3 +87,9 @@ target_link_libraries(Demo9 PRIVATE OpenXLSX::OpenXLSX) #======================================================================================================================= add_executable(Demo10 Demo10.cpp) target_link_libraries(Demo10 PRIVATE OpenXLSX::OpenXLSX) + +#======================================================================================================================= +# Define Demo11 target +#======================================================================================================================= +add_executable(Demo11 Demo11.cpp) +target_link_libraries(Demo11 PRIVATE OpenXLSX::OpenXLSX) diff --git a/Examples/Demo11.cpp b/Examples/Demo11.cpp new file mode 100644 index 00000000..032b3e22 --- /dev/null +++ b/Examples/Demo11.cpp @@ -0,0 +1,580 @@ +/* + + ____ ____ ___ ____ ____ ____ ___ + 6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M' + 8P Y8 `MM. d' MM 6M' ` `MM. d' +6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d' +MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d' +MM MM MM MM MM MM MM' MM `MMd MM YMMMMb `MMd +MM MM MM MM MMMMMMMM MM MM d'`MM. MM `Mb dMM. +YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. + 8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM. + YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_ + MM + MM + _MM_ + + Copyright (c) 2018, Kenneth Troldal Balslev + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + - Neither the name of the author nor the + names of any contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +// ===== External Includes ===== // +#include +#include + +// ===== OpenXLSX Includes ===== // +#include "OpenXLSX.hpp" + +using namespace OpenXLSX; + + +int main() +{ + std::cout << "OpenXLSX Image Demo" << std::endl; + std::cout << "==================" << std::endl; + + // Create a new workbook + XLDocument doc; + doc.create("./Demo11.xlsx"); + auto wks = doc.workbook().worksheet("Sheet1"); + + // Add some text to the worksheet + wks.cell("A1").value() = "Image Demo"; + wks.cell("A2").value() = "This worksheet demonstrates image functionality"; + wks.cell("A4").value() = "Image 1 (PNG):"; + wks.cell("A6").value() = "Image 2 (JPEG):"; + wks.cell("A8").value() = "Image 3 (BMP):"; + wks.cell("A10").value() = "Image 4 (GIF):"; + + // Demonstrate XLImage class functionality + std::cout << "\n=== XLImage Class Demo ===" << std::endl; + + // Create images from different sources + std::cout << "\n1. Creating images from binary data..." << std::endl; + + // Static const vectors containing actual image file data + // These contain the binary data from the tiny image files in the images directory + + // tiny_png.png - Small PNG image + static const std::vector pngData = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0f, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x83, 0xf5, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, + 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, + 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, + 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, + 0x00, 0x01, 0xa8, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4f, 0x63, 0x7c, 0xff, + 0x64, 0x03, 0x03, 0xcd, 0x00, 0x13, 0x94, 0xa6, 0x0d, 0x00, 0xbb, 0xfd, + 0xf1, 0xfe, 0x7f, 0xa1, 0x5b, 0xa1, 0x02, 0x50, 0x20, 0xc5, 0xb8, 0xba, + 0x88, 0x51, 0x96, 0x81, 0xe1, 0xfb, 0x85, 0x98, 0x6b, 0x67, 0x18, 0xa4, + 0x42, 0x96, 0x48, 0x0a, 0x40, 0x65, 0x40, 0x22, 0xef, 0xd3, 0x8c, 0x1d, + 0xed, 0xa0, 0x7c, 0xce, 0x89, 0x29, 0xc2, 0xfb, 0xa1, 0x6c, 0x06, 0xc5, + 0xe0, 0x17, 0xfd, 0x9e, 0x7f, 0xa0, 0x1c, 0x06, 0x06, 0x16, 0x28, 0xcd, + 0x60, 0xcc, 0x78, 0x22, 0x92, 0x11, 0xca, 0x66, 0x60, 0x38, 0xb2, 0xfc, + 0x5f, 0x68, 0x1f, 0x03, 0xd0, 0x02, 0x11, 0x10, 0xef, 0xfa, 0xb3, 0xf3, + 0x87, 0x24, 0xe1, 0xc6, 0x21, 0x81, 0x17, 0x7c, 0x85, 0x35, 0x7c, 0x0a, + 0xd9, 0x4f, 0x36, 0x18, 0x42, 0xf8, 0x40, 0x9b, 0x24, 0x0a, 0x19, 0x10, + 0x16, 0xe0, 0x08, 0x19, 0x1b, 0x17, 0x46, 0x8d, 0x67, 0x0c, 0x0f, 0x21, + 0x1c, 0xe5, 0x7c, 0xa9, 0xf7, 0xb3, 0x9e, 0x7f, 0x80, 0x70, 0x90, 0x00, + 0xcb, 0xba, 0x99, 0x7c, 0x0c, 0xc1, 0x2f, 0xf2, 0xa1, 0x46, 0x03, 0xc1, + 0xf7, 0xfc, 0x96, 0x4f, 0x0c, 0x6b, 0xf9, 0x4e, 0x41, 0xb9, 0xc4, 0x85, + 0xbb, 0x82, 0xa4, 0x93, 0xdb, 0xfb, 0xf3, 0x87, 0xa0, 0x3c, 0x18, 0x78, + 0xc1, 0x75, 0xf8, 0xf1, 0xb7, 0x48, 0xa4, 0x70, 0x00, 0x01, 0x89, 0x4f, + 0xfd, 0x73, 0xde, 0x99, 0x41, 0x39, 0xb8, 0x4c, 0x3f, 0xb2, 0xe7, 0xff, + 0x0d, 0x63, 0x06, 0x1b, 0x28, 0x8f, 0x81, 0x41, 0x20, 0x4e, 0x8a, 0xa1, + 0xf0, 0x1e, 0xd4, 0x2f, 0x50, 0xf0, 0x9c, 0xe5, 0xbe, 0xec, 0x1f, 0x19, + 0x28, 0x07, 0x3b, 0x80, 0x87, 0xfb, 0xd9, 0xff, 0x16, 0x67, 0xff, 0x43, + 0xd9, 0x40, 0x00, 0x8e, 0x55, 0x28, 0x1b, 0x0c, 0x04, 0x0c, 0xf3, 0x9f, + 0xed, 0x5b, 0xf4, 0x5d, 0x3e, 0x0e, 0xca, 0x47, 0x07, 0xa7, 0xe6, 0xc8, + 0xb4, 0x9d, 0x80, 0x30, 0xff, 0xc4, 0xb5, 0xbc, 0x08, 0x92, 0x00, 0xb1, + 0x70, 0xc4, 0x2a, 0x16, 0x20, 0x10, 0xa7, 0xa8, 0x14, 0xf3, 0xfc, 0x61, + 0x9c, 0x24, 0x94, 0xcf, 0x20, 0xf9, 0x47, 0xf1, 0x31, 0xcb, 0x13, 0xa0, + 0x43, 0xc0, 0x3c, 0xb3, 0x94, 0x27, 0x1b, 0x52, 0x80, 0x34, 0x30, 0x62, + 0xf9, 0xc1, 0x02, 0x20, 0x40, 0x4a, 0x7a, 0xe7, 0x34, 0x48, 0x63, 0xd8, + 0xdd, 0x00, 0x8f, 0x5d, 0x89, 0x6f, 0xb6, 0xb2, 0x5c, 0xcb, 0xb7, 0xc3, + 0xdd, 0x87, 0x05, 0x90, 0x96, 0x9b, 0xec, 0x24, 0x4d, 0xee, 0x3c, 0x3b, + 0x73, 0x1d, 0xca, 0xfb, 0x13, 0x94, 0x0e, 0x4c, 0x21, 0x12, 0x85, 0x08, + 0x0b, 0x58, 0xd6, 0x35, 0x22, 0xd2, 0x3e, 0x10, 0xe0, 0xb3, 0x19, 0x0b, + 0xe0, 0x34, 0x68, 0x95, 0xba, 0x17, 0xf4, 0x0c, 0xca, 0x03, 0xa7, 0x90, + 0x6f, 0xeb, 0x1a, 0x25, 0x02, 0xd6, 0x42, 0x05, 0x18, 0x2c, 0xde, 0x6e, + 0xa8, 0xff, 0x0e, 0x65, 0x43, 0xf3, 0x2a, 0xcd, 0x00, 0x69, 0x21, 0x43, + 0x2a, 0xa0, 0xa5, 0xe9, 0x0c, 0x0c, 0x00, 0xe4, 0x3f, 0x87, 0xe1, 0xb6, + 0x42, 0x8a, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, + 0x42, 0x60, 0x82 + }; + + // tiny_jpeg.jpg - Small JPEG image (actual file data from xxd) + static const std::vector jpegData = { + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, + 0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xe1, 0x00, 0x22, + 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, + 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x03, 0x05, 0x03, 0x03, 0x03, 0x03, 0x03, 0x06, 0x04, + 0x04, 0x03, 0x05, 0x07, 0x06, 0x07, 0x07, 0x07, 0x06, 0x07, 0x07, 0x08, + 0x09, 0x0b, 0x09, 0x08, 0x08, 0x0a, 0x08, 0x07, 0x07, 0x0a, 0x0d, 0x0a, + 0x0a, 0x0b, 0x0c, 0x0c, 0x0c, 0x0c, 0x07, 0x09, 0x0e, 0x0f, 0x0d, 0x0c, + 0x0e, 0x0b, 0x0c, 0x0c, 0x0c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x02, 0x02, + 0x02, 0x03, 0x03, 0x03, 0x06, 0x03, 0x03, 0x06, 0x0c, 0x08, 0x07, 0x08, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x12, 0x00, 0x20, 0x03, + 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, + 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, + 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, + 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, + 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, + 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, + 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, + 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, + 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, + 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, + 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, + 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, + 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, + 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, + 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, + 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, + 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, + 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, + 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, + 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, + 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, + 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfd, + 0x5e, 0xf8, 0xcf, 0xf1, 0x17, 0xfe, 0x15, 0x17, 0xc2, 0x5f, 0x12, 0x78, + 0xa7, 0xec, 0x7f, 0xda, 0x1f, 0xf0, 0x8f, 0xe9, 0xd3, 0xea, 0x1f, 0x65, + 0xf3, 0x7c, 0x9f, 0xb4, 0x79, 0x68, 0x5b, 0x66, 0xfd, 0xad, 0xb7, 0x38, + 0xc6, 0x70, 0x71, 0xe8, 0x6b, 0x26, 0x4f, 0x88, 0xfe, 0x29, 0xd6, 0x3c, + 0x59, 0xe2, 0x0d, 0x3f, 0x43, 0xf0, 0xee, 0x83, 0x79, 0x6d, 0xa0, 0x5c, + 0xa5, 0xab, 0xcd, 0x7b, 0xae, 0x4b, 0x6b, 0x24, 0xce, 0xd0, 0x47, 0x37, + 0x08, 0xb6, 0xb2, 0x00, 0x31, 0x20, 0x1c, 0xbf, 0x51, 0xda, 0xb9, 0xdf, + 0xda, 0x7b, 0x4d, 0xf1, 0xaf, 0x8e, 0xbc, 0x3d, 0xe2, 0x3f, 0x06, 0xe9, + 0x3e, 0x1b, 0x5d, 0x4f, 0x45, 0xf1, 0x76, 0x86, 0x34, 0xdb, 0x6d, 0x4e, + 0x2b, 0xa8, 0x22, 0xfe, 0xc9, 0xba, 0x95, 0xe5, 0x8e, 0x79, 0x2e, 0x84, + 0x92, 0xab, 0xb4, 0x2b, 0x13, 0x42, 0xeb, 0xe4, 0x47, 0x23, 0xe5, 0x25, + 0x04, 0x72, 0x95, 0xa9, 0xa6, 0xfc, 0x0f, 0x8b, 0x5b, 0xf1, 0xe7, 0x8c, + 0x35, 0x1d, 0x5c, 0xeb, 0xd6, 0xd0, 0xea, 0x7a, 0x84, 0x52, 0x5a, 0x7d, + 0x8b, 0x5f, 0xbb, 0xb2, 0x8e, 0x78, 0x85, 0xac, 0x08, 0x49, 0x8e, 0xde, + 0x65, 0x5c, 0xef, 0x57, 0x19, 0x61, 0xb8, 0x81, 0xe9, 0x8a, 0xfc, 0x3f, + 0x56, 0xfe, 0x4f, 0xef, 0xba, 0xff, 0x00, 0x83, 0xf9, 0x9f, 0x53, 0xc4, + 0x58, 0x4a, 0xd4, 0xb0, 0x18, 0x47, 0x82, 0x92, 0xf6, 0xb5, 0x2a, 0x4f, + 0x9f, 0x96, 0x49, 0xb5, 0x4f, 0xd9, 0xa7, 0x1e, 0x6f, 0x76, 0x7c, 0x9e, + 0xfd, 0xd6, 0xb1, 0x4d, 0xbd, 0x2f, 0x6b, 0x15, 0x57, 0xf6, 0x8a, 0xbf, + 0xf1, 0x8c, 0xda, 0x3e, 0x9f, 0xe0, 0xdf, 0x0d, 0xc5, 0xab, 0xeb, 0xba, + 0x86, 0x9c, 0xba, 0xb5, 0xe4, 0x1a, 0x9e, 0xa3, 0xfd, 0x9f, 0x69, 0xa4, + 0xdb, 0x33, 0xbc, 0x6b, 0xe7, 0x4f, 0x1c, 0x53, 0x93, 0x23, 0xc9, 0x1c, + 0x8a, 0x88, 0x91, 0xb0, 0x61, 0x14, 0x8c, 0x59, 0x40, 0x1b, 0xba, 0xff, + 0x00, 0x86, 0x3e, 0x34, 0xd5, 0x3c, 0x61, 0xa5, 0xde, 0xae, 0xb7, 0xe1, + 0xfb, 0x8f, 0x0e, 0xea, 0xda, 0x5d, 0xe3, 0xd9, 0xdc, 0xdb, 0x99, 0x0c, + 0xf6, 0xd3, 0x10, 0x15, 0x96, 0x6b, 0x69, 0xca, 0x27, 0x9d, 0x0b, 0xab, + 0xa9, 0x0d, 0xb1, 0x48, 0x3b, 0x95, 0x95, 0x59, 0x58, 0x0e, 0x23, 0xc4, + 0x7e, 0x00, 0xd5, 0x3e, 0x14, 0x7c, 0x46, 0x9b, 0x5c, 0xf0, 0xcf, 0x86, + 0xae, 0x35, 0xff, 0x00, 0x0f, 0xeb, 0x1a, 0x1d, 0xb6, 0x83, 0x7d, 0xa4, + 0xe9, 0x57, 0x70, 0xd9, 0x5f, 0xd8, 0x0b, 0x66, 0x9d, 0xa0, 0x96, 0xdd, + 0xa6, 0x92, 0x28, 0xca, 0x15, 0xb8, 0x74, 0x71, 0xe6, 0xa3, 0xa9, 0x58, + 0xd9, 0x77, 0x1d, 0xc0, 0x5a, 0xfd, 0x98, 0xfc, 0x11, 0xe2, 0x0f, 0x09, + 0x5b, 0x78, 0x9a, 0xe3, 0x5a, 0x87, 0x5c, 0xd3, 0xb4, 0xfd, 0x5b, 0x52, + 0x59, 0xf4, 0x7d, 0x2b, 0x58, 0xf1, 0x0c, 0xda, 0xe5, 0xf6, 0x99, 0x6c, + 0xb0, 0xc7, 0x11, 0x59, 0x66, 0x92, 0x49, 0x42, 0xb3, 0xc8, 0x8f, 0x2e, + 0xc8, 0xe5, 0x91, 0x54, 0x48, 0x06, 0xec, 0x82, 0x07, 0xd7, 0x63, 0x30, + 0x79, 0x7f, 0xf6, 0x73, 0xab, 0x47, 0x92, 0xe9, 0x41, 0xa7, 0xcd, 0xef, + 0xca, 0x4d, 0xfb, 0xf1, 0x71, 0xf6, 0x8e, 0xdc, 0xad, 0xbb, 0x7e, 0xee, + 0xce, 0x31, 0x4f, 0x9e, 0xef, 0xde, 0xf2, 0xf0, 0xaf, 0x13, 0x1e, 0x48, + 0x57, 0x77, 0x95, 0x92, 0x93, 0x4b, 0x46, 0xf9, 0x55, 0xda, 0x76, 0x5b, + 0xca, 0xfd, 0xad, 0xb5, 0x8f, 0x52, 0xa2, 0x8a, 0x2b, 0xe4, 0x4f, 0x4c, + 0x28, 0xa2, 0x8a, 0x00, 0xff, 0xd9 + }; + + // tiny_bmp.bmp - Small BMP image (actual file data from xxd) + static const std::vector bmpData = { + 0x42, 0x4d, 0x76, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, + 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, 0xb1, 0x22, 0xb0, 0xdb, 0x91, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x3e, 0x89, + 0xef, 0x6f, 0x44, 0xed, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x4c, 0xb1, + 0x22, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0x91, 0xe4, 0xef, 0x5e, + 0xb1, 0x6f, 0x80, 0xd4, 0x91, 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x24, 0x1c, 0xee, + 0xb0, 0xa8, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0x80, 0xdb, 0xef, 0x80, 0xba, 0x4b, 0x91, 0xe4, 0xd0, 0x6f, 0xb1, 0x6f, + 0xb0, 0xe4, 0xb1, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, + 0xef, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0x5e, 0xcb, 0xd0, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, + 0xef, 0x4c, 0xc2, 0xb1, 0xb0, 0xd4, 0x6f, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, + 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, + 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x9b, 0x89, 0xed, 0xb0, 0xe4, 0xef, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xa0, 0xe4, 0xb1, 0x5e, 0xba, 0x91, 0xb0, + 0xdb, 0x91, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x91, 0xc2, 0x22, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc2, 0x48, 0x3f, 0xb0, 0xe4, 0xb9, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x85, 0x67, 0xed, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0x80, 0xdb, 0xb1, + 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0xb0, 0xe4, 0xef, 0x91, 0xe4, 0xef, + 0x6f, 0xb1, 0x6f, 0x80, 0xdb, 0xb1, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, 0xef, 0xcc, 0x65, 0x9d, + 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, + 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0x5e, 0xcb, 0x91, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x80, 0xcb, 0x6f, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, + 0xef, 0xcc, 0x65, 0x9d, 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0x80, + 0xdb, 0xef, 0x5e, 0xb1, 0x4b, 0x4c, 0xba, 0x4b, 0xb0, 0xd4, 0x6f, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, + 0xba, 0x22, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x4c, 0xb1, 0x4b, 0x6f, 0xb1, 0x22, + 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x91, 0xe4, 0xef, 0x4c, 0xb1, 0x6f, 0x6f, 0xb1, 0x22, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, + 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00 + }; + + // tiny_gif.gif - Small GIF image (actual file data from xxd) + static const std::vector gifData = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x1a, 0x00, 0x10, 0x00, 0x70, 0x00, + 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x2c, 0x00, 0x00, + 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x66, 0x00, 0x00, 0x99, 0x00, 0x00, 0xcc, 0x00, 0x00, + 0xff, 0x00, 0x2b, 0x00, 0x00, 0x2b, 0x33, 0x00, 0x2b, 0x66, 0x00, 0x2b, + 0x99, 0x00, 0x2b, 0xcc, 0x00, 0x2b, 0xff, 0x00, 0x55, 0x00, 0x00, 0x55, + 0x33, 0x00, 0x55, 0x66, 0x00, 0x55, 0x99, 0x00, 0x55, 0xcc, 0x00, 0x55, + 0xff, 0x00, 0x80, 0x00, 0x00, 0x80, 0x33, 0x00, 0x80, 0x66, 0x00, 0x80, + 0x99, 0x00, 0x80, 0xcc, 0x00, 0x80, 0xff, 0x00, 0xaa, 0x00, 0x00, 0xaa, + 0x33, 0x00, 0xaa, 0x66, 0x00, 0xaa, 0x99, 0x00, 0xaa, 0xcc, 0x00, 0xaa, + 0xff, 0x00, 0xd5, 0x00, 0x00, 0xd5, 0x33, 0x00, 0xd5, 0x66, 0x00, 0xd5, + 0x99, 0x00, 0xd5, 0xcc, 0x00, 0xd5, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, + 0x33, 0x00, 0xff, 0x66, 0x00, 0xff, 0x99, 0x00, 0xff, 0xcc, 0x00, 0xff, + 0xff, 0x33, 0x00, 0x00, 0x33, 0x00, 0x33, 0x33, 0x00, 0x66, 0x33, 0x00, + 0x99, 0x33, 0x00, 0xcc, 0x33, 0x00, 0xff, 0x33, 0x2b, 0x00, 0x33, 0x2b, + 0x33, 0x33, 0x2b, 0x66, 0x33, 0x2b, 0x99, 0x33, 0x2b, 0xcc, 0x33, 0x2b, + 0xff, 0x33, 0x55, 0x00, 0x33, 0x55, 0x33, 0x33, 0x55, 0x66, 0x33, 0x55, + 0x99, 0x33, 0x55, 0xcc, 0x33, 0x55, 0xff, 0x33, 0x80, 0x00, 0x33, 0x80, + 0x33, 0x33, 0x80, 0x66, 0x33, 0x80, 0x99, 0x33, 0x80, 0xcc, 0x33, 0x80, + 0xff, 0x33, 0xaa, 0x00, 0x33, 0xaa, 0x33, 0x33, 0xaa, 0x66, 0x33, 0xaa, + 0x99, 0x33, 0xaa, 0xcc, 0x33, 0xaa, 0xff, 0x33, 0xd5, 0x00, 0x33, 0xd5, + 0x33, 0x33, 0xd5, 0x66, 0x33, 0xd5, 0x99, 0x33, 0xd5, 0xcc, 0x33, 0xd5, + 0xff, 0x33, 0xff, 0x00, 0x33, 0xff, 0x33, 0x33, 0xff, 0x66, 0x33, 0xff, + 0x99, 0x33, 0xff, 0xcc, 0x33, 0xff, 0xff, 0x66, 0x00, 0x00, 0x66, 0x00, + 0x33, 0x66, 0x00, 0x66, 0x66, 0x00, 0x99, 0x66, 0x00, 0xcc, 0x66, 0x00, + 0xff, 0x66, 0x2b, 0x00, 0x66, 0x2b, 0x33, 0x66, 0x2b, 0x66, 0x66, 0x2b, + 0x99, 0x66, 0x2b, 0xcc, 0x66, 0x2b, 0xff, 0x66, 0x55, 0x00, 0x66, 0x55, + 0x33, 0x66, 0x55, 0x66, 0x66, 0x55, 0x99, 0x66, 0x55, 0xcc, 0x66, 0x55, + 0xff, 0x66, 0x80, 0x00, 0x66, 0x80, 0x33, 0x66, 0x80, 0x66, 0x66, 0x80, + 0x99, 0x66, 0x80, 0xcc, 0x66, 0x80, 0xff, 0x66, 0xaa, 0x00, 0x66, 0xaa, + 0x33, 0x66, 0xaa, 0x66, 0x66, 0xaa, 0x99, 0x66, 0xaa, 0xcc, 0x66, 0xaa, + 0xff, 0x66, 0xd5, 0x00, 0x66, 0xd5, 0x33, 0x66, 0xd5, 0x66, 0x66, 0xd5, + 0x99, 0x66, 0xd5, 0xcc, 0x66, 0xd5, 0xff, 0x66, 0xff, 0x00, 0x66, 0xff, + 0x33, 0x66, 0xff, 0x66, 0x66, 0xff, 0x99, 0x66, 0xff, 0xcc, 0x66, 0xff, + 0xff, 0x99, 0x00, 0x00, 0x99, 0x00, 0x33, 0x99, 0x00, 0x66, 0x99, 0x00, + 0x99, 0x99, 0x00, 0xcc, 0x99, 0x00, 0xff, 0x99, 0x2b, 0x00, 0x99, 0x2b, + 0x33, 0x99, 0x2b, 0x66, 0x99, 0x2b, 0x99, 0x99, 0x2b, 0xcc, 0x99, 0x2b, + 0xff, 0x99, 0x55, 0x00, 0x99, 0x55, 0x33, 0x99, 0x55, 0x66, 0x99, 0x55, + 0x99, 0x99, 0x55, 0xcc, 0x99, 0x55, 0xff, 0x99, 0x80, 0x00, 0x99, 0x80, + 0x33, 0x99, 0x80, 0x66, 0x99, 0x80, 0x99, 0x99, 0x80, 0xcc, 0x99, 0x80, + 0xff, 0x99, 0xaa, 0x00, 0x99, 0xaa, 0x33, 0x99, 0xaa, 0x66, 0x99, 0xaa, + 0x99, 0x99, 0xaa, 0xcc, 0x99, 0xaa, 0xff, 0x99, 0xd5, 0x00, 0x99, 0xd5, + 0x33, 0x99, 0xd5, 0x66, 0x99, 0xd5, 0x99, 0x99, 0xd5, 0xcc, 0x99, 0xd5, + 0xff, 0x99, 0xff, 0x00, 0x99, 0xff, 0x33, 0x99, 0xff, 0x66, 0x99, 0xff, + 0x99, 0x99, 0xff, 0xcc, 0x99, 0xff, 0xff, 0xcc, 0x00, 0x00, 0xcc, 0x00, + 0x33, 0xcc, 0x00, 0x66, 0xcc, 0x00, 0x99, 0xcc, 0x00, 0xcc, 0xcc, 0x00, + 0xff, 0xcc, 0x2b, 0x00, 0xcc, 0x2b, 0x33, 0xcc, 0x2b, 0x66, 0xcc, 0x2b, + 0x99, 0xcc, 0x2b, 0xcc, 0xcc, 0x2b, 0xff, 0xcc, 0x55, 0x00, 0xcc, 0x55, + 0x33, 0xcc, 0x55, 0x66, 0xcc, 0x55, 0x99, 0xcc, 0x55, 0xcc, 0xcc, 0x55, + 0xff, 0xcc, 0x80, 0x00, 0xcc, 0x80, 0x33, 0xcc, 0x80, 0x66, 0xcc, 0x80, + 0x99, 0xcc, 0x80, 0xcc, 0xcc, 0x80, 0xff, 0xcc, 0xaa, 0x00, 0xcc, 0xaa, + 0x33, 0xcc, 0xaa, 0x66, 0xcc, 0xaa, 0x99, 0xcc, 0xaa, 0xcc, 0xcc, 0xaa, + 0xff, 0xcc, 0xd5, 0x00, 0xcc, 0xd5, 0x33, 0xcc, 0xd5, 0x66, 0xcc, 0xd5, + 0x99, 0xcc, 0xd5, 0xcc, 0xcc, 0xd5, 0xff, 0xcc, 0xff, 0x00, 0xcc, 0xff, + 0x33, 0xcc, 0xff, 0x66, 0xcc, 0xff, 0x99, 0xcc, 0xff, 0xcc, 0xcc, 0xff, + 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0x33, 0xff, 0x00, 0x66, 0xff, 0x00, + 0x99, 0xff, 0x00, 0xcc, 0xff, 0x00, 0xff, 0xff, 0x2b, 0x00, 0xff, 0x2b, + 0x33, 0xff, 0x2b, 0x66, 0xff, 0x2b, 0x99, 0xff, 0x2b, 0xcc, 0xff, 0x2b, + 0xff, 0xff, 0x55, 0x00, 0xff, 0x55, 0x33, 0xff, 0x55, 0x66, 0xff, 0x55, + 0x99, 0xff, 0x55, 0xcc, 0xff, 0x55, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, + 0x33, 0xff, 0x80, 0x66, 0xff, 0x80, 0x99, 0xff, 0x80, 0xcc, 0xff, 0x80, + 0xff, 0xff, 0xaa, 0x00, 0xff, 0xaa, 0x33, 0xff, 0xaa, 0x66, 0xff, 0xaa, + 0x99, 0xff, 0xaa, 0xcc, 0xff, 0xaa, 0xff, 0xff, 0xd5, 0x00, 0xff, 0xd5, + 0x33, 0xff, 0xd5, 0x66, 0xff, 0xd5, 0x99, 0xff, 0xd5, 0xcc, 0xff, 0xd5, + 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x33, 0xff, 0xff, 0x66, 0xff, 0xff, + 0x99, 0xff, 0xff, 0xcc, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0x00, 0xe7, 0xe9, + 0x13, 0x48, 0x70, 0xa0, 0xc1, 0x82, 0x08, 0x0f, 0xea, 0xd3, 0x97, 0x8c, + 0x5e, 0x32, 0x86, 0x0e, 0x21, 0x3e, 0x6c, 0x38, 0x31, 0x22, 0xc5, 0x7c, + 0xca, 0x04, 0x26, 0xc3, 0x38, 0x8f, 0x23, 0xc6, 0x7c, 0x0d, 0x41, 0xd2, + 0x03, 0xc9, 0x50, 0xe3, 0x48, 0x88, 0x1d, 0x51, 0x4d, 0xc3, 0x96, 0xea, + 0x5c, 0x3e, 0x7a, 0xf2, 0x88, 0x98, 0x72, 0x47, 0x86, 0x86, 0x0e, 0x9b, + 0x3a, 0xde, 0x0c, 0xcc, 0x38, 0x0f, 0x9a, 0x3c, 0x54, 0xee, 0xf2, 0x41, + 0xcb, 0x37, 0xad, 0x17, 0x48, 0x37, 0x9f, 0xe6, 0xe9, 0x78, 0x36, 0xb2, + 0x69, 0xc8, 0x92, 0xdf, 0xce, 0x41, 0x9b, 0x37, 0x2f, 0x99, 0x3c, 0x55, + 0xfa, 0x62, 0x42, 0xa3, 0xf9, 0xc9, 0xe1, 0x4b, 0x93, 0x0f, 0xe5, 0x51, + 0x6b, 0xc8, 0x54, 0x9f, 0xb2, 0x7c, 0x54, 0x89, 0x80, 0x9a, 0x47, 0x06, + 0xd4, 0xd7, 0xa9, 0x4c, 0xbf, 0x0a, 0x03, 0xf7, 0x75, 0xe3, 0x3c, 0x65, + 0xfa, 0x90, 0xb9, 0x99, 0x59, 0x53, 0xc7, 0x4d, 0x20, 0xf3, 0xe8, 0x31, + 0x9d, 0xf7, 0x8c, 0x1d, 0xac, 0x88, 0xdf, 0x52, 0x65, 0x4b, 0x45, 0x15, + 0xa9, 0xd2, 0x67, 0x81, 0xd1, 0x42, 0xcc, 0x37, 0xf0, 0x2a, 0xe5, 0x8d, + 0x59, 0xa7, 0x29, 0xd3, 0xca, 0x15, 0x62, 0x5c, 0x8a, 0x0c, 0xa3, 0x26, + 0x0b, 0xbc, 0xf1, 0x55, 0xbb, 0x7c, 0x44, 0x3e, 0x29, 0x23, 0xd3, 0xb0, + 0x27, 0xe9, 0x93, 0x54, 0x91, 0x4d, 0x6b, 0x37, 0x35, 0xdf, 0xab, 0x69, + 0xf4, 0xf4, 0x6e, 0x1d, 0xe3, 0xf6, 0xac, 0x6b, 0x86, 0x64, 0x47, 0xd6, + 0x92, 0x96, 0x6a, 0x1a, 0xb8, 0xa1, 0xf3, 0x64, 0xce, 0xf3, 0x62, 0xca, + 0x24, 0xc7, 0x85, 0x81, 0x21, 0x8f, 0x1c, 0x9a, 0x51, 0x30, 0xbd, 0xbb, + 0x68, 0x23, 0x56, 0x1d, 0x38, 0xfa, 0x21, 0xf6, 0x8c, 0x03, 0xa7, 0xba, + 0x11, 0x7e, 0xf9, 0x79, 0x24, 0xf8, 0x9d, 0x5f, 0x4b, 0x86, 0x7f, 0x3d, + 0xf8, 0xac, 0x76, 0x65, 0x01, 0x01, 0x00, 0x3b + }; + + // Create XLImage objects for each image format + XLImage image1(pngData, ImageMimeTypes::PNG); + XLImage image2(jpegData, ImageMimeTypes::JPEG); + XLImage image3(bmpData, ImageMimeTypes::BMP); + XLImage image4(gifData, ImageMimeTypes::GIF); + + // Demonstrate size control + std::cout << "\n2. Demonstrating size control..." << std::endl; + + // Set custom display sizes + image1.setDisplayWidth(50000); // 50,000 EMUs + image1.setDisplayHeight(30000); // 30,000 EMUs + + image2.setDisplayWidth(40000); // 40,000 EMUs + image2.setDisplayHeight(40000); // 40,000 EMUs (square) + + image3.setDisplayWidth(60000); // 60,000 EMUs + image3.setDisplayHeight(20000); // 20,000 EMUs + + image4.setDisplayWidth(35000); // 35,000 EMUs + image4.setDisplayHeight(35000); // 35,000 EMUs (square) + + std::cout << "Image 1 new display size: " << image1.displayWidth() << "x" << image1.displayHeight() << " EMUs" << std::endl; + std::cout << "Image 2 new display size: " << image2.displayWidth() << "x" << image2.displayHeight() << " EMUs" << std::endl; + std::cout << "Image 3 new display size: " << image3.displayWidth() << "x" << image3.displayHeight() << " EMUs" << std::endl; + std::cout << "Image 4 new display size: " << image4.displayWidth() << "x" << image4.displayHeight() << " EMUs" << std::endl; + + // Demonstrate MIME type constants + std::cout << "\n3. MIME type constants:" << std::endl; + std::cout << " PNG: " << ImageMimeTypes::PNG << std::endl; + std::cout << " JPEG: " << ImageMimeTypes::JPEG << std::endl; + std::cout << " BMP: " << ImageMimeTypes::BMP << std::endl; + std::cout << " GIF: " << ImageMimeTypes::GIF << std::endl; + + // Demonstrate worksheet image functionality + std::cout << "\n4. Demonstrating worksheet image functionality..." << std::endl; + + // Add images to the worksheet and assign final image IDs + bool success1 = wks.addImage(image1, "C4"); + bool success2 = wks.addImage(image2, "C6"); + bool success3 = wks.addImage(image3, "C8"); + bool success4 = wks.addImage(image4, "C10"); + + // Add some data to the worksheet (now showing the actual assigned IDs) + wks.cell("B4").value() = "PNG Image - " + image1.id(); + wks.cell("B6").value() = "JPEG Image - " + image2.id(); + wks.cell("B8").value() = "BMP Image - " + image3.id(); + wks.cell("B10").value() = "GIF Image - " + image4.id(); + + std::cout << " Added image1 (PNG) to C4: " << (success1 ? "Success" : "Failed") << std::endl; + std::cout << " Added image2 (JPEG) to C6: " << (success2 ? "Success" : "Failed") << std::endl; + std::cout << " Added image3 (BMP) to C8: " << (success3 ? "Success" : "Failed") << std::endl; + std::cout << " Added image4 (GIF) to C10: " << (success4 ? "Success" : "Failed") << std::endl; + + // Display image information (showing the actual assigned IDs from worksheet) + std::cout << "\nImage information after adding to worksheet:" << std::endl; + std::cout << " Note: Original image objects still show temporary IDs, but the worksheet" << std::endl; + std::cout << " stores copies with proper global IDs (img1, img2, img3, img4)" << std::endl; + + std::cout << "\nImage 1 (PNG):" << std::endl; + std::cout << " MIME Type: " << image1.mimeType() << std::endl; + std::cout << " Extension: " << image1.extension() << std::endl; + std::cout << " ID: " << image1.id() << " (temporary - actual ID in worksheet: img1)" << std::endl; + std::cout << " Dimensions: " << image1.width() << "x" << image1.height() << " pixels" << std::endl; + std::cout << " Display Size: " << image1.displayWidth() << "x" << image1.displayHeight() << " EMUs" << std::endl; + std::cout << " Valid: " << (image1.isValid() ? "Yes" : "No") << std::endl; + std::cout << " Content Type: " << XLContentTypeString(image1.contentType()) << std::endl; + + std::cout << "\nImage 2 (JPEG):" << std::endl; + std::cout << " MIME Type: " << image2.mimeType() << std::endl; + std::cout << " Extension: " << image2.extension() << std::endl; + std::cout << " ID: " << image2.id() << " (temporary - actual ID in worksheet: img2)" << std::endl; + std::cout << " Dimensions: " << image2.width() << "x" << image2.height() << " pixels" << std::endl; + std::cout << " Display Size: " << image2.displayWidth() << "x" << image2.displayHeight() << " EMUs" << std::endl; + std::cout << " Valid: " << (image2.isValid() ? "Yes" : "No") << std::endl; + std::cout << " Content Type: " << XLContentTypeString(image2.contentType()) << std::endl; + + std::cout << "\nImage 3 (BMP):" << std::endl; + std::cout << " MIME Type: " << image3.mimeType() << std::endl; + std::cout << " Extension: " << image3.extension() << std::endl; + std::cout << " ID: " << image3.id() << " (temporary - actual ID in worksheet: img3)" << std::endl; + std::cout << " Dimensions: " << image3.width() << "x" << image3.height() << " pixels" << std::endl; + std::cout << " Display Size: " << image3.displayWidth() << "x" << image3.displayHeight() << " EMUs" << std::endl; + std::cout << " Valid: " << (image3.isValid() ? "Yes" : "No") << std::endl; + std::cout << " Content Type: " << XLContentTypeString(image3.contentType()) << std::endl; + + std::cout << "\nImage 4 (GIF):" << std::endl; + std::cout << " MIME Type: " << image4.mimeType() << std::endl; + std::cout << " Extension: " << image4.extension() << std::endl; + std::cout << " ID: " << image4.id() << " (temporary - actual ID in worksheet: img4)" << std::endl; + std::cout << " Dimensions: " << image4.width() << "x" << image4.height() << " pixels" << std::endl; + std::cout << " Display Size: " << image4.displayWidth() << "x" << image4.displayHeight() << " EMUs" << std::endl; + std::cout << " Valid: " << (image4.isValid() ? "Yes" : "No") << std::endl; + std::cout << " Content Type: " << XLContentTypeString(image4.contentType()) << std::endl; + + // Test image count and status + std::cout << " Total images in worksheet: " << wks.imageCount() << std::endl; + std::cout << " Worksheet has images: " << (wks.hasImages() ? "Yes" : "No") << std::endl; + + // Test addImageFromFile (this will fail since we don't have actual image files) + std::cout << "\n5. Testing addImageFromFile (will fail - no actual files):" << std::endl; + bool fileSuccess = wks.addImageFromFile("nonexistent.png", "D1"); + std::cout << " Added image from file: " << (fileSuccess ? "Success" : "Failed (expected)") << std::endl; + + // Save the workbook + doc.save(); + std::cout << "\nWorkbook saved as 'Demo11.xlsx'" << std::endl; + std::cout << "\nNote: This demo shows the XLImage class and worksheet integration." << std::endl; + std::cout << " Images are stored in memory but not yet embedded in the Excel file." << std::endl; + std::cout << " Next step: implement drawing XML generation and binary storage." << std::endl; + + return 0; +} diff --git a/OpenXLSX/CMakeLists.txt b/OpenXLSX/CMakeLists.txt index 95c56f94..c44285cb 100644 --- a/OpenXLSX/CMakeLists.txt +++ b/OpenXLSX/CMakeLists.txt @@ -111,6 +111,8 @@ set(OPENXLSX_SOURCES ${CMAKE_CURRENT_LIST_DIR}/sources/XLSheet.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLStyles.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLTables.cpp + ${CMAKE_CURRENT_LIST_DIR}/sources/XLImage.cpp + ${CMAKE_CURRENT_LIST_DIR}/sources/XLDrawingML.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLWorkbook.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLXmlData.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLXmlFile.cpp diff --git a/OpenXLSX/OpenXLSX.hpp b/OpenXLSX/OpenXLSX.hpp index 568a90bf..1da0f5e8 100644 --- a/OpenXLSX/OpenXLSX.hpp +++ b/OpenXLSX/OpenXLSX.hpp @@ -59,5 +59,6 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #include "headers/XLSheet.hpp" #include "headers/XLWorkbook.hpp" #include "headers/XLZipArchive.hpp" +#include "sources/utilities/XLUtilities.hpp" #endif // OPENXLSX_OPENXLSX_HPP diff --git a/OpenXLSX/headers/XLContentTypes.hpp b/OpenXLSX/headers/XLContentTypes.hpp index 3319c332..0b40b331 100644 --- a/OpenXLSX/headers/XLContentTypes.hpp +++ b/OpenXLSX/headers/XLContentTypes.hpp @@ -91,9 +91,23 @@ namespace OpenXLSX Comments, Table, VMLDrawing, + ImagePNG, + ImageJPEG, + ImageBMP, + ImageGIF, Unknown }; + /** + * @brief Image MIME type constants + */ + namespace ImageMimeTypes { + static const std::string PNG = "image/png"; + static const std::string JPEG = "image/jpeg"; + static const std::string BMP = "image/bmp"; + static const std::string GIF = "image/gif"; + } + /** * @brief utility function: determine the name of an XLContentType value * @param type the XLContentType to get a name for diff --git a/OpenXLSX/headers/XLDocument.hpp b/OpenXLSX/headers/XLDocument.hpp index 070651c8..7503d06d 100644 --- a/OpenXLSX/headers/XLDocument.hpp +++ b/OpenXLSX/headers/XLDocument.hpp @@ -336,6 +336,12 @@ namespace OpenXLSX */ XLComments sheetComments(uint16_t sheetXmlNo); + /** + * @brief Generate the next globally unique image ID for this document + * @return The next sequential image ID (e.g., "img1", "img2", etc.) + */ + std::string generateNextImageId() const; + /** * @brief fetch the worksheet tables for sheetXmlNo, create the file if it does not exist * @param sheetXmlNo fetch for this sheet # @@ -352,6 +358,38 @@ namespace OpenXLSX */ bool validateSheetName(std::string sheetName, bool throwOnInvalid = false); + /** + * @brief Add an image entry to the archive + * @param imageFilename The filename for the image in the archive + * @param imageData The binary image data + * @param contentType The content type for the image + * @return true if successful, false otherwise + */ + bool addImageEntry(const std::string& imageFilename, const std::vector& imageData, XLContentType contentType); + + /** + * @brief Add a drawing file to the archive + * @param drawingFilename The filename for the drawing in the archive + * @param drawingXml The drawing XML content + * @return true if successful, false otherwise + */ + bool addDrawingFile(const std::string& drawingFilename, const std::string& drawingXml); + + /** + * @brief Get XML data for a drawing file + * @param drawingFilename The filename for the drawing in the archive + * @return Pointer to XLXmlData, or nullptr if not found + */ + XLXmlData* getDrawingXmlData(const std::string& drawingFilename); + + /** + * @brief Add a relationships file to the archive + * @param relsFilename The filename for the relationships file in the archive + * @param relsXml The relationships XML content + * @return true if successful, false otherwise + */ + bool addRelationshipsFile(const std::string& relsFilename, const std::string& relsXml); + /** * @brief * @param command @@ -450,6 +488,8 @@ namespace OpenXLSX XLStyles m_styles {}; /**< A pointer to the document styles object*/ XLWorkbook m_workbook {}; /**< A pointer to the workbook object */ IZipArchive m_archive {}; /**< */ + + mutable uint32_t m_globalImageCounter {0}; /**< Global counter for unique image IDs across all worksheets */ }; diff --git a/OpenXLSX/headers/XLDrawingML.hpp b/OpenXLSX/headers/XLDrawingML.hpp new file mode 100644 index 00000000..d6e9e665 --- /dev/null +++ b/OpenXLSX/headers/XLDrawingML.hpp @@ -0,0 +1,114 @@ +/* + * XLDrawingML.hpp + * + * DrawingML (Drawing Markup Language) support for OpenXLSX + * This provides modern Excel-compatible drawing format instead of legacy VML + */ + +#ifndef OPENXLSX_XLDRAWINGML_HPP +#define OPENXLSX_XLDRAWINGML_HPP + +// ===== External Includes ===== // +#include +#include +#include + +// ===== OpenXLSX Includes ===== // +#include "XLXmlFile.hpp" +#include "XLImage.hpp" + +namespace OpenXLSX +{ + /** + * @brief Class for managing DrawingML drawings in worksheets + */ + class OPENXLSX_EXPORT XLDrawingML : public XLXmlFile + { + public: + // ===== Constructors & Destructors ===== // + XLDrawingML() = default; + XLDrawingML(XLXmlData* xmlData); + ~XLDrawingML() = default; + + // ===== Copy & Move Constructors & Assignment Operators ===== // + XLDrawingML(const XLDrawingML& other) = default; + XLDrawingML(XLDrawingML&& other) noexcept = default; + XLDrawingML& operator=(const XLDrawingML& other) = default; + XLDrawingML& operator=(XLDrawingML&& other) noexcept = default; + + // ===== Public Methods ===== // + /** + * @brief Add an image to the drawing + * @param image The image to add + * @param row The row number (1-based) + * @param column The column number (1-based) + * @param relationshipId The relationship ID for the image + */ + void addImage(const XLImage& image, uint32_t row, uint16_t column, const std::string& relationshipId); + + /** + * @brief Add an image to the drawing with precise positioning + * @param image The image to add + * @param row The row number (1-based) + * @param column The column number (1-based) + * @param relationshipId The relationship ID for the image + * @param rowOffset Offset from the top-left of the cell in EMUs + * @param colOffset Offset from the top-left of the cell in EMUs + */ + void addImageWithOffset(const XLImage& image, uint32_t row, uint16_t column, + const std::string& relationshipId, + int32_t rowOffset = 0, int32_t colOffset = 0); + + /** + * @brief Add an image to the drawing with two-cell anchoring + * @param image The image to add + * @param fromRow Starting row number (1-based) + * @param fromCol Starting column number (1-based) + * @param toRow Ending row number (1-based) + * @param toCol Ending column number (1-based) + * @param relationshipId The relationship ID for the image + * @param fromRowOffset Offset from top-left of starting cell in EMUs + * @param fromColOffset Offset from top-left of starting cell in EMUs + * @param toRowOffset Offset from top-left of ending cell in EMUs + * @param toColOffset Offset from top-left of ending cell in EMUs + */ + void addImageTwoCellAnchor(const XLImage& image, + uint32_t fromRow, uint16_t fromCol, + uint32_t toRow, uint16_t toCol, + const std::string& relationshipId, + int32_t fromRowOffset = 0, int32_t fromColOffset = 0, + int32_t toRowOffset = 0, int32_t toColOffset = 0); + + /** + * @brief Get the number of images in the drawing + * @return Number of images + */ + size_t imageCount() const; + + private: + // ===== Private Methods ===== // + /** + * @brief Convert EMUs to Excel units + * @param emus EMU value + * @return Excel unit value + */ + uint32_t emusToExcelUnits(uint32_t emus) const; + + /** + * @brief Convert points to EMUs + * @param points Point value + * @return EMU value + */ + uint32_t pointsToEmus(double points) const; + + /** + * @brief Calculate cell position in Excel units + * @param row Row number (1-based) + * @param column Column number (1-based) + * @return Position in Excel units + */ + uint32_t calculateCellPosition(uint32_t row, uint16_t column) const; + }; +} + +#endif // OPENXLSX_XLDRAWINGML_HPP diff --git a/OpenXLSX/headers/XLImage.hpp b/OpenXLSX/headers/XLImage.hpp new file mode 100644 index 00000000..7a2ab079 --- /dev/null +++ b/OpenXLSX/headers/XLImage.hpp @@ -0,0 +1,300 @@ +/* + + ____ ____ ___ ____ ____ ____ ___ + 6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M' + 8P Y8 `MM. d' MM 6M' ` `MM. d' +6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d' +MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d' +MM MM MM MM MM MM MM' MM `MMd MM YMMMMb `MMd +MM MM MM MM MMMMMMMM MM MM d'`MM. MM `Mb dMM. +YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. + 8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM. + YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_ + MM + MM + _MM_ + + Copyright (c) 2018, Kenneth Troldal Balslev + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + - Neither the name of the author nor the + names of any contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +#ifndef OPENXLSX_XLIMAGE_HPP +#define OPENXLSX_XLIMAGE_HPP + +// ===== External Includes ===== // +#include // uint8_t, uint16_t, uint32_t +#include // std::string +#include // std::vector + +// ===== OpenXLSX Includes ===== // +#include "OpenXLSX-Exports.hpp" +#include "XLContentTypes.hpp" + +namespace OpenXLSX +{ + /** + * @brief The XLImage class represents an image that can be embedded in a worksheet + */ + class OPENXLSX_EXPORT XLImage + { + public: + /** + * @brief Default constructor + */ + XLImage() = default; + + /** + * @brief Constructor from file path + * @param imagePath Path to the image file + */ + explicit XLImage(const std::string& imagePath); + + /** + * @brief Constructor from binary data + * @param imageData Binary image data + * @param mimeType MIME type of the image + */ + XLImage(const std::vector& imageData, const std::string& mimeType); + + /** + * @brief Copy constructor + * @param other The XLImage to copy + */ + XLImage(const XLImage& other) = default; + + /** + * @brief Move constructor + * @param other The XLImage to move + */ + XLImage(XLImage&& other) noexcept = default; + + /** + * @brief Destructor + */ + ~XLImage() = default; + + /** + * @brief Copy assignment operator + * @param other The XLImage to copy + * @return Reference to this XLImage + */ + XLImage& operator=(const XLImage& other) = default; + + /** + * @brief Move assignment operator + * @param other The XLImage to move + * @return Reference to this XLImage + */ + XLImage& operator=(XLImage&& other) noexcept = default; + + /** + * @brief Load image from file + * @param imagePath Path to the image file + * @return True if successful, false otherwise + */ + bool loadFromFile(const std::string& imagePath); + + /** + * @brief Load image from file + * @param imagePath Path to the image file + * @param imageId The unique ID for this image + * @return True if successful, false otherwise + */ + bool loadFromFile(const std::string& imagePath, const std::string& imageId); + + /** + * @brief Load image from binary data + * @param imageData Binary image data + * @param mimeType MIME type of the image + * @return True if successful, false otherwise + */ + bool loadFromData(const std::vector& imageData, const std::string& mimeType); + + /** + * @brief Load image from binary data + * @param imageData Binary image data + * @param mimeType MIME type of the image + * @param imageId The unique ID for this image + * @return True if successful, false otherwise + */ + bool loadFromData(const std::vector& imageData, const std::string& mimeType, const std::string& imageId); + + /** + * @brief Set the image ID + * @param imageId The unique ID for this image + */ + void setId(const std::string& imageId); + + /** + * @brief Get the image data + * @return Reference to the image data vector + */ + const std::vector& data() const; + + /** + * @brief Get the MIME type + * @return The MIME type string + */ + const std::string& mimeType() const; + + /** + * @brief Get the file extension + * @return The file extension (e.g., ".png", ".jpg") + */ + const std::string& extension() const; + + /** + * @brief Get the unique image ID + * @return The unique image ID + */ + const std::string& id() const; + + /** + * @brief Get the image width in pixels + * @return The width in pixels + */ + uint32_t width() const; + + /** + * @brief Get the image height in pixels + * @return The height in pixels + */ + uint32_t height() const; + + /** + * @brief Set the display width in Excel units (EMUs) + * @param width The display width in EMUs + */ + void setDisplayWidth(uint32_t width); + + /** + * @brief Set the display height in Excel units (EMUs) + * @param height The display height in EMUs + */ + void setDisplayHeight(uint32_t height); + + /** + * @brief Get the display width in Excel units (EMUs) + * @return The display width in EMUs + */ + uint32_t displayWidth() const; + + /** + * @brief Get the display height in Excel units (EMUs) + * @return The display height in EMUs + */ + uint32_t displayHeight() const; + + /** + * @brief Set display size in pixels (converts to EMUs automatically) + * @param widthPixels The display width in pixels + * @param heightPixels The display height in pixels + */ + void setDisplaySizePixels(uint32_t widthPixels, uint32_t heightPixels); + + /** + * @brief Set display size maintaining aspect ratio + * @param maxWidthPixels Maximum width in pixels (0 = no limit) + * @param maxHeightPixels Maximum height in pixels (0 = no limit) + */ + void setDisplaySizeWithAspectRatio(uint32_t maxWidthPixels = 0, uint32_t maxHeightPixels = 0); + + /** + * @brief Convert pixels to EMUs (Excel units) + * @param pixels Number of pixels + * @return Equivalent EMUs + */ + static uint32_t pixelsToEmus(uint32_t pixels); + + /** + * @brief Convert EMUs to pixels + * @param emus Number of EMUs + * @return Equivalent pixels + */ + static uint32_t emusToPixels(uint32_t emus); + + /** + * @brief Check if the image is valid + * @return True if the image has valid data + */ + bool isValid() const; + + /** + * @brief Get the content type for this image + * @return The XLContentType enum value + */ + XLContentType contentType() const; + + private: + std::vector m_imageData; /**< Binary image data */ + std::string m_mimeType; /**< MIME type of the image */ + std::string m_extension; /**< File extension */ + std::string m_id; /**< Unique image ID */ + uint32_t m_width{0}; /**< Image width in pixels */ + uint32_t m_height{0}; /**< Image height in pixels */ + uint32_t m_displayWidth{0}; /**< Display width in EMUs */ + uint32_t m_displayHeight{0}; /**< Display height in EMUs */ + + /** + * @brief Generate a unique image ID + * @return A unique image ID string + */ + static std::string generateId(uint32_t imageNumber); + + /** + * @brief Determine MIME type from file extension + * @param extension File extension + * @return MIME type string + */ + static std::string mimeTypeFromExtension(const std::string& extension); + + /** + * @brief Determine file extension from MIME type + * @param mimeType MIME type string + * @return File extension string + */ + static std::string extensionFromMimeType(const std::string& mimeType); + + /** + * @brief Convert pixels to EMUs (Excel Measurement Units) + * @param pixels Number of pixels + * @return Number of EMUs + */ + static uint32_t pixelsToEMUs(uint32_t pixels); + + /** + * @brief Get image dimensions from binary data + * @param data Binary image data + * @param mimeType MIME type of the image + * @return Pair of (width, height) in pixels + */ + static std::pair getImageDimensions(const std::vector& data, const std::string& mimeType); + }; + +} // namespace OpenXLSX + +#endif // OPENXLSX_XLIMAGE_HPP diff --git a/OpenXLSX/headers/XLSheet.hpp b/OpenXLSX/headers/XLSheet.hpp index 78e61862..4c3f953b 100644 --- a/OpenXLSX/headers/XLSheet.hpp +++ b/OpenXLSX/headers/XLSheet.hpp @@ -75,6 +75,8 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #include "XLRow.hpp" #include "XLStyles.hpp" // XLStyleIndex #include "XLTables.hpp" // XLTables +#include "XLImage.hpp" // XLImage +#include "XLDrawingML.hpp" // XLDrawingML #include "XLXmlFile.hpp" namespace OpenXLSX @@ -1251,6 +1253,12 @@ namespace OpenXLSX */ XLVmlDrawing& vmlDrawing(); + /** + * @brief Get the DrawingML object for this worksheet + * @return Reference to the DrawingML object + */ + XLDrawingML& drawingML(); + /** * @brief fetch a reference to the worksheet comments */ @@ -1261,7 +1269,158 @@ namespace OpenXLSX */ XLTables& tables(); + //---------------------------------------------------------------------------------------------------------------------- + // Image Methods + //---------------------------------------------------------------------------------------------------------------------- + + /** + * @brief Add an image to the worksheet at the specified cell + * @param image The XLImage object to add + * @param cellRef The cell reference where to place the image (e.g., "A1") + * @return True if successful, false otherwise + */ + bool addImage(const XLImage& image, const std::string& cellRef); + + /** + * @brief Add an image to the worksheet at the specified cell + * @param image The XLImage object to add + * @param cellRef The XLCellReference where to place the image + * @return True if successful, false otherwise + */ + bool addImage(const XLImage& image, const XLCellReference& cellRef); + + /** + * @brief Add an image to the worksheet at the specified cell + * @param image The XLImage object to add + * @param row The row number (1-based) + * @param column The column number (1-based) + * @return True if successful, false otherwise + */ + bool addImage(const XLImage& image, uint32_t row, uint16_t column); + + /** + * @brief Add an image to the worksheet with precise positioning + * @param image The XLImage object to add + * @param row The row number (1-based) + * @param column The column number (1-based) + * @param rowOffset Offset from the top-left of the cell in EMUs + * @param colOffset Offset from the top-left of the cell in EMUs + * @return True if successful, false otherwise + */ + bool addImageWithOffset(const XLImage& image, uint32_t row, uint16_t column, + int32_t rowOffset = 0, int32_t colOffset = 0); + + /** + * @brief Add an image to the worksheet with two-cell anchoring + * @param image The XLImage object to add + * @param fromRow Starting row number (1-based) + * @param fromCol Starting column number (1-based) + * @param toRow Ending row number (1-based) + * @param toCol Ending column number (1-based) + * @param fromRowOffset Offset from top-left of starting cell in EMUs + * @param fromColOffset Offset from top-left of starting cell in EMUs + * @param toRowOffset Offset from top-left of ending cell in EMUs + * @param toColOffset Offset from top-left of ending cell in EMUs + * @return True if successful, false otherwise + */ + bool addImageTwoCellAnchor(const XLImage& image, + uint32_t fromRow, uint16_t fromCol, + uint32_t toRow, uint16_t toCol, + int32_t fromRowOffset = 0, int32_t fromColOffset = 0, + int32_t toRowOffset = 0, int32_t toColOffset = 0); + + /** + * @brief Add an image from file to the worksheet at the specified cell + * @param imagePath Path to the image file + * @param cellRef The cell reference where to place the image (e.g., "A1") + * @return True if successful, false otherwise + */ + bool addImageFromFile(const std::string& imagePath, const std::string& cellRef); + + /** + * @brief Add an image from file to the worksheet at the specified cell + * @param imagePath Path to the image file + * @param cellRef The XLCellReference where to place the image + * @return True if successful, false otherwise + */ + bool addImageFromFile(const std::string& imagePath, const XLCellReference& cellRef); + + /** + * @brief Add an image from file to the worksheet at the specified cell + * @param imagePath Path to the image file + * @param row The row number (1-based) + * @param column The column number (1-based) + * @return True if successful, false otherwise + */ + bool addImageFromFile(const std::string& imagePath, uint32_t row, uint16_t column); + + /** + * @brief Get the number of images in the worksheet + * @return Number of images + */ + size_t imageCount() const; + + /** + * @brief Check if the worksheet has any images + * @return True if the worksheet has images, false otherwise + */ + bool hasImages() const; + + /** + * @brief Generate the next available image ID for this worksheet + * @return The next globally unique image ID (e.g., "img1", "img2", etc.) + * @note This uses the document-level counter to ensure uniqueness across all worksheets + */ + std::string generateNextImageId() const; + private: + /** + * @brief Add an image to the DrawingML + * @param image The image to add + * @param row The row number (1-based) + * @param column The column number (1-based) + * @param relationshipId The relationship ID for the image + */ + void addImageToDrawingML(const XLImage& image, uint32_t row, uint16_t column, const std::string& relationshipId); + + /** + * @brief Add an image to the DrawingML with precise positioning + * @param image The image to add + * @param row The row number (1-based) + * @param column The column number (1-based) + * @param relationshipId The relationship ID for the image + * @param rowOffset Offset from the top-left of the cell in EMUs + * @param colOffset Offset from the top-left of the cell in EMUs + */ + void addImageToDrawingMLWithOffset(const XLImage& image, uint32_t row, uint16_t column, + const std::string& relationshipId, + int32_t rowOffset = 0, int32_t colOffset = 0); + + /** + * @brief Add an image to the DrawingML with two-cell anchoring + * @param image The image to add + * @param fromRow Starting row number (1-based) + * @param fromCol Starting column number (1-based) + * @param toRow Ending row number (1-based) + * @param toCol Ending column number (1-based) + * @param relationshipId The relationship ID for the image + * @param fromRowOffset Offset from top-left of starting cell in EMUs + * @param fromColOffset Offset from top-left of starting cell in EMUs + * @param toRowOffset Offset from top-left of ending cell in EMUs + * @param toColOffset Offset from top-left of ending cell in EMUs + */ + void addImageToDrawingMLTwoCellAnchor(const XLImage& image, + uint32_t fromRow, uint16_t fromCol, + uint32_t toRow, uint16_t toCol, + const std::string& relationshipId, + int32_t fromRowOffset = 0, int32_t fromColOffset = 0, + int32_t toRowOffset = 0, int32_t toColOffset = 0); + + /** + * @brief Create the drawing relationships file + * @param drawingFilename The drawing filename + */ + void createDrawingRelationshipsFile(const std::string& drawingFilename); /** * @brief fetch the # number from the xml path xl/worksheets/sheet#.xml @@ -1313,8 +1472,10 @@ namespace OpenXLSX XLRelationships m_relationships{}; /**< class handling the worksheet relationships */ XLMergeCells m_merges{}; /**< class handling the */ XLVmlDrawing m_vmlDrawing{}; /**< class handling the worksheet VML drawing object */ + XLDrawingML m_drawingML{}; /**< class handling the worksheet DrawingML object */ XLComments m_comments{}; /**< class handling the worksheet comments */ XLTables m_tables{}; /**< class handling the worksheet table settings */ + std::vector m_images{}; /**< vector storing images in the worksheet */ const std::vector< std::string_view >& m_nodeOrder = XLWorksheetNodeOrder; // worksheet XML root node required child sequence }; diff --git a/OpenXLSX/sources/XLContentTypes.cpp b/OpenXLSX/sources/XLContentTypes.cpp index f977da7a..417ed86a 100644 --- a/OpenXLSX/sources/XLContentTypes.cpp +++ b/OpenXLSX/sources/XLContentTypes.cpp @@ -115,6 +115,14 @@ namespace { // anonymous namespace for local functions type = XLContentType::Table; else if (typeString == applicationOpenXmlOfficeDocument + ".vmlDrawing") type = XLContentType::VMLDrawing; + else if (typeString == "image/png") + type = XLContentType::ImagePNG; + else if (typeString == "image/jpeg") + type = XLContentType::ImageJPEG; + else if (typeString == "image/bmp") + type = XLContentType::ImageBMP; + else if (typeString == "image/gif") + type = XLContentType::ImageGIF; else type = XLContentType::Unknown; @@ -172,6 +180,14 @@ namespace { // anonymous namespace for local functions typeString = applicationOpenXmlOfficeDocument + ".spreadsheetml.table+xml"; else if (type == XLContentType::VMLDrawing) typeString = applicationOpenXmlOfficeDocument + ".vmlDrawing"; + else if (type == XLContentType::ImagePNG) + typeString = "image/png"; + else if (type == XLContentType::ImageJPEG) + typeString = "image/jpeg"; + else if (type == XLContentType::ImageBMP) + typeString = "image/bmp"; + else if (type == XLContentType::ImageGIF) + typeString = "image/gif"; else throw XLInternalError("Unknown ContentType"); diff --git a/OpenXLSX/sources/XLDocument.cpp b/OpenXLSX/sources/XLDocument.cpp index 648c64f6..b0d23154 100644 --- a/OpenXLSX/sources/XLDocument.cpp +++ b/OpenXLSX/sources/XLDocument.cpp @@ -1201,6 +1201,78 @@ bool XLDocument::validateSheetName(std::string sheetName, bool throwOnInvalid) return valid; } +/** + * @details Add an image entry to the archive + */ +bool XLDocument::addImageEntry(const std::string& imageFilename, const std::vector& imageData, XLContentType contentType) +{ + try { + if (!m_archive.hasEntry(imageFilename)) { + // Convert vector to string for addEntry + std::string imageDataString(imageData.begin(), imageData.end()); + m_archive.addEntry(imageFilename, imageDataString); + m_contentTypes.addOverride("/" + imageFilename, contentType); + } + return true; + } + catch (const std::exception&) { + return false; + } +} + +/** + * @details Add a drawing file to the archive + */ +bool XLDocument::addDrawingFile(const std::string& drawingFilename, const std::string& drawingXml) +{ + try { + if (!m_archive.hasEntry(drawingFilename)) { + m_archive.addEntry(drawingFilename, drawingXml); + m_contentTypes.addOverride("/" + drawingFilename, XLContentType::Drawing); + + // Add XLXmlData object to the data list so getXmlData can find it + m_data.emplace_back(this, drawingFilename, "", XLContentType::Drawing); + } + return true; + } + catch (const std::exception&) { + return false; + } +} + +/** + * @details Get XML data for a drawing file + */ +XLXmlData* XLDocument::getDrawingXmlData(const std::string& drawingFilename) +{ + return getXmlData(drawingFilename, true); // Force loading the XML data +} + +/** + * @details Add a relationships file to the archive + */ +bool XLDocument::addRelationshipsFile(const std::string& relsFilename, const std::string& relsXml) +{ + try { + bool isNewFile = !m_archive.hasEntry(relsFilename); + + // Always update the relationships file (allow overwriting) + m_archive.addEntry(relsFilename, relsXml); + + // Only add content type and XML data if this is a new file + if (isNewFile) { + m_contentTypes.addOverride("/" + relsFilename, XLContentType::Relationships); + + // Add XLXmlData object to the data list so getXmlData can find it + m_data.emplace_back(this, relsFilename, "", XLContentType::Relationships); + } + return true; + } + catch (const std::exception&) { + return false; + } +} + /** * @details return value defaults to true, false only where the XLCommandType implements it */ @@ -1701,4 +1773,12 @@ namespace OpenXLSX return ((result.length() > 1 || result.front() != '/') && path.back() == '/') ? result + "/" : result; } + /** + * @details Generate the next globally unique image ID for this document + */ + std::string XLDocument::generateNextImageId() const + { + return "img" + std::to_string(++m_globalImageCounter); + } + } // namespace OpenXLSX diff --git a/OpenXLSX/sources/XLDrawingML.cpp b/OpenXLSX/sources/XLDrawingML.cpp new file mode 100644 index 00000000..dcbdd982 --- /dev/null +++ b/OpenXLSX/sources/XLDrawingML.cpp @@ -0,0 +1,296 @@ +/* + * XLDrawingML.cpp + * + * DrawingML (Drawing Markup Language) implementation for OpenXLSX + */ + +// ===== External Includes ===== // +#include +#include +#include + +// ===== OpenXLSX Includes ===== // +#include "XLDrawingML.hpp" +#include "XLImage.hpp" +#include "XLXmlData.hpp" +#include "utilities/XLUtilities.hpp" + +namespace OpenXLSX +{ + // ===== Constructors & Destructors ===== // + XLDrawingML::XLDrawingML(XLXmlData* xmlData) : XLXmlFile(xmlData) + { + } + + // ===== Public Methods ===== // + void XLDrawingML::addImage(const XLImage& image, uint32_t row, uint16_t column, const std::string& relationshipId) + { + // Get the root element + XMLNode rootNode = xmlDocument().document_element(); + + // Create a twoCellAnchor element + XMLNode anchor = rootNode.append_child("xdr:twoCellAnchor"); + anchor.append_attribute("editAs").set_value("oneCell"); + + // Create the 'from' element + XMLNode from = anchor.append_child("xdr:from"); + from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column - 1).c_str()); + from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value("0"); + from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row - 1).c_str()); + from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); + + // Create the 'to' element + XMLNode to = anchor.append_child("xdr:to"); + to.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column).c_str()); + to.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value("0"); + to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row).c_str()); + to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); + + // Create the picture element + XMLNode pic = anchor.append_child("xdr:pic"); + + // Create non-visual picture properties + XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); + XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); + cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); // Start from 2 + cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); + + // Add extension list with creation ID for better compatibility + // XMLNode cNvPrExtLst = cNvPr.append_child("a:extLst"); + // XMLNode cNvPrExt = cNvPrExtLst.append_child("a:ext"); + // cNvPrExt.append_attribute("uri").set_value("{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}"); + // XMLNode creationId = cNvPrExt.append_child("a16:creationId"); + // std::string creationIdValue = "{00000000-0008-0000-0000-00000" + std::to_string(imageCount() + 1) + "00000}"; + // creationId.append_attribute("id").set_value(creationIdValue.c_str()); + + XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); + XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); + picLocks.append_attribute("noChangeAspect").set_value("1"); + picLocks.append_attribute("noChangeArrowheads").set_value("1"); + + // Create blip fill + XMLNode blipFill = pic.append_child("xdr:blipFill"); + XMLNode blip = blipFill.append_child("a:blip"); + blip.append_attribute("r:embed").set_value(relationshipId.c_str()); + + // Add extension list for better compatibility + // XMLNode extLst = blip.append_child("a:extLst"); + // XMLNode blipExt = extLst.append_child("a:ext"); + // blipExt.append_attribute("uri").set_value("{28A0092B-C50C-407E-A947-70E740481C1C}"); + // XMLNode useLocalDpi = blipExt.append_child("a14:useLocalDpi"); + // useLocalDpi.append_attribute("val").set_value("0"); + + XMLNode srcRect = blipFill.append_child("a:srcRect"); + XMLNode stretch = blipFill.append_child("a:stretch"); + stretch.append_child("a:fillRect"); + + // Create shape properties + XMLNode spPr = pic.append_child("xdr:spPr"); + spPr.append_attribute("bwMode").set_value("auto"); + + XMLNode xfrm = spPr.append_child("a:xfrm"); + XMLNode off = xfrm.append_child("a:off"); + off.append_attribute("x").set_value("0"); + off.append_attribute("y").set_value("0"); + + XMLNode xfrmExt = xfrm.append_child("a:ext"); + xfrmExt.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); + xfrmExt.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); + + XMLNode prstGeom = spPr.append_child("a:prstGeom"); + prstGeom.append_attribute("prst").set_value("rect"); + prstGeom.append_child("a:avLst"); + + spPr.append_child("a:noFill"); + + // Create client data + anchor.append_child("xdr:clientData"); + } + + size_t XLDrawingML::imageCount() const + { + XMLNode rootNode = xmlDocument().document_element(); + size_t count = 0; + for (XMLNode child : rootNode.children("xdr:twoCellAnchor")) { + count++; + } + return count; + } + + // ===== Private Methods ===== // + uint32_t XLDrawingML::emusToExcelUnits(uint32_t emus) const + { + // Convert EMUs to Excel units (1 EMU = 1/9525 Excel units) + return emus / 9525; + } + + uint32_t XLDrawingML::pointsToEmus(double points) const + { + // Convert points to EMUs (1 point = 12700 EMUs) + return static_cast(points * 12700); + } + + uint32_t XLDrawingML::calculateCellPosition(uint32_t row, uint16_t column) const + { + // Calculate cell position in Excel units + // This is a simplified calculation - Excel uses more complex formulas + return (row - 1) * 15 + (column - 1) * 64; + } + + /** + * @details Add an image to the drawing with precise positioning + */ + void XLDrawingML::addImageWithOffset(const XLImage& image, uint32_t row, uint16_t column, + const std::string& relationshipId, + int32_t rowOffset, int32_t colOffset) + { + // Get the root element + XMLNode rootNode = xmlDocument().document_element(); + + // Create a twoCellAnchor element (but we'll use it as oneCell with offset) + XMLNode anchor = rootNode.append_child("xdr:twoCellAnchor"); + anchor.append_attribute("editAs").set_value("oneCell"); + + // Create the 'from' element with offset + XMLNode from = anchor.append_child("xdr:from"); + from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column - 1).c_str()); + from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(colOffset).c_str()); + from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row - 1).c_str()); + from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(rowOffset).c_str()); + + // Create the 'to' element (same as from for oneCell anchoring) + XMLNode to = anchor.append_child("xdr:to"); + to.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column).c_str()); + to.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value("0"); + to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row).c_str()); + to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); + + // Create the picture element (same as original addImage method) + XMLNode pic = anchor.append_child("xdr:pic"); + + // Create non-visual picture properties + XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); + XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); + cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); + cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); + + XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); + XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); + picLocks.append_attribute("noChangeAspect").set_value("1"); + picLocks.append_attribute("noChangeArrowheads").set_value("1"); + + // Create blip fill + XMLNode blipFill = pic.append_child("xdr:blipFill"); + XMLNode blip = blipFill.append_child("a:blip"); + blip.append_attribute("r:embed").set_value(relationshipId.c_str()); + + XMLNode srcRect = blipFill.append_child("a:srcRect"); + XMLNode stretch = blipFill.append_child("a:stretch"); + stretch.append_child("a:fillRect"); + + // Create shape properties + XMLNode spPr = pic.append_child("xdr:spPr"); + XMLNode xfrm = spPr.append_child("a:xfrm"); + XMLNode off = xfrm.append_child("a:off"); + off.append_attribute("x").set_value("0"); + off.append_attribute("y").set_value("0"); + + XMLNode xfrmExt = xfrm.append_child("a:ext"); + xfrmExt.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); + xfrmExt.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); + + XMLNode prstGeom = spPr.append_child("a:prstGeom"); + prstGeom.append_attribute("prst").set_value("rect"); + prstGeom.append_child("a:avLst"); + + XMLNode solidFill = spPr.append_child("a:solidFill"); + XMLNode srgbClr = solidFill.append_child("a:srgbClr"); + srgbClr.append_attribute("val").set_value("FFFFFF"); + + XMLNode ln = spPr.append_child("a:ln"); + ln.append_attribute("w").set_value("9525"); + XMLNode lnSolidFill = ln.append_child("a:solidFill"); + XMLNode lnSrgbClr = lnSolidFill.append_child("a:srgbClr"); + lnSrgbClr.append_attribute("val").set_value("000000"); + } + + /** + * @details Add an image to the drawing with two-cell anchoring + */ + void XLDrawingML::addImageTwoCellAnchor(const XLImage& image, + uint32_t fromRow, uint16_t fromCol, + uint32_t toRow, uint16_t toCol, + const std::string& relationshipId, + int32_t fromRowOffset, int32_t fromColOffset, + int32_t toRowOffset, int32_t toColOffset) + { + // Get the root element + XMLNode rootNode = xmlDocument().document_element(); + + // Create a twoCellAnchor element + XMLNode anchor = rootNode.append_child("xdr:twoCellAnchor"); + anchor.append_attribute("editAs").set_value("twoCell"); + + // Create the 'from' element + XMLNode from = anchor.append_child("xdr:from"); + from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(fromCol - 1).c_str()); + from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(fromColOffset).c_str()); + from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(fromRow - 1).c_str()); + from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(fromRowOffset).c_str()); + + // Create the 'to' element + XMLNode to = anchor.append_child("xdr:to"); + to.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(toCol).c_str()); + to.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(toColOffset).c_str()); + to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(toRow).c_str()); + to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(toRowOffset).c_str()); + + // Create the picture element (same as original addImage method) + XMLNode pic = anchor.append_child("xdr:pic"); + + // Create non-visual picture properties + XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); + XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); + cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); + cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); + + XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); + XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); + picLocks.append_attribute("noChangeAspect").set_value("1"); + picLocks.append_attribute("noChangeArrowheads").set_value("1"); + + // Create blip fill + XMLNode blipFill = pic.append_child("xdr:blipFill"); + XMLNode blip = blipFill.append_child("a:blip"); + blip.append_attribute("r:embed").set_value(relationshipId.c_str()); + + XMLNode srcRect = blipFill.append_child("a:srcRect"); + XMLNode stretch = blipFill.append_child("a:stretch"); + stretch.append_child("a:fillRect"); + + // Create shape properties + XMLNode spPr = pic.append_child("xdr:spPr"); + XMLNode xfrm = spPr.append_child("a:xfrm"); + XMLNode off = xfrm.append_child("a:off"); + off.append_attribute("x").set_value("0"); + off.append_attribute("y").set_value("0"); + + XMLNode xfrmExt = xfrm.append_child("a:ext"); + xfrmExt.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); + xfrmExt.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); + + XMLNode prstGeom = spPr.append_child("a:prstGeom"); + prstGeom.append_attribute("prst").set_value("rect"); + prstGeom.append_child("a:avLst"); + + XMLNode solidFill = spPr.append_child("a:solidFill"); + XMLNode srgbClr = solidFill.append_child("a:srgbClr"); + srgbClr.append_attribute("val").set_value("FFFFFF"); + + XMLNode ln = spPr.append_child("a:ln"); + ln.append_attribute("w").set_value("9525"); + XMLNode lnSolidFill = ln.append_child("a:solidFill"); + XMLNode lnSrgbClr = lnSolidFill.append_child("a:srgbClr"); + lnSrgbClr.append_attribute("val").set_value("000000"); + } +} diff --git a/OpenXLSX/sources/XLImage.cpp b/OpenXLSX/sources/XLImage.cpp new file mode 100644 index 00000000..425147dc --- /dev/null +++ b/OpenXLSX/sources/XLImage.cpp @@ -0,0 +1,425 @@ +/* + + ____ ____ ___ ____ ____ ____ ___ + 6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M' + 8P Y8 `MM. d' MM 6M' ` `MM. d' +6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d' +MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d' +MM MM MM MM MM MM MM' MM `MMd MM YMMMMb `MMd +MM MM MM MM MMMMMMMM MM MM d'`MM. MM `Mb dMM. +YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. + 8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM. + YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_ + MM + MM + _MM_ + + Copyright (c) 2018, Kenneth Troldal Balslev + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + - Neither the name of the author nor the + names of any contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +// ===== External Includes ===== // +#include // std::ifstream +#include // std::stringstream +#include // std::find + +// ===== OpenXLSX Includes ===== // +#include "XLImage.hpp" +#include "XLException.hpp" + +using namespace OpenXLSX; + +namespace OpenXLSX +{ + // ========== XLImage Member Functions ========== // + + /** + * @details Constructor from file path + */ + XLImage::XLImage(const std::string& imagePath) + { + loadFromFile(imagePath); + } + + /** + * @details Constructor from binary data + */ + XLImage::XLImage(const std::vector& imageData, const std::string& mimeType) + { + loadFromData(imageData, mimeType); + } + + /** + * @details Load image from file (backward compatibility) + */ + bool XLImage::loadFromFile(const std::string& imagePath) + { + // Generate a temporary ID for backward compatibility + std::string tempId = "temp_id"; + return loadFromFile(imagePath, tempId); + } + + /** + * @details Load image from file + */ + bool XLImage::loadFromFile(const std::string& imagePath, const std::string& imageId) + { + std::ifstream file(imagePath, std::ios::binary); + if (!file.is_open()) { + return false; + } + + // Read the entire file into a vector + file.seekg(0, std::ios::end); + size_t fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + m_imageData.resize(fileSize); + file.read(reinterpret_cast(m_imageData.data()), fileSize); + file.close(); + + // Determine file extension and MIME type + size_t dotPos = imagePath.find_last_of('.'); + if (dotPos != std::string::npos) { + m_extension = imagePath.substr(dotPos); + std::transform(m_extension.begin(), m_extension.end(), m_extension.begin(), ::tolower); + } + + m_mimeType = mimeTypeFromExtension(m_extension); + + // Get image dimensions + auto dimensions = getImageDimensions(m_imageData, m_mimeType); + m_width = dimensions.first; + m_height = dimensions.second; + + // Set default display size (same as original size) + m_displayWidth = pixelsToEMUs(m_width); + m_displayHeight = pixelsToEMUs(m_height); + + // Set the provided ID + m_id = imageId; + + return true; + } + + /** + * @details Load image from binary data (backward compatibility) + */ + bool XLImage::loadFromData(const std::vector& imageData, const std::string& mimeType) + { + // Generate a temporary ID for backward compatibility + std::string tempId = "temp_id"; + return loadFromData(imageData, mimeType, tempId); + } + + /** + * @details Load image from binary data + */ + bool XLImage::loadFromData(const std::vector& imageData, const std::string& mimeType, const std::string& imageId) + { + m_imageData = imageData; + m_mimeType = mimeType; + m_extension = extensionFromMimeType(mimeType); + + // Get image dimensions + auto dimensions = getImageDimensions(m_imageData, m_mimeType); + m_width = dimensions.first; + m_height = dimensions.second; + + // Set default display size (same as original size) + m_displayWidth = pixelsToEMUs(m_width); + m_displayHeight = pixelsToEMUs(m_height); + + // Set the provided ID + m_id = imageId; + + return true; + } + + /** + * @details Set the image ID + */ + void XLImage::setId(const std::string& imageId) + { + m_id = imageId; + } + + /** + * @details Get the image data + */ + const std::vector& XLImage::data() const + { + return m_imageData; + } + + /** + * @details Get the MIME type + */ + const std::string& XLImage::mimeType() const + { + return m_mimeType; + } + + /** + * @details Get the file extension + */ + const std::string& XLImage::extension() const + { + return m_extension; + } + + /** + * @details Get the unique image ID + */ + const std::string& XLImage::id() const + { + return m_id; + } + + /** + * @details Get the image width in pixels + */ + uint32_t XLImage::width() const + { + return m_width; + } + + /** + * @details Get the image height in pixels + */ + uint32_t XLImage::height() const + { + return m_height; + } + + /** + * @details Set the display width in Excel units (EMUs) + */ + void XLImage::setDisplayWidth(uint32_t width) + { + m_displayWidth = width; + } + + /** + * @details Set the display height in Excel units (EMUs) + */ + void XLImage::setDisplayHeight(uint32_t height) + { + m_displayHeight = height; + } + + /** + * @details Get the display width in Excel units (EMUs) + */ + uint32_t XLImage::displayWidth() const + { + return m_displayWidth; + } + + /** + * @details Get the display height in Excel units (EMUs) + */ + uint32_t XLImage::displayHeight() const + { + return m_displayHeight; + } + + /** + * @details Check if the image is valid + */ + bool XLImage::isValid() const + { + return !m_imageData.empty() && !m_mimeType.empty() && !m_id.empty(); + } + + /** + * @details Get the content type for this image + */ + XLContentType XLImage::contentType() const + { + if (m_mimeType == ImageMimeTypes::PNG) return XLContentType::ImagePNG; + if (m_mimeType == ImageMimeTypes::JPEG) return XLContentType::ImageJPEG; + if (m_mimeType == ImageMimeTypes::BMP) return XLContentType::ImageBMP; + if (m_mimeType == ImageMimeTypes::GIF) return XLContentType::ImageGIF; + return XLContentType::Unknown; + } + + // ========== XLImage Private Member Functions ========== // + + /** + * @details Generate a unique image ID + */ +std::string XLImage::generateId(uint32_t imageNumber) +{ + return "img" + std::to_string(imageNumber); +} + + /** + * @details Determine MIME type from file extension + */ + std::string XLImage::mimeTypeFromExtension(const std::string& extension) + { + if (extension == ".png") return ImageMimeTypes::PNG; + if (extension == ".jpg" || extension == ".jpeg") return ImageMimeTypes::JPEG; + if (extension == ".bmp") return ImageMimeTypes::BMP; + if (extension == ".gif") return ImageMimeTypes::GIF; + return ""; + } + + /** + * @details Determine file extension from MIME type + */ + std::string XLImage::extensionFromMimeType(const std::string& mimeType) + { + if (mimeType == ImageMimeTypes::PNG) return ".png"; + if (mimeType == ImageMimeTypes::JPEG) return ".jpg"; + if (mimeType == ImageMimeTypes::BMP) return ".bmp"; + if (mimeType == ImageMimeTypes::GIF) return ".gif"; + return ""; + } + + /** + * @details Convert pixels to EMUs (Excel Measurement Units) + * 1 pixel = 9525 EMUs (approximately) + */ + uint32_t XLImage::pixelsToEMUs(uint32_t pixels) + { + return pixels * 9525; + } + + /** + * @details Get image dimensions from binary data + * This is a simplified implementation that works for basic cases + * For production use, consider using a proper image library like libpng, libjpeg, etc. + */ + std::pair XLImage::getImageDimensions(const std::vector& data, const std::string& mimeType) + { + if (data.size() < 8) return {0, 0}; + + // PNG signature check and dimension extraction + if (mimeType == ImageMimeTypes::PNG) { + if (data.size() >= 24 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47) { + // PNG IHDR chunk contains width and height at bytes 16-23 + uint32_t width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19]; + uint32_t height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23]; + return {width, height}; + } + } + // JPEG signature check and dimension extraction + else if (mimeType == ImageMimeTypes::JPEG) { + if (data.size() >= 2 && data[0] == 0xFF && data[1] == 0xD8) { + // For JPEG, we need to find the SOF0 marker (0xFFC0) and extract dimensions + for (size_t i = 2; i < data.size() - 8; ++i) { + if (data[i] == 0xFF && data[i + 1] == 0xC0) { + uint32_t height = (data[i + 5] << 8) | data[i + 6]; + uint32_t width = (data[i + 7] << 8) | data[i + 8]; + return {width, height}; + } + } + } + } + // BMP signature check and dimension extraction + else if (mimeType == ImageMimeTypes::BMP) { + if (data.size() >= 26 && data[0] == 0x42 && data[1] == 0x4D) { + // BMP header contains width and height at bytes 18-25 + uint32_t width = data[18] | (data[19] << 8) | (data[20] << 16) | (data[21] << 24); + uint32_t height = data[22] | (data[23] << 8) | (data[24] << 16) | (data[25] << 24); + return {width, height}; + } + } + // GIF signature check and dimension extraction + else if (mimeType == ImageMimeTypes::GIF) { + if (data.size() >= 10 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46) { + // GIF header contains width and height at bytes 6-9 + uint32_t width = data[6] | (data[7] << 8); + uint32_t height = data[8] | (data[9] << 8); + return {width, height}; + } + } + + // Default fallback - assume square image + return {100, 100}; + } + + /** + * @details Set display size in pixels (converts to EMUs automatically) + */ + void XLImage::setDisplaySizePixels(uint32_t widthPixels, uint32_t heightPixels) + { + m_displayWidth = pixelsToEmus(widthPixels); + m_displayHeight = pixelsToEmus(heightPixels); + } + + /** + * @details Set display size maintaining aspect ratio + */ + void XLImage::setDisplaySizeWithAspectRatio(uint32_t maxWidthPixels, uint32_t maxHeightPixels) + { + if (m_width == 0 || m_height == 0) { + // If we don't have valid dimensions, use the current display size + return; + } + + double aspectRatio = static_cast(m_width) / static_cast(m_height); + + uint32_t targetWidth = m_width; + uint32_t targetHeight = m_height; + + // Apply width constraint if specified + if (maxWidthPixels > 0 && targetWidth > maxWidthPixels) { + targetWidth = maxWidthPixels; + targetHeight = static_cast(targetWidth / aspectRatio); + } + + // Apply height constraint if specified + if (maxHeightPixels > 0 && targetHeight > maxHeightPixels) { + targetHeight = maxHeightPixels; + targetWidth = static_cast(targetHeight * aspectRatio); + } + + m_displayWidth = pixelsToEmus(targetWidth); + m_displayHeight = pixelsToEmus(targetHeight); + } + + /** + * @details Convert pixels to EMUs (Excel units) + * 1 pixel = 9525 EMUs (approximately) + */ + uint32_t XLImage::pixelsToEmus(uint32_t pixels) + { + return pixels * 9525; + } + + /** + * @details Convert EMUs to pixels + */ + uint32_t XLImage::emusToPixels(uint32_t emus) + { + return emus / 9525; + } + +} // namespace OpenXLSX diff --git a/OpenXLSX/sources/XLSheet.cpp b/OpenXLSX/sources/XLSheet.cpp index 7eab5dbb..a2b01907 100644 --- a/OpenXLSX/sources/XLSheet.cpp +++ b/OpenXLSX/sources/XLSheet.cpp @@ -992,6 +992,7 @@ XLWorksheet::XLWorksheet(const XLWorksheet& other) : XLSheetBase(ot m_relationships = other.m_relationships; // invoke XLRelationships copy assignment operator m_merges = other.m_merges; // " XLMergeCells " m_vmlDrawing = other.m_vmlDrawing; // " XLVmlDrawing + m_drawingML = other.m_drawingML; // " XLDrawingML m_comments = other.m_comments; // " XLComments " m_tables = other.m_tables; // " XLTables " } @@ -1004,6 +1005,7 @@ XLWorksheet::XLWorksheet(XLWorksheet&& other) : XLSheetBase< XLWorksheet >(other m_relationships = std::move(other.m_relationships); // invoke XLRelationships move assignment operator m_merges = std::move(other.m_merges); // " XLMergeCells " m_vmlDrawing = std::move(other.m_vmlDrawing); // " XLVmlDrawing + m_drawingML = std::move(other.m_drawingML); // " XLDrawingML m_comments = std::move(other.m_comments); // " XLComments " m_tables = std::move(other.m_tables); // " XLTables " } @@ -1017,6 +1019,7 @@ XLWorksheet& XLWorksheet::operator=(const XLWorksheet& other) m_relationships = other.m_relationships; m_merges = other.m_merges; m_vmlDrawing = other.m_vmlDrawing; + m_drawingML = other.m_drawingML; m_comments = other.m_comments; m_tables = other.m_tables; return *this; @@ -1031,6 +1034,7 @@ XLWorksheet& XLWorksheet::operator=(XLWorksheet&& other) m_relationships = std::move(other.m_relationships); m_merges = std::move(other.m_merges); m_vmlDrawing = std::move(other.m_vmlDrawing); + m_drawingML = std::move(other.m_drawingML); m_comments = std::move(other.m_comments); m_tables = std::move(other.m_tables); return *this; @@ -1685,6 +1689,73 @@ XLVmlDrawing& XLWorksheet::vmlDrawing() return m_vmlDrawing; } +/** + * @details Get the DrawingML object for this worksheet + */ +XLDrawingML& XLWorksheet::drawingML() +{ + if (!m_drawingML.valid()) { + // ===== Append xdr namespace attribute to worksheet if not present + XMLNode docElement = xmlDocument().document_element(); + XMLAttribute xdrNamespace = appendAndGetAttribute(docElement, "xmlns:xdr", ""); + xdrNamespace = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"; + + std::ignore = relationships(); // create sheet relationships if not existing + + // ===== Create DrawingML XML file + uint16_t sheetXmlNo = sheetXmlNumber(); + std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; + + // Create the DrawingML XML content + std::string drawingXml = "\n" + "\n" + ""; + + // Add to archive using public method + if (!parentDoc().addDrawingFile(drawingFilename, drawingXml)) { + throw XLException("XLWorksheet::drawingML(): could not create drawing file"); + } + + // Create DrawingML object + XLXmlData* xmlData = parentDoc().getDrawingXmlData(drawingFilename); + if (!xmlData) { + throw XLException("XLWorksheet::drawingML(): could not get XML data for drawing"); + } + m_drawingML = XLDrawingML(xmlData); + + // Ensure the XML document is loaded + if (!m_drawingML.valid()) { + throw XLException("XLWorksheet::drawingML(): could not initialize DrawingML object"); + } + + // Add relationship + std::string drawingRelativePath = getPathARelativeToPathB(drawingFilename, getXmlPath()); + XLRelationshipItem drawingRelationship; + if (!m_relationships.targetExists(drawingRelativePath)) + drawingRelationship = m_relationships.addRelationship(XLRelationshipType::Drawing, drawingRelativePath); + else + drawingRelationship = m_relationships.relationshipByTarget(drawingRelativePath); + + if (drawingRelationship.empty()) + throw XLException("XLWorksheet::drawingML(): could not add determine sheet relationship for Drawing"); + + // Add element to worksheet + XMLNode drawing = appendAndGetNode(docElement, "drawing", m_nodeOrder); + if (drawing.empty()) + throw XLException("XLWorksheet::drawingML(): could not add element to worksheet XML"); + appendAndSetAttribute(drawing, "r:id", drawingRelationship.id()); + + // Create drawing relationships file + createDrawingRelationshipsFile(drawingFilename); + } + + return m_drawingML; +} + /** * @details fetches XLComments for the sheet - creates & assigns the class if empty */ @@ -1734,6 +1805,272 @@ XLTables& XLWorksheet::tables() return m_tables; } +//---------------------------------------------------------------------------------------------------------------------- +// Image Methods +//---------------------------------------------------------------------------------------------------------------------- + +/** + * @details Add an image to the worksheet at the specified cell + */ +bool XLWorksheet::addImage(const XLImage& image, const std::string& cellRef) +{ + return addImage(image, XLCellReference(cellRef)); +} + +/** + * @details Add an image to the worksheet at the specified cell + */ +bool XLWorksheet::addImage(const XLImage& image, const XLCellReference& cellRef) +{ + return addImage(image, cellRef.row(), cellRef.column()); +} + +/** + * @details Add an image to the worksheet at the specified cell + */ +bool XLWorksheet::addImage(const XLImage& image, uint32_t row, uint16_t column) +{ + if (!image.isValid()) { + return false; + } + + try { + // Create a copy of the image and set its ID + XLImage imageWithId = image; + imageWithId.setId(generateNextImageId()); + + // Store the image in the vector + m_images.push_back(imageWithId); + + // Get the parent document + XLDocument& doc = parentDoc(); + + // Create the image file path in the archive + std::string imageFilename = "xl/media/image_" + imageWithId.id() + imageWithId.extension(); + + // Store the image binary data in the archive + if (!doc.addImageEntry(imageFilename, imageWithId.data(), imageWithId.contentType())) { + return false; + } + + // Ensure we have a DrawingML drawing + std::ignore = drawingML(); + + // Add image to DrawingML + std::string relationshipId = "rId" + std::to_string(m_images.size()); + addImageToDrawingML(imageWithId, row, column, relationshipId); + + return true; + } + catch (const std::exception&) { + return false; + } +} + +/** + * @details Add an image from file to the worksheet at the specified cell + */ +bool XLWorksheet::addImageFromFile(const std::string& imagePath, const std::string& cellRef) +{ + return addImageFromFile(imagePath, XLCellReference(cellRef)); +} + +/** + * @details Add an image from file to the worksheet at the specified cell + */ +bool XLWorksheet::addImageFromFile(const std::string& imagePath, const XLCellReference& cellRef) +{ + return addImageFromFile(imagePath, cellRef.row(), cellRef.column()); +} + +/** + * @details Add an image from file to the worksheet at the specified cell + */ +bool XLWorksheet::addImageFromFile(const std::string& imagePath, uint32_t row, uint16_t column) +{ + XLImage image; + std::string imageId = generateNextImageId(); + if (!image.loadFromFile(imagePath, imageId)) { + return false; + } + + return addImage(image, row, column); +} + +/** + * @details Add an image to the worksheet with precise positioning + */ +bool XLWorksheet::addImageWithOffset(const XLImage& image, uint32_t row, uint16_t column, + int32_t rowOffset, int32_t colOffset) +{ + // Create a copy of the image and set its ID + XLImage imageWithId = image; + imageWithId.setId(generateNextImageId()); + + // Store the image + m_images.push_back(imageWithId); + + // Add image entry to document + std::string imageFilename = "xl/media/image_" + imageWithId.id() + imageWithId.extension(); + if (!parentDoc().addImageEntry(imageFilename, imageWithId.data(), imageWithId.contentType())) { + throw XLException("XLWorksheet::addImageWithOffset(): could not add image entry to document"); + } + + // Get or create DrawingML + XLDrawingML& drawing = drawingML(); + + // Generate relationship ID + std::string relationshipId = "rId" + std::to_string(m_images.size()); + + // Add image to DrawingML with offset + addImageToDrawingMLWithOffset(imageWithId, row, column, relationshipId, rowOffset, colOffset); + + return true; +} + +/** + * @details Add an image to the worksheet with two-cell anchoring + */ +bool XLWorksheet::addImageTwoCellAnchor(const XLImage& image, + uint32_t fromRow, uint16_t fromCol, + uint32_t toRow, uint16_t toCol, + int32_t fromRowOffset, int32_t fromColOffset, + int32_t toRowOffset, int32_t toColOffset) +{ + // Create a copy of the image and set its ID + XLImage imageWithId = image; + imageWithId.setId(generateNextImageId()); + + // Store the image + m_images.push_back(imageWithId); + + // Add image entry to document + std::string imageFilename = "xl/media/image_" + imageWithId.id() + imageWithId.extension(); + if (!parentDoc().addImageEntry(imageFilename, imageWithId.data(), imageWithId.contentType())) { + throw XLException("XLWorksheet::addImageTwoCellAnchor(): could not add image entry to document"); + } + + // Get or create DrawingML + XLDrawingML& drawing = drawingML(); + + // Generate relationship ID + std::string relationshipId = "rId" + std::to_string(m_images.size()); + + // Add image to DrawingML with two-cell anchoring + addImageToDrawingMLTwoCellAnchor(imageWithId, fromRow, fromCol, toRow, toCol, relationshipId, + fromRowOffset, fromColOffset, toRowOffset, toColOffset); + + return true; +} + +/** + * @details Get the number of images in the worksheet + */ +size_t XLWorksheet::imageCount() const +{ + return m_images.size(); +} + +/** + * @details Check if the worksheet has any images + */ +bool XLWorksheet::hasImages() const +{ + return !m_images.empty(); +} + +/** + * @details Generate the next available image ID for this worksheet + */ +std::string XLWorksheet::generateNextImageId() const +{ + return parentDoc().generateNextImageId(); +} + +/** + * @details Add an image to the DrawingML + */ +void XLWorksheet::addImageToDrawingML(const XLImage& image, uint32_t row, uint16_t column, const std::string& relationshipId) +{ + // Add image to DrawingML + m_drawingML.addImage(image, row, column, relationshipId); + + // Update the drawing relationships file to include all current images + uint16_t sheetXmlNo = sheetXmlNumber(); + std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; + createDrawingRelationshipsFile(drawingFilename); +} + +/** + * @details Create the drawing relationships file + */ +void XLWorksheet::createDrawingRelationshipsFile(const std::string& drawingFilename) +{ + // Create the relationships filename + std::string drawingRelsFilename = drawingFilename.substr(0, drawingFilename.find_last_of('/') + 1) + + "_rels/" + + drawingFilename.substr(drawingFilename.find_last_of('/') + 1) + ".rels"; + + // Create the relationships XML content + std::string relsXml = "\n" + "\n"; + + // Add relationships for each image + int relationshipId = 1; + for (const auto& image : m_images) { + std::string imageFilename = "xl/media/image_" + image.id() + image.extension(); + std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); + + relsXml += "\n"; + relationshipId++; + } + + relsXml += ""; + + // Add the relationships file to the archive + if (!parentDoc().addRelationshipsFile(drawingRelsFilename, relsXml)) { + throw XLException("XLWorksheet::createDrawingRelationshipsFile(): could not create drawing relationships file"); + } +} + +/** + * @details Add an image to the DrawingML with precise positioning + */ +void XLWorksheet::addImageToDrawingMLWithOffset(const XLImage& image, uint32_t row, uint16_t column, + const std::string& relationshipId, + int32_t rowOffset, int32_t colOffset) +{ + // Add image to DrawingML + m_drawingML.addImageWithOffset(image, row, column, relationshipId, rowOffset, colOffset); + + // Update the drawing relationships file to include all current images + uint16_t sheetXmlNo = sheetXmlNumber(); + std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; + createDrawingRelationshipsFile(drawingFilename); +} + +/** + * @details Add an image to the DrawingML with two-cell anchoring + */ +void XLWorksheet::addImageToDrawingMLTwoCellAnchor(const XLImage& image, + uint32_t fromRow, uint16_t fromCol, + uint32_t toRow, uint16_t toCol, + const std::string& relationshipId, + int32_t fromRowOffset, int32_t fromColOffset, + int32_t toRowOffset, int32_t toColOffset) +{ + // Add image to DrawingML + m_drawingML.addImageTwoCellAnchor(image, fromRow, fromCol, toRow, toCol, relationshipId, + fromRowOffset, fromColOffset, toRowOffset, toColOffset); + + // Update the drawing relationships file to include all current images + uint16_t sheetXmlNo = sheetXmlNumber(); + std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; + createDrawingRelationshipsFile(drawingFilename); +} + /** * @details perform a pattern matching on getXmlPath for (regex) .*xl/worksheets/sheet([0-9]*)\.xml$ and extract the numeric part \1 */ diff --git a/OpenXLSX/sources/utilities/XLUtilities.hpp b/OpenXLSX/sources/utilities/XLUtilities.hpp index 8c4a134d..7a54c821 100644 --- a/OpenXLSX/sources/utilities/XLUtilities.hpp +++ b/OpenXLSX/sources/utilities/XLUtilities.hpp @@ -102,6 +102,10 @@ namespace OpenXLSX case XLContentType::VMLDrawing: return "VMLDrawing"; case XLContentType::Comments: return "Comments"; case XLContentType::Table: return "Table"; + case XLContentType::ImagePNG: return "ImagePNG"; + case XLContentType::ImageJPEG: return "ImageJPEG"; + case XLContentType::ImageBMP: return "ImageBMP"; + case XLContentType::ImageGIF: return "ImageGIF"; case XLContentType::Unknown: return "Unknown"; } return "invalid"; From 68b2445c896cf79e86e91f9ada901397bd41f437 Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Wed, 1 Oct 2025 21:15:47 -0700 Subject: [PATCH 03/13] Embedded Image Support -- phase II oneCellAnchor --- Examples/Demo11.cpp | 40 +++++++++++++++++++++++++------- OpenXLSX/sources/XLDrawingML.cpp | 20 ++++++++-------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/Examples/Demo11.cpp b/Examples/Demo11.cpp index 032b3e22..ee923cac 100644 --- a/Examples/Demo11.cpp +++ b/Examples/Demo11.cpp @@ -474,18 +474,22 @@ int main() // Demonstrate size control std::cout << "\n2. Demonstrating size control..." << std::endl; - // Set custom display sizes - image1.setDisplayWidth(50000); // 50,000 EMUs - image1.setDisplayHeight(30000); // 30,000 EMUs + // Set custom display sizes (convert pixels to EMUs: 1 pixel = 9525 EMUs) + // Note: EMU-to-pixel ratio may vary based on screen resolution. + // Standard Excel cell height = 190500 EMUs (15 points × 12700 EMUs/point) + // Cell vertical spacing = 190500 × (26/25) EMUs (25 pixels white + 1 pixel divider) + // This allows sizing images to exact Excel cell heights: 1 cell = 190500 EMUs + image1.setDisplayWidth(50 * 9525); // 50 pixels = 476,250 EMUs + image1.setDisplayHeight(30 * 9525); // 30 pixels = 285,750 EMUs - image2.setDisplayWidth(40000); // 40,000 EMUs - image2.setDisplayHeight(40000); // 40,000 EMUs (square) + image2.setDisplayWidth(40 * 9525); // 40 pixels = 381,000 EMUs + image2.setDisplayHeight(40 * 9525); // 40 pixels = 381,000 EMUs (square) - image3.setDisplayWidth(60000); // 60,000 EMUs - image3.setDisplayHeight(20000); // 20,000 EMUs + image3.setDisplayWidth(60 * 9525); // 60 pixels = 571,500 EMUs + image3.setDisplayHeight(20 * 9525); // 20 pixels = 190,500 EMUs - image4.setDisplayWidth(35000); // 35,000 EMUs - image4.setDisplayHeight(35000); // 35,000 EMUs (square) + image4.setDisplayWidth(35 * 9525); // 35 pixels = 333,375 EMUs + image4.setDisplayHeight(35 * 9525); // 35 pixels = 333,375 EMUs (square) std::cout << "Image 1 new display size: " << image1.displayWidth() << "x" << image1.displayHeight() << " EMUs" << std::endl; std::cout << "Image 2 new display size: " << image2.displayWidth() << "x" << image2.displayHeight() << " EMUs" << std::endl; @@ -569,6 +573,24 @@ int main() bool fileSuccess = wks.addImageFromFile("nonexistent.png", "D1"); std::cout << " Added image from file: " << (fileSuccess ? "Success" : "Failed (expected)") << std::endl; + // Test single-cell image with specific pixel dimensions + std::cout << "\n6. Testing single-cell image with specific dimensions:" << std::endl; + + // Create a new image for single-cell test + XLImage singleCellImage(pngData, ImageMimeTypes::PNG); + + // Set specific pixel dimensions (200x100 pixels) + // Convert pixels to EMUs: 1 pixel = 9525 EMUs (standard) + // Note: For cell-based sizing, use 190500 EMUs per Excel cell height + uint32_t widthEMUs = 200 * 9525; // 200 pixels = 1,905,000 EMUs + uint32_t heightEMUs = 100 * 9525; // 100 pixels = 952,500 EMUs + singleCellImage.setDisplayWidth(widthEMUs); + singleCellImage.setDisplayHeight(heightEMUs); + + // Add to cell E12 (single cell, not spanning) + bool singleCellSuccess = wks.addImage(singleCellImage, "E12"); + std::cout << " Added single-cell image (200x100 pixels) to E12: " << (singleCellSuccess ? "Success" : "Failed") << std::endl; + // Save the workbook doc.save(); std::cout << "\nWorkbook saved as 'Demo11.xlsx'" << std::endl; diff --git a/OpenXLSX/sources/XLDrawingML.cpp b/OpenXLSX/sources/XLDrawingML.cpp index dcbdd982..f5a414e5 100644 --- a/OpenXLSX/sources/XLDrawingML.cpp +++ b/OpenXLSX/sources/XLDrawingML.cpp @@ -28,9 +28,8 @@ namespace OpenXLSX // Get the root element XMLNode rootNode = xmlDocument().document_element(); - // Create a twoCellAnchor element - XMLNode anchor = rootNode.append_child("xdr:twoCellAnchor"); - anchor.append_attribute("editAs").set_value("oneCell"); + // Create a oneCellAnchor element for single-cell images + XMLNode anchor = rootNode.append_child("xdr:oneCellAnchor"); // Create the 'from' element XMLNode from = anchor.append_child("xdr:from"); @@ -39,12 +38,10 @@ namespace OpenXLSX from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row - 1).c_str()); from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); - // Create the 'to' element - XMLNode to = anchor.append_child("xdr:to"); - to.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column).c_str()); - to.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value("0"); - to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row).c_str()); - to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); + // Create the 'ext' element - this is crucial for oneCellAnchor! + XMLNode ext = anchor.append_child("xdr:ext"); + ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); + ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); // Create the picture element XMLNode pic = anchor.append_child("xdr:pic"); @@ -103,7 +100,7 @@ namespace OpenXLSX spPr.append_child("a:noFill"); - // Create client data + // Create client data - simple empty element as per Google example anchor.append_child("xdr:clientData"); } @@ -111,6 +108,9 @@ namespace OpenXLSX { XMLNode rootNode = xmlDocument().document_element(); size_t count = 0; + for (XMLNode child : rootNode.children("xdr:oneCellAnchor")) { + count++; + } for (XMLNode child : rootNode.children("xdr:twoCellAnchor")) { count++; } From 722b2d288fd731e77645d2f6de3858befcdd995f Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Wed, 1 Oct 2025 23:20:11 -0700 Subject: [PATCH 04/13] Embedded Image Support -- phase III Preserve image aspect ratio --- Examples/Demo11.cpp | 52 ++++++-------- OpenXLSX/headers/XLImage.hpp | 18 +++-- OpenXLSX/sources/XLDrawingML.cpp | 21 +++++- OpenXLSX/sources/XLImage.cpp | 116 +++++++++++++++++++++---------- 4 files changed, 133 insertions(+), 74 deletions(-) diff --git a/Examples/Demo11.cpp b/Examples/Demo11.cpp index ee923cac..5e82014f 100644 --- a/Examples/Demo11.cpp +++ b/Examples/Demo11.cpp @@ -474,22 +474,18 @@ int main() // Demonstrate size control std::cout << "\n2. Demonstrating size control..." << std::endl; - // Set custom display sizes (convert pixels to EMUs: 1 pixel = 9525 EMUs) + // Set custom display sizes using aspect ratio preservation // Note: EMU-to-pixel ratio may vary based on screen resolution. // Standard Excel cell height = 190500 EMUs (15 points × 12700 EMUs/point) - // Cell vertical spacing = 190500 × (26/25) EMUs (25 pixels white + 1 pixel divider) - // This allows sizing images to exact Excel cell heights: 1 cell = 190500 EMUs - image1.setDisplayWidth(50 * 9525); // 50 pixels = 476,250 EMUs - image1.setDisplayHeight(30 * 9525); // 30 pixels = 285,750 EMUs + // Cell vertical spacing = 200000 ~= × (26/25) EMUs (25 pixels white + 1 pixel divider) + // This allows sizing images to exact Excel cell heights: 1 cell ~= 200000 EMUs + constexpr uint32_t EXCEL_CELL_Y_SPACING_EMUS = 200000; - image2.setDisplayWidth(40 * 9525); // 40 pixels = 381,000 EMUs - image2.setDisplayHeight(40 * 9525); // 40 pixels = 381,000 EMUs (square) - - image3.setDisplayWidth(60 * 9525); // 60 pixels = 571,500 EMUs - image3.setDisplayHeight(20 * 9525); // 20 pixels = 190,500 EMUs - - image4.setDisplayWidth(35 * 9525); // 35 pixels = 333,375 EMUs - image4.setDisplayHeight(35 * 9525); // 35 pixels = 333,375 EMUs (square) + // Set maximum dimensions and let the method preserve aspect ratio + image1.setDisplaySizePixelsWithAspectRatio(50, 30); // Max 50×30 pixels, preserve aspect ratio + image2.setDisplaySizePixelsWithAspectRatio(40, 40); // Max 40×40 pixels, preserve aspect ratio + image3.setDisplaySizePixelsWithAspectRatio(60, 20); // Max 60×20 pixels, preserve aspect ratio + image4.setDisplaySizePixelsWithAspectRatio(70,70); // Max 35×35 pixels, preserve aspect ratio std::cout << "Image 1 new display size: " << image1.displayWidth() << "x" << image1.displayHeight() << " EMUs" << std::endl; std::cout << "Image 2 new display size: " << image2.displayWidth() << "x" << image2.displayHeight() << " EMUs" << std::endl; @@ -532,7 +528,7 @@ int main() std::cout << " MIME Type: " << image1.mimeType() << std::endl; std::cout << " Extension: " << image1.extension() << std::endl; std::cout << " ID: " << image1.id() << " (temporary - actual ID in worksheet: img1)" << std::endl; - std::cout << " Dimensions: " << image1.width() << "x" << image1.height() << " pixels" << std::endl; + std::cout << " Dimensions: " << image1.widthPixels() << "x" << image1.heightPixels() << " pixels" << std::endl; std::cout << " Display Size: " << image1.displayWidth() << "x" << image1.displayHeight() << " EMUs" << std::endl; std::cout << " Valid: " << (image1.isValid() ? "Yes" : "No") << std::endl; std::cout << " Content Type: " << XLContentTypeString(image1.contentType()) << std::endl; @@ -541,7 +537,7 @@ int main() std::cout << " MIME Type: " << image2.mimeType() << std::endl; std::cout << " Extension: " << image2.extension() << std::endl; std::cout << " ID: " << image2.id() << " (temporary - actual ID in worksheet: img2)" << std::endl; - std::cout << " Dimensions: " << image2.width() << "x" << image2.height() << " pixels" << std::endl; + std::cout << " Dimensions: " << image2.widthPixels() << "x" << image2.heightPixels() << " pixels" << std::endl; std::cout << " Display Size: " << image2.displayWidth() << "x" << image2.displayHeight() << " EMUs" << std::endl; std::cout << " Valid: " << (image2.isValid() ? "Yes" : "No") << std::endl; std::cout << " Content Type: " << XLContentTypeString(image2.contentType()) << std::endl; @@ -550,7 +546,7 @@ int main() std::cout << " MIME Type: " << image3.mimeType() << std::endl; std::cout << " Extension: " << image3.extension() << std::endl; std::cout << " ID: " << image3.id() << " (temporary - actual ID in worksheet: img3)" << std::endl; - std::cout << " Dimensions: " << image3.width() << "x" << image3.height() << " pixels" << std::endl; + std::cout << " Dimensions: " << image3.widthPixels() << "x" << image3.heightPixels() << " pixels" << std::endl; std::cout << " Display Size: " << image3.displayWidth() << "x" << image3.displayHeight() << " EMUs" << std::endl; std::cout << " Valid: " << (image3.isValid() ? "Yes" : "No") << std::endl; std::cout << " Content Type: " << XLContentTypeString(image3.contentType()) << std::endl; @@ -559,7 +555,7 @@ int main() std::cout << " MIME Type: " << image4.mimeType() << std::endl; std::cout << " Extension: " << image4.extension() << std::endl; std::cout << " ID: " << image4.id() << " (temporary - actual ID in worksheet: img4)" << std::endl; - std::cout << " Dimensions: " << image4.width() << "x" << image4.height() << " pixels" << std::endl; + std::cout << " Dimensions: " << image4.widthPixels() << "x" << image4.heightPixels() << " pixels" << std::endl; std::cout << " Display Size: " << image4.displayWidth() << "x" << image4.displayHeight() << " EMUs" << std::endl; std::cout << " Valid: " << (image4.isValid() ? "Yes" : "No") << std::endl; std::cout << " Content Type: " << XLContentTypeString(image4.contentType()) << std::endl; @@ -574,29 +570,25 @@ int main() std::cout << " Added image from file: " << (fileSuccess ? "Success" : "Failed (expected)") << std::endl; // Test single-cell image with specific pixel dimensions - std::cout << "\n6. Testing single-cell image with specific dimensions:" << std::endl; + std::cout << "\n6. Testing single-cell image preserving aspect ratio, limiting height to about 3 cells:" << std::endl; // Create a new image for single-cell test - XLImage singleCellImage(pngData, ImageMimeTypes::PNG); + XLImage image6(pngData, ImageMimeTypes::PNG); - // Set specific pixel dimensions (200x100 pixels) - // Convert pixels to EMUs: 1 pixel = 9525 EMUs (standard) - // Note: For cell-based sizing, use 190500 EMUs per Excel cell height - uint32_t widthEMUs = 200 * 9525; // 200 pixels = 1,905,000 EMUs - uint32_t heightEMUs = 100 * 9525; // 100 pixels = 952,500 EMUs - singleCellImage.setDisplayWidth(widthEMUs); - singleCellImage.setDisplayHeight(heightEMUs); + // Set specific EMU dimensions, max ~3 cells high, with aspect ratio preservation + // Note: For cell-based sizing, use 200000 EMUs per Excel cell height + image6.setDisplaySizeWithAspectRatio(12 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); // Max 3 cells high // Add to cell E12 (single cell, not spanning) - bool singleCellSuccess = wks.addImage(singleCellImage, "E12"); - std::cout << " Added single-cell image (200x100 pixels) to E12: " << (singleCellSuccess ? "Success" : "Failed") << std::endl; + bool image6Success = wks.addImage(image6, "E12"); + std::cout << " Added image6 (3 rows high) to E12: " << (image6Success ? "Success" : "Failed") << std::endl; // Save the workbook doc.save(); std::cout << "\nWorkbook saved as 'Demo11.xlsx'" << std::endl; std::cout << "\nNote: This demo shows the XLImage class and worksheet integration." << std::endl; - std::cout << " Images are stored in memory but not yet embedded in the Excel file." << std::endl; - std::cout << " Next step: implement drawing XML generation and binary storage." << std::endl; + std::cout << " Images are embedded in the Excel file with proper aspect ratio preservation." << std::endl; + std::cout << " The .xlsx file can be opened in Excel, OpenOffice Calc, and other compatible applications." << std::endl; return 0; } diff --git a/OpenXLSX/headers/XLImage.hpp b/OpenXLSX/headers/XLImage.hpp index 7a2ab079..ac4e93f1 100644 --- a/OpenXLSX/headers/XLImage.hpp +++ b/OpenXLSX/headers/XLImage.hpp @@ -177,13 +177,13 @@ namespace OpenXLSX * @brief Get the image width in pixels * @return The width in pixels */ - uint32_t width() const; + uint32_t widthPixels() const; /** * @brief Get the image height in pixels * @return The height in pixels */ - uint32_t height() const; + uint32_t heightPixels() const; /** * @brief Set the display width in Excel units (EMUs) @@ -216,12 +216,20 @@ namespace OpenXLSX */ void setDisplaySizePixels(uint32_t widthPixels, uint32_t heightPixels); + /** + * @brief Set display size maintaining aspect ratio + * @param maxWidthEmus Maximum width in EMUs (0 = no limit) + * @param maxHeightEmus Maximum height in EMUs (0 = no limit) + */ + void setDisplaySizeWithAspectRatio(uint32_t maxWidthEmus = 0, uint32_t maxHeightEmus = 0); + /** * @brief Set display size maintaining aspect ratio * @param maxWidthPixels Maximum width in pixels (0 = no limit) * @param maxHeightPixels Maximum height in pixels (0 = no limit) */ - void setDisplaySizeWithAspectRatio(uint32_t maxWidthPixels = 0, uint32_t maxHeightPixels = 0); + void setDisplaySizePixelsWithAspectRatio(uint32_t maxWidthPixels = 0, uint32_t maxHeightPixels = 0); + /** * @brief Convert pixels to EMUs (Excel units) @@ -254,8 +262,8 @@ namespace OpenXLSX std::string m_mimeType; /**< MIME type of the image */ std::string m_extension; /**< File extension */ std::string m_id; /**< Unique image ID */ - uint32_t m_width{0}; /**< Image width in pixels */ - uint32_t m_height{0}; /**< Image height in pixels */ + uint32_t m_widthPixels{0}; /**< Image width in pixels */ + uint32_t m_heightPixels{0}; /**< Image height in pixels */ uint32_t m_displayWidth{0}; /**< Display width in EMUs */ uint32_t m_displayHeight{0}; /**< Display height in EMUs */ diff --git a/OpenXLSX/sources/XLDrawingML.cpp b/OpenXLSX/sources/XLDrawingML.cpp index f5a414e5..55a4ef8c 100644 --- a/OpenXLSX/sources/XLDrawingML.cpp +++ b/OpenXLSX/sources/XLDrawingML.cpp @@ -90,9 +90,26 @@ namespace OpenXLSX off.append_attribute("x").set_value("0"); off.append_attribute("y").set_value("0"); + // Calculate image size that preserves aspect ratio within the bounding box + uint32_t imageWidth = image.displayWidth(); + uint32_t imageHeight = image.displayHeight(); + + // Get the image's natural dimensions + uint32_t naturalWidth = image.widthPixels(); + uint32_t naturalHeight = image.heightPixels(); + + // Calculate scaling factor to fit within bounding box while preserving aspect ratio + double scaleX = static_cast(imageWidth) / naturalWidth; + double scaleY = static_cast(imageHeight) / naturalHeight; + double scale = std::min(scaleX, scaleY); // Use smaller scale to fit within box + + // Calculate actual image size (preserving aspect ratio) + uint32_t actualWidth = static_cast(naturalWidth * scale); + uint32_t actualHeight = static_cast(naturalHeight * scale); + XMLNode xfrmExt = xfrm.append_child("a:ext"); - xfrmExt.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); - xfrmExt.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); + xfrmExt.append_attribute("cx").set_value(std::to_string(actualWidth).c_str()); + xfrmExt.append_attribute("cy").set_value(std::to_string(actualHeight).c_str()); XMLNode prstGeom = spPr.append_child("a:prstGeom"); prstGeom.append_attribute("prst").set_value("rect"); diff --git a/OpenXLSX/sources/XLImage.cpp b/OpenXLSX/sources/XLImage.cpp index 425147dc..c76ab068 100644 --- a/OpenXLSX/sources/XLImage.cpp +++ b/OpenXLSX/sources/XLImage.cpp @@ -46,6 +46,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. #include // std::ifstream #include // std::stringstream #include // std::find +#include // std::round, std::fabs // ===== OpenXLSX Includes ===== // #include "XLImage.hpp" @@ -113,12 +114,12 @@ namespace OpenXLSX // Get image dimensions auto dimensions = getImageDimensions(m_imageData, m_mimeType); - m_width = dimensions.first; - m_height = dimensions.second; + m_widthPixels = dimensions.first; + m_heightPixels = dimensions.second; // Set default display size (same as original size) - m_displayWidth = pixelsToEMUs(m_width); - m_displayHeight = pixelsToEMUs(m_height); + m_displayWidth = pixelsToEMUs(m_widthPixels); + m_displayHeight = pixelsToEMUs(m_heightPixels); // Set the provided ID m_id = imageId; @@ -147,12 +148,12 @@ namespace OpenXLSX // Get image dimensions auto dimensions = getImageDimensions(m_imageData, m_mimeType); - m_width = dimensions.first; - m_height = dimensions.second; + m_widthPixels = dimensions.first; + m_heightPixels = dimensions.second; // Set default display size (same as original size) - m_displayWidth = pixelsToEMUs(m_width); - m_displayHeight = pixelsToEMUs(m_height); + m_displayWidth = pixelsToEMUs(m_widthPixels); + m_displayHeight = pixelsToEMUs(m_heightPixels); // Set the provided ID m_id = imageId; @@ -203,17 +204,17 @@ namespace OpenXLSX /** * @details Get the image width in pixels */ - uint32_t XLImage::width() const + uint32_t XLImage::widthPixels() const { - return m_width; + return m_widthPixels; } /** * @details Get the image height in pixels */ - uint32_t XLImage::height() const + uint32_t XLImage::heightPixels() const { - return m_height; + return m_heightPixels; } /** @@ -273,10 +274,10 @@ namespace OpenXLSX /** * @details Generate a unique image ID */ -std::string XLImage::generateId(uint32_t imageNumber) -{ - return "img" + std::to_string(imageNumber); -} + std::string XLImage::generateId(uint32_t imageNumber) + { + return "img" + std::to_string(imageNumber); + } /** * @details Determine MIME type from file extension @@ -377,32 +378,73 @@ std::string XLImage::generateId(uint32_t imageNumber) /** * @details Set display size maintaining aspect ratio */ - void XLImage::setDisplaySizeWithAspectRatio(uint32_t maxWidthPixels, uint32_t maxHeightPixels) + void XLImage::setDisplaySizeWithAspectRatio(uint32_t maxWidthEmus, uint32_t maxHeightEmus) { - if (m_width == 0 || m_height == 0) { - // If we don't have valid dimensions, use the current display size - return; - } - - double aspectRatio = static_cast(m_width) / static_cast(m_height); - - uint32_t targetWidth = m_width; - uint32_t targetHeight = m_height; + uint32_t targetWidth = pixelsToEmus(m_widthPixels); + uint32_t targetHeight = pixelsToEmus(m_heightPixels); + + if (m_widthPixels > 0 || m_heightPixels > 0) { + double aspectRatio = static_cast(m_widthPixels) / static_cast(m_heightPixels); + assert(aspectRatio > 0.0); + + const double maxWidth = static_cast(maxWidthEmus); + const double maxHeight = static_cast(maxHeightEmus); + const double widthA = maxHeight * aspectRatio; + + if (widthA <= maxWidth) { + /* + +------------+--------+ + | | | + | | | + | | | + | | |maxHeight + | | | + | | | + | | | + | widthA | | + +------------+--------+ + maxWidth + */ + targetWidth = static_cast(round(widthA)); + targetHeight = maxHeightEmus; + } + else { + /* + +---------------------+ + | | + | | + +---------------------+ + | |maxHeight + | | + | heightB| + | | + | | + +---------------------+ + maxWidth + */ + const double heightB = maxWidth / aspectRatio; + assert(heightB <= (1.00000001 * maxHeight)); + targetWidth = maxWidthEmus; + targetHeight = static_cast(round(heightB)); + } - // Apply width constraint if specified - if (maxWidthPixels > 0 && targetWidth > maxWidthPixels) { - targetWidth = maxWidthPixels; - targetHeight = static_cast(targetWidth / aspectRatio); + assert(fabs(aspectRatio - (static_cast(targetWidth) / + static_cast(targetHeight))) < 1e-4); } - // Apply height constraint if specified - if (maxHeightPixels > 0 && targetHeight > maxHeightPixels) { - targetHeight = maxHeightPixels; - targetWidth = static_cast(targetHeight * aspectRatio); - } + m_displayWidth = targetWidth; + m_displayHeight = targetHeight; + } - m_displayWidth = pixelsToEmus(targetWidth); - m_displayHeight = pixelsToEmus(targetHeight); + + /** + * @details Set display size maintaining aspect ratio + */ + void XLImage::setDisplaySizePixelsWithAspectRatio(uint32_t maxWidthPixels, uint32_t maxHeightPixels) + { + const uint32_t maxWidthEmus = pixelsToEmus(maxWidthPixels); + const uint32_t maxHeightEmus = pixelsToEmus(maxHeightPixels); + setDisplaySizeWithAspectRatio(maxWidthEmus, maxHeightEmus); } /** From d5e5cfb259be84d6a8c3e6e5d87592155038b19d Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Thu, 2 Oct 2025 14:33:49 -0700 Subject: [PATCH 05/13] Embedded Image Support -- phase IV twoCellAnchor QA matrix --- Examples/Demo11.cpp | 1059 ++++++++++++++---------------- OpenXLSX/headers/XLImage.hpp | 19 + OpenXLSX/sources/XLDrawingML.cpp | 25 +- OpenXLSX/sources/XLImage.cpp | 6 +- OpenXLSX/sources/XLSheet.cpp | 1 + 5 files changed, 544 insertions(+), 566 deletions(-) diff --git a/Examples/Demo11.cpp b/Examples/Demo11.cpp index 5e82014f..c628bfa0 100644 --- a/Examples/Demo11.cpp +++ b/Examples/Demo11.cpp @@ -1,594 +1,543 @@ /* - - ____ ____ ___ ____ ____ ____ ___ - 6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M' - 8P Y8 `MM. d' MM 6M' ` `MM. d' -6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d' -MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d' -MM MM MM MM MM MM MM' MM `MMd MM YMMMMb `MMd -MM MM MM MM MMMMMMMM MM MM d'`MM. MM `Mb dMM. -YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. - 8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM. - YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_ - MM - MM - _MM_ - - Copyright (c) 2018, Kenneth Troldal Balslev - - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - Neither the name of the author nor the - names of any contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - + * Demo11.cpp - Comprehensive Image Embedding Test Matrix + * + * This demo tests various combinations of image embedding options: + * - Anchor types: oneCellAnchor vs twoCellAnchor + * - File formats: PNG, JPEG, GIF, BMP + * - Sizing strategies: aspect ratio preservation, original size, exact dimensions + * - Units: pixels, EMUs, cell spacing */ -// ===== External Includes ===== // #include #include - -// ===== OpenXLSX Includes ===== // +#include #include "OpenXLSX.hpp" using namespace OpenXLSX; -int main() -{ - std::cout << "OpenXLSX Image Demo" << std::endl; - std::cout << "==================" << std::endl; - - // Create a new workbook - XLDocument doc; - doc.create("./Demo11.xlsx"); - auto wks = doc.workbook().worksheet("Sheet1"); - - // Add some text to the worksheet - wks.cell("A1").value() = "Image Demo"; - wks.cell("A2").value() = "This worksheet demonstrates image functionality"; - wks.cell("A4").value() = "Image 1 (PNG):"; - wks.cell("A6").value() = "Image 2 (JPEG):"; - wks.cell("A8").value() = "Image 3 (BMP):"; - wks.cell("A10").value() = "Image 4 (GIF):"; - // Demonstrate XLImage class functionality - std::cout << "\n=== XLImage Class Demo ===" << std::endl; - - // Create images from different sources - std::cout << "\n1. Creating images from binary data..." << std::endl; - - // Static const vectors containing actual image file data - // These contain the binary data from the tiny image files in the images directory +// These contain the binary data from the tiny image files in the images directory - // tiny_png.png - Small PNG image - static const std::vector pngData = { - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0f, - 0x08, 0x02, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x83, 0xf5, 0x00, 0x00, 0x00, - 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, - 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, - 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, - 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, - 0x00, 0x01, 0xa8, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4f, 0x63, 0x7c, 0xff, - 0x64, 0x03, 0x03, 0xcd, 0x00, 0x13, 0x94, 0xa6, 0x0d, 0x00, 0xbb, 0xfd, - 0xf1, 0xfe, 0x7f, 0xa1, 0x5b, 0xa1, 0x02, 0x50, 0x20, 0xc5, 0xb8, 0xba, - 0x88, 0x51, 0x96, 0x81, 0xe1, 0xfb, 0x85, 0x98, 0x6b, 0x67, 0x18, 0xa4, - 0x42, 0x96, 0x48, 0x0a, 0x40, 0x65, 0x40, 0x22, 0xef, 0xd3, 0x8c, 0x1d, - 0xed, 0xa0, 0x7c, 0xce, 0x89, 0x29, 0xc2, 0xfb, 0xa1, 0x6c, 0x06, 0xc5, - 0xe0, 0x17, 0xfd, 0x9e, 0x7f, 0xa0, 0x1c, 0x06, 0x06, 0x16, 0x28, 0xcd, - 0x60, 0xcc, 0x78, 0x22, 0x92, 0x11, 0xca, 0x66, 0x60, 0x38, 0xb2, 0xfc, - 0x5f, 0x68, 0x1f, 0x03, 0xd0, 0x02, 0x11, 0x10, 0xef, 0xfa, 0xb3, 0xf3, - 0x87, 0x24, 0xe1, 0xc6, 0x21, 0x81, 0x17, 0x7c, 0x85, 0x35, 0x7c, 0x0a, - 0xd9, 0x4f, 0x36, 0x18, 0x42, 0xf8, 0x40, 0x9b, 0x24, 0x0a, 0x19, 0x10, - 0x16, 0xe0, 0x08, 0x19, 0x1b, 0x17, 0x46, 0x8d, 0x67, 0x0c, 0x0f, 0x21, - 0x1c, 0xe5, 0x7c, 0xa9, 0xf7, 0xb3, 0x9e, 0x7f, 0x80, 0x70, 0x90, 0x00, - 0xcb, 0xba, 0x99, 0x7c, 0x0c, 0xc1, 0x2f, 0xf2, 0xa1, 0x46, 0x03, 0xc1, - 0xf7, 0xfc, 0x96, 0x4f, 0x0c, 0x6b, 0xf9, 0x4e, 0x41, 0xb9, 0xc4, 0x85, - 0xbb, 0x82, 0xa4, 0x93, 0xdb, 0xfb, 0xf3, 0x87, 0xa0, 0x3c, 0x18, 0x78, - 0xc1, 0x75, 0xf8, 0xf1, 0xb7, 0x48, 0xa4, 0x70, 0x00, 0x01, 0x89, 0x4f, - 0xfd, 0x73, 0xde, 0x99, 0x41, 0x39, 0xb8, 0x4c, 0x3f, 0xb2, 0xe7, 0xff, - 0x0d, 0x63, 0x06, 0x1b, 0x28, 0x8f, 0x81, 0x41, 0x20, 0x4e, 0x8a, 0xa1, - 0xf0, 0x1e, 0xd4, 0x2f, 0x50, 0xf0, 0x9c, 0xe5, 0xbe, 0xec, 0x1f, 0x19, - 0x28, 0x07, 0x3b, 0x80, 0x87, 0xfb, 0xd9, 0xff, 0x16, 0x67, 0xff, 0x43, - 0xd9, 0x40, 0x00, 0x8e, 0x55, 0x28, 0x1b, 0x0c, 0x04, 0x0c, 0xf3, 0x9f, - 0xed, 0x5b, 0xf4, 0x5d, 0x3e, 0x0e, 0xca, 0x47, 0x07, 0xa7, 0xe6, 0xc8, - 0xb4, 0x9d, 0x80, 0x30, 0xff, 0xc4, 0xb5, 0xbc, 0x08, 0x92, 0x00, 0xb1, - 0x70, 0xc4, 0x2a, 0x16, 0x20, 0x10, 0xa7, 0xa8, 0x14, 0xf3, 0xfc, 0x61, - 0x9c, 0x24, 0x94, 0xcf, 0x20, 0xf9, 0x47, 0xf1, 0x31, 0xcb, 0x13, 0xa0, - 0x43, 0xc0, 0x3c, 0xb3, 0x94, 0x27, 0x1b, 0x52, 0x80, 0x34, 0x30, 0x62, - 0xf9, 0xc1, 0x02, 0x20, 0x40, 0x4a, 0x7a, 0xe7, 0x34, 0x48, 0x63, 0xd8, - 0xdd, 0x00, 0x8f, 0x5d, 0x89, 0x6f, 0xb6, 0xb2, 0x5c, 0xcb, 0xb7, 0xc3, - 0xdd, 0x87, 0x05, 0x90, 0x96, 0x9b, 0xec, 0x24, 0x4d, 0xee, 0x3c, 0x3b, - 0x73, 0x1d, 0xca, 0xfb, 0x13, 0x94, 0x0e, 0x4c, 0x21, 0x12, 0x85, 0x08, - 0x0b, 0x58, 0xd6, 0x35, 0x22, 0xd2, 0x3e, 0x10, 0xe0, 0xb3, 0x19, 0x0b, - 0xe0, 0x34, 0x68, 0x95, 0xba, 0x17, 0xf4, 0x0c, 0xca, 0x03, 0xa7, 0x90, - 0x6f, 0xeb, 0x1a, 0x25, 0x02, 0xd6, 0x42, 0x05, 0x18, 0x2c, 0xde, 0x6e, - 0xa8, 0xff, 0x0e, 0x65, 0x43, 0xf3, 0x2a, 0xcd, 0x00, 0x69, 0x21, 0x43, - 0x2a, 0xa0, 0xa5, 0xe9, 0x0c, 0x0c, 0x00, 0xe4, 0x3f, 0x87, 0xe1, 0xb6, - 0x42, 0x8a, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, - 0x42, 0x60, 0x82 - }; +// tiny_png.png - Small PNG image +static const std::vector pngData = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0f, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x83, 0xf5, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, + 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, + 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, + 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, + 0x00, 0x01, 0xa8, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4f, 0x63, 0x7c, 0xff, + 0x64, 0x03, 0x03, 0xcd, 0x00, 0x13, 0x94, 0xa6, 0x0d, 0x00, 0xbb, 0xfd, + 0xf1, 0xfe, 0x7f, 0xa1, 0x5b, 0xa1, 0x02, 0x50, 0x20, 0xc5, 0xb8, 0xba, + 0x88, 0x51, 0x96, 0x81, 0xe1, 0xfb, 0x85, 0x98, 0x6b, 0x67, 0x18, 0xa4, + 0x42, 0x96, 0x48, 0x0a, 0x40, 0x65, 0x40, 0x22, 0xef, 0xd3, 0x8c, 0x1d, + 0xed, 0xa0, 0x7c, 0xce, 0x89, 0x29, 0xc2, 0xfb, 0xa1, 0x6c, 0x06, 0xc5, + 0xe0, 0x17, 0xfd, 0x9e, 0x7f, 0xa0, 0x1c, 0x06, 0x06, 0x16, 0x28, 0xcd, + 0x60, 0xcc, 0x78, 0x22, 0x92, 0x11, 0xca, 0x66, 0x60, 0x38, 0xb2, 0xfc, + 0x5f, 0x68, 0x1f, 0x03, 0xd0, 0x02, 0x11, 0x10, 0xef, 0xfa, 0xb3, 0xf3, + 0x87, 0x24, 0xe1, 0xc6, 0x21, 0x81, 0x17, 0x7c, 0x85, 0x35, 0x7c, 0x0a, + 0xd9, 0x4f, 0x36, 0x18, 0x42, 0xf8, 0x40, 0x9b, 0x24, 0x0a, 0x19, 0x10, + 0x16, 0xe0, 0x08, 0x19, 0x1b, 0x17, 0x46, 0x8d, 0x67, 0x0c, 0x0f, 0x21, + 0x1c, 0xe5, 0x7c, 0xa9, 0xf7, 0xb3, 0x9e, 0x7f, 0x80, 0x70, 0x90, 0x00, + 0xcb, 0xba, 0x99, 0x7c, 0x0c, 0xc1, 0x2f, 0xf2, 0xa1, 0x46, 0x03, 0xc1, + 0xf7, 0xfc, 0x96, 0x4f, 0x0c, 0x6b, 0xf9, 0x4e, 0x41, 0xb9, 0xc4, 0x85, + 0xbb, 0x82, 0xa4, 0x93, 0xdb, 0xfb, 0xf3, 0x87, 0xa0, 0x3c, 0x18, 0x78, + 0xc1, 0x75, 0xf8, 0xf1, 0xb7, 0x48, 0xa4, 0x70, 0x00, 0x01, 0x89, 0x4f, + 0xfd, 0x73, 0xde, 0x99, 0x41, 0x39, 0xb8, 0x4c, 0x3f, 0xb2, 0xe7, 0xff, + 0x0d, 0x63, 0x06, 0x1b, 0x28, 0x8f, 0x81, 0x41, 0x20, 0x4e, 0x8a, 0xa1, + 0xf0, 0x1e, 0xd4, 0x2f, 0x50, 0xf0, 0x9c, 0xe5, 0xbe, 0xec, 0x1f, 0x19, + 0x28, 0x07, 0x3b, 0x80, 0x87, 0xfb, 0xd9, 0xff, 0x16, 0x67, 0xff, 0x43, + 0xd9, 0x40, 0x00, 0x8e, 0x55, 0x28, 0x1b, 0x0c, 0x04, 0x0c, 0xf3, 0x9f, + 0xed, 0x5b, 0xf4, 0x5d, 0x3e, 0x0e, 0xca, 0x47, 0x07, 0xa7, 0xe6, 0xc8, + 0xb4, 0x9d, 0x80, 0x30, 0xff, 0xc4, 0xb5, 0xbc, 0x08, 0x92, 0x00, 0xb1, + 0x70, 0xc4, 0x2a, 0x16, 0x20, 0x10, 0xa7, 0xa8, 0x14, 0xf3, 0xfc, 0x61, + 0x9c, 0x24, 0x94, 0xcf, 0x20, 0xf9, 0x47, 0xf1, 0x31, 0xcb, 0x13, 0xa0, + 0x43, 0xc0, 0x3c, 0xb3, 0x94, 0x27, 0x1b, 0x52, 0x80, 0x34, 0x30, 0x62, + 0xf9, 0xc1, 0x02, 0x20, 0x40, 0x4a, 0x7a, 0xe7, 0x34, 0x48, 0x63, 0xd8, + 0xdd, 0x00, 0x8f, 0x5d, 0x89, 0x6f, 0xb6, 0xb2, 0x5c, 0xcb, 0xb7, 0xc3, + 0xdd, 0x87, 0x05, 0x90, 0x96, 0x9b, 0xec, 0x24, 0x4d, 0xee, 0x3c, 0x3b, + 0x73, 0x1d, 0xca, 0xfb, 0x13, 0x94, 0x0e, 0x4c, 0x21, 0x12, 0x85, 0x08, + 0x0b, 0x58, 0xd6, 0x35, 0x22, 0xd2, 0x3e, 0x10, 0xe0, 0xb3, 0x19, 0x0b, + 0xe0, 0x34, 0x68, 0x95, 0xba, 0x17, 0xf4, 0x0c, 0xca, 0x03, 0xa7, 0x90, + 0x6f, 0xeb, 0x1a, 0x25, 0x02, 0xd6, 0x42, 0x05, 0x18, 0x2c, 0xde, 0x6e, + 0xa8, 0xff, 0x0e, 0x65, 0x43, 0xf3, 0x2a, 0xcd, 0x00, 0x69, 0x21, 0x43, + 0x2a, 0xa0, 0xa5, 0xe9, 0x0c, 0x0c, 0x00, 0xe4, 0x3f, 0x87, 0xe1, 0xb6, + 0x42, 0x8a, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, + 0x42, 0x60, 0x82 +}; - // tiny_jpeg.jpg - Small JPEG image (actual file data from xxd) - static const std::vector jpegData = { - 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, - 0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xe1, 0x00, 0x22, - 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, - 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, - 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x03, 0x05, 0x03, 0x03, 0x03, 0x03, 0x03, 0x06, 0x04, - 0x04, 0x03, 0x05, 0x07, 0x06, 0x07, 0x07, 0x07, 0x06, 0x07, 0x07, 0x08, - 0x09, 0x0b, 0x09, 0x08, 0x08, 0x0a, 0x08, 0x07, 0x07, 0x0a, 0x0d, 0x0a, - 0x0a, 0x0b, 0x0c, 0x0c, 0x0c, 0x0c, 0x07, 0x09, 0x0e, 0x0f, 0x0d, 0x0c, - 0x0e, 0x0b, 0x0c, 0x0c, 0x0c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x02, 0x02, - 0x02, 0x03, 0x03, 0x03, 0x06, 0x03, 0x03, 0x06, 0x0c, 0x08, 0x07, 0x08, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x12, 0x00, 0x20, 0x03, - 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, - 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, - 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, - 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, - 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, - 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, - 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, - 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, - 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, - 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, - 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, - 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, - 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, - 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, - 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, - 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, - 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, - 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, - 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, - 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, - 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, - 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, - 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, - 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, - 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, - 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, - 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, - 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, - 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, - 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, - 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, - 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, - 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, - 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfd, - 0x5e, 0xf8, 0xcf, 0xf1, 0x17, 0xfe, 0x15, 0x17, 0xc2, 0x5f, 0x12, 0x78, - 0xa7, 0xec, 0x7f, 0xda, 0x1f, 0xf0, 0x8f, 0xe9, 0xd3, 0xea, 0x1f, 0x65, - 0xf3, 0x7c, 0x9f, 0xb4, 0x79, 0x68, 0x5b, 0x66, 0xfd, 0xad, 0xb7, 0x38, - 0xc6, 0x70, 0x71, 0xe8, 0x6b, 0x26, 0x4f, 0x88, 0xfe, 0x29, 0xd6, 0x3c, - 0x59, 0xe2, 0x0d, 0x3f, 0x43, 0xf0, 0xee, 0x83, 0x79, 0x6d, 0xa0, 0x5c, - 0xa5, 0xab, 0xcd, 0x7b, 0xae, 0x4b, 0x6b, 0x24, 0xce, 0xd0, 0x47, 0x37, - 0x08, 0xb6, 0xb2, 0x00, 0x31, 0x20, 0x1c, 0xbf, 0x51, 0xda, 0xb9, 0xdf, - 0xda, 0x7b, 0x4d, 0xf1, 0xaf, 0x8e, 0xbc, 0x3d, 0xe2, 0x3f, 0x06, 0xe9, - 0x3e, 0x1b, 0x5d, 0x4f, 0x45, 0xf1, 0x76, 0x86, 0x34, 0xdb, 0x6d, 0x4e, - 0x2b, 0xa8, 0x22, 0xfe, 0xc9, 0xba, 0x95, 0xe5, 0x8e, 0x79, 0x2e, 0x84, - 0x92, 0xab, 0xb4, 0x2b, 0x13, 0x42, 0xeb, 0xe4, 0x47, 0x23, 0xe5, 0x25, - 0x04, 0x72, 0x95, 0xa9, 0xa6, 0xfc, 0x0f, 0x8b, 0x5b, 0xf1, 0xe7, 0x8c, - 0x35, 0x1d, 0x5c, 0xeb, 0xd6, 0xd0, 0xea, 0x7a, 0x84, 0x52, 0x5a, 0x7d, - 0x8b, 0x5f, 0xbb, 0xb2, 0x8e, 0x78, 0x85, 0xac, 0x08, 0x49, 0x8e, 0xde, - 0x65, 0x5c, 0xef, 0x57, 0x19, 0x61, 0xb8, 0x81, 0xe9, 0x8a, 0xfc, 0x3f, - 0x56, 0xfe, 0x4f, 0xef, 0xba, 0xff, 0x00, 0x83, 0xf9, 0x9f, 0x53, 0xc4, - 0x58, 0x4a, 0xd4, 0xb0, 0x18, 0x47, 0x82, 0x92, 0xf6, 0xb5, 0x2a, 0x4f, - 0x9f, 0x96, 0x49, 0xb5, 0x4f, 0xd9, 0xa7, 0x1e, 0x6f, 0x76, 0x7c, 0x9e, - 0xfd, 0xd6, 0xb1, 0x4d, 0xbd, 0x2f, 0x6b, 0x15, 0x57, 0xf6, 0x8a, 0xbf, - 0xf1, 0x8c, 0xda, 0x3e, 0x9f, 0xe0, 0xdf, 0x0d, 0xc5, 0xab, 0xeb, 0xba, - 0x86, 0x9c, 0xba, 0xb5, 0xe4, 0x1a, 0x9e, 0xa3, 0xfd, 0x9f, 0x69, 0xa4, - 0xdb, 0x33, 0xbc, 0x6b, 0xe7, 0x4f, 0x1c, 0x53, 0x93, 0x23, 0xc9, 0x1c, - 0x8a, 0x88, 0x91, 0xb0, 0x61, 0x14, 0x8c, 0x59, 0x40, 0x1b, 0xba, 0xff, - 0x00, 0x86, 0x3e, 0x34, 0xd5, 0x3c, 0x61, 0xa5, 0xde, 0xae, 0xb7, 0xe1, - 0xfb, 0x8f, 0x0e, 0xea, 0xda, 0x5d, 0xe3, 0xd9, 0xdc, 0xdb, 0x99, 0x0c, - 0xf6, 0xd3, 0x10, 0x15, 0x96, 0x6b, 0x69, 0xca, 0x27, 0x9d, 0x0b, 0xab, - 0xa9, 0x0d, 0xb1, 0x48, 0x3b, 0x95, 0x95, 0x59, 0x58, 0x0e, 0x23, 0xc4, - 0x7e, 0x00, 0xd5, 0x3e, 0x14, 0x7c, 0x46, 0x9b, 0x5c, 0xf0, 0xcf, 0x86, - 0xae, 0x35, 0xff, 0x00, 0x0f, 0xeb, 0x1a, 0x1d, 0xb6, 0x83, 0x7d, 0xa4, - 0xe9, 0x57, 0x70, 0xd9, 0x5f, 0xd8, 0x0b, 0x66, 0x9d, 0xa0, 0x96, 0xdd, - 0xa6, 0x92, 0x28, 0xca, 0x15, 0xb8, 0x74, 0x71, 0xe6, 0xa3, 0xa9, 0x58, - 0xd9, 0x77, 0x1d, 0xc0, 0x5a, 0xfd, 0x98, 0xfc, 0x11, 0xe2, 0x0f, 0x09, - 0x5b, 0x78, 0x9a, 0xe3, 0x5a, 0x87, 0x5c, 0xd3, 0xb4, 0xfd, 0x5b, 0x52, - 0x59, 0xf4, 0x7d, 0x2b, 0x58, 0xf1, 0x0c, 0xda, 0xe5, 0xf6, 0x99, 0x6c, - 0xb0, 0xc7, 0x11, 0x59, 0x66, 0x92, 0x49, 0x42, 0xb3, 0xc8, 0x8f, 0x2e, - 0xc8, 0xe5, 0x91, 0x54, 0x48, 0x06, 0xec, 0x82, 0x07, 0xd7, 0x63, 0x30, - 0x79, 0x7f, 0xf6, 0x73, 0xab, 0x47, 0x92, 0xe9, 0x41, 0xa7, 0xcd, 0xef, - 0xca, 0x4d, 0xfb, 0xf1, 0x71, 0xf6, 0x8e, 0xdc, 0xad, 0xbb, 0x7e, 0xee, - 0xce, 0x31, 0x4f, 0x9e, 0xef, 0xde, 0xf2, 0xf0, 0xaf, 0x13, 0x1e, 0x48, - 0x57, 0x77, 0x95, 0x92, 0x93, 0x4b, 0x46, 0xf9, 0x55, 0xda, 0x76, 0x5b, - 0xca, 0xfd, 0xad, 0xb5, 0x8f, 0x52, 0xa2, 0x8a, 0x2b, 0xe4, 0x4f, 0x4c, - 0x28, 0xa2, 0x8a, 0x00, 0xff, 0xd9 - }; +// tiny_jpeg.jpg - Small JPEG image (actual file data from xxd) +static const std::vector jpegData = { + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, + 0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xe1, 0x00, 0x22, + 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, + 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x03, 0x05, 0x03, 0x03, 0x03, 0x03, 0x03, 0x06, 0x04, + 0x04, 0x03, 0x05, 0x07, 0x06, 0x07, 0x07, 0x07, 0x06, 0x07, 0x07, 0x08, + 0x09, 0x0b, 0x09, 0x08, 0x08, 0x0a, 0x08, 0x07, 0x07, 0x0a, 0x0d, 0x0a, + 0x0a, 0x0b, 0x0c, 0x0c, 0x0c, 0x0c, 0x07, 0x09, 0x0e, 0x0f, 0x0d, 0x0c, + 0x0e, 0x0b, 0x0c, 0x0c, 0x0c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x02, 0x02, + 0x02, 0x03, 0x03, 0x03, 0x06, 0x03, 0x03, 0x06, 0x0c, 0x08, 0x07, 0x08, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x12, 0x00, 0x20, 0x03, + 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, + 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, + 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, + 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, + 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, + 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, + 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, + 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, + 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, + 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, + 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, + 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, + 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, + 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, + 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, + 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, + 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, + 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, + 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, + 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, + 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, + 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfd, + 0x5e, 0xf8, 0xcf, 0xf1, 0x17, 0xfe, 0x15, 0x17, 0xc2, 0x5f, 0x12, 0x78, + 0xa7, 0xec, 0x7f, 0xda, 0x1f, 0xf0, 0x8f, 0xe9, 0xd3, 0xea, 0x1f, 0x65, + 0xf3, 0x7c, 0x9f, 0xb4, 0x79, 0x68, 0x5b, 0x66, 0xfd, 0xad, 0xb7, 0x38, + 0xc6, 0x70, 0x71, 0xe8, 0x6b, 0x26, 0x4f, 0x88, 0xfe, 0x29, 0xd6, 0x3c, + 0x59, 0xe2, 0x0d, 0x3f, 0x43, 0xf0, 0xee, 0x83, 0x79, 0x6d, 0xa0, 0x5c, + 0xa5, 0xab, 0xcd, 0x7b, 0xae, 0x4b, 0x6b, 0x24, 0xce, 0xd0, 0x47, 0x37, + 0x08, 0xb6, 0xb2, 0x00, 0x31, 0x20, 0x1c, 0xbf, 0x51, 0xda, 0xb9, 0xdf, + 0xda, 0x7b, 0x4d, 0xf1, 0xaf, 0x8e, 0xbc, 0x3d, 0xe2, 0x3f, 0x06, 0xe9, + 0x3e, 0x1b, 0x5d, 0x4f, 0x45, 0xf1, 0x76, 0x86, 0x34, 0xdb, 0x6d, 0x4e, + 0x2b, 0xa8, 0x22, 0xfe, 0xc9, 0xba, 0x95, 0xe5, 0x8e, 0x79, 0x2e, 0x84, + 0x92, 0xab, 0xb4, 0x2b, 0x13, 0x42, 0xeb, 0xe4, 0x47, 0x23, 0xe5, 0x25, + 0x04, 0x72, 0x95, 0xa9, 0xa6, 0xfc, 0x0f, 0x8b, 0x5b, 0xf1, 0xe7, 0x8c, + 0x35, 0x1d, 0x5c, 0xeb, 0xd6, 0xd0, 0xea, 0x7a, 0x84, 0x52, 0x5a, 0x7d, + 0x8b, 0x5f, 0xbb, 0xb2, 0x8e, 0x78, 0x85, 0xac, 0x08, 0x49, 0x8e, 0xde, + 0x65, 0x5c, 0xef, 0x57, 0x19, 0x61, 0xb8, 0x81, 0xe9, 0x8a, 0xfc, 0x3f, + 0x56, 0xfe, 0x4f, 0xef, 0xba, 0xff, 0x00, 0x83, 0xf9, 0x9f, 0x53, 0xc4, + 0x58, 0x4a, 0xd4, 0xb0, 0x18, 0x47, 0x82, 0x92, 0xf6, 0xb5, 0x2a, 0x4f, + 0x9f, 0x96, 0x49, 0xb5, 0x4f, 0xd9, 0xa7, 0x1e, 0x6f, 0x76, 0x7c, 0x9e, + 0xfd, 0xd6, 0xb1, 0x4d, 0xbd, 0x2f, 0x6b, 0x15, 0x57, 0xf6, 0x8a, 0xbf, + 0xf1, 0x8c, 0xda, 0x3e, 0x9f, 0xe0, 0xdf, 0x0d, 0xc5, 0xab, 0xeb, 0xba, + 0x86, 0x9c, 0xba, 0xb5, 0xe4, 0x1a, 0x9e, 0xa3, 0xfd, 0x9f, 0x69, 0xa4, + 0xdb, 0x33, 0xbc, 0x6b, 0xe7, 0x4f, 0x1c, 0x53, 0x93, 0x23, 0xc9, 0x1c, + 0x8a, 0x88, 0x91, 0xb0, 0x61, 0x14, 0x8c, 0x59, 0x40, 0x1b, 0xba, 0xff, + 0x00, 0x86, 0x3e, 0x34, 0xd5, 0x3c, 0x61, 0xa5, 0xde, 0xae, 0xb7, 0xe1, + 0xfb, 0x8f, 0x0e, 0xea, 0xda, 0x5d, 0xe3, 0xd9, 0xdc, 0xdb, 0x99, 0x0c, + 0xf6, 0xd3, 0x10, 0x15, 0x96, 0x6b, 0x69, 0xca, 0x27, 0x9d, 0x0b, 0xab, + 0xa9, 0x0d, 0xb1, 0x48, 0x3b, 0x95, 0x95, 0x59, 0x58, 0x0e, 0x23, 0xc4, + 0x7e, 0x00, 0xd5, 0x3e, 0x14, 0x7c, 0x46, 0x9b, 0x5c, 0xf0, 0xcf, 0x86, + 0xae, 0x35, 0xff, 0x00, 0x0f, 0xeb, 0x1a, 0x1d, 0xb6, 0x83, 0x7d, 0xa4, + 0xe9, 0x57, 0x70, 0xd9, 0x5f, 0xd8, 0x0b, 0x66, 0x9d, 0xa0, 0x96, 0xdd, + 0xa6, 0x92, 0x28, 0xca, 0x15, 0xb8, 0x74, 0x71, 0xe6, 0xa3, 0xa9, 0x58, + 0xd9, 0x77, 0x1d, 0xc0, 0x5a, 0xfd, 0x98, 0xfc, 0x11, 0xe2, 0x0f, 0x09, + 0x5b, 0x78, 0x9a, 0xe3, 0x5a, 0x87, 0x5c, 0xd3, 0xb4, 0xfd, 0x5b, 0x52, + 0x59, 0xf4, 0x7d, 0x2b, 0x58, 0xf1, 0x0c, 0xda, 0xe5, 0xf6, 0x99, 0x6c, + 0xb0, 0xc7, 0x11, 0x59, 0x66, 0x92, 0x49, 0x42, 0xb3, 0xc8, 0x8f, 0x2e, + 0xc8, 0xe5, 0x91, 0x54, 0x48, 0x06, 0xec, 0x82, 0x07, 0xd7, 0x63, 0x30, + 0x79, 0x7f, 0xf6, 0x73, 0xab, 0x47, 0x92, 0xe9, 0x41, 0xa7, 0xcd, 0xef, + 0xca, 0x4d, 0xfb, 0xf1, 0x71, 0xf6, 0x8e, 0xdc, 0xad, 0xbb, 0x7e, 0xee, + 0xce, 0x31, 0x4f, 0x9e, 0xef, 0xde, 0xf2, 0xf0, 0xaf, 0x13, 0x1e, 0x48, + 0x57, 0x77, 0x95, 0x92, 0x93, 0x4b, 0x46, 0xf9, 0x55, 0xda, 0x76, 0x5b, + 0xca, 0xfd, 0xad, 0xb5, 0x8f, 0x52, 0xa2, 0x8a, 0x2b, 0xe4, 0x4f, 0x4c, + 0x28, 0xa2, 0x8a, 0x00, 0xff, 0xd9 +}; - // tiny_bmp.bmp - Small BMP image (actual file data from xxd) - static const std::vector bmpData = { - 0x42, 0x4d, 0x76, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, - 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x10, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, - 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, 0xb1, 0x22, 0xb0, 0xdb, 0x91, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x3e, 0x89, - 0xef, 0x6f, 0x44, 0xed, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x4c, 0xb1, - 0x22, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, - 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, - 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0x91, 0xe4, 0xef, 0x5e, - 0xb1, 0x6f, 0x80, 0xd4, 0x91, 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x24, 0x1c, 0xee, - 0xb0, 0xa8, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0x80, 0xdb, 0xef, 0x80, 0xba, 0x4b, 0x91, 0xe4, 0xd0, 0x6f, 0xb1, 0x6f, - 0xb0, 0xe4, 0xb1, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, - 0xef, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0x5e, 0xcb, 0xd0, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, - 0xef, 0x4c, 0xc2, 0xb1, 0xb0, 0xd4, 0x6f, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, - 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, - 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x24, - 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, - 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x9b, 0x89, 0xed, 0xb0, 0xe4, 0xef, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xa0, 0xe4, 0xb1, 0x5e, 0xba, 0x91, 0xb0, - 0xdb, 0x91, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x91, 0xc2, 0x22, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc2, 0x48, 0x3f, 0xb0, 0xe4, 0xb9, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x85, 0x67, 0xed, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0x80, 0xdb, 0xb1, - 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0xb0, 0xe4, 0xef, 0x91, 0xe4, 0xef, - 0x6f, 0xb1, 0x6f, 0x80, 0xdb, 0xb1, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, 0xef, 0xcc, 0x65, 0x9d, - 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, - 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0x5e, 0xcb, 0x91, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x80, 0xcb, 0x6f, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, - 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, - 0xef, 0xcc, 0x65, 0x9d, 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, - 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0x80, - 0xdb, 0xef, 0x5e, 0xb1, 0x4b, 0x4c, 0xba, 0x4b, 0xb0, 0xd4, 0x6f, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, - 0xba, 0x22, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, - 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x4c, 0xb1, 0x4b, 0x6f, 0xb1, 0x22, - 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x91, 0xe4, 0xef, 0x4c, 0xb1, 0x6f, 0x6f, 0xb1, 0x22, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, - 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00 - }; +// tiny_bmp.bmp - Small BMP image (actual file data from xxd) +static const std::vector bmpData = { + 0x42, 0x4d, 0x76, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, + 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, 0xb1, 0x22, 0xb0, 0xdb, 0x91, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x3e, 0x89, + 0xef, 0x6f, 0x44, 0xed, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x4c, 0xb1, + 0x22, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0x91, 0xe4, 0xef, 0x5e, + 0xb1, 0x6f, 0x80, 0xd4, 0x91, 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x24, 0x1c, 0xee, + 0xb0, 0xa8, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0x80, 0xdb, 0xef, 0x80, 0xba, 0x4b, 0x91, 0xe4, 0xd0, 0x6f, 0xb1, 0x6f, + 0xb0, 0xe4, 0xb1, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, + 0xef, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0x5e, 0xcb, 0xd0, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, + 0xef, 0x4c, 0xc2, 0xb1, 0xb0, 0xd4, 0x6f, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, + 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, + 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x9b, 0x89, 0xed, 0xb0, 0xe4, 0xef, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xa0, 0xe4, 0xb1, 0x5e, 0xba, 0x91, 0xb0, + 0xdb, 0x91, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x91, 0xc2, 0x22, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc2, 0x48, 0x3f, 0xb0, 0xe4, 0xb9, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x85, 0x67, 0xed, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0x80, 0xdb, 0xb1, + 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0xb0, 0xe4, 0xef, 0x91, 0xe4, 0xef, + 0x6f, 0xb1, 0x6f, 0x80, 0xdb, 0xb1, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, 0xef, 0xcc, 0x65, 0x9d, + 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, + 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0x5e, 0xcb, 0x91, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x80, 0xcb, 0x6f, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, + 0xef, 0xcc, 0x65, 0x9d, 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0x80, + 0xdb, 0xef, 0x5e, 0xb1, 0x4b, 0x4c, 0xba, 0x4b, 0xb0, 0xd4, 0x6f, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, + 0xba, 0x22, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x4c, 0xb1, 0x4b, 0x6f, 0xb1, 0x22, + 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x91, 0xe4, 0xef, 0x4c, 0xb1, 0x6f, 0x6f, 0xb1, 0x22, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, + 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00 +}; - // tiny_gif.gif - Small GIF image (actual file data from xxd) - static const std::vector gifData = { - 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x1a, 0x00, 0x10, 0x00, 0x70, 0x00, - 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x2c, 0x00, 0x00, - 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x33, 0x00, 0x00, 0x66, 0x00, 0x00, 0x99, 0x00, 0x00, 0xcc, 0x00, 0x00, - 0xff, 0x00, 0x2b, 0x00, 0x00, 0x2b, 0x33, 0x00, 0x2b, 0x66, 0x00, 0x2b, - 0x99, 0x00, 0x2b, 0xcc, 0x00, 0x2b, 0xff, 0x00, 0x55, 0x00, 0x00, 0x55, - 0x33, 0x00, 0x55, 0x66, 0x00, 0x55, 0x99, 0x00, 0x55, 0xcc, 0x00, 0x55, - 0xff, 0x00, 0x80, 0x00, 0x00, 0x80, 0x33, 0x00, 0x80, 0x66, 0x00, 0x80, - 0x99, 0x00, 0x80, 0xcc, 0x00, 0x80, 0xff, 0x00, 0xaa, 0x00, 0x00, 0xaa, - 0x33, 0x00, 0xaa, 0x66, 0x00, 0xaa, 0x99, 0x00, 0xaa, 0xcc, 0x00, 0xaa, - 0xff, 0x00, 0xd5, 0x00, 0x00, 0xd5, 0x33, 0x00, 0xd5, 0x66, 0x00, 0xd5, - 0x99, 0x00, 0xd5, 0xcc, 0x00, 0xd5, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, - 0x33, 0x00, 0xff, 0x66, 0x00, 0xff, 0x99, 0x00, 0xff, 0xcc, 0x00, 0xff, - 0xff, 0x33, 0x00, 0x00, 0x33, 0x00, 0x33, 0x33, 0x00, 0x66, 0x33, 0x00, - 0x99, 0x33, 0x00, 0xcc, 0x33, 0x00, 0xff, 0x33, 0x2b, 0x00, 0x33, 0x2b, - 0x33, 0x33, 0x2b, 0x66, 0x33, 0x2b, 0x99, 0x33, 0x2b, 0xcc, 0x33, 0x2b, - 0xff, 0x33, 0x55, 0x00, 0x33, 0x55, 0x33, 0x33, 0x55, 0x66, 0x33, 0x55, - 0x99, 0x33, 0x55, 0xcc, 0x33, 0x55, 0xff, 0x33, 0x80, 0x00, 0x33, 0x80, - 0x33, 0x33, 0x80, 0x66, 0x33, 0x80, 0x99, 0x33, 0x80, 0xcc, 0x33, 0x80, - 0xff, 0x33, 0xaa, 0x00, 0x33, 0xaa, 0x33, 0x33, 0xaa, 0x66, 0x33, 0xaa, - 0x99, 0x33, 0xaa, 0xcc, 0x33, 0xaa, 0xff, 0x33, 0xd5, 0x00, 0x33, 0xd5, - 0x33, 0x33, 0xd5, 0x66, 0x33, 0xd5, 0x99, 0x33, 0xd5, 0xcc, 0x33, 0xd5, - 0xff, 0x33, 0xff, 0x00, 0x33, 0xff, 0x33, 0x33, 0xff, 0x66, 0x33, 0xff, - 0x99, 0x33, 0xff, 0xcc, 0x33, 0xff, 0xff, 0x66, 0x00, 0x00, 0x66, 0x00, - 0x33, 0x66, 0x00, 0x66, 0x66, 0x00, 0x99, 0x66, 0x00, 0xcc, 0x66, 0x00, - 0xff, 0x66, 0x2b, 0x00, 0x66, 0x2b, 0x33, 0x66, 0x2b, 0x66, 0x66, 0x2b, - 0x99, 0x66, 0x2b, 0xcc, 0x66, 0x2b, 0xff, 0x66, 0x55, 0x00, 0x66, 0x55, - 0x33, 0x66, 0x55, 0x66, 0x66, 0x55, 0x99, 0x66, 0x55, 0xcc, 0x66, 0x55, - 0xff, 0x66, 0x80, 0x00, 0x66, 0x80, 0x33, 0x66, 0x80, 0x66, 0x66, 0x80, - 0x99, 0x66, 0x80, 0xcc, 0x66, 0x80, 0xff, 0x66, 0xaa, 0x00, 0x66, 0xaa, - 0x33, 0x66, 0xaa, 0x66, 0x66, 0xaa, 0x99, 0x66, 0xaa, 0xcc, 0x66, 0xaa, - 0xff, 0x66, 0xd5, 0x00, 0x66, 0xd5, 0x33, 0x66, 0xd5, 0x66, 0x66, 0xd5, - 0x99, 0x66, 0xd5, 0xcc, 0x66, 0xd5, 0xff, 0x66, 0xff, 0x00, 0x66, 0xff, - 0x33, 0x66, 0xff, 0x66, 0x66, 0xff, 0x99, 0x66, 0xff, 0xcc, 0x66, 0xff, - 0xff, 0x99, 0x00, 0x00, 0x99, 0x00, 0x33, 0x99, 0x00, 0x66, 0x99, 0x00, - 0x99, 0x99, 0x00, 0xcc, 0x99, 0x00, 0xff, 0x99, 0x2b, 0x00, 0x99, 0x2b, - 0x33, 0x99, 0x2b, 0x66, 0x99, 0x2b, 0x99, 0x99, 0x2b, 0xcc, 0x99, 0x2b, - 0xff, 0x99, 0x55, 0x00, 0x99, 0x55, 0x33, 0x99, 0x55, 0x66, 0x99, 0x55, - 0x99, 0x99, 0x55, 0xcc, 0x99, 0x55, 0xff, 0x99, 0x80, 0x00, 0x99, 0x80, - 0x33, 0x99, 0x80, 0x66, 0x99, 0x80, 0x99, 0x99, 0x80, 0xcc, 0x99, 0x80, - 0xff, 0x99, 0xaa, 0x00, 0x99, 0xaa, 0x33, 0x99, 0xaa, 0x66, 0x99, 0xaa, - 0x99, 0x99, 0xaa, 0xcc, 0x99, 0xaa, 0xff, 0x99, 0xd5, 0x00, 0x99, 0xd5, - 0x33, 0x99, 0xd5, 0x66, 0x99, 0xd5, 0x99, 0x99, 0xd5, 0xcc, 0x99, 0xd5, - 0xff, 0x99, 0xff, 0x00, 0x99, 0xff, 0x33, 0x99, 0xff, 0x66, 0x99, 0xff, - 0x99, 0x99, 0xff, 0xcc, 0x99, 0xff, 0xff, 0xcc, 0x00, 0x00, 0xcc, 0x00, - 0x33, 0xcc, 0x00, 0x66, 0xcc, 0x00, 0x99, 0xcc, 0x00, 0xcc, 0xcc, 0x00, - 0xff, 0xcc, 0x2b, 0x00, 0xcc, 0x2b, 0x33, 0xcc, 0x2b, 0x66, 0xcc, 0x2b, - 0x99, 0xcc, 0x2b, 0xcc, 0xcc, 0x2b, 0xff, 0xcc, 0x55, 0x00, 0xcc, 0x55, - 0x33, 0xcc, 0x55, 0x66, 0xcc, 0x55, 0x99, 0xcc, 0x55, 0xcc, 0xcc, 0x55, - 0xff, 0xcc, 0x80, 0x00, 0xcc, 0x80, 0x33, 0xcc, 0x80, 0x66, 0xcc, 0x80, - 0x99, 0xcc, 0x80, 0xcc, 0xcc, 0x80, 0xff, 0xcc, 0xaa, 0x00, 0xcc, 0xaa, - 0x33, 0xcc, 0xaa, 0x66, 0xcc, 0xaa, 0x99, 0xcc, 0xaa, 0xcc, 0xcc, 0xaa, - 0xff, 0xcc, 0xd5, 0x00, 0xcc, 0xd5, 0x33, 0xcc, 0xd5, 0x66, 0xcc, 0xd5, - 0x99, 0xcc, 0xd5, 0xcc, 0xcc, 0xd5, 0xff, 0xcc, 0xff, 0x00, 0xcc, 0xff, - 0x33, 0xcc, 0xff, 0x66, 0xcc, 0xff, 0x99, 0xcc, 0xff, 0xcc, 0xcc, 0xff, - 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0x33, 0xff, 0x00, 0x66, 0xff, 0x00, - 0x99, 0xff, 0x00, 0xcc, 0xff, 0x00, 0xff, 0xff, 0x2b, 0x00, 0xff, 0x2b, - 0x33, 0xff, 0x2b, 0x66, 0xff, 0x2b, 0x99, 0xff, 0x2b, 0xcc, 0xff, 0x2b, - 0xff, 0xff, 0x55, 0x00, 0xff, 0x55, 0x33, 0xff, 0x55, 0x66, 0xff, 0x55, - 0x99, 0xff, 0x55, 0xcc, 0xff, 0x55, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, - 0x33, 0xff, 0x80, 0x66, 0xff, 0x80, 0x99, 0xff, 0x80, 0xcc, 0xff, 0x80, - 0xff, 0xff, 0xaa, 0x00, 0xff, 0xaa, 0x33, 0xff, 0xaa, 0x66, 0xff, 0xaa, - 0x99, 0xff, 0xaa, 0xcc, 0xff, 0xaa, 0xff, 0xff, 0xd5, 0x00, 0xff, 0xd5, - 0x33, 0xff, 0xd5, 0x66, 0xff, 0xd5, 0x99, 0xff, 0xd5, 0xcc, 0xff, 0xd5, - 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x33, 0xff, 0xff, 0x66, 0xff, 0xff, - 0x99, 0xff, 0xff, 0xcc, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0x00, 0xe7, 0xe9, - 0x13, 0x48, 0x70, 0xa0, 0xc1, 0x82, 0x08, 0x0f, 0xea, 0xd3, 0x97, 0x8c, - 0x5e, 0x32, 0x86, 0x0e, 0x21, 0x3e, 0x6c, 0x38, 0x31, 0x22, 0xc5, 0x7c, - 0xca, 0x04, 0x26, 0xc3, 0x38, 0x8f, 0x23, 0xc6, 0x7c, 0x0d, 0x41, 0xd2, - 0x03, 0xc9, 0x50, 0xe3, 0x48, 0x88, 0x1d, 0x51, 0x4d, 0xc3, 0x96, 0xea, - 0x5c, 0x3e, 0x7a, 0xf2, 0x88, 0x98, 0x72, 0x47, 0x86, 0x86, 0x0e, 0x9b, - 0x3a, 0xde, 0x0c, 0xcc, 0x38, 0x0f, 0x9a, 0x3c, 0x54, 0xee, 0xf2, 0x41, - 0xcb, 0x37, 0xad, 0x17, 0x48, 0x37, 0x9f, 0xe6, 0xe9, 0x78, 0x36, 0xb2, - 0x69, 0xc8, 0x92, 0xdf, 0xce, 0x41, 0x9b, 0x37, 0x2f, 0x99, 0x3c, 0x55, - 0xfa, 0x62, 0x42, 0xa3, 0xf9, 0xc9, 0xe1, 0x4b, 0x93, 0x0f, 0xe5, 0x51, - 0x6b, 0xc8, 0x54, 0x9f, 0xb2, 0x7c, 0x54, 0x89, 0x80, 0x9a, 0x47, 0x06, - 0xd4, 0xd7, 0xa9, 0x4c, 0xbf, 0x0a, 0x03, 0xf7, 0x75, 0xe3, 0x3c, 0x65, - 0xfa, 0x90, 0xb9, 0x99, 0x59, 0x53, 0xc7, 0x4d, 0x20, 0xf3, 0xe8, 0x31, - 0x9d, 0xf7, 0x8c, 0x1d, 0xac, 0x88, 0xdf, 0x52, 0x65, 0x4b, 0x45, 0x15, - 0xa9, 0xd2, 0x67, 0x81, 0xd1, 0x42, 0xcc, 0x37, 0xf0, 0x2a, 0xe5, 0x8d, - 0x59, 0xa7, 0x29, 0xd3, 0xca, 0x15, 0x62, 0x5c, 0x8a, 0x0c, 0xa3, 0x26, - 0x0b, 0xbc, 0xf1, 0x55, 0xbb, 0x7c, 0x44, 0x3e, 0x29, 0x23, 0xd3, 0xb0, - 0x27, 0xe9, 0x93, 0x54, 0x91, 0x4d, 0x6b, 0x37, 0x35, 0xdf, 0xab, 0x69, - 0xf4, 0xf4, 0x6e, 0x1d, 0xe3, 0xf6, 0xac, 0x6b, 0x86, 0x64, 0x47, 0xd6, - 0x92, 0x96, 0x6a, 0x1a, 0xb8, 0xa1, 0xf3, 0x64, 0xce, 0xf3, 0x62, 0xca, - 0x24, 0xc7, 0x85, 0x81, 0x21, 0x8f, 0x1c, 0x9a, 0x51, 0x30, 0xbd, 0xbb, - 0x68, 0x23, 0x56, 0x1d, 0x38, 0xfa, 0x21, 0xf6, 0x8c, 0x03, 0xa7, 0xba, - 0x11, 0x7e, 0xf9, 0x79, 0x24, 0xf8, 0x9d, 0x5f, 0x4b, 0x86, 0x7f, 0x3d, - 0xf8, 0xac, 0x76, 0x65, 0x01, 0x01, 0x00, 0x3b - }; +// tiny_gif.gif - Small GIF image (actual file data from xxd) +static const std::vector gifData = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x1a, 0x00, 0x10, 0x00, 0x70, 0x00, + 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x2c, 0x00, 0x00, + 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x66, 0x00, 0x00, 0x99, 0x00, 0x00, 0xcc, 0x00, 0x00, + 0xff, 0x00, 0x2b, 0x00, 0x00, 0x2b, 0x33, 0x00, 0x2b, 0x66, 0x00, 0x2b, + 0x99, 0x00, 0x2b, 0xcc, 0x00, 0x2b, 0xff, 0x00, 0x55, 0x00, 0x00, 0x55, + 0x33, 0x00, 0x55, 0x66, 0x00, 0x55, 0x99, 0x00, 0x55, 0xcc, 0x00, 0x55, + 0xff, 0x00, 0x80, 0x00, 0x00, 0x80, 0x33, 0x00, 0x80, 0x66, 0x00, 0x80, + 0x99, 0x00, 0x80, 0xcc, 0x00, 0x80, 0xff, 0x00, 0xaa, 0x00, 0x00, 0xaa, + 0x33, 0x00, 0xaa, 0x66, 0x00, 0xaa, 0x99, 0x00, 0xaa, 0xcc, 0x00, 0xaa, + 0xff, 0x00, 0xd5, 0x00, 0x00, 0xd5, 0x33, 0x00, 0xd5, 0x66, 0x00, 0xd5, + 0x99, 0x00, 0xd5, 0xcc, 0x00, 0xd5, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, + 0x33, 0x00, 0xff, 0x66, 0x00, 0xff, 0x99, 0x00, 0xff, 0xcc, 0x00, 0xff, + 0xff, 0x33, 0x00, 0x00, 0x33, 0x00, 0x33, 0x33, 0x00, 0x66, 0x33, 0x00, + 0x99, 0x33, 0x00, 0xcc, 0x33, 0x00, 0xff, 0x33, 0x2b, 0x00, 0x33, 0x2b, + 0x33, 0x33, 0x2b, 0x66, 0x33, 0x2b, 0x99, 0x33, 0x2b, 0xcc, 0x33, 0x2b, + 0xff, 0x33, 0x55, 0x00, 0x33, 0x55, 0x33, 0x33, 0x55, 0x66, 0x33, 0x55, + 0x99, 0x33, 0x55, 0xcc, 0x33, 0x55, 0xff, 0x33, 0x80, 0x00, 0x33, 0x80, + 0x33, 0x33, 0x80, 0x66, 0x33, 0x80, 0x99, 0x33, 0x80, 0xcc, 0x33, 0x80, + 0xff, 0x33, 0xaa, 0x00, 0x33, 0xaa, 0x33, 0x33, 0xaa, 0x66, 0x33, 0xaa, + 0x99, 0x33, 0xaa, 0xcc, 0x33, 0xaa, 0xff, 0x33, 0xd5, 0x00, 0x33, 0xd5, + 0x33, 0x33, 0xd5, 0x66, 0x33, 0xd5, 0x99, 0x33, 0xd5, 0xcc, 0x33, 0xd5, + 0xff, 0x33, 0xff, 0x00, 0x33, 0xff, 0x33, 0x33, 0xff, 0x66, 0x33, 0xff, + 0x99, 0x33, 0xff, 0xcc, 0x33, 0xff, 0xff, 0x66, 0x00, 0x00, 0x66, 0x00, + 0x33, 0x66, 0x00, 0x66, 0x66, 0x00, 0x99, 0x66, 0x00, 0xcc, 0x66, 0x00, + 0xff, 0x66, 0x2b, 0x00, 0x66, 0x2b, 0x33, 0x66, 0x2b, 0x66, 0x66, 0x2b, + 0x99, 0x66, 0x2b, 0xcc, 0x66, 0x2b, 0xff, 0x66, 0x55, 0x00, 0x66, 0x55, + 0x33, 0x66, 0x55, 0x66, 0x66, 0x55, 0x99, 0x66, 0x55, 0xcc, 0x66, 0x55, + 0xff, 0x66, 0x80, 0x00, 0x66, 0x80, 0x33, 0x66, 0x80, 0x66, 0x66, 0x80, + 0x99, 0x66, 0x80, 0xcc, 0x66, 0x80, 0xff, 0x66, 0xaa, 0x00, 0x66, 0xaa, + 0x33, 0x66, 0xaa, 0x66, 0x66, 0xaa, 0x99, 0x66, 0xaa, 0xcc, 0x66, 0xaa, + 0xff, 0x66, 0xd5, 0x00, 0x66, 0xd5, 0x33, 0x66, 0xd5, 0x66, 0x66, 0xd5, + 0x99, 0x66, 0xd5, 0xcc, 0x66, 0xd5, 0xff, 0x66, 0xff, 0x00, 0x66, 0xff, + 0x33, 0x66, 0xff, 0x66, 0x66, 0xff, 0x99, 0x66, 0xff, 0xcc, 0x66, 0xff, + 0xff, 0x99, 0x00, 0x00, 0x99, 0x00, 0x33, 0x99, 0x00, 0x66, 0x99, 0x00, + 0x99, 0x99, 0x00, 0xcc, 0x99, 0x00, 0xff, 0x99, 0x2b, 0x00, 0x99, 0x2b, + 0x33, 0x99, 0x2b, 0x66, 0x99, 0x2b, 0x99, 0x99, 0x2b, 0xcc, 0x99, 0x2b, + 0xff, 0x99, 0x55, 0x00, 0x99, 0x55, 0x33, 0x99, 0x55, 0x66, 0x99, 0x55, + 0x99, 0x99, 0x55, 0xcc, 0x99, 0x55, 0xff, 0x99, 0x80, 0x00, 0x99, 0x80, + 0x33, 0x99, 0x80, 0x66, 0x99, 0x80, 0x99, 0x99, 0x80, 0xcc, 0x99, 0x80, + 0xff, 0x99, 0xaa, 0x00, 0x99, 0xaa, 0x33, 0x99, 0xaa, 0x66, 0x99, 0xaa, + 0x99, 0x99, 0xaa, 0xcc, 0x99, 0xaa, 0xff, 0x99, 0xd5, 0x00, 0x99, 0xd5, + 0x33, 0x99, 0xd5, 0x66, 0x99, 0xd5, 0x99, 0x99, 0xd5, 0xcc, 0x99, 0xd5, + 0xff, 0x99, 0xff, 0x00, 0x99, 0xff, 0x33, 0x99, 0xff, 0x66, 0x99, 0xff, + 0x99, 0x99, 0xff, 0xcc, 0x99, 0xff, 0xff, 0xcc, 0x00, 0x00, 0xcc, 0x00, + 0x33, 0xcc, 0x00, 0x66, 0xcc, 0x00, 0x99, 0xcc, 0x00, 0xcc, 0xcc, 0x00, + 0xff, 0xcc, 0x2b, 0x00, 0xcc, 0x2b, 0x33, 0xcc, 0x2b, 0x66, 0xcc, 0x2b, + 0x99, 0xcc, 0x2b, 0xcc, 0xcc, 0x2b, 0xff, 0xcc, 0x55, 0x00, 0xcc, 0x55, + 0x33, 0xcc, 0x55, 0x66, 0xcc, 0x55, 0x99, 0xcc, 0x55, 0xcc, 0xcc, 0x55, + 0xff, 0xcc, 0x80, 0x00, 0xcc, 0x80, 0x33, 0xcc, 0x80, 0x66, 0xcc, 0x80, + 0x99, 0xcc, 0x80, 0xcc, 0xcc, 0x80, 0xff, 0xcc, 0xaa, 0x00, 0xcc, 0xaa, + 0x33, 0xcc, 0xaa, 0x66, 0xcc, 0xaa, 0x99, 0xcc, 0xaa, 0xcc, 0xcc, 0xaa, + 0xff, 0xcc, 0xd5, 0x00, 0xcc, 0xd5, 0x33, 0xcc, 0xd5, 0x66, 0xcc, 0xd5, + 0x99, 0xcc, 0xd5, 0xcc, 0xcc, 0xd5, 0xff, 0xcc, 0xff, 0x00, 0xcc, 0xff, + 0x33, 0xcc, 0xff, 0x66, 0xcc, 0xff, 0x99, 0xcc, 0xff, 0xcc, 0xcc, 0xff, + 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0x33, 0xff, 0x00, 0x66, 0xff, 0x00, + 0x99, 0xff, 0x00, 0xcc, 0xff, 0x00, 0xff, 0xff, 0x2b, 0x00, 0xff, 0x2b, + 0x33, 0xff, 0x2b, 0x66, 0xff, 0x2b, 0x99, 0xff, 0x2b, 0xcc, 0xff, 0x2b, + 0xff, 0xff, 0x55, 0x00, 0xff, 0x55, 0x33, 0xff, 0x55, 0x66, 0xff, 0x55, + 0x99, 0xff, 0x55, 0xcc, 0xff, 0x55, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, + 0x33, 0xff, 0x80, 0x66, 0xff, 0x80, 0x99, 0xff, 0x80, 0xcc, 0xff, 0x80, + 0xff, 0xff, 0xaa, 0x00, 0xff, 0xaa, 0x33, 0xff, 0xaa, 0x66, 0xff, 0xaa, + 0x99, 0xff, 0xaa, 0xcc, 0xff, 0xaa, 0xff, 0xff, 0xd5, 0x00, 0xff, 0xd5, + 0x33, 0xff, 0xd5, 0x66, 0xff, 0xd5, 0x99, 0xff, 0xd5, 0xcc, 0xff, 0xd5, + 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x33, 0xff, 0xff, 0x66, 0xff, 0xff, + 0x99, 0xff, 0xff, 0xcc, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0x00, 0xe7, 0xe9, + 0x13, 0x48, 0x70, 0xa0, 0xc1, 0x82, 0x08, 0x0f, 0xea, 0xd3, 0x97, 0x8c, + 0x5e, 0x32, 0x86, 0x0e, 0x21, 0x3e, 0x6c, 0x38, 0x31, 0x22, 0xc5, 0x7c, + 0xca, 0x04, 0x26, 0xc3, 0x38, 0x8f, 0x23, 0xc6, 0x7c, 0x0d, 0x41, 0xd2, + 0x03, 0xc9, 0x50, 0xe3, 0x48, 0x88, 0x1d, 0x51, 0x4d, 0xc3, 0x96, 0xea, + 0x5c, 0x3e, 0x7a, 0xf2, 0x88, 0x98, 0x72, 0x47, 0x86, 0x86, 0x0e, 0x9b, + 0x3a, 0xde, 0x0c, 0xcc, 0x38, 0x0f, 0x9a, 0x3c, 0x54, 0xee, 0xf2, 0x41, + 0xcb, 0x37, 0xad, 0x17, 0x48, 0x37, 0x9f, 0xe6, 0xe9, 0x78, 0x36, 0xb2, + 0x69, 0xc8, 0x92, 0xdf, 0xce, 0x41, 0x9b, 0x37, 0x2f, 0x99, 0x3c, 0x55, + 0xfa, 0x62, 0x42, 0xa3, 0xf9, 0xc9, 0xe1, 0x4b, 0x93, 0x0f, 0xe5, 0x51, + 0x6b, 0xc8, 0x54, 0x9f, 0xb2, 0x7c, 0x54, 0x89, 0x80, 0x9a, 0x47, 0x06, + 0xd4, 0xd7, 0xa9, 0x4c, 0xbf, 0x0a, 0x03, 0xf7, 0x75, 0xe3, 0x3c, 0x65, + 0xfa, 0x90, 0xb9, 0x99, 0x59, 0x53, 0xc7, 0x4d, 0x20, 0xf3, 0xe8, 0x31, + 0x9d, 0xf7, 0x8c, 0x1d, 0xac, 0x88, 0xdf, 0x52, 0x65, 0x4b, 0x45, 0x15, + 0xa9, 0xd2, 0x67, 0x81, 0xd1, 0x42, 0xcc, 0x37, 0xf0, 0x2a, 0xe5, 0x8d, + 0x59, 0xa7, 0x29, 0xd3, 0xca, 0x15, 0x62, 0x5c, 0x8a, 0x0c, 0xa3, 0x26, + 0x0b, 0xbc, 0xf1, 0x55, 0xbb, 0x7c, 0x44, 0x3e, 0x29, 0x23, 0xd3, 0xb0, + 0x27, 0xe9, 0x93, 0x54, 0x91, 0x4d, 0x6b, 0x37, 0x35, 0xdf, 0xab, 0x69, + 0xf4, 0xf4, 0x6e, 0x1d, 0xe3, 0xf6, 0xac, 0x6b, 0x86, 0x64, 0x47, 0xd6, + 0x92, 0x96, 0x6a, 0x1a, 0xb8, 0xa1, 0xf3, 0x64, 0xce, 0xf3, 0x62, 0xca, + 0x24, 0xc7, 0x85, 0x81, 0x21, 0x8f, 0x1c, 0x9a, 0x51, 0x30, 0xbd, 0xbb, + 0x68, 0x23, 0x56, 0x1d, 0x38, 0xfa, 0x21, 0xf6, 0x8c, 0x03, 0xa7, 0xba, + 0x11, 0x7e, 0xf9, 0x79, 0x24, 0xf8, 0x9d, 0x5f, 0x4b, 0x86, 0x7f, 0x3d, + 0xf8, 0xac, 0x76, 0x65, 0x01, 0x01, 0x00, 0x3b +}; - // Create XLImage objects for each image format - XLImage image1(pngData, ImageMimeTypes::PNG); - XLImage image2(jpegData, ImageMimeTypes::JPEG); - XLImage image3(bmpData, ImageMimeTypes::BMP); - XLImage image4(gifData, ImageMimeTypes::GIF); - // Demonstrate size control - std::cout << "\n2. Demonstrating size control..." << std::endl; - - // Set custom display sizes using aspect ratio preservation - // Note: EMU-to-pixel ratio may vary based on screen resolution. - // Standard Excel cell height = 190500 EMUs (15 points × 12700 EMUs/point) - // Cell vertical spacing = 200000 ~= × (26/25) EMUs (25 pixels white + 1 pixel divider) - // This allows sizing images to exact Excel cell heights: 1 cell ~= 200000 EMUs - constexpr uint32_t EXCEL_CELL_Y_SPACING_EMUS = 200000; - - // Set maximum dimensions and let the method preserve aspect ratio - image1.setDisplaySizePixelsWithAspectRatio(50, 30); // Max 50×30 pixels, preserve aspect ratio - image2.setDisplaySizePixelsWithAspectRatio(40, 40); // Max 40×40 pixels, preserve aspect ratio - image3.setDisplaySizePixelsWithAspectRatio(60, 20); // Max 60×20 pixels, preserve aspect ratio - image4.setDisplaySizePixelsWithAspectRatio(70,70); // Max 35×35 pixels, preserve aspect ratio +int main() +{ + std::cout << "OpenXLSX Comprehensive Image Test Matrix" << std::endl; + std::cout << "=========================================" << std::endl; - std::cout << "Image 1 new display size: " << image1.displayWidth() << "x" << image1.displayHeight() << " EMUs" << std::endl; - std::cout << "Image 2 new display size: " << image2.displayWidth() << "x" << image2.displayHeight() << " EMUs" << std::endl; - std::cout << "Image 3 new display size: " << image3.displayWidth() << "x" << image3.displayHeight() << " EMUs" << std::endl; - std::cout << "Image 4 new display size: " << image4.displayWidth() << "x" << image4.displayHeight() << " EMUs" << std::endl; + // Create workbook and document + XLDocument doc; + std::string xlsxFileName = "Demo11.xlsx"; + doc.create(xlsxFileName); + auto wb = doc.workbook(); - // Demonstrate MIME type constants - std::cout << "\n3. MIME type constants:" << std::endl; - std::cout << " PNG: " << ImageMimeTypes::PNG << std::endl; - std::cout << " JPEG: " << ImageMimeTypes::JPEG << std::endl; - std::cout << " BMP: " << ImageMimeTypes::BMP << std::endl; - std::cout << " GIF: " << ImageMimeTypes::GIF << std::endl; + // Create test worksheets + wb.addWorksheet("Anchor Types"); + XLWorksheet anchorSheet = wb.worksheet("Anchor Types"); + wb.addWorksheet("File Formats"); + XLWorksheet formatSheet = wb.worksheet("File Formats"); + wb.addWorksheet("Sizing Strategies"); + XLWorksheet sizingSheet = wb.worksheet("Sizing Strategies"); + wb.addWorksheet("Units & Scaling"); + XLWorksheet unitsSheet = wb.worksheet("Units & Scaling"); - // Demonstrate worksheet image functionality - std::cout << "\n4. Demonstrating worksheet image functionality..." << std::endl; + std::cout << "\n=== Sheet 1: Anchor Types Comparison ===" << std::endl; - // Add images to the worksheet and assign final image IDs - bool success1 = wks.addImage(image1, "C4"); - bool success2 = wks.addImage(image2, "C6"); - bool success3 = wks.addImage(image3, "C8"); - bool success4 = wks.addImage(image4, "C10"); + // Test 1: oneCellAnchor vs twoCellAnchor with same image + XLImage testImage1(pngData, ImageMimeTypes::PNG); + testImage1.setDisplaySizeWithAspectRatio(2 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); - // Add some data to the worksheet (now showing the actual assigned IDs) - wks.cell("B4").value() = "PNG Image - " + image1.id(); - wks.cell("B6").value() = "JPEG Image - " + image2.id(); - wks.cell("B8").value() = "BMP Image - " + image3.id(); - wks.cell("B10").value() = "GIF Image - " + image4.id(); + // Add headers + anchorSheet.cell("A1").value() = "oneCellAnchor (PNG, preserve aspect ratio within 2x2 cell height bounding box)"; + anchorSheet.cell("A7").value() = "twoCellAnchor (PNG, 4x3 cells)"; + anchorSheet.cell("A13").value() = "oneCellAnchor (JPEG, original size)"; + anchorSheet.cell("A19").value() = "twoCellAnchor (JPEG, 3x2 cells)"; - std::cout << " Added image1 (PNG) to C4: " << (success1 ? "Success" : "Failed") << std::endl; - std::cout << " Added image2 (JPEG) to C6: " << (success2 ? "Success" : "Failed") << std::endl; - std::cout << " Added image3 (BMP) to C8: " << (success3 ? "Success" : "Failed") << std::endl; - std::cout << " Added image4 (GIF) to C10: " << (success4 ? "Success" : "Failed") << std::endl; + // Add images + bool success1 = anchorSheet.addImage(testImage1, "A2"); + std::cout << " Added oneCellAnchor PNG: " << (success1 ? "Success" : "Failed") << std::endl; - // Display image information (showing the actual assigned IDs from worksheet) - std::cout << "\nImage information after adding to worksheet:" << std::endl; - std::cout << " Note: Original image objects still show temporary IDs, but the worksheet" << std::endl; - std::cout << " stores copies with proper global IDs (img1, img2, img3, img4)" << std::endl; + // Add twoCellAnchor image + bool success2 = anchorSheet.addImageTwoCellAnchor(testImage1, 8, 1, 10, 4); // A8 to D10 + std::cout << " Added twoCellAnchor PNG: " << (success2 ? "Success" : "Failed") << std::endl; - std::cout << "\nImage 1 (PNG):" << std::endl; - std::cout << " MIME Type: " << image1.mimeType() << std::endl; - std::cout << " Extension: " << image1.extension() << std::endl; - std::cout << " ID: " << image1.id() << " (temporary - actual ID in worksheet: img1)" << std::endl; - std::cout << " Dimensions: " << image1.widthPixels() << "x" << image1.heightPixels() << " pixels" << std::endl; - std::cout << " Display Size: " << image1.displayWidth() << "x" << image1.displayHeight() << " EMUs" << std::endl; - std::cout << " Valid: " << (image1.isValid() ? "Yes" : "No") << std::endl; - std::cout << " Content Type: " << XLContentTypeString(image1.contentType()) << std::endl; - - std::cout << "\nImage 2 (JPEG):" << std::endl; - std::cout << " MIME Type: " << image2.mimeType() << std::endl; - std::cout << " Extension: " << image2.extension() << std::endl; - std::cout << " ID: " << image2.id() << " (temporary - actual ID in worksheet: img2)" << std::endl; - std::cout << " Dimensions: " << image2.widthPixels() << "x" << image2.heightPixels() << " pixels" << std::endl; - std::cout << " Display Size: " << image2.displayWidth() << "x" << image2.displayHeight() << " EMUs" << std::endl; - std::cout << " Valid: " << (image2.isValid() ? "Yes" : "No") << std::endl; - std::cout << " Content Type: " << XLContentTypeString(image2.contentType()) << std::endl; + // Add more twoCellAnchor tests + XLImage testImage2(jpegData, ImageMimeTypes::JPEG); + bool success3 = anchorSheet.addImage(testImage2, "A14"); + std::cout << " Added oneCellAnchor JPEG: " << (success3 ? "Success" : "Failed") << std::endl; + + bool success4 = anchorSheet.addImageTwoCellAnchor(testImage2, 20, 1, 21, 3); // A20 to C21 + std::cout << " Added twoCellAnchor JPEG: " << (success4 ? "Success" : "Failed") << std::endl; - std::cout << "\nImage 3 (BMP):" << std::endl; - std::cout << " MIME Type: " << image3.mimeType() << std::endl; - std::cout << " Extension: " << image3.extension() << std::endl; - std::cout << " ID: " << image3.id() << " (temporary - actual ID in worksheet: img3)" << std::endl; - std::cout << " Dimensions: " << image3.widthPixels() << "x" << image3.heightPixels() << " pixels" << std::endl; - std::cout << " Display Size: " << image3.displayWidth() << "x" << image3.displayHeight() << " EMUs" << std::endl; - std::cout << " Valid: " << (image3.isValid() ? "Yes" : "No") << std::endl; - std::cout << " Content Type: " << XLContentTypeString(image3.contentType()) << std::endl; + std::cout << "\n=== Sheet 2: File Format Testing ===" << std::endl; + + // Test different file formats + static const std::vector, std::string, std::string>> formats = { + {pngData, "PNG", ImageMimeTypes::PNG}, + {jpegData, "JPEG", ImageMimeTypes::JPEG}, + {gifData, "GIF", ImageMimeTypes::GIF}, + {bmpData, "BMP", ImageMimeTypes::BMP} + }; + + for (size_t i = 0; i < formats.size(); ++i) { + XLImage formatImage(std::get<0>(formats[i]), std::get<2>(formats[i])); + formatImage.setDisplaySizeWithAspectRatio(4 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); + + std::string textCell = "A" + std::to_string(1 + i * 6); + std::string imageCell = "A" + std::to_string(2 + i * 6); + formatSheet.cell(textCell).value() = std::get<1>(formats[i]) + " Format Test"; + + bool success = formatSheet.addImage(formatImage, imageCell); + std::cout << " Added " << std::get<1>(formats[i]) << " image: " << (success ? "Success" : "Failed") << std::endl; + } - std::cout << "\nImage 4 (GIF):" << std::endl; - std::cout << " MIME Type: " << image4.mimeType() << std::endl; - std::cout << " Extension: " << image4.extension() << std::endl; - std::cout << " ID: " << image4.id() << " (temporary - actual ID in worksheet: img4)" << std::endl; - std::cout << " Dimensions: " << image4.widthPixels() << "x" << image4.heightPixels() << " pixels" << std::endl; - std::cout << " Display Size: " << image4.displayWidth() << "x" << image4.displayHeight() << " EMUs" << std::endl; - std::cout << " Valid: " << (image4.isValid() ? "Yes" : "No") << std::endl; - std::cout << " Content Type: " << XLContentTypeString(image4.contentType()) << std::endl; + std::cout << "\n=== Sheet 3: Sizing Strategy Testing ===" << std::endl; - // Test image count and status - std::cout << " Total images in worksheet: " << wks.imageCount() << std::endl; - std::cout << " Worksheet has images: " << (wks.hasImages() ? "Yes" : "No") << std::endl; + // Test different sizing strategies + XLImage sizingImage(pngData, ImageMimeTypes::PNG); - // Test addImageFromFile (this will fail since we don't have actual image files) - std::cout << "\n5. Testing addImageFromFile (will fail - no actual files):" << std::endl; - bool fileSuccess = wks.addImageFromFile("nonexistent.png", "D1"); - std::cout << " Added image from file: " << (fileSuccess ? "Success" : "Failed (expected)") << std::endl; + // Strategy 1: Preserve aspect ratio + bounding box + sizingImage.setDisplaySizeWithAspectRatio(7 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); + sizingSheet.cell("A1").value() = "Aspect Ratio + Bounding Box (7x3 cell height units)"; + bool success3a = sizingSheet.addImage(sizingImage, "A2"); + std::cout << " Aspect ratio + bounding box: " << (success3a ? "Success" : "Failed") << std::endl; + + // Strategy 2: Original size (no scaling) + XLImage originalImage(pngData, ImageMimeTypes::PNG); + // Don't call setDisplaySize - use original dimensions + sizingSheet.cell("A7").value() = "Original Size (no scaling)"; + bool success3b = sizingSheet.addImage(originalImage, "A8"); + std::cout << " Original size: " << (success3b ? "Success" : "Failed") << std::endl; + + // Strategy 3: Force exact dimensions + XLImage exactImage(pngData, ImageMimeTypes::PNG); + exactImage.setDisplayWidth(11 * EXCEL_CELL_Y_SPACING_EMUS); + exactImage.setDisplayHeight(3 * EXCEL_CELL_Y_SPACING_EMUS); + sizingSheet.cell("A13").value() = "Exact Dimensions (11x3 cell height units, may distort)"; + bool success3c = sizingSheet.addImage(exactImage, "A14"); + std::cout << " Exact dimensions: " << (success3c ? "Success" : "Failed") << std::endl; - // Test single-cell image with specific pixel dimensions - std::cout << "\n6. Testing single-cell image preserving aspect ratio, limiting height to about 3 cells:" << std::endl; + std::cout << "\n=== Sheet 4: Units & Scaling Testing ===" << std::endl; + + // Test different unit systems + XLImage unitsImage(pngData, ImageMimeTypes::PNG); - // Create a new image for single-cell test - XLImage image6(pngData, ImageMimeTypes::PNG); + // Pixels + unitsImage.setDisplaySizePixelsWithAspectRatio(100, 50); + unitsSheet.cell("A1").value() = "Pixel-based (100x50 pixels)"; + bool success4a = unitsSheet.addImage(unitsImage, "A2"); + std::cout << " Pixel-based sizing: " << (success4a ? "Success" : "Failed") << std::endl; - // Set specific EMU dimensions, max ~3 cells high, with aspect ratio preservation - // Note: For cell-based sizing, use 200000 EMUs per Excel cell height - image6.setDisplaySizeWithAspectRatio(12 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); // Max 3 cells high + // EMUs + XLImage emuImage(pngData, ImageMimeTypes::PNG); + emuImage.setDisplayWidth(476250); // 50 pixels in EMUs + emuImage.setDisplayHeight(238125); // 25 pixels in EMUs + unitsSheet.cell("A7").value() = "EMU-based (476250x238125 EMUs)"; + bool success4b = unitsSheet.addImage(emuImage, "A8"); + std::cout << " EMU-based sizing: " << (success4b ? "Success" : "Failed") << std::endl; - // Add to cell E12 (single cell, not spanning) - bool image6Success = wks.addImage(image6, "E12"); - std::cout << " Added image6 (3 rows high) to E12: " << (image6Success ? "Success" : "Failed") << std::endl; + // Cell spacing + XLImage cellImage(pngData, ImageMimeTypes::PNG); + cellImage.setDisplaySizeWithAspectRatio(19 * EXCEL_CELL_Y_SPACING_EMUS, 5 * EXCEL_CELL_Y_SPACING_EMUS); + unitsSheet.cell("A13").value() = "Cell-based (19x5 cell height units) -- preserve aspect ratio"; + bool success4c = unitsSheet.addImage(cellImage, "A14"); + std::cout << " Cell-based sizing: " << (success4c ? "Success" : "Failed") << std::endl; // Save the workbook doc.save(); - std::cout << "\nWorkbook saved as 'Demo11.xlsx'" << std::endl; - std::cout << "\nNote: This demo shows the XLImage class and worksheet integration." << std::endl; - std::cout << " Images are embedded in the Excel file with proper aspect ratio preservation." << std::endl; - std::cout << " The .xlsx file can be opened in Excel, OpenOffice Calc, and other compatible applications." << std::endl; + std::cout << "\nWorkbook saved as '" << xlsxFileName << "'" << std::endl; + std::cout << "\nTest Matrix Summary:" << std::endl; + std::cout << "- Sheet 1: Anchor Types (oneCellAnchor vs twoCellAnchor)" << std::endl; + std::cout << "- Sheet 2: File Formats (PNG, JPEG, GIF, BMP)" << std::endl; + std::cout << "- Sheet 3: Sizing Strategies (aspect ratio, original, exact)" << std::endl; + std::cout << "- Sheet 4: Units & Scaling (pixels, EMUs, cells)" << std::endl; + std::cout << "\nNote: Both oneCellAnchor and twoCellAnchor are fully implemented and tested." << std::endl; return 0; } diff --git a/OpenXLSX/headers/XLImage.hpp b/OpenXLSX/headers/XLImage.hpp index ac4e93f1..c17f1acd 100644 --- a/OpenXLSX/headers/XLImage.hpp +++ b/OpenXLSX/headers/XLImage.hpp @@ -56,6 +56,25 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. namespace OpenXLSX { + // ========== Excel Constants ========== // + + /** + * @brief Standard Excel cell height in EMUs (15 points × 12700 EMUs/point) + */ + constexpr uint32_t EXCEL_CELL_HEIGHT_EMUS = 190500; + + /** + * @brief Excel cell vertical spacing in EMUs (includes cell height + divider) + * This is approximately 200000 EMUs (cell height × 26/25) + */ + constexpr uint32_t EXCEL_CELL_Y_SPACING_EMUS = 200000; + + /** + * @brief Standard EMU-to-pixel conversion ratio (approximately) + * Note: This may vary based on screen resolution + */ + constexpr uint32_t EMU_TO_PIXEL_RATIO = 9525; + /** * @brief The XLImage class represents an image that can be embedded in a worksheet */ diff --git a/OpenXLSX/sources/XLDrawingML.cpp b/OpenXLSX/sources/XLDrawingML.cpp index 55a4ef8c..c9f3e07a 100644 --- a/OpenXLSX/sources/XLDrawingML.cpp +++ b/OpenXLSX/sources/XLDrawingML.cpp @@ -182,6 +182,11 @@ namespace OpenXLSX to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row).c_str()); to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); + // Create the 'ext' element (required for twoCellAnchor) + XMLNode ext = anchor.append_child("xdr:ext"); + ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); + ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); + // Create the picture element (same as original addImage method) XMLNode pic = anchor.append_child("xdr:pic"); @@ -229,6 +234,9 @@ namespace OpenXLSX XMLNode lnSolidFill = ln.append_child("a:solidFill"); XMLNode lnSrgbClr = lnSolidFill.append_child("a:srgbClr"); lnSrgbClr.append_attribute("val").set_value("000000"); + + // Add clientData element (required for Excel compatibility) + anchor.append_child("xdr:clientData"); } /** @@ -262,6 +270,11 @@ namespace OpenXLSX to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(toRow).c_str()); to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(toRowOffset).c_str()); + // Create the 'ext' element (required for twoCellAnchor) + XMLNode ext = anchor.append_child("xdr:ext"); + ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); + ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); + // Create the picture element (same as original addImage method) XMLNode pic = anchor.append_child("xdr:pic"); @@ -300,14 +313,10 @@ namespace OpenXLSX prstGeom.append_attribute("prst").set_value("rect"); prstGeom.append_child("a:avLst"); - XMLNode solidFill = spPr.append_child("a:solidFill"); - XMLNode srgbClr = solidFill.append_child("a:srgbClr"); - srgbClr.append_attribute("val").set_value("FFFFFF"); + // Use noFill to match oneCellAnchor behavior (no borders) + spPr.append_child("a:noFill"); - XMLNode ln = spPr.append_child("a:ln"); - ln.append_attribute("w").set_value("9525"); - XMLNode lnSolidFill = ln.append_child("a:solidFill"); - XMLNode lnSrgbClr = lnSolidFill.append_child("a:srgbClr"); - lnSrgbClr.append_attribute("val").set_value("000000"); + // Add clientData element (required for Excel compatibility) + anchor.append_child("xdr:clientData"); } } diff --git a/OpenXLSX/sources/XLImage.cpp b/OpenXLSX/sources/XLImage.cpp index c76ab068..290ef4fa 100644 --- a/OpenXLSX/sources/XLImage.cpp +++ b/OpenXLSX/sources/XLImage.cpp @@ -309,7 +309,7 @@ namespace OpenXLSX */ uint32_t XLImage::pixelsToEMUs(uint32_t pixels) { - return pixels * 9525; + return pixels * EMU_TO_PIXEL_RATIO; } /** @@ -453,7 +453,7 @@ namespace OpenXLSX */ uint32_t XLImage::pixelsToEmus(uint32_t pixels) { - return pixels * 9525; + return pixels * EMU_TO_PIXEL_RATIO; } /** @@ -461,7 +461,7 @@ namespace OpenXLSX */ uint32_t XLImage::emusToPixels(uint32_t emus) { - return emus / 9525; + return emus / EMU_TO_PIXEL_RATIO; } } // namespace OpenXLSX diff --git a/OpenXLSX/sources/XLSheet.cpp b/OpenXLSX/sources/XLSheet.cpp index a2b01907..fde4d531 100644 --- a/OpenXLSX/sources/XLSheet.cpp +++ b/OpenXLSX/sources/XLSheet.cpp @@ -1857,6 +1857,7 @@ bool XLWorksheet::addImage(const XLImage& image, uint32_t row, uint16_t column) std::ignore = drawingML(); // Add image to DrawingML + // Use sequential relationship ID that matches createDrawingRelationshipsFile std::string relationshipId = "rId" + std::to_string(m_images.size()); addImageToDrawingML(imageWithId, row, column, relationshipId); From 5b8a4a6e704a45f3409f5999e14dc16c29a14c0d Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Thu, 2 Oct 2025 18:13:49 -0700 Subject: [PATCH 06/13] Embedded Image Support -- phase V read images from file --- Examples/Demo11.cpp | 911 +++++++++++++++------------ OpenXLSX/headers/XLImage.hpp | 129 ++++ OpenXLSX/headers/XLRelationships.hpp | 72 +++ OpenXLSX/headers/XLSheet.hpp | 12 + OpenXLSX/sources/XLDrawingML.cpp | 11 +- OpenXLSX/sources/XLSheet.cpp | 14 +- 6 files changed, 751 insertions(+), 398 deletions(-) diff --git a/Examples/Demo11.cpp b/Examples/Demo11.cpp index c628bfa0..3a43fd09 100644 --- a/Examples/Demo11.cpp +++ b/Examples/Demo11.cpp @@ -11,405 +11,435 @@ #include #include #include +#include #include "OpenXLSX.hpp" using namespace OpenXLSX; +/** + * @brief Search upward through directory tree to find the images folder + * @param relativePath The relative path to search for (e.g., "OpenXLSX/Examples/images/") + * @return The full path to the images folder if found, empty string if not found + */ +std::string findImagesDirectory(const std::string& relativePath = "OpenXLSX/Examples/images/") +{ + const std::filesystem::path currentPath = std::filesystem::current_path(); + std::filesystem::path testPath = currentPath; + std::string result; + // Search upward through the directory tree + for (int levels = 0; (levels < 10) && result.empty(); ++levels) { + // Append the relative path + const std::filesystem::path testImagePath = testPath / relativePath; + + // Check if the directory exists + if (std::filesystem::exists(testImagePath) && + std::filesystem::is_directory(testImagePath)) { + result = testImagePath.string() + "/"; + } + else{ + testPath = testPath.parent_path(); + } + } + + return result; // Not found +} -// These contain the binary data from the tiny image files in the images directory + + + // These contain the binary data from the tiny image files in the images directory -// tiny_png.png - Small PNG image -static const std::vector pngData = { - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0f, - 0x08, 0x02, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x83, 0xf5, 0x00, 0x00, 0x00, - 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, - 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, - 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, - 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, - 0x00, 0x01, 0xa8, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4f, 0x63, 0x7c, 0xff, - 0x64, 0x03, 0x03, 0xcd, 0x00, 0x13, 0x94, 0xa6, 0x0d, 0x00, 0xbb, 0xfd, - 0xf1, 0xfe, 0x7f, 0xa1, 0x5b, 0xa1, 0x02, 0x50, 0x20, 0xc5, 0xb8, 0xba, - 0x88, 0x51, 0x96, 0x81, 0xe1, 0xfb, 0x85, 0x98, 0x6b, 0x67, 0x18, 0xa4, - 0x42, 0x96, 0x48, 0x0a, 0x40, 0x65, 0x40, 0x22, 0xef, 0xd3, 0x8c, 0x1d, - 0xed, 0xa0, 0x7c, 0xce, 0x89, 0x29, 0xc2, 0xfb, 0xa1, 0x6c, 0x06, 0xc5, - 0xe0, 0x17, 0xfd, 0x9e, 0x7f, 0xa0, 0x1c, 0x06, 0x06, 0x16, 0x28, 0xcd, - 0x60, 0xcc, 0x78, 0x22, 0x92, 0x11, 0xca, 0x66, 0x60, 0x38, 0xb2, 0xfc, - 0x5f, 0x68, 0x1f, 0x03, 0xd0, 0x02, 0x11, 0x10, 0xef, 0xfa, 0xb3, 0xf3, - 0x87, 0x24, 0xe1, 0xc6, 0x21, 0x81, 0x17, 0x7c, 0x85, 0x35, 0x7c, 0x0a, - 0xd9, 0x4f, 0x36, 0x18, 0x42, 0xf8, 0x40, 0x9b, 0x24, 0x0a, 0x19, 0x10, - 0x16, 0xe0, 0x08, 0x19, 0x1b, 0x17, 0x46, 0x8d, 0x67, 0x0c, 0x0f, 0x21, - 0x1c, 0xe5, 0x7c, 0xa9, 0xf7, 0xb3, 0x9e, 0x7f, 0x80, 0x70, 0x90, 0x00, - 0xcb, 0xba, 0x99, 0x7c, 0x0c, 0xc1, 0x2f, 0xf2, 0xa1, 0x46, 0x03, 0xc1, - 0xf7, 0xfc, 0x96, 0x4f, 0x0c, 0x6b, 0xf9, 0x4e, 0x41, 0xb9, 0xc4, 0x85, - 0xbb, 0x82, 0xa4, 0x93, 0xdb, 0xfb, 0xf3, 0x87, 0xa0, 0x3c, 0x18, 0x78, - 0xc1, 0x75, 0xf8, 0xf1, 0xb7, 0x48, 0xa4, 0x70, 0x00, 0x01, 0x89, 0x4f, - 0xfd, 0x73, 0xde, 0x99, 0x41, 0x39, 0xb8, 0x4c, 0x3f, 0xb2, 0xe7, 0xff, - 0x0d, 0x63, 0x06, 0x1b, 0x28, 0x8f, 0x81, 0x41, 0x20, 0x4e, 0x8a, 0xa1, - 0xf0, 0x1e, 0xd4, 0x2f, 0x50, 0xf0, 0x9c, 0xe5, 0xbe, 0xec, 0x1f, 0x19, - 0x28, 0x07, 0x3b, 0x80, 0x87, 0xfb, 0xd9, 0xff, 0x16, 0x67, 0xff, 0x43, - 0xd9, 0x40, 0x00, 0x8e, 0x55, 0x28, 0x1b, 0x0c, 0x04, 0x0c, 0xf3, 0x9f, - 0xed, 0x5b, 0xf4, 0x5d, 0x3e, 0x0e, 0xca, 0x47, 0x07, 0xa7, 0xe6, 0xc8, - 0xb4, 0x9d, 0x80, 0x30, 0xff, 0xc4, 0xb5, 0xbc, 0x08, 0x92, 0x00, 0xb1, - 0x70, 0xc4, 0x2a, 0x16, 0x20, 0x10, 0xa7, 0xa8, 0x14, 0xf3, 0xfc, 0x61, - 0x9c, 0x24, 0x94, 0xcf, 0x20, 0xf9, 0x47, 0xf1, 0x31, 0xcb, 0x13, 0xa0, - 0x43, 0xc0, 0x3c, 0xb3, 0x94, 0x27, 0x1b, 0x52, 0x80, 0x34, 0x30, 0x62, - 0xf9, 0xc1, 0x02, 0x20, 0x40, 0x4a, 0x7a, 0xe7, 0x34, 0x48, 0x63, 0xd8, - 0xdd, 0x00, 0x8f, 0x5d, 0x89, 0x6f, 0xb6, 0xb2, 0x5c, 0xcb, 0xb7, 0xc3, - 0xdd, 0x87, 0x05, 0x90, 0x96, 0x9b, 0xec, 0x24, 0x4d, 0xee, 0x3c, 0x3b, - 0x73, 0x1d, 0xca, 0xfb, 0x13, 0x94, 0x0e, 0x4c, 0x21, 0x12, 0x85, 0x08, - 0x0b, 0x58, 0xd6, 0x35, 0x22, 0xd2, 0x3e, 0x10, 0xe0, 0xb3, 0x19, 0x0b, - 0xe0, 0x34, 0x68, 0x95, 0xba, 0x17, 0xf4, 0x0c, 0xca, 0x03, 0xa7, 0x90, - 0x6f, 0xeb, 0x1a, 0x25, 0x02, 0xd6, 0x42, 0x05, 0x18, 0x2c, 0xde, 0x6e, - 0xa8, 0xff, 0x0e, 0x65, 0x43, 0xf3, 0x2a, 0xcd, 0x00, 0x69, 0x21, 0x43, - 0x2a, 0xa0, 0xa5, 0xe9, 0x0c, 0x0c, 0x00, 0xe4, 0x3f, 0x87, 0xe1, 0xb6, - 0x42, 0x8a, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, - 0x42, 0x60, 0x82 -}; + // tiny_png.png - Small PNG image + static const std::vector pngData = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0f, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x83, 0xf5, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, + 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, + 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, + 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, + 0x00, 0x01, 0xa8, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4f, 0x63, 0x7c, 0xff, + 0x64, 0x03, 0x03, 0xcd, 0x00, 0x13, 0x94, 0xa6, 0x0d, 0x00, 0xbb, 0xfd, + 0xf1, 0xfe, 0x7f, 0xa1, 0x5b, 0xa1, 0x02, 0x50, 0x20, 0xc5, 0xb8, 0xba, + 0x88, 0x51, 0x96, 0x81, 0xe1, 0xfb, 0x85, 0x98, 0x6b, 0x67, 0x18, 0xa4, + 0x42, 0x96, 0x48, 0x0a, 0x40, 0x65, 0x40, 0x22, 0xef, 0xd3, 0x8c, 0x1d, + 0xed, 0xa0, 0x7c, 0xce, 0x89, 0x29, 0xc2, 0xfb, 0xa1, 0x6c, 0x06, 0xc5, + 0xe0, 0x17, 0xfd, 0x9e, 0x7f, 0xa0, 0x1c, 0x06, 0x06, 0x16, 0x28, 0xcd, + 0x60, 0xcc, 0x78, 0x22, 0x92, 0x11, 0xca, 0x66, 0x60, 0x38, 0xb2, 0xfc, + 0x5f, 0x68, 0x1f, 0x03, 0xd0, 0x02, 0x11, 0x10, 0xef, 0xfa, 0xb3, 0xf3, + 0x87, 0x24, 0xe1, 0xc6, 0x21, 0x81, 0x17, 0x7c, 0x85, 0x35, 0x7c, 0x0a, + 0xd9, 0x4f, 0x36, 0x18, 0x42, 0xf8, 0x40, 0x9b, 0x24, 0x0a, 0x19, 0x10, + 0x16, 0xe0, 0x08, 0x19, 0x1b, 0x17, 0x46, 0x8d, 0x67, 0x0c, 0x0f, 0x21, + 0x1c, 0xe5, 0x7c, 0xa9, 0xf7, 0xb3, 0x9e, 0x7f, 0x80, 0x70, 0x90, 0x00, + 0xcb, 0xba, 0x99, 0x7c, 0x0c, 0xc1, 0x2f, 0xf2, 0xa1, 0x46, 0x03, 0xc1, + 0xf7, 0xfc, 0x96, 0x4f, 0x0c, 0x6b, 0xf9, 0x4e, 0x41, 0xb9, 0xc4, 0x85, + 0xbb, 0x82, 0xa4, 0x93, 0xdb, 0xfb, 0xf3, 0x87, 0xa0, 0x3c, 0x18, 0x78, + 0xc1, 0x75, 0xf8, 0xf1, 0xb7, 0x48, 0xa4, 0x70, 0x00, 0x01, 0x89, 0x4f, + 0xfd, 0x73, 0xde, 0x99, 0x41, 0x39, 0xb8, 0x4c, 0x3f, 0xb2, 0xe7, 0xff, + 0x0d, 0x63, 0x06, 0x1b, 0x28, 0x8f, 0x81, 0x41, 0x20, 0x4e, 0x8a, 0xa1, + 0xf0, 0x1e, 0xd4, 0x2f, 0x50, 0xf0, 0x9c, 0xe5, 0xbe, 0xec, 0x1f, 0x19, + 0x28, 0x07, 0x3b, 0x80, 0x87, 0xfb, 0xd9, 0xff, 0x16, 0x67, 0xff, 0x43, + 0xd9, 0x40, 0x00, 0x8e, 0x55, 0x28, 0x1b, 0x0c, 0x04, 0x0c, 0xf3, 0x9f, + 0xed, 0x5b, 0xf4, 0x5d, 0x3e, 0x0e, 0xca, 0x47, 0x07, 0xa7, 0xe6, 0xc8, + 0xb4, 0x9d, 0x80, 0x30, 0xff, 0xc4, 0xb5, 0xbc, 0x08, 0x92, 0x00, 0xb1, + 0x70, 0xc4, 0x2a, 0x16, 0x20, 0x10, 0xa7, 0xa8, 0x14, 0xf3, 0xfc, 0x61, + 0x9c, 0x24, 0x94, 0xcf, 0x20, 0xf9, 0x47, 0xf1, 0x31, 0xcb, 0x13, 0xa0, + 0x43, 0xc0, 0x3c, 0xb3, 0x94, 0x27, 0x1b, 0x52, 0x80, 0x34, 0x30, 0x62, + 0xf9, 0xc1, 0x02, 0x20, 0x40, 0x4a, 0x7a, 0xe7, 0x34, 0x48, 0x63, 0xd8, + 0xdd, 0x00, 0x8f, 0x5d, 0x89, 0x6f, 0xb6, 0xb2, 0x5c, 0xcb, 0xb7, 0xc3, + 0xdd, 0x87, 0x05, 0x90, 0x96, 0x9b, 0xec, 0x24, 0x4d, 0xee, 0x3c, 0x3b, + 0x73, 0x1d, 0xca, 0xfb, 0x13, 0x94, 0x0e, 0x4c, 0x21, 0x12, 0x85, 0x08, + 0x0b, 0x58, 0xd6, 0x35, 0x22, 0xd2, 0x3e, 0x10, 0xe0, 0xb3, 0x19, 0x0b, + 0xe0, 0x34, 0x68, 0x95, 0xba, 0x17, 0xf4, 0x0c, 0xca, 0x03, 0xa7, 0x90, + 0x6f, 0xeb, 0x1a, 0x25, 0x02, 0xd6, 0x42, 0x05, 0x18, 0x2c, 0xde, 0x6e, + 0xa8, 0xff, 0x0e, 0x65, 0x43, 0xf3, 0x2a, 0xcd, 0x00, 0x69, 0x21, 0x43, + 0x2a, 0xa0, 0xa5, 0xe9, 0x0c, 0x0c, 0x00, 0xe4, 0x3f, 0x87, 0xe1, 0xb6, + 0x42, 0x8a, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, + 0x42, 0x60, 0x82 + }; -// tiny_jpeg.jpg - Small JPEG image (actual file data from xxd) -static const std::vector jpegData = { - 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, - 0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xe1, 0x00, 0x22, - 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, - 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, - 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x03, 0x05, 0x03, 0x03, 0x03, 0x03, 0x03, 0x06, 0x04, - 0x04, 0x03, 0x05, 0x07, 0x06, 0x07, 0x07, 0x07, 0x06, 0x07, 0x07, 0x08, - 0x09, 0x0b, 0x09, 0x08, 0x08, 0x0a, 0x08, 0x07, 0x07, 0x0a, 0x0d, 0x0a, - 0x0a, 0x0b, 0x0c, 0x0c, 0x0c, 0x0c, 0x07, 0x09, 0x0e, 0x0f, 0x0d, 0x0c, - 0x0e, 0x0b, 0x0c, 0x0c, 0x0c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x02, 0x02, - 0x02, 0x03, 0x03, 0x03, 0x06, 0x03, 0x03, 0x06, 0x0c, 0x08, 0x07, 0x08, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x12, 0x00, 0x20, 0x03, - 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, - 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, - 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, - 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, - 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, - 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, - 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, - 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, - 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, - 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, - 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, - 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, - 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, - 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, - 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, - 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, - 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, - 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, - 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, - 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, - 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, - 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, - 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, - 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, - 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, - 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, - 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, - 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, - 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, - 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, - 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, - 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, - 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, - 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfd, - 0x5e, 0xf8, 0xcf, 0xf1, 0x17, 0xfe, 0x15, 0x17, 0xc2, 0x5f, 0x12, 0x78, - 0xa7, 0xec, 0x7f, 0xda, 0x1f, 0xf0, 0x8f, 0xe9, 0xd3, 0xea, 0x1f, 0x65, - 0xf3, 0x7c, 0x9f, 0xb4, 0x79, 0x68, 0x5b, 0x66, 0xfd, 0xad, 0xb7, 0x38, - 0xc6, 0x70, 0x71, 0xe8, 0x6b, 0x26, 0x4f, 0x88, 0xfe, 0x29, 0xd6, 0x3c, - 0x59, 0xe2, 0x0d, 0x3f, 0x43, 0xf0, 0xee, 0x83, 0x79, 0x6d, 0xa0, 0x5c, - 0xa5, 0xab, 0xcd, 0x7b, 0xae, 0x4b, 0x6b, 0x24, 0xce, 0xd0, 0x47, 0x37, - 0x08, 0xb6, 0xb2, 0x00, 0x31, 0x20, 0x1c, 0xbf, 0x51, 0xda, 0xb9, 0xdf, - 0xda, 0x7b, 0x4d, 0xf1, 0xaf, 0x8e, 0xbc, 0x3d, 0xe2, 0x3f, 0x06, 0xe9, - 0x3e, 0x1b, 0x5d, 0x4f, 0x45, 0xf1, 0x76, 0x86, 0x34, 0xdb, 0x6d, 0x4e, - 0x2b, 0xa8, 0x22, 0xfe, 0xc9, 0xba, 0x95, 0xe5, 0x8e, 0x79, 0x2e, 0x84, - 0x92, 0xab, 0xb4, 0x2b, 0x13, 0x42, 0xeb, 0xe4, 0x47, 0x23, 0xe5, 0x25, - 0x04, 0x72, 0x95, 0xa9, 0xa6, 0xfc, 0x0f, 0x8b, 0x5b, 0xf1, 0xe7, 0x8c, - 0x35, 0x1d, 0x5c, 0xeb, 0xd6, 0xd0, 0xea, 0x7a, 0x84, 0x52, 0x5a, 0x7d, - 0x8b, 0x5f, 0xbb, 0xb2, 0x8e, 0x78, 0x85, 0xac, 0x08, 0x49, 0x8e, 0xde, - 0x65, 0x5c, 0xef, 0x57, 0x19, 0x61, 0xb8, 0x81, 0xe9, 0x8a, 0xfc, 0x3f, - 0x56, 0xfe, 0x4f, 0xef, 0xba, 0xff, 0x00, 0x83, 0xf9, 0x9f, 0x53, 0xc4, - 0x58, 0x4a, 0xd4, 0xb0, 0x18, 0x47, 0x82, 0x92, 0xf6, 0xb5, 0x2a, 0x4f, - 0x9f, 0x96, 0x49, 0xb5, 0x4f, 0xd9, 0xa7, 0x1e, 0x6f, 0x76, 0x7c, 0x9e, - 0xfd, 0xd6, 0xb1, 0x4d, 0xbd, 0x2f, 0x6b, 0x15, 0x57, 0xf6, 0x8a, 0xbf, - 0xf1, 0x8c, 0xda, 0x3e, 0x9f, 0xe0, 0xdf, 0x0d, 0xc5, 0xab, 0xeb, 0xba, - 0x86, 0x9c, 0xba, 0xb5, 0xe4, 0x1a, 0x9e, 0xa3, 0xfd, 0x9f, 0x69, 0xa4, - 0xdb, 0x33, 0xbc, 0x6b, 0xe7, 0x4f, 0x1c, 0x53, 0x93, 0x23, 0xc9, 0x1c, - 0x8a, 0x88, 0x91, 0xb0, 0x61, 0x14, 0x8c, 0x59, 0x40, 0x1b, 0xba, 0xff, - 0x00, 0x86, 0x3e, 0x34, 0xd5, 0x3c, 0x61, 0xa5, 0xde, 0xae, 0xb7, 0xe1, - 0xfb, 0x8f, 0x0e, 0xea, 0xda, 0x5d, 0xe3, 0xd9, 0xdc, 0xdb, 0x99, 0x0c, - 0xf6, 0xd3, 0x10, 0x15, 0x96, 0x6b, 0x69, 0xca, 0x27, 0x9d, 0x0b, 0xab, - 0xa9, 0x0d, 0xb1, 0x48, 0x3b, 0x95, 0x95, 0x59, 0x58, 0x0e, 0x23, 0xc4, - 0x7e, 0x00, 0xd5, 0x3e, 0x14, 0x7c, 0x46, 0x9b, 0x5c, 0xf0, 0xcf, 0x86, - 0xae, 0x35, 0xff, 0x00, 0x0f, 0xeb, 0x1a, 0x1d, 0xb6, 0x83, 0x7d, 0xa4, - 0xe9, 0x57, 0x70, 0xd9, 0x5f, 0xd8, 0x0b, 0x66, 0x9d, 0xa0, 0x96, 0xdd, - 0xa6, 0x92, 0x28, 0xca, 0x15, 0xb8, 0x74, 0x71, 0xe6, 0xa3, 0xa9, 0x58, - 0xd9, 0x77, 0x1d, 0xc0, 0x5a, 0xfd, 0x98, 0xfc, 0x11, 0xe2, 0x0f, 0x09, - 0x5b, 0x78, 0x9a, 0xe3, 0x5a, 0x87, 0x5c, 0xd3, 0xb4, 0xfd, 0x5b, 0x52, - 0x59, 0xf4, 0x7d, 0x2b, 0x58, 0xf1, 0x0c, 0xda, 0xe5, 0xf6, 0x99, 0x6c, - 0xb0, 0xc7, 0x11, 0x59, 0x66, 0x92, 0x49, 0x42, 0xb3, 0xc8, 0x8f, 0x2e, - 0xc8, 0xe5, 0x91, 0x54, 0x48, 0x06, 0xec, 0x82, 0x07, 0xd7, 0x63, 0x30, - 0x79, 0x7f, 0xf6, 0x73, 0xab, 0x47, 0x92, 0xe9, 0x41, 0xa7, 0xcd, 0xef, - 0xca, 0x4d, 0xfb, 0xf1, 0x71, 0xf6, 0x8e, 0xdc, 0xad, 0xbb, 0x7e, 0xee, - 0xce, 0x31, 0x4f, 0x9e, 0xef, 0xde, 0xf2, 0xf0, 0xaf, 0x13, 0x1e, 0x48, - 0x57, 0x77, 0x95, 0x92, 0x93, 0x4b, 0x46, 0xf9, 0x55, 0xda, 0x76, 0x5b, - 0xca, 0xfd, 0xad, 0xb5, 0x8f, 0x52, 0xa2, 0x8a, 0x2b, 0xe4, 0x4f, 0x4c, - 0x28, 0xa2, 0x8a, 0x00, 0xff, 0xd9 -}; + // tiny_jpeg.jpg - Small JPEG image (actual file data from xxd) + static const std::vector jpegData = { + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, + 0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xe1, 0x00, 0x22, + 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, + 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x03, 0x05, 0x03, 0x03, 0x03, 0x03, 0x03, 0x06, 0x04, + 0x04, 0x03, 0x05, 0x07, 0x06, 0x07, 0x07, 0x07, 0x06, 0x07, 0x07, 0x08, + 0x09, 0x0b, 0x09, 0x08, 0x08, 0x0a, 0x08, 0x07, 0x07, 0x0a, 0x0d, 0x0a, + 0x0a, 0x0b, 0x0c, 0x0c, 0x0c, 0x0c, 0x07, 0x09, 0x0e, 0x0f, 0x0d, 0x0c, + 0x0e, 0x0b, 0x0c, 0x0c, 0x0c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x02, 0x02, + 0x02, 0x03, 0x03, 0x03, 0x06, 0x03, 0x03, 0x06, 0x0c, 0x08, 0x07, 0x08, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x12, 0x00, 0x20, 0x03, + 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, + 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, + 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, + 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, + 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, + 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, + 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, + 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, + 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, + 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, + 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, + 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, + 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, + 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, + 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, + 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, + 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, + 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, + 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, + 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, + 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, + 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfd, + 0x5e, 0xf8, 0xcf, 0xf1, 0x17, 0xfe, 0x15, 0x17, 0xc2, 0x5f, 0x12, 0x78, + 0xa7, 0xec, 0x7f, 0xda, 0x1f, 0xf0, 0x8f, 0xe9, 0xd3, 0xea, 0x1f, 0x65, + 0xf3, 0x7c, 0x9f, 0xb4, 0x79, 0x68, 0x5b, 0x66, 0xfd, 0xad, 0xb7, 0x38, + 0xc6, 0x70, 0x71, 0xe8, 0x6b, 0x26, 0x4f, 0x88, 0xfe, 0x29, 0xd6, 0x3c, + 0x59, 0xe2, 0x0d, 0x3f, 0x43, 0xf0, 0xee, 0x83, 0x79, 0x6d, 0xa0, 0x5c, + 0xa5, 0xab, 0xcd, 0x7b, 0xae, 0x4b, 0x6b, 0x24, 0xce, 0xd0, 0x47, 0x37, + 0x08, 0xb6, 0xb2, 0x00, 0x31, 0x20, 0x1c, 0xbf, 0x51, 0xda, 0xb9, 0xdf, + 0xda, 0x7b, 0x4d, 0xf1, 0xaf, 0x8e, 0xbc, 0x3d, 0xe2, 0x3f, 0x06, 0xe9, + 0x3e, 0x1b, 0x5d, 0x4f, 0x45, 0xf1, 0x76, 0x86, 0x34, 0xdb, 0x6d, 0x4e, + 0x2b, 0xa8, 0x22, 0xfe, 0xc9, 0xba, 0x95, 0xe5, 0x8e, 0x79, 0x2e, 0x84, + 0x92, 0xab, 0xb4, 0x2b, 0x13, 0x42, 0xeb, 0xe4, 0x47, 0x23, 0xe5, 0x25, + 0x04, 0x72, 0x95, 0xa9, 0xa6, 0xfc, 0x0f, 0x8b, 0x5b, 0xf1, 0xe7, 0x8c, + 0x35, 0x1d, 0x5c, 0xeb, 0xd6, 0xd0, 0xea, 0x7a, 0x84, 0x52, 0x5a, 0x7d, + 0x8b, 0x5f, 0xbb, 0xb2, 0x8e, 0x78, 0x85, 0xac, 0x08, 0x49, 0x8e, 0xde, + 0x65, 0x5c, 0xef, 0x57, 0x19, 0x61, 0xb8, 0x81, 0xe9, 0x8a, 0xfc, 0x3f, + 0x56, 0xfe, 0x4f, 0xef, 0xba, 0xff, 0x00, 0x83, 0xf9, 0x9f, 0x53, 0xc4, + 0x58, 0x4a, 0xd4, 0xb0, 0x18, 0x47, 0x82, 0x92, 0xf6, 0xb5, 0x2a, 0x4f, + 0x9f, 0x96, 0x49, 0xb5, 0x4f, 0xd9, 0xa7, 0x1e, 0x6f, 0x76, 0x7c, 0x9e, + 0xfd, 0xd6, 0xb1, 0x4d, 0xbd, 0x2f, 0x6b, 0x15, 0x57, 0xf6, 0x8a, 0xbf, + 0xf1, 0x8c, 0xda, 0x3e, 0x9f, 0xe0, 0xdf, 0x0d, 0xc5, 0xab, 0xeb, 0xba, + 0x86, 0x9c, 0xba, 0xb5, 0xe4, 0x1a, 0x9e, 0xa3, 0xfd, 0x9f, 0x69, 0xa4, + 0xdb, 0x33, 0xbc, 0x6b, 0xe7, 0x4f, 0x1c, 0x53, 0x93, 0x23, 0xc9, 0x1c, + 0x8a, 0x88, 0x91, 0xb0, 0x61, 0x14, 0x8c, 0x59, 0x40, 0x1b, 0xba, 0xff, + 0x00, 0x86, 0x3e, 0x34, 0xd5, 0x3c, 0x61, 0xa5, 0xde, 0xae, 0xb7, 0xe1, + 0xfb, 0x8f, 0x0e, 0xea, 0xda, 0x5d, 0xe3, 0xd9, 0xdc, 0xdb, 0x99, 0x0c, + 0xf6, 0xd3, 0x10, 0x15, 0x96, 0x6b, 0x69, 0xca, 0x27, 0x9d, 0x0b, 0xab, + 0xa9, 0x0d, 0xb1, 0x48, 0x3b, 0x95, 0x95, 0x59, 0x58, 0x0e, 0x23, 0xc4, + 0x7e, 0x00, 0xd5, 0x3e, 0x14, 0x7c, 0x46, 0x9b, 0x5c, 0xf0, 0xcf, 0x86, + 0xae, 0x35, 0xff, 0x00, 0x0f, 0xeb, 0x1a, 0x1d, 0xb6, 0x83, 0x7d, 0xa4, + 0xe9, 0x57, 0x70, 0xd9, 0x5f, 0xd8, 0x0b, 0x66, 0x9d, 0xa0, 0x96, 0xdd, + 0xa6, 0x92, 0x28, 0xca, 0x15, 0xb8, 0x74, 0x71, 0xe6, 0xa3, 0xa9, 0x58, + 0xd9, 0x77, 0x1d, 0xc0, 0x5a, 0xfd, 0x98, 0xfc, 0x11, 0xe2, 0x0f, 0x09, + 0x5b, 0x78, 0x9a, 0xe3, 0x5a, 0x87, 0x5c, 0xd3, 0xb4, 0xfd, 0x5b, 0x52, + 0x59, 0xf4, 0x7d, 0x2b, 0x58, 0xf1, 0x0c, 0xda, 0xe5, 0xf6, 0x99, 0x6c, + 0xb0, 0xc7, 0x11, 0x59, 0x66, 0x92, 0x49, 0x42, 0xb3, 0xc8, 0x8f, 0x2e, + 0xc8, 0xe5, 0x91, 0x54, 0x48, 0x06, 0xec, 0x82, 0x07, 0xd7, 0x63, 0x30, + 0x79, 0x7f, 0xf6, 0x73, 0xab, 0x47, 0x92, 0xe9, 0x41, 0xa7, 0xcd, 0xef, + 0xca, 0x4d, 0xfb, 0xf1, 0x71, 0xf6, 0x8e, 0xdc, 0xad, 0xbb, 0x7e, 0xee, + 0xce, 0x31, 0x4f, 0x9e, 0xef, 0xde, 0xf2, 0xf0, 0xaf, 0x13, 0x1e, 0x48, + 0x57, 0x77, 0x95, 0x92, 0x93, 0x4b, 0x46, 0xf9, 0x55, 0xda, 0x76, 0x5b, + 0xca, 0xfd, 0xad, 0xb5, 0x8f, 0x52, 0xa2, 0x8a, 0x2b, 0xe4, 0x4f, 0x4c, + 0x28, 0xa2, 0x8a, 0x00, 0xff, 0xd9 + }; -// tiny_bmp.bmp - Small BMP image (actual file data from xxd) -static const std::vector bmpData = { - 0x42, 0x4d, 0x76, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, - 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x10, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, - 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, 0xb1, 0x22, 0xb0, 0xdb, 0x91, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x3e, 0x89, - 0xef, 0x6f, 0x44, 0xed, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x4c, 0xb1, - 0x22, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, - 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, - 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0x91, 0xe4, 0xef, 0x5e, - 0xb1, 0x6f, 0x80, 0xd4, 0x91, 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x24, 0x1c, 0xee, - 0xb0, 0xa8, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0x80, 0xdb, 0xef, 0x80, 0xba, 0x4b, 0x91, 0xe4, 0xd0, 0x6f, 0xb1, 0x6f, - 0xb0, 0xe4, 0xb1, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, - 0xef, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0x5e, 0xcb, 0xd0, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, - 0xef, 0x4c, 0xc2, 0xb1, 0xb0, 0xd4, 0x6f, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, - 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, - 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x24, - 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, - 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x9b, 0x89, 0xed, 0xb0, 0xe4, 0xef, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xa0, 0xe4, 0xb1, 0x5e, 0xba, 0x91, 0xb0, - 0xdb, 0x91, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x91, 0xc2, 0x22, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc2, 0x48, 0x3f, 0xb0, 0xe4, 0xb9, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x85, 0x67, 0xed, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0x80, 0xdb, 0xb1, - 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0xb0, 0xe4, 0xef, 0x91, 0xe4, 0xef, - 0x6f, 0xb1, 0x6f, 0x80, 0xdb, 0xb1, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, 0xef, 0xcc, 0x65, 0x9d, - 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, - 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0x5e, 0xcb, 0x91, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x80, 0xcb, 0x6f, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, - 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, - 0xef, 0xcc, 0x65, 0x9d, 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, - 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0x80, - 0xdb, 0xef, 0x5e, 0xb1, 0x4b, 0x4c, 0xba, 0x4b, 0xb0, 0xd4, 0x6f, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, - 0xba, 0x22, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, - 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x4c, 0xb1, 0x4b, 0x6f, 0xb1, 0x22, - 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x91, 0xe4, 0xef, 0x4c, 0xb1, 0x6f, 0x6f, 0xb1, 0x22, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, - 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00 -}; + // tiny_bmp.bmp - Small BMP image (actual file data from xxd) + static const std::vector bmpData = { + 0x42, 0x4d, 0x76, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, + 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, 0xb1, 0x22, 0xb0, 0xdb, 0x91, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x3e, 0x89, + 0xef, 0x6f, 0x44, 0xed, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x4c, 0xb1, + 0x22, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0x91, 0xe4, 0xef, 0x5e, + 0xb1, 0x6f, 0x80, 0xd4, 0x91, 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x24, 0x1c, 0xee, + 0xb0, 0xa8, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0x80, 0xdb, 0xef, 0x80, 0xba, 0x4b, 0x91, 0xe4, 0xd0, 0x6f, 0xb1, 0x6f, + 0xb0, 0xe4, 0xb1, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, + 0xef, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0x5e, 0xcb, 0xd0, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, + 0xef, 0x4c, 0xc2, 0xb1, 0xb0, 0xd4, 0x6f, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, + 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, + 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x9b, 0x89, 0xed, 0xb0, 0xe4, 0xef, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xa0, 0xe4, 0xb1, 0x5e, 0xba, 0x91, 0xb0, + 0xdb, 0x91, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x91, 0xc2, 0x22, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc2, 0x48, 0x3f, 0xb0, 0xe4, 0xb9, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x85, 0x67, 0xed, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0x80, 0xdb, 0xb1, + 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0xb0, 0xe4, 0xef, 0x91, 0xe4, 0xef, + 0x6f, 0xb1, 0x6f, 0x80, 0xdb, 0xb1, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, 0xef, 0xcc, 0x65, 0x9d, + 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, + 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0x5e, 0xcb, 0x91, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x80, 0xcb, 0x6f, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, + 0xef, 0xcc, 0x65, 0x9d, 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0x80, + 0xdb, 0xef, 0x5e, 0xb1, 0x4b, 0x4c, 0xba, 0x4b, 0xb0, 0xd4, 0x6f, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, + 0xba, 0x22, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x4c, 0xb1, 0x4b, 0x6f, 0xb1, 0x22, + 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x91, 0xe4, 0xef, 0x4c, 0xb1, 0x6f, 0x6f, 0xb1, 0x22, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, + 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00 + }; -// tiny_gif.gif - Small GIF image (actual file data from xxd) -static const std::vector gifData = { - 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x1a, 0x00, 0x10, 0x00, 0x70, 0x00, - 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x2c, 0x00, 0x00, - 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x33, 0x00, 0x00, 0x66, 0x00, 0x00, 0x99, 0x00, 0x00, 0xcc, 0x00, 0x00, - 0xff, 0x00, 0x2b, 0x00, 0x00, 0x2b, 0x33, 0x00, 0x2b, 0x66, 0x00, 0x2b, - 0x99, 0x00, 0x2b, 0xcc, 0x00, 0x2b, 0xff, 0x00, 0x55, 0x00, 0x00, 0x55, - 0x33, 0x00, 0x55, 0x66, 0x00, 0x55, 0x99, 0x00, 0x55, 0xcc, 0x00, 0x55, - 0xff, 0x00, 0x80, 0x00, 0x00, 0x80, 0x33, 0x00, 0x80, 0x66, 0x00, 0x80, - 0x99, 0x00, 0x80, 0xcc, 0x00, 0x80, 0xff, 0x00, 0xaa, 0x00, 0x00, 0xaa, - 0x33, 0x00, 0xaa, 0x66, 0x00, 0xaa, 0x99, 0x00, 0xaa, 0xcc, 0x00, 0xaa, - 0xff, 0x00, 0xd5, 0x00, 0x00, 0xd5, 0x33, 0x00, 0xd5, 0x66, 0x00, 0xd5, - 0x99, 0x00, 0xd5, 0xcc, 0x00, 0xd5, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, - 0x33, 0x00, 0xff, 0x66, 0x00, 0xff, 0x99, 0x00, 0xff, 0xcc, 0x00, 0xff, - 0xff, 0x33, 0x00, 0x00, 0x33, 0x00, 0x33, 0x33, 0x00, 0x66, 0x33, 0x00, - 0x99, 0x33, 0x00, 0xcc, 0x33, 0x00, 0xff, 0x33, 0x2b, 0x00, 0x33, 0x2b, - 0x33, 0x33, 0x2b, 0x66, 0x33, 0x2b, 0x99, 0x33, 0x2b, 0xcc, 0x33, 0x2b, - 0xff, 0x33, 0x55, 0x00, 0x33, 0x55, 0x33, 0x33, 0x55, 0x66, 0x33, 0x55, - 0x99, 0x33, 0x55, 0xcc, 0x33, 0x55, 0xff, 0x33, 0x80, 0x00, 0x33, 0x80, - 0x33, 0x33, 0x80, 0x66, 0x33, 0x80, 0x99, 0x33, 0x80, 0xcc, 0x33, 0x80, - 0xff, 0x33, 0xaa, 0x00, 0x33, 0xaa, 0x33, 0x33, 0xaa, 0x66, 0x33, 0xaa, - 0x99, 0x33, 0xaa, 0xcc, 0x33, 0xaa, 0xff, 0x33, 0xd5, 0x00, 0x33, 0xd5, - 0x33, 0x33, 0xd5, 0x66, 0x33, 0xd5, 0x99, 0x33, 0xd5, 0xcc, 0x33, 0xd5, - 0xff, 0x33, 0xff, 0x00, 0x33, 0xff, 0x33, 0x33, 0xff, 0x66, 0x33, 0xff, - 0x99, 0x33, 0xff, 0xcc, 0x33, 0xff, 0xff, 0x66, 0x00, 0x00, 0x66, 0x00, - 0x33, 0x66, 0x00, 0x66, 0x66, 0x00, 0x99, 0x66, 0x00, 0xcc, 0x66, 0x00, - 0xff, 0x66, 0x2b, 0x00, 0x66, 0x2b, 0x33, 0x66, 0x2b, 0x66, 0x66, 0x2b, - 0x99, 0x66, 0x2b, 0xcc, 0x66, 0x2b, 0xff, 0x66, 0x55, 0x00, 0x66, 0x55, - 0x33, 0x66, 0x55, 0x66, 0x66, 0x55, 0x99, 0x66, 0x55, 0xcc, 0x66, 0x55, - 0xff, 0x66, 0x80, 0x00, 0x66, 0x80, 0x33, 0x66, 0x80, 0x66, 0x66, 0x80, - 0x99, 0x66, 0x80, 0xcc, 0x66, 0x80, 0xff, 0x66, 0xaa, 0x00, 0x66, 0xaa, - 0x33, 0x66, 0xaa, 0x66, 0x66, 0xaa, 0x99, 0x66, 0xaa, 0xcc, 0x66, 0xaa, - 0xff, 0x66, 0xd5, 0x00, 0x66, 0xd5, 0x33, 0x66, 0xd5, 0x66, 0x66, 0xd5, - 0x99, 0x66, 0xd5, 0xcc, 0x66, 0xd5, 0xff, 0x66, 0xff, 0x00, 0x66, 0xff, - 0x33, 0x66, 0xff, 0x66, 0x66, 0xff, 0x99, 0x66, 0xff, 0xcc, 0x66, 0xff, - 0xff, 0x99, 0x00, 0x00, 0x99, 0x00, 0x33, 0x99, 0x00, 0x66, 0x99, 0x00, - 0x99, 0x99, 0x00, 0xcc, 0x99, 0x00, 0xff, 0x99, 0x2b, 0x00, 0x99, 0x2b, - 0x33, 0x99, 0x2b, 0x66, 0x99, 0x2b, 0x99, 0x99, 0x2b, 0xcc, 0x99, 0x2b, - 0xff, 0x99, 0x55, 0x00, 0x99, 0x55, 0x33, 0x99, 0x55, 0x66, 0x99, 0x55, - 0x99, 0x99, 0x55, 0xcc, 0x99, 0x55, 0xff, 0x99, 0x80, 0x00, 0x99, 0x80, - 0x33, 0x99, 0x80, 0x66, 0x99, 0x80, 0x99, 0x99, 0x80, 0xcc, 0x99, 0x80, - 0xff, 0x99, 0xaa, 0x00, 0x99, 0xaa, 0x33, 0x99, 0xaa, 0x66, 0x99, 0xaa, - 0x99, 0x99, 0xaa, 0xcc, 0x99, 0xaa, 0xff, 0x99, 0xd5, 0x00, 0x99, 0xd5, - 0x33, 0x99, 0xd5, 0x66, 0x99, 0xd5, 0x99, 0x99, 0xd5, 0xcc, 0x99, 0xd5, - 0xff, 0x99, 0xff, 0x00, 0x99, 0xff, 0x33, 0x99, 0xff, 0x66, 0x99, 0xff, - 0x99, 0x99, 0xff, 0xcc, 0x99, 0xff, 0xff, 0xcc, 0x00, 0x00, 0xcc, 0x00, - 0x33, 0xcc, 0x00, 0x66, 0xcc, 0x00, 0x99, 0xcc, 0x00, 0xcc, 0xcc, 0x00, - 0xff, 0xcc, 0x2b, 0x00, 0xcc, 0x2b, 0x33, 0xcc, 0x2b, 0x66, 0xcc, 0x2b, - 0x99, 0xcc, 0x2b, 0xcc, 0xcc, 0x2b, 0xff, 0xcc, 0x55, 0x00, 0xcc, 0x55, - 0x33, 0xcc, 0x55, 0x66, 0xcc, 0x55, 0x99, 0xcc, 0x55, 0xcc, 0xcc, 0x55, - 0xff, 0xcc, 0x80, 0x00, 0xcc, 0x80, 0x33, 0xcc, 0x80, 0x66, 0xcc, 0x80, - 0x99, 0xcc, 0x80, 0xcc, 0xcc, 0x80, 0xff, 0xcc, 0xaa, 0x00, 0xcc, 0xaa, - 0x33, 0xcc, 0xaa, 0x66, 0xcc, 0xaa, 0x99, 0xcc, 0xaa, 0xcc, 0xcc, 0xaa, - 0xff, 0xcc, 0xd5, 0x00, 0xcc, 0xd5, 0x33, 0xcc, 0xd5, 0x66, 0xcc, 0xd5, - 0x99, 0xcc, 0xd5, 0xcc, 0xcc, 0xd5, 0xff, 0xcc, 0xff, 0x00, 0xcc, 0xff, - 0x33, 0xcc, 0xff, 0x66, 0xcc, 0xff, 0x99, 0xcc, 0xff, 0xcc, 0xcc, 0xff, - 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0x33, 0xff, 0x00, 0x66, 0xff, 0x00, - 0x99, 0xff, 0x00, 0xcc, 0xff, 0x00, 0xff, 0xff, 0x2b, 0x00, 0xff, 0x2b, - 0x33, 0xff, 0x2b, 0x66, 0xff, 0x2b, 0x99, 0xff, 0x2b, 0xcc, 0xff, 0x2b, - 0xff, 0xff, 0x55, 0x00, 0xff, 0x55, 0x33, 0xff, 0x55, 0x66, 0xff, 0x55, - 0x99, 0xff, 0x55, 0xcc, 0xff, 0x55, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, - 0x33, 0xff, 0x80, 0x66, 0xff, 0x80, 0x99, 0xff, 0x80, 0xcc, 0xff, 0x80, - 0xff, 0xff, 0xaa, 0x00, 0xff, 0xaa, 0x33, 0xff, 0xaa, 0x66, 0xff, 0xaa, - 0x99, 0xff, 0xaa, 0xcc, 0xff, 0xaa, 0xff, 0xff, 0xd5, 0x00, 0xff, 0xd5, - 0x33, 0xff, 0xd5, 0x66, 0xff, 0xd5, 0x99, 0xff, 0xd5, 0xcc, 0xff, 0xd5, - 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x33, 0xff, 0xff, 0x66, 0xff, 0xff, - 0x99, 0xff, 0xff, 0xcc, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0x00, 0xe7, 0xe9, - 0x13, 0x48, 0x70, 0xa0, 0xc1, 0x82, 0x08, 0x0f, 0xea, 0xd3, 0x97, 0x8c, - 0x5e, 0x32, 0x86, 0x0e, 0x21, 0x3e, 0x6c, 0x38, 0x31, 0x22, 0xc5, 0x7c, - 0xca, 0x04, 0x26, 0xc3, 0x38, 0x8f, 0x23, 0xc6, 0x7c, 0x0d, 0x41, 0xd2, - 0x03, 0xc9, 0x50, 0xe3, 0x48, 0x88, 0x1d, 0x51, 0x4d, 0xc3, 0x96, 0xea, - 0x5c, 0x3e, 0x7a, 0xf2, 0x88, 0x98, 0x72, 0x47, 0x86, 0x86, 0x0e, 0x9b, - 0x3a, 0xde, 0x0c, 0xcc, 0x38, 0x0f, 0x9a, 0x3c, 0x54, 0xee, 0xf2, 0x41, - 0xcb, 0x37, 0xad, 0x17, 0x48, 0x37, 0x9f, 0xe6, 0xe9, 0x78, 0x36, 0xb2, - 0x69, 0xc8, 0x92, 0xdf, 0xce, 0x41, 0x9b, 0x37, 0x2f, 0x99, 0x3c, 0x55, - 0xfa, 0x62, 0x42, 0xa3, 0xf9, 0xc9, 0xe1, 0x4b, 0x93, 0x0f, 0xe5, 0x51, - 0x6b, 0xc8, 0x54, 0x9f, 0xb2, 0x7c, 0x54, 0x89, 0x80, 0x9a, 0x47, 0x06, - 0xd4, 0xd7, 0xa9, 0x4c, 0xbf, 0x0a, 0x03, 0xf7, 0x75, 0xe3, 0x3c, 0x65, - 0xfa, 0x90, 0xb9, 0x99, 0x59, 0x53, 0xc7, 0x4d, 0x20, 0xf3, 0xe8, 0x31, - 0x9d, 0xf7, 0x8c, 0x1d, 0xac, 0x88, 0xdf, 0x52, 0x65, 0x4b, 0x45, 0x15, - 0xa9, 0xd2, 0x67, 0x81, 0xd1, 0x42, 0xcc, 0x37, 0xf0, 0x2a, 0xe5, 0x8d, - 0x59, 0xa7, 0x29, 0xd3, 0xca, 0x15, 0x62, 0x5c, 0x8a, 0x0c, 0xa3, 0x26, - 0x0b, 0xbc, 0xf1, 0x55, 0xbb, 0x7c, 0x44, 0x3e, 0x29, 0x23, 0xd3, 0xb0, - 0x27, 0xe9, 0x93, 0x54, 0x91, 0x4d, 0x6b, 0x37, 0x35, 0xdf, 0xab, 0x69, - 0xf4, 0xf4, 0x6e, 0x1d, 0xe3, 0xf6, 0xac, 0x6b, 0x86, 0x64, 0x47, 0xd6, - 0x92, 0x96, 0x6a, 0x1a, 0xb8, 0xa1, 0xf3, 0x64, 0xce, 0xf3, 0x62, 0xca, - 0x24, 0xc7, 0x85, 0x81, 0x21, 0x8f, 0x1c, 0x9a, 0x51, 0x30, 0xbd, 0xbb, - 0x68, 0x23, 0x56, 0x1d, 0x38, 0xfa, 0x21, 0xf6, 0x8c, 0x03, 0xa7, 0xba, - 0x11, 0x7e, 0xf9, 0x79, 0x24, 0xf8, 0x9d, 0x5f, 0x4b, 0x86, 0x7f, 0x3d, - 0xf8, 0xac, 0x76, 0x65, 0x01, 0x01, 0x00, 0x3b -}; + // tiny_gif.gif - Small GIF image (actual file data from xxd) + static const std::vector gifData = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x1a, 0x00, 0x10, 0x00, 0x70, 0x00, + 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x2c, 0x00, 0x00, + 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x66, 0x00, 0x00, 0x99, 0x00, 0x00, 0xcc, 0x00, 0x00, + 0xff, 0x00, 0x2b, 0x00, 0x00, 0x2b, 0x33, 0x00, 0x2b, 0x66, 0x00, 0x2b, + 0x99, 0x00, 0x2b, 0xcc, 0x00, 0x2b, 0xff, 0x00, 0x55, 0x00, 0x00, 0x55, + 0x33, 0x00, 0x55, 0x66, 0x00, 0x55, 0x99, 0x00, 0x55, 0xcc, 0x00, 0x55, + 0xff, 0x00, 0x80, 0x00, 0x00, 0x80, 0x33, 0x00, 0x80, 0x66, 0x00, 0x80, + 0x99, 0x00, 0x80, 0xcc, 0x00, 0x80, 0xff, 0x00, 0xaa, 0x00, 0x00, 0xaa, + 0x33, 0x00, 0xaa, 0x66, 0x00, 0xaa, 0x99, 0x00, 0xaa, 0xcc, 0x00, 0xaa, + 0xff, 0x00, 0xd5, 0x00, 0x00, 0xd5, 0x33, 0x00, 0xd5, 0x66, 0x00, 0xd5, + 0x99, 0x00, 0xd5, 0xcc, 0x00, 0xd5, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, + 0x33, 0x00, 0xff, 0x66, 0x00, 0xff, 0x99, 0x00, 0xff, 0xcc, 0x00, 0xff, + 0xff, 0x33, 0x00, 0x00, 0x33, 0x00, 0x33, 0x33, 0x00, 0x66, 0x33, 0x00, + 0x99, 0x33, 0x00, 0xcc, 0x33, 0x00, 0xff, 0x33, 0x2b, 0x00, 0x33, 0x2b, + 0x33, 0x33, 0x2b, 0x66, 0x33, 0x2b, 0x99, 0x33, 0x2b, 0xcc, 0x33, 0x2b, + 0xff, 0x33, 0x55, 0x00, 0x33, 0x55, 0x33, 0x33, 0x55, 0x66, 0x33, 0x55, + 0x99, 0x33, 0x55, 0xcc, 0x33, 0x55, 0xff, 0x33, 0x80, 0x00, 0x33, 0x80, + 0x33, 0x33, 0x80, 0x66, 0x33, 0x80, 0x99, 0x33, 0x80, 0xcc, 0x33, 0x80, + 0xff, 0x33, 0xaa, 0x00, 0x33, 0xaa, 0x33, 0x33, 0xaa, 0x66, 0x33, 0xaa, + 0x99, 0x33, 0xaa, 0xcc, 0x33, 0xaa, 0xff, 0x33, 0xd5, 0x00, 0x33, 0xd5, + 0x33, 0x33, 0xd5, 0x66, 0x33, 0xd5, 0x99, 0x33, 0xd5, 0xcc, 0x33, 0xd5, + 0xff, 0x33, 0xff, 0x00, 0x33, 0xff, 0x33, 0x33, 0xff, 0x66, 0x33, 0xff, + 0x99, 0x33, 0xff, 0xcc, 0x33, 0xff, 0xff, 0x66, 0x00, 0x00, 0x66, 0x00, + 0x33, 0x66, 0x00, 0x66, 0x66, 0x00, 0x99, 0x66, 0x00, 0xcc, 0x66, 0x00, + 0xff, 0x66, 0x2b, 0x00, 0x66, 0x2b, 0x33, 0x66, 0x2b, 0x66, 0x66, 0x2b, + 0x99, 0x66, 0x2b, 0xcc, 0x66, 0x2b, 0xff, 0x66, 0x55, 0x00, 0x66, 0x55, + 0x33, 0x66, 0x55, 0x66, 0x66, 0x55, 0x99, 0x66, 0x55, 0xcc, 0x66, 0x55, + 0xff, 0x66, 0x80, 0x00, 0x66, 0x80, 0x33, 0x66, 0x80, 0x66, 0x66, 0x80, + 0x99, 0x66, 0x80, 0xcc, 0x66, 0x80, 0xff, 0x66, 0xaa, 0x00, 0x66, 0xaa, + 0x33, 0x66, 0xaa, 0x66, 0x66, 0xaa, 0x99, 0x66, 0xaa, 0xcc, 0x66, 0xaa, + 0xff, 0x66, 0xd5, 0x00, 0x66, 0xd5, 0x33, 0x66, 0xd5, 0x66, 0x66, 0xd5, + 0x99, 0x66, 0xd5, 0xcc, 0x66, 0xd5, 0xff, 0x66, 0xff, 0x00, 0x66, 0xff, + 0x33, 0x66, 0xff, 0x66, 0x66, 0xff, 0x99, 0x66, 0xff, 0xcc, 0x66, 0xff, + 0xff, 0x99, 0x00, 0x00, 0x99, 0x00, 0x33, 0x99, 0x00, 0x66, 0x99, 0x00, + 0x99, 0x99, 0x00, 0xcc, 0x99, 0x00, 0xff, 0x99, 0x2b, 0x00, 0x99, 0x2b, + 0x33, 0x99, 0x2b, 0x66, 0x99, 0x2b, 0x99, 0x99, 0x2b, 0xcc, 0x99, 0x2b, + 0xff, 0x99, 0x55, 0x00, 0x99, 0x55, 0x33, 0x99, 0x55, 0x66, 0x99, 0x55, + 0x99, 0x99, 0x55, 0xcc, 0x99, 0x55, 0xff, 0x99, 0x80, 0x00, 0x99, 0x80, + 0x33, 0x99, 0x80, 0x66, 0x99, 0x80, 0x99, 0x99, 0x80, 0xcc, 0x99, 0x80, + 0xff, 0x99, 0xaa, 0x00, 0x99, 0xaa, 0x33, 0x99, 0xaa, 0x66, 0x99, 0xaa, + 0x99, 0x99, 0xaa, 0xcc, 0x99, 0xaa, 0xff, 0x99, 0xd5, 0x00, 0x99, 0xd5, + 0x33, 0x99, 0xd5, 0x66, 0x99, 0xd5, 0x99, 0x99, 0xd5, 0xcc, 0x99, 0xd5, + 0xff, 0x99, 0xff, 0x00, 0x99, 0xff, 0x33, 0x99, 0xff, 0x66, 0x99, 0xff, + 0x99, 0x99, 0xff, 0xcc, 0x99, 0xff, 0xff, 0xcc, 0x00, 0x00, 0xcc, 0x00, + 0x33, 0xcc, 0x00, 0x66, 0xcc, 0x00, 0x99, 0xcc, 0x00, 0xcc, 0xcc, 0x00, + 0xff, 0xcc, 0x2b, 0x00, 0xcc, 0x2b, 0x33, 0xcc, 0x2b, 0x66, 0xcc, 0x2b, + 0x99, 0xcc, 0x2b, 0xcc, 0xcc, 0x2b, 0xff, 0xcc, 0x55, 0x00, 0xcc, 0x55, + 0x33, 0xcc, 0x55, 0x66, 0xcc, 0x55, 0x99, 0xcc, 0x55, 0xcc, 0xcc, 0x55, + 0xff, 0xcc, 0x80, 0x00, 0xcc, 0x80, 0x33, 0xcc, 0x80, 0x66, 0xcc, 0x80, + 0x99, 0xcc, 0x80, 0xcc, 0xcc, 0x80, 0xff, 0xcc, 0xaa, 0x00, 0xcc, 0xaa, + 0x33, 0xcc, 0xaa, 0x66, 0xcc, 0xaa, 0x99, 0xcc, 0xaa, 0xcc, 0xcc, 0xaa, + 0xff, 0xcc, 0xd5, 0x00, 0xcc, 0xd5, 0x33, 0xcc, 0xd5, 0x66, 0xcc, 0xd5, + 0x99, 0xcc, 0xd5, 0xcc, 0xcc, 0xd5, 0xff, 0xcc, 0xff, 0x00, 0xcc, 0xff, + 0x33, 0xcc, 0xff, 0x66, 0xcc, 0xff, 0x99, 0xcc, 0xff, 0xcc, 0xcc, 0xff, + 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0x33, 0xff, 0x00, 0x66, 0xff, 0x00, + 0x99, 0xff, 0x00, 0xcc, 0xff, 0x00, 0xff, 0xff, 0x2b, 0x00, 0xff, 0x2b, + 0x33, 0xff, 0x2b, 0x66, 0xff, 0x2b, 0x99, 0xff, 0x2b, 0xcc, 0xff, 0x2b, + 0xff, 0xff, 0x55, 0x00, 0xff, 0x55, 0x33, 0xff, 0x55, 0x66, 0xff, 0x55, + 0x99, 0xff, 0x55, 0xcc, 0xff, 0x55, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, + 0x33, 0xff, 0x80, 0x66, 0xff, 0x80, 0x99, 0xff, 0x80, 0xcc, 0xff, 0x80, + 0xff, 0xff, 0xaa, 0x00, 0xff, 0xaa, 0x33, 0xff, 0xaa, 0x66, 0xff, 0xaa, + 0x99, 0xff, 0xaa, 0xcc, 0xff, 0xaa, 0xff, 0xff, 0xd5, 0x00, 0xff, 0xd5, + 0x33, 0xff, 0xd5, 0x66, 0xff, 0xd5, 0x99, 0xff, 0xd5, 0xcc, 0xff, 0xd5, + 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x33, 0xff, 0xff, 0x66, 0xff, 0xff, + 0x99, 0xff, 0xff, 0xcc, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0x00, 0xe7, 0xe9, + 0x13, 0x48, 0x70, 0xa0, 0xc1, 0x82, 0x08, 0x0f, 0xea, 0xd3, 0x97, 0x8c, + 0x5e, 0x32, 0x86, 0x0e, 0x21, 0x3e, 0x6c, 0x38, 0x31, 0x22, 0xc5, 0x7c, + 0xca, 0x04, 0x26, 0xc3, 0x38, 0x8f, 0x23, 0xc6, 0x7c, 0x0d, 0x41, 0xd2, + 0x03, 0xc9, 0x50, 0xe3, 0x48, 0x88, 0x1d, 0x51, 0x4d, 0xc3, 0x96, 0xea, + 0x5c, 0x3e, 0x7a, 0xf2, 0x88, 0x98, 0x72, 0x47, 0x86, 0x86, 0x0e, 0x9b, + 0x3a, 0xde, 0x0c, 0xcc, 0x38, 0x0f, 0x9a, 0x3c, 0x54, 0xee, 0xf2, 0x41, + 0xcb, 0x37, 0xad, 0x17, 0x48, 0x37, 0x9f, 0xe6, 0xe9, 0x78, 0x36, 0xb2, + 0x69, 0xc8, 0x92, 0xdf, 0xce, 0x41, 0x9b, 0x37, 0x2f, 0x99, 0x3c, 0x55, + 0xfa, 0x62, 0x42, 0xa3, 0xf9, 0xc9, 0xe1, 0x4b, 0x93, 0x0f, 0xe5, 0x51, + 0x6b, 0xc8, 0x54, 0x9f, 0xb2, 0x7c, 0x54, 0x89, 0x80, 0x9a, 0x47, 0x06, + 0xd4, 0xd7, 0xa9, 0x4c, 0xbf, 0x0a, 0x03, 0xf7, 0x75, 0xe3, 0x3c, 0x65, + 0xfa, 0x90, 0xb9, 0x99, 0x59, 0x53, 0xc7, 0x4d, 0x20, 0xf3, 0xe8, 0x31, + 0x9d, 0xf7, 0x8c, 0x1d, 0xac, 0x88, 0xdf, 0x52, 0x65, 0x4b, 0x45, 0x15, + 0xa9, 0xd2, 0x67, 0x81, 0xd1, 0x42, 0xcc, 0x37, 0xf0, 0x2a, 0xe5, 0x8d, + 0x59, 0xa7, 0x29, 0xd3, 0xca, 0x15, 0x62, 0x5c, 0x8a, 0x0c, 0xa3, 0x26, + 0x0b, 0xbc, 0xf1, 0x55, 0xbb, 0x7c, 0x44, 0x3e, 0x29, 0x23, 0xd3, 0xb0, + 0x27, 0xe9, 0x93, 0x54, 0x91, 0x4d, 0x6b, 0x37, 0x35, 0xdf, 0xab, 0x69, + 0xf4, 0xf4, 0x6e, 0x1d, 0xe3, 0xf6, 0xac, 0x6b, 0x86, 0x64, 0x47, 0xd6, + 0x92, 0x96, 0x6a, 0x1a, 0xb8, 0xa1, 0xf3, 0x64, 0xce, 0xf3, 0x62, 0xca, + 0x24, 0xc7, 0x85, 0x81, 0x21, 0x8f, 0x1c, 0x9a, 0x51, 0x30, 0xbd, 0xbb, + 0x68, 0x23, 0x56, 0x1d, 0x38, 0xfa, 0x21, 0xf6, 0x8c, 0x03, 0xa7, 0xba, + 0x11, 0x7e, 0xf9, 0x79, 0x24, 0xf8, 0x9d, 0x5f, 0x4b, 0x86, 0x7f, 0x3d, + 0xf8, 0xac, 0x76, 0x65, 0x01, 0x01, 0x00, 0x3b + }; int main() { - std::cout << "OpenXLSX Comprehensive Image Test Matrix" << std::endl; - std::cout << "=========================================" << std::endl; + std::cout << "OpenXLSX Image Test" << std::endl; + std::cout << "===================" << std::endl; // Create workbook and document XLDocument doc; @@ -426,6 +456,8 @@ int main() XLWorksheet sizingSheet = wb.worksheet("Sizing Strategies"); wb.addWorksheet("Units & Scaling"); XLWorksheet unitsSheet = wb.worksheet("Units & Scaling"); + wb.addWorksheet("File Loading"); + XLWorksheet fileSheet = wb.worksheet("File Loading"); std::cout << "\n=== Sheet 1: Anchor Types Comparison ===" << std::endl; @@ -529,6 +561,113 @@ int main() bool success4c = unitsSheet.addImage(cellImage, "A14"); std::cout << " Cell-based sizing: " << (success4c ? "Success" : "Failed") << std::endl; + std::cout << "\n=== Sheet 5: Loading Images from Disk Files ===" << std::endl; + + // Find the images directory by searching upward through the directory tree + std::string imageDir = findImagesDirectory(); + if (imageDir.empty()) { + std::cout << " ERROR: Could not find images directory!" << std::endl; + std::cout << " Searched for: OpenXLSX/Examples/images/ in current " + "directory and up to 10 levels up" << std::endl; + } else { + std::cout << " Found images directory: " << imageDir << std::endl; + } + + // Only proceed with file loading tests if we found the images directory + if (!imageDir.empty()) { + // Image 1: PNG - Original size + std::string pngPath = imageDir + "tiny_png.png"; + fileSheet.cell("A1").value() = "PNG - Original Size"; + bool fileSuccess1 = fileSheet.addImageFromFile(pngPath, "A2"); + std::cout << " PNG original size: " << (fileSuccess1 ? "Success" : "Failed") << std::endl; + + // Image 2: JPEG - Aspect ratio preserved (2x1 cells) + std::string jpegPath = imageDir + "tiny_jpeg.jpg"; + XLImage jpegImage; + std::string jpegId = fileSheet.generateNextImageId(); + if (jpegImage.loadFromFile(jpegPath, jpegId)) { + jpegImage.setDisplaySizeWithAspectRatio(2 * EXCEL_CELL_Y_SPACING_EMUS, 1 * EXCEL_CELL_Y_SPACING_EMUS); + fileSheet.cell("A7").value() = "JPEG - Aspect Ratio (2x1 cells)"; + bool fileSuccess2 = fileSheet.addImage(jpegImage, "A8"); + std::cout << " JPEG aspect ratio: " << (fileSuccess2 ? "Success" : "Failed") << std::endl; + } else { + std::cout << " JPEG aspect ratio: Failed to load image" << std::endl; + } + + // Image 3: GIF - Exact dimensions (may distort) + std::string gifPath = imageDir + "tiny_gif.gif"; + XLImage gifImage; + std::string gifId = fileSheet.generateNextImageId(); + if (gifImage.loadFromFile(gifPath, gifId)) { + gifImage.setDisplayWidth(3 * EXCEL_CELL_Y_SPACING_EMUS); + gifImage.setDisplayHeight(2 * EXCEL_CELL_Y_SPACING_EMUS); + fileSheet.cell("A13").value() = "GIF - Exact Dimensions (3x2 cells)"; + bool fileSuccess3 = fileSheet.addImage(gifImage, "A14"); + std::cout << " GIF exact dimensions: " << (fileSuccess3 ? "Success" : "Failed") << std::endl; + } else { + std::cout << " GIF exact dimensions: Failed to load image" << std::endl; + } + + // Image 4: BMP - Two-cell anchor spanning multiple cells + std::string bmpPath = imageDir + "tiny_bmp.bmp"; + XLImage bmpImage; + std::string bmpId = fileSheet.generateNextImageId(); + if (bmpImage.loadFromFile(bmpPath, bmpId)) { + bmpImage.setDisplaySizeWithAspectRatio(3 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); + fileSheet.cell("A19").value() = "BMP - Two-Cell Anchor (A20:C22)"; + bool fileSuccess4 = fileSheet.addImageTwoCellAnchor(bmpImage, 20, 1, 22, 3); // A20 to C22 + std::cout << " BMP two-cell anchor: " << (fileSuccess4 ? "Success" : "Failed") << std::endl; + } else { + std::cout << " BMP two-cell anchor: Failed to load image" << std::endl; + } + + // Image 5: PNG - Precise positioning with offset (using previously unused addImageWithOffset) + std::string pngPath2 = imageDir + "tiny_png.png"; + XLImage offsetImage; + std::string imageId = fileSheet.generateNextImageId(); + if (offsetImage.loadFromFile(pngPath2, imageId)) { + // Set a specific size for the offset test + offsetImage.setDisplaySizeWithAspectRatio(3 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); + + fileSheet.cell("A25").value() = "PNG - Precise Offset (A26 + 50% cell width/height offset)"; + // Offset by 50% of cell width and height (100000 EMUs each) + int32_t colOffset = EXCEL_CELL_Y_SPACING_EMUS / 2; // 50% of cell width + int32_t rowOffset = EXCEL_CELL_Y_SPACING_EMUS / 2; // 50% of cell height + bool fileSuccess5 = fileSheet.addImageWithOffset(offsetImage, 26, 1, rowOffset, colOffset); + std::cout << " PNG with offset: " << (fileSuccess5 ? "Success" : "Failed") << std::endl; + } else { + std::cout << " PNG with offset: Failed to load image" << std::endl; + } + + // Image 6: JPEG - Larger size with offset (demonstrates variety in offset sizing) + std::string jpegPath2 = imageDir + "tiny_jpeg.jpg"; + XLImage offsetImage2; + std::string imageId2 = fileSheet.generateNextImageId(); + if (offsetImage2.loadFromFile(jpegPath2, imageId2)) { + // Set a larger size for variety + offsetImage2.setDisplaySizeWithAspectRatio(4 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); + + fileSheet.cell("A31").value() = "JPEG - Large Size with Offset (A32 + 25% cell offset)"; + // Smaller offset for variety + int32_t colOffset2 = EXCEL_CELL_Y_SPACING_EMUS / 4; // 25% of cell width + int32_t rowOffset2 = EXCEL_CELL_Y_SPACING_EMUS / 4; // 25% of cell height + bool fileSuccess6 = fileSheet.addImageWithOffset(offsetImage2, 32, 1, rowOffset2, colOffset2); + std::cout << " JPEG large with offset: " << (fileSuccess6 ? "Success" : "Failed") << std::endl; + } else { + std::cout << " JPEG large with offset: Failed to load image" << std::endl; + } + + // Note: The addImageFromFile() function loads images at their original size + // To apply different sizing strategies, we would need to: + // 1. Load the image with addImageFromFile() + // 2. Get the XLImage object from the worksheet + // 3. Modify its display size + // 4. Re-add it with the new size + // + // For now, this demonstrates that the file loading functionality works + // and creates a foundation for more advanced sizing operations. + } // End of file loading tests + // Save the workbook doc.save(); std::cout << "\nWorkbook saved as '" << xlsxFileName << "'" << std::endl; @@ -537,7 +676,7 @@ int main() std::cout << "- Sheet 2: File Formats (PNG, JPEG, GIF, BMP)" << std::endl; std::cout << "- Sheet 3: Sizing Strategies (aspect ratio, original, exact)" << std::endl; std::cout << "- Sheet 4: Units & Scaling (pixels, EMUs, cells)" << std::endl; - std::cout << "\nNote: Both oneCellAnchor and twoCellAnchor are fully implemented and tested." << std::endl; + std::cout << "- Sheet 5: File Loading (loading images from disk files)" << std::endl; return 0; } diff --git a/OpenXLSX/headers/XLImage.hpp b/OpenXLSX/headers/XLImage.hpp index c17f1acd..b64bf162 100644 --- a/OpenXLSX/headers/XLImage.hpp +++ b/OpenXLSX/headers/XLImage.hpp @@ -54,6 +54,135 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. #include "OpenXLSX-Exports.hpp" #include "XLContentTypes.hpp" +/* + * ================================================================================ + * IMAGE EMBEDDING RELATIONSHIP CREATION IN OPENXLSX + * ================================================================================ + * + * When images are embedded in Excel worksheets, OpenXLSX creates TWO types of + * relationships that work together to link worksheets → drawings → images. + * Each relationship type uses different ID management strategies appropriate + * to their scope and purpose. + * + * ┌─────────────────────────────────────────────────────────────────────────────┐ + * │ RELATIONSHIP CREATION FLOW │ + * ├─────────────────────────────────────────────────────────────────────────────┤ + * │ │ + * │ 1. Worksheet.addImage() called │ + * │ ↓ │ + * │ 2. drawingML() ensures drawing.xml exists │ + * │ ↓ │ + * │ 3. Creates Worksheet → Drawing relationship │ + * │ ↓ │ + * │ 4. addImageToDrawingML() adds image to drawing.xml │ + * │ ↓ │ + * │ 5. createDrawingRelationshipsFile() creates Drawing → Image relationships │ + * │ │ + * └─────────────────────────────────────────────────────────────────────────────┘ + * + * RELATIONSHIP TYPE 1: WORKSHEET-LEVEL RELATIONSHIPS + * ────────────────────────────────────────────────────────────────────────────── + * + * Location: Lines 1735-1750 in XLWorksheet::drawingML() + * + * Code: + * std::string drawingRelativePath = getPathARelativeToPathB(drawingFilename, getXmlPath()); + * XLRelationshipItem drawingRelationship; + * if (!m_relationships.targetExists(drawingRelativePath)) + * drawingRelationship = m_relationships.addRelationship(XLRelationshipType::Drawing, drawingRelativePath); + * else + * drawingRelationship = m_relationships.relationshipByTarget(drawingRelativePath); + * + * XMLNode drawing = appendAndGetNode(docElement, "drawing", m_nodeOrder); + * appendAndSetAttribute(drawing, "r:id", drawingRelationship.id()); + * + * Creates: + * File: xl/worksheets/_rels/sheet1.xml.rels + * Content: + * + * Purpose: Links worksheet to drawing.xml file + * Uses: XLRelationships::addRelationship() with intelligent ID management + * + * ID Management Strategy: + * - Uses GetNewRelsID() function with intelligent scanning + * - Checks existing relationships to avoid conflicts + * - Handles complex scenarios with multiple relationship types + * - Ensures uniqueness within the worksheet's relationship file + * + * RELATIONSHIP TYPE 2: DRAWING-LEVEL RELATIONSHIPS + * ────────────────────────────────────────────────────────────────────────────── + * + * Location: Lines 2016-2045 in XLWorksheet::createDrawingRelationshipsFile() + * + * Code: + * int relationshipId = 1; + * for (const auto& image : m_images) { + * std::string imageFilename = "xl/media/image_" + image.id() + image.extension(); + * std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); + * + * relsXml += "\n"; + * relationshipId++; + * } + * + * Creates: + * File: xl/drawings/_rels/drawing1.xml.rels + * Content: + * + * Purpose: Links drawing.xml to image files + * Uses: Simple sequential counter (rId1, rId2, rId3...) + * + * ID Management Strategy: + * - Uses simple sequential numbering starting from 1 + * - Regenerates entire relationship file each time + * - No conflict checking needed (file is recreated) + * - Fast and predictable for drawing-specific relationships + * + * WHY TWO DIFFERENT STRATEGIES? + * ────────────────────────────────────────────────────────────────────────────── + * + * 1. SCOPE DIFFERENCES: + * - Worksheet relationships: Complex, shared namespace with other worksheet parts + * - Drawing relationships: Simple, isolated to drawing objects only + * + * 2. FREQUENCY OF CHANGES: + * - Worksheet relationships: Infrequent, stable (drawing, comments, tables) + * - Drawing relationships: Frequent, dynamic (images added/removed often) + * + * 3. COMPLEXITY REQUIREMENTS: + * - Worksheet relationships: Must avoid conflicts with existing relationships + * - Drawing relationships: Can use simple sequential approach + * + * 4. PERFORMANCE CONSIDERATIONS: + * - Worksheet relationships: Intelligent scanning for uniqueness + * - Drawing relationships: Fast regeneration of entire file + * + * RELATIONSHIP FILE STRUCTURE: + * ────────────────────────────────────────────────────────────────────────────── + * + * xl/worksheets/_rels/sheet1.xml.rels: + * + * + * + * + * + * + * xl/drawings/_rels/drawing1.xml.rels: + * + * + * + * + * + * + * KEY INSIGHT: + * Both relationship types can use the same ID (e.g., "rId1") because they exist + * in separate relationship files with independent namespaces. This is the + * fundamental principle of Excel's relationship scoping system. + * + * ================================================================================ + */ + namespace OpenXLSX { // ========== Excel Constants ========== // diff --git a/OpenXLSX/headers/XLRelationships.hpp b/OpenXLSX/headers/XLRelationships.hpp index bb572ab6..43a29888 100644 --- a/OpenXLSX/headers/XLRelationships.hpp +++ b/OpenXLSX/headers/XLRelationships.hpp @@ -62,6 +62,78 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #include "XLXmlFile.hpp" #include "XLXmlParser.hpp" +/* + * ================================================================================ + * RELATIONSHIP ID SCOPING IN OPENXLSX + * ================================================================================ + * + * In .xlsx files, relationship IDs are scoped to specific XML parts and exist in + * separate namespaces. Each XML part has its own corresponding .rels file containing + * relationship definitions for that specific part. + * + * ┌─────────────────────────────────────────────────────────────────────────────┐ + * │ HIERARCHICAL RELATIONSHIP STRUCTURE │ + * ├─────────────────────────────────────────────────────────────────────────────┤ + * │ │ + * │ _rels/.rels (Package-level relationships) │ + * │ ├─ rId1 → xl/workbook.xml │ + * │ ├─ rId2 → docProps/core.xml │ + * │ └─ rId3 → docProps/app.xml │ + * │ │ + * │ xl/_rels/workbook.xml.rels (Workbook-level relationships) │ + * │ ├─ rId1 → worksheets/sheet1.xml │ + * │ ├─ rId2 → worksheets/sheet2.xml │ + * │ ├─ rId3 → sharedStrings.xml │ + * │ ├─ rId4 → styles.xml │ + * │ └─ rId5 → theme/theme1.xml │ + * │ │ + * │ xl/worksheets/_rels/sheet1.xml.rels (Worksheet-level relationships) │ + * │ ├─ rId1 → ../drawings/drawing1.xml │ + * │ ├─ rId2 → ../comments/comments1.xml │ + * │ └─ rId3 → ../tables/table1.xml │ + * │ │ + * │ xl/drawings/_rels/drawing1.xml.rels (Drawing-level relationships) │ + * │ ├─ rId1 → ../media/image_img1.png │ + * │ ├─ rId2 → ../media/image_img2.jpg │ + * │ └─ rId3 → ../media/image_img3.gif │ + * │ │ + * └─────────────────────────────────────────────────────────────────────────────┘ + * + * RELATIONSHIP ID USAGE IN OPENXLSX: + * + * 1. DOCUMENT-LEVEL RELATIONSHIPS (XLRelationships class): + * - Location: _rels/.rels, xl/_rels/workbook.xml.rels, etc. + * - Purpose: Link core document components (worksheets, styles, themes) + * - ID Generation: Uses GetNewRelsID() with intelligent scanning + * - Scope: Per relationship file + * - Example: rId1 in workbook.xml.rels ≠ rId1 in sheet1.xml.rels + * + * 2. DRAWING-LEVEL RELATIONSHIPS (createDrawingRelationshipsFile): + * - Location: xl/drawings/_rels/drawing1.xml.rels, etc. + * - Purpose: Link drawing objects (images, shapes) to media files + * - ID Generation: Simple sequential counter (rId1, rId2, rId3...) + * - Scope: Per drawing file + * - Example: rId1 in drawing1.xml.rels ≠ rId1 in drawing2.xml.rels + * + * 3. WORKSHEET-LEVEL RELATIONSHIPS (XLWorksheet relationships): + * - Location: xl/worksheets/_rels/sheet1.xml.rels, etc. + * - Purpose: Link worksheet-specific parts (drawings, comments, tables) + * - ID Generation: Uses XLRelationships::addRelationship() + * - Scope: Per worksheet + * + * KEY PRINCIPLES: + * - Each relationship file maintains its own ID namespace + * - IDs are unique only within their respective .rels file + * - Different relationship types use different management strategies + * - Document relationships use complex ID management + * - Drawing relationships use simple sequential numbering + * + * This scoping system ensures that relationship IDs don't conflict across + * different parts of the Excel document structure. + * + * ================================================================================ + */ + namespace OpenXLSX { /** diff --git a/OpenXLSX/headers/XLSheet.hpp b/OpenXLSX/headers/XLSheet.hpp index 4c3f953b..5b47030d 100644 --- a/OpenXLSX/headers/XLSheet.hpp +++ b/OpenXLSX/headers/XLSheet.hpp @@ -1306,6 +1306,11 @@ namespace OpenXLSX * @param rowOffset Offset from the top-left of the cell in EMUs * @param colOffset Offset from the top-left of the cell in EMUs * @return True if successful, false otherwise + * @note Images with offset positioning are constrained to fit within a single cell + * by Excel/OpenOffice. This is the expected behavior - the offset positions + * the image within the cell, but the image size is automatically scaled to + * fit the cell dimensions. This is different from regular addImage() which + * can span multiple cells. */ bool addImageWithOffset(const XLImage& image, uint32_t row, uint16_t column, int32_t rowOffset = 0, int32_t colOffset = 0); @@ -1373,6 +1378,13 @@ namespace OpenXLSX */ std::string generateNextImageId() const; + /** + * @brief Generate a relationship ID based on the current image count + * @return A relationship ID string (e.g., "rId1", "rId2", etc.) + * @note This generates IDs for DrawingML XML based on the current number of images + */ + std::string getRelationshipIdFromImageCount() const; + private: /** * @brief Add an image to the DrawingML diff --git a/OpenXLSX/sources/XLDrawingML.cpp b/OpenXLSX/sources/XLDrawingML.cpp index c9f3e07a..e287abbf 100644 --- a/OpenXLSX/sources/XLDrawingML.cpp +++ b/OpenXLSX/sources/XLDrawingML.cpp @@ -225,15 +225,8 @@ namespace OpenXLSX prstGeom.append_attribute("prst").set_value("rect"); prstGeom.append_child("a:avLst"); - XMLNode solidFill = spPr.append_child("a:solidFill"); - XMLNode srgbClr = solidFill.append_child("a:srgbClr"); - srgbClr.append_attribute("val").set_value("FFFFFF"); - - XMLNode ln = spPr.append_child("a:ln"); - ln.append_attribute("w").set_value("9525"); - XMLNode lnSolidFill = ln.append_child("a:solidFill"); - XMLNode lnSrgbClr = lnSolidFill.append_child("a:srgbClr"); - lnSrgbClr.append_attribute("val").set_value("000000"); + // Use noFill to match other methods (no borders) + spPr.append_child("a:noFill"); // Add clientData element (required for Excel compatibility) anchor.append_child("xdr:clientData"); diff --git a/OpenXLSX/sources/XLSheet.cpp b/OpenXLSX/sources/XLSheet.cpp index fde4d531..262729c3 100644 --- a/OpenXLSX/sources/XLSheet.cpp +++ b/OpenXLSX/sources/XLSheet.cpp @@ -1858,7 +1858,7 @@ bool XLWorksheet::addImage(const XLImage& image, uint32_t row, uint16_t column) // Add image to DrawingML // Use sequential relationship ID that matches createDrawingRelationshipsFile - std::string relationshipId = "rId" + std::to_string(m_images.size()); + std::string relationshipId = getRelationshipIdFromImageCount(); addImageToDrawingML(imageWithId, row, column, relationshipId); return true; @@ -1921,7 +1921,7 @@ bool XLWorksheet::addImageWithOffset(const XLImage& image, uint32_t row, uint16_ XLDrawingML& drawing = drawingML(); // Generate relationship ID - std::string relationshipId = "rId" + std::to_string(m_images.size()); + std::string relationshipId = getRelationshipIdFromImageCount(); // Add image to DrawingML with offset addImageToDrawingMLWithOffset(imageWithId, row, column, relationshipId, rowOffset, colOffset); @@ -1955,7 +1955,7 @@ bool XLWorksheet::addImageTwoCellAnchor(const XLImage& image, XLDrawingML& drawing = drawingML(); // Generate relationship ID - std::string relationshipId = "rId" + std::to_string(m_images.size()); + std::string relationshipId = getRelationshipIdFromImageCount(); // Add image to DrawingML with two-cell anchoring addImageToDrawingMLTwoCellAnchor(imageWithId, fromRow, fromCol, toRow, toCol, relationshipId, @@ -1988,6 +1988,14 @@ std::string XLWorksheet::generateNextImageId() const return parentDoc().generateNextImageId(); } +/** + * @details Generate a relationship ID based on the current image count + */ +std::string XLWorksheet::getRelationshipIdFromImageCount() const +{ + return "rId" + std::to_string(m_images.size()); +} + /** * @details Add an image to the DrawingML */ From 1beab8c8051c8f312d5596f7479cae2cd5b74c7d Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Sat, 4 Oct 2025 20:09:27 -0700 Subject: [PATCH 07/13] Embedded Image Support -- phase VI load Excel with embedded images --- .gitignore | 4 + Examples/Demo11.cpp | 1534 +++++++++-------- .../files/from_excel_embedded_images.xlsx | Bin 0 -> 27825 bytes Examples/images/bmp_40x40.bmp | Bin 0 -> 4854 bytes Examples/images/gif_60x60.gif | Bin 0 -> 1485 bytes Examples/images/jpeg_60x40.jpg | Bin 0 -> 2538 bytes Examples/images/png_40x60.png | Bin 0 -> 1042 bytes Examples/images/tiny_bmp.bmp | Bin 0 -> 1654 bytes Examples/images/tiny_bmp.h | 141 ++ Examples/images/tiny_gif.gif | Bin 0 -> 1076 bytes Examples/images/tiny_gif.h | 93 + Examples/images/tiny_jpeg.h | 100 ++ Examples/images/tiny_jpeg.jpg | Bin 0 -> 1158 bytes Examples/images/tiny_png.h | 48 + Examples/images/tiny_png.png | Bin 0 -> 531 bytes OpenXLSX/headers/XLDocument.hpp | 19 + OpenXLSX/headers/XLImage.hpp | 4 +- OpenXLSX/headers/XLSheet.hpp | 13 + OpenXLSX/sources/XLDocument.cpp | 86 + OpenXLSX/sources/XLDrawingML.cpp | 636 +++---- OpenXLSX/sources/XLImage.cpp | 14 +- OpenXLSX/sources/XLSheet.cpp | 179 +- 22 files changed, 1847 insertions(+), 1024 deletions(-) create mode 100644 Examples/files/from_excel_embedded_images.xlsx create mode 100644 Examples/images/bmp_40x40.bmp create mode 100644 Examples/images/gif_60x60.gif create mode 100644 Examples/images/jpeg_60x40.jpg create mode 100644 Examples/images/png_40x60.png create mode 100644 Examples/images/tiny_bmp.bmp create mode 100644 Examples/images/tiny_bmp.h create mode 100644 Examples/images/tiny_gif.gif create mode 100644 Examples/images/tiny_gif.h create mode 100644 Examples/images/tiny_jpeg.h create mode 100644 Examples/images/tiny_jpeg.jpg create mode 100644 Examples/images/tiny_png.h create mode 100644 Examples/images/tiny_png.png diff --git a/.gitignore b/.gitignore index ec396240..e0a243b6 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,10 @@ !/Examples/*.cpp !/Examples/external !/Examples/external/* +!/Examples/files +!/Examples/files/* +!/Examples/images +!/Examples/images/* # include gnu-make-crutch folder !/gnu-make-crutch diff --git a/Examples/Demo11.cpp b/Examples/Demo11.cpp index 3a43fd09..72b44464 100644 --- a/Examples/Demo11.cpp +++ b/Examples/Demo11.cpp @@ -1,682 +1,852 @@ -/* - * Demo11.cpp - Comprehensive Image Embedding Test Matrix - * - * This demo tests various combinations of image embedding options: - * - Anchor types: oneCellAnchor vs twoCellAnchor - * - File formats: PNG, JPEG, GIF, BMP - * - Sizing strategies: aspect ratio preservation, original size, exact dimensions - * - Units: pixels, EMUs, cell spacing - */ - -#include -#include -#include -#include -#include "OpenXLSX.hpp" - -using namespace OpenXLSX; - -/** - * @brief Search upward through directory tree to find the images folder - * @param relativePath The relative path to search for (e.g., "OpenXLSX/Examples/images/") - * @return The full path to the images folder if found, empty string if not found - */ -std::string findImagesDirectory(const std::string& relativePath = "OpenXLSX/Examples/images/") -{ - const std::filesystem::path currentPath = std::filesystem::current_path(); - std::filesystem::path testPath = currentPath; - std::string result; - - // Search upward through the directory tree - for (int levels = 0; (levels < 10) && result.empty(); ++levels) { - // Append the relative path - const std::filesystem::path testImagePath = testPath / relativePath; - - // Check if the directory exists - if (std::filesystem::exists(testImagePath) && - std::filesystem::is_directory(testImagePath)) { - result = testImagePath.string() + "/"; - } - else{ - testPath = testPath.parent_path(); - } - } - - return result; // Not found -} - - - - // These contain the binary data from the tiny image files in the images directory - - // tiny_png.png - Small PNG image - static const std::vector pngData = { - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0f, - 0x08, 0x02, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x83, 0xf5, 0x00, 0x00, 0x00, - 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, - 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, - 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, - 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, - 0x00, 0x01, 0xa8, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4f, 0x63, 0x7c, 0xff, - 0x64, 0x03, 0x03, 0xcd, 0x00, 0x13, 0x94, 0xa6, 0x0d, 0x00, 0xbb, 0xfd, - 0xf1, 0xfe, 0x7f, 0xa1, 0x5b, 0xa1, 0x02, 0x50, 0x20, 0xc5, 0xb8, 0xba, - 0x88, 0x51, 0x96, 0x81, 0xe1, 0xfb, 0x85, 0x98, 0x6b, 0x67, 0x18, 0xa4, - 0x42, 0x96, 0x48, 0x0a, 0x40, 0x65, 0x40, 0x22, 0xef, 0xd3, 0x8c, 0x1d, - 0xed, 0xa0, 0x7c, 0xce, 0x89, 0x29, 0xc2, 0xfb, 0xa1, 0x6c, 0x06, 0xc5, - 0xe0, 0x17, 0xfd, 0x9e, 0x7f, 0xa0, 0x1c, 0x06, 0x06, 0x16, 0x28, 0xcd, - 0x60, 0xcc, 0x78, 0x22, 0x92, 0x11, 0xca, 0x66, 0x60, 0x38, 0xb2, 0xfc, - 0x5f, 0x68, 0x1f, 0x03, 0xd0, 0x02, 0x11, 0x10, 0xef, 0xfa, 0xb3, 0xf3, - 0x87, 0x24, 0xe1, 0xc6, 0x21, 0x81, 0x17, 0x7c, 0x85, 0x35, 0x7c, 0x0a, - 0xd9, 0x4f, 0x36, 0x18, 0x42, 0xf8, 0x40, 0x9b, 0x24, 0x0a, 0x19, 0x10, - 0x16, 0xe0, 0x08, 0x19, 0x1b, 0x17, 0x46, 0x8d, 0x67, 0x0c, 0x0f, 0x21, - 0x1c, 0xe5, 0x7c, 0xa9, 0xf7, 0xb3, 0x9e, 0x7f, 0x80, 0x70, 0x90, 0x00, - 0xcb, 0xba, 0x99, 0x7c, 0x0c, 0xc1, 0x2f, 0xf2, 0xa1, 0x46, 0x03, 0xc1, - 0xf7, 0xfc, 0x96, 0x4f, 0x0c, 0x6b, 0xf9, 0x4e, 0x41, 0xb9, 0xc4, 0x85, - 0xbb, 0x82, 0xa4, 0x93, 0xdb, 0xfb, 0xf3, 0x87, 0xa0, 0x3c, 0x18, 0x78, - 0xc1, 0x75, 0xf8, 0xf1, 0xb7, 0x48, 0xa4, 0x70, 0x00, 0x01, 0x89, 0x4f, - 0xfd, 0x73, 0xde, 0x99, 0x41, 0x39, 0xb8, 0x4c, 0x3f, 0xb2, 0xe7, 0xff, - 0x0d, 0x63, 0x06, 0x1b, 0x28, 0x8f, 0x81, 0x41, 0x20, 0x4e, 0x8a, 0xa1, - 0xf0, 0x1e, 0xd4, 0x2f, 0x50, 0xf0, 0x9c, 0xe5, 0xbe, 0xec, 0x1f, 0x19, - 0x28, 0x07, 0x3b, 0x80, 0x87, 0xfb, 0xd9, 0xff, 0x16, 0x67, 0xff, 0x43, - 0xd9, 0x40, 0x00, 0x8e, 0x55, 0x28, 0x1b, 0x0c, 0x04, 0x0c, 0xf3, 0x9f, - 0xed, 0x5b, 0xf4, 0x5d, 0x3e, 0x0e, 0xca, 0x47, 0x07, 0xa7, 0xe6, 0xc8, - 0xb4, 0x9d, 0x80, 0x30, 0xff, 0xc4, 0xb5, 0xbc, 0x08, 0x92, 0x00, 0xb1, - 0x70, 0xc4, 0x2a, 0x16, 0x20, 0x10, 0xa7, 0xa8, 0x14, 0xf3, 0xfc, 0x61, - 0x9c, 0x24, 0x94, 0xcf, 0x20, 0xf9, 0x47, 0xf1, 0x31, 0xcb, 0x13, 0xa0, - 0x43, 0xc0, 0x3c, 0xb3, 0x94, 0x27, 0x1b, 0x52, 0x80, 0x34, 0x30, 0x62, - 0xf9, 0xc1, 0x02, 0x20, 0x40, 0x4a, 0x7a, 0xe7, 0x34, 0x48, 0x63, 0xd8, - 0xdd, 0x00, 0x8f, 0x5d, 0x89, 0x6f, 0xb6, 0xb2, 0x5c, 0xcb, 0xb7, 0xc3, - 0xdd, 0x87, 0x05, 0x90, 0x96, 0x9b, 0xec, 0x24, 0x4d, 0xee, 0x3c, 0x3b, - 0x73, 0x1d, 0xca, 0xfb, 0x13, 0x94, 0x0e, 0x4c, 0x21, 0x12, 0x85, 0x08, - 0x0b, 0x58, 0xd6, 0x35, 0x22, 0xd2, 0x3e, 0x10, 0xe0, 0xb3, 0x19, 0x0b, - 0xe0, 0x34, 0x68, 0x95, 0xba, 0x17, 0xf4, 0x0c, 0xca, 0x03, 0xa7, 0x90, - 0x6f, 0xeb, 0x1a, 0x25, 0x02, 0xd6, 0x42, 0x05, 0x18, 0x2c, 0xde, 0x6e, - 0xa8, 0xff, 0x0e, 0x65, 0x43, 0xf3, 0x2a, 0xcd, 0x00, 0x69, 0x21, 0x43, - 0x2a, 0xa0, 0xa5, 0xe9, 0x0c, 0x0c, 0x00, 0xe4, 0x3f, 0x87, 0xe1, 0xb6, - 0x42, 0x8a, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, - 0x42, 0x60, 0x82 - }; - - // tiny_jpeg.jpg - Small JPEG image (actual file data from xxd) - static const std::vector jpegData = { - 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, - 0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xe1, 0x00, 0x22, - 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, - 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, - 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x03, 0x05, 0x03, 0x03, 0x03, 0x03, 0x03, 0x06, 0x04, - 0x04, 0x03, 0x05, 0x07, 0x06, 0x07, 0x07, 0x07, 0x06, 0x07, 0x07, 0x08, - 0x09, 0x0b, 0x09, 0x08, 0x08, 0x0a, 0x08, 0x07, 0x07, 0x0a, 0x0d, 0x0a, - 0x0a, 0x0b, 0x0c, 0x0c, 0x0c, 0x0c, 0x07, 0x09, 0x0e, 0x0f, 0x0d, 0x0c, - 0x0e, 0x0b, 0x0c, 0x0c, 0x0c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x02, 0x02, - 0x02, 0x03, 0x03, 0x03, 0x06, 0x03, 0x03, 0x06, 0x0c, 0x08, 0x07, 0x08, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x12, 0x00, 0x20, 0x03, - 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, - 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, - 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, - 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, - 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, - 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, - 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, - 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, - 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, - 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, - 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, - 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, - 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, - 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, - 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, - 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, - 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, - 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, - 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, - 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, - 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, - 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, - 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, - 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, - 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, - 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, - 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, - 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, - 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, - 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, - 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, - 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, - 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, - 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfd, - 0x5e, 0xf8, 0xcf, 0xf1, 0x17, 0xfe, 0x15, 0x17, 0xc2, 0x5f, 0x12, 0x78, - 0xa7, 0xec, 0x7f, 0xda, 0x1f, 0xf0, 0x8f, 0xe9, 0xd3, 0xea, 0x1f, 0x65, - 0xf3, 0x7c, 0x9f, 0xb4, 0x79, 0x68, 0x5b, 0x66, 0xfd, 0xad, 0xb7, 0x38, - 0xc6, 0x70, 0x71, 0xe8, 0x6b, 0x26, 0x4f, 0x88, 0xfe, 0x29, 0xd6, 0x3c, - 0x59, 0xe2, 0x0d, 0x3f, 0x43, 0xf0, 0xee, 0x83, 0x79, 0x6d, 0xa0, 0x5c, - 0xa5, 0xab, 0xcd, 0x7b, 0xae, 0x4b, 0x6b, 0x24, 0xce, 0xd0, 0x47, 0x37, - 0x08, 0xb6, 0xb2, 0x00, 0x31, 0x20, 0x1c, 0xbf, 0x51, 0xda, 0xb9, 0xdf, - 0xda, 0x7b, 0x4d, 0xf1, 0xaf, 0x8e, 0xbc, 0x3d, 0xe2, 0x3f, 0x06, 0xe9, - 0x3e, 0x1b, 0x5d, 0x4f, 0x45, 0xf1, 0x76, 0x86, 0x34, 0xdb, 0x6d, 0x4e, - 0x2b, 0xa8, 0x22, 0xfe, 0xc9, 0xba, 0x95, 0xe5, 0x8e, 0x79, 0x2e, 0x84, - 0x92, 0xab, 0xb4, 0x2b, 0x13, 0x42, 0xeb, 0xe4, 0x47, 0x23, 0xe5, 0x25, - 0x04, 0x72, 0x95, 0xa9, 0xa6, 0xfc, 0x0f, 0x8b, 0x5b, 0xf1, 0xe7, 0x8c, - 0x35, 0x1d, 0x5c, 0xeb, 0xd6, 0xd0, 0xea, 0x7a, 0x84, 0x52, 0x5a, 0x7d, - 0x8b, 0x5f, 0xbb, 0xb2, 0x8e, 0x78, 0x85, 0xac, 0x08, 0x49, 0x8e, 0xde, - 0x65, 0x5c, 0xef, 0x57, 0x19, 0x61, 0xb8, 0x81, 0xe9, 0x8a, 0xfc, 0x3f, - 0x56, 0xfe, 0x4f, 0xef, 0xba, 0xff, 0x00, 0x83, 0xf9, 0x9f, 0x53, 0xc4, - 0x58, 0x4a, 0xd4, 0xb0, 0x18, 0x47, 0x82, 0x92, 0xf6, 0xb5, 0x2a, 0x4f, - 0x9f, 0x96, 0x49, 0xb5, 0x4f, 0xd9, 0xa7, 0x1e, 0x6f, 0x76, 0x7c, 0x9e, - 0xfd, 0xd6, 0xb1, 0x4d, 0xbd, 0x2f, 0x6b, 0x15, 0x57, 0xf6, 0x8a, 0xbf, - 0xf1, 0x8c, 0xda, 0x3e, 0x9f, 0xe0, 0xdf, 0x0d, 0xc5, 0xab, 0xeb, 0xba, - 0x86, 0x9c, 0xba, 0xb5, 0xe4, 0x1a, 0x9e, 0xa3, 0xfd, 0x9f, 0x69, 0xa4, - 0xdb, 0x33, 0xbc, 0x6b, 0xe7, 0x4f, 0x1c, 0x53, 0x93, 0x23, 0xc9, 0x1c, - 0x8a, 0x88, 0x91, 0xb0, 0x61, 0x14, 0x8c, 0x59, 0x40, 0x1b, 0xba, 0xff, - 0x00, 0x86, 0x3e, 0x34, 0xd5, 0x3c, 0x61, 0xa5, 0xde, 0xae, 0xb7, 0xe1, - 0xfb, 0x8f, 0x0e, 0xea, 0xda, 0x5d, 0xe3, 0xd9, 0xdc, 0xdb, 0x99, 0x0c, - 0xf6, 0xd3, 0x10, 0x15, 0x96, 0x6b, 0x69, 0xca, 0x27, 0x9d, 0x0b, 0xab, - 0xa9, 0x0d, 0xb1, 0x48, 0x3b, 0x95, 0x95, 0x59, 0x58, 0x0e, 0x23, 0xc4, - 0x7e, 0x00, 0xd5, 0x3e, 0x14, 0x7c, 0x46, 0x9b, 0x5c, 0xf0, 0xcf, 0x86, - 0xae, 0x35, 0xff, 0x00, 0x0f, 0xeb, 0x1a, 0x1d, 0xb6, 0x83, 0x7d, 0xa4, - 0xe9, 0x57, 0x70, 0xd9, 0x5f, 0xd8, 0x0b, 0x66, 0x9d, 0xa0, 0x96, 0xdd, - 0xa6, 0x92, 0x28, 0xca, 0x15, 0xb8, 0x74, 0x71, 0xe6, 0xa3, 0xa9, 0x58, - 0xd9, 0x77, 0x1d, 0xc0, 0x5a, 0xfd, 0x98, 0xfc, 0x11, 0xe2, 0x0f, 0x09, - 0x5b, 0x78, 0x9a, 0xe3, 0x5a, 0x87, 0x5c, 0xd3, 0xb4, 0xfd, 0x5b, 0x52, - 0x59, 0xf4, 0x7d, 0x2b, 0x58, 0xf1, 0x0c, 0xda, 0xe5, 0xf6, 0x99, 0x6c, - 0xb0, 0xc7, 0x11, 0x59, 0x66, 0x92, 0x49, 0x42, 0xb3, 0xc8, 0x8f, 0x2e, - 0xc8, 0xe5, 0x91, 0x54, 0x48, 0x06, 0xec, 0x82, 0x07, 0xd7, 0x63, 0x30, - 0x79, 0x7f, 0xf6, 0x73, 0xab, 0x47, 0x92, 0xe9, 0x41, 0xa7, 0xcd, 0xef, - 0xca, 0x4d, 0xfb, 0xf1, 0x71, 0xf6, 0x8e, 0xdc, 0xad, 0xbb, 0x7e, 0xee, - 0xce, 0x31, 0x4f, 0x9e, 0xef, 0xde, 0xf2, 0xf0, 0xaf, 0x13, 0x1e, 0x48, - 0x57, 0x77, 0x95, 0x92, 0x93, 0x4b, 0x46, 0xf9, 0x55, 0xda, 0x76, 0x5b, - 0xca, 0xfd, 0xad, 0xb5, 0x8f, 0x52, 0xa2, 0x8a, 0x2b, 0xe4, 0x4f, 0x4c, - 0x28, 0xa2, 0x8a, 0x00, 0xff, 0xd9 - }; - - // tiny_bmp.bmp - Small BMP image (actual file data from xxd) - static const std::vector bmpData = { - 0x42, 0x4d, 0x76, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, - 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x10, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, - 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, 0xb1, 0x22, 0xb0, 0xdb, 0x91, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x3e, 0x89, - 0xef, 0x6f, 0x44, 0xed, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x4c, 0xb1, - 0x22, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, - 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, - 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0x91, 0xe4, 0xef, 0x5e, - 0xb1, 0x6f, 0x80, 0xd4, 0x91, 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x24, 0x1c, 0xee, - 0xb0, 0xa8, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0x80, 0xdb, 0xef, 0x80, 0xba, 0x4b, 0x91, 0xe4, 0xd0, 0x6f, 0xb1, 0x6f, - 0xb0, 0xe4, 0xb1, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, - 0xef, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0x5e, 0xcb, 0xd0, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, - 0xef, 0x4c, 0xc2, 0xb1, 0xb0, 0xd4, 0x6f, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, - 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, - 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x24, - 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, - 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x9b, 0x89, 0xed, 0xb0, 0xe4, 0xef, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xa0, 0xe4, 0xb1, 0x5e, 0xba, 0x91, 0xb0, - 0xdb, 0x91, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x91, 0xc2, 0x22, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc2, 0x48, 0x3f, 0xb0, 0xe4, 0xb9, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x85, 0x67, 0xed, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0x80, 0xdb, 0xb1, - 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0xb0, 0xe4, 0xef, 0x91, 0xe4, 0xef, - 0x6f, 0xb1, 0x6f, 0x80, 0xdb, 0xb1, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, 0xef, 0xcc, 0x65, 0x9d, - 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, - 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0x5e, 0xcb, 0x91, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x80, 0xcb, 0x6f, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, - 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, - 0xef, 0xcc, 0x65, 0x9d, 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, - 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0x80, - 0xdb, 0xef, 0x5e, 0xb1, 0x4b, 0x4c, 0xba, 0x4b, 0xb0, 0xd4, 0x6f, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, - 0xba, 0x22, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, - 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x4c, 0xb1, 0x4b, 0x6f, 0xb1, 0x22, - 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x91, 0xe4, 0xef, 0x4c, 0xb1, 0x6f, 0x6f, 0xb1, 0x22, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, - 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00 - }; - - // tiny_gif.gif - Small GIF image (actual file data from xxd) - static const std::vector gifData = { - 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x1a, 0x00, 0x10, 0x00, 0x70, 0x00, - 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x2c, 0x00, 0x00, - 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x33, 0x00, 0x00, 0x66, 0x00, 0x00, 0x99, 0x00, 0x00, 0xcc, 0x00, 0x00, - 0xff, 0x00, 0x2b, 0x00, 0x00, 0x2b, 0x33, 0x00, 0x2b, 0x66, 0x00, 0x2b, - 0x99, 0x00, 0x2b, 0xcc, 0x00, 0x2b, 0xff, 0x00, 0x55, 0x00, 0x00, 0x55, - 0x33, 0x00, 0x55, 0x66, 0x00, 0x55, 0x99, 0x00, 0x55, 0xcc, 0x00, 0x55, - 0xff, 0x00, 0x80, 0x00, 0x00, 0x80, 0x33, 0x00, 0x80, 0x66, 0x00, 0x80, - 0x99, 0x00, 0x80, 0xcc, 0x00, 0x80, 0xff, 0x00, 0xaa, 0x00, 0x00, 0xaa, - 0x33, 0x00, 0xaa, 0x66, 0x00, 0xaa, 0x99, 0x00, 0xaa, 0xcc, 0x00, 0xaa, - 0xff, 0x00, 0xd5, 0x00, 0x00, 0xd5, 0x33, 0x00, 0xd5, 0x66, 0x00, 0xd5, - 0x99, 0x00, 0xd5, 0xcc, 0x00, 0xd5, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, - 0x33, 0x00, 0xff, 0x66, 0x00, 0xff, 0x99, 0x00, 0xff, 0xcc, 0x00, 0xff, - 0xff, 0x33, 0x00, 0x00, 0x33, 0x00, 0x33, 0x33, 0x00, 0x66, 0x33, 0x00, - 0x99, 0x33, 0x00, 0xcc, 0x33, 0x00, 0xff, 0x33, 0x2b, 0x00, 0x33, 0x2b, - 0x33, 0x33, 0x2b, 0x66, 0x33, 0x2b, 0x99, 0x33, 0x2b, 0xcc, 0x33, 0x2b, - 0xff, 0x33, 0x55, 0x00, 0x33, 0x55, 0x33, 0x33, 0x55, 0x66, 0x33, 0x55, - 0x99, 0x33, 0x55, 0xcc, 0x33, 0x55, 0xff, 0x33, 0x80, 0x00, 0x33, 0x80, - 0x33, 0x33, 0x80, 0x66, 0x33, 0x80, 0x99, 0x33, 0x80, 0xcc, 0x33, 0x80, - 0xff, 0x33, 0xaa, 0x00, 0x33, 0xaa, 0x33, 0x33, 0xaa, 0x66, 0x33, 0xaa, - 0x99, 0x33, 0xaa, 0xcc, 0x33, 0xaa, 0xff, 0x33, 0xd5, 0x00, 0x33, 0xd5, - 0x33, 0x33, 0xd5, 0x66, 0x33, 0xd5, 0x99, 0x33, 0xd5, 0xcc, 0x33, 0xd5, - 0xff, 0x33, 0xff, 0x00, 0x33, 0xff, 0x33, 0x33, 0xff, 0x66, 0x33, 0xff, - 0x99, 0x33, 0xff, 0xcc, 0x33, 0xff, 0xff, 0x66, 0x00, 0x00, 0x66, 0x00, - 0x33, 0x66, 0x00, 0x66, 0x66, 0x00, 0x99, 0x66, 0x00, 0xcc, 0x66, 0x00, - 0xff, 0x66, 0x2b, 0x00, 0x66, 0x2b, 0x33, 0x66, 0x2b, 0x66, 0x66, 0x2b, - 0x99, 0x66, 0x2b, 0xcc, 0x66, 0x2b, 0xff, 0x66, 0x55, 0x00, 0x66, 0x55, - 0x33, 0x66, 0x55, 0x66, 0x66, 0x55, 0x99, 0x66, 0x55, 0xcc, 0x66, 0x55, - 0xff, 0x66, 0x80, 0x00, 0x66, 0x80, 0x33, 0x66, 0x80, 0x66, 0x66, 0x80, - 0x99, 0x66, 0x80, 0xcc, 0x66, 0x80, 0xff, 0x66, 0xaa, 0x00, 0x66, 0xaa, - 0x33, 0x66, 0xaa, 0x66, 0x66, 0xaa, 0x99, 0x66, 0xaa, 0xcc, 0x66, 0xaa, - 0xff, 0x66, 0xd5, 0x00, 0x66, 0xd5, 0x33, 0x66, 0xd5, 0x66, 0x66, 0xd5, - 0x99, 0x66, 0xd5, 0xcc, 0x66, 0xd5, 0xff, 0x66, 0xff, 0x00, 0x66, 0xff, - 0x33, 0x66, 0xff, 0x66, 0x66, 0xff, 0x99, 0x66, 0xff, 0xcc, 0x66, 0xff, - 0xff, 0x99, 0x00, 0x00, 0x99, 0x00, 0x33, 0x99, 0x00, 0x66, 0x99, 0x00, - 0x99, 0x99, 0x00, 0xcc, 0x99, 0x00, 0xff, 0x99, 0x2b, 0x00, 0x99, 0x2b, - 0x33, 0x99, 0x2b, 0x66, 0x99, 0x2b, 0x99, 0x99, 0x2b, 0xcc, 0x99, 0x2b, - 0xff, 0x99, 0x55, 0x00, 0x99, 0x55, 0x33, 0x99, 0x55, 0x66, 0x99, 0x55, - 0x99, 0x99, 0x55, 0xcc, 0x99, 0x55, 0xff, 0x99, 0x80, 0x00, 0x99, 0x80, - 0x33, 0x99, 0x80, 0x66, 0x99, 0x80, 0x99, 0x99, 0x80, 0xcc, 0x99, 0x80, - 0xff, 0x99, 0xaa, 0x00, 0x99, 0xaa, 0x33, 0x99, 0xaa, 0x66, 0x99, 0xaa, - 0x99, 0x99, 0xaa, 0xcc, 0x99, 0xaa, 0xff, 0x99, 0xd5, 0x00, 0x99, 0xd5, - 0x33, 0x99, 0xd5, 0x66, 0x99, 0xd5, 0x99, 0x99, 0xd5, 0xcc, 0x99, 0xd5, - 0xff, 0x99, 0xff, 0x00, 0x99, 0xff, 0x33, 0x99, 0xff, 0x66, 0x99, 0xff, - 0x99, 0x99, 0xff, 0xcc, 0x99, 0xff, 0xff, 0xcc, 0x00, 0x00, 0xcc, 0x00, - 0x33, 0xcc, 0x00, 0x66, 0xcc, 0x00, 0x99, 0xcc, 0x00, 0xcc, 0xcc, 0x00, - 0xff, 0xcc, 0x2b, 0x00, 0xcc, 0x2b, 0x33, 0xcc, 0x2b, 0x66, 0xcc, 0x2b, - 0x99, 0xcc, 0x2b, 0xcc, 0xcc, 0x2b, 0xff, 0xcc, 0x55, 0x00, 0xcc, 0x55, - 0x33, 0xcc, 0x55, 0x66, 0xcc, 0x55, 0x99, 0xcc, 0x55, 0xcc, 0xcc, 0x55, - 0xff, 0xcc, 0x80, 0x00, 0xcc, 0x80, 0x33, 0xcc, 0x80, 0x66, 0xcc, 0x80, - 0x99, 0xcc, 0x80, 0xcc, 0xcc, 0x80, 0xff, 0xcc, 0xaa, 0x00, 0xcc, 0xaa, - 0x33, 0xcc, 0xaa, 0x66, 0xcc, 0xaa, 0x99, 0xcc, 0xaa, 0xcc, 0xcc, 0xaa, - 0xff, 0xcc, 0xd5, 0x00, 0xcc, 0xd5, 0x33, 0xcc, 0xd5, 0x66, 0xcc, 0xd5, - 0x99, 0xcc, 0xd5, 0xcc, 0xcc, 0xd5, 0xff, 0xcc, 0xff, 0x00, 0xcc, 0xff, - 0x33, 0xcc, 0xff, 0x66, 0xcc, 0xff, 0x99, 0xcc, 0xff, 0xcc, 0xcc, 0xff, - 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0x33, 0xff, 0x00, 0x66, 0xff, 0x00, - 0x99, 0xff, 0x00, 0xcc, 0xff, 0x00, 0xff, 0xff, 0x2b, 0x00, 0xff, 0x2b, - 0x33, 0xff, 0x2b, 0x66, 0xff, 0x2b, 0x99, 0xff, 0x2b, 0xcc, 0xff, 0x2b, - 0xff, 0xff, 0x55, 0x00, 0xff, 0x55, 0x33, 0xff, 0x55, 0x66, 0xff, 0x55, - 0x99, 0xff, 0x55, 0xcc, 0xff, 0x55, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, - 0x33, 0xff, 0x80, 0x66, 0xff, 0x80, 0x99, 0xff, 0x80, 0xcc, 0xff, 0x80, - 0xff, 0xff, 0xaa, 0x00, 0xff, 0xaa, 0x33, 0xff, 0xaa, 0x66, 0xff, 0xaa, - 0x99, 0xff, 0xaa, 0xcc, 0xff, 0xaa, 0xff, 0xff, 0xd5, 0x00, 0xff, 0xd5, - 0x33, 0xff, 0xd5, 0x66, 0xff, 0xd5, 0x99, 0xff, 0xd5, 0xcc, 0xff, 0xd5, - 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x33, 0xff, 0xff, 0x66, 0xff, 0xff, - 0x99, 0xff, 0xff, 0xcc, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0x00, 0xe7, 0xe9, - 0x13, 0x48, 0x70, 0xa0, 0xc1, 0x82, 0x08, 0x0f, 0xea, 0xd3, 0x97, 0x8c, - 0x5e, 0x32, 0x86, 0x0e, 0x21, 0x3e, 0x6c, 0x38, 0x31, 0x22, 0xc5, 0x7c, - 0xca, 0x04, 0x26, 0xc3, 0x38, 0x8f, 0x23, 0xc6, 0x7c, 0x0d, 0x41, 0xd2, - 0x03, 0xc9, 0x50, 0xe3, 0x48, 0x88, 0x1d, 0x51, 0x4d, 0xc3, 0x96, 0xea, - 0x5c, 0x3e, 0x7a, 0xf2, 0x88, 0x98, 0x72, 0x47, 0x86, 0x86, 0x0e, 0x9b, - 0x3a, 0xde, 0x0c, 0xcc, 0x38, 0x0f, 0x9a, 0x3c, 0x54, 0xee, 0xf2, 0x41, - 0xcb, 0x37, 0xad, 0x17, 0x48, 0x37, 0x9f, 0xe6, 0xe9, 0x78, 0x36, 0xb2, - 0x69, 0xc8, 0x92, 0xdf, 0xce, 0x41, 0x9b, 0x37, 0x2f, 0x99, 0x3c, 0x55, - 0xfa, 0x62, 0x42, 0xa3, 0xf9, 0xc9, 0xe1, 0x4b, 0x93, 0x0f, 0xe5, 0x51, - 0x6b, 0xc8, 0x54, 0x9f, 0xb2, 0x7c, 0x54, 0x89, 0x80, 0x9a, 0x47, 0x06, - 0xd4, 0xd7, 0xa9, 0x4c, 0xbf, 0x0a, 0x03, 0xf7, 0x75, 0xe3, 0x3c, 0x65, - 0xfa, 0x90, 0xb9, 0x99, 0x59, 0x53, 0xc7, 0x4d, 0x20, 0xf3, 0xe8, 0x31, - 0x9d, 0xf7, 0x8c, 0x1d, 0xac, 0x88, 0xdf, 0x52, 0x65, 0x4b, 0x45, 0x15, - 0xa9, 0xd2, 0x67, 0x81, 0xd1, 0x42, 0xcc, 0x37, 0xf0, 0x2a, 0xe5, 0x8d, - 0x59, 0xa7, 0x29, 0xd3, 0xca, 0x15, 0x62, 0x5c, 0x8a, 0x0c, 0xa3, 0x26, - 0x0b, 0xbc, 0xf1, 0x55, 0xbb, 0x7c, 0x44, 0x3e, 0x29, 0x23, 0xd3, 0xb0, - 0x27, 0xe9, 0x93, 0x54, 0x91, 0x4d, 0x6b, 0x37, 0x35, 0xdf, 0xab, 0x69, - 0xf4, 0xf4, 0x6e, 0x1d, 0xe3, 0xf6, 0xac, 0x6b, 0x86, 0x64, 0x47, 0xd6, - 0x92, 0x96, 0x6a, 0x1a, 0xb8, 0xa1, 0xf3, 0x64, 0xce, 0xf3, 0x62, 0xca, - 0x24, 0xc7, 0x85, 0x81, 0x21, 0x8f, 0x1c, 0x9a, 0x51, 0x30, 0xbd, 0xbb, - 0x68, 0x23, 0x56, 0x1d, 0x38, 0xfa, 0x21, 0xf6, 0x8c, 0x03, 0xa7, 0xba, - 0x11, 0x7e, 0xf9, 0x79, 0x24, 0xf8, 0x9d, 0x5f, 0x4b, 0x86, 0x7f, 0x3d, - 0xf8, 0xac, 0x76, 0x65, 0x01, 0x01, 0x00, 0x3b - }; - - -int main() -{ - std::cout << "OpenXLSX Image Test" << std::endl; - std::cout << "===================" << std::endl; - - // Create workbook and document - XLDocument doc; - std::string xlsxFileName = "Demo11.xlsx"; - doc.create(xlsxFileName); - auto wb = doc.workbook(); - - // Create test worksheets - wb.addWorksheet("Anchor Types"); - XLWorksheet anchorSheet = wb.worksheet("Anchor Types"); - wb.addWorksheet("File Formats"); - XLWorksheet formatSheet = wb.worksheet("File Formats"); - wb.addWorksheet("Sizing Strategies"); - XLWorksheet sizingSheet = wb.worksheet("Sizing Strategies"); - wb.addWorksheet("Units & Scaling"); - XLWorksheet unitsSheet = wb.worksheet("Units & Scaling"); - wb.addWorksheet("File Loading"); - XLWorksheet fileSheet = wb.worksheet("File Loading"); - - std::cout << "\n=== Sheet 1: Anchor Types Comparison ===" << std::endl; - - // Test 1: oneCellAnchor vs twoCellAnchor with same image - XLImage testImage1(pngData, ImageMimeTypes::PNG); - testImage1.setDisplaySizeWithAspectRatio(2 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); - - // Add headers - anchorSheet.cell("A1").value() = "oneCellAnchor (PNG, preserve aspect ratio within 2x2 cell height bounding box)"; - anchorSheet.cell("A7").value() = "twoCellAnchor (PNG, 4x3 cells)"; - anchorSheet.cell("A13").value() = "oneCellAnchor (JPEG, original size)"; - anchorSheet.cell("A19").value() = "twoCellAnchor (JPEG, 3x2 cells)"; - - // Add images - bool success1 = anchorSheet.addImage(testImage1, "A2"); - std::cout << " Added oneCellAnchor PNG: " << (success1 ? "Success" : "Failed") << std::endl; - - // Add twoCellAnchor image - bool success2 = anchorSheet.addImageTwoCellAnchor(testImage1, 8, 1, 10, 4); // A8 to D10 - std::cout << " Added twoCellAnchor PNG: " << (success2 ? "Success" : "Failed") << std::endl; - - // Add more twoCellAnchor tests - XLImage testImage2(jpegData, ImageMimeTypes::JPEG); - bool success3 = anchorSheet.addImage(testImage2, "A14"); - std::cout << " Added oneCellAnchor JPEG: " << (success3 ? "Success" : "Failed") << std::endl; - - bool success4 = anchorSheet.addImageTwoCellAnchor(testImage2, 20, 1, 21, 3); // A20 to C21 - std::cout << " Added twoCellAnchor JPEG: " << (success4 ? "Success" : "Failed") << std::endl; - - std::cout << "\n=== Sheet 2: File Format Testing ===" << std::endl; - - // Test different file formats - static const std::vector, std::string, std::string>> formats = { - {pngData, "PNG", ImageMimeTypes::PNG}, - {jpegData, "JPEG", ImageMimeTypes::JPEG}, - {gifData, "GIF", ImageMimeTypes::GIF}, - {bmpData, "BMP", ImageMimeTypes::BMP} - }; - - for (size_t i = 0; i < formats.size(); ++i) { - XLImage formatImage(std::get<0>(formats[i]), std::get<2>(formats[i])); - formatImage.setDisplaySizeWithAspectRatio(4 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); - - std::string textCell = "A" + std::to_string(1 + i * 6); - std::string imageCell = "A" + std::to_string(2 + i * 6); - formatSheet.cell(textCell).value() = std::get<1>(formats[i]) + " Format Test"; - - bool success = formatSheet.addImage(formatImage, imageCell); - std::cout << " Added " << std::get<1>(formats[i]) << " image: " << (success ? "Success" : "Failed") << std::endl; - } - - std::cout << "\n=== Sheet 3: Sizing Strategy Testing ===" << std::endl; - - // Test different sizing strategies - XLImage sizingImage(pngData, ImageMimeTypes::PNG); - - // Strategy 1: Preserve aspect ratio + bounding box - sizingImage.setDisplaySizeWithAspectRatio(7 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); - sizingSheet.cell("A1").value() = "Aspect Ratio + Bounding Box (7x3 cell height units)"; - bool success3a = sizingSheet.addImage(sizingImage, "A2"); - std::cout << " Aspect ratio + bounding box: " << (success3a ? "Success" : "Failed") << std::endl; - - // Strategy 2: Original size (no scaling) - XLImage originalImage(pngData, ImageMimeTypes::PNG); - // Don't call setDisplaySize - use original dimensions - sizingSheet.cell("A7").value() = "Original Size (no scaling)"; - bool success3b = sizingSheet.addImage(originalImage, "A8"); - std::cout << " Original size: " << (success3b ? "Success" : "Failed") << std::endl; - - // Strategy 3: Force exact dimensions - XLImage exactImage(pngData, ImageMimeTypes::PNG); - exactImage.setDisplayWidth(11 * EXCEL_CELL_Y_SPACING_EMUS); - exactImage.setDisplayHeight(3 * EXCEL_CELL_Y_SPACING_EMUS); - sizingSheet.cell("A13").value() = "Exact Dimensions (11x3 cell height units, may distort)"; - bool success3c = sizingSheet.addImage(exactImage, "A14"); - std::cout << " Exact dimensions: " << (success3c ? "Success" : "Failed") << std::endl; - - std::cout << "\n=== Sheet 4: Units & Scaling Testing ===" << std::endl; - - // Test different unit systems - XLImage unitsImage(pngData, ImageMimeTypes::PNG); - - // Pixels - unitsImage.setDisplaySizePixelsWithAspectRatio(100, 50); - unitsSheet.cell("A1").value() = "Pixel-based (100x50 pixels)"; - bool success4a = unitsSheet.addImage(unitsImage, "A2"); - std::cout << " Pixel-based sizing: " << (success4a ? "Success" : "Failed") << std::endl; - - // EMUs - XLImage emuImage(pngData, ImageMimeTypes::PNG); - emuImage.setDisplayWidth(476250); // 50 pixels in EMUs - emuImage.setDisplayHeight(238125); // 25 pixels in EMUs - unitsSheet.cell("A7").value() = "EMU-based (476250x238125 EMUs)"; - bool success4b = unitsSheet.addImage(emuImage, "A8"); - std::cout << " EMU-based sizing: " << (success4b ? "Success" : "Failed") << std::endl; - - // Cell spacing - XLImage cellImage(pngData, ImageMimeTypes::PNG); - cellImage.setDisplaySizeWithAspectRatio(19 * EXCEL_CELL_Y_SPACING_EMUS, 5 * EXCEL_CELL_Y_SPACING_EMUS); - unitsSheet.cell("A13").value() = "Cell-based (19x5 cell height units) -- preserve aspect ratio"; - bool success4c = unitsSheet.addImage(cellImage, "A14"); - std::cout << " Cell-based sizing: " << (success4c ? "Success" : "Failed") << std::endl; - - std::cout << "\n=== Sheet 5: Loading Images from Disk Files ===" << std::endl; - - // Find the images directory by searching upward through the directory tree - std::string imageDir = findImagesDirectory(); - if (imageDir.empty()) { - std::cout << " ERROR: Could not find images directory!" << std::endl; - std::cout << " Searched for: OpenXLSX/Examples/images/ in current " - "directory and up to 10 levels up" << std::endl; - } else { - std::cout << " Found images directory: " << imageDir << std::endl; - } - - // Only proceed with file loading tests if we found the images directory - if (!imageDir.empty()) { - // Image 1: PNG - Original size - std::string pngPath = imageDir + "tiny_png.png"; - fileSheet.cell("A1").value() = "PNG - Original Size"; - bool fileSuccess1 = fileSheet.addImageFromFile(pngPath, "A2"); - std::cout << " PNG original size: " << (fileSuccess1 ? "Success" : "Failed") << std::endl; - - // Image 2: JPEG - Aspect ratio preserved (2x1 cells) - std::string jpegPath = imageDir + "tiny_jpeg.jpg"; - XLImage jpegImage; - std::string jpegId = fileSheet.generateNextImageId(); - if (jpegImage.loadFromFile(jpegPath, jpegId)) { - jpegImage.setDisplaySizeWithAspectRatio(2 * EXCEL_CELL_Y_SPACING_EMUS, 1 * EXCEL_CELL_Y_SPACING_EMUS); - fileSheet.cell("A7").value() = "JPEG - Aspect Ratio (2x1 cells)"; - bool fileSuccess2 = fileSheet.addImage(jpegImage, "A8"); - std::cout << " JPEG aspect ratio: " << (fileSuccess2 ? "Success" : "Failed") << std::endl; - } else { - std::cout << " JPEG aspect ratio: Failed to load image" << std::endl; - } - - // Image 3: GIF - Exact dimensions (may distort) - std::string gifPath = imageDir + "tiny_gif.gif"; - XLImage gifImage; - std::string gifId = fileSheet.generateNextImageId(); - if (gifImage.loadFromFile(gifPath, gifId)) { - gifImage.setDisplayWidth(3 * EXCEL_CELL_Y_SPACING_EMUS); - gifImage.setDisplayHeight(2 * EXCEL_CELL_Y_SPACING_EMUS); - fileSheet.cell("A13").value() = "GIF - Exact Dimensions (3x2 cells)"; - bool fileSuccess3 = fileSheet.addImage(gifImage, "A14"); - std::cout << " GIF exact dimensions: " << (fileSuccess3 ? "Success" : "Failed") << std::endl; - } else { - std::cout << " GIF exact dimensions: Failed to load image" << std::endl; - } - - // Image 4: BMP - Two-cell anchor spanning multiple cells - std::string bmpPath = imageDir + "tiny_bmp.bmp"; - XLImage bmpImage; - std::string bmpId = fileSheet.generateNextImageId(); - if (bmpImage.loadFromFile(bmpPath, bmpId)) { - bmpImage.setDisplaySizeWithAspectRatio(3 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); - fileSheet.cell("A19").value() = "BMP - Two-Cell Anchor (A20:C22)"; - bool fileSuccess4 = fileSheet.addImageTwoCellAnchor(bmpImage, 20, 1, 22, 3); // A20 to C22 - std::cout << " BMP two-cell anchor: " << (fileSuccess4 ? "Success" : "Failed") << std::endl; - } else { - std::cout << " BMP two-cell anchor: Failed to load image" << std::endl; - } - - // Image 5: PNG - Precise positioning with offset (using previously unused addImageWithOffset) - std::string pngPath2 = imageDir + "tiny_png.png"; - XLImage offsetImage; - std::string imageId = fileSheet.generateNextImageId(); - if (offsetImage.loadFromFile(pngPath2, imageId)) { - // Set a specific size for the offset test - offsetImage.setDisplaySizeWithAspectRatio(3 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); - - fileSheet.cell("A25").value() = "PNG - Precise Offset (A26 + 50% cell width/height offset)"; - // Offset by 50% of cell width and height (100000 EMUs each) - int32_t colOffset = EXCEL_CELL_Y_SPACING_EMUS / 2; // 50% of cell width - int32_t rowOffset = EXCEL_CELL_Y_SPACING_EMUS / 2; // 50% of cell height - bool fileSuccess5 = fileSheet.addImageWithOffset(offsetImage, 26, 1, rowOffset, colOffset); - std::cout << " PNG with offset: " << (fileSuccess5 ? "Success" : "Failed") << std::endl; - } else { - std::cout << " PNG with offset: Failed to load image" << std::endl; - } - - // Image 6: JPEG - Larger size with offset (demonstrates variety in offset sizing) - std::string jpegPath2 = imageDir + "tiny_jpeg.jpg"; - XLImage offsetImage2; - std::string imageId2 = fileSheet.generateNextImageId(); - if (offsetImage2.loadFromFile(jpegPath2, imageId2)) { - // Set a larger size for variety - offsetImage2.setDisplaySizeWithAspectRatio(4 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); - - fileSheet.cell("A31").value() = "JPEG - Large Size with Offset (A32 + 25% cell offset)"; - // Smaller offset for variety - int32_t colOffset2 = EXCEL_CELL_Y_SPACING_EMUS / 4; // 25% of cell width - int32_t rowOffset2 = EXCEL_CELL_Y_SPACING_EMUS / 4; // 25% of cell height - bool fileSuccess6 = fileSheet.addImageWithOffset(offsetImage2, 32, 1, rowOffset2, colOffset2); - std::cout << " JPEG large with offset: " << (fileSuccess6 ? "Success" : "Failed") << std::endl; - } else { - std::cout << " JPEG large with offset: Failed to load image" << std::endl; - } - - // Note: The addImageFromFile() function loads images at their original size - // To apply different sizing strategies, we would need to: - // 1. Load the image with addImageFromFile() - // 2. Get the XLImage object from the worksheet - // 3. Modify its display size - // 4. Re-add it with the new size - // - // For now, this demonstrates that the file loading functionality works - // and creates a foundation for more advanced sizing operations. - } // End of file loading tests - - // Save the workbook - doc.save(); - std::cout << "\nWorkbook saved as '" << xlsxFileName << "'" << std::endl; - std::cout << "\nTest Matrix Summary:" << std::endl; - std::cout << "- Sheet 1: Anchor Types (oneCellAnchor vs twoCellAnchor)" << std::endl; - std::cout << "- Sheet 2: File Formats (PNG, JPEG, GIF, BMP)" << std::endl; - std::cout << "- Sheet 3: Sizing Strategies (aspect ratio, original, exact)" << std::endl; - std::cout << "- Sheet 4: Units & Scaling (pixels, EMUs, cells)" << std::endl; - std::cout << "- Sheet 5: File Loading (loading images from disk files)" << std::endl; - - return 0; -} +/* + * Demo11.cpp - Comprehensive Image Embedding Test Matrix + * + * This demo tests various combinations of image embedding options: + * - Anchor types: oneCellAnchor vs twoCellAnchor + * - File formats: PNG, JPEG, GIF, BMP + * - Sizing strategies: aspect ratio preservation, original size, exact dimensions + * - Units: pixels, EMUs, cell spacing + */ + +#include +#include +#include +#include +#include "OpenXLSX.hpp" + +using namespace OpenXLSX; + +/** + * @brief Search upward through directory tree to find a directory + * @param relativePath The relative path to search for + * @return The full path to the directory if found, empty string if not found + */ +std::string findDirectory(const std::string& relativePath) +{ + const std::filesystem::path currentPath = std::filesystem::current_path(); + std::filesystem::path testPath = currentPath; + std::string result; + + // Search upward through the directory tree + for (int levels = 0; (levels < 10) && result.empty(); ++levels) { + // Append the relative path + const std::filesystem::path testImagePath = testPath / relativePath; + + // Check if the directory exists + if (std::filesystem::exists(testImagePath) && + std::filesystem::is_directory(testImagePath)) { + result = testImagePath.string() + "/"; + } + else{ + testPath = testPath.parent_path(); + } + } + + return result; // Not found +} + + + + // These contain the binary data from the tiny image files in the images directory + + // tiny_png.png - Small PNG image + static const std::vector pngData = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0f, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x83, 0xf5, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, + 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, + 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, + 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, + 0x00, 0x01, 0xa8, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4f, 0x63, 0x7c, 0xff, + 0x64, 0x03, 0x03, 0xcd, 0x00, 0x13, 0x94, 0xa6, 0x0d, 0x00, 0xbb, 0xfd, + 0xf1, 0xfe, 0x7f, 0xa1, 0x5b, 0xa1, 0x02, 0x50, 0x20, 0xc5, 0xb8, 0xba, + 0x88, 0x51, 0x96, 0x81, 0xe1, 0xfb, 0x85, 0x98, 0x6b, 0x67, 0x18, 0xa4, + 0x42, 0x96, 0x48, 0x0a, 0x40, 0x65, 0x40, 0x22, 0xef, 0xd3, 0x8c, 0x1d, + 0xed, 0xa0, 0x7c, 0xce, 0x89, 0x29, 0xc2, 0xfb, 0xa1, 0x6c, 0x06, 0xc5, + 0xe0, 0x17, 0xfd, 0x9e, 0x7f, 0xa0, 0x1c, 0x06, 0x06, 0x16, 0x28, 0xcd, + 0x60, 0xcc, 0x78, 0x22, 0x92, 0x11, 0xca, 0x66, 0x60, 0x38, 0xb2, 0xfc, + 0x5f, 0x68, 0x1f, 0x03, 0xd0, 0x02, 0x11, 0x10, 0xef, 0xfa, 0xb3, 0xf3, + 0x87, 0x24, 0xe1, 0xc6, 0x21, 0x81, 0x17, 0x7c, 0x85, 0x35, 0x7c, 0x0a, + 0xd9, 0x4f, 0x36, 0x18, 0x42, 0xf8, 0x40, 0x9b, 0x24, 0x0a, 0x19, 0x10, + 0x16, 0xe0, 0x08, 0x19, 0x1b, 0x17, 0x46, 0x8d, 0x67, 0x0c, 0x0f, 0x21, + 0x1c, 0xe5, 0x7c, 0xa9, 0xf7, 0xb3, 0x9e, 0x7f, 0x80, 0x70, 0x90, 0x00, + 0xcb, 0xba, 0x99, 0x7c, 0x0c, 0xc1, 0x2f, 0xf2, 0xa1, 0x46, 0x03, 0xc1, + 0xf7, 0xfc, 0x96, 0x4f, 0x0c, 0x6b, 0xf9, 0x4e, 0x41, 0xb9, 0xc4, 0x85, + 0xbb, 0x82, 0xa4, 0x93, 0xdb, 0xfb, 0xf3, 0x87, 0xa0, 0x3c, 0x18, 0x78, + 0xc1, 0x75, 0xf8, 0xf1, 0xb7, 0x48, 0xa4, 0x70, 0x00, 0x01, 0x89, 0x4f, + 0xfd, 0x73, 0xde, 0x99, 0x41, 0x39, 0xb8, 0x4c, 0x3f, 0xb2, 0xe7, 0xff, + 0x0d, 0x63, 0x06, 0x1b, 0x28, 0x8f, 0x81, 0x41, 0x20, 0x4e, 0x8a, 0xa1, + 0xf0, 0x1e, 0xd4, 0x2f, 0x50, 0xf0, 0x9c, 0xe5, 0xbe, 0xec, 0x1f, 0x19, + 0x28, 0x07, 0x3b, 0x80, 0x87, 0xfb, 0xd9, 0xff, 0x16, 0x67, 0xff, 0x43, + 0xd9, 0x40, 0x00, 0x8e, 0x55, 0x28, 0x1b, 0x0c, 0x04, 0x0c, 0xf3, 0x9f, + 0xed, 0x5b, 0xf4, 0x5d, 0x3e, 0x0e, 0xca, 0x47, 0x07, 0xa7, 0xe6, 0xc8, + 0xb4, 0x9d, 0x80, 0x30, 0xff, 0xc4, 0xb5, 0xbc, 0x08, 0x92, 0x00, 0xb1, + 0x70, 0xc4, 0x2a, 0x16, 0x20, 0x10, 0xa7, 0xa8, 0x14, 0xf3, 0xfc, 0x61, + 0x9c, 0x24, 0x94, 0xcf, 0x20, 0xf9, 0x47, 0xf1, 0x31, 0xcb, 0x13, 0xa0, + 0x43, 0xc0, 0x3c, 0xb3, 0x94, 0x27, 0x1b, 0x52, 0x80, 0x34, 0x30, 0x62, + 0xf9, 0xc1, 0x02, 0x20, 0x40, 0x4a, 0x7a, 0xe7, 0x34, 0x48, 0x63, 0xd8, + 0xdd, 0x00, 0x8f, 0x5d, 0x89, 0x6f, 0xb6, 0xb2, 0x5c, 0xcb, 0xb7, 0xc3, + 0xdd, 0x87, 0x05, 0x90, 0x96, 0x9b, 0xec, 0x24, 0x4d, 0xee, 0x3c, 0x3b, + 0x73, 0x1d, 0xca, 0xfb, 0x13, 0x94, 0x0e, 0x4c, 0x21, 0x12, 0x85, 0x08, + 0x0b, 0x58, 0xd6, 0x35, 0x22, 0xd2, 0x3e, 0x10, 0xe0, 0xb3, 0x19, 0x0b, + 0xe0, 0x34, 0x68, 0x95, 0xba, 0x17, 0xf4, 0x0c, 0xca, 0x03, 0xa7, 0x90, + 0x6f, 0xeb, 0x1a, 0x25, 0x02, 0xd6, 0x42, 0x05, 0x18, 0x2c, 0xde, 0x6e, + 0xa8, 0xff, 0x0e, 0x65, 0x43, 0xf3, 0x2a, 0xcd, 0x00, 0x69, 0x21, 0x43, + 0x2a, 0xa0, 0xa5, 0xe9, 0x0c, 0x0c, 0x00, 0xe4, 0x3f, 0x87, 0xe1, 0xb6, + 0x42, 0x8a, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, + 0x42, 0x60, 0x82 + }; + + // tiny_jpeg.jpg - Small JPEG image (actual file data from xxd) + static const std::vector jpegData = { + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, + 0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xe1, 0x00, 0x22, + 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, + 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x03, 0x05, 0x03, 0x03, 0x03, 0x03, 0x03, 0x06, 0x04, + 0x04, 0x03, 0x05, 0x07, 0x06, 0x07, 0x07, 0x07, 0x06, 0x07, 0x07, 0x08, + 0x09, 0x0b, 0x09, 0x08, 0x08, 0x0a, 0x08, 0x07, 0x07, 0x0a, 0x0d, 0x0a, + 0x0a, 0x0b, 0x0c, 0x0c, 0x0c, 0x0c, 0x07, 0x09, 0x0e, 0x0f, 0x0d, 0x0c, + 0x0e, 0x0b, 0x0c, 0x0c, 0x0c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x02, 0x02, + 0x02, 0x03, 0x03, 0x03, 0x06, 0x03, 0x03, 0x06, 0x0c, 0x08, 0x07, 0x08, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x12, 0x00, 0x20, 0x03, + 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, + 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, + 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, + 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, + 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, + 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, + 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, + 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, + 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, + 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, + 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, + 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, + 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, + 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, + 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, + 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, + 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, + 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, + 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, + 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, + 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, + 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfd, + 0x5e, 0xf8, 0xcf, 0xf1, 0x17, 0xfe, 0x15, 0x17, 0xc2, 0x5f, 0x12, 0x78, + 0xa7, 0xec, 0x7f, 0xda, 0x1f, 0xf0, 0x8f, 0xe9, 0xd3, 0xea, 0x1f, 0x65, + 0xf3, 0x7c, 0x9f, 0xb4, 0x79, 0x68, 0x5b, 0x66, 0xfd, 0xad, 0xb7, 0x38, + 0xc6, 0x70, 0x71, 0xe8, 0x6b, 0x26, 0x4f, 0x88, 0xfe, 0x29, 0xd6, 0x3c, + 0x59, 0xe2, 0x0d, 0x3f, 0x43, 0xf0, 0xee, 0x83, 0x79, 0x6d, 0xa0, 0x5c, + 0xa5, 0xab, 0xcd, 0x7b, 0xae, 0x4b, 0x6b, 0x24, 0xce, 0xd0, 0x47, 0x37, + 0x08, 0xb6, 0xb2, 0x00, 0x31, 0x20, 0x1c, 0xbf, 0x51, 0xda, 0xb9, 0xdf, + 0xda, 0x7b, 0x4d, 0xf1, 0xaf, 0x8e, 0xbc, 0x3d, 0xe2, 0x3f, 0x06, 0xe9, + 0x3e, 0x1b, 0x5d, 0x4f, 0x45, 0xf1, 0x76, 0x86, 0x34, 0xdb, 0x6d, 0x4e, + 0x2b, 0xa8, 0x22, 0xfe, 0xc9, 0xba, 0x95, 0xe5, 0x8e, 0x79, 0x2e, 0x84, + 0x92, 0xab, 0xb4, 0x2b, 0x13, 0x42, 0xeb, 0xe4, 0x47, 0x23, 0xe5, 0x25, + 0x04, 0x72, 0x95, 0xa9, 0xa6, 0xfc, 0x0f, 0x8b, 0x5b, 0xf1, 0xe7, 0x8c, + 0x35, 0x1d, 0x5c, 0xeb, 0xd6, 0xd0, 0xea, 0x7a, 0x84, 0x52, 0x5a, 0x7d, + 0x8b, 0x5f, 0xbb, 0xb2, 0x8e, 0x78, 0x85, 0xac, 0x08, 0x49, 0x8e, 0xde, + 0x65, 0x5c, 0xef, 0x57, 0x19, 0x61, 0xb8, 0x81, 0xe9, 0x8a, 0xfc, 0x3f, + 0x56, 0xfe, 0x4f, 0xef, 0xba, 0xff, 0x00, 0x83, 0xf9, 0x9f, 0x53, 0xc4, + 0x58, 0x4a, 0xd4, 0xb0, 0x18, 0x47, 0x82, 0x92, 0xf6, 0xb5, 0x2a, 0x4f, + 0x9f, 0x96, 0x49, 0xb5, 0x4f, 0xd9, 0xa7, 0x1e, 0x6f, 0x76, 0x7c, 0x9e, + 0xfd, 0xd6, 0xb1, 0x4d, 0xbd, 0x2f, 0x6b, 0x15, 0x57, 0xf6, 0x8a, 0xbf, + 0xf1, 0x8c, 0xda, 0x3e, 0x9f, 0xe0, 0xdf, 0x0d, 0xc5, 0xab, 0xeb, 0xba, + 0x86, 0x9c, 0xba, 0xb5, 0xe4, 0x1a, 0x9e, 0xa3, 0xfd, 0x9f, 0x69, 0xa4, + 0xdb, 0x33, 0xbc, 0x6b, 0xe7, 0x4f, 0x1c, 0x53, 0x93, 0x23, 0xc9, 0x1c, + 0x8a, 0x88, 0x91, 0xb0, 0x61, 0x14, 0x8c, 0x59, 0x40, 0x1b, 0xba, 0xff, + 0x00, 0x86, 0x3e, 0x34, 0xd5, 0x3c, 0x61, 0xa5, 0xde, 0xae, 0xb7, 0xe1, + 0xfb, 0x8f, 0x0e, 0xea, 0xda, 0x5d, 0xe3, 0xd9, 0xdc, 0xdb, 0x99, 0x0c, + 0xf6, 0xd3, 0x10, 0x15, 0x96, 0x6b, 0x69, 0xca, 0x27, 0x9d, 0x0b, 0xab, + 0xa9, 0x0d, 0xb1, 0x48, 0x3b, 0x95, 0x95, 0x59, 0x58, 0x0e, 0x23, 0xc4, + 0x7e, 0x00, 0xd5, 0x3e, 0x14, 0x7c, 0x46, 0x9b, 0x5c, 0xf0, 0xcf, 0x86, + 0xae, 0x35, 0xff, 0x00, 0x0f, 0xeb, 0x1a, 0x1d, 0xb6, 0x83, 0x7d, 0xa4, + 0xe9, 0x57, 0x70, 0xd9, 0x5f, 0xd8, 0x0b, 0x66, 0x9d, 0xa0, 0x96, 0xdd, + 0xa6, 0x92, 0x28, 0xca, 0x15, 0xb8, 0x74, 0x71, 0xe6, 0xa3, 0xa9, 0x58, + 0xd9, 0x77, 0x1d, 0xc0, 0x5a, 0xfd, 0x98, 0xfc, 0x11, 0xe2, 0x0f, 0x09, + 0x5b, 0x78, 0x9a, 0xe3, 0x5a, 0x87, 0x5c, 0xd3, 0xb4, 0xfd, 0x5b, 0x52, + 0x59, 0xf4, 0x7d, 0x2b, 0x58, 0xf1, 0x0c, 0xda, 0xe5, 0xf6, 0x99, 0x6c, + 0xb0, 0xc7, 0x11, 0x59, 0x66, 0x92, 0x49, 0x42, 0xb3, 0xc8, 0x8f, 0x2e, + 0xc8, 0xe5, 0x91, 0x54, 0x48, 0x06, 0xec, 0x82, 0x07, 0xd7, 0x63, 0x30, + 0x79, 0x7f, 0xf6, 0x73, 0xab, 0x47, 0x92, 0xe9, 0x41, 0xa7, 0xcd, 0xef, + 0xca, 0x4d, 0xfb, 0xf1, 0x71, 0xf6, 0x8e, 0xdc, 0xad, 0xbb, 0x7e, 0xee, + 0xce, 0x31, 0x4f, 0x9e, 0xef, 0xde, 0xf2, 0xf0, 0xaf, 0x13, 0x1e, 0x48, + 0x57, 0x77, 0x95, 0x92, 0x93, 0x4b, 0x46, 0xf9, 0x55, 0xda, 0x76, 0x5b, + 0xca, 0xfd, 0xad, 0xb5, 0x8f, 0x52, 0xa2, 0x8a, 0x2b, 0xe4, 0x4f, 0x4c, + 0x28, 0xa2, 0x8a, 0x00, 0xff, 0xd9 + }; + + // tiny_bmp.bmp - Small BMP image (actual file data from xxd) + static const std::vector bmpData = { + 0x42, 0x4d, 0x76, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, + 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, 0xb1, 0x22, 0xb0, 0xdb, 0x91, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x3e, 0x89, + 0xef, 0x6f, 0x44, 0xed, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x4c, 0xb1, + 0x22, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0x91, 0xe4, 0xef, 0x5e, + 0xb1, 0x6f, 0x80, 0xd4, 0x91, 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x24, 0x1c, 0xee, + 0xb0, 0xa8, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0x80, 0xdb, 0xef, 0x80, 0xba, 0x4b, 0x91, 0xe4, 0xd0, 0x6f, 0xb1, 0x6f, + 0xb0, 0xe4, 0xb1, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, + 0xef, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0x5e, 0xcb, 0xd0, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, + 0xef, 0x4c, 0xc2, 0xb1, 0xb0, 0xd4, 0x6f, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, + 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, + 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x9b, 0x89, 0xed, 0xb0, 0xe4, 0xef, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xa0, 0xe4, 0xb1, 0x5e, 0xba, 0x91, 0xb0, + 0xdb, 0x91, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x91, 0xc2, 0x22, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc2, 0x48, 0x3f, 0xb0, 0xe4, 0xb9, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x85, 0x67, 0xed, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0x80, 0xdb, 0xb1, + 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0xb0, 0xe4, 0xef, 0x91, 0xe4, 0xef, + 0x6f, 0xb1, 0x6f, 0x80, 0xdb, 0xb1, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, 0xef, 0xcc, 0x65, 0x9d, + 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, + 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0x5e, 0xcb, 0x91, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x80, 0xcb, 0x6f, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, + 0xef, 0xcc, 0x65, 0x9d, 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0x80, + 0xdb, 0xef, 0x5e, 0xb1, 0x4b, 0x4c, 0xba, 0x4b, 0xb0, 0xd4, 0x6f, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, + 0xba, 0x22, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x4c, 0xb1, 0x4b, 0x6f, 0xb1, 0x22, + 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x91, 0xe4, 0xef, 0x4c, 0xb1, 0x6f, 0x6f, 0xb1, 0x22, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, + 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00 + }; + + // tiny_gif.gif - Small GIF image (actual file data from xxd) + static const std::vector gifData = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x1a, 0x00, 0x10, 0x00, 0x70, 0x00, + 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x2c, 0x00, 0x00, + 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x66, 0x00, 0x00, 0x99, 0x00, 0x00, 0xcc, 0x00, 0x00, + 0xff, 0x00, 0x2b, 0x00, 0x00, 0x2b, 0x33, 0x00, 0x2b, 0x66, 0x00, 0x2b, + 0x99, 0x00, 0x2b, 0xcc, 0x00, 0x2b, 0xff, 0x00, 0x55, 0x00, 0x00, 0x55, + 0x33, 0x00, 0x55, 0x66, 0x00, 0x55, 0x99, 0x00, 0x55, 0xcc, 0x00, 0x55, + 0xff, 0x00, 0x80, 0x00, 0x00, 0x80, 0x33, 0x00, 0x80, 0x66, 0x00, 0x80, + 0x99, 0x00, 0x80, 0xcc, 0x00, 0x80, 0xff, 0x00, 0xaa, 0x00, 0x00, 0xaa, + 0x33, 0x00, 0xaa, 0x66, 0x00, 0xaa, 0x99, 0x00, 0xaa, 0xcc, 0x00, 0xaa, + 0xff, 0x00, 0xd5, 0x00, 0x00, 0xd5, 0x33, 0x00, 0xd5, 0x66, 0x00, 0xd5, + 0x99, 0x00, 0xd5, 0xcc, 0x00, 0xd5, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, + 0x33, 0x00, 0xff, 0x66, 0x00, 0xff, 0x99, 0x00, 0xff, 0xcc, 0x00, 0xff, + 0xff, 0x33, 0x00, 0x00, 0x33, 0x00, 0x33, 0x33, 0x00, 0x66, 0x33, 0x00, + 0x99, 0x33, 0x00, 0xcc, 0x33, 0x00, 0xff, 0x33, 0x2b, 0x00, 0x33, 0x2b, + 0x33, 0x33, 0x2b, 0x66, 0x33, 0x2b, 0x99, 0x33, 0x2b, 0xcc, 0x33, 0x2b, + 0xff, 0x33, 0x55, 0x00, 0x33, 0x55, 0x33, 0x33, 0x55, 0x66, 0x33, 0x55, + 0x99, 0x33, 0x55, 0xcc, 0x33, 0x55, 0xff, 0x33, 0x80, 0x00, 0x33, 0x80, + 0x33, 0x33, 0x80, 0x66, 0x33, 0x80, 0x99, 0x33, 0x80, 0xcc, 0x33, 0x80, + 0xff, 0x33, 0xaa, 0x00, 0x33, 0xaa, 0x33, 0x33, 0xaa, 0x66, 0x33, 0xaa, + 0x99, 0x33, 0xaa, 0xcc, 0x33, 0xaa, 0xff, 0x33, 0xd5, 0x00, 0x33, 0xd5, + 0x33, 0x33, 0xd5, 0x66, 0x33, 0xd5, 0x99, 0x33, 0xd5, 0xcc, 0x33, 0xd5, + 0xff, 0x33, 0xff, 0x00, 0x33, 0xff, 0x33, 0x33, 0xff, 0x66, 0x33, 0xff, + 0x99, 0x33, 0xff, 0xcc, 0x33, 0xff, 0xff, 0x66, 0x00, 0x00, 0x66, 0x00, + 0x33, 0x66, 0x00, 0x66, 0x66, 0x00, 0x99, 0x66, 0x00, 0xcc, 0x66, 0x00, + 0xff, 0x66, 0x2b, 0x00, 0x66, 0x2b, 0x33, 0x66, 0x2b, 0x66, 0x66, 0x2b, + 0x99, 0x66, 0x2b, 0xcc, 0x66, 0x2b, 0xff, 0x66, 0x55, 0x00, 0x66, 0x55, + 0x33, 0x66, 0x55, 0x66, 0x66, 0x55, 0x99, 0x66, 0x55, 0xcc, 0x66, 0x55, + 0xff, 0x66, 0x80, 0x00, 0x66, 0x80, 0x33, 0x66, 0x80, 0x66, 0x66, 0x80, + 0x99, 0x66, 0x80, 0xcc, 0x66, 0x80, 0xff, 0x66, 0xaa, 0x00, 0x66, 0xaa, + 0x33, 0x66, 0xaa, 0x66, 0x66, 0xaa, 0x99, 0x66, 0xaa, 0xcc, 0x66, 0xaa, + 0xff, 0x66, 0xd5, 0x00, 0x66, 0xd5, 0x33, 0x66, 0xd5, 0x66, 0x66, 0xd5, + 0x99, 0x66, 0xd5, 0xcc, 0x66, 0xd5, 0xff, 0x66, 0xff, 0x00, 0x66, 0xff, + 0x33, 0x66, 0xff, 0x66, 0x66, 0xff, 0x99, 0x66, 0xff, 0xcc, 0x66, 0xff, + 0xff, 0x99, 0x00, 0x00, 0x99, 0x00, 0x33, 0x99, 0x00, 0x66, 0x99, 0x00, + 0x99, 0x99, 0x00, 0xcc, 0x99, 0x00, 0xff, 0x99, 0x2b, 0x00, 0x99, 0x2b, + 0x33, 0x99, 0x2b, 0x66, 0x99, 0x2b, 0x99, 0x99, 0x2b, 0xcc, 0x99, 0x2b, + 0xff, 0x99, 0x55, 0x00, 0x99, 0x55, 0x33, 0x99, 0x55, 0x66, 0x99, 0x55, + 0x99, 0x99, 0x55, 0xcc, 0x99, 0x55, 0xff, 0x99, 0x80, 0x00, 0x99, 0x80, + 0x33, 0x99, 0x80, 0x66, 0x99, 0x80, 0x99, 0x99, 0x80, 0xcc, 0x99, 0x80, + 0xff, 0x99, 0xaa, 0x00, 0x99, 0xaa, 0x33, 0x99, 0xaa, 0x66, 0x99, 0xaa, + 0x99, 0x99, 0xaa, 0xcc, 0x99, 0xaa, 0xff, 0x99, 0xd5, 0x00, 0x99, 0xd5, + 0x33, 0x99, 0xd5, 0x66, 0x99, 0xd5, 0x99, 0x99, 0xd5, 0xcc, 0x99, 0xd5, + 0xff, 0x99, 0xff, 0x00, 0x99, 0xff, 0x33, 0x99, 0xff, 0x66, 0x99, 0xff, + 0x99, 0x99, 0xff, 0xcc, 0x99, 0xff, 0xff, 0xcc, 0x00, 0x00, 0xcc, 0x00, + 0x33, 0xcc, 0x00, 0x66, 0xcc, 0x00, 0x99, 0xcc, 0x00, 0xcc, 0xcc, 0x00, + 0xff, 0xcc, 0x2b, 0x00, 0xcc, 0x2b, 0x33, 0xcc, 0x2b, 0x66, 0xcc, 0x2b, + 0x99, 0xcc, 0x2b, 0xcc, 0xcc, 0x2b, 0xff, 0xcc, 0x55, 0x00, 0xcc, 0x55, + 0x33, 0xcc, 0x55, 0x66, 0xcc, 0x55, 0x99, 0xcc, 0x55, 0xcc, 0xcc, 0x55, + 0xff, 0xcc, 0x80, 0x00, 0xcc, 0x80, 0x33, 0xcc, 0x80, 0x66, 0xcc, 0x80, + 0x99, 0xcc, 0x80, 0xcc, 0xcc, 0x80, 0xff, 0xcc, 0xaa, 0x00, 0xcc, 0xaa, + 0x33, 0xcc, 0xaa, 0x66, 0xcc, 0xaa, 0x99, 0xcc, 0xaa, 0xcc, 0xcc, 0xaa, + 0xff, 0xcc, 0xd5, 0x00, 0xcc, 0xd5, 0x33, 0xcc, 0xd5, 0x66, 0xcc, 0xd5, + 0x99, 0xcc, 0xd5, 0xcc, 0xcc, 0xd5, 0xff, 0xcc, 0xff, 0x00, 0xcc, 0xff, + 0x33, 0xcc, 0xff, 0x66, 0xcc, 0xff, 0x99, 0xcc, 0xff, 0xcc, 0xcc, 0xff, + 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0x33, 0xff, 0x00, 0x66, 0xff, 0x00, + 0x99, 0xff, 0x00, 0xcc, 0xff, 0x00, 0xff, 0xff, 0x2b, 0x00, 0xff, 0x2b, + 0x33, 0xff, 0x2b, 0x66, 0xff, 0x2b, 0x99, 0xff, 0x2b, 0xcc, 0xff, 0x2b, + 0xff, 0xff, 0x55, 0x00, 0xff, 0x55, 0x33, 0xff, 0x55, 0x66, 0xff, 0x55, + 0x99, 0xff, 0x55, 0xcc, 0xff, 0x55, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, + 0x33, 0xff, 0x80, 0x66, 0xff, 0x80, 0x99, 0xff, 0x80, 0xcc, 0xff, 0x80, + 0xff, 0xff, 0xaa, 0x00, 0xff, 0xaa, 0x33, 0xff, 0xaa, 0x66, 0xff, 0xaa, + 0x99, 0xff, 0xaa, 0xcc, 0xff, 0xaa, 0xff, 0xff, 0xd5, 0x00, 0xff, 0xd5, + 0x33, 0xff, 0xd5, 0x66, 0xff, 0xd5, 0x99, 0xff, 0xd5, 0xcc, 0xff, 0xd5, + 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x33, 0xff, 0xff, 0x66, 0xff, 0xff, + 0x99, 0xff, 0xff, 0xcc, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0x00, 0xe7, 0xe9, + 0x13, 0x48, 0x70, 0xa0, 0xc1, 0x82, 0x08, 0x0f, 0xea, 0xd3, 0x97, 0x8c, + 0x5e, 0x32, 0x86, 0x0e, 0x21, 0x3e, 0x6c, 0x38, 0x31, 0x22, 0xc5, 0x7c, + 0xca, 0x04, 0x26, 0xc3, 0x38, 0x8f, 0x23, 0xc6, 0x7c, 0x0d, 0x41, 0xd2, + 0x03, 0xc9, 0x50, 0xe3, 0x48, 0x88, 0x1d, 0x51, 0x4d, 0xc3, 0x96, 0xea, + 0x5c, 0x3e, 0x7a, 0xf2, 0x88, 0x98, 0x72, 0x47, 0x86, 0x86, 0x0e, 0x9b, + 0x3a, 0xde, 0x0c, 0xcc, 0x38, 0x0f, 0x9a, 0x3c, 0x54, 0xee, 0xf2, 0x41, + 0xcb, 0x37, 0xad, 0x17, 0x48, 0x37, 0x9f, 0xe6, 0xe9, 0x78, 0x36, 0xb2, + 0x69, 0xc8, 0x92, 0xdf, 0xce, 0x41, 0x9b, 0x37, 0x2f, 0x99, 0x3c, 0x55, + 0xfa, 0x62, 0x42, 0xa3, 0xf9, 0xc9, 0xe1, 0x4b, 0x93, 0x0f, 0xe5, 0x51, + 0x6b, 0xc8, 0x54, 0x9f, 0xb2, 0x7c, 0x54, 0x89, 0x80, 0x9a, 0x47, 0x06, + 0xd4, 0xd7, 0xa9, 0x4c, 0xbf, 0x0a, 0x03, 0xf7, 0x75, 0xe3, 0x3c, 0x65, + 0xfa, 0x90, 0xb9, 0x99, 0x59, 0x53, 0xc7, 0x4d, 0x20, 0xf3, 0xe8, 0x31, + 0x9d, 0xf7, 0x8c, 0x1d, 0xac, 0x88, 0xdf, 0x52, 0x65, 0x4b, 0x45, 0x15, + 0xa9, 0xd2, 0x67, 0x81, 0xd1, 0x42, 0xcc, 0x37, 0xf0, 0x2a, 0xe5, 0x8d, + 0x59, 0xa7, 0x29, 0xd3, 0xca, 0x15, 0x62, 0x5c, 0x8a, 0x0c, 0xa3, 0x26, + 0x0b, 0xbc, 0xf1, 0x55, 0xbb, 0x7c, 0x44, 0x3e, 0x29, 0x23, 0xd3, 0xb0, + 0x27, 0xe9, 0x93, 0x54, 0x91, 0x4d, 0x6b, 0x37, 0x35, 0xdf, 0xab, 0x69, + 0xf4, 0xf4, 0x6e, 0x1d, 0xe3, 0xf6, 0xac, 0x6b, 0x86, 0x64, 0x47, 0xd6, + 0x92, 0x96, 0x6a, 0x1a, 0xb8, 0xa1, 0xf3, 0x64, 0xce, 0xf3, 0x62, 0xca, + 0x24, 0xc7, 0x85, 0x81, 0x21, 0x8f, 0x1c, 0x9a, 0x51, 0x30, 0xbd, 0xbb, + 0x68, 0x23, 0x56, 0x1d, 0x38, 0xfa, 0x21, 0xf6, 0x8c, 0x03, 0xa7, 0xba, + 0x11, 0x7e, 0xf9, 0x79, 0x24, 0xf8, 0x9d, 0x5f, 0x4b, 0x86, 0x7f, 0x3d, + 0xf8, 0xac, 0x76, 0x65, 0x01, 0x01, 0x00, 0x3b + }; + + +int main() +{ + std::cout << "OpenXLSX Image Test" << std::endl; + std::cout << "===================" << std::endl; + + // Create workbook and document + XLDocument doc; + std::string xlsxFileName = "Demo11.xlsx"; + doc.create(xlsxFileName); + auto wb = doc.workbook(); + + // Create test worksheets + wb.addWorksheet("Anchor Types"); + XLWorksheet anchorSheet = wb.worksheet("Anchor Types"); + wb.addWorksheet("File Formats"); + XLWorksheet formatSheet = wb.worksheet("File Formats"); + wb.addWorksheet("Sizing Strategies"); + XLWorksheet sizingSheet = wb.worksheet("Sizing Strategies"); + wb.addWorksheet("Units & Scaling"); + XLWorksheet unitsSheet = wb.worksheet("Units & Scaling"); + wb.addWorksheet("File Loading"); + XLWorksheet fileSheet = wb.worksheet("File Loading"); + + std::cout << "\n=== Sheet 1: Anchor Types Comparison ===" << std::endl; + + // Test 1: oneCellAnchor vs twoCellAnchor with same image + XLImage testImage1(pngData, ImageMimeTypes::PNG); + testImage1.setDisplaySizeWithAspectRatio(2 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); + + // Add headers + anchorSheet.cell("A1").value() = "oneCellAnchor (PNG, preserve aspect ratio within 2x2 cell height bounding box)"; + anchorSheet.cell("A7").value() = "twoCellAnchor (PNG, 4x3 cells)"; + anchorSheet.cell("A13").value() = "oneCellAnchor (JPEG, original size)"; + anchorSheet.cell("A19").value() = "twoCellAnchor (JPEG, 3x2 cells)"; + + // Add images + bool success1 = anchorSheet.addImage(testImage1, "A2"); + std::cout << " Added oneCellAnchor PNG: " << (success1 ? "Success" : "Failed") << std::endl; + + // Add twoCellAnchor image + bool success2 = anchorSheet.addImageTwoCellAnchor(testImage1, 8, 1, 10, 4); // A8 to D10 + std::cout << " Added twoCellAnchor PNG: " << (success2 ? "Success" : "Failed") << std::endl; + + // Add more twoCellAnchor tests + XLImage testImage2(jpegData, ImageMimeTypes::JPEG); + bool success3 = anchorSheet.addImage(testImage2, "A14"); + std::cout << " Added oneCellAnchor JPEG: " << (success3 ? "Success" : "Failed") << std::endl; + + bool success4 = anchorSheet.addImageTwoCellAnchor(testImage2, 20, 1, 21, 3); // A20 to C21 + std::cout << " Added twoCellAnchor JPEG: " << (success4 ? "Success" : "Failed") << std::endl; + + std::cout << "\n=== Sheet 2: File Format Testing ===" << std::endl; + + // Test different file formats + static const std::vector, std::string, std::string>> formats = { + {pngData, "PNG", ImageMimeTypes::PNG}, + {jpegData, "JPEG", ImageMimeTypes::JPEG}, + {gifData, "GIF", ImageMimeTypes::GIF}, + {bmpData, "BMP", ImageMimeTypes::BMP} + }; + + for (size_t i = 0; i < formats.size(); ++i) { + XLImage formatImage(std::get<0>(formats[i]), std::get<2>(formats[i])); + formatImage.setDisplaySizeWithAspectRatio(4 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); + + std::string textCell = "A" + std::to_string(1 + i * 6); + std::string imageCell = "A" + std::to_string(2 + i * 6); + formatSheet.cell(textCell).value() = std::get<1>(formats[i]) + " Format Test"; + + bool success = formatSheet.addImage(formatImage, imageCell); + std::cout << " Added " << std::get<1>(formats[i]) << " image: " << (success ? "Success" : "Failed") << std::endl; + } + + std::cout << "\n=== Sheet 3: Sizing Strategy Testing ===" << std::endl; + + // Test different sizing strategies + XLImage sizingImage(pngData, ImageMimeTypes::PNG); + + // Strategy 1: Preserve aspect ratio + bounding box + sizingImage.setDisplaySizeWithAspectRatio(7 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); + sizingSheet.cell("A1").value() = "Aspect Ratio + Bounding Box (7x3 cell height units)"; + bool success3a = sizingSheet.addImage(sizingImage, "A2"); + std::cout << " Aspect ratio + bounding box: " << (success3a ? "Success" : "Failed") << std::endl; + + // Strategy 2: Original size (no scaling) + XLImage originalImage(pngData, ImageMimeTypes::PNG); + // Don't call setDisplaySize - use original dimensions + sizingSheet.cell("A7").value() = "Original Size (no scaling)"; + bool success3b = sizingSheet.addImage(originalImage, "A8"); + std::cout << " Original size: " << (success3b ? "Success" : "Failed") << std::endl; + + // Strategy 3: Force exact dimensions + XLImage exactImage(pngData, ImageMimeTypes::PNG); + exactImage.setDisplayWidth(11 * EXCEL_CELL_Y_SPACING_EMUS); + exactImage.setDisplayHeight(3 * EXCEL_CELL_Y_SPACING_EMUS); + sizingSheet.cell("A13").value() = "Exact Dimensions (11x3 cell height units, may distort)"; + bool success3c = sizingSheet.addImage(exactImage, "A14"); + std::cout << " Exact dimensions: " << (success3c ? "Success" : "Failed") << std::endl; + + std::cout << "\n=== Sheet 4: Units & Scaling Testing ===" << std::endl; + + // Test different unit systems + XLImage unitsImage(pngData, ImageMimeTypes::PNG); + + // Pixels + unitsImage.setDisplaySizePixelsWithAspectRatio(100, 50); + unitsSheet.cell("A1").value() = "Pixel-based (100x50 pixels)"; + bool success4a = unitsSheet.addImage(unitsImage, "A2"); + std::cout << " Pixel-based sizing: " << (success4a ? "Success" : "Failed") << std::endl; + + // EMUs + XLImage emuImage(pngData, ImageMimeTypes::PNG); + emuImage.setDisplayWidth(476250); // 50 pixels in EMUs + emuImage.setDisplayHeight(238125); // 25 pixels in EMUs + unitsSheet.cell("A7").value() = "EMU-based (476250x238125 EMUs)"; + bool success4b = unitsSheet.addImage(emuImage, "A8"); + std::cout << " EMU-based sizing: " << (success4b ? "Success" : "Failed") << std::endl; + + // Cell spacing + XLImage cellImage(pngData, ImageMimeTypes::PNG); + cellImage.setDisplaySizeWithAspectRatio(19 * EXCEL_CELL_Y_SPACING_EMUS, 5 * EXCEL_CELL_Y_SPACING_EMUS); + unitsSheet.cell("A13").value() = "Cell-based (19x5 cell height units) -- preserve aspect ratio"; + bool success4c = unitsSheet.addImage(cellImage, "A14"); + std::cout << " Cell-based sizing: " << (success4c ? "Success" : "Failed") << std::endl; + + std::cout << "\n=== Sheet 5: Loading Images from Disk Files ===" << std::endl; + + // Find the images directory by searching upward through the directory tree + std::string imageDir = findDirectory("OpenXLSX/Examples/images/"); + if (imageDir.empty()) { + std::cout << " ERROR: Could not find images directory!" << std::endl; + std::cout << " Searched for: OpenXLSX/Examples/images/ in current " + "directory and up to 10 levels up" << std::endl; + } else { + std::cout << " Found images directory: " << imageDir << std::endl; + } + + // Only proceed with file loading tests if we found the images directory + if (!imageDir.empty()) { + // Image 1: PNG - Original size + std::string pngPath = imageDir + "tiny_png.png"; + fileSheet.cell("A1").value() = "PNG - Original Size"; + bool fileSuccess1 = fileSheet.addImageFromFile(pngPath, "A2"); + std::cout << " PNG original size: " << (fileSuccess1 ? "Success" : "Failed") << std::endl; + + // Image 2: JPEG - Aspect ratio preserved (2x1 cells) + std::string jpegPath = imageDir + "tiny_jpeg.jpg"; + XLImage jpegImage; + std::string jpegId = fileSheet.generateNextImageId(); + if (jpegImage.loadFromFile(jpegPath, jpegId)) { + jpegImage.setDisplaySizeWithAspectRatio(2 * EXCEL_CELL_Y_SPACING_EMUS, 1 * EXCEL_CELL_Y_SPACING_EMUS); + fileSheet.cell("A7").value() = "JPEG - Aspect Ratio (2x1 cells)"; + bool fileSuccess2 = fileSheet.addImage(jpegImage, "A8"); + std::cout << " JPEG aspect ratio: " << (fileSuccess2 ? "Success" : "Failed") << std::endl; + } else { + std::cout << " JPEG aspect ratio: Failed to load image" << std::endl; + } + + // Image 3: GIF - Exact dimensions (may distort) + std::string gifPath = imageDir + "tiny_gif.gif"; + XLImage gifImage; + std::string gifId = fileSheet.generateNextImageId(); + if (gifImage.loadFromFile(gifPath, gifId)) { + gifImage.setDisplayWidth(3 * EXCEL_CELL_Y_SPACING_EMUS); + gifImage.setDisplayHeight(2 * EXCEL_CELL_Y_SPACING_EMUS); + fileSheet.cell("A13").value() = "GIF - Exact Dimensions (3x2 cells)"; + bool fileSuccess3 = fileSheet.addImage(gifImage, "A14"); + std::cout << " GIF exact dimensions: " << (fileSuccess3 ? "Success" : "Failed") << std::endl; + } else { + std::cout << " GIF exact dimensions: Failed to load image" << std::endl; + } + + // Image 4: BMP - Two-cell anchor spanning multiple cells + std::string bmpPath = imageDir + "tiny_bmp.bmp"; + XLImage bmpImage; + std::string bmpId = fileSheet.generateNextImageId(); + if (bmpImage.loadFromFile(bmpPath, bmpId)) { + bmpImage.setDisplaySizeWithAspectRatio(3 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); + fileSheet.cell("A19").value() = "BMP - Two-Cell Anchor (A20:C22)"; + bool fileSuccess4 = fileSheet.addImageTwoCellAnchor(bmpImage, 20, 1, 22, 3); // A20 to C22 + std::cout << " BMP two-cell anchor: " << (fileSuccess4 ? "Success" : "Failed") << std::endl; + } else { + std::cout << " BMP two-cell anchor: Failed to load image" << std::endl; + } + + // Image 5: PNG - Precise positioning with offset (using previously unused addImageWithOffset) + std::string pngPath2 = imageDir + "tiny_png.png"; + XLImage offsetImage; + std::string imageId = fileSheet.generateNextImageId(); + if (offsetImage.loadFromFile(pngPath2, imageId)) { + // Set a specific size for the offset test + offsetImage.setDisplaySizeWithAspectRatio(3 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); + + fileSheet.cell("A25").value() = "PNG - Precise Offset (A26 + 50% cell width/height offset)"; + // Offset by 50% of cell width and height (100000 EMUs each) + int32_t colOffset = EXCEL_CELL_Y_SPACING_EMUS / 2; // 50% of cell width + int32_t rowOffset = EXCEL_CELL_Y_SPACING_EMUS / 2; // 50% of cell height + bool fileSuccess5 = fileSheet.addImageWithOffset(offsetImage, 26, 1, rowOffset, colOffset); + std::cout << " PNG with offset: " << (fileSuccess5 ? "Success" : "Failed") << std::endl; + } else { + std::cout << " PNG with offset: Failed to load image" << std::endl; + } + + // Image 6: JPEG - Larger size with offset (demonstrates variety in offset sizing) + std::string jpegPath2 = imageDir + "tiny_jpeg.jpg"; + XLImage offsetImage2; + std::string imageId2 = fileSheet.generateNextImageId(); + if (offsetImage2.loadFromFile(jpegPath2, imageId2)) { + // Set a larger size for variety + offsetImage2.setDisplaySizeWithAspectRatio(4 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); + + fileSheet.cell("A31").value() = "JPEG - Large Size with Offset (A32 + 25% cell offset)"; + // Smaller offset for variety + int32_t colOffset2 = EXCEL_CELL_Y_SPACING_EMUS / 4; // 25% of cell width + int32_t rowOffset2 = EXCEL_CELL_Y_SPACING_EMUS / 4; // 25% of cell height + bool fileSuccess6 = fileSheet.addImageWithOffset(offsetImage2, 32, 1, rowOffset2, colOffset2); + std::cout << " JPEG large with offset: " << (fileSuccess6 ? "Success" : "Failed") << std::endl; + } else { + std::cout << " JPEG large with offset: Failed to load image" << std::endl; + } + + // Note: The addImageFromFile() function loads images at their original size + // To apply different sizing strategies, we would need to: + // 1. Load the image with addImageFromFile() + // 2. Get the XLImage object from the worksheet + // 3. Modify its display size + // 4. Re-add it with the new size + // + // For now, this demonstrates that the file loading functionality works + // and creates a foundation for more advanced sizing operations. + } // End of file loading tests + + // Save the workbook + std::cout << "\nSaving workbook as '" << xlsxFileName << "'..." << std::endl; + try { + doc.save(); + std::cout << "Workbook saved successfully!" << std::endl; + } catch (const std::exception& e) { + std::cout << "Error saving workbook: " << e.what() << std::endl; + return 1; + } + std::cout << "\nTest Matrix Summary:" << std::endl; + std::cout << "- Sheet 1: Anchor Types (oneCellAnchor vs twoCellAnchor)" << std::endl; + std::cout << "- Sheet 2: File Formats (PNG, JPEG, GIF, BMP)" << std::endl; + std::cout << "- Sheet 3: Sizing Strategies (aspect ratio, original, exact)" << std::endl; + std::cout << "- Sheet 4: Units & Scaling (pixels, EMUs, cells)" << std::endl; + std::cout << "- Sheet 5: File Loading (loading images from disk files)" << std::endl; + + // ================================================================================ + // PHASE VI: READ-MODIFY-WRITE CYCLE + // ================================================================================ + std::cout << "\n=== Phase VI: Read-Modify-Write Cycle ===" << std::endl; + + // Step 1: Read back the .xlsx file we just created + std::cout << "\n1. Reading back the .xlsx file..." << std::endl; + OpenXLSX::XLDocument readDoc; + try { + readDoc.open(xlsxFileName); + std::cout << " Successfully opened: " << xlsxFileName << std::endl; + + // Step 2: Search for embedded images in all worksheets + std::cout << "\n2. Searching for embedded images..." << std::endl; + int totalImagesFound = 0; + std::vector worksheetsWithImages; + + for (const auto& sheetName : readDoc.workbook().worksheetNames()) { + OpenXLSX::XLWorksheet sheet = readDoc.workbook().worksheet(sheetName); + + // Check if worksheet has images by checking if it has DrawingML with actual images + // (hasImages() only checks in-memory m_images vector, which is empty for loaded files) + try { + OpenXLSX::XLDrawingML& drawing = sheet.drawingML(); + if (drawing.valid()) { + // Use the public imageCount() method to check for images + size_t imageCount = drawing.imageCount(); + std::cout << " Worksheet '" << sheetName << "' DrawingML valid: " << (drawing.valid() ? "Yes" : "No") + << ", imageCount: " << imageCount << std::endl; + if (imageCount > 0) { + worksheetsWithImages.push_back(sheetName); + std::cout << " Found " << imageCount << " image(s) in worksheet: " << sheetName << std::endl; + totalImagesFound++; + } + } else { + std::cout << " Worksheet '" << sheetName << "' has no DrawingML" << std::endl; + } + } catch (const std::exception& e) { + std::cout << " Worksheet '" << sheetName << "' DrawingML error: " << e.what() << std::endl; + } + } + + std::cout << " Total worksheets with images: " << totalImagesFound << std::endl; + + // Step 3: Add a new image to the "File Loading" worksheet (object B) + std::cout << "\n3. Adding new image to 'File Loading' worksheet..." << std::endl; + + if (readDoc.workbook().worksheetExists("File Loading")) { + OpenXLSX::XLWorksheet fileLoadingSheet = readDoc.workbook().worksheet("File Loading"); + std::cout << " Found 'File Loading' worksheet" << std::endl; + + // Create a new test image (using the same PNG data as before) + OpenXLSX::XLImage newImage; + std::string newImageId = fileLoadingSheet.generateNextImageId(); + std::cout << " Generated new image ID: " << newImageId << std::endl; + + // Use the same PNG data from our static constants + if (newImage.loadFromData(pngData, "image/png", newImageId)) { + std::cout << " Successfully loaded image data" << std::endl; + std::cout << " Image ID before addImage(): " << newImage.id() << std::endl; + + // Set a distinctive size for this new image + newImage.setDisplaySizeWithAspectRatio(2 * EXCEL_CELL_Y_SPACING_EMUS, 1 * EXCEL_CELL_Y_SPACING_EMUS); + std::cout << " Set image display size" << std::endl; + + // Add it to cell A37 (after the existing images) + fileLoadingSheet.cell("A37").value() = "NEW IMAGE - Added via Read-Modify-Write Cycle"; + std::cout << " Set cell A37 text" << std::endl; + + // Debug: Check if image is valid before adding + std::cout << " Image valid: " << (newImage.isValid() ? "Yes" : "No") << std::endl; + std::cout << " Image data size: " << newImage.data().size() << " bytes" << std::endl; + std::cout << " Image MIME type: " << newImage.mimeType() << std::endl; + + bool addSuccess = fileLoadingSheet.addImage(newImage, "A38"); + std::cout << " addImage() returned: " << (addSuccess ? "Success" : "Failed") << std::endl; + std::cout << " Image ID after addImage(): " << newImage.id() << std::endl; + + if (addSuccess) { + std::cout << " Successfully added new image to 'File Loading' worksheet" << std::endl; + std::cout << " New image ID: " << newImageId << std::endl; + std::cout << " Image positioned at: A38" << std::endl; + } else { + std::cout << " Failed to add new image to worksheet" << std::endl; + } + } else { + std::cout << " Failed to create new image from data" << std::endl; + } + } else { + std::cout << " 'File Loading' worksheet not found!" << std::endl; + } + + // Step 4: Write the modified workbook to a new file + std::cout << "\n4. Writing modified workbook to new file..." << std::endl; + std::string modifiedFileName = "Demo11_Modified.xlsx"; + try { + readDoc.saveAs(modifiedFileName); + std::cout << " Modified workbook saved as: " << modifiedFileName << std::endl; + } catch (const std::exception& e) { + std::cout << " Error saving modified workbook: " << e.what() << std::endl; + } + + // Step 5: Verify the modification + std::cout << "\n5. Verification summary:" << std::endl; + std::cout << " - Original file: " << xlsxFileName << std::endl; + std::cout << " - Modified file: " << modifiedFileName << std::endl; + std::cout << " - Worksheets with images: " << totalImagesFound << std::endl; + std::cout << " - New image added to: File Loading worksheet" << std::endl; + + readDoc.close(); + + } catch (const std::exception& e) { + std::cout << " Error during read-modify-write cycle: " << e.what() << std::endl; + } + + /* read file generated by Excel. Count the images in the file. */ + std::cout << "\n=== Phase VII: Loading Excel-Generated File ===" << std::endl; + + // Find the Excel-generated file + std::string excelFileDir = findDirectory("OpenXLSX/Examples/files/"); + std::string excelFilePath = excelFileDir + "from_excel_embedded_images.xlsx"; + + if (excelFileDir.empty() || !std::filesystem::exists(excelFilePath)) { + std::cout << " ERROR: Could not find Excel-generated file!" << std::endl; + std::cout << " Expected: " << excelFilePath << std::endl; + std::cout << " Searched for: OpenXLSX/Examples/files/ in current directory and up to 10 levels up" << std::endl; + } else { + std::cout << " Found Excel file: " << excelFilePath << std::endl; + + // Load the Excel-generated file + XLDocument excelDoc; + try { + excelDoc.open(excelFilePath); + std::cout << " Successfully opened Excel-generated file" << std::endl; + + // Count images in each worksheet + int totalImages = 0; + for (uint16_t i = 1; i <= excelDoc.workbook().sheetCount(); ++i) { + XLSheet sheet = excelDoc.workbook().sheet(i); + std::string sheetName = sheet.name(); + + // Check if it's a worksheet (not a chartsheet) + if (sheet.isType()) { + XLWorksheet worksheet = sheet.get(); + + // Debug: Check if drawingML is valid + bool drawingValid = worksheet.drawingML().valid(); + std::cout << " DEBUG: About to call imageCount() for worksheet '" << sheetName << "'" << std::endl; + int imageCount = static_cast(worksheet.imageCount()); + std::cout << " DEBUG: imageCount() returned: " << imageCount << std::endl; + totalImages += imageCount; + std::cout << " Worksheet '" << sheetName << "': " << imageCount << " image(s) (drawingML valid: " << (drawingValid ? "Yes" : "No") << ")" << std::endl; + } else { + std::cout << " Sheet '" << sheetName << "': (not a worksheet, skipping)" << std::endl; + } + } + std::cout << " Total images in Excel file: " << totalImages << std::endl; + + excelDoc.close(); + } catch (const std::exception& e) { + std::cout << " ERROR: Failed to open Excel file: " << e.what() << std::endl; + } + } + + return 0; +} diff --git a/Examples/files/from_excel_embedded_images.xlsx b/Examples/files/from_excel_embedded_images.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a2b9f1b551ae846fde5f991dfce4002474ca5278 GIT binary patch literal 27825 zcmeFZ1yo$mmN$BE2?Qr-2<|}|OCY#Q2-3I&Yur5$G`Iy%L$Cz5Ab|uAZVkZ+uEE{C zUz7ixxifR;&YSnvTWj98zOVY!aobhD{i|JjSD#l(D5%5$I)DiP04)$*8jw(m1OQox zO(FmjSzpT0!Ogp}sh7RolTWC~EZG1u;{5-U{x>cGm{zYt8#jKF_Li7* zvsOf)78pz*7uS^7C-2deN*Y#q#pX8(bh z)rOgqIIxGo;*B2~vWb$gZ2+R_be?%zc zA}?vV6g{il(|Dv4Z>Nm?BSUNZv8B2WzCi6f`j&eZEpuX@ESIZblcstFm5gft+e|i@ zXrIQc?was;HX8zZw9bd&gs1po#;Tukc=b7?iy|#L>Vf5=Z2iG*_HLpu93SQ0q&nx} zsn$hpKjFK?*C9Th%=zRYR^ngZ($9uq{A3qNd~?J2TWHELS2hpknu3%(^{ydUA$ml- zkQIsJO9D63NAAjrrZ6!(0U?}S(1XJ}w060z4TJF5nFqtp=c><3+3^C}t1rhfArB2u z%^s0_a4nGHeiL5DK(P@vUj9DQ@Q3OQhTAI@h!L_;6JOkQ-%xzxfW`se{2Hq#h2t^t zHOiNgNfgdxXIyam;Wyv8G?C|8gZNhcuSY*_Mc#xC^XGM0qczWIWb@Ao-58xTBUBj< zM*&{_J0-6HbJL$7kgJT);RgsMH+HeGbLDz^KmR|<{lBqB{~OUu6I4{&xp6}GsXD~%!~ z7N)QDEDKG&a(2gHV{%E6buL}%An+JJ9Y2H0DSESbG{v!%d@syV>Rq6h9X*t;#Q*tJ zn*xg{k1CY(1vF5vOGS6y^r9STTvB7VEVSaAVD?tx&vgIEl>ALxvG8ZgJ0nn%9#>P# zsdC>QJKEDr3b2ONGwTY|Oqb_$-p1y3KaQo-TZnFbIbVM4QK#c0yx{#Q*UgxH>aX{e z@323^yF(DGy`<-$Hz*kJ(x9-?Ht?6^D~U2IJq9c}Mh!BViT z<7Xk_+vbrwj1JF8+)(^6w+;>cB5JU8?OZ$?Eyl;7KHC^;b@1`RdfGd_49(AWmY+Mi z&Uz2)Cc9IQxzdX@Gs*WO^D~PsaEh=?Mw!^Rx}jI+bQ4tIP1G`wo&bM(5m%{3i^0uh z_htQ(YJa}BdO-Fb#OA5=_ST!!wbD;>x}#MTT3WbY3Ik~U#~M;VY3|R=KC!u@(CUA& zQY+C%Pt9r@F4dChYh&7F*NWoAvScKA($XJQOF311LR=qtRM0#=l5Z1es+$zzMJ^A{ zQ=IGXu3vJ~mHb6%$4dFon9`3-!Yo?2b!1oz9DkH=tF??%cv`mc`GRMJ$1gQ#!1>`z zGrH%NKSM;?o7f!u)svgns}_2=uj~bICMX)0C67dVA-gr?Tb-B1EK_q6_1N|^U%nXeyjP!WzE&O}e6Zz873{vKYMEeK5Fa8rbqg%gTCd#KKcbru?MwV^QGs z6P)Pdgq421%ucR ze94VkO=d?dg($eiYa8lP-}~4xx%U};ut2n?TyTO8YC1 z6Y8L)+H`@hk1qGd7*CZMojT-{Cfz{guUF4x&X`t6c16>6{B62w_IY3HF6?2~?#4kl7bQ-vLL<6oAGmz5Xi>&8+UsQn0_$=WRrQLJuaT!mI)KMyl13M+A9SK@pr zQn+|z-BVZWq`lY11>%qrX`_tr55R!>G#=8RW)RUggI{eC9kHR@WbPa!W2$_%~o$9&a;`H$7fs=^1??_X1&JdQl&?3QDaXW)tD4=Rv9ew%Kk zMV3B+8YW@h>1b+kQKCw!Bu8^MP`HuvzK4Q0+Hk6?Vd{3(M!3>-uwi6zPzriJ*W!=X z7_y<{K_}EKOhD$_WMq&P-V{mN`_`B3SHqdqzFVVUmd^s|u-FReRT$RX06&w zp86pa_aEE8K>hE%4-iUkf(if>045Sbwf|}Y|E+@mOLK^XuyqmV{_mX{lUi;5GzY;= z!NYHz6Fla~+~2SU8-3eDQvTGD6G6@4u0QX`RN)brU?}+%UEu;Q#xONA=Q$IudZH;@ z@+86(-^x{ctXBQN+4rp%xy|v&Yi<`Xv~^Zgz{WGCR++I+1j3rVjpg8 z8r@_LZt}>bmMi94M&XPsw-DZ#m6#KM{wI{yky1H7Xd+3%%@)ws;WdW36VLVx{Tzmm za35U$zKOu0!%*90H%Ch)?#-!+MCR$4;Z}8Rdq4a*%JVE|(hZFwui*7ah;ZV|R)z=d z3>w_&y4@XbiBj&WxcNFX&f;9BUA+{&+-W4AeN5!k;Sz~Eb(C7&Fzflxp|rhp;Pe_{ zMhPOIM25h~-&zS*D+>!ZSFS$`p8MwFW1^ws%oF0!edb-UCwumGE3(xj1%1o9yK{Us zP0SZUvP0w& z<3)e8thX4kDm=iV@-y&-3y6_5xW5RH3(PhQ@!1bc!0+~X1?z+W)E>{xNRCZx1Uqm^ zb%d$a6U-HO=_9dXe!d%-Z|62-78(oPC${nr)p-vKyE5cL_sn7S!+XioFo#PZXoDGE z^S&*MiA6Rt^(Uo0hkk$d^@p|S&!2HQA7mJfC~pfwlNKk0MlXfl_tvfHU!t*Mu4s>x z-8FDqzvS5Iya=Ev+_7cM!_(F}L;7s|ZlJv4u->kY$UbBtRYg|&^viP_!sk&(boQSt z;)e=fT4#=8C`Lgp@wnE?Fu6pEB}wbC;kh3;))KwvQK@DN$YP8PD66RjGo@k~s63wp z#+8e6;P~hSCXpD$c-#piNs$GtD&9~TZw#!?FOb#@Rc4&{!a6=C>Ix43Gpk}9lvm+&R^U+hTFu(WrOi?W=G9VXc`6TMuLbi~@v5sA za`P!D*D4mdDR91gIlx{C<{biQRaGmXv$`V}qeT*P9vqCi~M=ce>Ct* z#LXm~5U7m(=cwfU3zaHy>D>6C`^i@{@AqG0GH|k(gI|B%E7PlK5{6)E-ALH4D}muY z7@y3w`xy1GqIl@VKwQUfF5T35g#z5$HWC=rl#z#Kze+kP9c`~azfnY9hdMy{C=^}L zTxx9yLaXg<~^5sl(2 zH6S{n&f8K6ajZBAuIR)SxO=L;N=5S*y8u_HCE~(w$E8c=eHP;|ScKe+NBFJP^@iH0{gYaS_I$&uV;Fs4; zSZ9z^g+mL?qUGP1HOGCqIbB_%5wOZJ^Vu{WI1~=I-#=_KR|}n%btT@dL+Tz*))Ao) zzD3xY|DOUg^xpu};Ug{US|D*dE}tam70r?s0zCinu>S#+p4Tg&zDJ<+;Gd%u^cPB% zY*)C6L(Wld@g#lp{eG(v6?|8adQi>M4%;EQd`>#tPS45}dbv;kjDgdf2%EGy<@f%q z8TCMBta|nfvi_Q4Ckke=K-2mq8;vDnG3{Yu`t3rS!Xa!nU*xE)HM|7~hJ9@Q{WRa3v`2vf( zp##AdSQG}MYwO)BnG;=qemkS-rO(+>tUvXC#KajM_yUjeBCxM{Lj8GtRTZAjoy@Nb z><2CVNy77*7miJeTJ9MtpVW-vK7G=O{vmdCe-(ayVx25A+v!;&{avbxVE<0P@ ziv1l%{{@+%SCuL!5wqc_5GIzXvl*&KC2AZi+9l(hL?`dqB}gb{1lk1Bb&C5zGb&>c zvlu^Kd=!-+N=qx*M(?LjI-O8dDk!bqIb|0mv-)Aw2i>Ds<{rh~|FbBLDpmG|B2X-f z_D?MUzCS2VP>PzlM{&p(s`#p0oCn%P~`C@-dva6FTQk2*r(hsefio7b|65J9ag?EZNypZeCW1&fjFYW`&oAP^j#{9hoL`d<+Y z{dWkKZFGJqUgNX^t3;q#vC(_*9?iH2G@Ap0LmhpnE&hh$kxS?lpU4a2FqQsi;e7V` ztHgH<03e{d4*>izXx*$V>@B$d%>UT3J9-0gl;22NFnZwPFvAJhhL6d!p#s6Py*EqA zhO6=lNufrAPU7kf?8Hde>tjlR$kxM3f%9)zBGIh^(Cb#?cwW=1z{5pcWYd-+Bb)BT z6HGn1Kx6wa?$(;Ry9MK1&l5{N#ChMs!S`2Gg9hH=CS0e z@{{^Rn{QqASK{`TQApxHJbACWinRA8UVT%dhRu>Y`ZslE0;-?B6M-4pH&fk=2fkQu z%(QUdw&=$2sE8yjl8uS#aco3qM`&%%&9Kn$d{;>tLjf@jezh@GAltbS8N#efL17v~ zgBaqiL67^f>eL^b>abnpY+tVZESYc6@5HFX*YGh{axptj?f;E@d^`kWO&Q(Iy<@SP z3@-CSd2RDZ&}JK6CW`W-nq0)XE3Xt}#dquR!AvQ8XC$X#6x&9iIk%;@{n-ZJwaMIw zqd4^LY7Z1E{Mn?vjq+8f>~Kjz%Pli?xT%qFXg>@gaiQ;lr!GkLK)ZW(KI5 zLO6tk-P(i)@)s!94v^7?D`K9$=nP`EF)r=AAb(^ASUqWvWRfq_sQLj4Up6ZKE-l5T z5jy!;(YcKg=h_5cd}I99>DrMa77z2Og00aH={+24QTahs74W1Fpw~pW`de_J(d+Is zY8~!<*!hSn6aIjD*dmt~oH9%MGR2~f=I*W=etYV<(E`7l!85+=jY+k*De=F%8~?U& zoAgGMU>eM!erW4#>XA^a}7lf&lXVv zkw8@Kd`;BwI}1jt=0?@;O^+w-0wdV_Po(WECNr}KH#WYcXj#z^v>h;J5(Q=YZoM8_ zFS0leItj+C>Z5f=n?SMaE^by0f5)pZ$=|?w6t2i_{<9}xf62A6ImK&2D5COL!6j;= zL&%=y4^y{SULBBii$$!EBfRBK>m)fsx1=DSud3VD@5-8#7L+_2%R_#*2@U%|9le8!`RdaozGAeO7Tk;F38NmONpn*Ko>#$tUY44y!xesstI`u8Z z@j=>5&{}P_b5qdU_d)rECfSXwdN1-1iS~D|IatgrZ3cSc`O0XpzVILO;G|_HSt~nS z2Uw$~UU|Ppe(;U>SI?8yamT{QLAsBU{w&;vusj%;i&3%eU2QzMz^$f>$~SA#vSl;T zpVJxnEvuGHNjfAeekc^GQ$k;hHA5Y0k0b^hrRFLO)3mlj15%o>B1X&wBGQ7TZ9x|l zg!QgxemZVc9MgKYU0~sSz()Ve4Qyxg_hCT^cehC2G^{vr(F%x&-TYhucLeW z2!OIqLsd_{$)>o1tfvoZ1P{K-vM!3&dDO z0v9M^BvHq$jOL9>FHO8;&X2_Gu$nzRId*IhWMqmkUwvNT=3lfb*|g|qz4~LjHnrDO z0#dwlJ`!H*=J9}4REc!ZIObv|xQ4+vu;J&YOUAUSWUpx24vI2yAJ3PJR5Ym2l6B(2 z_F8CZc#EyDM&@mqR5=}uS&FRCf_mSSNN_r?g;Mvdz5T2D{w#8wdfT>5_5F(d#@%MW zFB2*3XWcI=t@W&B9Mkvgu9iG52}?{L)#zB%UB4OJwM(EzEp|=!I&W%`F8cucluy3D zW|HysSS@|prxa7MLDe;Gt~UMMD4}D+Y4Gf6;ka-|eg$8k1nm+fgWXJhg&s%nQ2729DMeM9?~WA0@Rkco%=nbWx@W~|?* zYv{iAqJ@q;6NU}AaIHewleMRvLbQTBnij5NjNal4pp&%Bk%6~r>W!@h-_$6687@To zEW3zzy|gZ_D&suV)9g#uZjGLenJmrPjm1VeXr|+zL90Py6XSPTXNr*`(vK}M<0Re8Oul!!fw?@xGHe} zYUo|vyzMMp?~T=5u)XaJH~yXIFMs^2y*`f>4y;ec9AIC8qrmg+|f2xr1GI zKB^u@RL^UN{UTHgMqPi-(HoAGXkmY~=yECJr80a+#sl7pECGkMh>^6SB6?lLg+q}R zUfmqcePaHKWiwc3-ytyCfL8+Tw(Du+w6z8xh20Q`4?!7vlnT@w1ChVFxb<}X<*^CF zmOjY$k?avbs%~iT6$Xy;u@pHew=NPM?0Ha=%2s`xD@zh^V-2vOjIoNCwz4xFOOr|% zyk?49%^TL5+(UMXD7Q7L`sP+(u4v^giM)DFD}#l#Qa!DI{4$q-$F9H0S4h>i?4wNH zJPU>?vf4t>RP_i~@qzyQl-%#4+lX4t0OmFZRVB`^5)X5O2i+gbFmBVAQxC zM5C|K+gZL4)$}>hmxkjR=3+)i;*>uJ)v_d2QD-Ka&=E188>|qS)XGt8mtrw{vHq0X za8~S-cIe(GA#$oGhHWB-HX5y`qWr%X`!rf@Fr;yf%Wfw((fv}VpM6L6)^n~OiNrQm z#B<@x{+NJuxRLcE?h2=&EPlF2vc7?>{osQy(L*NUDnXw{zoo-4s~2ZsCTjXwD=5nW zXXKk^Z&qb~9t8VWBbWbtjLa!$AXI)QPR3nwHWoC*zIb!Jo#qNd&M4GT5o~)wEeP_h zYN?8JH200Z>iRbAj!!N+b#7E-wc}bbF{l6C=sMU)!Y|~Dnr;Ab!g?6PTU*db2O++kifE45X+neqe@kFav$2t9vXT; z*LuVy^FEkx^(5|n3j(?S=-7Xya{6Wgf#ooS!=CtWn6@%;u`t(gbFp@?`~z*b_pLA6 zx$&hDkPX2 zHeHJSDiVKHaD?IDH~GQcvRut!+XG?`YiXi=OtS5gSXP!Cti3o{=~%=vVe!7qKK|z= z8oz(;HNuJVj|Jklo}HPAC@QPJcTUY+Og!()g6nT9f4QeM?Hv)0z&lZEIMStXC;l=% zu{tAazT&8s$_$=UjCCoJZ0)f+_;z2Il2EHGx%REXpy#^JUgkP%>ugqFKAxS^JT>P4 zoj%S}S?iMzY$$F&FYAdalLR^2o-l{tDm6e%`k;?GX?IE6eQ2PI(SH-#++{WYuXlV3{AU3Uh=N1 zRB!$=VvEymvvUi5jl50mYQs1~o8AkdE=Lwl;5S6$Zk{-$FrGVGrBlqDsBX0%h}!>V z=hR@Q?Ht785)?Hbn*LKkdcKvx(Jb^>2r5amhgSB;E?_tIJZP?@GK{O+=XV5~6lm4` zE1ET}&BI6^f&(cHKY^?oa?bs1>Cnu6-s!<8-ZQ0&8!|^TfBg=nmS~6q0cW2`T0A0n z$-s(Yfy*&&=&r`c;BPgJou8XhQ)g0zYi{_Ui%S@plCUdqbnNs}`3M=3fihb=djY}` zyYxuC#(Me)$V~FC^p$9RL-Ku(3+@mpp)Y*QcPEan5}5rn;dac3`S``|<4f7|kK|ss zM;f7x z+`ZxhLyJ%5!H2xlSC>@l*|1Lsg8g1gTuznY&L7`-f57YO*pzVnkSfq%WRMpD*QF3q zbj?)>J@EO8m!xC!Rc*GSiQo41HYK-+ygROt4YL5}e*L$g;S&RPuVOy5=0?|0Od~=Z zor;wxK}GK{Q`Bl;(*(O*3n5VF}MsI{=#PC&Nh zb6qzKhp*LkSKv+v4cRqJqEGjvLpxIl>~FC%jf z9*=RoI{5gzd54Ca1w%L>?0O1LY6+K`5(i?zER;B?ILBGL`X`2>5BuaW3f6kQW(07< zJ8Bv+6M#ym4mQ#!_yvW;$2TS)8IpcJxs*HP=_ksT`XN%C#7wCkz{hQR*^2x`T=C61 zpS;p#NL@To~H>l=K}2l|2z*SL(iNqNIn)w1gp;4|FE@=|)8H7puu1 zEb>_;*FQSJ=|d@a@3>9Dh&&;QM#eGkPzZl$A$80)09eyXv1R9N;@|+QVqr_Ql92<5 ziUbs7UP_lsz7G2LBcRrarz$8AgF9{zgFF5fw)>B~_$NTJsx4diT!EuE>W16nk znFo13&wh(GOEoj+%hukWe7N?%)y0U|w{9;;!E+(3gR5D=Pp`%u&%?AVe!eLY!Q|e= zNvUfAK?d7i2@3PLyk)lJ3H+95vx=Uw#bZa}A3C&xPxwx!(2~)s#8dLa?($*W;YCZh zz>(k4WI69KhY}A?=h#l}k@QTuqTr_P@Y&6d5lm>jl z#T6h?2~Lw%kXf#0Ah@=3G)v+C@g;Eh%}l0O=MFohyID=We{h!H1;=9YG)71T8-|si zV>U5<8Ig@TY#NNBwXE`XROEH6mu=13w?xY*baa1aRt>meR4&cpll`@3?PRkkAkk-3#@%goKQOiiVDOCF}=?6Ke1guaAO)jCffzR7Ck8y+s@cPzlfo zX?Y~jiPTIm=$wgp1LJcr=_M;$Nz{iA7(k{jL0H(NkI2X=7@3$^SlRga1q6kjJ(rS} zk(HBIP}I-_YiaA~>Y15aKrF4SZCu^lJv_a<-vqx42@MO6h)hUKN=`}r08RV!IX5rA zps=X8s`_h9?YHlB^=<7Pon75My?rC2W8)K(zow>_mRDAPudQ!vZXF&SpPZhZUtC_@ z%Y_7>{3X`EO7K*(8mh`1dW7=L*uP{i zq`T{_PyQY2{q$Wz$yatF$?a7+`rnMGF3CmtH+WI{hwIV&)CSuVLtAg&sq4LsGH$4f z^$ID)REV9oFxb~7H~Aj88F?e7b*H*t4+nxTvotz%6vryah>QWR=brwSfB zdiZAEPE@KiaoLh+pGCGDMo)`oEiT}8mTcFDeyp!qrO5nzn`NCl18THgSEbTOr0=DQ z43DocA&%A+r$pR2RFrRA#3XON{M*uX+{3MD!@p430|Za>@*EOTM$d!?=Dg&B-~z13IOJ$cKnbBHtj`wVJV(M_NWNl=ii?umQa z^E+Ns9(^YWW73S<+yBh9L@r@dqF`XZPv!d1`7Cy}tif+@kViFhfBxv;3(;eFZO`O{ zL?ziv@U(}1-)&h=dqcDD4bcC; z(H#pQ@_*>=4~_q?Pj`kFqlYwi#BiWZmC&o?p6`B9_@2Xof8;yXI|i=;okQbrzFWNZ zdfeZ&fi5Xu`1k5KOB019sAlP=dw`Arms>fTH8C3%uI|So2j}69l%Fr``Ckxq*0YS3 zoeGs_hi_{0@)e1sN-BV-Nn&!`kLcr%5`vy7{z43ymMOj1@4O1FaNqWF=agjS6z%*n zHzg+`If2U1?Z{j1SrT0_D}K`HM_=N3^d6V^2kHOEg< zxtdnXwT!3P;6NAe#+84tuOR7{@+}INt;zQ`Z{p70<|o<>9)8IXaek>B$Kb+7C)@f? zoyP)j*uEq88r3`uO?^|p5QgY;dgYieg7!eNSW7?B`AsY72QS zZ&KUHe)m8Qy~y(*YY|5A_JqPfkcO<02lw~tS?CrepZBUxo>djWnE@FY*295T7d+td z(eCbwailSJ!A0kR?j60Y7}1G8*$u|wqe#xYNe;gLx5iR+Uqz+R?4M6e3x3NKjz(gT z3GO^<#?qLPdsqkv7x@ z$P2_iU=fqo{;^Qo`MbOY4xmomrrFLCMn3@SG7}l`l3~q$y)wO6?RDjd-QogsKit z6_FTv9q}V^N0b<1QxEkN>rQQae3N1~PoqV{X z*q)ZD+Dem|Pk{r|1e<9l9`buRa&A`YDhUO>bU|v+H|rt&OV33{R4>1brY7W$?0WW; zUp)AeQ6uBT6*8?nw@ZJg_GOq5P=9xHO` zzqnQR582&{bzH${g#$P@eizoIUcy~^{FyCFWp7X%-Lm-<{> zm283PYK-o0?vLb)KIUF>i=i~;P~%7-L*1NLygfY!2g3P5hxPj)Zt+_TUxsJ<9eH!)dVBujhE{6x zld&3VuJ%w@=*tWm#zUKp-8qR)HDQVw7&3zR)= zSQc7usYl?bIez$V$U#rzxqJ+jpKh$o(K{>pU&Mf#HiwMX<;RR$V>dX!msPCl;!Rg; zJf3!wZWMDa3EB+Hde;!fp=mT;Q6 z9NalJi0i=n1qYVaJN85T$PXOKa$dTrqETueTu*JKraTV~KN}8TN|?UFQr{vc<*BGw zBu|FwR6tNax+RsEk~C6N>j}zd#AUDr;MAD2OI-is#}8LfF<5Tqs>f$ zk=)8S&89@PF^9eVa(-$laQ*w6(K#c7UdGOj4@gq|^c6B-RJKCu8e+}C-2BV@y_;^b zyvn~!!@vI#R^b_w%xgqP^v%P6>LL0wIB`dNCU!;u|1H;$KmN%eiGtdr2eugXq)0&? zYJ4<5dQS8T8L$~9x3ha%Jmq^msv-jCh-F5eYOg{Lm{qx~&$;yxqMvfbEca5q; z#ePI(@{B{H@wKDB?W--H=>z9}pJ$OZa3d@D_*yrOL!5r{i!DWO{MTdTBIUPDJQ}2^ z^>a~y+mz2D2s}L1ac6&guE(p56qDS@!DH!i&Sqx3(Ap2-9&cRXI=s-9rt}iLqV(iF zW>FeGzx=hAchpe}sx!KN6=-Pa>!J9t+LQenw+Yuy;g65V;@iyPan)NFVMS4gHt(sl zf||HSlPT|p@Kiooqmjx-dGuU8o_)OTyB{4yX5Du)^jzS+-!+WB8D8h?$M^z@CX*s8 zOKLwtESIh3t*UkAgSRH`XjG)43LVI7en{dx!Xe?;&j}5+R!PjyHMwbRR#`4Zlf~s5 z-QJB*AoTD0JyfG;QwI_(qe8BmaW4}(MR!9t30xV;AgTh%9)SbXa$Cctt>2~bvf{@c zRg@EIb$`8D#7Lj{k#TpjUHsu$4Zm?;R_h`;a3WjVV(XanJ2UQ_ZZgpdcNX~(%pIli zSwn+()|1Qf{rvpx978Y2Zr-e=VQ}5DVl7@U*?{k(qz!7PHw!A$=bB8Rp2%M6U%suf zf%pb^jE0eu37!T*b7e0h8ChE0+qjh!6#Ag#DKD&$PLGOuTFonwn3X(;=XqRr^n zIDh$s8osSP9J(UG&n$ZGC}hQDr8j$Oj9NZsC)OeWcYNy!(TauG_cY}TAQzHljxZqL074sE`OwH z+TzIqgDhA2s}1AKa8iCBHov40)&_x)_;vP0*ANzOHt9n_Hfe>&iJ280cAxkj;$a0DeDndT?lrl0$ReE$vbih`l6>3bW<4| ztP)A3>A4^9$HQ@KpOC{|zcLEXa~};m;pv9$=93zlKWabCthN+hSN;~;WaxExx7c~^ zJs)>_?n|`@k!juit5juKjV54Z_<%JZ=E0EzIys~e^GifpldQ_0Y zXzou{uv=3|`{-4`b}T<;0;7pAvD88{s_vDV%+SMjFD|Vjxi93s^xE;!L!@2Tb(3B% zj~OFKT01+IW;t%=PcG26-(!^*$WG^G-LRGy2b*kD;WJTEE^@@IXq{k8O_3dLKnQ+( z`pvW?fX^WjOHQHo)(*0IA_x2rUdR`{cMW1(72HWjhq_1oAC^DV}fAC;E! zeiha737i_yM0o+*N=&8LYV?v8SUNp7G=-=y>FT!WNXJ%8llXgi$gJc-mtBPXRhI`v zro2BK=nZYi;}JdXw31-HOgI1d+>3xCvi1~Hy84sV%O9jXrYXu184vT+YM)< zyLN?7c1i6#YV%QXKejWRrk?*1{ONl&v+|CDI&Cjif%iXJ1&Z{mX;@1exU93H-YgNa z$Q&we+VEo5>6;a1a$DQ%4^BIB4De9#4U1-k!#f)>1OK}d2P^lg`{#}k%+#w8ql3^L zrMNeaNu)=hEv>;M9f#LRH*fa!Ih-J5_U+&e8(Mt`cKHN%eb-I+&ng}d>;nTSI1cB! zIN@Y;>=?QC$Ii#zGVr&$$e%YnV{+$F$u-aaLPG%Dc05??Q)KK-9*2&WhKN2 zB}jPm>k`G`Dw{R>VvFaOA?+8d9#3OZia^?Zeh~+uFH{VYS&!hW70#n6W4UJ>=-(b@ z$K=Vo`mM3gPq%NVxzE(sTkT0&lX4$^r+_%SJMRV zR3&=eVpfFz+A6MSKNn{;CEb0zK*~3+Fr2XE0nGfGXH_K|NRriIA2!E(%|Olbym~6M zWwwBEUCFHoMegisIQ8yS#wK(EAZI-sNjs4*zVIS;t|Qm+)O(JkmnizZ*(Sh(N#-hA zIrh4W!12>z92jUikx_xT?wMDU+b2rc4% z_?Y*O=`U1kV8r6Tq>%Dg%9=9!s}=j*j=NM0yHuik-4**Y0ZNt1bd`>ki2bYaj+M&$ zu;$nDFtPioCj(ZoOQpSsI8>;uRB5DCuk1Khu|MWWS8t?L>G)c{tqyUhx@~_oWRFTT zzTZfxKfAI`*|9%hfAwpI1uUQ%5fKTgL_DHCU}&|%vC?rjBE51oJHFm1ex`!1EnZB% zZK&U9kBVWhuwv-`=|=Hm0s9f6yV8zy_ittNBi8W)LahHR@CkgmCqEXArdVyFAaR^` zQqQnVkw~OvT4mKLC*kmJ7;QKAr?`5&x`A22^bmiAggRmp9TlnR)A~@^&k8m*u5Dkf zJd~r{eFUBr?0@1ROQ8JS6LrwV=E^Ku_nIwy%C{9Mz7lmUR1N11p;mZlSR#>f`@Ny<}k%so;6h44}qlaEf1 zt&=b6fXuZhycY$GwhRb){s{<<_wt?Y2kqbIe*}ThKLNpgJHqk@1PxE*jS(CFB?!*` z*8{;|zeRouj$gB>6F;MUyq}N8+;7Nh&*SRb#2qB)$4cZ)L=T# z1H8P(=udTC#)>Z4N{*?qtyp;!Cl0Tg9$M&GuH%~@f9g+&C09>1>HD2Lb!phx+!`Gn zt)J0yEaUv>mx1Q~n~>!sR&SkJsnzM)-A<&Q{+nxzu%PbuHwE0WF4cFgLQXwzC7vD3 zP73ZYgG`Y6>EDs!d2gEZniJezb5o;#jx!O6T0}83Cs-zf6PYqyBl#ca9GV4lR!(!G%!jas?(;t)ng6M!HAGH9 zR_K`t1%MAYA;wQ!q9Taz2H?2A<^8^}`xO8L0Ej3Y6aZidvkPDc0Co_-4guJq06Pp| zhXV-81A_oC1OP(;Fbn|0fdBvq0099IAOH#kz<>ZaPz(UYAfOlm6hnbx7*Grc#sOd) z1dKy~aVRhj1IFQqdjoI~0EYl@C;*2cEIh=6K>!E@KtKQ#1i%o-K+|L00atvfC8YP02n9$4k`vf#UM~I1XK(K6~jQq zaL_ma8V7;KA)s+6XdDI_hlAh%2o3_lAs{#u1c!m(i1$E<4T1m=2mpluFvM#?*Z~MT z2*M75utOp2FbF#w0tO&p5CjZ?fT0jD3<8Ej0su$=2oeB+1VAAHFh~F#QVc+f5q}hf zK#HM|Vi=?t4jBg^;~>a51TqeVjKd(~a0ncLz(Ei=1OkUb;4larQ3Qw*2SEV{6o5hj z7@|a=>;RM<1Z9Un*`ZK&7?d3j1p`np2nvQk!B8j|1_i^R0RS`r1Py>d1EA0V7&HJ5 zEe4>)AZRfJS`392!=S}*=r{l!2SLXn&~Yeq90nbSL*W1v4uZlVP&gC{he6?p(nnM_ z2nIl402Btm5S0OA2Vm?V7&`>U4u!G9VC--h7=VF6FfarLhQh!w7#I!<0AK+iSO5eT z0EGp>U;%JgF#szD!HOZUVkoQ_1}lcc#sSzk2sRFZjYDDMFxWU81_xkp5DX51!J#lX z3UWkJTD6j)?b`YE$0%wQ9*F<>hX(-g z01!L?0uO+~17Pp~IJ_8u7lYu%5O^^ZUJQd5!{Orqd>jNHhrq|7@NpP?{QhAGpdo4- zQRE1@|2+H;>i^6z;lTPPk-SrOYY-;h)@W+95pU>2T2VV89=cB7K2)Z5p*Z?3UtEb1 zl-^e>^5N8KD(xv-2BL3{!aumkg@!&%e?E@`6T(Xq(cC+d=oc(|Brlk?w&^8MW!;mo zFes5O$ORPvUztjNzU*C6O2qrEX4|8gRpqN05s)T_J~mmT+=7jA?!F>oarM3qs;kkh z^5l4fC*wStx-@)2-9kx*q-ex4a99#1c)<2MMz?@pIgc=YkPr;>Pln6<`H$O< z&Wyv#zdA$01>=+OeQ0~tD}R_$67OPISv)5#rex_nG_8cs6EJo=3!u+_WvQAHh0@7^ z+MlfslN-0pOeR;4=5x(JmJ--^p4YQZyHCwNofY%@`Q9bNuP5Ezs3ykfdzSltv;N)9 z=&7H5+qItsmDA7(t;o~aU2eCW_0vwRNpJsgkNUuI727S}06Ev~G8I3(^6kFQWDmaV zM-NWkYHgeFmwm5pxj~!XYP6fp8?gKCNMs0B)3DQD*>Ha*ecKj79bH{L(Dn7y-0|P)5Ns@%E;qW8XvnjBo1i?ga=?AwLy=!rbLxcy`r7SO9bB;F%M?P0SM;%C z^=Efd&|qEGyBDb2eWsDqI}UxzT`*SP**UTT11^KL=PE`hRTa-?$#-9|s6O7hJ;1wnA;aL4 zjff)>m(6Fv{w_^ekYH9WFPVTv{&@bB;w)Xeq}(?k6mD;IfHUy)3+Yo)?Xw~VGwZ9? zjhtVx?YNrTf`tdDiB8&@;HbfqcCPxe3}*lwB4P(Vrlv|_{Tz(-G24vL5luWoxRx&kG6u;9zUjC94P!%rCp=VKcyB@)E{i5D0jq49igo#x5=hrv8s`zZXAgHDZ3w) z$cm(Uq&xbMC3Zo8x)Of+eq8EZLI7$YSVq$Yy=R}vnL9(&Yx!kdN!8Osk# z(X!rSxJ?y;!c_wb>^(eVyWVG?PZpTzejI=0(fE=SegponUb|q%YyYtFy^HULH`a1%mW@6O{`HK(8=Q@wmaMlJ7#ZqKyEhynldUuGx_P@Ad z{_Y?p`BE&>gdk9MggHTvSpNGjU;OnwmA`-g;?LJq-o*`IBfc2-Za-)r|IA_J4SsyS zk@NxEDN>60b+A2Q)nS37(OFHs5aiCJ?VA#*q|r(SuMCGdYFeq@CH&;L>|L{~x7)f&SI}za{;jVTAvQ^y>dtduJXF_4dc{K`Lb!Dq<8S+lZ`fW3Dx_ zB%_8Xp*xl=$(AhTCbCV$C|N_ysKKR$63M=dEp15I%Mywy%KH17mM`C?d+)!$=ZVKN z51#XKKA*G9_q@+JZ+<}Z4@0|6hS_B5CRcTc+4OaFBob3aygS-$AI0BHsKJIIwKqR} znitEy+P%7?<&$93=&^kEGLGb`I+Z6M#DXc7+o>2B4RZAVPhJ<6`(&DSG0s0rZyQmATi4BcC_vPD(PYrc8%=uW|xejdqnWMMv< zPVs=9K-jI!3=P^;WS17&!v=SXTq6BKj%oCQgy=W_DSC!P{v&!;1>-?Z#&-| z)YRL($-$eaq9mc2rUVC~KYf-Fy(c4jjNsSk$K5I~+d2c$SH(V~O6`#LS4^G$Gd$)w z!pMu`y8ng6wojz&Tx@&NlD0Ph+Wu)|<}q0ydXt+a(xxYwMt=ey_Uu1J&v;K{7QOcW zarAOB^x6_2FJj=h|8aEo>!@FPvEWn)hvVfU|)+?3fdl zLTGQ>uj^u^wG+Pn!LxT<+BqyIhTOFev@R>^U{}x{r6^jz0ozvYw zYiha@F4ASisRsit^C`)(;%4)@lf33Hh1YQ2DpH9GAFxw*x;Vl**Pq_h%QC5*W1HU-R(1}$6a9Pn@(2Da@ddC+YYA#3a&TkWz7rWCw_%={X ze{KrCUPEKBv9IBS>iY`k0yTd=`=+X@RBN;fLda8jn;Co8Ompjo&;#g=2?T3){%v`i zEcp`UuH7ua#al>UH`X+kf1dCnRNH9eW^LTAyY(CIWWKC+R)|(5*^U&AZKBJv1*=fn z1lWD-eGxMTj*Sa<`gUO&g^g3NA=9a>$tPdKb=%!dM8th}M9xp1oQRU?CPch>syv;w zvh$($XQuQ2*bZxa3y=s!KtlO8|1<9g;2cmKU8cW60WcM0%mML7Pw})jS?+1e%H~GA z+?Tn_(BMebS--887G@s{YQ*|y)JUUaO52754d8m5*&kuyRTBJ_Nbf(eUD$Kfh{_gH z5L~FR+@-kWWyuH49J%Ma`>7ahYCvKBY^d32Lo{`NJlTkXN99}cU5Xz;`c!|Ccg~x& zEA=kN``M?Cyq3LDp)l3I(C(0M{!OM$*A@enbpfK2BU~B1g#wQSw-l+X%|%QeW5wp) zDv7I}vl7fAI=mW^nV%8#o){Wm%dBh2jSi=@0U0U$w~T=ELNRGsMg+9^zh?voWCZgy zBSau0FFJ%`qTVlvw2zgPqg2uS!sZ%sDu_yn!_E84-t~O3W4n%TG?k0iD~nz;QvEJ# zbgk@z0MCBLU61D`i8QPmQl-@6}`R^tY?q6!*AS4r>g67+B}>eol?BThRs#TrravbXvw9F zyXy71Y>&_=A*ZBw*HuJx0pWiiGoVKC>ZYuEBhZN!zkb#zc7PfM7KZ&vqgedeZ;j%& zM)6yt_^wfKCDr7mRfHh6s6Ja1E5i8{EVJ(`Y^gaz6AKqYN26+eBsJb<-6wVGhqIcK zlJt1D=;NQ}kug^<$2x8(96lI(Nt6diuRRy;6zgR5VBf$RHvXOFR*Uil2L`{_ z7?3aAv^ggQRLle(k!i8vqNX{`ic?kIarA}&2YS4PilM-fdmG`0JPkdzwA;jQdgOTZ zQmv$|jaOp+AZf}exR#Qyl=%L+vM;G4QlR1VHS@Tpy>AyznHwCYI&@Q-*f7mRbfF}F zuw4A^{Q8@s+nYzPxpitU(8I3c&vYv6N%oiJ*=W6ot%?1qdVR!BZmT(Te)olqb~k#0 zg}erPLPUb4dPBuxkN1X2QEv2x%M=Xu66LF<`YtNJINldQ&r4^8VKI-Ru=o$FOOXac zQW=*xSh>W#cQ%VUj-Ay8y4STCA?x#?DqGI;(^1>8@iVxdG3@w)Q*&%sUm?o-1&_#+t5~PdF$tSH zBp-S@e#x$+nhK3RK+sp;jzQUP=gy~lKFF$NNs+OEN%Tq48>w4U?A5*)jRsa0!U@0dMo8 ze2=8w`-B4A)am=ynm!j?U-< zD@|73s>8j``|d>Es!?}r!abGjIC_2FSj38P@8Y|b^#L!h47P||M-V5)n`krrLp~4H zZFO#&Cz0YBUIaZJxkehYpP8Q!5v&!qsr zaRii0#L|v;%+>XGzXLklw~wTlGMA1;uWz5%TIXb`4+*e(6zR`YneW`<$;Axq*iurfXC1b_$ zf8$@MNVN&@7F`im3qA3X1tq8}V16H;C#aCU%Lv#RCJWR>UZ&OPh?2)^&zcCpk+~^I z*IIrb|B9ZVZhfiT^5eBDYfOj>PKSM8kCSWWb|doEpB6Ps-|*z}L{UqNq}8#>HyLGX zR5!4yiIad578|sI9{iJ|aDW*-#;!~cE6Q?J>D6npq zyOLUgHp{wh%*B!Age0jJh#tGFcJ_?^c9=xj&0|uNJ$FwFCXy{C;p;7Sc&%hx&2cr< zFOcuaCC4n(e4zJk@iBpwoh50*#g^Ye!^zbCeae46Q<$n#P1eq&XzLN`l5iZqTvS#+Lf z%vK)Xtmwh1i|JGh-G2aqQpW!P1Vu?`oKQ(01WEw*89<+9UPDmag94!{IS3Rx_zyr( z8-oI&!ZQd&;TgamE%6H(8VZC;xgby!IPS7^urCAtI)_k66a-2GR?EKx{yK+HMG*vI z5B&oWnnQ3zLzOrXXkx|yE}5Aax*RlT%NKKCaTx5NB@Fh{(hgdD`6Kz#;tkeIihp}R X#~H9M9@>V90-w{XVX!p9;&=Z8k34|n literal 0 HcmV?d00001 diff --git a/Examples/images/bmp_40x40.bmp b/Examples/images/bmp_40x40.bmp new file mode 100644 index 0000000000000000000000000000000000000000..6d175f517a47629239784a4b601687fe8073b2b0 GIT binary patch literal 4854 zcmeI0u}i~16voeg!O5Xh5$ay**n&tYG|mo%P$=Sf|AAz&;Fzg{2!e|^-Jy#e97;i< zK{|BndaJ$7*{^-_dM28-Y8OpO!{yz*`{jM_OA6gPc-<7U&o0h8&UZs>(ZnZbWo&k2 z3jAROMB_$XHL1YrmE~uEf`#Duq#-bSVldU@m9#!RH(fN&ZS`$IlvKZs^*rtXS8M~v zH{kA;p6jlpv0E1v*a=Fpov|l^bPqz5ssdHt;Buwh7#ihNQQB-DzULShm&7H7nY&VM zjEhU&b|IGv=k+V)Cb?)_0gdaonF|5QLbRB02)2q)_?g#ZmdEM2Nhn32SgTkPjj3%A zfu038_M@-Xk&8W{9(=`5hwB+??pNF&=B8|$O=SvX3Sbf!-V>z4saXu`0sL0 zjVT$yLh6csTU=#*jaEBcKb?!E}*K;mFAckfrwS??z;zIc{TI|&84~C>L)K(+u za-&)^s#W-Ss#R2Uu}r&@zJkZj&*uHTG}`Zm$(RI zk6@?wKP9FQayS1K7dkK5;$(lxQ~-hN>AJ-0V)p7FG^F-4>YWCm^S6D)g=B*@Dewjt C;ZwT+ literal 0 HcmV?d00001 diff --git a/Examples/images/gif_60x60.gif b/Examples/images/gif_60x60.gif new file mode 100644 index 0000000000000000000000000000000000000000..e2f375e8bf0a9443eb5a5d2ef34a3106116d6850 GIT binary patch literal 1485 zcmW-fd2H215XYyjCIzJ}H7Zp!P-+hr4?392<7v>UPzpxlTcCh~+KveRfm#ua$9i$R zqO=bM1}j)gpSVzin1XFIB3=b1#cMSNL*xjw8qoNRcxL>;{;}EJN%lMQ`FMF1oJIwuR@Ul;??I`g*t%mEO902n|36byhtFa(Cd2p9zmU=b{VWv~KP!2vh~ zN8lKofK%`Q9>EiM1~1?h0zg0r1c4z41daX%MNk4|PyvlDkv1X_0wYLNLj!0K4WVH) zf=1B-T0~1|8LgmIbbt=g5jsXE=oCGmNA!fA(F=OT02mMhVPFh`K~Vt}Q3;h%1vTm; z8XF0SfC(gOiwuxKGDL>S2pJ^{WRWb9WwJt6$pJYeN935CkW=zN9?27VCNJcb0#HB- zM1d&?1tkSiBqdTN719VnL>w82ff*zs!UotN8)Cz3gpINVw#b&)GFxG*?0_AzBX-PA z*eQEpkL-y(vlsTt0XQHB;=mk)gR%lEvJxw^3Ts3^G8+X*fCVHnLkx&PF(ih?h!_tS*7BAve0!Tm!B!MM}1Qi8Q6eUp>6;VACgvgBu zNz_+VjG|yb4XPnEtVYzRT2PB>NiC}twW>O`HY2lc3))U$d~uNpuDY9I}) zK{T8Zi-H!p9U+hAJ}Z7U^=G$Sg%bl8OkVW*N6+Tw=e2&mz3t=W1*2~GX5Fg0E0Y(E zy?fhxV@G}d#J-Z`@QRk=-lb(X9^d+2>d=&Ac3rBqrf}4h#Mj$9V(V6Kj2EYWXlZME zZp%wmIn`gy89j7EJaOu`8~0aremA9R)A4~N$9HUWo`_?8$PVYQWdHT~O>ofAq0~ww(VOr7bUBiaVXq@)QsPF6fvJV%m zTisjMP`~)quKv{Nh0A)=UHIxRVcg@XgD(}yV;k$%_f0ykkJi=B@(=u&?#qcCXsYNg zyLj2vJicrHTO+SMaB|~Ahi=i#%5{k+4voF}wWqEeI&9v}Ieq7TJ#qhwRrz!OtLXai zg<-Vi@Ur2H`wuT4v8?>aijuYSkE|?DZ#j}2v$Ow5(>0%!A6+%>+xbUd%xrJZhIs6Y z=6K@Q+})Sf4wP?ReSTI&NlR^K&hGqzffdKDs_r{h@^XI7-hx-ArZ%_d5B;(b7G-*? zbF=HG{NAwszS5<%p?IKhVrfCv%984P_Y7)jZM>oW&FZCnzthX3=j|=n6nmrl3TVvR z(9o85va+m(ViTL1ld(ZmJ*Tg|_LbO8bpsu78oWEbeERrQYUWQ%CRW~-_J^C-Of4*g z%IDvoMa`Z5*9F_&%^vZyPn0K5w9benn*6P|^=y47vw~}O47u`P$K>vGO+#s0ERokf zSEjLNYj4)3 UG4)V-v^*o@zIzapSy?dQe_(CIbpQYW literal 0 HcmV?d00001 diff --git a/Examples/images/jpeg_60x40.jpg b/Examples/images/jpeg_60x40.jpg new file mode 100644 index 0000000000000000000000000000000000000000..aba410c61097f9f6df7dec1a0542bc58b2dc1f79 GIT binary patch literal 2538 zcmbW1cT`i^7RGN#AOQpnO^Q6Es1!j(niLsf0t5ySq)3g5^a+t70i`GiW*8w-BXIyx zibxX>kS>OZz$ii@B`6RO=@6=vki2AO*34S(ulL?}&tB)d>+W;c{++YeVGpw>fP+@% zNOJ%L0s)bn2Vm0yH$-HBF96us0BQgLzyMw_2;k*75GSm_;6G-r2ka02>A5fVZ~(%Y zI94>=Ui2T&%>zJ5j{%?>&mINL03HyCJN^=w7t9qO1On!T@b*Vcd-WP3+eI90sTWMqrR4zyk+?;UIP=AkV3k zm$MVx&G<_o9x#NLk5iI>ASa>jAg6pVn1@p=FNA{&is5_*AaGt$h12Kx#O$v?6+^_e z?xYpKluWBTBpmt~%4e>I#`6nE9+HxlIeP54imIBnj;@~mSp%~l5#|<_NUMvEPR=fu zT;05{`S|+z2Ly(N--y6QM%_%fo0xPD_e*kmMrKwv;b9K($ zYHWH#>LkDIde_}EK%ssb92y>>(I=Ti@8+`o6uh%jE(A;6JhcCi@RA zIERaea|RG7mkY#$IbEdKu@`y1>ZE*fxv)0jMPFdQ%e_C2>LtMV*yHt@zyG_q_ljwU}W6aAeH z_^e0gzl`#C_ua2((0_Y9WHv~Zr5ssuX%&Uj-WMS?2~F4E3C*t3S#H!Q&oDfH__HGI z9ng6@bl)QIn@OO}$*zAHF}~wkWS?%Mb&SQxO)8X%P5sKpxH%Us?NTFxDbk51ky3DD zO(|_HJ^rlK@Bj;nDBoTo?(@$W6itl{yw#fBjZ28uldP(o zmkFI8dJuRMzZFxQ@$-k(s$8QG+n?~tp=yfe9d{j0`vAcUEa^y!<7yJ&X4Ck+Wh29~ z04&}wEcn`)(Vu>BcC9E;$qU!4EYu|*S$1~vxvYcVWe}iuZEZ=<^B;g zp{M-)ueI@C9JLTGxfkaAEjYky>*bMRT7$dp zW`L`DAc=)Cebh=!Kx0;@*R4_7Et_-b*h~hLDNcFQrcY&xg@sJCd*-W%F(yy{YZ_kGWsV(kAjY2}JUwf!kPw=Sswfmz|^GoG5!FIn~y~(6z5mI|{ zo5BF54kKaxd>mQI)O*!}%xy8jV3XVsySan1G{7k$n=x~iyfkbdF&K2x@ z?=|KyZF_2quphsvPamw5!j>|*1WLBfR$+xI$K*u}X$ zr&&$|+ZIh$$c1w*GZXR{v^llUi!qKDV#b0yWjCm0z)D@kkJL<1%z`b7oTYt)`}d zm+I*5?05Is!SX?kb*q!^#`_L&iA(dTm?>xn8#us>-3};^G<@r(lh;nKxCzF@c!vs) zxF}>MIY`T0QZK}rJ$+u214Z*uZg=@`CK zf>;C(Vb|?U9yDHkac_;<{NQn|sJ*LsA=N`H;#MV|O1Xt99kPRG;*6NQ{W}g1kB3tR!TN;V=q8#gAS(0C*Sm1YE26xfE_@yfa|_D1Fj*#=3tt$rLm zowm#GFfT26`bCqKbXLx#7ru}u#X84wDYr`2-A$aZegpKLa>BKtZrVcs2&y&fJ?2h( zZF>S6@DmaCn#+R^6jtLMTQlsb!OJU^I&x-CO%IrqFJ2zqN6DQJL1w*5FAz=;wP$j5 U;*K8*i+75bnO#yv)$Eaf1G!X*IRF3v literal 0 HcmV?d00001 diff --git a/Examples/images/png_40x60.png b/Examples/images/png_40x60.png new file mode 100644 index 0000000000000000000000000000000000000000..053aec337a630917892743936f36d9a779a99456 GIT binary patch literal 1042 zcmeAS@N?(olHy`uVBq!ia0vp^8bEBr!3HEncK&z(q!^2X+?^QKos)S9a~60+7BevL9RguSQ4OyK1_tKko-U3d8P0EK#(D%NN*w>+efLFc#@!sV z%-7PpJku|>aP%pxp-a#g|W0O{tez{&wpMMe%@!*L|iOlT;I} zMV#;NV~P9~=z4+gaD{%@O851WCzZn|_FND<_Iu~`3ct^)ouUVi9(j02=g++_KiFPR z;9R%d?bed{e>B&vYEFDDBcLWN{XnPpao893n>QsszDN^1u;{6d`t&n4bEelbIlInK z=#QHg+Z^^e@5=FF7sId)@rm&k+d{N_?Jqw6+c!7mmFoTj8`C}NHf;8oq#N8LytLu9 zx7r%6KxL;?&WIC#ZI6m9>ir*`v$ka=Q%B6_Q-r9-Fv54E)~x zQhe2EuEPKFyG+IZ>rDtuUUy{q&hQF;1V~`8sC}WlS#G&F%PqbPq=(u}DYL;7RG?BbPX)|7wl<(Duf=zbb!< z*eByxVdthq$SW=)0}!UUEDkJzfppRA4->vGWCIHED&F#l(zAQK$@WcbHWU{s^!E%z@Kn2#AeUHx3vIVCg!0PeNn AlK=n! literal 0 HcmV?d00001 diff --git a/Examples/images/tiny_bmp.bmp b/Examples/images/tiny_bmp.bmp new file mode 100644 index 0000000000000000000000000000000000000000..ed4d948ae04056872a40adcb3b0aaabcb74a6995 GIT binary patch literal 1654 zcmchWp-;m=7{!Z0BM~(;6Z{3pmJmdf7CqO@5ac;@&BWwsvSf1dF2hiPs2NEsD(Y&_ zuJuV~f8TxSCnn_B z!x?!draLO6D@X;Hj?m&363?XWx_qyP`mzK$~>Gg@rL4$qt^k3ZmNFg zFC@7K<_javdCa|40i>z_oiP{EcU!h?wGJvEf}}M~GcYP$KZ-E{V{I5{itbcYgcDfz z4-B~HBN9tTQdG(%IL2yXGssH3zNr6@Fb%uZ6r@~&t?oV z&o?*)fj$L>G01$nmh&gWNE@zV!0r$X>_SDIx9@qp3Rylb*(Oy|4l)LrCk0hTslUOl zv>?YaphGExCkc;YoTVC3_cvgFQ8Mok176vY7#)K=RN!a|M$99EtJhsag@Ii$hmL&_ VUi~U_y}>aSy^b+j>z*fJd;%j^r^WyP literal 0 HcmV?d00001 diff --git a/Examples/images/tiny_bmp.h b/Examples/images/tiny_bmp.h new file mode 100644 index 00000000..4239e548 --- /dev/null +++ b/Examples/images/tiny_bmp.h @@ -0,0 +1,141 @@ +unsigned char tiny_bmp_bmp[] = { + 0x42, 0x4d, 0x76, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, + 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, 0xb1, 0x22, 0xb0, 0xdb, 0x91, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x3e, 0x89, + 0xef, 0x6f, 0x44, 0xed, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x4c, 0xb1, + 0x22, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0x91, 0xe4, 0xef, 0x5e, + 0xb1, 0x6f, 0x80, 0xd4, 0x91, 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x24, 0x1c, 0xee, + 0xb0, 0xa8, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0x80, 0xdb, 0xef, 0x80, 0xba, 0x4b, 0x91, 0xe4, 0xd0, 0x6f, 0xb1, 0x6f, + 0xb0, 0xe4, 0xb1, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, + 0xef, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0x5e, 0xcb, 0xd0, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, + 0xef, 0x4c, 0xc2, 0xb1, 0xb0, 0xd4, 0x6f, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, + 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, + 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x9b, 0x89, 0xed, 0xb0, 0xe4, 0xef, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xa0, 0xe4, 0xb1, 0x5e, 0xba, 0x91, 0xb0, + 0xdb, 0x91, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x91, 0xc2, 0x22, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc2, 0x48, 0x3f, 0xb0, 0xe4, 0xb9, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x85, 0x67, 0xed, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0x80, 0xdb, 0xb1, + 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0xb0, 0xe4, 0xef, 0x91, 0xe4, 0xef, + 0x6f, 0xb1, 0x6f, 0x80, 0xdb, 0xb1, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, 0xef, 0xcc, 0x65, 0x9d, + 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, + 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0x5e, 0xcb, 0x91, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x80, 0xcb, 0x6f, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, + 0xef, 0xcc, 0x65, 0x9d, 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0x80, + 0xdb, 0xef, 0x5e, 0xb1, 0x4b, 0x4c, 0xba, 0x4b, 0xb0, 0xd4, 0x6f, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, + 0xba, 0x22, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x4c, 0xb1, 0x4b, 0x6f, 0xb1, 0x22, + 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x91, 0xe4, 0xef, 0x4c, 0xb1, 0x6f, 0x6f, 0xb1, 0x22, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, + 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00 +}; +unsigned int tiny_bmp_bmp_len = 1654; diff --git a/Examples/images/tiny_gif.gif b/Examples/images/tiny_gif.gif new file mode 100644 index 0000000000000000000000000000000000000000..f9b76d8aee05745c507b657e161c48097cc3fb8a GIT binary patch literal 1076 zcmW-fQD_%b6vn?X2`O_$NFWVb6T@y$cBuWUHJDUetU;^YK#-#0PT3+PC@Mx>gVu_L zrC^sP||8P6%T7G+H`2YbFFu(yK06`EG!4MpQh(HuXMKnZ5BodGWNs$c6k%$ar zK~`i#c4VReMNkyQP#lHzZ%{x54Ro+xsJG`nVMffmuCT1<;;p%O|_s?wCMtOzP` zF{r@|UWr(M1+kzO%z|5>MOYMzYSApZMOuO-v80yFl3Su>SQg7_*(|$dT7ea@qE^g` zTcHs~F{;swZmjgH+2T-#IlP+T0UpGIdN2>}fga&eJgP_Y=pN|_p2U-SGEeS_p5a+M zt7r4bGnnuL8xw2B(<+vtWgMH5C&y124^567==+8jnNs& z1SVlpCS!6YGJ{!|mD!k`nJi!t7G*IOXIU1jL91?89@s-rCY^#Ys=WpoU`N?;0 zt!_C#^U=gpca5!W>w2NF`IhfTr&nz}(b(Po!|1i$XPZwyHrG4eI?#V&-}!A_uU{Dd z_@$n)v9*29}-rq>N^dvDF5jn{nrZ#+KwVArPhnJ;dg-#hqW z|47H(v!Cs_c=5&7xl2H)!(Da6rZ|`c`ebeN?o!=aPuKmf@ j#!}m*iRQz{u76|kwGCGeJl!|;<^xxzUKws`!u|gP(gFls literal 0 HcmV?d00001 diff --git a/Examples/images/tiny_gif.h b/Examples/images/tiny_gif.h new file mode 100644 index 00000000..0d0e8ac8 --- /dev/null +++ b/Examples/images/tiny_gif.h @@ -0,0 +1,93 @@ +unsigned char tiny_gif_gif[] = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x1a, 0x00, 0x10, 0x00, 0x70, 0x00, + 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x2c, 0x00, 0x00, + 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x66, 0x00, 0x00, 0x99, 0x00, 0x00, 0xcc, 0x00, 0x00, + 0xff, 0x00, 0x2b, 0x00, 0x00, 0x2b, 0x33, 0x00, 0x2b, 0x66, 0x00, 0x2b, + 0x99, 0x00, 0x2b, 0xcc, 0x00, 0x2b, 0xff, 0x00, 0x55, 0x00, 0x00, 0x55, + 0x33, 0x00, 0x55, 0x66, 0x00, 0x55, 0x99, 0x00, 0x55, 0xcc, 0x00, 0x55, + 0xff, 0x00, 0x80, 0x00, 0x00, 0x80, 0x33, 0x00, 0x80, 0x66, 0x00, 0x80, + 0x99, 0x00, 0x80, 0xcc, 0x00, 0x80, 0xff, 0x00, 0xaa, 0x00, 0x00, 0xaa, + 0x33, 0x00, 0xaa, 0x66, 0x00, 0xaa, 0x99, 0x00, 0xaa, 0xcc, 0x00, 0xaa, + 0xff, 0x00, 0xd5, 0x00, 0x00, 0xd5, 0x33, 0x00, 0xd5, 0x66, 0x00, 0xd5, + 0x99, 0x00, 0xd5, 0xcc, 0x00, 0xd5, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, + 0x33, 0x00, 0xff, 0x66, 0x00, 0xff, 0x99, 0x00, 0xff, 0xcc, 0x00, 0xff, + 0xff, 0x33, 0x00, 0x00, 0x33, 0x00, 0x33, 0x33, 0x00, 0x66, 0x33, 0x00, + 0x99, 0x33, 0x00, 0xcc, 0x33, 0x00, 0xff, 0x33, 0x2b, 0x00, 0x33, 0x2b, + 0x33, 0x33, 0x2b, 0x66, 0x33, 0x2b, 0x99, 0x33, 0x2b, 0xcc, 0x33, 0x2b, + 0xff, 0x33, 0x55, 0x00, 0x33, 0x55, 0x33, 0x33, 0x55, 0x66, 0x33, 0x55, + 0x99, 0x33, 0x55, 0xcc, 0x33, 0x55, 0xff, 0x33, 0x80, 0x00, 0x33, 0x80, + 0x33, 0x33, 0x80, 0x66, 0x33, 0x80, 0x99, 0x33, 0x80, 0xcc, 0x33, 0x80, + 0xff, 0x33, 0xaa, 0x00, 0x33, 0xaa, 0x33, 0x33, 0xaa, 0x66, 0x33, 0xaa, + 0x99, 0x33, 0xaa, 0xcc, 0x33, 0xaa, 0xff, 0x33, 0xd5, 0x00, 0x33, 0xd5, + 0x33, 0x33, 0xd5, 0x66, 0x33, 0xd5, 0x99, 0x33, 0xd5, 0xcc, 0x33, 0xd5, + 0xff, 0x33, 0xff, 0x00, 0x33, 0xff, 0x33, 0x33, 0xff, 0x66, 0x33, 0xff, + 0x99, 0x33, 0xff, 0xcc, 0x33, 0xff, 0xff, 0x66, 0x00, 0x00, 0x66, 0x00, + 0x33, 0x66, 0x00, 0x66, 0x66, 0x00, 0x99, 0x66, 0x00, 0xcc, 0x66, 0x00, + 0xff, 0x66, 0x2b, 0x00, 0x66, 0x2b, 0x33, 0x66, 0x2b, 0x66, 0x66, 0x2b, + 0x99, 0x66, 0x2b, 0xcc, 0x66, 0x2b, 0xff, 0x66, 0x55, 0x00, 0x66, 0x55, + 0x33, 0x66, 0x55, 0x66, 0x66, 0x55, 0x99, 0x66, 0x55, 0xcc, 0x66, 0x55, + 0xff, 0x66, 0x80, 0x00, 0x66, 0x80, 0x33, 0x66, 0x80, 0x66, 0x66, 0x80, + 0x99, 0x66, 0x80, 0xcc, 0x66, 0x80, 0xff, 0x66, 0xaa, 0x00, 0x66, 0xaa, + 0x33, 0x66, 0xaa, 0x66, 0x66, 0xaa, 0x99, 0x66, 0xaa, 0xcc, 0x66, 0xaa, + 0xff, 0x66, 0xd5, 0x00, 0x66, 0xd5, 0x33, 0x66, 0xd5, 0x66, 0x66, 0xd5, + 0x99, 0x66, 0xd5, 0xcc, 0x66, 0xd5, 0xff, 0x66, 0xff, 0x00, 0x66, 0xff, + 0x33, 0x66, 0xff, 0x66, 0x66, 0xff, 0x99, 0x66, 0xff, 0xcc, 0x66, 0xff, + 0xff, 0x99, 0x00, 0x00, 0x99, 0x00, 0x33, 0x99, 0x00, 0x66, 0x99, 0x00, + 0x99, 0x99, 0x00, 0xcc, 0x99, 0x00, 0xff, 0x99, 0x2b, 0x00, 0x99, 0x2b, + 0x33, 0x99, 0x2b, 0x66, 0x99, 0x2b, 0x99, 0x99, 0x2b, 0xcc, 0x99, 0x2b, + 0xff, 0x99, 0x55, 0x00, 0x99, 0x55, 0x33, 0x99, 0x55, 0x66, 0x99, 0x55, + 0x99, 0x99, 0x55, 0xcc, 0x99, 0x55, 0xff, 0x99, 0x80, 0x00, 0x99, 0x80, + 0x33, 0x99, 0x80, 0x66, 0x99, 0x80, 0x99, 0x99, 0x80, 0xcc, 0x99, 0x80, + 0xff, 0x99, 0xaa, 0x00, 0x99, 0xaa, 0x33, 0x99, 0xaa, 0x66, 0x99, 0xaa, + 0x99, 0x99, 0xaa, 0xcc, 0x99, 0xaa, 0xff, 0x99, 0xd5, 0x00, 0x99, 0xd5, + 0x33, 0x99, 0xd5, 0x66, 0x99, 0xd5, 0x99, 0x99, 0xd5, 0xcc, 0x99, 0xd5, + 0xff, 0x99, 0xff, 0x00, 0x99, 0xff, 0x33, 0x99, 0xff, 0x66, 0x99, 0xff, + 0x99, 0x99, 0xff, 0xcc, 0x99, 0xff, 0xff, 0xcc, 0x00, 0x00, 0xcc, 0x00, + 0x33, 0xcc, 0x00, 0x66, 0xcc, 0x00, 0x99, 0xcc, 0x00, 0xcc, 0xcc, 0x00, + 0xff, 0xcc, 0x2b, 0x00, 0xcc, 0x2b, 0x33, 0xcc, 0x2b, 0x66, 0xcc, 0x2b, + 0x99, 0xcc, 0x2b, 0xcc, 0xcc, 0x2b, 0xff, 0xcc, 0x55, 0x00, 0xcc, 0x55, + 0x33, 0xcc, 0x55, 0x66, 0xcc, 0x55, 0x99, 0xcc, 0x55, 0xcc, 0xcc, 0x55, + 0xff, 0xcc, 0x80, 0x00, 0xcc, 0x80, 0x33, 0xcc, 0x80, 0x66, 0xcc, 0x80, + 0x99, 0xcc, 0x80, 0xcc, 0xcc, 0x80, 0xff, 0xcc, 0xaa, 0x00, 0xcc, 0xaa, + 0x33, 0xcc, 0xaa, 0x66, 0xcc, 0xaa, 0x99, 0xcc, 0xaa, 0xcc, 0xcc, 0xaa, + 0xff, 0xcc, 0xd5, 0x00, 0xcc, 0xd5, 0x33, 0xcc, 0xd5, 0x66, 0xcc, 0xd5, + 0x99, 0xcc, 0xd5, 0xcc, 0xcc, 0xd5, 0xff, 0xcc, 0xff, 0x00, 0xcc, 0xff, + 0x33, 0xcc, 0xff, 0x66, 0xcc, 0xff, 0x99, 0xcc, 0xff, 0xcc, 0xcc, 0xff, + 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0x33, 0xff, 0x00, 0x66, 0xff, 0x00, + 0x99, 0xff, 0x00, 0xcc, 0xff, 0x00, 0xff, 0xff, 0x2b, 0x00, 0xff, 0x2b, + 0x33, 0xff, 0x2b, 0x66, 0xff, 0x2b, 0x99, 0xff, 0x2b, 0xcc, 0xff, 0x2b, + 0xff, 0xff, 0x55, 0x00, 0xff, 0x55, 0x33, 0xff, 0x55, 0x66, 0xff, 0x55, + 0x99, 0xff, 0x55, 0xcc, 0xff, 0x55, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, + 0x33, 0xff, 0x80, 0x66, 0xff, 0x80, 0x99, 0xff, 0x80, 0xcc, 0xff, 0x80, + 0xff, 0xff, 0xaa, 0x00, 0xff, 0xaa, 0x33, 0xff, 0xaa, 0x66, 0xff, 0xaa, + 0x99, 0xff, 0xaa, 0xcc, 0xff, 0xaa, 0xff, 0xff, 0xd5, 0x00, 0xff, 0xd5, + 0x33, 0xff, 0xd5, 0x66, 0xff, 0xd5, 0x99, 0xff, 0xd5, 0xcc, 0xff, 0xd5, + 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x33, 0xff, 0xff, 0x66, 0xff, 0xff, + 0x99, 0xff, 0xff, 0xcc, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0x00, 0xe7, 0xe9, + 0x13, 0x48, 0x70, 0xa0, 0xc1, 0x82, 0x08, 0x0f, 0xea, 0xd3, 0x97, 0x8c, + 0x5e, 0x32, 0x86, 0x0e, 0x21, 0x3e, 0x6c, 0x38, 0x31, 0x22, 0xc5, 0x7c, + 0xca, 0x04, 0x26, 0xc3, 0x38, 0x8f, 0x23, 0xc6, 0x7c, 0x0d, 0x41, 0xd2, + 0x03, 0xc9, 0x50, 0xe3, 0x48, 0x88, 0x1d, 0x51, 0x4d, 0xc3, 0x96, 0xea, + 0x5c, 0x3e, 0x7a, 0xf2, 0x88, 0x98, 0x72, 0x47, 0x86, 0x86, 0x0e, 0x9b, + 0x3a, 0xde, 0x0c, 0xcc, 0x38, 0x0f, 0x9a, 0x3c, 0x54, 0xee, 0xf2, 0x41, + 0xcb, 0x37, 0xad, 0x17, 0x48, 0x37, 0x9f, 0xe6, 0xe9, 0x78, 0x36, 0xb2, + 0x69, 0xc8, 0x92, 0xdf, 0xce, 0x41, 0x9b, 0x37, 0x2f, 0x99, 0x3c, 0x55, + 0xfa, 0x62, 0x42, 0xa3, 0xf9, 0xc9, 0xe1, 0x4b, 0x93, 0x0f, 0xe5, 0x51, + 0x6b, 0xc8, 0x54, 0x9f, 0xb2, 0x7c, 0x54, 0x89, 0x80, 0x9a, 0x47, 0x06, + 0xd4, 0xd7, 0xa9, 0x4c, 0xbf, 0x0a, 0x03, 0xf7, 0x75, 0xe3, 0x3c, 0x65, + 0xfa, 0x90, 0xb9, 0x99, 0x59, 0x53, 0xc7, 0x4d, 0x20, 0xf3, 0xe8, 0x31, + 0x9d, 0xf7, 0x8c, 0x1d, 0xac, 0x88, 0xdf, 0x52, 0x65, 0x4b, 0x45, 0x15, + 0xa9, 0xd2, 0x67, 0x81, 0xd1, 0x42, 0xcc, 0x37, 0xf0, 0x2a, 0xe5, 0x8d, + 0x59, 0xa7, 0x29, 0xd3, 0xca, 0x15, 0x62, 0x5c, 0x8a, 0x0c, 0xa3, 0x26, + 0x0b, 0xbc, 0xf1, 0x55, 0xbb, 0x7c, 0x44, 0x3e, 0x29, 0x23, 0xd3, 0xb0, + 0x27, 0xe9, 0x93, 0x54, 0x91, 0x4d, 0x6b, 0x37, 0x35, 0xdf, 0xab, 0x69, + 0xf4, 0xf4, 0x6e, 0x1d, 0xe3, 0xf6, 0xac, 0x6b, 0x86, 0x64, 0x47, 0xd6, + 0x92, 0x96, 0x6a, 0x1a, 0xb8, 0xa1, 0xf3, 0x64, 0xce, 0xf3, 0x62, 0xca, + 0x24, 0xc7, 0x85, 0x81, 0x21, 0x8f, 0x1c, 0x9a, 0x51, 0x30, 0xbd, 0xbb, + 0x68, 0x23, 0x56, 0x1d, 0x38, 0xfa, 0x21, 0xf6, 0x8c, 0x03, 0xa7, 0xba, + 0x11, 0x7e, 0xf9, 0x79, 0x24, 0xf8, 0x9d, 0x5f, 0x4b, 0x86, 0x7f, 0x3d, + 0xf8, 0xac, 0x76, 0x65, 0x01, 0x01, 0x00, 0x3b +}; +unsigned int tiny_gif_gif_len = 1076; diff --git a/Examples/images/tiny_jpeg.h b/Examples/images/tiny_jpeg.h new file mode 100644 index 00000000..93a940ce --- /dev/null +++ b/Examples/images/tiny_jpeg.h @@ -0,0 +1,100 @@ +unsigned char tiny_jpeg_jpg[] = { + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, + 0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xe1, 0x00, 0x22, + 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, + 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x03, 0x05, 0x03, 0x03, 0x03, 0x03, 0x03, 0x06, 0x04, + 0x04, 0x03, 0x05, 0x07, 0x06, 0x07, 0x07, 0x07, 0x06, 0x07, 0x07, 0x08, + 0x09, 0x0b, 0x09, 0x08, 0x08, 0x0a, 0x08, 0x07, 0x07, 0x0a, 0x0d, 0x0a, + 0x0a, 0x0b, 0x0c, 0x0c, 0x0c, 0x0c, 0x07, 0x09, 0x0e, 0x0f, 0x0d, 0x0c, + 0x0e, 0x0b, 0x0c, 0x0c, 0x0c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x02, 0x02, + 0x02, 0x03, 0x03, 0x03, 0x06, 0x03, 0x03, 0x06, 0x0c, 0x08, 0x07, 0x08, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x12, 0x00, 0x20, 0x03, + 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, + 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, + 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, + 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, + 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, + 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, + 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, + 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, + 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, + 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, + 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, + 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, + 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, + 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, + 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, + 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, + 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, + 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, + 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, + 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, + 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, + 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfd, + 0x5e, 0xf8, 0xcf, 0xf1, 0x17, 0xfe, 0x15, 0x17, 0xc2, 0x5f, 0x12, 0x78, + 0xa7, 0xec, 0x7f, 0xda, 0x1f, 0xf0, 0x8f, 0xe9, 0xd3, 0xea, 0x1f, 0x65, + 0xf3, 0x7c, 0x9f, 0xb4, 0x79, 0x68, 0x5b, 0x66, 0xfd, 0xad, 0xb7, 0x38, + 0xc6, 0x70, 0x71, 0xe8, 0x6b, 0x26, 0x4f, 0x88, 0xfe, 0x29, 0xd6, 0x3c, + 0x59, 0xe2, 0x0d, 0x3f, 0x43, 0xf0, 0xee, 0x83, 0x79, 0x6d, 0xa0, 0x5c, + 0xa5, 0xab, 0xcd, 0x7b, 0xae, 0x4b, 0x6b, 0x24, 0xce, 0xd0, 0x47, 0x37, + 0x08, 0xb6, 0xb2, 0x00, 0x31, 0x20, 0x1c, 0xbf, 0x51, 0xda, 0xb9, 0xdf, + 0xda, 0x7b, 0x4d, 0xf1, 0xaf, 0x8e, 0xbc, 0x3d, 0xe2, 0x3f, 0x06, 0xe9, + 0x3e, 0x1b, 0x5d, 0x4f, 0x45, 0xf1, 0x76, 0x86, 0x34, 0xdb, 0x6d, 0x4e, + 0x2b, 0xa8, 0x22, 0xfe, 0xc9, 0xba, 0x95, 0xe5, 0x8e, 0x79, 0x2e, 0x84, + 0x92, 0xab, 0xb4, 0x2b, 0x13, 0x42, 0xeb, 0xe4, 0x47, 0x23, 0xe5, 0x25, + 0x04, 0x72, 0x95, 0xa9, 0xa6, 0xfc, 0x0f, 0x8b, 0x5b, 0xf1, 0xe7, 0x8c, + 0x35, 0x1d, 0x5c, 0xeb, 0xd6, 0xd0, 0xea, 0x7a, 0x84, 0x52, 0x5a, 0x7d, + 0x8b, 0x5f, 0xbb, 0xb2, 0x8e, 0x78, 0x85, 0xac, 0x08, 0x49, 0x8e, 0xde, + 0x65, 0x5c, 0xef, 0x57, 0x19, 0x61, 0xb8, 0x81, 0xe9, 0x8a, 0xfc, 0x3f, + 0x56, 0xfe, 0x4f, 0xef, 0xba, 0xff, 0x00, 0x83, 0xf9, 0x9f, 0x53, 0xc4, + 0x58, 0x4a, 0xd4, 0xb0, 0x18, 0x47, 0x82, 0x92, 0xf6, 0xb5, 0x2a, 0x4f, + 0x9f, 0x96, 0x49, 0xb5, 0x4f, 0xd9, 0xa7, 0x1e, 0x6f, 0x76, 0x7c, 0x9e, + 0xfd, 0xd6, 0xb1, 0x4d, 0xbd, 0x2f, 0x6b, 0x15, 0x57, 0xf6, 0x8a, 0xbf, + 0xf1, 0x8c, 0xda, 0x3e, 0x9f, 0xe0, 0xdf, 0x0d, 0xc5, 0xab, 0xeb, 0xba, + 0x86, 0x9c, 0xba, 0xb5, 0xe4, 0x1a, 0x9e, 0xa3, 0xfd, 0x9f, 0x69, 0xa4, + 0xdb, 0x33, 0xbc, 0x6b, 0xe7, 0x4f, 0x1c, 0x53, 0x93, 0x23, 0xc9, 0x1c, + 0x8a, 0x88, 0x91, 0xb0, 0x61, 0x14, 0x8c, 0x59, 0x40, 0x1b, 0xba, 0xff, + 0x00, 0x86, 0x3e, 0x34, 0xd5, 0x3c, 0x61, 0xa5, 0xde, 0xae, 0xb7, 0xe1, + 0xfb, 0x8f, 0x0e, 0xea, 0xda, 0x5d, 0xe3, 0xd9, 0xdc, 0xdb, 0x99, 0x0c, + 0xf6, 0xd3, 0x10, 0x15, 0x96, 0x6b, 0x69, 0xca, 0x27, 0x9d, 0x0b, 0xab, + 0xa9, 0x0d, 0xb1, 0x48, 0x3b, 0x95, 0x95, 0x59, 0x58, 0x0e, 0x23, 0xc4, + 0x7e, 0x00, 0xd5, 0x3e, 0x14, 0x7c, 0x46, 0x9b, 0x5c, 0xf0, 0xcf, 0x86, + 0xae, 0x35, 0xff, 0x00, 0x0f, 0xeb, 0x1a, 0x1d, 0xb6, 0x83, 0x7d, 0xa4, + 0xe9, 0x57, 0x70, 0xd9, 0x5f, 0xd8, 0x0b, 0x66, 0x9d, 0xa0, 0x96, 0xdd, + 0xa6, 0x92, 0x28, 0xca, 0x15, 0xb8, 0x74, 0x71, 0xe6, 0xa3, 0xa9, 0x58, + 0xd9, 0x77, 0x1d, 0xc0, 0x5a, 0xfd, 0x98, 0xfc, 0x11, 0xe2, 0x2f, 0x09, + 0x5b, 0x78, 0x9a, 0xe3, 0x5a, 0x87, 0x5c, 0xd3, 0xb4, 0xfd, 0x5b, 0x52, + 0x59, 0xf4, 0x7d, 0x2b, 0x58, 0xf1, 0x0c, 0xda, 0xe5, 0xf6, 0x99, 0x6c, + 0xb0, 0xc7, 0x11, 0x59, 0x66, 0x92, 0x49, 0x42, 0xb3, 0xc8, 0x8f, 0x2e, + 0xc8, 0xe5, 0x91, 0x54, 0x48, 0x06, 0xec, 0x82, 0x07, 0xd7, 0x63, 0x30, + 0x79, 0x7f, 0xf6, 0x73, 0xab, 0x47, 0x92, 0xe9, 0x41, 0xa7, 0xcd, 0xef, + 0xca, 0x4d, 0xfb, 0xf1, 0x71, 0xf6, 0x8e, 0xdc, 0xad, 0xbb, 0x7e, 0xee, + 0xce, 0x31, 0x4f, 0x9e, 0xef, 0xde, 0xf2, 0xf0, 0xaf, 0x13, 0x1e, 0x48, + 0x57, 0x77, 0x95, 0x92, 0x93, 0x4b, 0x46, 0xf9, 0x55, 0xda, 0x76, 0x5b, + 0xca, 0xfd, 0xad, 0xb5, 0x8f, 0x52, 0xa2, 0x8a, 0x2b, 0xe4, 0x4f, 0x4c, + 0x28, 0xa2, 0x8a, 0x00, 0xff, 0xd9 +}; +unsigned int tiny_jpeg_jpg_len = 1158; diff --git a/Examples/images/tiny_jpeg.jpg b/Examples/images/tiny_jpeg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f67f41c0cf79ef75a2bc59cc56ebd2ed45346f81 GIT binary patch literal 1158 zcmex= z&EU+y#K;IjP{7Q}3<7K{EX=IzZ0zhH%)!ac$-%+J!OqUb%f-db0|e}xeEhsTd>}T+ z5Ri6|E+FFJVCMj-APxLKz#z!MAjF`+%&5e`B*@4t$oT&VgFMi!tUw=uJOu@eOw25- zY(OV*aRU`>6#zP)nVAXbSXLGmpz2znJOhg$tB|6hBb#twBD+$dh*9Ijg&fLG8xM*G zUHqV8oK)1r$t5N(At|M*rmmr>WnyY(ZeeNV?BeR??&0Yb91<+v*#~fzWVs-^OvvRzW@073*;|G24;x2;66k1mmttz zOu#r`VF&q(k*OSrnFU!`6%E;h90S=C3x$=88aYIqCNA7~kW<+>=!0ld(M2vX6_bam zA3)QXZ=a$|42lsi8u716%ZO*Q(Po(B8{yRT&$!+63+0XrDf+s7VlPMf=6+TCT7G){@` zC@FllcxA-Ra@hk>e`ov=e5B7AT`}u%RC~SFkR6967i%tinJ literal 0 HcmV?d00001 diff --git a/Examples/images/tiny_png.h b/Examples/images/tiny_png.h new file mode 100644 index 00000000..b8606fa5 --- /dev/null +++ b/Examples/images/tiny_png.h @@ -0,0 +1,48 @@ +unsigned char tiny_png_png[] = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0f, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x83, 0xf5, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, + 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, + 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, + 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, + 0x00, 0x01, 0xa8, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4f, 0x63, 0x7c, 0xff, + 0x64, 0x03, 0x03, 0xcd, 0x00, 0x13, 0x94, 0xa6, 0x0d, 0x00, 0xbb, 0xfd, + 0xf1, 0xfe, 0x7f, 0xa1, 0x5b, 0xa1, 0x02, 0x50, 0x20, 0xc5, 0xb8, 0xba, + 0x88, 0x51, 0x96, 0x81, 0xe1, 0xfb, 0x85, 0x98, 0x6b, 0x67, 0x18, 0xa4, + 0x42, 0x96, 0x48, 0x0a, 0x40, 0x65, 0x40, 0x22, 0xef, 0xd3, 0x8c, 0x1d, + 0xed, 0xa0, 0x7c, 0xce, 0x89, 0x29, 0xc2, 0xfb, 0xa1, 0x6c, 0x06, 0xc5, + 0xe0, 0x17, 0xfd, 0x9e, 0x7f, 0xa0, 0x1c, 0x06, 0x06, 0x16, 0x28, 0xcd, + 0x60, 0xcc, 0x78, 0x22, 0x92, 0x11, 0xca, 0x66, 0x60, 0x38, 0xb2, 0xfc, + 0x5f, 0x68, 0x1f, 0x03, 0xd0, 0x02, 0x11, 0x10, 0xef, 0xfa, 0xb3, 0xf3, + 0x87, 0x24, 0xe1, 0xc6, 0x21, 0x81, 0x17, 0x7c, 0x85, 0x35, 0x7c, 0x0a, + 0xd9, 0x4f, 0x36, 0x18, 0x42, 0xf8, 0x40, 0x9b, 0x24, 0x0a, 0x19, 0x10, + 0x16, 0xe0, 0x08, 0x19, 0x1b, 0x17, 0x46, 0x8d, 0x67, 0x0c, 0x0f, 0x21, + 0x1c, 0xe5, 0x7c, 0xa9, 0xf7, 0xb3, 0x9e, 0x7f, 0x80, 0x70, 0x90, 0x00, + 0xcb, 0xba, 0x99, 0x7c, 0x0c, 0xc1, 0x2f, 0xf2, 0xa1, 0x46, 0x03, 0xc1, + 0xf7, 0xfc, 0x96, 0x4f, 0x0c, 0x6b, 0xf9, 0x4e, 0x41, 0xb9, 0xc4, 0x85, + 0xbb, 0x82, 0xa4, 0x93, 0xdb, 0xfb, 0xf3, 0x87, 0xa0, 0x3c, 0x18, 0x78, + 0xc1, 0x75, 0xf8, 0xf1, 0xb7, 0x48, 0xa4, 0x70, 0x00, 0x01, 0x89, 0x4f, + 0xfd, 0x73, 0xde, 0x99, 0x41, 0x39, 0xb8, 0x4c, 0x3f, 0xb2, 0xe7, 0xff, + 0x0d, 0x63, 0x06, 0x1b, 0x28, 0x8f, 0x81, 0x41, 0x20, 0x4e, 0x8a, 0xa1, + 0xf0, 0x1e, 0xd4, 0x2f, 0x50, 0xf0, 0x9c, 0xe5, 0xbe, 0xec, 0x1f, 0x19, + 0x28, 0x07, 0x3b, 0x80, 0x87, 0xfb, 0xd9, 0xff, 0x16, 0x67, 0xff, 0x43, + 0xd9, 0x40, 0x00, 0x8e, 0x55, 0x28, 0x1b, 0x0c, 0x04, 0x0c, 0xf3, 0x9f, + 0xed, 0x5b, 0xf4, 0x5d, 0x3e, 0x0e, 0xca, 0x47, 0x07, 0xa7, 0xe6, 0xc8, + 0xb4, 0x9d, 0x80, 0x30, 0xff, 0xc4, 0xb5, 0xbc, 0x08, 0x92, 0x00, 0xb1, + 0x70, 0xc4, 0x2a, 0x16, 0x20, 0x10, 0xa7, 0xa8, 0x14, 0xf3, 0xfc, 0x61, + 0x9c, 0x24, 0x94, 0xcf, 0x20, 0xf9, 0x47, 0xf1, 0x31, 0xcb, 0x13, 0xa0, + 0x43, 0xc0, 0x3c, 0xb3, 0x94, 0x27, 0x1b, 0x52, 0x80, 0x34, 0x30, 0x62, + 0xf9, 0xc1, 0x02, 0x20, 0x40, 0x4a, 0x7a, 0xe7, 0x34, 0x48, 0x63, 0xd8, + 0xdd, 0x00, 0x8f, 0x5d, 0x89, 0x6f, 0xb6, 0xb2, 0x5c, 0xcb, 0xb7, 0xc3, + 0xdd, 0x87, 0x05, 0x90, 0x96, 0x9b, 0xec, 0x24, 0x4d, 0xee, 0x3c, 0x3b, + 0x73, 0x1d, 0xca, 0xfb, 0x13, 0x94, 0x0e, 0x4c, 0x21, 0x12, 0x85, 0x08, + 0x0b, 0x58, 0xd6, 0x35, 0x22, 0xd2, 0x3e, 0x10, 0xe0, 0xb3, 0x19, 0x0b, + 0xe0, 0x34, 0x68, 0x95, 0xba, 0x17, 0xf4, 0x0c, 0xca, 0x03, 0xa7, 0x90, + 0x6f, 0xeb, 0x1a, 0x25, 0x02, 0xd6, 0x42, 0x05, 0x18, 0x2c, 0xde, 0x6e, + 0xa8, 0xff, 0x0e, 0x65, 0x43, 0xf3, 0x2a, 0xcd, 0x00, 0x69, 0x21, 0x43, + 0x2a, 0xa0, 0xa5, 0xe9, 0x0c, 0x0c, 0x00, 0xe4, 0x3f, 0x87, 0xe1, 0xb6, + 0x42, 0x8a, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, + 0x42, 0x60, 0x82 +}; +unsigned int tiny_png_png_len = 531; diff --git a/Examples/images/tiny_png.png b/Examples/images/tiny_png.png new file mode 100644 index 0000000000000000000000000000000000000000..c36187395030b6d42ce4befb83ddf549af089093 GIT binary patch literal 531 zcmV+u0_^>XP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0jNntK~y+TV|@Q) z0|U(f6O^V60K5J1{(qrcp#o4K#kjhNQI>(>`-PZmXBebHmPiUfWk4eD(~KSMpnT4W zDZ=}qYzD>P7yX`ppd1DU7AVbN%y=S_5z1y@II{d-XdeU60ud1J`m^(gB;m#(ffsy* zHGB%$Pc|4r_&}Q^3K2 zkAXoTPKu%M9@H;T@SNqo>>n8@2Rnd=``P~%Xa7UlKmd+aC>sm}4D+AuTl8H%4$4Od zr{>7Coq#a^#I?K#k^r%A#3~jb5T~dV^Za3)B$Uq}`A6|F%M+kOz&x{*CmT|LG%#ZM z!2%#aN_yusNMqRD0FPaXZ?>{r%eTYbhXs(9o9rY_?mRnl9m@L?lnzWG5`_p0Sk^Tn z(moL2vl$EEG-#E&7xWCu1E-L0>l!5j)y3;^Um VhvBwDip>B3002ovPDHLkV1n#8+-d*- literal 0 HcmV?d00001 diff --git a/OpenXLSX/headers/XLDocument.hpp b/OpenXLSX/headers/XLDocument.hpp index 7503d06d..021b110e 100644 --- a/OpenXLSX/headers/XLDocument.hpp +++ b/OpenXLSX/headers/XLDocument.hpp @@ -389,6 +389,20 @@ namespace OpenXLSX * @return true if successful, false otherwise */ bool addRelationshipsFile(const std::string& relsFilename, const std::string& relsXml); + + /** + * @brief Check if a relationships file exists in the archive + * @param relsFilename The filename for the relationships file in the archive + * @return true if the file exists, false otherwise + */ + bool hasRelationshipsFile(const std::string& relsFilename) const; + + /** + * @brief Read the content of a relationships file from the archive + * @param relsFilename The filename for the relationships file in the archive + * @return The content of the file, or empty string if not found + */ + std::string readRelationshipsFile(const std::string& relsFilename); /** * @brief @@ -458,6 +472,11 @@ namespace OpenXLSX */ const XLXmlData* getXmlData(const std::string& path, bool doNotThrow = false) const; + /** + * @brief Load all XML files from the archive into m_data list + */ + void loadAllXmlFilesFromArchive(); + /** * @brief * @param path diff --git a/OpenXLSX/headers/XLImage.hpp b/OpenXLSX/headers/XLImage.hpp index b64bf162..d2206109 100644 --- a/OpenXLSX/headers/XLImage.hpp +++ b/OpenXLSX/headers/XLImage.hpp @@ -194,9 +194,9 @@ namespace OpenXLSX /** * @brief Excel cell vertical spacing in EMUs (includes cell height + divider) - * This is approximately 200000 EMUs (cell height × 26/25) + * This is approximately 198000 EMUs (cell height × 26/25) */ - constexpr uint32_t EXCEL_CELL_Y_SPACING_EMUS = 200000; + constexpr uint32_t EXCEL_CELL_Y_SPACING_EMUS = 198000; /** * @brief Standard EMU-to-pixel conversion ratio (approximately) diff --git a/OpenXLSX/headers/XLSheet.hpp b/OpenXLSX/headers/XLSheet.hpp index 5b47030d..83c5b688 100644 --- a/OpenXLSX/headers/XLSheet.hpp +++ b/OpenXLSX/headers/XLSheet.hpp @@ -1258,6 +1258,12 @@ namespace OpenXLSX * @return Reference to the DrawingML object */ XLDrawingML& drawingML(); + + /** + * @brief Get the DrawingML object for this worksheet (const version) + * @return Reference to the DrawingML object + */ + const XLDrawingML& drawingML() const; /** * @brief fetch a reference to the worksheet comments @@ -1433,6 +1439,13 @@ namespace OpenXLSX * @param drawingFilename The drawing filename */ void createDrawingRelationshipsFile(const std::string& drawingFilename); + + /** + * @brief Add a relationship for a new image to the existing relationships file + * @param image The image to add a relationship for + * @param relationshipId The relationship ID to use + */ + void addImageRelationship(const XLImage& image, const std::string& relationshipId); /** * @brief fetch the # number from the xml path xl/worksheets/sheet#.xml diff --git a/OpenXLSX/sources/XLDocument.cpp b/OpenXLSX/sources/XLDocument.cpp index b0d23154..9b49fa3f 100644 --- a/OpenXLSX/sources/XLDocument.cpp +++ b/OpenXLSX/sources/XLDocument.cpp @@ -621,6 +621,9 @@ void XLDocument::open(const std::string& fileName) m_sharedStrings = XLSharedStrings(getXmlData("xl/sharedStrings.xml"), &m_sharedStringCache); m_styles = XLStyles(getXmlData("xl/styles.xml"), m_suppressWarnings); // 2024-10-14: forward supress warnings setting to XLStyles + + // ===== Load all remaining XML files from the archive (including drawing files) + loadAllXmlFilesFromArchive(); } namespace { @@ -1273,6 +1276,29 @@ bool XLDocument::addRelationshipsFile(const std::string& relsFilename, const std } } +/** + * @details Check if a relationships file exists in the archive + */ +bool XLDocument::hasRelationshipsFile(const std::string& relsFilename) const +{ + return m_archive.hasEntry(relsFilename); +} + +/** + * @details Read the content of a relationships file from the archive + */ +std::string XLDocument::readRelationshipsFile(const std::string& relsFilename) +{ + try { + if (m_archive.hasEntry(relsFilename)) { + return m_archive.getEntry(relsFilename); + } + } catch (const std::exception&) { + // Return empty string if there's an error + } + return ""; +} + /** * @details return value defaults to true, false only where the XLCommandType implements it */ @@ -1594,6 +1620,66 @@ std::string XLDocument::extractXmlFromArchive(const std::string& path) return (m_archive.hasEntry(path) ? m_archive.getEntry(path) : ""); } +/** + * @details Load all XML files from the archive into m_data list + */ +void XLDocument::loadAllXmlFilesFromArchive() +{ + // Get all content items from the content types + auto contentItems = m_contentTypes.getContentItems(); + + for (const auto& item : contentItems) { + std::string path = item.path(); + + // Remove leading slash if present (standardize path format) + if (path.length() > 0 && path[0] == '/') { + path = path.substr(1); + } + + // Skip if already loaded + if (hasXmlData(path)) { + continue; + } + + // Load XML files and media files + bool shouldLoad = false; + XLContentType contentType = item.type(); + + if (path.length() >= 4 && path.substr(path.length() - 4) == ".xml") { + // Load drawing files and other safe XML files + switch (contentType) { + case XLContentType::Drawing: + case XLContentType::Chart: + case XLContentType::Comments: + case XLContentType::Table: + case XLContentType::VMLDrawing: + shouldLoad = true; + break; + default: + // Also load any XML file that starts with xl/drawings/, xl/charts/, or is a .rels file + if (path.find("xl/drawings/") == 0 || path.find("xl/charts/") == 0 || + path.find("_rels/") != std::string::npos || path.find(".rels") != std::string::npos) { + shouldLoad = true; + // Set correct content type for relationship files + if (path.find(".rels") != std::string::npos) { + contentType = XLContentType::Relationships; + } + } + break; + } + } else { + // Don't load media files (images) into m_data - they should remain as binary files in the archive + // Image files will be preserved automatically by the archive when saving + shouldLoad = false; + } + + if (shouldLoad) { + // Add to m_data list + m_data.emplace_back(this, path, "", contentType); + } + } +} + /** * @details */ diff --git a/OpenXLSX/sources/XLDrawingML.cpp b/OpenXLSX/sources/XLDrawingML.cpp index e287abbf..66448678 100644 --- a/OpenXLSX/sources/XLDrawingML.cpp +++ b/OpenXLSX/sources/XLDrawingML.cpp @@ -1,315 +1,321 @@ -/* - * XLDrawingML.cpp - * - * DrawingML (Drawing Markup Language) implementation for OpenXLSX - */ - -// ===== External Includes ===== // -#include -#include -#include - -// ===== OpenXLSX Includes ===== // -#include "XLDrawingML.hpp" -#include "XLImage.hpp" -#include "XLXmlData.hpp" -#include "utilities/XLUtilities.hpp" - -namespace OpenXLSX -{ - // ===== Constructors & Destructors ===== // - XLDrawingML::XLDrawingML(XLXmlData* xmlData) : XLXmlFile(xmlData) - { - } - - // ===== Public Methods ===== // - void XLDrawingML::addImage(const XLImage& image, uint32_t row, uint16_t column, const std::string& relationshipId) - { - // Get the root element - XMLNode rootNode = xmlDocument().document_element(); - - // Create a oneCellAnchor element for single-cell images - XMLNode anchor = rootNode.append_child("xdr:oneCellAnchor"); - - // Create the 'from' element - XMLNode from = anchor.append_child("xdr:from"); - from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column - 1).c_str()); - from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value("0"); - from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row - 1).c_str()); - from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); - - // Create the 'ext' element - this is crucial for oneCellAnchor! - XMLNode ext = anchor.append_child("xdr:ext"); - ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); - ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); - - // Create the picture element - XMLNode pic = anchor.append_child("xdr:pic"); - - // Create non-visual picture properties - XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); - XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); - cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); // Start from 2 - cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); - - // Add extension list with creation ID for better compatibility - // XMLNode cNvPrExtLst = cNvPr.append_child("a:extLst"); - // XMLNode cNvPrExt = cNvPrExtLst.append_child("a:ext"); - // cNvPrExt.append_attribute("uri").set_value("{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}"); - // XMLNode creationId = cNvPrExt.append_child("a16:creationId"); - // std::string creationIdValue = "{00000000-0008-0000-0000-00000" + std::to_string(imageCount() + 1) + "00000}"; - // creationId.append_attribute("id").set_value(creationIdValue.c_str()); - - XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); - XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); - picLocks.append_attribute("noChangeAspect").set_value("1"); - picLocks.append_attribute("noChangeArrowheads").set_value("1"); - - // Create blip fill - XMLNode blipFill = pic.append_child("xdr:blipFill"); - XMLNode blip = blipFill.append_child("a:blip"); - blip.append_attribute("r:embed").set_value(relationshipId.c_str()); - - // Add extension list for better compatibility - // XMLNode extLst = blip.append_child("a:extLst"); - // XMLNode blipExt = extLst.append_child("a:ext"); - // blipExt.append_attribute("uri").set_value("{28A0092B-C50C-407E-A947-70E740481C1C}"); - // XMLNode useLocalDpi = blipExt.append_child("a14:useLocalDpi"); - // useLocalDpi.append_attribute("val").set_value("0"); - - XMLNode srcRect = blipFill.append_child("a:srcRect"); - XMLNode stretch = blipFill.append_child("a:stretch"); - stretch.append_child("a:fillRect"); - - // Create shape properties - XMLNode spPr = pic.append_child("xdr:spPr"); - spPr.append_attribute("bwMode").set_value("auto"); - - XMLNode xfrm = spPr.append_child("a:xfrm"); - XMLNode off = xfrm.append_child("a:off"); - off.append_attribute("x").set_value("0"); - off.append_attribute("y").set_value("0"); - - // Calculate image size that preserves aspect ratio within the bounding box - uint32_t imageWidth = image.displayWidth(); - uint32_t imageHeight = image.displayHeight(); - - // Get the image's natural dimensions - uint32_t naturalWidth = image.widthPixels(); - uint32_t naturalHeight = image.heightPixels(); - - // Calculate scaling factor to fit within bounding box while preserving aspect ratio - double scaleX = static_cast(imageWidth) / naturalWidth; - double scaleY = static_cast(imageHeight) / naturalHeight; - double scale = std::min(scaleX, scaleY); // Use smaller scale to fit within box - - // Calculate actual image size (preserving aspect ratio) - uint32_t actualWidth = static_cast(naturalWidth * scale); - uint32_t actualHeight = static_cast(naturalHeight * scale); - - XMLNode xfrmExt = xfrm.append_child("a:ext"); - xfrmExt.append_attribute("cx").set_value(std::to_string(actualWidth).c_str()); - xfrmExt.append_attribute("cy").set_value(std::to_string(actualHeight).c_str()); - - XMLNode prstGeom = spPr.append_child("a:prstGeom"); - prstGeom.append_attribute("prst").set_value("rect"); - prstGeom.append_child("a:avLst"); - - spPr.append_child("a:noFill"); - - // Create client data - simple empty element as per Google example - anchor.append_child("xdr:clientData"); - } - - size_t XLDrawingML::imageCount() const - { - XMLNode rootNode = xmlDocument().document_element(); - size_t count = 0; - for (XMLNode child : rootNode.children("xdr:oneCellAnchor")) { - count++; - } - for (XMLNode child : rootNode.children("xdr:twoCellAnchor")) { - count++; - } - return count; - } - - // ===== Private Methods ===== // - uint32_t XLDrawingML::emusToExcelUnits(uint32_t emus) const - { - // Convert EMUs to Excel units (1 EMU = 1/9525 Excel units) - return emus / 9525; - } - - uint32_t XLDrawingML::pointsToEmus(double points) const - { - // Convert points to EMUs (1 point = 12700 EMUs) - return static_cast(points * 12700); - } - - uint32_t XLDrawingML::calculateCellPosition(uint32_t row, uint16_t column) const - { - // Calculate cell position in Excel units - // This is a simplified calculation - Excel uses more complex formulas - return (row - 1) * 15 + (column - 1) * 64; - } - - /** - * @details Add an image to the drawing with precise positioning - */ - void XLDrawingML::addImageWithOffset(const XLImage& image, uint32_t row, uint16_t column, - const std::string& relationshipId, - int32_t rowOffset, int32_t colOffset) - { - // Get the root element - XMLNode rootNode = xmlDocument().document_element(); - - // Create a twoCellAnchor element (but we'll use it as oneCell with offset) - XMLNode anchor = rootNode.append_child("xdr:twoCellAnchor"); - anchor.append_attribute("editAs").set_value("oneCell"); - - // Create the 'from' element with offset - XMLNode from = anchor.append_child("xdr:from"); - from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column - 1).c_str()); - from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(colOffset).c_str()); - from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row - 1).c_str()); - from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(rowOffset).c_str()); - - // Create the 'to' element (same as from for oneCell anchoring) - XMLNode to = anchor.append_child("xdr:to"); - to.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column).c_str()); - to.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value("0"); - to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row).c_str()); - to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); - - // Create the 'ext' element (required for twoCellAnchor) - XMLNode ext = anchor.append_child("xdr:ext"); - ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); - ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); - - // Create the picture element (same as original addImage method) - XMLNode pic = anchor.append_child("xdr:pic"); - - // Create non-visual picture properties - XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); - XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); - cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); - cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); - - XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); - XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); - picLocks.append_attribute("noChangeAspect").set_value("1"); - picLocks.append_attribute("noChangeArrowheads").set_value("1"); - - // Create blip fill - XMLNode blipFill = pic.append_child("xdr:blipFill"); - XMLNode blip = blipFill.append_child("a:blip"); - blip.append_attribute("r:embed").set_value(relationshipId.c_str()); - - XMLNode srcRect = blipFill.append_child("a:srcRect"); - XMLNode stretch = blipFill.append_child("a:stretch"); - stretch.append_child("a:fillRect"); - - // Create shape properties - XMLNode spPr = pic.append_child("xdr:spPr"); - XMLNode xfrm = spPr.append_child("a:xfrm"); - XMLNode off = xfrm.append_child("a:off"); - off.append_attribute("x").set_value("0"); - off.append_attribute("y").set_value("0"); - - XMLNode xfrmExt = xfrm.append_child("a:ext"); - xfrmExt.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); - xfrmExt.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); - - XMLNode prstGeom = spPr.append_child("a:prstGeom"); - prstGeom.append_attribute("prst").set_value("rect"); - prstGeom.append_child("a:avLst"); - - // Use noFill to match other methods (no borders) - spPr.append_child("a:noFill"); - - // Add clientData element (required for Excel compatibility) - anchor.append_child("xdr:clientData"); - } - - /** - * @details Add an image to the drawing with two-cell anchoring - */ - void XLDrawingML::addImageTwoCellAnchor(const XLImage& image, - uint32_t fromRow, uint16_t fromCol, - uint32_t toRow, uint16_t toCol, - const std::string& relationshipId, - int32_t fromRowOffset, int32_t fromColOffset, - int32_t toRowOffset, int32_t toColOffset) - { - // Get the root element - XMLNode rootNode = xmlDocument().document_element(); - - // Create a twoCellAnchor element - XMLNode anchor = rootNode.append_child("xdr:twoCellAnchor"); - anchor.append_attribute("editAs").set_value("twoCell"); - - // Create the 'from' element - XMLNode from = anchor.append_child("xdr:from"); - from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(fromCol - 1).c_str()); - from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(fromColOffset).c_str()); - from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(fromRow - 1).c_str()); - from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(fromRowOffset).c_str()); - - // Create the 'to' element - XMLNode to = anchor.append_child("xdr:to"); - to.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(toCol).c_str()); - to.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(toColOffset).c_str()); - to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(toRow).c_str()); - to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(toRowOffset).c_str()); - - // Create the 'ext' element (required for twoCellAnchor) - XMLNode ext = anchor.append_child("xdr:ext"); - ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); - ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); - - // Create the picture element (same as original addImage method) - XMLNode pic = anchor.append_child("xdr:pic"); - - // Create non-visual picture properties - XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); - XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); - cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); - cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); - - XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); - XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); - picLocks.append_attribute("noChangeAspect").set_value("1"); - picLocks.append_attribute("noChangeArrowheads").set_value("1"); - - // Create blip fill - XMLNode blipFill = pic.append_child("xdr:blipFill"); - XMLNode blip = blipFill.append_child("a:blip"); - blip.append_attribute("r:embed").set_value(relationshipId.c_str()); - - XMLNode srcRect = blipFill.append_child("a:srcRect"); - XMLNode stretch = blipFill.append_child("a:stretch"); - stretch.append_child("a:fillRect"); - - // Create shape properties - XMLNode spPr = pic.append_child("xdr:spPr"); - XMLNode xfrm = spPr.append_child("a:xfrm"); - XMLNode off = xfrm.append_child("a:off"); - off.append_attribute("x").set_value("0"); - off.append_attribute("y").set_value("0"); - - XMLNode xfrmExt = xfrm.append_child("a:ext"); - xfrmExt.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); - xfrmExt.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); - - XMLNode prstGeom = spPr.append_child("a:prstGeom"); - prstGeom.append_attribute("prst").set_value("rect"); - prstGeom.append_child("a:avLst"); - - // Use noFill to match oneCellAnchor behavior (no borders) - spPr.append_child("a:noFill"); - - // Add clientData element (required for Excel compatibility) - anchor.append_child("xdr:clientData"); - } -} +/* + * XLDrawingML.cpp + * + * DrawingML (Drawing Markup Language) implementation for OpenXLSX + */ + +// ===== External Includes ===== // +#include +#include +#include + +// ===== OpenXLSX Includes ===== // +#include "XLDrawingML.hpp" +#include "XLImage.hpp" +#include "XLXmlData.hpp" +#include "utilities/XLUtilities.hpp" + +namespace OpenXLSX +{ + // ===== Constructors & Destructors ===== // + XLDrawingML::XLDrawingML(XLXmlData* xmlData) : XLXmlFile(xmlData) + { + } + + // ===== Public Methods ===== // + void XLDrawingML::addImage(const XLImage& image, uint32_t row, uint16_t column, const std::string& relationshipId) + { + // Get the root element + XMLNode rootNode = xmlDocument().document_element(); + + // Create a oneCellAnchor element for single-cell images + XMLNode anchor = rootNode.append_child("xdr:oneCellAnchor"); + + // Create the 'from' element + XMLNode from = anchor.append_child("xdr:from"); + from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column - 1).c_str()); + from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value("0"); + from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row - 1).c_str()); + from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); + + // Create the 'ext' element - this is crucial for oneCellAnchor! + XMLNode ext = anchor.append_child("xdr:ext"); + ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); + ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); + + // Create the picture element + XMLNode pic = anchor.append_child("xdr:pic"); + + // Create non-visual picture properties + XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); + XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); + cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); // Start from 2 + cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); + + // Add extension list with creation ID for better compatibility + // XMLNode cNvPrExtLst = cNvPr.append_child("a:extLst"); + // XMLNode cNvPrExt = cNvPrExtLst.append_child("a:ext"); + // cNvPrExt.append_attribute("uri").set_value("{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}"); + // XMLNode creationId = cNvPrExt.append_child("a16:creationId"); + // std::string creationIdValue = "{00000000-0008-0000-0000-00000" + std::to_string(imageCount() + 1) + "00000}"; + // creationId.append_attribute("id").set_value(creationIdValue.c_str()); + + XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); + XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); + picLocks.append_attribute("noChangeAspect").set_value("1"); + picLocks.append_attribute("noChangeArrowheads").set_value("1"); + + // Create blip fill + XMLNode blipFill = pic.append_child("xdr:blipFill"); + XMLNode blip = blipFill.append_child("a:blip"); + blip.append_attribute("r:embed").set_value(relationshipId.c_str()); + + // Add extension list for better compatibility + // XMLNode extLst = blip.append_child("a:extLst"); + // XMLNode blipExt = extLst.append_child("a:ext"); + // blipExt.append_attribute("uri").set_value("{28A0092B-C50C-407E-A947-70E740481C1C}"); + // XMLNode useLocalDpi = blipExt.append_child("a14:useLocalDpi"); + // useLocalDpi.append_attribute("val").set_value("0"); + + XMLNode srcRect = blipFill.append_child("a:srcRect"); + XMLNode stretch = blipFill.append_child("a:stretch"); + stretch.append_child("a:fillRect"); + + // Create shape properties + XMLNode spPr = pic.append_child("xdr:spPr"); + spPr.append_attribute("bwMode").set_value("auto"); + + XMLNode xfrm = spPr.append_child("a:xfrm"); + XMLNode off = xfrm.append_child("a:off"); + off.append_attribute("x").set_value("0"); + off.append_attribute("y").set_value("0"); + + // Calculate image size that preserves aspect ratio within the bounding box + uint32_t imageWidth = image.displayWidth(); + uint32_t imageHeight = image.displayHeight(); + + // Get the image's natural dimensions + uint32_t naturalWidth = image.widthPixels(); + uint32_t naturalHeight = image.heightPixels(); + + // Calculate scaling factor to fit within bounding box while preserving aspect ratio + double scaleX = static_cast(imageWidth) / naturalWidth; + double scaleY = static_cast(imageHeight) / naturalHeight; + double scale = std::min(scaleX, scaleY); // Use smaller scale to fit within box + + // Calculate actual image size (preserving aspect ratio) + uint32_t actualWidth = static_cast(naturalWidth * scale); + uint32_t actualHeight = static_cast(naturalHeight * scale); + + XMLNode xfrmExt = xfrm.append_child("a:ext"); + xfrmExt.append_attribute("cx").set_value(std::to_string(actualWidth).c_str()); + xfrmExt.append_attribute("cy").set_value(std::to_string(actualHeight).c_str()); + + XMLNode prstGeom = spPr.append_child("a:prstGeom"); + prstGeom.append_attribute("prst").set_value("rect"); + prstGeom.append_child("a:avLst"); + + spPr.append_child("a:noFill"); + + // Create client data - simple empty element as per Google example + anchor.append_child("xdr:clientData"); + } + + size_t XLDrawingML::imageCount() const + { + if (!valid()) { + return 0; + } + + XMLNode rootNode = xmlDocument().document_element(); + size_t count = 0; + + for (XMLNode child : rootNode.children("xdr:oneCellAnchor")) { + count++; + } + for (XMLNode child : rootNode.children("xdr:twoCellAnchor")) { + count++; + } + + return count; + } + + // ===== Private Methods ===== // + uint32_t XLDrawingML::emusToExcelUnits(uint32_t emus) const + { + // Convert EMUs to Excel units (1 EMU = 1/9525 Excel units) + return emus / 9525; + } + + uint32_t XLDrawingML::pointsToEmus(double points) const + { + // Convert points to EMUs (1 point = 12700 EMUs) + return static_cast(points * 12700); + } + + uint32_t XLDrawingML::calculateCellPosition(uint32_t row, uint16_t column) const + { + // Calculate cell position in Excel units + // This is a simplified calculation - Excel uses more complex formulas + return (row - 1) * 15 + (column - 1) * 64; + } + + /** + * @details Add an image to the drawing with precise positioning + */ + void XLDrawingML::addImageWithOffset(const XLImage& image, uint32_t row, uint16_t column, + const std::string& relationshipId, + int32_t rowOffset, int32_t colOffset) + { + // Get the root element + XMLNode rootNode = xmlDocument().document_element(); + + // Create a twoCellAnchor element with proper oneCell editing (Excel standard) + XMLNode anchor = rootNode.append_child("xdr:twoCellAnchor"); + anchor.append_attribute("editAs").set_value("oneCell"); + + // Create the 'from' element with offset + XMLNode from = anchor.append_child("xdr:from"); + from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column - 1).c_str()); + from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(colOffset).c_str()); + from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row - 1).c_str()); + from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(rowOffset).c_str()); + + // Create the 'to' element (same as from for oneCell anchoring) + XMLNode to = anchor.append_child("xdr:to"); + to.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column).c_str()); + to.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value("0"); + to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row).c_str()); + to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); + + // Create the 'ext' element (required for twoCellAnchor) + XMLNode ext = anchor.append_child("xdr:ext"); + ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); + ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); + + // Create the picture element (same as original addImage method) + XMLNode pic = anchor.append_child("xdr:pic"); + + // Create non-visual picture properties + XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); + XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); + cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); + cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); + + XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); + XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); + picLocks.append_attribute("noChangeAspect").set_value("1"); + picLocks.append_attribute("noChangeArrowheads").set_value("1"); + + // Create blip fill + XMLNode blipFill = pic.append_child("xdr:blipFill"); + XMLNode blip = blipFill.append_child("a:blip"); + blip.append_attribute("r:embed").set_value(relationshipId.c_str()); + + XMLNode srcRect = blipFill.append_child("a:srcRect"); + XMLNode stretch = blipFill.append_child("a:stretch"); + stretch.append_child("a:fillRect"); + + // Create shape properties + XMLNode spPr = pic.append_child("xdr:spPr"); + XMLNode xfrm = spPr.append_child("a:xfrm"); + XMLNode off = xfrm.append_child("a:off"); + off.append_attribute("x").set_value("0"); + off.append_attribute("y").set_value("0"); + + XMLNode xfrmExt = xfrm.append_child("a:ext"); + xfrmExt.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); + xfrmExt.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); + + XMLNode prstGeom = spPr.append_child("a:prstGeom"); + prstGeom.append_attribute("prst").set_value("rect"); + prstGeom.append_child("a:avLst"); + + // Use noFill to match other methods (no borders) + spPr.append_child("a:noFill"); + + // Add clientData element (required for Excel compatibility) + anchor.append_child("xdr:clientData"); + } + + /** + * @details Add an image to the drawing with two-cell anchoring + */ + void XLDrawingML::addImageTwoCellAnchor(const XLImage& image, + uint32_t fromRow, uint16_t fromCol, + uint32_t toRow, uint16_t toCol, + const std::string& relationshipId, + int32_t fromRowOffset, int32_t fromColOffset, + int32_t toRowOffset, int32_t toColOffset) + { + // Get the root element + XMLNode rootNode = xmlDocument().document_element(); + + // Create a twoCellAnchor element + XMLNode anchor = rootNode.append_child("xdr:twoCellAnchor"); + anchor.append_attribute("editAs").set_value("oneCell"); + + // Create the 'from' element + XMLNode from = anchor.append_child("xdr:from"); + from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(fromCol - 1).c_str()); + from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(fromColOffset).c_str()); + from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(fromRow - 1).c_str()); + from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(fromRowOffset).c_str()); + + // Create the 'to' element + XMLNode to = anchor.append_child("xdr:to"); + to.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(toCol).c_str()); + to.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(toColOffset).c_str()); + to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(toRow).c_str()); + to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(toRowOffset).c_str()); + + // Create the 'ext' element (required for twoCellAnchor) + XMLNode ext = anchor.append_child("xdr:ext"); + ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); + ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); + + // Create the picture element (same as original addImage method) + XMLNode pic = anchor.append_child("xdr:pic"); + + // Create non-visual picture properties + XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); + XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); + cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); + cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); + + XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); + XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); + picLocks.append_attribute("noChangeAspect").set_value("1"); + picLocks.append_attribute("noChangeArrowheads").set_value("1"); + + // Create blip fill + XMLNode blipFill = pic.append_child("xdr:blipFill"); + XMLNode blip = blipFill.append_child("a:blip"); + blip.append_attribute("r:embed").set_value(relationshipId.c_str()); + + XMLNode srcRect = blipFill.append_child("a:srcRect"); + XMLNode stretch = blipFill.append_child("a:stretch"); + stretch.append_child("a:fillRect"); + + // Create shape properties + XMLNode spPr = pic.append_child("xdr:spPr"); + XMLNode xfrm = spPr.append_child("a:xfrm"); + XMLNode off = xfrm.append_child("a:off"); + off.append_attribute("x").set_value("0"); + off.append_attribute("y").set_value("0"); + + XMLNode xfrmExt = xfrm.append_child("a:ext"); + xfrmExt.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); + xfrmExt.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); + + XMLNode prstGeom = spPr.append_child("a:prstGeom"); + prstGeom.append_attribute("prst").set_value("rect"); + prstGeom.append_child("a:avLst"); + + // Use noFill to match oneCellAnchor behavior (no borders) + spPr.append_child("a:noFill"); + + // Add clientData element (required for Excel compatibility) + anchor.append_child("xdr:clientData"); + } +} diff --git a/OpenXLSX/sources/XLImage.cpp b/OpenXLSX/sources/XLImage.cpp index 290ef4fa..15ddfc5e 100644 --- a/OpenXLSX/sources/XLImage.cpp +++ b/OpenXLSX/sources/XLImage.cpp @@ -79,9 +79,10 @@ namespace OpenXLSX */ bool XLImage::loadFromFile(const std::string& imagePath) { - // Generate a temporary ID for backward compatibility - std::string tempId = "temp_id"; - return loadFromFile(imagePath, tempId); + // Generate a proper sequential ID instead of temp_id + static int counter = 1; + std::string imageId = "img" + std::to_string(counter++); + return loadFromFile(imagePath, imageId); } /** @@ -132,9 +133,10 @@ namespace OpenXLSX */ bool XLImage::loadFromData(const std::vector& imageData, const std::string& mimeType) { - // Generate a temporary ID for backward compatibility - std::string tempId = "temp_id"; - return loadFromData(imageData, mimeType, tempId); + // Generate a proper sequential ID instead of temp_id + static int counter = 1; + std::string imageId = "img" + std::to_string(counter++); + return loadFromData(imageData, mimeType, imageId); } /** diff --git a/OpenXLSX/sources/XLSheet.cpp b/OpenXLSX/sources/XLSheet.cpp index 262729c3..536033ed 100644 --- a/OpenXLSX/sources/XLSheet.cpp +++ b/OpenXLSX/sources/XLSheet.cpp @@ -48,6 +48,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #include // std::isdigit (issue #330) #include // std::numeric_limits #include // std::multimap +#include // std::set #include // ===== OpenXLSX Includes ===== // @@ -1749,8 +1750,15 @@ XLDrawingML& XLWorksheet::drawingML() throw XLException("XLWorksheet::drawingML(): could not add element to worksheet XML"); appendAndSetAttribute(drawing, "r:id", drawingRelationship.id()); - // Create drawing relationships file - createDrawingRelationshipsFile(drawingFilename); + // Create drawing relationships file only if it doesn't exist + std::string drawingRelsFilename = drawingFilename.substr(0, drawingFilename.find_last_of('/') + 1) + + "_rels/" + + drawingFilename.substr(drawingFilename.find_last_of('/') + 1) + ".rels"; + + // Only create relationships file if it doesn't exist + if (!parentDoc().hasRelationshipsFile(drawingRelsFilename)) { + createDrawingRelationshipsFile(drawingFilename); + } } return m_drawingML; @@ -1837,7 +1845,10 @@ bool XLWorksheet::addImage(const XLImage& image, uint32_t row, uint16_t column) try { // Create a copy of the image and set its ID XLImage imageWithId = image; - imageWithId.setId(generateNextImageId()); + // Only generate a new ID if the image doesn't already have one + if (imageWithId.id().empty()) { + imageWithId.setId(generateNextImageId()); + } // Store the image in the vector m_images.push_back(imageWithId); @@ -1906,7 +1917,10 @@ bool XLWorksheet::addImageWithOffset(const XLImage& image, uint32_t row, uint16_ { // Create a copy of the image and set its ID XLImage imageWithId = image; - imageWithId.setId(generateNextImageId()); + // Only generate a new ID if the image doesn't already have one + if (imageWithId.id().empty()) { + imageWithId.setId(generateNextImageId()); + } // Store the image m_images.push_back(imageWithId); @@ -1940,7 +1954,10 @@ bool XLWorksheet::addImageTwoCellAnchor(const XLImage& image, { // Create a copy of the image and set its ID XLImage imageWithId = image; - imageWithId.setId(generateNextImageId()); + // Only generate a new ID if the image doesn't already have one + if (imageWithId.id().empty()) { + imageWithId.setId(generateNextImageId()); + } // Store the image m_images.push_back(imageWithId); @@ -1969,7 +1986,23 @@ bool XLWorksheet::addImageTwoCellAnchor(const XLImage& image, */ size_t XLWorksheet::imageCount() const { - return m_images.size(); + // For existing files, count images from DrawingML + // For newly created files, count from m_images vector + if (m_images.empty()) { + // Try to get count from existing DrawingML without creating it + try { + // Check if DrawingML already exists and is valid + if (m_drawingML.valid()) { + return m_drawingML.imageCount(); + } + return 0; + } catch (...) { + return 0; + } + } else { + // Use the vector count for newly added images + return m_images.size(); + } } /** @@ -1993,7 +2026,20 @@ std::string XLWorksheet::generateNextImageId() const */ std::string XLWorksheet::getRelationshipIdFromImageCount() const { - return "rId" + std::to_string(m_images.size()); + // For existing files, count from DrawingML; for new files, use m_images + if (m_images.empty()) { + // Try to get count from existing DrawingML + try { + if (m_drawingML.valid()) { + return "rId" + std::to_string(m_drawingML.imageCount() + 1); + } + } catch (...) { + // Fall through to default + } + return "rId1"; // Default for first image + } else { + return "rId" + std::to_string(m_images.size() + 1); + } } /** @@ -2004,10 +2050,8 @@ void XLWorksheet::addImageToDrawingML(const XLImage& image, uint32_t row, uint16 // Add image to DrawingML m_drawingML.addImage(image, row, column, relationshipId); - // Update the drawing relationships file to include all current images - uint16_t sheetXmlNo = sheetXmlNumber(); - std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; - createDrawingRelationshipsFile(drawingFilename); + // Add relationship for the new image + addImageRelationship(image, relationshipId); } /** @@ -2044,6 +2088,107 @@ void XLWorksheet::createDrawingRelationshipsFile(const std::string& drawingFilen } } +/** + * @details Add a relationship for a new image to the existing relationships file + */ +void XLWorksheet::addImageRelationship(const XLImage& image, const std::string& relationshipId) +{ + // Get the drawing filename for this worksheet + uint16_t sheetXmlNo = sheetXmlNumber(); + std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; + + // Create the relationships filename + std::string drawingRelsFilename = drawingFilename.substr(0, drawingFilename.find_last_of('/') + 1) + + "_rels/" + + drawingFilename.substr(drawingFilename.find_last_of('/') + 1) + ".rels"; + + std::string relsXml; + + // Try to read existing relationships file + if (parentDoc().hasRelationshipsFile(drawingRelsFilename)) { + // Read existing content and add new relationship + try { + // Get existing content from archive + std::string existingContent = parentDoc().readRelationshipsFile(drawingRelsFilename); + + // Parse existing XML + XMLDocument relsDoc; + relsDoc.load_string(existingContent.c_str()); + XMLNode relationshipsNode = relsDoc.document_element(); + + if (relationshipsNode) { + // Start building new XML + relsXml = "\n" + "\n"; + + // Find the highest existing relationship ID to avoid duplicates + int maxId = 0; + std::set existingIds; + + for (auto relNode : relationshipsNode.children("Relationship")) { + std::string relId = relNode.attribute("Id").value(); + std::string relType = relNode.attribute("Type").value(); + std::string relTarget = relNode.attribute("Target").value(); + + // Extract numeric part of ID + if (relId.length() > 3 && relId.substr(0, 3) == "rId") { + try { + int idNum = std::stoi(relId.substr(3)); + maxId = std::max(maxId, idNum); + } catch (...) { + // Ignore non-numeric IDs + } + } + + existingIds.insert(relId); + + relsXml += "\n"; + } + + // Generate a unique relationship ID + std::string uniqueRelId = relationshipId; + int idNum = maxId + 1; + while (existingIds.find(uniqueRelId) != existingIds.end()) { + uniqueRelId = "rId" + std::to_string(idNum); + idNum++; + } + + // Add new relationship with unique ID + std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); + relsXml += "\n"; + + relsXml += ""; + } + } catch (const std::exception&) { + // If parsing fails, fall back to creating new file + relsXml = "\n" + "\n" + "\n" + ""; + } + } else { + // Create new relationships file + std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); + relsXml = "\n" + "\n" + "\n" + ""; + } + + // Update the relationships file + if (!parentDoc().addRelationshipsFile(drawingRelsFilename, relsXml)) { + throw XLException("XLWorksheet::addImageRelationship(): could not update drawing relationships file"); + } +} + /** * @details Add an image to the DrawingML with precise positioning */ @@ -2054,10 +2199,8 @@ void XLWorksheet::addImageToDrawingMLWithOffset(const XLImage& image, uint32_t r // Add image to DrawingML m_drawingML.addImageWithOffset(image, row, column, relationshipId, rowOffset, colOffset); - // Update the drawing relationships file to include all current images - uint16_t sheetXmlNo = sheetXmlNumber(); - std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; - createDrawingRelationshipsFile(drawingFilename); + // Add relationship for the new image + addImageRelationship(image, relationshipId); } /** @@ -2074,10 +2217,8 @@ void XLWorksheet::addImageToDrawingMLTwoCellAnchor(const XLImage& image, m_drawingML.addImageTwoCellAnchor(image, fromRow, fromCol, toRow, toCol, relationshipId, fromRowOffset, fromColOffset, toRowOffset, toColOffset); - // Update the drawing relationships file to include all current images - uint16_t sheetXmlNo = sheetXmlNumber(); - std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; - createDrawingRelationshipsFile(drawingFilename); + // Add relationship for the new image + addImageRelationship(image, relationshipId); } /** From 107b3745b356b29cc2624241bd82f4c7149ab6b8 Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Thu, 9 Oct 2025 18:15:23 -0700 Subject: [PATCH 08/13] Embedded Image Support -- phase VII / query and remove embedded images --- Examples/Demo11.cpp | 269 ++++- OpenXLSX/headers/IZipArchive.hpp | 6 +- OpenXLSX/headers/XLDocument.hpp | 24 +- OpenXLSX/headers/XLImage.hpp | 86 ++ OpenXLSX/headers/XLSheet.hpp | 247 ++++- OpenXLSX/sources/XLDocument.cpp | 141 ++- OpenXLSX/sources/XLImage.cpp | 128 +++ OpenXLSX/sources/XLSheet.cpp | 249 ++++- OpenXLSX/sources/XLWorkbook.cpp | 36 +- OpenXLSX/sources/XLWorksheetImageQuery.cpp | 1030 ++++++++++++++++++++ 10 files changed, 2173 insertions(+), 43 deletions(-) create mode 100644 OpenXLSX/sources/XLWorksheetImageQuery.cpp diff --git a/Examples/Demo11.cpp b/Examples/Demo11.cpp index 72b44464..567c5b8d 100644 --- a/Examples/Demo11.cpp +++ b/Examples/Demo11.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "OpenXLSX.hpp" using namespace OpenXLSX; @@ -46,6 +47,39 @@ std::string findDirectory(const std::string& relativePath) } +/* + a. get image ID + b. get relationshiop ID + c. get anchor cell location(s) + d. get image width & height in pixels + e. get image displayed width and displayed height in EMUs + +Return image information in the following format +"img_id:ABCD r_id:EFGH oneCellAnchor:C44 330x492 px 1249900x1700000 emu" +*/ +std::string imageInfoStr( const XLImageInfo& imageInfo ){ + std::string result; + + // a. get image ID + std::string imageId = imageInfo.imageId; + result += "img_id:" + imageId; + + // b. get relationship ID + result += " r_id:" + imageInfo.relationshipId; + + // c. get anchor type + // d. get anchor cell location(s) + result += " " + imageInfo.anchorType + ":" + imageInfo.anchorCell; + + // e. get image width & height in pixels + result += " " + std::to_string(imageInfo.widthPixels) + "x" + std::to_string(imageInfo.heightPixels) + " px"; + + // f. get image displayed width and displayed height in EMUs + result += " " + std::to_string(imageInfo.displayWidthEMUs) + "x" + std::to_string(imageInfo.displayHeightEMUs) + " EMU"; + + return result; +} + // These contain the binary data from the tiny image files in the images directory @@ -672,7 +706,7 @@ int main() std::cout << "\nSaving workbook as '" << xlsxFileName << "'..." << std::endl; try { doc.save(); - std::cout << "Workbook saved successfully!" << std::endl; + std::cout << "Workbook saved successfully." << std::endl; } catch (const std::exception& e) { std::cout << "Error saving workbook: " << e.what() << std::endl; return 1; @@ -695,9 +729,24 @@ int main() try { readDoc.open(xlsxFileName); std::cout << " Successfully opened: " << xlsxFileName << std::endl; + + // Step 2: Compare original and read documents for debugging + std::cout << "\n2. Comparing original and read documents for debugging..." << std::endl; + + // Compare the original doc (that was saved) with the readDoc (that was loaded) + std::string diffMsg; + int result = doc.compare(readDoc, &diffMsg); - // Step 2: Search for embedded images in all worksheets - std::cout << "\n2. Searching for embedded images..." << std::endl; + if (result == 0) { + std::cout << " ✓ Documents are identical - no differences found" << std::endl; + } else { + std::cout << " ✗ Documents differ (result: " << result << ")" << std::endl; + std::cout << " Differences: " << diffMsg << std::endl; + } + + // Step 3: Search for embedded images in all worksheets + std::cout << "\n3. Searching for embedded images..." << std::endl; + int totalImagesFound = 0; std::vector worksheetsWithImages; @@ -717,6 +766,16 @@ int main() worksheetsWithImages.push_back(sheetName); std::cout << " Found " << imageCount << " image(s) in worksheet: " << sheetName << std::endl; totalImagesFound++; + + // Iterate through images and print their information + try { + auto imageInfos = sheet.getImageInfos(); + for (size_t i = 0; i < imageInfos.size(); ++i) { + std::cout << " " << (i + 1) << ". " << imageInfoStr(imageInfos[i]) << std::endl; + } + } catch (const std::exception& e) { + std::cout << " Error getting image info: " << e.what() << std::endl; + } } } else { std::cout << " Worksheet '" << sheetName << "' has no DrawingML" << std::endl; @@ -728,18 +787,64 @@ int main() std::cout << " Total worksheets with images: " << totalImagesFound << std::endl; - // Step 3: Add a new image to the "File Loading" worksheet (object B) - std::cout << "\n3. Adding new image to 'File Loading' worksheet..." << std::endl; + // Step 3b: Compare documents AFTER search operation (to test lazy initialization hypothesis) + std::cout << "\n3b. Comparing documents AFTER search operation..." << std::endl; + std::string diffMsgAfterSearch; + int resultAfterSearch = doc.compare(readDoc, &diffMsgAfterSearch); + + if (resultAfterSearch == 0) { + std::cout << " ✓ Documents still identical after search" << std::endl; + } else { + std::cout << " ✗ Documents differ after search (result: " << resultAfterSearch << ")" << std::endl; + std::cout << " Differences: " << diffMsgAfterSearch << std::endl; + } + + // Step 4: Adding and removing images in 'File Loading' worksheet + std::cout << "\n4. Adding and removing images in 'File Loading' worksheet..." << std::endl; + + std::string newImageId = ""; // Declare outside the if block for use in verification summary if (readDoc.workbook().worksheetExists("File Loading")) { OpenXLSX::XLWorksheet fileLoadingSheet = readDoc.workbook().worksheet("File Loading"); std::cout << " Found 'File Loading' worksheet" << std::endl; - // Create a new test image (using the same PNG data as before) + // First, let's see what images we currently have + std::cout << " Current images in worksheet:" << std::endl; + try { + const auto& currentImages = fileLoadingSheet.getImageInfos(); + for (size_t i = 0; i < currentImages.size(); ++i) { + std::cout << " " << (i + 1) << ". " << imageInfoStr(currentImages[i]) << std::endl; + } + } catch (const std::exception& e) { + std::cout << " Error getting current images: " << e.what() << std::endl; + } + + // Remove the third and fourth images (img3 and img4) + std::cout << "\n Removing third and fourth images (img3 and img4)..." << std::endl; + bool removedImg3 = fileLoadingSheet.removeImageByImageID("img3"); + bool removedImg4 = fileLoadingSheet.removeImageByImageID("img4"); + std::cout << " Removed img3: " << (removedImg3 ? "Success" : "Failed") << std::endl; + std::cout << " Removed img4: " << (removedImg4 ? "Success" : "Failed") << std::endl; + + // Set "REMOVED" text above the cells where images were removed + fileLoadingSheet.cell("B13").value() = (removedImg3) ? "REMOVED" : "FAILED TO REMOVE"; // Near img3 at A14 + fileLoadingSheet.cell("B19").value() = (removedImg4) ? "REMOVED" : "FAILED TO REMOVE"; // Near img4 at A20 + + + // Add a new image + std::cout << "\n Adding new image..." << std::endl; OpenXLSX::XLImage newImage; - std::string newImageId = fileLoadingSheet.generateNextImageId(); + newImageId = fileLoadingSheet.generateNextImageId(); std::cout << " Generated new image ID: " << newImageId << std::endl; + // Check if the generated ID already exists and create a unique one if needed + if (fileLoadingSheet.hasImageWithId(newImageId)) { + std::cout << " WARNING: Generated ID '" << newImageId << "' already exists, creating unique ID..." << std::endl; + // Create a unique ID by appending timestamp or counter + newImageId = "img_new_" + std::to_string(time(nullptr) % 10000); + std::cout << " Using unique ID: " << newImageId << std::endl; + } + // Use the same PNG data from our static constants if (newImage.loadFromData(pngData, "image/png", newImageId)) { std::cout << " Successfully loaded image data" << std::endl; @@ -766,18 +871,61 @@ int main() std::cout << " Successfully added new image to 'File Loading' worksheet" << std::endl; std::cout << " New image ID: " << newImageId << std::endl; std::cout << " Image positioned at: A38" << std::endl; + + // Verify the image was actually added by checking the registry + try { + const auto& testImages = fileLoadingSheet.getImageInfos(); + std::cout << " DEBUG: Registry now has " << testImages.size() << " images after addition" << std::endl; + bool foundNewImage = false; + for (const auto& img : testImages) { + if (img.imageId == newImageId) { + foundNewImage = true; + std::cout << " DEBUG: Found new image in registry: " << imageInfoStr(img) << std::endl; + break; + } + } + if (!foundNewImage) { + std::cout << " WARNING: New image not found in registry after addition!" << std::endl; + } + } catch (const std::exception& e) { + std::cout << " DEBUG: Error checking registry after addition: " << e.what() << std::endl; + } } else { std::cout << " Failed to add new image to worksheet" << std::endl; } } else { std::cout << " Failed to create new image from data" << std::endl; } + + // Show final state + std::cout << "\n Final images in worksheet:" << std::endl; + try { + const auto& finalImages = fileLoadingSheet.getImageInfos(); + for (size_t i = 0; i < finalImages.size(); ++i) { + std::cout << " " << (i + 1) << ". " << imageInfoStr(finalImages[i]) << std::endl; + } + std::cout << " Total images after modifications: " << finalImages.size() << std::endl; + } catch (const std::exception& e) { + std::cout << " Error getting final images: " << e.what() << std::endl; + } } else { std::cout << " 'File Loading' worksheet not found!" << std::endl; } - // Step 4: Write the modified workbook to a new file - std::cout << "\n4. Writing modified workbook to new file..." << std::endl; + // Step 4b: Compare documents AFTER adding and removing images + std::cout << "\n4b. Comparing documents AFTER adding and removing images..." << std::endl; + std::string diffMsgAfterModify; + int resultAfterModify = doc.compare(readDoc, &diffMsgAfterModify); + + if (resultAfterModify == 0) { + std::cout << " ✓ Documents still identical after modifications" << std::endl; + } else { + std::cout << " ✗ Documents differ after modifications (result: " << resultAfterModify << ")" << std::endl; + std::cout << " Differences: " << diffMsgAfterModify << std::endl; + } + + // Step 5: Write the modified workbook to a new file + std::cout << "\n5. Writing modified workbook to new file..." << std::endl; std::string modifiedFileName = "Demo11_Modified.xlsx"; try { readDoc.saveAs(modifiedFileName); @@ -786,12 +934,17 @@ int main() std::cout << " Error saving modified workbook: " << e.what() << std::endl; } - // Step 5: Verify the modification - std::cout << "\n5. Verification summary:" << std::endl; + // Step 6: Verify the modification + std::cout << "\n6. Verification summary:" << std::endl; std::cout << " - Original file: " << xlsxFileName << std::endl; std::cout << " - Modified file: " << modifiedFileName << std::endl; std::cout << " - Worksheets with images: " << totalImagesFound << std::endl; - std::cout << " - New image added to: File Loading worksheet" << std::endl; + std::cout << " - Images removed from 'File Loading': img3, img4" << std::endl; + if (!newImageId.empty()) { + std::cout << " - New image added to 'File Loading': " << newImageId << std::endl; + } else { + std::cout << " - New image added to 'File Loading': (none - worksheet not found)" << std::endl; + } readDoc.close(); @@ -828,14 +981,22 @@ int main() // Check if it's a worksheet (not a chartsheet) if (sheet.isType()) { XLWorksheet worksheet = sheet.get(); - - // Debug: Check if drawingML is valid - bool drawingValid = worksheet.drawingML().valid(); - std::cout << " DEBUG: About to call imageCount() for worksheet '" << sheetName << "'" << std::endl; int imageCount = static_cast(worksheet.imageCount()); - std::cout << " DEBUG: imageCount() returned: " << imageCount << std::endl; totalImages += imageCount; - std::cout << " Worksheet '" << sheetName << "': " << imageCount << " image(s) (drawingML valid: " << (drawingValid ? "Yes" : "No") << ")" << std::endl; + std::cout << " Worksheet '" << sheetName << "': " << imageCount << " image(s)" << std::endl; + + // Iterate through images and print their information + if (imageCount > 0) { + try { + auto imageInfos = worksheet.getImageInfos(); + for (size_t i = 0; i < imageInfos.size(); ++i) { + std::cout << " " << (i + 1) << ". " << imageInfoStr(imageInfos[i]) << std::endl; + } + } catch (const std::exception& e) { + std::cout << " Error getting image info: " << e.what() << std::endl; + } + } + } else { std::cout << " Sheet '" << sheetName << "': (not a worksheet, skipping)" << std::endl; } @@ -848,5 +1009,77 @@ int main() } } + /* Test the new image query functionality */ + std::cout << "\n=== Phase VIII: Testing Image Query Functionality ===" << std::endl; + + // Re-open the Demo11 file to test image queries + try { + XLDocument testDoc; + testDoc.open("Demo11.xlsx"); + std::cout << " Successfully opened Demo11.xlsx for query testing" << std::endl; + + // Test each worksheet with images + for (uint16_t i = 1; i <= testDoc.workbook().sheetCount(); ++i) { + XLSheet sheet = testDoc.workbook().sheet(i); + std::string sheetName = sheet.name(); + + if (sheet.isType()) { + XLWorksheet worksheet = sheet.get(); + int imageCount = static_cast(worksheet.imageCount()); + std::cout << " DEBUG: Worksheet '" << sheetName << "' imageCount: " << imageCount << std::endl; + + if (imageCount > 0) { + std::cout << "\n Worksheet '" << sheetName << "' has " << imageCount << " image(s):" << std::endl; + std::cout << " DEBUG: About to call getImageInfos() for worksheet '" << sheetName << "'" << std::endl; + std::cout << " DEBUG: Calling getImageInfos() now..." << std::endl; + + // Test getImageInfos() + const auto& imageInfos = worksheet.getImageInfos(); + std::cout << " DEBUG: getImageInfos() completed, returned " << imageInfos.size() << " image(s)" << std::endl; + + // Test individual image queries + for (size_t i = 0; i < imageInfos.size(); ++i) { + const auto& imgInfo = imageInfos[i]; + std::cout << " " << (i + 1) << ". " << imageInfoStr(imgInfo) << std::endl; + + // Test getImageInfoByImageID() + const auto& foundImg = worksheet.getImageInfoByImageID(imgInfo.imageId); + if (foundImg.isValid()) { + std::cout << " ✓ getImageInfoByImageID() found the image" << std::endl; + } else { + std::cout << " ✗ getImageInfoByImageID() failed to find the image" << std::endl; + } + + // Test getImageInfoByRelationshipId() + const auto& foundRel = worksheet.getImageInfoByRelationshipId(imgInfo.relationshipId); + if (foundRel.isValid()) { + std::cout << " ✓ getImageInfoByRelationshipId() found the image" << std::endl; + } else { + std::cout << " ✗ getImageInfoByRelationshipId() failed to find the image" << std::endl; + } + + // Test getImageInfosAtCell() + const auto& cellImgs = worksheet.getImageInfosAtCell(imgInfo.anchorCell); + std::cout << " getImageInfosAtCell('" << imgInfo.anchorCell << "') returned " << cellImgs.size() << " image(s)" << std::endl; + } + + // Test range queries + const auto& rangeImgs = worksheet.getImageInfosInRange("A1:Z100"); + std::cout << " getImageInfosInRange('A1:Z100') returned " << rangeImgs.size() << " image(s)" << std::endl; + + // Test validation + bool isValid = worksheet.validateImageRegistry(); + std::cout << " validateImageRegistry() returned: " << (isValid ? "Valid" : "Invalid") << std::endl; + } + } + } + + testDoc.close(); + std::cout << "\n Image query testing completed successfully!" << std::endl; + + } catch (const std::exception& e) { + std::cout << " ERROR: Failed to test image queries: " << e.what() << std::endl; + } + return 0; } diff --git a/OpenXLSX/headers/IZipArchive.hpp b/OpenXLSX/headers/IZipArchive.hpp index e7db7bf7..febd7626 100644 --- a/OpenXLSX/headers/IZipArchive.hpp +++ b/OpenXLSX/headers/IZipArchive.hpp @@ -171,7 +171,7 @@ namespace OpenXLSX m_zipArchive->deleteEntry(entryName); } - inline std::string getEntry(const std::string& name) { + inline std::string getEntry(const std::string& name) const { return m_zipArchive->getEntry(name); } @@ -238,7 +238,7 @@ namespace OpenXLSX inline virtual void deleteEntry(const std::string& entryName) = 0; - inline virtual std::string getEntry(const std::string& name) = 0; + inline virtual std::string getEntry(const std::string& name) const = 0; inline virtual bool hasEntry(const std::string& entryName) const = 0; @@ -326,7 +326,7 @@ namespace OpenXLSX ZipType.deleteEntry(entryName); } - inline std::string getEntry(const std::string& name) override { + inline std::string getEntry(const std::string& name) const override { return ZipType.getEntry(name); } diff --git a/OpenXLSX/headers/XLDocument.hpp b/OpenXLSX/headers/XLDocument.hpp index 021b110e..c12ea782 100644 --- a/OpenXLSX/headers/XLDocument.hpp +++ b/OpenXLSX/headers/XLDocument.hpp @@ -367,6 +367,14 @@ namespace OpenXLSX */ bool addImageEntry(const std::string& imageFilename, const std::vector& imageData, XLContentType contentType); + /** + * @brief Compare two documents for debugging + * @param other The other XLDocument to compare with + * @param diffMsg Optional pointer to append difference message (up to 16384 characters) + * @return 0 if identical, <0 if this precedes other, >0 if this follows other + */ + int compare(const XLDocument& other, std::string* diffMsg = nullptr) const; + /** * @brief Add a drawing file to the archive * @param drawingFilename The filename for the drawing in the archive @@ -402,7 +410,21 @@ namespace OpenXLSX * @param relsFilename The filename for the relationships file in the archive * @return The content of the file, or empty string if not found */ - std::string readRelationshipsFile(const std::string& relsFilename); + std::string readRelationshipsFile(const std::string& relsFilename) const; + + /** + * @brief Read the content of any file from the archive + * @param filePath The path of the file in the archive + * @return The content of the file, or empty string if not found + */ + std::string readFile(const std::string& filePath) const; + + /** + * @brief Delete an entry from the archive + * @param entryPath The path of the entry to delete + * @return true if the entry was deleted successfully, false otherwise + */ + bool deleteEntry(const std::string& entryPath); /** * @brief diff --git a/OpenXLSX/headers/XLImage.hpp b/OpenXLSX/headers/XLImage.hpp index d2206109..acbc7c4c 100644 --- a/OpenXLSX/headers/XLImage.hpp +++ b/OpenXLSX/headers/XLImage.hpp @@ -185,6 +185,21 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. namespace OpenXLSX { + // ========== Global Helper Functions ========== // + + /** + * @brief Helper function to append difference messages for debugging + * @param diffMsg Optional pointer to append difference message (up to 16384 characters) + * @param msg The message to append + */ + inline void appendDiff(std::string* diffMsg, const std::string& msg) + { + if (diffMsg && diffMsg->length() < 16000) { // Leave some buffer + if (!diffMsg->empty()) *diffMsg += "; "; + *diffMsg += msg; + } + } + // ========== Excel Constants ========== // /** @@ -204,6 +219,69 @@ namespace OpenXLSX */ constexpr uint32_t EMU_TO_PIXEL_RATIO = 9525; + /** + * @brief Structure containing image metadata for query operations + * + * This structure holds all the essential information about an embedded image + * that can be queried from a worksheet. It provides a unified interface + * for accessing image properties without needing to parse XML directly. + * + * The structure is designed to be efficient for both storage and access, + * with all data pre-parsed and readily available for common operations. + */ + struct OPENXLSX_EXPORT XLImageInfo + { + std::string imageId; /**< Unique image identifier (e.g., "img1", "img2") */ + std::string relationshipId; /**< Relationship ID in drawing.xml.rels (e.g., "rId1", "rId2") */ + std::string anchorCell; /**< Primary anchor cell reference (e.g., "A1", "B5") */ + std::string anchorType; /**< Anchor type: "oneCellAnchor" or "twoCellAnchor" */ + uint32_t widthPixels{0}; /**< Original image width in pixels */ + uint32_t heightPixels{0}; /**< Original image height in pixels */ + uint32_t displayWidthEMUs{0}; /**< Display width in Excel units (EMUs) */ + uint32_t displayHeightEMUs{0}; /**< Display height in Excel units (EMUs) */ + + /** + * @brief Default constructor + */ + XLImageInfo() = default; + + /** + * @brief Constructor with all parameters + * @param id Image ID + * @param relId Relationship ID + * @param cell Anchor cell reference + * @param type Anchor type + * @param wPx Width in pixels + * @param hPx Height in pixels + * @param wEmu Display width in EMUs + * @param hEmu Display height in EMUs + */ + XLImageInfo(const std::string& id, const std::string& relId, const std::string& cell, + const std::string& type, uint32_t wPx, uint32_t hPx, uint32_t wEmu, uint32_t hEmu) + : imageId(id), relationshipId(relId), anchorCell(cell), anchorType(type), + widthPixels(wPx), heightPixels(hPx), displayWidthEMUs(wEmu), displayHeightEMUs(hEmu) {} + + /** + * @brief Check if this image info is valid (has non-empty imageId) + * @return True if valid, false otherwise + */ + bool isValid() const { return !imageId.empty(); } + + /** + * @brief Check if this image info is empty (default constructed) + * @return True if empty, false otherwise + */ + bool isEmpty() const { return imageId.empty(); } + + /** + * @brief Compare two image info structures for debugging + * @param other The other XLImageInfo to compare with + * @param diffMsg Optional pointer to append difference message (up to 16384 characters) + * @return 0 if identical, <0 if this precedes other, >0 if this follows other + */ + int compare(const XLImageInfo& other, std::string* diffMsg = nullptr) const; + }; + /** * @brief The XLImage class represents an image that can be embedded in a worksheet */ @@ -405,6 +483,14 @@ namespace OpenXLSX */ XLContentType contentType() const; + /** + * @brief Compare two images for debugging + * @param other The other XLImage to compare with + * @param diffMsg Optional pointer to append difference message (up to 16384 characters) + * @return 0 if identical, <0 if this precedes other, >0 if this follows other + */ + int compare(const XLImage& other, std::string* diffMsg = nullptr) const; + private: std::vector m_imageData; /**< Binary image data */ std::string m_mimeType; /**< MIME type of the image */ diff --git a/OpenXLSX/headers/XLSheet.hpp b/OpenXLSX/headers/XLSheet.hpp index 83c5b688..0b792935 100644 --- a/OpenXLSX/headers/XLSheet.hpp +++ b/OpenXLSX/headers/XLSheet.hpp @@ -911,6 +911,14 @@ namespace OpenXLSX */ XLWorksheet& operator=(XLWorksheet&& other); + /** + Compare two worksheets -- for debugging + Result = 0 => worksheets same + Result < 0 => this worksheet precedes other, in a sorted list -- (arbitrary sorting method) + @param diffMsg (optional) if result is non-zero, message exlaining difference will be appended here, up to maximum of 16384 charcaters + */ + int compare(const XLWorksheet& other, std::string *diffMsg = nullptr) const; + /** * @brief * @param ref @@ -1284,6 +1292,7 @@ namespace OpenXLSX * @param image The XLImage object to add * @param cellRef The cell reference where to place the image (e.g., "A1") * @return True if successful, false otherwise + * @note Will fail if an image with the same ID already exists */ bool addImage(const XLImage& image, const std::string& cellRef); @@ -1377,6 +1386,13 @@ namespace OpenXLSX */ bool hasImages() const; + /** + * @brief Check if the worksheet has images by examining the XML directly + * @return True if the worksheet XML contains a element, false otherwise + * @note This method does not modify the document and can be called safely on temporary objects + */ + bool hasImagesInXML() const; + /** * @brief Generate the next available image ID for this worksheet * @return The next globally unique image ID (e.g., "img1", "img2", etc.) @@ -1391,7 +1407,209 @@ namespace OpenXLSX */ std::string getRelationshipIdFromImageCount() const; + //---------------------------------------------------------------------------------------------------------------------- + // Image Query Methods + //---------------------------------------------------------------------------------------------------------------------- + + /** + * @brief Get all image information in the worksheet + * @return Const reference to vector of XLImageInfo objects + * @note Returns a const reference for efficiency - no copying of the vector + */ + const std::vector& getImageInfos() const; + + /** + * @brief Get specific image information by its ID + * @param imageId The unique image identifier (e.g., "img1", "img2") + * @return Const reference to XLImageInfo object, or empty XLImageInfo if not found + * @note Returns a const reference for efficiency - no copying of the struct + */ + const XLImageInfo& getImageInfoByImageID(const std::string& imageId) const; + + /** + * @brief Get specific image information by its relationship ID + * @param relationshipId The relationship ID in drawing.xml.rels (e.g., "rId1", "rId2") + * @return Const reference to XLImageInfo object, or empty XLImageInfo if not found + * @note Returns a const reference for efficiency - no copying of the struct + */ + const XLImageInfo& getImageInfoByRelationshipId(const std::string& relationshipId) const; + + /** + * @brief Get all image information at a specific cell + * @param cellRef The cell reference (e.g., "A1", "B5") + * @return Const reference to vector of XLImageInfo objects at that cell + * @note Returns a const reference for efficiency - no copying of the vector + */ + const std::vector& getImageInfosAtCell(const std::string& cellRef) const; + + /** + * @brief Get all image information in a specific range + * @param cellRange The cell range (e.g., "A1:B5", "C10:D20") + * @return Const reference to vector of XLImageInfo objects in that range + * @note Returns a const reference for efficiency - no copying of the vector + */ + const std::vector& getImageInfosInRange(const std::string& cellRange) const; + + /** + * @brief Get iterator to the beginning of the image information collection + * @return Const iterator to the first image info + * @note Useful for range-based for loops and STL algorithms + */ + std::vector::const_iterator imageInfosBegin() const; + + /** + * @brief Get iterator to the end of the image information collection + * @return Const iterator to the end of the image information collection + * @note Useful for range-based for loops and STL algorithms + */ + std::vector::const_iterator imageInfosEnd() const; + + //---------------------------------------------------------------------------------------------------------------------- + // Image Modification Methods + //---------------------------------------------------------------------------------------------------------------------- + + /** + * @brief Remove an image by its ID + * @param imageId The unique image identifier to remove + * @return True if the image was found and removed, false otherwise + * @note This will update the image registry and remove the image from DrawingML + */ + bool removeImageByImageID(const std::string& imageId); + + /** + * @brief Remove an image by its relationship ID + * @param relationshipId The relationship ID to remove + * @return True if the image was found and removed, false otherwise + * @note This will update the image registry and remove the image from DrawingML + */ + bool removeImageByRelationshipId(const std::string& relationshipId); + + /** + * @brief Remove all images from the worksheet + * @note This will clear the image registry and remove all images from DrawingML + */ + void clearImages(); + + //---------------------------------------------------------------------------------------------------------------------- + // Image Removal Helper Functions (Private) + //---------------------------------------------------------------------------------------------------------------------- + private: + /** + * @brief Remove image node from DrawingML XML (in-memory document) + * @param relationshipId The relationship ID of the image to remove + * @return True if the image node was found and removed + */ + bool removeImageFromDrawingXML(const std::string& relationshipId) const; + + /** + * @brief Remove image relationship from relationships file (in-memory document) + * @param relationshipId The relationship ID to remove + * @return True if the relationship was found and removed + */ + bool removeImageFromRelationships(const std::string& relationshipId) const; + + /** + * @brief Remove image file from archive (delete binary file entry) + * @param relationshipId The relationship ID of the image file to remove + * @return True if the file was found and removed from archive + */ + bool removeImageFileFromArchive(const std::string& relationshipId) const; + + /** + * @brief Get image path from relationship ID + * @param relationshipId The relationship ID to look up + * @return The image path, or empty string if not found + */ + std::string getImagePathFromRelationship(const std::string& relationshipId) const; + + /** + * @brief Remove image file by direct path + * @param imagePath The relative image path (e.g., "../media/image_img3.png") + * @return True if the file was found and removed + */ + bool removeImageFileByPath(const std::string& imagePath) const; + + public: + //---------------------------------------------------------------------------------------------------------------------- + // Image Registry Management + //---------------------------------------------------------------------------------------------------------------------- + + /** + * @brief Refresh the image registry from DrawingML XML + * @note This method parses the DrawingML XML and rebuilds the image registry. + * The registry is automatically refreshed when needed (when images are queried + * and the registry is empty), but this method can be called manually if needed. + */ + void refreshImageRegistry() const; + + /** + * @brief Validate image registry for data integrity + * @return True if registry is valid (no duplicate IDs), false otherwise + * @note Checks for duplicate image IDs, relationship IDs, and other corruption + */ + bool validateImageRegistry() const; + + /** + * @brief Check if an image ID already exists + * @param imageId The image ID to check + * @return True if the ID exists, false otherwise + */ + bool hasImageWithId(const std::string& imageId) const; + + /** + * @brief Check if a relationship ID already exists + * @param relationshipId The relationship ID to check + * @return True if the ID exists, false otherwise + */ + bool hasImageWithRelationshipId(const std::string& relationshipId) const; + + /** + * @brief Robust XML element name matching that handles namespace prefixes + * @param elementName The actual element name (may include namespace prefix) + * @param targetName The target element name to match + * @return True if the element name matches the target (with or without namespace) + */ + bool matchesElementName(const std::string& elementName, const std::string& targetName) const; + + //---------------------------------------------------------------------------------------------------------------------- + // Private Helper Methods for Image Registry + //---------------------------------------------------------------------------------------------------------------------- + + private: + /** + * @brief Read the drawing relationships file to map relationship IDs to image files + * @param relationshipMap Output map of relationship ID to image file path + */ + void readDrawingRelationships(std::map& relationshipMap) const; + + /** + * @brief Process a oneCellAnchor element and add to registry + * @param anchor The oneCellAnchor XML node + * @param relationshipMap Map of relationship IDs to image files + */ + void processOneCellAnchor(const pugi::xml_node& anchor, const std::map& relationshipMap) const; + + /** + * @brief Process a twoCellAnchor element and add to registry + * @param anchor The twoCellAnchor XML node + * @param relationshipMap Map of relationship IDs to image files + */ + void processTwoCellAnchor(const pugi::xml_node& anchor, const std::map& relationshipMap) const; + + /** + * @brief Extract image ID from image file path + * @param imagePath The image file path (e.g., "../media/image_img1.png") + * @return The image ID (e.g., "img1") + */ + std::string extractImageIdFromPath(const std::string& imagePath) const; + + /** + * @brief Convert EMUs to pixels (approximate conversion) + * @param emus EMUs value + * @return Approximate pixel value + */ + uint32_t emusToPixels(uint32_t emus) const; /** * @brief Add an image to the DrawingML * @param image The image to add @@ -1439,6 +1657,13 @@ namespace OpenXLSX * @param drawingFilename The drawing filename */ void createDrawingRelationshipsFile(const std::string& drawingFilename); + + /** + * @brief Create an empty DrawingML object for worksheets without images + * This is needed when addImage() is called on a worksheet that doesn't have images yet + * @return Reference to the newly created DrawingML object + */ + XLDrawingML& createEmptyDrawingML(); /** * @brief Add a relationship for a new image to the existing relationships file @@ -1501,6 +1726,9 @@ namespace OpenXLSX XLComments m_comments{}; /**< class handling the worksheet comments */ XLTables m_tables{}; /**< class handling the worksheet table settings */ std::vector m_images{}; /**< vector storing images in the worksheet */ + mutable std::vector m_imageRegistry{}; /**< vector storing parsed image metadata for query operations */ + mutable XLImageInfo m_emptyImageInfo{}; /**< empty image info for returning when image not found */ + mutable std::vector m_emptyImageInfoVector{}; /**< empty image info vector for returning when no images found */ const std::vector< std::string_view >& m_nodeOrder = XLWorksheetNodeOrder; // worksheet XML root node required child sequence }; @@ -1747,9 +1975,22 @@ namespace OpenXLSX T get() const { try { - if constexpr (std::is_same::value) - return std::get(m_sheet); - + if constexpr (std::is_same::value) { + XLWorksheet worksheet = std::get(m_sheet); + + // Only initialize DrawingML if the worksheet actually has images + // This avoids unnecessary XML modifications for worksheets without images + if (worksheet.hasImagesInXML()) { + try { + std::ignore = worksheet.drawingML(); // Initialize DrawingML + } catch (...) { + // DrawingML initialization failed, but worksheet is still usable + } + } + // If no images, leave DrawingML invalid (fast path) + + return worksheet; + } else if constexpr (std::is_same::value) return std::get(m_sheet); } diff --git a/OpenXLSX/sources/XLDocument.cpp b/OpenXLSX/sources/XLDocument.cpp index 9b49fa3f..a37bfec2 100644 --- a/OpenXLSX/sources/XLDocument.cpp +++ b/OpenXLSX/sources/XLDocument.cpp @@ -1287,7 +1287,7 @@ bool XLDocument::hasRelationshipsFile(const std::string& relsFilename) const /** * @details Read the content of a relationships file from the archive */ -std::string XLDocument::readRelationshipsFile(const std::string& relsFilename) +std::string XLDocument::readRelationshipsFile(const std::string& relsFilename) const { try { if (m_archive.hasEntry(relsFilename)) { @@ -1299,6 +1299,37 @@ std::string XLDocument::readRelationshipsFile(const std::string& relsFilename) return ""; } +/** + * @details Read the content of any file from the archive + */ +std::string XLDocument::readFile(const std::string& filePath) const +{ + try { + if (m_archive.hasEntry(filePath)) { + return m_archive.getEntry(filePath); + } + } catch (const std::exception&) { + // Return empty string if there's an error + } + return ""; +} + +/** + * @details Delete an entry from the archive + */ +bool XLDocument::deleteEntry(const std::string& entryPath) +{ + try { + if (m_archive.hasEntry(entryPath)) { + m_archive.deleteEntry(entryPath); + return true; + } + } catch (const std::exception&) { + // Return false if there's an error + } + return false; +} + /** * @details return value defaults to true, false only where the XLCommandType implements it */ @@ -1867,4 +1898,112 @@ namespace OpenXLSX return "img" + std::to_string(++m_globalImageCounter); } + /** + * @details Compare two documents for debugging + */ + int XLDocument::compare(const XLDocument& other, std::string* diffMsg) const + { + + // Compare document names/paths + std::string thisPath = path(); + std::string otherPath = other.path(); + int pathCompare = thisPath.compare(otherPath); + if (pathCompare != 0) { + appendDiff(diffMsg, "document path differs: '" + thisPath + "' vs '" + otherPath + "'"); + return pathCompare; + } + + // Compare workbook worksheets + if (m_workbook.valid() && other.m_workbook.valid()) { + // Get worksheet names + auto thisSheetNames = m_workbook.sheetNames(); + auto otherSheetNames = other.m_workbook.sheetNames(); + + if (thisSheetNames.size() != otherSheetNames.size()) { + appendDiff(diffMsg, "worksheet count differs: " + std::to_string(thisSheetNames.size()) + + " vs " + std::to_string(otherSheetNames.size())); + return thisSheetNames.size() < otherSheetNames.size() ? -1 : 1; + } + + // Compare each worksheet + for (size_t i = 0; i < thisSheetNames.size(); ++i) { + if (thisSheetNames[i] != otherSheetNames[i]) { + appendDiff(diffMsg, "worksheet name differs at index " + std::to_string(i) + + ": '" + thisSheetNames[i] + "' vs '" + otherSheetNames[i] + "'"); + return thisSheetNames[i].compare(otherSheetNames[i]); + } + + // Compare worksheet content + try { + // Use const_cast to access non-const worksheet method + XLWorksheet thisSheet = const_cast(this)->m_workbook.worksheet(thisSheetNames[i]); + XLWorksheet otherSheet = const_cast(&other)->m_workbook.worksheet(otherSheetNames[i]); + + int sheetCompare = thisSheet.compare(otherSheet, diffMsg); + if (sheetCompare != 0) { + appendDiff(diffMsg, "worksheet '" + thisSheetNames[i] + "' differs"); + return sheetCompare; + } + } catch (const std::exception&) { + appendDiff(diffMsg, "error comparing worksheet '" + thisSheetNames[i] + "'"); + return -1; + } + } + } else if (m_workbook.valid() != other.m_workbook.valid()) { + appendDiff(diffMsg, "workbook validity differs: " + std::string(m_workbook.valid() ? "valid" : "invalid") + + " vs " + std::string(other.m_workbook.valid() ? "valid" : "invalid")); + return m_workbook.valid() ? 1 : -1; + } + + // Compare document-level relationships (simplified) + try { + // Just check if both have valid relationships objects + bool thisDocRelValid = m_docRelationships.valid(); + bool otherDocRelValid = other.m_docRelationships.valid(); + if (thisDocRelValid != otherDocRelValid) { + appendDiff(diffMsg, "document relationships validity differs: " + std::string(thisDocRelValid ? "valid" : "invalid") + + " vs " + std::string(otherDocRelValid ? "valid" : "invalid")); + return thisDocRelValid ? 1 : -1; + } + + bool thisWbkRelValid = m_wbkRelationships.valid(); + bool otherWbkRelValid = other.m_wbkRelationships.valid(); + if (thisWbkRelValid != otherWbkRelValid) { + appendDiff(diffMsg, "workbook relationships validity differs: " + std::string(thisWbkRelValid ? "valid" : "invalid") + + " vs " + std::string(otherWbkRelValid ? "valid" : "invalid")); + return thisWbkRelValid ? 1 : -1; + } + } catch (const std::exception&) { + appendDiff(diffMsg, "error comparing document relationships"); + return -1; + } + + // Compare content types (simplified) + try { + // Just check if both have valid content types objects + bool thisContentTypeValid = m_contentTypes.valid(); + bool otherContentTypeValid = other.m_contentTypes.valid(); + if (thisContentTypeValid != otherContentTypeValid) { + appendDiff(diffMsg, "content types validity differs: " + std::string(thisContentTypeValid ? "valid" : "invalid") + + " vs " + std::string(otherContentTypeValid ? "valid" : "invalid")); + return thisContentTypeValid ? 1 : -1; + } + } catch (const std::exception&) { + appendDiff(diffMsg, "error comparing content types"); + return -1; + } + + // TODO: Compare other document properties + // - Core properties (m_coreProperties) + // - App properties (m_appProperties) + // - Styles (m_styles) + // - Shared strings (m_sharedStrings) + // - Content types (m_contentTypes) + // - Document relationships (m_docRelationships) + // - Workbook relationships (m_wbkRelationships) + + // Documents are identical + return 0; + } + } // namespace OpenXLSX diff --git a/OpenXLSX/sources/XLImage.cpp b/OpenXLSX/sources/XLImage.cpp index 15ddfc5e..aa092a75 100644 --- a/OpenXLSX/sources/XLImage.cpp +++ b/OpenXLSX/sources/XLImage.cpp @@ -466,4 +466,132 @@ namespace OpenXLSX return emus / EMU_TO_PIXEL_RATIO; } + /** + * @details Compare two image info structures for debugging + */ + int XLImageInfo::compare(const XLImageInfo& other, std::string* diffMsg) const + { + + // Compare image ID (most important for identification) + int idCompare = imageId.compare(other.imageId); + if (idCompare != 0) { + appendDiff(diffMsg, "image ID differs: '" + imageId + "' vs '" + other.imageId + "'"); + return idCompare; + } + + // Compare relationship ID + int relIdCompare = relationshipId.compare(other.relationshipId); + if (relIdCompare != 0) { + appendDiff(diffMsg, "relationship ID differs: '" + relationshipId + "' vs '" + other.relationshipId + "'"); + return relIdCompare; + } + + // Compare anchor cell + int cellCompare = anchorCell.compare(other.anchorCell); + if (cellCompare != 0) { + appendDiff(diffMsg, "anchor cell differs: '" + anchorCell + "' vs '" + other.anchorCell + "'"); + return cellCompare; + } + + // Compare anchor type + int typeCompare = anchorType.compare(other.anchorType); + if (typeCompare != 0) { + appendDiff(diffMsg, "anchor type differs: '" + anchorType + "' vs '" + other.anchorType + "'"); + return typeCompare; + } + + // Compare dimensions + if (widthPixels != other.widthPixels) { + appendDiff(diffMsg, "width differs: " + std::to_string(widthPixels) + + " vs " + std::to_string(other.widthPixels) + " pixels"); + return widthPixels < other.widthPixels ? -1 : 1; + } + + if (heightPixels != other.heightPixels) { + appendDiff(diffMsg, "height differs: " + std::to_string(heightPixels) + + " vs " + std::to_string(other.heightPixels) + " pixels"); + return heightPixels < other.heightPixels ? -1 : 1; + } + + // Compare display dimensions + if (displayWidthEMUs != other.displayWidthEMUs) { + appendDiff(diffMsg, "display width differs: " + std::to_string(displayWidthEMUs) + + " vs " + std::to_string(other.displayWidthEMUs) + " EMUs"); + return displayWidthEMUs < other.displayWidthEMUs ? -1 : 1; + } + + if (displayHeightEMUs != other.displayHeightEMUs) { + appendDiff(diffMsg, "display height differs: " + std::to_string(displayHeightEMUs) + + " vs " + std::to_string(other.displayHeightEMUs) + " EMUs"); + return displayHeightEMUs < other.displayHeightEMUs ? -1 : 1; + } + + // Image info structures are identical + return 0; + } + + /** + * @details Compare two images for debugging + */ + int XLImage::compare(const XLImage& other, std::string* diffMsg) const + { + + // Compare image data (most important for embedded images) + if (m_imageData != other.m_imageData) { + appendDiff(diffMsg, "image data differs (size: " + std::to_string(m_imageData.size()) + + " vs " + std::to_string(other.m_imageData.size()) + " bytes)"); + return m_imageData.size() < other.m_imageData.size() ? -1 : 1; + } + + // Compare MIME type + int mimeCompare = m_mimeType.compare(other.m_mimeType); + if (mimeCompare != 0) { + appendDiff(diffMsg, "MIME type differs: '" + m_mimeType + "' vs '" + other.m_mimeType + "'"); + return mimeCompare; + } + + // Compare extension + int extCompare = m_extension.compare(other.m_extension); + if (extCompare != 0) { + appendDiff(diffMsg, "extension differs: '" + m_extension + "' vs '" + other.m_extension + "'"); + return extCompare; + } + + // Compare ID + int idCompare = m_id.compare(other.m_id); + if (idCompare != 0) { + appendDiff(diffMsg, "image ID differs: '" + m_id + "' vs '" + other.m_id + "'"); + return idCompare; + } + + // Compare dimensions + if (m_widthPixels != other.m_widthPixels) { + appendDiff(diffMsg, "width differs: " + std::to_string(m_widthPixels) + + " vs " + std::to_string(other.m_widthPixels) + " pixels"); + return m_widthPixels < other.m_widthPixels ? -1 : 1; + } + + if (m_heightPixels != other.m_heightPixels) { + appendDiff(diffMsg, "height differs: " + std::to_string(m_heightPixels) + + " vs " + std::to_string(other.m_heightPixels) + " pixels"); + return m_heightPixels < other.m_heightPixels ? -1 : 1; + } + + // Compare display dimensions + if (m_displayWidth != other.m_displayWidth) { + appendDiff(diffMsg, "display width differs: " + std::to_string(m_displayWidth) + + " vs " + std::to_string(other.m_displayWidth) + " EMUs"); + return m_displayWidth < other.m_displayWidth ? -1 : 1; + } + + if (m_displayHeight != other.m_displayHeight) { + appendDiff(diffMsg, "display height differs: " + std::to_string(m_displayHeight) + + " vs " + std::to_string(other.m_displayHeight) + " EMUs"); + return m_displayHeight < other.m_displayHeight ? -1 : 1; + } + + // Images are identical + return 0; + } + } // namespace OpenXLSX diff --git a/OpenXLSX/sources/XLSheet.cpp b/OpenXLSX/sources/XLSheet.cpp index 536033ed..5571dac9 100644 --- a/OpenXLSX/sources/XLSheet.cpp +++ b/OpenXLSX/sources/XLSheet.cpp @@ -1696,6 +1696,15 @@ XLVmlDrawing& XLWorksheet::vmlDrawing() XLDrawingML& XLWorksheet::drawingML() { if (!m_drawingML.valid()) { + // Only initialize DrawingML if the worksheet actually has images + // This prevents unnecessary initialization for worksheets without images + if (!hasImagesInXML()) { + // For worksheets without images, return an invalid DrawingML + // DrawingML will be created when addImage() is called + static XLDrawingML invalidDrawingML(nullptr); + return invalidDrawingML; + } + // ===== Append xdr namespace attribute to worksheet if not present XMLNode docElement = xmlDocument().document_element(); XMLAttribute xdrNamespace = appendAndGetAttribute(docElement, "xmlns:xdr", ""); @@ -1703,11 +1712,24 @@ XLDrawingML& XLWorksheet::drawingML() std::ignore = relationships(); // create sheet relationships if not existing - // ===== Create DrawingML XML file + // ===== Check if DrawingML XML file already exists uint16_t sheetXmlNo = sheetXmlNumber(); std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; - // Create the DrawingML XML content + // Try to load existing DrawingML data first + try { + XLXmlData* xmlData = parentDoc().getDrawingXmlData(drawingFilename); + if (xmlData) { + m_drawingML = XLDrawingML(xmlData); + if (m_drawingML.valid()) { + return m_drawingML; // Successfully loaded existing data + } + } + } catch (...) { + // DrawingML data doesn't exist or failed to load, will create new one below + } + + // If no existing data or loading failed, create new DrawingML XML file std::string drawingXml = "\n" "\n" + "\n" + ""; + + // Add to archive using public method + uint16_t sheetXmlNo = sheetXmlNumber(); + std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; + + if (!parentDoc().addDrawingFile(drawingFilename, drawingXml)) { + throw XLException("XLWorksheet::createEmptyDrawingML(): could not create drawing file"); + } + + // Create DrawingML object + XLXmlData* xmlData = parentDoc().getDrawingXmlData(drawingFilename); + if (!xmlData) { + throw XLException("XLWorksheet::createEmptyDrawingML(): could not get XML data for drawing"); + } + m_drawingML = XLDrawingML(xmlData); + + // Ensure the XML document is loaded + if (!m_drawingML.valid()) { + throw XLException("XLWorksheet::createEmptyDrawingML(): could not initialize DrawingML object"); + } + + // Add relationship + std::string drawingRelativePath = getPathARelativeToPathB(drawingFilename, getXmlPath()); + XLRelationshipItem drawingRelationship; + if (!m_relationships.targetExists(drawingRelativePath)) + drawingRelationship = m_relationships.addRelationship(XLRelationshipType::Drawing, drawingRelativePath); + else + drawingRelationship = m_relationships.relationshipByTarget(drawingRelativePath); + + if (drawingRelationship.empty()) + throw XLException("XLWorksheet::createEmptyDrawingML(): could not add determine sheet relationship for Drawing"); + + // Add element to worksheet + XMLNode drawing = appendAndGetNode(docElement, "drawing", m_nodeOrder); + if (drawing.empty()) + throw XLException("XLWorksheet::createEmptyDrawingML(): could not add element to worksheet XML"); + appendAndSetAttribute(drawing, "r:id", drawingRelationship.id()); + + // Create drawing relationships file + std::string drawingRelsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNo) + ".xml.rels"; + if (!parentDoc().hasRelationshipsFile(drawingRelsFilename)) { + createDrawingRelationshipsFile(drawingFilename); + } + + return m_drawingML; +} + /** * @details fetches XLComments for the sheet - creates & assigns the class if empty */ @@ -1864,8 +1954,10 @@ bool XLWorksheet::addImage(const XLImage& image, uint32_t row, uint16_t column) return false; } - // Ensure we have a DrawingML drawing - std::ignore = drawingML(); + // Ensure we have a DrawingML drawing - create it if needed + if (!m_drawingML.valid()) { + createEmptyDrawingML(); + } // Add image to DrawingML // Use sequential relationship ID that matches createDrawingRelationshipsFile @@ -1932,7 +2024,10 @@ bool XLWorksheet::addImageWithOffset(const XLImage& image, uint32_t row, uint16_ } // Get or create DrawingML - XLDrawingML& drawing = drawingML(); + if (!m_drawingML.valid()) { + createEmptyDrawingML(); + } + XLDrawingML& drawing = m_drawingML; // Generate relationship ID std::string relationshipId = getRelationshipIdFromImageCount(); @@ -1969,7 +2064,10 @@ bool XLWorksheet::addImageTwoCellAnchor(const XLImage& image, } // Get or create DrawingML - XLDrawingML& drawing = drawingML(); + if (!m_drawingML.valid()) { + createEmptyDrawingML(); + } + XLDrawingML& drawing = m_drawingML; // Generate relationship ID std::string relationshipId = getRelationshipIdFromImageCount(); @@ -1986,17 +2084,23 @@ bool XLWorksheet::addImageTwoCellAnchor(const XLImage& image, */ size_t XLWorksheet::imageCount() const { - // For existing files, count images from DrawingML - // For newly created files, count from m_images vector if (m_images.empty()) { - // Try to get count from existing DrawingML without creating it - try { - // Check if DrawingML already exists and is valid - if (m_drawingML.valid()) { - return m_drawingML.imageCount(); + // Check if DrawingML is valid (was initialized because images exist) + if (m_drawingML.valid()) { + size_t count = m_drawingML.imageCount(); + return count; + } else { + // DrawingML not initialized = no images in XML + // Debug assertion: if DrawingML is invalid, XML should not have element + #if (defined(_DEBUG) || !defined(NDEBUG)) + bool hasDrawingInXML = hasImagesInXML(); + if (hasDrawingInXML) { + std::cerr << "WARNING: Worksheet '" << name() + << "' has element in XML but DrawingML is invalid!" << std::endl; + std::cerr << " This indicates DrawingML initialization failed despite XML having images." << std::endl; } - return 0; - } catch (...) { + #endif + return 0; } } else { @@ -2013,6 +2117,18 @@ bool XLWorksheet::hasImages() const return !m_images.empty(); } +/** + * @details Check if the worksheet has images by examining the XML directly + * This method does not modify the document and can be called safely on temporary objects + */ +bool XLWorksheet::hasImagesInXML() const +{ + // Check if worksheet XML has a element + XMLNode docElement = xmlDocument().document_element(); + XMLNode drawingNode = docElement.child("drawing"); + return !drawingNode.empty(); +} + /** * @details Generate the next available image ID for this worksheet */ @@ -2285,3 +2401,106 @@ bool XLChartsheet::isSelected_impl() const { return tabIsSelected(xmlDocument()) * @details Calls the setTabSelected() free function. */ void XLChartsheet::setSelected_impl(bool selected) { setTabSelected(xmlDocument(), selected); } + +/** + * @details Compare two worksheets for debugging + */ +int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const +{ + // Compare worksheet names first (most important for human readability) + std::string thisName = name(); + std::string otherName = other.name(); + int nameCompare = thisName.compare(otherName); + if (nameCompare != 0) { + appendDiff(diffMsg, "worksheet name differs: '" + thisName + "' vs '" + otherName + "'"); + return nameCompare; + } + + // Compare DrawingML validity (crucial for image detection) + // DrawingML is now automatically initialized in XLWorkbook::worksheet() if images exist + bool thisDrawingMLValid = m_drawingML.valid(); + bool otherDrawingMLValid = other.m_drawingML.valid(); + if (thisDrawingMLValid != otherDrawingMLValid) { + appendDiff(diffMsg, "DrawingML validity differs: " + std::string(thisDrawingMLValid ? "valid" : "invalid") + + " vs " + std::string(otherDrawingMLValid ? "valid" : "invalid")); + return thisDrawingMLValid ? 1 : -1; + } + + // Compare image count + size_t thisImageCount = imageCount(); + size_t otherImageCount = other.imageCount(); + if (thisImageCount != otherImageCount) { + appendDiff(diffMsg, "image count differs: " + std::to_string(thisImageCount) + + " vs " + std::to_string(otherImageCount)); + return thisImageCount < otherImageCount ? -1 : 1; + } + + // Compare image registry (parsed image metadata) + size_t thisRegistrySize = m_imageRegistry.size(); + size_t otherRegistrySize = other.m_imageRegistry.size(); + if (thisRegistrySize != otherRegistrySize) { + appendDiff(diffMsg, "image registry size differs: " + std::to_string(thisRegistrySize) + + " vs " + std::to_string(otherRegistrySize)); + return thisRegistrySize < otherRegistrySize ? -1 : 1; + } + + // Compare images if both worksheets have images in memory + size_t thisImagesInMemory = m_images.size(); + size_t otherImagesInMemory = other.m_images.size(); + if (thisImagesInMemory > 0 || otherImagesInMemory > 0) { + if (thisImagesInMemory != otherImagesInMemory) { + appendDiff(diffMsg, "images in memory count differs: " + std::to_string(thisImagesInMemory) + + " vs " + std::to_string(otherImagesInMemory)); + return thisImagesInMemory < otherImagesInMemory ? -1 : 1; + } + + // Compare each image in memory + for (size_t i = 0; i < thisImagesInMemory && i < otherImagesInMemory; ++i) { + int imageCompare = m_images[i].compare(other.m_images[i], diffMsg); + if (imageCompare != 0) { + appendDiff(diffMsg, "image " + std::to_string(i) + " differs"); + return imageCompare; + } + } + } + + // Compare image registry entries if both have registry entries + if (thisRegistrySize > 0) { + for (size_t i = 0; i < thisRegistrySize && i < otherRegistrySize; ++i) { + int registryCompare = m_imageRegistry[i].compare(other.m_imageRegistry[i], diffMsg); + if (registryCompare != 0) { + appendDiff(diffMsg, "image registry entry " + std::to_string(i) + " differs"); + return registryCompare; + } + } + } + + // Compare relationships (crucial for image detection) - simplified + try { + // Just check if both have valid relationships objects + bool thisRelValid = m_relationships.valid(); + bool otherRelValid = other.m_relationships.valid(); + if (thisRelValid != otherRelValid) { + appendDiff(diffMsg, "relationships validity differs: " + std::string(thisRelValid ? "valid" : "invalid") + + " vs " + std::string(otherRelValid ? "valid" : "invalid")); + return thisRelValid ? 1 : -1; + } + + // Skip DrawingML relationship check for now to avoid path issues + // TODO: Implement safer DrawingML relationship comparison + } catch (const std::exception&) { + appendDiff(diffMsg, "error comparing relationships"); + return -1; + } + + // TODO: Compare other worksheet properties + // - Cell content (would require extensive implementation) + // - Merge cells + // - Comments + // - Tables + // - DrawingML XML content (would require XML comparison) + // - VML drawing content + + // Worksheets are identical + return 0; +} diff --git a/OpenXLSX/sources/XLWorkbook.cpp b/OpenXLSX/sources/XLWorkbook.cpp index 34df1c79..b55e5ad4 100644 --- a/OpenXLSX/sources/XLWorkbook.cpp +++ b/OpenXLSX/sources/XLWorkbook.cpp @@ -130,12 +130,44 @@ XLSheet XLWorkbook::sheet(uint16_t index) // 2024-04-30: whitespace support /** * @details */ -XLWorksheet XLWorkbook::worksheet(const std::string& sheetName) { return sheet(sheetName).get(); } +XLWorksheet XLWorkbook::worksheet(const std::string& sheetName) +{ + XLWorksheet worksheet = sheet(sheetName).get(); + + // Only initialize DrawingML if the worksheet actually has images + // This avoids unnecessary XML modifications for worksheets without images + if (worksheet.hasImagesInXML()) { + try { + std::ignore = worksheet.drawingML(); // Initialize DrawingML + } catch (...) { + // DrawingML initialization failed, but worksheet is still usable + } + } + // If no images, leave DrawingML invalid (fast path) + + return worksheet; +} /** * @details */ -XLWorksheet XLWorkbook::worksheet(uint16_t index) { return sheet(index).get(); } +XLWorksheet XLWorkbook::worksheet(uint16_t index) +{ + XLWorksheet worksheet = sheet(index).get(); + + // Only initialize DrawingML if the worksheet actually has images + // This avoids unnecessary XML modifications for worksheets without images + if (worksheet.hasImagesInXML()) { + try { + std::ignore = worksheet.drawingML(); // Initialize DrawingML + } catch (...) { + // DrawingML initialization failed, but worksheet is still usable + } + } + // If no images, leave DrawingML invalid (fast path) + + return worksheet; +} /** * @details diff --git a/OpenXLSX/sources/XLWorksheetImageQuery.cpp b/OpenXLSX/sources/XLWorksheetImageQuery.cpp new file mode 100644 index 00000000..c0afa9a3 --- /dev/null +++ b/OpenXLSX/sources/XLWorksheetImageQuery.cpp @@ -0,0 +1,1030 @@ +/* + + ____ ____ ___ ____ ____ ____ ___ + 6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M' + 8P Y8 `MM. d' MM 6M' ` `MM. d' +6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d' +MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d' +MM MM MM' `Mb 6M' `Mb MMM9 `Mb `MMd MM YMMMMb `MMd +MM MM MM MM MM MM MM' MM dMM. MM `Mb dMM. +MM MM MM MM MMMMMMMM MM MM d'`MM. MM MM d'`MM. +YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. + 8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM. + YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_ + MM + MM + _MM_ + + Copyright (c) 2018, Kenneth Troldal Balslev + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + - Neither the name of the author nor the + names of any contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +// ===== External Includes ===== // +#include +#include +#include +#include + +// ===== OpenXLSX Includes ===== // +#include "XLSheet.hpp" +#include "XLCellReference.hpp" +#include "XLDocument.hpp" + +namespace OpenXLSX +{ + //---------------------------------------------------------------------------------------------------------------------- + // Image Query Methods Implementation + //---------------------------------------------------------------------------------------------------------------------- + + /** + * @brief Get all image information in the worksheet + * @return Const reference to vector of XLImageInfo objects + */ + const std::vector& XLWorksheet::getImageInfos() const + { + // Auto-refresh registry if empty + if (m_imageRegistry.empty()) { + refreshImageRegistry(); + } + + return m_imageRegistry; + } + + /** + * @brief Get specific image information by its ID + * @param imageId The unique image identifier + * @return Const reference to XLImageInfo object, or empty XLImageInfo if not found + */ + const XLImageInfo& XLWorksheet::getImageInfoByImageID(const std::string& imageId) const + { + // Auto-refresh registry if empty + if (m_imageRegistry.empty()) { + refreshImageRegistry(); + } + + // Linear search for the image ID + for (const auto& imgInfo : m_imageRegistry) { + if (imgInfo.imageId == imageId) { + return imgInfo; + } + } + + // Return empty image info if not found + return m_emptyImageInfo; + } + + /** + * @brief Get specific image information by its relationship ID + * @param relationshipId The relationship ID in drawing.xml.rels + * @return Const reference to XLImageInfo object, or empty XLImageInfo if not found + */ + const XLImageInfo& XLWorksheet::getImageInfoByRelationshipId(const std::string& relationshipId) const + { + // Auto-refresh registry if empty + if (m_imageRegistry.empty()) { + refreshImageRegistry(); + } + + // Linear search for the relationship ID + for (const auto& imgInfo : m_imageRegistry) { + if (imgInfo.relationshipId == relationshipId) { + return imgInfo; + } + } + + // Return empty image info if not found + return m_emptyImageInfo; + } + + /** + * @brief Get all image information at a specific cell + * @param cellRef The cell reference (e.g., "A1", "B5") + * @return Const reference to vector of XLImageInfo objects at that cell + */ + const std::vector& XLWorksheet::getImageInfosAtCell(const std::string& cellRef) const + { + // Auto-refresh registry if empty + if (m_imageRegistry.empty()) { + refreshImageRegistry(); + } + + // Clear the result vector + m_emptyImageInfoVector.clear(); + + // Filter images by cell reference + for (const auto& imgInfo : m_imageRegistry) { + if (imgInfo.anchorCell == cellRef) { + m_emptyImageInfoVector.push_back(imgInfo); + } + } + + return m_emptyImageInfoVector; + } + + /** + * @brief Get all image information in a specific range + * @param cellRange The cell range (e.g., "A1:B5", "C10:D20") + * @return Const reference to vector of XLImageInfo objects in that range + */ + const std::vector& XLWorksheet::getImageInfosInRange(const std::string& cellRange) const + { + // Auto-refresh registry if empty + if (m_imageRegistry.empty()) { + refreshImageRegistry(); + } + + // Clear the result vector + m_emptyImageInfoVector.clear(); + + // Parse the range + size_t colonPos = cellRange.find(':'); + if (colonPos == std::string::npos) { + // Single cell range + return getImageInfosAtCell(cellRange); + } + + std::string topLeft = cellRange.substr(0, colonPos); + std::string bottomRight = cellRange.substr(colonPos + 1); + + try { + XLCellReference topLeftRef(topLeft); + XLCellReference bottomRightRef(bottomRight); + + // Filter images by range + for (const auto& imgInfo : m_imageRegistry) { + if (!imgInfo.anchorCell.empty()) { + XLCellReference imgRef(imgInfo.anchorCell); + + // Check if image is within range + if (imgRef.row() >= topLeftRef.row() && imgRef.row() <= bottomRightRef.row() && + imgRef.column() >= topLeftRef.column() && imgRef.column() <= bottomRightRef.column()) { + m_emptyImageInfoVector.push_back(imgInfo); + } + } + } + } catch (const std::exception&) { + // Invalid range format, return empty vector + } + + return m_emptyImageInfoVector; + } + + /** + * @brief Get iterator to the beginning of the image information collection + * @return Const iterator to the first image info + */ + std::vector::const_iterator XLWorksheet::imageInfosBegin() const + { + // Auto-refresh registry if empty + if (m_imageRegistry.empty()) { + refreshImageRegistry(); + } + return m_imageRegistry.begin(); + } + + /** + * @brief Get iterator to the end of the image information collection + * @return Const iterator to the end of the image information collection + */ + std::vector::const_iterator XLWorksheet::imageInfosEnd() const + { + // Auto-refresh registry if empty + if (m_imageRegistry.empty()) { + refreshImageRegistry(); + } + return m_imageRegistry.end(); + } + + //---------------------------------------------------------------------------------------------------------------------- + // Image Modification Methods Implementation + //---------------------------------------------------------------------------------------------------------------------- + + /** + * @brief Remove an image by its ID + * @param imageId The unique image identifier to remove + * @return True if the image was found and removed, false otherwise + */ + bool XLWorksheet::removeImageByImageID(const std::string& imageId) + { + // Auto-refresh registry if empty + if (m_imageRegistry.empty()) { + refreshImageRegistry(); + } + + // Find the image in the registry + auto it = std::find_if(m_imageRegistry.begin(), m_imageRegistry.end(), + [&imageId](const XLImageInfo& img) { return img.imageId == imageId; }); + + if (it == m_imageRegistry.end()) { + return false; // Image not found + } + + // Store the relationship ID before removing from registry + std::string relationshipId = it->relationshipId; + + // Get image path BEFORE removing relationships (we need the relationship to find the file path) + std::string imagePath = getImagePathFromRelationship(relationshipId); + + // Remove from DrawingML XML (in-memory XML document) + bool xmlRemoved = removeImageFromDrawingXML(relationshipId); + + // Remove from relationships file (in-memory XML document) + bool relsRemoved = removeImageFromRelationships(relationshipId); + + // Remove image file from archive (delete binary file entry) - use the path we got earlier + bool fileRemoved = removeImageFileByPath(imagePath); + + // Remove from registry (in-memory cache) + m_imageRegistry.erase(it); + + // Return true if at least XML removal succeeded + return xmlRemoved; + } + + /** + * @brief Remove an image by its relationship ID + * @param relationshipId The relationship ID to remove + * @return True if the image was found and removed, false otherwise + */ + bool XLWorksheet::removeImageByRelationshipId(const std::string& relationshipId) + { + // Auto-refresh registry if empty + if (m_imageRegistry.empty()) { + refreshImageRegistry(); + } + + // Find the image in the registry + auto it = std::find_if(m_imageRegistry.begin(), m_imageRegistry.end(), + [&relationshipId](const XLImageInfo& img) { return img.relationshipId == relationshipId; }); + + if (it == m_imageRegistry.end()) { + return false; // Image not found + } + + // Remove from DrawingML XML (in-memory XML document) + bool xmlRemoved = removeImageFromDrawingXML(relationshipId); + + // Remove from relationships file (in-memory XML document) + bool relsRemoved = removeImageFromRelationships(relationshipId); + + // Remove image file from archive (delete binary file entry) + bool fileRemoved = removeImageFileFromArchive(relationshipId); + + // Remove from registry (in-memory cache) + m_imageRegistry.erase(it); + + // Return true if at least XML removal succeeded + return xmlRemoved; + } + + /** + * @brief Remove all images from the worksheet + */ + void XLWorksheet::clearImages() + { + // Clear the registry + m_imageRegistry.clear(); + + // TODO: Implement XML clearing from DrawingML + // This would involve: + // 1. Clearing all image nodes from drawing.xml + // 2. Clearing the relationships file + // 3. Removing all image files from the archive + } + + //---------------------------------------------------------------------------------------------------------------------- + // Image Registry Management Implementation + //---------------------------------------------------------------------------------------------------------------------- + + /** + * @brief Refresh the image registry from DrawingML XML + */ + void XLWorksheet::refreshImageRegistry() const + { + // Clear existing registry + m_imageRegistry.clear(); + + // Check if DrawingML is valid + if (!m_drawingML.valid()) { + return; // No drawing data available + } + + try { + // Get the DrawingML XML content by reading directly from the archive + std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNumber()) + ".xml"; + + std::string xmlContent = parentDoc().readRelationshipsFile(drawingFilename); + if (xmlContent.empty()) { + return; // No XML data available + } + + // Parse the XML document + pugi::xml_document doc; + if (!doc.load_string(xmlContent.c_str())) { + return; // Failed to parse XML + } + + // Get the root element (xdr:wsDr) + auto wsDr = doc.document_element(); + std::string rootName = wsDr.name(); + + // Check for both "wsDr" and "xdr:wsDr" (with namespace prefix) + if (rootName != "wsDr" && rootName != "xdr:wsDr") { + return; // Invalid root element + } + + // Read the relationships file to get image mappings + std::map relationshipMap; + readDrawingRelationships(relationshipMap); + + // Process each anchor element + for (auto anchor : wsDr.children()) { + std::string anchorName = anchor.name(); + + if (matchesElementName(anchorName, "oneCellAnchor")) { + processOneCellAnchor(anchor, relationshipMap); + } + else if (matchesElementName(anchorName, "twoCellAnchor")) { + processTwoCellAnchor(anchor, relationshipMap); + } + } + } + catch (const std::exception& ) { + // If parsing fails, leave registry empty + m_imageRegistry.clear(); + } + } + + /** + * @brief Read the drawing relationships file to map relationship IDs to image files + * @param relationshipMap Output map of relationship ID to image file path + */ + void XLWorksheet::readDrawingRelationships(std::map& relationshipMap) const + { + try { + // Get the drawing filename + std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNumber()) + ".xml"; + std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + + // Check if relationships file exists + if (!parentDoc().hasRelationshipsFile(relsFilename)) { + return; // No relationships file + } + + // Read the relationships file + std::string relsContent = parentDoc().readRelationshipsFile(relsFilename); + if (relsContent.empty()) { + return; // Empty relationships file + } + + // Parse the relationships XML + pugi::xml_document relsDoc; + if (!relsDoc.load_string(relsContent.c_str())) { + return; // Failed to parse relationships + } + + // Extract relationship mappings + for (auto rel : relsDoc.document_element().children("Relationship")) { + std::string id = rel.attribute("Id").value(); + std::string target = rel.attribute("Target").value(); + std::string type = rel.attribute("Type").value(); + + // Only process image relationships + if (type == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image") { + relationshipMap[id] = target; + } + } + } + catch (const std::exception& ) { + // If reading relationships fails, leave map empty + } + } + + /** + * @brief Process a oneCellAnchor element and add to registry + * @param anchor The oneCellAnchor XML node + * @param relationshipMap Map of relationship IDs to image files + */ + void XLWorksheet::processOneCellAnchor(const pugi::xml_node& anchor, const std::map& relationshipMap) const + { + try { + // Extract cell position + auto from = anchor.child("xdr:from"); + if (from.empty()) { + from = anchor.child("from"); // Try without namespace prefix + } + if (from.empty()) { + return; + } + + auto colNode = from.child("xdr:col"); + if (colNode.empty()) { + colNode = from.child("col"); // Try without namespace prefix + } + auto rowNode = from.child("xdr:row"); + if (rowNode.empty()) { + rowNode = from.child("row"); // Try without namespace prefix + } + + int col = colNode.text().as_int(); + int row = rowNode.text().as_int(); + std::string cellRef = XLCellReference(row + 1, col + 1).address(); // Convert to 1-based + + // Extract dimensions + auto ext = anchor.child("xdr:ext"); + if (ext.empty()) { + ext = anchor.child("ext"); // Try without namespace prefix + } + if (ext.empty()) { + return; + } + + uint32_t widthEMUs = ext.attribute("cx").as_uint(); + uint32_t heightEMUs = ext.attribute("cy").as_uint(); + + // Extract image information + auto pic = anchor.child("xdr:pic"); + if (pic.empty()) { + pic = anchor.child("pic"); // Try without namespace prefix + } + if (pic.empty()) { + return; + } + + auto blipFill = pic.child("xdr:blipFill"); + if (blipFill.empty()) { + blipFill = pic.child("a:blipFill"); // Try with a namespace prefix + } + if (blipFill.empty()) { + blipFill = pic.child("blipFill"); // Try without namespace prefix + } + if (blipFill.empty()) { + return; + } + + auto blip = blipFill.child("a:blip"); + if (blip.empty()) { + blip = blipFill.child("blip"); // Try without namespace prefix + } + if (blip.empty()) { + return; + } + + std::string relationshipId = blip.attribute("r:embed").value(); + if (relationshipId.empty()) { + return; + } + + // Get image file path from relationship + auto it = relationshipMap.find(relationshipId); + if (it == relationshipMap.end()) { + return; + } + + std::string imagePath = it->second; + + // Extract image ID from filename (e.g., "../media/image_img1.png" -> "img1") + std::string imageId = extractImageIdFromPath(imagePath); + + // Create XLImageInfo + XLImageInfo imgInfo; + imgInfo.imageId = imageId; + imgInfo.relationshipId = relationshipId; + imgInfo.anchorCell = cellRef; + imgInfo.anchorType = "oneCellAnchor"; + imgInfo.displayWidthEMUs = widthEMUs; + imgInfo.displayHeightEMUs = heightEMUs; + + // Convert EMUs to pixels (approximate) + imgInfo.widthPixels = emusToPixels(widthEMUs); + imgInfo.heightPixels = emusToPixels(heightEMUs); + + // Add to registry + m_imageRegistry.push_back(imgInfo); + } + catch (const std::exception& ) { + // If processing fails, skip this anchor + } + } + + /** + * @brief Process a twoCellAnchor element and add to registry + * @param anchor The twoCellAnchor XML node + * @param relationshipMap Map of relationship IDs to image files + */ + void XLWorksheet::processTwoCellAnchor(const pugi::xml_node& anchor, const std::map& relationshipMap) const + { + try { + // Extract cell position (use 'from' cell as primary anchor) + auto from = anchor.child("xdr:from"); + if (from.empty()) { + from = anchor.child("from"); // Try without namespace prefix + } + if (from.empty()) { + return; + } + + auto colNode = from.child("xdr:col"); + if (colNode.empty()) { + colNode = from.child("col"); // Try without namespace prefix + } + auto rowNode = from.child("xdr:row"); + if (rowNode.empty()) { + rowNode = from.child("row"); // Try without namespace prefix + } + + int col = colNode.text().as_int(); + int row = rowNode.text().as_int(); + std::string cellRef = XLCellReference(row + 1, col + 1).address(); // Convert to 1-based + + // Extract dimensions + auto ext = anchor.child("xdr:ext"); + if (ext.empty()) { + ext = anchor.child("ext"); // Try without namespace prefix + } + if (ext.empty()) { + return; + } + + uint32_t widthEMUs = ext.attribute("cx").as_uint(); + uint32_t heightEMUs = ext.attribute("cy").as_uint(); + + // Extract image information + auto pic = anchor.child("xdr:pic"); + if (pic.empty()) { + pic = anchor.child("pic"); // Try without namespace prefix + } + if (pic.empty()) { + return; + } + + auto blipFill = pic.child("xdr:blipFill"); + if (blipFill.empty()) { + blipFill = pic.child("a:blipFill"); // Try with a namespace prefix + } + if (blipFill.empty()) { + blipFill = pic.child("blipFill"); // Try without namespace prefix + } + if (blipFill.empty()) { + return; + } + + auto blip = blipFill.child("a:blip"); + if (blip.empty()) { + blip = blipFill.child("blip"); // Try without namespace prefix + } + if (blip.empty()) { + return; + } + + std::string relationshipId = blip.attribute("r:embed").value(); + if (relationshipId.empty()) { + return; + } + + // Get image file path from relationship + auto it = relationshipMap.find(relationshipId); + if (it == relationshipMap.end()) { + return; + } + + std::string imagePath = it->second; + + // Extract image ID from filename + std::string imageId = extractImageIdFromPath(imagePath); + + // Create XLImageInfo + XLImageInfo imgInfo; + imgInfo.imageId = imageId; + imgInfo.relationshipId = relationshipId; + imgInfo.anchorCell = cellRef; + imgInfo.anchorType = "twoCellAnchor"; + imgInfo.displayWidthEMUs = widthEMUs; + imgInfo.displayHeightEMUs = heightEMUs; + + // Convert EMUs to pixels (approximate) + imgInfo.widthPixels = emusToPixels(widthEMUs); + imgInfo.heightPixels = emusToPixels(heightEMUs); + + // Add to registry + m_imageRegistry.push_back(imgInfo); + } + catch (const std::exception& ) { + // If processing fails, skip this anchor + } + } + + /** + * @brief Extract image ID from image file path + * @param imagePath The image file path (e.g., "../media/image_img1.png") + * @return The image ID (e.g., "img1") + */ + std::string XLWorksheet::extractImageIdFromPath(const std::string& imagePath) const + { + // Extract filename from path + size_t lastSlash = imagePath.find_last_of('/'); + std::string filename = (lastSlash != std::string::npos) ? imagePath.substr(lastSlash + 1) : imagePath; + + // Remove "image_" prefix and file extension + if (filename.substr(0, 6) == "image_") { + std::string withoutPrefix = filename.substr(6); + size_t dotPos = withoutPrefix.find_last_of('.'); + if (dotPos != std::string::npos) { + return withoutPrefix.substr(0, dotPos); + } + } + + return filename; // Fallback to full filename + } + + /** + * @brief Convert EMUs to pixels (approximate conversion) + * @param emus EMUs value + * @return Approximate pixel value + */ + uint32_t XLWorksheet::emusToPixels(uint32_t emus) const + { + // Standard conversion: 1 inch = 914400 EMUs, 1 inch = 96 pixels (at 96 DPI) + // So: 1 pixel = 914400 / 96 = 9525 EMUs + return static_cast(emus / 9525); + } + + /** + * @brief Validate image registry for data integrity + * @return True if registry is valid (no duplicate IDs), false otherwise + */ + bool XLWorksheet::validateImageRegistry() const + { + std::set imageIds; + std::set relationshipIds; + + for (const auto& imgInfo : m_imageRegistry) { + // Check for duplicate image IDs + if (!imgInfo.imageId.empty()) { + if (imageIds.find(imgInfo.imageId) != imageIds.end()) { + return false; // Duplicate image ID found + } + imageIds.insert(imgInfo.imageId); + } + + // Check for duplicate relationship IDs + if (!imgInfo.relationshipId.empty()) { + if (relationshipIds.find(imgInfo.relationshipId) != relationshipIds.end()) { + return false; // Duplicate relationship ID found + } + relationshipIds.insert(imgInfo.relationshipId); + } + } + + return true; // No duplicates found + } + + /** + * @brief Check if an image ID already exists + * @param imageId The image ID to check + * @return True if the ID exists, false otherwise + */ + bool XLWorksheet::hasImageWithId(const std::string& imageId) const + { + // Auto-refresh registry if empty + if (m_imageRegistry.empty()) { + refreshImageRegistry(); + } + + return std::any_of(m_imageRegistry.begin(), m_imageRegistry.end(), + [&imageId](const XLImageInfo& img) { return img.imageId == imageId; }); + } + + /** + * @brief Check if a relationship ID already exists + * @param relationshipId The relationship ID to check + * @return True if the ID exists, false otherwise + */ + bool XLWorksheet::hasImageWithRelationshipId(const std::string& relationshipId) const + { + // Auto-refresh registry if empty + if (m_imageRegistry.empty()) { + refreshImageRegistry(); + } + + return std::any_of(m_imageRegistry.begin(), m_imageRegistry.end(), + [&relationshipId](const XLImageInfo& img) { return img.relationshipId == relationshipId; }); + } + + /** + * @brief Get image path from relationship ID + * @param relationshipId The relationship ID to look up + * @return The image path, or empty string if not found + */ + std::string XLWorksheet::getImagePathFromRelationship(const std::string& relationshipId) const + { + try { + std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + + if (!parentDoc().hasRelationshipsFile(relsFilename)) { + return ""; + } + + std::string relsContent = parentDoc().readRelationshipsFile(relsFilename); + if (relsContent.empty()) { + return ""; + } + + pugi::xml_document relsDoc; + if (!relsDoc.load_string(relsContent.c_str())) { + return ""; + } + + for (auto rel : relsDoc.document_element().children("Relationship")) { + if (rel.attribute("Id").value() == relationshipId) { + return rel.attribute("Target").value(); + } + } + + return ""; + } + catch (const std::exception&) { + return ""; + } + } + + /** + * @brief Remove image file by direct path + * @param imagePath The relative image path (e.g., "../media/image_img3.png") + * @return True if the file was found and removed + */ + bool XLWorksheet::removeImageFileByPath(const std::string& imagePath) const + { + if (imagePath.empty()) { + return false; + } + + try { + // Convert relative path to absolute path within the archive + std::string fullImagePath = "xl/media/" + imagePath.substr(imagePath.find_last_of('/') + 1); + + bool result = const_cast(parentDoc()).deleteEntry(fullImagePath); + return result; + } + catch (const std::exception&) { + return false; + } + } + + /** + * @brief Remove image node from DrawingML XML (in-memory document) + * @param relationshipId The relationship ID of the image to remove + * @return True if the image node was found and removed + * + * @note This function modifies the existing XLXmlData object for the drawing file. + * The changes are automatically persisted when the document is saved. + */ + bool XLWorksheet::removeImageFromDrawingXML(const std::string& relationshipId) const + { + try { + // Use standard sequential numbering approach (consistent with rest of codebase) + std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNumber()) + ".xml"; + // Get the existing XLXmlData object for the drawing file + // This is the proper way - use the existing XML data structure rather than bypassing it + XLXmlData* xmlData = const_cast(parentDoc()).getDrawingXmlData(drawingFilename); + if (!xmlData) { + return false; + } + + // Get the underlying XMLDocument (pugi::xml_document) + XMLDocument& xmlDoc = *xmlData->getXmlDocument(); + if (!xmlDoc.document_element()) { + return false; + } + + // Get the root element (xdr:wsDr) + auto wsDr = xmlDoc.document_element(); + std::string rootName = wsDr.name(); + + // Check for both "wsDr" and "xdr:wsDr" (with namespace prefix) + if (rootName != "wsDr" && rootName != "xdr:wsDr") { + return false; + } + + // Find and remove the image node + for (auto anchor : wsDr.children()) { + // Check both oneCellAnchor and twoCellAnchor + if (matchesElementName(anchor.name(), "oneCellAnchor") || + matchesElementName(anchor.name(), "twoCellAnchor")) { + + // Look for the pic element with the matching relationship ID + auto pic = anchor.child("xdr:pic"); + if (pic.empty()) { + pic = anchor.child("pic"); + } + + if (!pic.empty()) { + // Check blipFill -> blip -> r:embed + auto blipFill = pic.child("xdr:blipFill"); + if (blipFill.empty()) { + blipFill = pic.child("a:blipFill"); + } + if (blipFill.empty()) { + blipFill = pic.child("blipFill"); + } + + if (!blipFill.empty()) { + auto blip = blipFill.child("a:blip"); + if (blip.empty()) { + blip = blipFill.child("blip"); + } + + if (!blip.empty()) { + std::string embedId = blip.attribute("r:embed").value(); + if (embedId == relationshipId) { + // Found the matching image, remove the entire anchor + wsDr.remove_child(anchor); + + // The XMLDocument is automatically updated - no need to save/restore + // The XLXmlData will persist the changes when the document is saved + return true; + } + } + } + } + } + } + + return false; // Image not found + } + catch (const std::exception&) { + return false; + } + } + + /** + * @brief Remove image relationship from relationships file (in-memory document) + * @param relationshipId The relationship ID to remove + * @return True if the relationship was found and removed + */ + bool XLWorksheet::removeImageFromRelationships(const std::string& relationshipId) const + { + try { + // Use standard sequential numbering approach (consistent with rest of codebase) + std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + + // Check if relationships file exists + if (!parentDoc().hasRelationshipsFile(relsFilename)) { + return false; + } + + // Read the relationships file from in-memory document + std::string relsContent = parentDoc().readRelationshipsFile(relsFilename); + if (relsContent.empty()) { + return false; + } + + // Parse the relationships XML + pugi::xml_document relsDoc; + if (!relsDoc.load_string(relsContent.c_str())) { + return false; + } + + // Find and remove the relationship + for (auto rel : relsDoc.document_element().children("Relationship")) { + std::string relId = rel.attribute("Id").value(); + if (relId == relationshipId) { + relsDoc.document_element().remove_child(rel); + + // Update the in-memory XML document + std::ostringstream oss; + relsDoc.save(oss); + const_cast(parentDoc()).addRelationshipsFile(relsFilename, oss.str()); + + return true; + } + } + + return false; // Relationship not found + } + catch (const std::exception&) { + return false; + } + } + + /** + * @brief Remove image file from archive (delete binary file entry) + * @param relationshipId The relationship ID of the image file to remove + * @return True if the file was found and removed from archive + */ + bool XLWorksheet::removeImageFileFromArchive(const std::string& relationshipId) const + { + try { + // Use standard sequential numbering approach (consistent with rest of codebase) + std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + + if (!parentDoc().hasRelationshipsFile(relsFilename)) { + return false; + } + + std::string relsContent = parentDoc().readRelationshipsFile(relsFilename); + if (relsContent.empty()) { + return false; + } + + pugi::xml_document relsDoc; + if (!relsDoc.load_string(relsContent.c_str())) { + return false; + } + + std::string imagePath = ""; + for (auto rel : relsDoc.document_element().children("Relationship")) { + std::string relId = rel.attribute("Id").value(); + if (relId == relationshipId) { + imagePath = rel.attribute("Target").value(); + break; + } + } + + if (!imagePath.empty()) { + // Convert relative path to absolute path within the archive + std::string fullImagePath = "xl/media/" + imagePath.substr(imagePath.find_last_of('/') + 1); + + // Remove the image file from the archive + // This deletes the binary file entry from the in-memory archive + try { + bool result = const_cast(parentDoc()).deleteEntry(fullImagePath); + return result; + } catch (const std::exception&) { + return false; + } + } else { + return false; + } + + return false; + } + catch (const std::exception&) { + return false; + } + } + + /** + * @brief Robust XML element name matching that handles namespace prefixes + * @param elementName The actual element name (may include namespace prefix) + * @param targetName The target element name to match + * @return True if the element name matches the target (with or without namespace) + * + * @example + * Exact match: "oneCellAnchor" matches "oneCellAnchor" + * Namespace prefix: "xdr:oneCellAnchor" matches "oneCellAnchor" + * Different namespace: "a:oneCellAnchor" matches "oneCellAnchor" + */ + bool XLWorksheet::matchesElementName(const std::string& elementName, const std::string& targetName) const + { + // Exact match + if (elementName == targetName) { + return true; + } + + // Check if element name ends with target name (handles namespace prefixes) + if (elementName.length() > targetName.length() + 1) { + size_t pos = elementName.find_last_of(':'); + if (pos != std::string::npos && pos + 1 < elementName.length()) { + std::string localName = elementName.substr(pos + 1); + if (localName == targetName) { + return true; + } + } + } + + // Check if element name starts with target name followed by colon + if (elementName.length() == targetName.length() + 1 && elementName[targetName.length()] == ':') { + if (elementName.substr(0, targetName.length()) == targetName) { + return true; + } + } + + return false; + } + +} // namespace OpenXLSX From 26d7082fa5873fabaf7427640b3ebebd24131cd2 Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Thu, 9 Oct 2025 22:17:27 -0700 Subject: [PATCH 09/13] Embedded Image Support -- phase VIII / Document Properties --- Examples/Demo11.cpp | 118 +++++++++++--- OpenXLSX/headers/XLDocument.hpp | 147 +++++++++++++++++ OpenXLSX/sources/XLDocument.cpp | 253 ++++++++++++++++++++++++++++++ OpenXLSX/sources/XLProperties.cpp | 4 +- 4 files changed, 497 insertions(+), 25 deletions(-) diff --git a/Examples/Demo11.cpp b/Examples/Demo11.cpp index 567c5b8d..75d960cb 100644 --- a/Examples/Demo11.cpp +++ b/Examples/Demo11.cpp @@ -702,11 +702,58 @@ int main() // and creates a foundation for more advanced sizing operations. } // End of file loading tests + std::cout << "\n=== Inspect Image Registry ===" << std::endl; + for (uint16_t sheet_idx = 1; sheet_idx <= doc.workbook().sheetCount(); ++sheet_idx) { + XLSheet sheet = doc.workbook().sheet(sheet_idx); + std::string sheetName = sheet.name(); + if (sheet.isType()) { + XLWorksheet worksheet = sheet.get(); + try { + const auto& imageInfos = worksheet.getImageInfos(); + std::cout << " sheet " << sheetName << " now has " << imageInfos.size() << " images in registry" << std::endl; + for (const auto& imgInfo : imageInfos) { + std::cout << " " << imageInfoStr( imgInfo ) << std::endl; + } + } + catch (const std::exception& e) { + std::cout << " ERROR: Failed to query registry: " << e.what() << std::endl; + } + } + } + + // Save the workbook std::cout << "\nSaving workbook as '" << xlsxFileName << "'..." << std::endl; + + // Set document properties before saving + doc.setCreator("OpenXLSX Demo11"); + doc.setTitle("Comprehensive Image Embedding Test Matrix"); + doc.setSubject("Testing various image embedding options in Excel"); + doc.setDescription("This document demonstrates different combinations of image embedding options including anchor types, file formats, sizing strategies, and units"); + doc.setKeywords("OpenXLSX, images, embedding, Excel, testing, demo"); + doc.setLastModifiedBy("Demo11 Application"); + doc.setCategory("Test Document"); + doc.setApplication("OpenXLSX Demo11 Application"); + doc.setCreationDateToNow(); + doc.setModificationDateToNow(); + doc.setAbsPath(""); + try { doc.save(); std::cout << "Workbook saved successfully." << std::endl; + + // Display the properties that were set + std::cout << "\nDocument properties set:" << std::endl; + std::cout << " Creator: " << doc.creator() << std::endl; + std::cout << " Title: " << doc.title() << std::endl; + std::cout << " Subject: " << doc.subject() << std::endl; + std::cout << " Description: " << doc.description() << std::endl; + std::cout << " Keywords: " << doc.keywords() << std::endl; + std::cout << " Last Modified By: " << doc.lastModifiedBy() << std::endl; + std::cout << " Category: " << doc.category() << std::endl; + std::cout << " Application: " << doc.application() << std::endl; + std::cout << " Creation Date: " << doc.creationDate() << std::endl; + std::cout << " Modification Date: " << doc.modificationDate() << std::endl; } catch (const std::exception& e) { std::cout << "Error saving workbook: " << e.what() << std::endl; return 1; @@ -819,32 +866,34 @@ int main() std::cout << " Error getting current images: " << e.what() << std::endl; } - // Remove the third and fourth images (img3 and img4) - std::cout << "\n Removing third and fourth images (img3 and img4)..." << std::endl; - bool removedImg3 = fileLoadingSheet.removeImageByImageID("img3"); - bool removedImg4 = fileLoadingSheet.removeImageByImageID("img4"); - std::cout << " Removed img3: " << (removedImg3 ? "Success" : "Failed") << std::endl; - std::cout << " Removed img4: " << (removedImg4 ? "Success" : "Failed") << std::endl; + // Remove image 3 by Image ID and image 4 by Relationship ID (testing both methods) + std::cout << "\n Removing image 3 by Image ID and image 4 by Relationship ID..." << std::endl; + bool removedImg3 = fileLoadingSheet.removeImageByImageID("img3"); // Third image (A14) + bool removedImg4 = fileLoadingSheet.removeImageByRelationshipId("rId5"); // Fourth image (A20) + std::cout << " Removed img3 (by Image ID): " << (removedImg3 ? "Success" : "Failed") << std::endl; + std::cout << " Removed img4 (by Relationship ID): " << (removedImg4 ? "Success" : "Failed") << std::endl; // Set "REMOVED" text above the cells where images were removed fileLoadingSheet.cell("B13").value() = (removedImg3) ? "REMOVED" : "FAILED TO REMOVE"; // Near img3 at A14 fileLoadingSheet.cell("B19").value() = (removedImg4) ? "REMOVED" : "FAILED TO REMOVE"; // Near img4 at A20 - + // Verify the images are removed by checking the registry + try { + const auto& imageInfos = fileLoadingSheet.getImageInfos(); + std::cout << " DEBUG: Registry now has " << imageInfos.size() << " images after removal" << std::endl; + for (const auto& imgInfo : imageInfos) { + std::cout << " " << imageInfoStr( imgInfo ) << std::endl; + } + }catch (const std::exception& e) { + std::cout << " ERROR: Failed to verify image removal: " << e.what() << std::endl; + } + // Add a new image std::cout << "\n Adding new image..." << std::endl; OpenXLSX::XLImage newImage; newImageId = fileLoadingSheet.generateNextImageId(); std::cout << " Generated new image ID: " << newImageId << std::endl; - // Check if the generated ID already exists and create a unique one if needed - if (fileLoadingSheet.hasImageWithId(newImageId)) { - std::cout << " WARNING: Generated ID '" << newImageId << "' already exists, creating unique ID..." << std::endl; - // Create a unique ID by appending timestamp or counter - newImageId = "img_new_" + std::to_string(time(nullptr) % 10000); - std::cout << " Using unique ID: " << newImageId << std::endl; - } - // Use the same PNG data from our static constants if (newImage.loadFromData(pngData, "image/png", newImageId)) { std::cout << " Successfully loaded image data" << std::endl; @@ -874,21 +923,21 @@ int main() // Verify the image was actually added by checking the registry try { - const auto& testImages = fileLoadingSheet.getImageInfos(); - std::cout << " DEBUG: Registry now has " << testImages.size() << " images after addition" << std::endl; + const auto& imageInfos = fileLoadingSheet.getImageInfos(); + std::cout << " DEBUG: Registry now has " << imageInfos.size() << " images after addition" << std::endl; bool foundNewImage = false; - for (const auto& img : testImages) { - if (img.imageId == newImageId) { + for (const auto& imgInfo : imageInfos) { + std::cout << " " << imageInfoStr( imgInfo ) << std::endl; + if (imgInfo.imageId == newImageId) { foundNewImage = true; - std::cout << " DEBUG: Found new image in registry: " << imageInfoStr(img) << std::endl; - break; + std::cout << " DEBUG: Found new image in registry: " << imageInfoStr(imgInfo) << std::endl; } } if (!foundNewImage) { - std::cout << " WARNING: New image not found in registry after addition!" << std::endl; + std::cout << " WARNING: New image '" << newImageId << "' not found in registry after addition!" << std::endl; } } catch (const std::exception& e) { - std::cout << " DEBUG: Error checking registry after addition: " << e.what() << std::endl; + std::cout << " ERROR: Failed to verify image addition: " << e.what() << std::endl; } } else { std::cout << " Failed to add new image to worksheet" << std::endl; @@ -927,9 +976,32 @@ int main() // Step 5: Write the modified workbook to a new file std::cout << "\n5. Writing modified workbook to new file..." << std::endl; std::string modifiedFileName = "Demo11_Modified.xlsx"; + + // Set document properties for the modified version + readDoc.setCreator("OpenXLSX Demo11 - Modified"); + readDoc.setTitle("Comprehensive Image Embedding Test Matrix - Modified"); + readDoc.setSubject("Testing image embedding with modifications (add/remove operations)"); + readDoc.setDescription("This document demonstrates image embedding operations including adding and removing images during read-modify-write cycles"); + readDoc.setKeywords("OpenXLSX, images, embedding, Excel, testing, demo, modified, read-modify-write"); + readDoc.setLastModifiedBy("Demo11 Application - Modified"); + readDoc.setCategory("Test Document - Modified"); + readDoc.setApplication("OpenXLSX Demo11 Application - Modified"); + readDoc.setModificationDateToNow(); // Update modification date for the modified version + try { readDoc.saveAs(modifiedFileName); std::cout << " Modified workbook saved as: " << modifiedFileName << std::endl; + + // Display the properties that were set for the modified version + std::cout << "\n Modified document properties:" << std::endl; + std::cout << " Creator: " << readDoc.creator() << std::endl; + std::cout << " Title: " << readDoc.title() << std::endl; + std::cout << " Subject: " << readDoc.subject() << std::endl; + std::cout << " Keywords: " << readDoc.keywords() << std::endl; + std::cout << " Last Modified By: " << readDoc.lastModifiedBy() << std::endl; + std::cout << " Category: " << readDoc.category() << std::endl; + std::cout << " Application: " << readDoc.application() << std::endl; + std::cout << " Modification Date: " << readDoc.modificationDate() << std::endl; } catch (const std::exception& e) { std::cout << " Error saving modified workbook: " << e.what() << std::endl; } diff --git a/OpenXLSX/headers/XLDocument.hpp b/OpenXLSX/headers/XLDocument.hpp index c12ea782..7af74f45 100644 --- a/OpenXLSX/headers/XLDocument.hpp +++ b/OpenXLSX/headers/XLDocument.hpp @@ -54,6 +54,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. // ===== External Includes ===== // #include // std::find_if +#include // time_t #include #include @@ -269,6 +270,152 @@ namespace OpenXLSX */ void deleteProperty(XLProperty theProperty); + //---------------------------------------------------------------------------------------------------------------------- + // Convenience Property Methods + //---------------------------------------------------------------------------------------------------------------------- + + /** + * @brief Set the document creator (dc:creator) + * @param creator The creator name + */ + void setCreator(const std::string& creator); + + /** + * @brief Get the document creator (dc:creator) + * @return The creator name + */ + std::string creator() const; + + /** + * @brief Set the document title (dc:title) + * @param title The document title + */ + void setTitle(const std::string& title); + + /** + * @brief Get the document title (dc:title) + * @return The document title + */ + std::string title() const; + + /** + * @brief Set the document subject (dc:subject) + * @param subject The document subject + */ + void setSubject(const std::string& subject); + + /** + * @brief Get the document subject (dc:subject) + * @return The document subject + */ + std::string subject() const; + + /** + * @brief Set the document description (dc:description) + * @param description The document description + */ + void setDescription(const std::string& description); + + /** + * @brief Get the document description (dc:description) + * @return The document description + */ + std::string description() const; + + /** + * @brief Set the document keywords (cp:keywords) + * @param keywords The document keywords + */ + void setKeywords(const std::string& keywords); + + /** + * @brief Get the document keywords (cp:keywords) + * @return The document keywords + */ + std::string keywords() const; + + /** + * @brief Set the last modified by (cp:lastModifiedBy) + * @param lastModifiedBy The last modified by name + */ + void setLastModifiedBy(const std::string& lastModifiedBy); + + /** + * @brief Get the last modified by (cp:lastModifiedBy) + * @return The last modified by name + */ + std::string lastModifiedBy() const; + + /** + * @brief Set the document category (cp:category) + * @param category The document category + */ + void setCategory(const std::string& category); + + /** + * @brief Get the document category (cp:category) + * @return The document category + */ + std::string category() const; + + /** + * @brief Set the creation date to the current time (dcterms:created) + */ + void setCreationDateToNow(); + + /** + * @brief Set the creation date to a specific time (dcterms:created) + * @param timestamp The time_t timestamp to set as creation date + */ + void setCreationDate(time_t timestamp); + + /** + * @brief Get the creation date (dcterms:created) + * @return The creation date in W3CDTF format + */ + std::string creationDate() const; + + /** + * @brief Set the modification date to the current time (dcterms:modified) + */ + void setModificationDateToNow(); + + /** + * @brief Set the modification date to a specific time (dcterms:modified) + * @param timestamp The time_t timestamp to set as modification date + */ + void setModificationDate(time_t timestamp); + + /** + * @brief Get the modification date (dcterms:modified) + * @return The modification date in W3CDTF format + */ + std::string modificationDate() const; + + /** + * @brief Set the absolute path (x15ac:absPath) + * @param absPath The absolute path URL to set + */ + void setAbsPath(const std::string& absPath); + + /** + * @brief Get the absolute path (x15ac:absPath) + * @return The absolute path URL + */ + std::string absPath() const; + + /** + * @brief Set the application name (Application) + * @param application The application name + */ + void setApplication(const std::string& application); + + /** + * @brief Get the application name (Application) + * @return The application name + */ + std::string application() const; + /** * @brief * @return diff --git a/OpenXLSX/sources/XLDocument.cpp b/OpenXLSX/sources/XLDocument.cpp index a37bfec2..5104d2f9 100644 --- a/OpenXLSX/sources/XLDocument.cpp +++ b/OpenXLSX/sources/XLDocument.cpp @@ -45,6 +45,9 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. // ===== External Includes ===== // #include +#include +#include +#include #ifdef ENABLE_NOWIDE # include #endif @@ -1011,6 +1014,256 @@ void XLDocument::setProperty(XLProperty prop, const std::string& value) // NO */ void XLDocument::deleteProperty(XLProperty theProperty) { setProperty(theProperty, ""); } +//---------------------------------------------------------------------------------------------------------------------- +// Convenience Property Methods +//---------------------------------------------------------------------------------------------------------------------- + +/** + * @details Set the document creator (dc:creator) + */ +void XLDocument::setCreator(const std::string& creator) +{ + setProperty(XLProperty::Creator, creator); +} + +/** + * @details Get the document creator (dc:creator) + */ +std::string XLDocument::creator() const +{ + return property(XLProperty::Creator); +} + +/** + * @details Set the document title (dc:title) + */ +void XLDocument::setTitle(const std::string& title) +{ + setProperty(XLProperty::Title, title); +} + +/** + * @details Get the document title (dc:title) + */ +std::string XLDocument::title() const +{ + return property(XLProperty::Title); +} + +/** + * @details Set the document subject (dc:subject) + */ +void XLDocument::setSubject(const std::string& subject) +{ + setProperty(XLProperty::Subject, subject); +} + +/** + * @details Get the document subject (dc:subject) + */ +std::string XLDocument::subject() const +{ + return property(XLProperty::Subject); +} + +/** + * @details Set the document description (dc:description) + */ +void XLDocument::setDescription(const std::string& description) +{ + setProperty(XLProperty::Description, description); +} + +/** + * @details Get the document description (dc:description) + */ +std::string XLDocument::description() const +{ + return property(XLProperty::Description); +} + +/** + * @details Set the document keywords (cp:keywords) + */ +void XLDocument::setKeywords(const std::string& keywords) +{ + setProperty(XLProperty::Keywords, keywords); +} + +/** + * @details Get the document keywords (cp:keywords) + */ +std::string XLDocument::keywords() const +{ + return property(XLProperty::Keywords); +} + +/** + * @details Set the last modified by (cp:lastModifiedBy) + */ +void XLDocument::setLastModifiedBy(const std::string& lastModifiedBy) +{ + setProperty(XLProperty::LastModifiedBy, lastModifiedBy); +} + +/** + * @details Get the last modified by (cp:lastModifiedBy) + */ +std::string XLDocument::lastModifiedBy() const +{ + return property(XLProperty::LastModifiedBy); +} + +/** + * @details Set the document category (cp:category) + */ +void XLDocument::setCategory(const std::string& category) +{ + setProperty(XLProperty::Category, category); +} + +/** + * @details Get the document category (cp:category) + */ +std::string XLDocument::category() const +{ + return property(XLProperty::Category); +} + +/** + * @details Set the creation date to the current time (dcterms:created) + */ +void XLDocument::setCreationDateToNow() +{ + setCreationDate(std::time(nullptr)); +} + +/** + * @details Set the creation date to a specific time (dcterms:created) + */ +void XLDocument::setCreationDate(time_t timestamp) +{ + // Convert time_t to GMT tm struct + std::tm tm = *std::gmtime(×tamp); + + // Format as W3CDTF (YYYY-MM-DDTHH:MM:SSZ) + std::ostringstream ss; + ss << std::put_time(&tm, "%FT%TZ"); + std::string datetime = ss.str(); + + setProperty(XLProperty::CreationDate, datetime); +} + +/** + * @details Get the creation date (dcterms:created) + */ +std::string XLDocument::creationDate() const +{ + return property(XLProperty::CreationDate); +} + +/** + * @details Set the modification date to the current time (dcterms:modified) + */ +void XLDocument::setModificationDateToNow() +{ + setModificationDate(std::time(nullptr)); +} + +/** + * @details Set the modification date to a specific time (dcterms:modified) + */ +void XLDocument::setModificationDate(time_t timestamp) +{ + // Convert time_t to GMT tm struct + std::tm tm = *std::gmtime(×tamp); + + // Format as W3CDTF (YYYY-MM-DDTHH:MM:SSZ) + std::ostringstream ss; + ss << std::put_time(&tm, "%FT%TZ"); + std::string datetime = ss.str(); + + setProperty(XLProperty::ModificationDate, datetime); +} + +/** + * @details Get the modification date (dcterms:modified) + */ +std::string XLDocument::modificationDate() const +{ + return property(XLProperty::ModificationDate); +} + +/** + * @details Set the absolute path (x15ac:absPath) + */ +void XLDocument::setAbsPath(const std::string& absPath) +{ + if (m_workbook.xmlDocument().empty()) return; + + XMLNode workbookRoot = m_workbook.xmlDocument().document_element(); + + // Find or create the mc:AlternateContent node + XMLNode alternateContent = workbookRoot.child("mc:AlternateContent"); + if (alternateContent.empty()) { + alternateContent = workbookRoot.append_child("mc:AlternateContent"); + alternateContent.append_attribute("xmlns:mc") = "http://schemas.openxmlformats.org/markup-compatibility/2006"; + } + + // Find or create the mc:Choice node + XMLNode choice = alternateContent.child("mc:Choice"); + if (choice.empty()) { + choice = alternateContent.append_child("mc:Choice"); + choice.append_attribute("Requires") = "x15"; + } + + // Find or create the x15ac:absPath node + XMLNode absPathNode = choice.child("x15ac:absPath"); + if (absPathNode.empty()) { + absPathNode = choice.append_child("x15ac:absPath"); + absPathNode.append_attribute("xmlns:x15ac") = "http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac"; + } + + // Set the url attribute + absPathNode.attribute("url").set_value(absPath.c_str()); +} + +/** + * @details Get the absolute path (x15ac:absPath) + */ +std::string XLDocument::absPath() const +{ + if (m_workbook.xmlDocument().empty()) return ""; + + XMLNode workbookRoot = m_workbook.xmlDocument().document_element(); + XMLNode alternateContent = workbookRoot.child("mc:AlternateContent"); + if (alternateContent.empty()) return ""; + + XMLNode choice = alternateContent.child("mc:Choice"); + if (choice.empty()) return ""; + + XMLNode absPathNode = choice.child("x15ac:absPath"); + if (absPathNode.empty()) return ""; + + return absPathNode.attribute("url").value(); +} + +/** + * @details Set the application name (Application) + */ +void XLDocument::setApplication(const std::string& application) +{ + setProperty(XLProperty::Application, application); +} + +/** + * @details Get the application name (Application) + */ +std::string XLDocument::application() const +{ + return property(XLProperty::Application); +} + /** * @details */ diff --git a/OpenXLSX/sources/XLProperties.cpp b/OpenXLSX/sources/XLProperties.cpp index 2ca4dd89..07ef6892 100644 --- a/OpenXLSX/sources/XLProperties.cpp +++ b/OpenXLSX/sources/XLProperties.cpp @@ -234,8 +234,8 @@ void XLProperties::createFromTemplate() props.append_attribute("xmlns:dcmitype") = "http://purl.org/dc/dcmitype/"; props.append_attribute("xmlns:xsi") = "http://www.w3.org/2001/XMLSchema-instance"; - props.append_child("dc:creator").text().set("Kenneth Balslev"); - props.append_child("cp:lastModifiedBy").text().set("Kenneth Balslev"); + props.append_child("dc:creator").text().set("OpenXLSX"); + props.append_child("cp:lastModifiedBy").text().set("OpenXLSX"); XMLNode prop {}; prop = props.append_child("dcterms:created"); From 6b6700d6fc803b76b4c21363c56472faa1e12999 Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Mon, 13 Oct 2025 00:16:45 -0700 Subject: [PATCH 10/13] Embedded Image Support -- phase IX / bug fixes --- Examples/Demo11.cpp | 650 ++++++++++++--------- OpenXLSX/headers/XLContentTypes.hpp | 14 + OpenXLSX/headers/XLDocument.hpp | 38 +- OpenXLSX/headers/XLDrawingML.hpp | 13 + OpenXLSX/headers/XLImage.hpp | 21 +- OpenXLSX/headers/XLRelationships.hpp | 7 + OpenXLSX/headers/XLSheet.hpp | 78 ++- OpenXLSX/headers/XLXmlData.hpp | 38 ++ OpenXLSX/headers/XLXmlFile.hpp | 7 + OpenXLSX/sources/XLContentTypes.cpp | 24 + OpenXLSX/sources/XLDocument.cpp | 176 +++++- OpenXLSX/sources/XLDrawingML.cpp | 83 ++- OpenXLSX/sources/XLImage.cpp | 99 +++- OpenXLSX/sources/XLRelationships.cpp | 10 + OpenXLSX/sources/XLSheet.cpp | 640 +++++++++++++++++--- OpenXLSX/sources/XLWorkbook.cpp | 4 + OpenXLSX/sources/XLWorksheetImageQuery.cpp | 511 ++++++++++++---- OpenXLSX/sources/XLXmlData.cpp | 232 ++++++++ OpenXLSX/sources/XLXmlFile.cpp | 12 + Tests/testXLCellValue.cpp | 2 +- Tests/testXLImage.cpp | 180 ++++++ 21 files changed, 2305 insertions(+), 534 deletions(-) create mode 100644 Tests/testXLImage.cpp diff --git a/Examples/Demo11.cpp b/Examples/Demo11.cpp index 75d960cb..89d9ef13 100644 --- a/Examples/Demo11.cpp +++ b/Examples/Demo11.cpp @@ -36,7 +36,7 @@ std::string findDirectory(const std::string& relativePath) // Check if the directory exists if (std::filesystem::exists(testImagePath) && std::filesystem::is_directory(testImagePath)) { - result = testImagePath.string() + "/"; + result = testImagePath.string(); } else{ testPath = testPath.parent_path(); @@ -57,7 +57,7 @@ std::string findDirectory(const std::string& relativePath) Return image information in the following format "img_id:ABCD r_id:EFGH oneCellAnchor:C44 330x492 px 1249900x1700000 emu" */ -std::string imageInfoStr( const XLImageInfo& imageInfo ){ +std::string imageInfoStr(const XLImageInfo& imageInfo) { std::string result; // a. get image ID @@ -80,6 +80,139 @@ std::string imageInfoStr( const XLImageInfo& imageInfo ){ return result; } +// Helper function to print image registry information for a worksheet +void printImageRegistry(const XLWorksheet& worksheet, const std::string& sheetName, int* totalImages = nullptr) { + try { + size_t imageCount = worksheet.imageCount(); + + // Update total count if pointer provided + if (totalImages != nullptr) { + *totalImages += static_cast(imageCount); + } + + // Print header + auto imageInfos = worksheet.getImageInfos(); + const OpenXLSX::XLDrawingML& drawing = worksheet.drawingML(); + const size_t drawingImageCount = drawing.imageCount(); + std::cout << " Worksheet '" << sheetName << "': " << imageCount << " image(s) " + << imageInfos.size() << " images in registry" + << " drawing valid:" << (drawing.valid() ? "Yes" : "No") + << " drawing images:" << drawingImageCount + << std::endl; + + // Print image details if any images exist + if (imageCount > 0) { + for (size_t i = 0; i < imageInfos.size(); ++i) { + std::cout << " " << (i + 1) << ". " << imageInfoStr(imageInfos[i]) << std::endl; + } + } + } catch (const std::exception& e) { + std::cout << "Error getting image info for worksheet '" << sheetName << "': " << e.what() << std::endl; + } +} + +// Helper function to print image registry information for all worksheets +// TODO: Is it ok that image registry is empty? +// TODO: Should the image registry be refreshed as part of this query +void printAllImageRegistries(const XLDocument& doc, int* totalImages = nullptr) { + for (uint16_t sheet_idx = 1; sheet_idx <= doc.workbook().sheetCount(); ++sheet_idx) { + XLSheet sheet = doc.workbook().sheet(sheet_idx); + std::string sheetName = sheet.name(); + if (sheet.isType()) { + XLWorksheet worksheet = sheet.get(); + try { + const auto& imageInfos = worksheet.getImageInfos(); + printImageRegistry(worksheet, sheetName, totalImages); + } catch (const std::exception& e) { + std::cout << " ERROR: Failed to query registry: " << e.what() << std::endl; + } + } + } +} + +// Helper function to print detailed image analysis for a worksheet +void printDetailedImageAnalysis(const XLWorksheet& worksheet, const std::string& sheetName) { + int imageCount = static_cast(worksheet.imageCount()); + std::cout << " Worksheet '" << sheetName << "' imageCount: " << imageCount << std::endl; + + if (imageCount > 0) { + try { + const auto& imageInfos = worksheet.getImageInfos(); + std::vector anchorCells; + std::vector imageIds; + std::vector relationshipIds; + for (size_t i = 0; i < imageInfos.size(); ++i) { + std::cout << " " << (i + 1) << ". " << imageInfoStr(imageInfos[i]) << std::endl; + anchorCells.push_back(imageInfos[i].anchorCell); + imageIds.push_back(imageInfos[i].imageId); + relationshipIds.push_back(imageInfos[i].relationshipId); + } + + // Test getImageInfosAtCell() + for( const std::string& anchorCell : anchorCells ){ + const auto& cellImgs = worksheet.getImageInfosAtCell(anchorCell); + std::cout << " getImageInfosAtCell('" << anchorCell << "') returned " + << cellImgs.size() << " image(s)" << std::endl; + } + + // Test getImageInfoByImageID() + for( const std::string& imageId : imageIds ){ + // TODO: Should imageId be unique, or is it ok to just get the first image that matches imageId? + // TODO: If imageId should be unique, getImageInfoByImageID() should, in DEBUG mode, check for uniqueness. + const auto& foundImg = worksheet.getImageInfoByImageID(imageId); + if (foundImg.isValid()) { + std::cout << " getImageInfoByImageID( " << imageId << ") found image" << std::endl; + } else { + std::cout << " getImageInfoByImageID( " << imageId << ") failed to find image" << std::endl; + } + } + + // Test getImageInfoByRelationshipId() + for( const std::string& relationshipId : relationshipIds ){ + // TODO: If relationshipId should be unique, getImageInfoByRelationshipId() should, in DEBUG mode, check for uniqueness. + const auto& foundRel = worksheet.getImageInfoByRelationshipId(relationshipId); + if (foundRel.isValid()) { + std::cout << " ✓ getImageInfoByRelationshipId( " << relationshipId << ") found the image" << std::endl; + } else { + std::cout << " ✗ getImageInfoByRelationshipId( " << relationshipId << ") failed to find the image" << std::endl; + } + } + + // Test getImageInfosInRange() + const auto& rangeImgs = worksheet.getImageInfosInRange("A1:Z100"); + std::cout << " getImageInfosInRange('A1:Z100') returned " << rangeImgs.size() << " image(s)" << std::endl; + + // Test validation + bool isRegstryValid = worksheet.validateImageRegistry(); + std::cout << " validateImageRegistry() returned: " << (isRegstryValid ? "Valid" : "Invalid") << std::endl; + + } catch (const std::exception& e) { + std::cout << " Error getting image info: " << e.what() << std::endl; + } + } +} + +// Data integrity verification functions - checking for data integrity can sometimes be expensive, +// but is handy for debugging +void verifyDocData(const XLDocument& doc) +{ + std::string dbgMsg; + int errorCount = doc.verifyData(&dbgMsg); + if (errorCount > 0 || !dbgMsg.empty()) { + std::cout << "ERROR: Document data integrity check failed with " << errorCount + << " error(s): " << dbgMsg << std::endl; + } +} + +void verifyWorksheetData(const XLWorksheet& worksheet) +{ + std::string dbgMsg; + int errorCount = worksheet.verifyData(&dbgMsg); + if (errorCount > 0 || !dbgMsg.empty()) { + std::cout << "ERROR: Worksheet '" << worksheet.name() << "' data integrity check failed with " << errorCount + << " error(s): " << dbgMsg << std::endl; + } +} // These contain the binary data from the tiny image files in the images directory @@ -474,8 +607,18 @@ int main() { std::cout << "OpenXLSX Image Test" << std::endl; std::cout << "===================" << std::endl; + std::cout << std::endl; + + // Error tracking + int errorCount = 0; + + // ======================================== + // I. CREATE NEW DOCUMENT + // ======================================== + std::cout << "I. CREATE NEW DOCUMENT" << std::endl; + std::cout << "======================" << std::endl; - // Create workbook and document + // Create a new workbook XLDocument doc; std::string xlsxFileName = "Demo11.xlsx"; doc.create(xlsxFileName); @@ -493,7 +636,12 @@ int main() wb.addWorksheet("File Loading"); XLWorksheet fileSheet = wb.worksheet("File Loading"); - std::cout << "\n=== Sheet 1: Anchor Types Comparison ===" << std::endl; + // 1. Sheet 1 (empty) + std::cout << "1. Sheet 1 (empty)" << std::endl; + // Sheet1 is created by default and remains empty + + // 2. Sheet 2: Anchor Types (oneCellAnchor vs twoCellAnchor) + std::cout << "2. Sheet 2: Anchor Types (oneCellAnchor vs twoCellAnchor)" << std::endl; // Test 1: oneCellAnchor vs twoCellAnchor with same image XLImage testImage1(pngData, ImageMimeTypes::PNG); @@ -508,20 +656,28 @@ int main() // Add images bool success1 = anchorSheet.addImage(testImage1, "A2"); std::cout << " Added oneCellAnchor PNG: " << (success1 ? "Success" : "Failed") << std::endl; + if (!success1) ++errorCount; // Add twoCellAnchor image bool success2 = anchorSheet.addImageTwoCellAnchor(testImage1, 8, 1, 10, 4); // A8 to D10 std::cout << " Added twoCellAnchor PNG: " << (success2 ? "Success" : "Failed") << std::endl; + if (!success2) ++errorCount; // Add more twoCellAnchor tests XLImage testImage2(jpegData, ImageMimeTypes::JPEG); bool success3 = anchorSheet.addImage(testImage2, "A14"); std::cout << " Added oneCellAnchor JPEG: " << (success3 ? "Success" : "Failed") << std::endl; + if (!success3) ++errorCount; bool success4 = anchorSheet.addImageTwoCellAnchor(testImage2, 20, 1, 21, 3); // A20 to C21 std::cout << " Added twoCellAnchor JPEG: " << (success4 ? "Success" : "Failed") << std::endl; + if (!success4) ++errorCount; + + // Check data integrity -- typically only done for debugging + verifyWorksheetData(anchorSheet); - std::cout << "\n=== Sheet 2: File Format Testing ===" << std::endl; + // 3. Sheet 3: File Formats (PNG, JPEG, GIF, BMP) + std::cout << "3. Sheet 3: File Formats (PNG, JPEG, GIF, BMP)" << std::endl; // Test different file formats static const std::vector, std::string, std::string>> formats = { @@ -541,9 +697,14 @@ int main() bool success = formatSheet.addImage(formatImage, imageCell); std::cout << " Added " << std::get<1>(formats[i]) << " image: " << (success ? "Success" : "Failed") << std::endl; + if (!success) ++errorCount; } - std::cout << "\n=== Sheet 3: Sizing Strategy Testing ===" << std::endl; + // Check data integrity -- typically only done for debugging + verifyWorksheetData(formatSheet); + + // 4. Sheet 4: Sizing Strategy (aspect ratio, original, exact) + std::cout << "4. Sheet 4: Sizing Strategy (aspect ratio, original, exact)" << std::endl; // Test different sizing strategies XLImage sizingImage(pngData, ImageMimeTypes::PNG); @@ -553,6 +714,7 @@ int main() sizingSheet.cell("A1").value() = "Aspect Ratio + Bounding Box (7x3 cell height units)"; bool success3a = sizingSheet.addImage(sizingImage, "A2"); std::cout << " Aspect ratio + bounding box: " << (success3a ? "Success" : "Failed") << std::endl; + if (!success3a) ++errorCount; // Strategy 2: Original size (no scaling) XLImage originalImage(pngData, ImageMimeTypes::PNG); @@ -560,6 +722,7 @@ int main() sizingSheet.cell("A7").value() = "Original Size (no scaling)"; bool success3b = sizingSheet.addImage(originalImage, "A8"); std::cout << " Original size: " << (success3b ? "Success" : "Failed") << std::endl; + if (!success3b) ++errorCount; // Strategy 3: Force exact dimensions XLImage exactImage(pngData, ImageMimeTypes::PNG); @@ -568,8 +731,13 @@ int main() sizingSheet.cell("A13").value() = "Exact Dimensions (11x3 cell height units, may distort)"; bool success3c = sizingSheet.addImage(exactImage, "A14"); std::cout << " Exact dimensions: " << (success3c ? "Success" : "Failed") << std::endl; + if (!success3c) ++errorCount; - std::cout << "\n=== Sheet 4: Units & Scaling Testing ===" << std::endl; + // Check data integrity -- typically only done for debugging + verifyWorksheetData(sizingSheet); + + // 5. Sheet 5: Units & Scaling Testing (pixels, EMUs, cells) + std::cout << "5. Sheet 5: Units & Scaling Testing" << std::endl; // Test different unit systems XLImage unitsImage(pngData, ImageMimeTypes::PNG); @@ -579,6 +747,7 @@ int main() unitsSheet.cell("A1").value() = "Pixel-based (100x50 pixels)"; bool success4a = unitsSheet.addImage(unitsImage, "A2"); std::cout << " Pixel-based sizing: " << (success4a ? "Success" : "Failed") << std::endl; + if (!success4a) ++errorCount; // EMUs XLImage emuImage(pngData, ImageMimeTypes::PNG); @@ -587,6 +756,7 @@ int main() unitsSheet.cell("A7").value() = "EMU-based (476250x238125 EMUs)"; bool success4b = unitsSheet.addImage(emuImage, "A8"); std::cout << " EMU-based sizing: " << (success4b ? "Success" : "Failed") << std::endl; + if (!success4b) ++errorCount; // Cell spacing XLImage cellImage(pngData, ImageMimeTypes::PNG); @@ -594,8 +764,13 @@ int main() unitsSheet.cell("A13").value() = "Cell-based (19x5 cell height units) -- preserve aspect ratio"; bool success4c = unitsSheet.addImage(cellImage, "A14"); std::cout << " Cell-based sizing: " << (success4c ? "Success" : "Failed") << std::endl; + if (!success4c) ++errorCount; + + // Check data integrity -- typically only done for debugging + verifyWorksheetData(unitsSheet); - std::cout << "\n=== Sheet 5: Loading Images from Disk Files ===" << std::endl; + // 6. Sheet 6: Loading Images from Disk Files + std::cout << "6. Sheet 6: Loading Images from Disk Files" << std::endl; // Find the images directory by searching upward through the directory tree std::string imageDir = findDirectory("OpenXLSX/Examples/images/"); @@ -603,6 +778,7 @@ int main() std::cout << " ERROR: Could not find images directory!" << std::endl; std::cout << " Searched for: OpenXLSX/Examples/images/ in current " "directory and up to 10 levels up" << std::endl; + ++errorCount; } else { std::cout << " Found images directory: " << imageDir << std::endl; } @@ -614,6 +790,7 @@ int main() fileSheet.cell("A1").value() = "PNG - Original Size"; bool fileSuccess1 = fileSheet.addImageFromFile(pngPath, "A2"); std::cout << " PNG original size: " << (fileSuccess1 ? "Success" : "Failed") << std::endl; + if (!fileSuccess1) ++errorCount; // Image 2: JPEG - Aspect ratio preserved (2x1 cells) std::string jpegPath = imageDir + "tiny_jpeg.jpg"; @@ -624,8 +801,10 @@ int main() fileSheet.cell("A7").value() = "JPEG - Aspect Ratio (2x1 cells)"; bool fileSuccess2 = fileSheet.addImage(jpegImage, "A8"); std::cout << " JPEG aspect ratio: " << (fileSuccess2 ? "Success" : "Failed") << std::endl; + if (!fileSuccess2) ++errorCount; } else { std::cout << " JPEG aspect ratio: Failed to load image" << std::endl; + ++errorCount; } // Image 3: GIF - Exact dimensions (may distort) @@ -638,8 +817,10 @@ int main() fileSheet.cell("A13").value() = "GIF - Exact Dimensions (3x2 cells)"; bool fileSuccess3 = fileSheet.addImage(gifImage, "A14"); std::cout << " GIF exact dimensions: " << (fileSuccess3 ? "Success" : "Failed") << std::endl; + if (!fileSuccess3) ++errorCount; } else { std::cout << " GIF exact dimensions: Failed to load image" << std::endl; + ++errorCount; } // Image 4: BMP - Two-cell anchor spanning multiple cells @@ -651,8 +832,10 @@ int main() fileSheet.cell("A19").value() = "BMP - Two-Cell Anchor (A20:C22)"; bool fileSuccess4 = fileSheet.addImageTwoCellAnchor(bmpImage, 20, 1, 22, 3); // A20 to C22 std::cout << " BMP two-cell anchor: " << (fileSuccess4 ? "Success" : "Failed") << std::endl; + if (!fileSuccess4) ++errorCount; } else { std::cout << " BMP two-cell anchor: Failed to load image" << std::endl; + ++errorCount; } // Image 5: PNG - Precise positioning with offset (using previously unused addImageWithOffset) @@ -669,8 +852,10 @@ int main() int32_t rowOffset = EXCEL_CELL_Y_SPACING_EMUS / 2; // 50% of cell height bool fileSuccess5 = fileSheet.addImageWithOffset(offsetImage, 26, 1, rowOffset, colOffset); std::cout << " PNG with offset: " << (fileSuccess5 ? "Success" : "Failed") << std::endl; + if (!fileSuccess5) ++errorCount; } else { std::cout << " PNG with offset: Failed to load image" << std::endl; + ++errorCount; } // Image 6: JPEG - Larger size with offset (demonstrates variety in offset sizing) @@ -687,45 +872,28 @@ int main() int32_t rowOffset2 = EXCEL_CELL_Y_SPACING_EMUS / 4; // 25% of cell height bool fileSuccess6 = fileSheet.addImageWithOffset(offsetImage2, 32, 1, rowOffset2, colOffset2); std::cout << " JPEG large with offset: " << (fileSuccess6 ? "Success" : "Failed") << std::endl; + if (!fileSuccess6) ++errorCount; } else { std::cout << " JPEG large with offset: Failed to load image" << std::endl; + ++errorCount; } - // Note: The addImageFromFile() function loads images at their original size - // To apply different sizing strategies, we would need to: - // 1. Load the image with addImageFromFile() - // 2. Get the XLImage object from the worksheet - // 3. Modify its display size - // 4. Re-add it with the new size - // - // For now, this demonstrates that the file loading functionality works - // and creates a foundation for more advanced sizing operations. + // Check data integrity -- typically only done for debugging + verifyWorksheetData(fileSheet); } // End of file loading tests - std::cout << "\n=== Inspect Image Registry ===" << std::endl; - for (uint16_t sheet_idx = 1; sheet_idx <= doc.workbook().sheetCount(); ++sheet_idx) { - XLSheet sheet = doc.workbook().sheet(sheet_idx); - std::string sheetName = sheet.name(); - if (sheet.isType()) { - XLWorksheet worksheet = sheet.get(); - try { - const auto& imageInfos = worksheet.getImageInfos(); - std::cout << " sheet " << sheetName << " now has " << imageInfos.size() << " images in registry" << std::endl; - for (const auto& imgInfo : imageInfos) { - std::cout << " " << imageInfoStr( imgInfo ) << std::endl; - } - } - catch (const std::exception& e) { - std::cout << " ERROR: Failed to query registry: " << e.what() << std::endl; - } - } - } + // Check data integrity -- typically only done for debugging + verifyDocData(doc); + // 7. Inspect Image Registry + std::cout << "7. Inspect Image Registry" << std::endl; + printAllImageRegistries(doc); - // Save the workbook - std::cout << "\nSaving workbook as '" << xlsxFileName << "'..." << std::endl; - - // Set document properties before saving + // Check data integrity -- typically only done for debugging + verifyDocData(doc); + + // 8. Set document properties + std::cout << "8. Set document properties" << std::endl; doc.setCreator("OpenXLSX Demo11"); doc.setTitle("Comprehensive Image Embedding Test Matrix"); doc.setSubject("Testing various image embedding options in Excel"); @@ -737,48 +905,53 @@ int main() doc.setCreationDateToNow(); doc.setModificationDateToNow(); doc.setAbsPath(""); - + + // Check data integrity -- typically only done for debugging + verifyDocData(doc); + + // Display the properties that were set + std::cout << " Creator: " << doc.creator() << std::endl; + std::cout << " Title: " << doc.title() << std::endl; + std::cout << " Subject: " << doc.subject() << std::endl; + std::cout << " Description: " << doc.description() << std::endl; + std::cout << " Keywords: " << doc.keywords() << std::endl; + std::cout << " Last Modified By: " << doc.lastModifiedBy() << std::endl; + std::cout << " Category: " << doc.category() << std::endl; + std::cout << " Application: " << doc.application() << std::endl; + std::cout << " Creation Date: " << doc.creationDate() << std::endl; + std::cout << " Modification Date: " << doc.modificationDate() << std::endl; + + // 9. Save the workbook + std::cout << "9. Save" << std::endl; + std::cout << " Saving workbook as '" << xlsxFileName << "'..." << std::endl; try { doc.save(); - std::cout << "Workbook saved successfully." << std::endl; - - // Display the properties that were set - std::cout << "\nDocument properties set:" << std::endl; - std::cout << " Creator: " << doc.creator() << std::endl; - std::cout << " Title: " << doc.title() << std::endl; - std::cout << " Subject: " << doc.subject() << std::endl; - std::cout << " Description: " << doc.description() << std::endl; - std::cout << " Keywords: " << doc.keywords() << std::endl; - std::cout << " Last Modified By: " << doc.lastModifiedBy() << std::endl; - std::cout << " Category: " << doc.category() << std::endl; - std::cout << " Application: " << doc.application() << std::endl; - std::cout << " Creation Date: " << doc.creationDate() << std::endl; - std::cout << " Modification Date: " << doc.modificationDate() << std::endl; + std::cout << " Workbook saved successfully." << std::endl; + } catch (const std::exception& e) { - std::cout << "Error saving workbook: " << e.what() << std::endl; + std::cout << " Error saving workbook: " << e.what() << std::endl; return 1; } - std::cout << "\nTest Matrix Summary:" << std::endl; - std::cout << "- Sheet 1: Anchor Types (oneCellAnchor vs twoCellAnchor)" << std::endl; - std::cout << "- Sheet 2: File Formats (PNG, JPEG, GIF, BMP)" << std::endl; - std::cout << "- Sheet 3: Sizing Strategies (aspect ratio, original, exact)" << std::endl; - std::cout << "- Sheet 4: Units & Scaling (pixels, EMUs, cells)" << std::endl; - std::cout << "- Sheet 5: File Loading (loading images from disk files)" << std::endl; - - // ================================================================================ - // PHASE VI: READ-MODIFY-WRITE CYCLE - // ================================================================================ - std::cout << "\n=== Phase VI: Read-Modify-Write Cycle ===" << std::endl; - - // Step 1: Read back the .xlsx file we just created - std::cout << "\n1. Reading back the .xlsx file..." << std::endl; + + // ======================================== + // II. READ-MODIFY-WRITE + // ======================================== + std::cout << std::endl; + std::cout << "II. READ-MODIFY-WRITE" << std::endl; + std::cout << "====================" << std::endl; + + // 1. Read back the .xlsx file + std::cout << "1. Read back the .xlsx file" << std::endl; OpenXLSX::XLDocument readDoc; try { readDoc.open(xlsxFileName); std::cout << " Successfully opened: " << xlsxFileName << std::endl; - // Step 2: Compare original and read documents for debugging - std::cout << "\n2. Comparing original and read documents for debugging..." << std::endl; + // Check data integrity -- typically only done for debugging + verifyDocData(readDoc); + + // 2. Compare to original + std::cout << "2. Compare to original" << std::endl; // Compare the original doc (that was saved) with the readDoc (that was loaded) std::string diffMsg; @@ -791,54 +964,22 @@ int main() std::cout << " Differences: " << diffMsg << std::endl; } - // Step 3: Search for embedded images in all worksheets - std::cout << "\n3. Searching for embedded images..." << std::endl; + // 3. Inspect Image Registry + std::cout << "3. Inspect Image Registry" << std::endl; int totalImagesFound = 0; std::vector worksheetsWithImages; for (const auto& sheetName : readDoc.workbook().worksheetNames()) { OpenXLSX::XLWorksheet sheet = readDoc.workbook().worksheet(sheetName); - - // Check if worksheet has images by checking if it has DrawingML with actual images - // (hasImages() only checks in-memory m_images vector, which is empty for loaded files) - try { - OpenXLSX::XLDrawingML& drawing = sheet.drawingML(); - if (drawing.valid()) { - // Use the public imageCount() method to check for images - size_t imageCount = drawing.imageCount(); - std::cout << " Worksheet '" << sheetName << "' DrawingML valid: " << (drawing.valid() ? "Yes" : "No") - << ", imageCount: " << imageCount << std::endl; - if (imageCount > 0) { - worksheetsWithImages.push_back(sheetName); - std::cout << " Found " << imageCount << " image(s) in worksheet: " << sheetName << std::endl; - totalImagesFound++; - - // Iterate through images and print their information - try { - auto imageInfos = sheet.getImageInfos(); - for (size_t i = 0; i < imageInfos.size(); ++i) { - std::cout << " " << (i + 1) << ". " << imageInfoStr(imageInfos[i]) << std::endl; - } - } catch (const std::exception& e) { - std::cout << " Error getting image info: " << e.what() << std::endl; - } - } - } else { - std::cout << " Worksheet '" << sheetName << "' has no DrawingML" << std::endl; - } - } catch (const std::exception& e) { - std::cout << " Worksheet '" << sheetName << "' DrawingML error: " << e.what() << std::endl; - } - } - - std::cout << " Total worksheets with images: " << totalImagesFound << std::endl; + printImageRegistry(sheet, sheetName, &totalImagesFound); + } + std::cout << " Total images: " << totalImagesFound << std::endl; - // Step 3b: Compare documents AFTER search operation (to test lazy initialization hypothesis) - std::cout << "\n3b. Comparing documents AFTER search operation..." << std::endl; + // 4. Compare to original, after search + std::cout << "4. Compare to original, after search" << std::endl; std::string diffMsgAfterSearch; int resultAfterSearch = doc.compare(readDoc, &diffMsgAfterSearch); - if (resultAfterSearch == 0) { std::cout << " ✓ Documents still identical after search" << std::endl; } else { @@ -846,51 +987,39 @@ int main() std::cout << " Differences: " << diffMsgAfterSearch << std::endl; } - // Step 4: Adding and removing images in 'File Loading' worksheet - std::cout << "\n4. Adding and removing images in 'File Loading' worksheet..." << std::endl; + // 5. Remove images + std::cout << "5. Remove images" << std::endl; std::string newImageId = ""; // Declare outside the if block for use in verification summary + OpenXLSX::XLWorksheet fileLoadingSheet; // Declare outside the if block if (readDoc.workbook().worksheetExists("File Loading")) { - OpenXLSX::XLWorksheet fileLoadingSheet = readDoc.workbook().worksheet("File Loading"); - std::cout << " Found 'File Loading' worksheet" << std::endl; - - // First, let's see what images we currently have - std::cout << " Current images in worksheet:" << std::endl; - try { - const auto& currentImages = fileLoadingSheet.getImageInfos(); - for (size_t i = 0; i < currentImages.size(); ++i) { - std::cout << " " << (i + 1) << ". " << imageInfoStr(currentImages[i]) << std::endl; - } - } catch (const std::exception& e) { - std::cout << " Error getting current images: " << e.what() << std::endl; - } + fileLoadingSheet = readDoc.workbook().worksheet("File Loading"); // Remove image 3 by Image ID and image 4 by Relationship ID (testing both methods) - std::cout << "\n Removing image 3 by Image ID and image 4 by Relationship ID..." << std::endl; - bool removedImg3 = fileLoadingSheet.removeImageByImageID("img3"); // Third image (A14) - bool removedImg4 = fileLoadingSheet.removeImageByRelationshipId("rId5"); // Fourth image (A20) - std::cout << " Removed img3 (by Image ID): " << (removedImg3 ? "Success" : "Failed") << std::endl; - std::cout << " Removed img4 (by Relationship ID): " << (removedImg4 ? "Success" : "Failed") << std::endl; + std::cout << " Removing image 3 by Image ID and image 4 by Relationship ID..." << std::endl; + bool removedImage3 = fileLoadingSheet.removeImageByImageID("3"); // Third image (A14) + bool removedImage4 = fileLoadingSheet.removeImageByRelationshipId("rId5"); // Fourth image (A20) + std::cout << " Removed image 3 (by Image ID): " << (removedImage3 ? "Success" : "Failed") << std::endl; + if (!removedImage3) ++errorCount; + std::cout << " Removed image 4 (by Relationship ID): " << (removedImage4 ? "Success" : "Failed") << std::endl; + if (!removedImage4) ++errorCount; // Set "REMOVED" text above the cells where images were removed - fileLoadingSheet.cell("B13").value() = (removedImg3) ? "REMOVED" : "FAILED TO REMOVE"; // Near img3 at A14 - fileLoadingSheet.cell("B19").value() = (removedImg4) ? "REMOVED" : "FAILED TO REMOVE"; // Near img4 at A20 + fileLoadingSheet.cell("B13").value() = (removedImage3) ? "REMOVED" : "FAILED TO REMOVE"; // Near image 3 at A14 + fileLoadingSheet.cell("B19").value() = (removedImage4) ? "REMOVED" : "FAILED TO REMOVE"; // Near image 4 at A20 - // Verify the images are removed by checking the registry - try { - const auto& imageInfos = fileLoadingSheet.getImageInfos(); - std::cout << " DEBUG: Registry now has " << imageInfos.size() << " images after removal" << std::endl; - for (const auto& imgInfo : imageInfos) { - std::cout << " " << imageInfoStr( imgInfo ) << std::endl; - } - }catch (const std::exception& e) { - std::cout << " ERROR: Failed to verify image removal: " << e.what() << std::endl; - } + // Check data integrity -- typically only done for debugging + verifyWorksheetData(fileLoadingSheet); + + // 6. Inspect Image Registry + std::cout << "6. Inspect Image Registry" << std::endl; + printImageRegistry(fileLoadingSheet, "File Loading"); - // Add a new image - std::cout << "\n Adding new image..." << std::endl; + // 7. Add image + std::cout << "7. Add image" << std::endl; OpenXLSX::XLImage newImage; + // TODO: should generateNextImageId() return a unique image ID for the worksheet? newImageId = fileLoadingSheet.generateNextImageId(); std::cout << " Generated new image ID: " << newImageId << std::endl; @@ -907,13 +1036,14 @@ int main() fileLoadingSheet.cell("A37").value() = "NEW IMAGE - Added via Read-Modify-Write Cycle"; std::cout << " Set cell A37 text" << std::endl; - // Debug: Check if image is valid before adding + // Check if image is valid before adding std::cout << " Image valid: " << (newImage.isValid() ? "Yes" : "No") << std::endl; std::cout << " Image data size: " << newImage.data().size() << " bytes" << std::endl; std::cout << " Image MIME type: " << newImage.mimeType() << std::endl; bool addSuccess = fileLoadingSheet.addImage(newImage, "A38"); std::cout << " addImage() returned: " << (addSuccess ? "Success" : "Failed") << std::endl; + if (!addSuccess) ++errorCount; std::cout << " Image ID after addImage(): " << newImage.id() << std::endl; if (addSuccess) { @@ -923,14 +1053,13 @@ int main() // Verify the image was actually added by checking the registry try { + // Check if the new image was added const auto& imageInfos = fileLoadingSheet.getImageInfos(); - std::cout << " DEBUG: Registry now has " << imageInfos.size() << " images after addition" << std::endl; bool foundNewImage = false; for (const auto& imgInfo : imageInfos) { - std::cout << " " << imageInfoStr( imgInfo ) << std::endl; if (imgInfo.imageId == newImageId) { foundNewImage = true; - std::cout << " DEBUG: Found new image in registry: " << imageInfoStr(imgInfo) << std::endl; + std::cout << " Found new image in registry: " << imageInfoStr(imgInfo) << std::endl; } } if (!foundNewImage) { @@ -941,28 +1070,23 @@ int main() } } else { std::cout << " Failed to add new image to worksheet" << std::endl; + ++errorCount; } } else { std::cout << " Failed to create new image from data" << std::endl; - } - - // Show final state - std::cout << "\n Final images in worksheet:" << std::endl; - try { - const auto& finalImages = fileLoadingSheet.getImageInfos(); - for (size_t i = 0; i < finalImages.size(); ++i) { - std::cout << " " << (i + 1) << ". " << imageInfoStr(finalImages[i]) << std::endl; - } - std::cout << " Total images after modifications: " << finalImages.size() << std::endl; - } catch (const std::exception& e) { - std::cout << " Error getting final images: " << e.what() << std::endl; + ++errorCount; } } else { std::cout << " 'File Loading' worksheet not found!" << std::endl; + ++errorCount; } - // Step 4b: Compare documents AFTER adding and removing images - std::cout << "\n4b. Comparing documents AFTER adding and removing images..." << std::endl; + // Check data integrity -- typically only done for debugging + verifyWorksheetData(fileLoadingSheet); + + // 8. Compare to original, after modification + // TODO: XLDocument::compare() should compare list m_data / std::unique_ptr m_xmlDoc / xml_node/type()/name()/value() + std::cout << "8. Compare to original, after modification" << std::endl; std::string diffMsgAfterModify; int resultAfterModify = doc.compare(readDoc, &diffMsgAfterModify); @@ -973,9 +1097,21 @@ int main() std::cout << " Differences: " << diffMsgAfterModify << std::endl; } - // Step 5: Write the modified workbook to a new file - std::cout << "\n5. Writing modified workbook to new file..." << std::endl; - std::string modifiedFileName = "Demo11_Modified.xlsx"; + // 9. Inspect Image Registry + std::cout << "9. Inspect Image Registry" << std::endl; + try { + if (readDoc.workbook().worksheetExists("File Loading")) { + printImageRegistry(fileLoadingSheet, "File Loading"); + std::cout << " Total images after modifications: " << fileLoadingSheet.imageCount() << std::endl; + } else { + std::cout << " 'File Loading' worksheet not found!" << std::endl; + } + } catch (const std::exception& e) { + std::cout << " Error getting final images: " << e.what() << std::endl; + } + + // 10. Set Document properties + std::cout << "10. Set Document properties" << std::endl; // Set document properties for the modified version readDoc.setCreator("OpenXLSX Demo11 - Modified"); @@ -987,45 +1123,44 @@ int main() readDoc.setCategory("Test Document - Modified"); readDoc.setApplication("OpenXLSX Demo11 Application - Modified"); readDoc.setModificationDateToNow(); // Update modification date for the modified version - + + // Display the properties that were set for the modified version + std::cout << " Creator: " << readDoc.creator() << std::endl; + std::cout << " Title: " << readDoc.title() << std::endl; + std::cout << " Subject: " << readDoc.subject() << std::endl; + std::cout << " Keywords: " << readDoc.keywords() << std::endl; + std::cout << " Last Modified By: " << readDoc.lastModifiedBy() << std::endl; + std::cout << " Category: " << readDoc.category() << std::endl; + std::cout << " Application: " << readDoc.application() << std::endl; + std::cout << " Modification Date: " << readDoc.modificationDate() << std::endl; + + // Check data integrity -- typically only done for debugging + verifyDocData(readDoc); + + // 11. Save modified file + std::string modifiedFileName = "Demo11_Modified.xlsx"; + std::cout << "11. Save modified file: " << modifiedFileName << std::endl; try { readDoc.saveAs(modifiedFileName); - std::cout << " Modified workbook saved as: " << modifiedFileName << std::endl; - - // Display the properties that were set for the modified version - std::cout << "\n Modified document properties:" << std::endl; - std::cout << " Creator: " << readDoc.creator() << std::endl; - std::cout << " Title: " << readDoc.title() << std::endl; - std::cout << " Subject: " << readDoc.subject() << std::endl; - std::cout << " Keywords: " << readDoc.keywords() << std::endl; - std::cout << " Last Modified By: " << readDoc.lastModifiedBy() << std::endl; - std::cout << " Category: " << readDoc.category() << std::endl; - std::cout << " Application: " << readDoc.application() << std::endl; - std::cout << " Modification Date: " << readDoc.modificationDate() << std::endl; } catch (const std::exception& e) { std::cout << " Error saving modified workbook: " << e.what() << std::endl; } - // Step 6: Verify the modification - std::cout << "\n6. Verification summary:" << std::endl; - std::cout << " - Original file: " << xlsxFileName << std::endl; - std::cout << " - Modified file: " << modifiedFileName << std::endl; - std::cout << " - Worksheets with images: " << totalImagesFound << std::endl; - std::cout << " - Images removed from 'File Loading': img3, img4" << std::endl; - if (!newImageId.empty()) { - std::cout << " - New image added to 'File Loading': " << newImageId << std::endl; - } else { - std::cout << " - New image added to 'File Loading': (none - worksheet not found)" << std::endl; - } - readDoc.close(); } catch (const std::exception& e) { std::cout << " Error during read-modify-write cycle: " << e.what() << std::endl; } - /* read file generated by Excel. Count the images in the file. */ - std::cout << "\n=== Phase VII: Loading Excel-Generated File ===" << std::endl; + // ======================================== + // III. READ EXCEL-GENERATED FILE + // ======================================== + std::cout << std::endl; + std::cout << "III. READ EXCEL-GENERATED FILE" << std::endl; + std::cout << "=============================" << std::endl; + + // 1. Read file + std::cout << "1. Read file" << std::endl; // Find the Excel-generated file std::string excelFileDir = findDirectory("OpenXLSX/Examples/files/"); @@ -1035,6 +1170,7 @@ int main() std::cout << " ERROR: Could not find Excel-generated file!" << std::endl; std::cout << " Expected: " << excelFilePath << std::endl; std::cout << " Searched for: OpenXLSX/Examples/files/ in current directory and up to 10 levels up" << std::endl; + ++errorCount; } else { std::cout << " Found Excel file: " << excelFilePath << std::endl; @@ -1042,107 +1178,63 @@ int main() XLDocument excelDoc; try { excelDoc.open(excelFilePath); - std::cout << " Successfully opened Excel-generated file" << std::endl; - - // Count images in each worksheet - int totalImages = 0; - for (uint16_t i = 1; i <= excelDoc.workbook().sheetCount(); ++i) { - XLSheet sheet = excelDoc.workbook().sheet(i); - std::string sheetName = sheet.name(); - - // Check if it's a worksheet (not a chartsheet) - if (sheet.isType()) { - XLWorksheet worksheet = sheet.get(); - int imageCount = static_cast(worksheet.imageCount()); - totalImages += imageCount; - std::cout << " Worksheet '" << sheetName << "': " << imageCount << " image(s)" << std::endl; - // Iterate through images and print their information - if (imageCount > 0) { - try { - auto imageInfos = worksheet.getImageInfos(); - for (size_t i = 0; i < imageInfos.size(); ++i) { - std::cout << " " << (i + 1) << ". " << imageInfoStr(imageInfos[i]) << std::endl; - } - } catch (const std::exception& e) { - std::cout << " Error getting image info: " << e.what() << std::endl; - } - } + // Check data integrity -- typically only done for debugging + verifyDocData(excelDoc); - } else { - std::cout << " Sheet '" << sheetName << "': (not a worksheet, skipping)" << std::endl; - } - } + // 2. Inspect Image Registry + std::cout << "2. Inspect Image Registry" << std::endl; + int totalImages = 0; + printAllImageRegistries(excelDoc, &totalImages); std::cout << " Total images in Excel file: " << totalImages << std::endl; - - excelDoc.close(); } catch (const std::exception& e) { std::cout << " ERROR: Failed to open Excel file: " << e.what() << std::endl; + ++errorCount; } + + // Display the properties that were set for the Excel-generated file + std::cout << "3. Properties" << std::endl; + std::cout << " Creator: " << excelDoc.creator() << std::endl; + std::cout << " Title: " << excelDoc.title() << std::endl; + std::cout << " Subject: " << excelDoc.subject() << std::endl; + std::cout << " Keywords: " << excelDoc.keywords() << std::endl; + std::cout << " Last Modified By: " << excelDoc.lastModifiedBy() << std::endl; + std::cout << " Category: " << excelDoc.category() << std::endl; + std::cout << " Application: " << excelDoc.application() << std::endl; + std::cout << " Modification Date: " << excelDoc.modificationDate() << std::endl; } - /* Test the new image query functionality */ - std::cout << "\n=== Phase VIII: Testing Image Query Functionality ===" << std::endl; + // ======================================== + // IV. IMAGE QUERY + // ======================================== + std::cout << std::endl; + std::cout << "IV. IMAGE QUERY" << std::endl; + std::cout << "===============" << std::endl; + + // 1. Read file + std::cout << "1. Read file" << std::endl; // Re-open the Demo11 file to test image queries - try { XLDocument testDoc; + try { testDoc.open("Demo11.xlsx"); std::cout << " Successfully opened Demo11.xlsx for query testing" << std::endl; + // Check data integrity -- typically only done for debugging + verifyDocData(testDoc); + + // 2. Queries + std::cout << "2. Queries" << std::endl; + // Test each worksheet with images for (uint16_t i = 1; i <= testDoc.workbook().sheetCount(); ++i) { XLSheet sheet = testDoc.workbook().sheet(i); std::string sheetName = sheet.name(); if (sheet.isType()) { + std::cout << std::endl; XLWorksheet worksheet = sheet.get(); - int imageCount = static_cast(worksheet.imageCount()); - std::cout << " DEBUG: Worksheet '" << sheetName << "' imageCount: " << imageCount << std::endl; - - if (imageCount > 0) { - std::cout << "\n Worksheet '" << sheetName << "' has " << imageCount << " image(s):" << std::endl; - std::cout << " DEBUG: About to call getImageInfos() for worksheet '" << sheetName << "'" << std::endl; - std::cout << " DEBUG: Calling getImageInfos() now..." << std::endl; - - // Test getImageInfos() - const auto& imageInfos = worksheet.getImageInfos(); - std::cout << " DEBUG: getImageInfos() completed, returned " << imageInfos.size() << " image(s)" << std::endl; - - // Test individual image queries - for (size_t i = 0; i < imageInfos.size(); ++i) { - const auto& imgInfo = imageInfos[i]; - std::cout << " " << (i + 1) << ". " << imageInfoStr(imgInfo) << std::endl; - - // Test getImageInfoByImageID() - const auto& foundImg = worksheet.getImageInfoByImageID(imgInfo.imageId); - if (foundImg.isValid()) { - std::cout << " ✓ getImageInfoByImageID() found the image" << std::endl; - } else { - std::cout << " ✗ getImageInfoByImageID() failed to find the image" << std::endl; - } - - // Test getImageInfoByRelationshipId() - const auto& foundRel = worksheet.getImageInfoByRelationshipId(imgInfo.relationshipId); - if (foundRel.isValid()) { - std::cout << " ✓ getImageInfoByRelationshipId() found the image" << std::endl; - } else { - std::cout << " ✗ getImageInfoByRelationshipId() failed to find the image" << std::endl; - } - - // Test getImageInfosAtCell() - const auto& cellImgs = worksheet.getImageInfosAtCell(imgInfo.anchorCell); - std::cout << " getImageInfosAtCell('" << imgInfo.anchorCell << "') returned " << cellImgs.size() << " image(s)" << std::endl; - } - - // Test range queries - const auto& rangeImgs = worksheet.getImageInfosInRange("A1:Z100"); - std::cout << " getImageInfosInRange('A1:Z100') returned " << rangeImgs.size() << " image(s)" << std::endl; - - // Test validation - bool isValid = worksheet.validateImageRegistry(); - std::cout << " validateImageRegistry() returned: " << (isValid ? "Valid" : "Invalid") << std::endl; - } + printDetailedImageAnalysis(worksheet, sheetName); } } @@ -1151,7 +1243,21 @@ int main() } catch (const std::exception& e) { std::cout << " ERROR: Failed to test image queries: " << e.what() << std::endl; + ++errorCount; + } + + // Print final error summary + std::cout << std::endl; + std::cout << "=========================================" << std::endl; + std::cout << "FINAL ERROR SUMMARY" << std::endl; + std::cout << "=========================================" << std::endl; + std::cout << "Total errors encountered: " << errorCount << std::endl; + if (errorCount == 0) { + std::cout << "All operations completed successfully!" << std::endl; + } else { + std::cout << "Some operations failed. Check the output above for details." << std::endl; } + std::cout << "=========================================" << std::endl; return 0; } diff --git a/OpenXLSX/headers/XLContentTypes.hpp b/OpenXLSX/headers/XLContentTypes.hpp index 0b40b331..1c6355fa 100644 --- a/OpenXLSX/headers/XLContentTypes.hpp +++ b/OpenXLSX/headers/XLContentTypes.hpp @@ -242,6 +242,13 @@ namespace OpenXLSX */ void addOverride(const std::string& path, XLContentType type); + /** + * @brief Check if an override with the given path already exists + * @param path The path to check for + * @return true if override exists, false otherwise + */ + bool hasOverride(const std::string& path) const; + /** * @brief * @param path @@ -267,6 +274,13 @@ namespace OpenXLSX */ std::vector getContentItems(); + /** + * @brief Verify internal data integrity and class invariants + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + int verifyData(std::string* dbgMsg = nullptr) const; + private: // ---------- Private Member Variables ---------- // }; } // namespace OpenXLSX diff --git a/OpenXLSX/headers/XLDocument.hpp b/OpenXLSX/headers/XLDocument.hpp index 7af74f45..d2065274 100644 --- a/OpenXLSX/headers/XLDocument.hpp +++ b/OpenXLSX/headers/XLDocument.hpp @@ -496,6 +496,13 @@ namespace OpenXLSX */ XLTables sheetTables(uint16_t sheetXmlNo); + /** + * @brief Verify internal data integrity and class invariants + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + int verifyData(std::string* dbgMsg = nullptr) const; + public: /** * @brief validate whether sheetName is a valid Excel worksheet name @@ -522,6 +529,13 @@ namespace OpenXLSX */ int compare(const XLDocument& other, std::string* diffMsg = nullptr) const; + /** + * @brief Verify XML data integrity for all XML files in m_data + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + int verifyXLXmlData(std::string* dbgMsg = nullptr) const; + /** * @brief Add a drawing file to the archive * @param drawingFilename The filename for the drawing in the archive @@ -537,6 +551,21 @@ namespace OpenXLSX */ XLXmlData* getDrawingXmlData(const std::string& drawingFilename); + /** + * @brief Get XML data for a relationships file + * @param relsFilename The filename for the relationships file in the archive + * @return Pointer to XLXmlData, or nullptr if not found + */ + XLXmlData* getRelationshipsXmlData(const std::string& relsFilename); + + /** + * @brief Update relationships XML data in-memory without archive access + * @param relsFilename The filename for the relationships file + * @param relsXml The relationships XML content + * @return true if successful, false otherwise + */ + bool updateRelationshipsXmlData(const std::string& relsFilename, const std::string& relsXml); + /** * @brief Add a relationships file to the archive * @param relsFilename The filename for the relationships file in the archive @@ -573,6 +602,13 @@ namespace OpenXLSX */ bool deleteEntry(const std::string& entryPath); + /** + * @brief Check if a binary file is referenced by any relationships in any worksheet + * @param imagePath The relative image path (e.g., "../media/image_3.gif") + * @return True if the file is referenced by at least one relationship in any worksheet + */ + bool isBinaryFileReferenced(const std::string& imagePath) const; + /** * @brief * @param command @@ -676,8 +712,6 @@ namespace OpenXLSX XLStyles m_styles {}; /**< A pointer to the document styles object*/ XLWorkbook m_workbook {}; /**< A pointer to the workbook object */ IZipArchive m_archive {}; /**< */ - - mutable uint32_t m_globalImageCounter {0}; /**< Global counter for unique image IDs across all worksheets */ }; diff --git a/OpenXLSX/headers/XLDrawingML.hpp b/OpenXLSX/headers/XLDrawingML.hpp index d6e9e665..423618c8 100644 --- a/OpenXLSX/headers/XLDrawingML.hpp +++ b/OpenXLSX/headers/XLDrawingML.hpp @@ -85,6 +85,19 @@ namespace OpenXLSX */ size_t imageCount() const; + /** + * @brief Verify internal data integrity and class invariants + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + int verifyData(std::string* dbgMsg = nullptr) const; + + /** + * @brief Get the XML root node for verification purposes + * @return XML root node (empty if invalid) + */ + XMLNode getRootNode() const; + private: // ===== Private Methods ===== // /** diff --git a/OpenXLSX/headers/XLImage.hpp b/OpenXLSX/headers/XLImage.hpp index acbc7c4c..bbb9f26e 100644 --- a/OpenXLSX/headers/XLImage.hpp +++ b/OpenXLSX/headers/XLImage.hpp @@ -188,15 +188,17 @@ namespace OpenXLSX // ========== Global Helper Functions ========== // /** - * @brief Helper function to append difference messages for debugging - * @param diffMsg Optional pointer to append difference message (up to 16384 characters) + * @brief Helper function to append debug messages for debugging + * @param dbgMsg Optional pointer to append debug message (up to 16384 characters) * @param msg The message to append */ - inline void appendDiff(std::string* diffMsg, const std::string& msg) + inline void appendDbgMsg(std::string* dbgMsg, const std::string& msg) { - if (diffMsg && diffMsg->length() < 16000) { // Leave some buffer - if (!diffMsg->empty()) *diffMsg += "; "; - *diffMsg += msg; + if (dbgMsg && dbgMsg->length() < 16000) { // Leave some buffer + if (!dbgMsg->empty()){ + dbgMsg->append("\n "); + } + dbgMsg->append(msg); } } @@ -491,6 +493,13 @@ namespace OpenXLSX */ int compare(const XLImage& other, std::string* diffMsg = nullptr) const; + /** + * @brief Verify internal data integrity and class invariants + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + int verifyData(std::string* dbgMsg = nullptr) const; + private: std::vector m_imageData; /**< Binary image data */ std::string m_mimeType; /**< MIME type of the image */ diff --git a/OpenXLSX/headers/XLRelationships.hpp b/OpenXLSX/headers/XLRelationships.hpp index 43a29888..fba60078 100644 --- a/OpenXLSX/headers/XLRelationships.hpp +++ b/OpenXLSX/headers/XLRelationships.hpp @@ -402,6 +402,13 @@ namespace OpenXLSX { */ void print(std::basic_ostream& ostr) const; + /** + * @brief Verify internal data integrity and class invariants + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + int verifyData(std::string* dbgMsg = nullptr) const; + // ---------- Protected Member Functions ---------- // protected: diff --git a/OpenXLSX/headers/XLSheet.hpp b/OpenXLSX/headers/XLSheet.hpp index 0b792935..edd4eeef 100644 --- a/OpenXLSX/headers/XLSheet.hpp +++ b/OpenXLSX/headers/XLSheet.hpp @@ -1437,18 +1437,16 @@ namespace OpenXLSX /** * @brief Get all image information at a specific cell * @param cellRef The cell reference (e.g., "A1", "B5") - * @return Const reference to vector of XLImageInfo objects at that cell - * @note Returns a const reference for efficiency - no copying of the vector + * @return Vector of XLImageInfo objects at that cell */ - const std::vector& getImageInfosAtCell(const std::string& cellRef) const; + std::vector getImageInfosAtCell(const std::string& cellRef) const; /** * @brief Get all image information in a specific range * @param cellRange The cell range (e.g., "A1:B5", "C10:D20") - * @return Const reference to vector of XLImageInfo objects in that range - * @note Returns a const reference for efficiency - no copying of the vector + * @return Vector of XLImageInfo objects in that range */ - const std::vector& getImageInfosInRange(const std::string& cellRange) const; + std::vector getImageInfosInRange(const std::string& cellRange) const; /** * @brief Get iterator to the beginning of the image information collection @@ -1514,7 +1512,7 @@ namespace OpenXLSX * @param relationshipId The relationship ID of the image file to remove * @return True if the file was found and removed from archive */ - bool removeImageFileFromArchive(const std::string& relationshipId) const; + bool removeImageFileFromArchiveIfUnreferenced(const std::string& relationshipId) const; /** * @brief Get image path from relationship ID @@ -1528,7 +1526,7 @@ namespace OpenXLSX * @param imagePath The relative image path (e.g., "../media/image_img3.png") * @return True if the file was found and removed */ - bool removeImageFileByPath(const std::string& imagePath) const; + bool removeImageFileIfUnreferenced(const std::string& imagePath) const; public: //---------------------------------------------------------------------------------------------------------------------- @@ -1543,6 +1541,14 @@ namespace OpenXLSX */ void refreshImageRegistry() const; + /** + * @brief Populate m_images vector from existing XML data + * @note This method calls refreshImageRegistry() to get registry data, + * then converts registry entries to XLImage objects and populates m_images. + * Only populates if m_images is empty to avoid duplicates. + */ + void populateImagesFromXML(); + /** * @brief Validate image registry for data integrity * @return True if registry is valid (no duplicate IDs), false otherwise @@ -1598,11 +1604,11 @@ namespace OpenXLSX void processTwoCellAnchor(const pugi::xml_node& anchor, const std::map& relationshipMap) const; /** - * @brief Extract image ID from image file path - * @param imagePath The image file path (e.g., "../media/image_img1.png") - * @return The image ID (e.g., "img1") + * @brief Extract image ID from XML pic element + * @param pic The pic XML node containing the image information + * @return The image ID from the cNvPr id attribute */ - std::string extractImageIdFromPath(const std::string& imagePath) const; + std::string extractImageIdFromXML(const pugi::xml_node& pic) const; /** * @brief Convert EMUs to pixels (approximate conversion) @@ -1718,6 +1724,49 @@ namespace OpenXLSX */ bool setActive_impl(); + public: + /** + * @brief Get all image IDs in this worksheet + * @return Vector of all image ID strings, sorted and unique (duplicates removed) + */ + std::vector getImageIDs() const; + + /** + * @brief Verify internal data integrity and class invariants + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + int verifyData(std::string* dbgMsg = nullptr) const; + + /** + * @brief Verify that all image IDs are unique within this worksheet + * @param dbgMsg Optional pointer to append debug message explaining any duplicate IDs found + * @return Number of duplicate IDs found (0 = all IDs are unique) + */ + int verifyUniqueImageIDs(std::string* dbgMsg = nullptr) const; + + /** + * @brief Verify DrawingML XML consistency with image registry data + * @param dbgMsg Optional pointer to append debug message explaining any inconsistencies found + * @return Number of inconsistencies found (0 = XML and registry are consistent) + */ + int verifyDrawingMLConsistency(std::string* dbgMsg = nullptr) const; + + /** + * @brief Verify m_images vector consistency with DrawingML XML data + * @param dbgMsg Optional pointer to append debug message explaining any inconsistencies found + * @return Number of inconsistencies found (0 = m_images and XML are consistent) + */ + int verifyImagesVectorConsistency(std::string* dbgMsg = nullptr) const; + + /** + * @brief Verify that binary image data exists in the archive + * @param dbgMsg Optional pointer to append debug message explaining any issues found + * @return Number of issues found (0 = all referenced image files exist in archive) + */ + int verifyBinaryImageData(std::string* dbgMsg = nullptr) const; + bool isBinaryFileReferenced(const std::string& imagePath) const; + private: // ---------- Private Member Variables ---------- // XLRelationships m_relationships{}; /**< class handling the worksheet relationships */ XLMergeCells m_merges{}; /**< class handling the */ @@ -1727,8 +1776,7 @@ namespace OpenXLSX XLTables m_tables{}; /**< class handling the worksheet table settings */ std::vector m_images{}; /**< vector storing images in the worksheet */ mutable std::vector m_imageRegistry{}; /**< vector storing parsed image metadata for query operations */ - mutable XLImageInfo m_emptyImageInfo{}; /**< empty image info for returning when image not found */ - mutable std::vector m_emptyImageInfoVector{}; /**< empty image info vector for returning when no images found */ + static const XLImageInfo m_emptyImageInfo; /**< empty image info for returning when image not found */ const std::vector< std::string_view >& m_nodeOrder = XLWorksheetNodeOrder; // worksheet XML root node required child sequence }; @@ -1983,6 +2031,8 @@ namespace OpenXLSX if (worksheet.hasImagesInXML()) { try { std::ignore = worksheet.drawingML(); // Initialize DrawingML + // Populate m_images vector from existing XML data + worksheet.populateImagesFromXML(); } catch (...) { // DrawingML initialization failed, but worksheet is still usable } diff --git a/OpenXLSX/headers/XLXmlData.hpp b/OpenXLSX/headers/XLXmlData.hpp index 3e2446c3..fabb193f 100644 --- a/OpenXLSX/headers/XLXmlData.hpp +++ b/OpenXLSX/headers/XLXmlData.hpp @@ -232,6 +232,44 @@ namespace OpenXLSX */ bool empty() const; + /** + * @brief Verify internal data integrity and class invariants + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + int verifyData(std::string* dbgMsg = nullptr) const; + + /** + * @brief Verify unique XML records across the entire XML tree + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + int verifyUniqueXMLRecords(std::string* dbgMsg = nullptr) const; + + /** + * @brief Compare two XML nodes for ordering + * @param x First XML node to compare + * @param y Second XML node to compare + * @return -1 if x < y, 0 if x == y, 1 if x > y + */ + static int compareXMLNode(const pugi::xml_node& x, const pugi::xml_node& y); + + /** + * @brief Less-than comparison for XML nodes (for use with std::sort) + * @param x First XML node to compare + * @param y Second XML node to compare + * @return true if x < y, false otherwise + */ + static bool lessXMLNode(const pugi::xml_node& x, const pugi::xml_node& y); + + /** + * @brief Verify unique XML records recursively starting from a root node + * @param rootNode The root XML node to start verification from + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + static int verifyUniqueXMLRecordsRecursive(const pugi::xml_node& rootNode, std::string* dbgMsg = nullptr); + private: // ===== PRIVATE MEMBER VARIABLES ===== // diff --git a/OpenXLSX/headers/XLXmlFile.hpp b/OpenXLSX/headers/XLXmlFile.hpp index 09280586..e6da5a02 100644 --- a/OpenXLSX/headers/XLXmlFile.hpp +++ b/OpenXLSX/headers/XLXmlFile.hpp @@ -182,6 +182,13 @@ namespace OpenXLSX */ std::string getXmlPath() const; + /** + * @brief Verify internal data integrity and class invariants + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + int verifyData(std::string* dbgMsg = nullptr) const; + protected: // ===== PROTECTED MEMBER VARIABLES XLXmlData* m_xmlData { nullptr }; /**< The underlying XML data object. */ }; diff --git a/OpenXLSX/sources/XLContentTypes.cpp b/OpenXLSX/sources/XLContentTypes.cpp index 417ed86a..55bbb349 100644 --- a/OpenXLSX/sources/XLContentTypes.cpp +++ b/OpenXLSX/sources/XLContentTypes.cpp @@ -282,6 +282,11 @@ XLContentTypes& XLContentTypes::operator=(XLContentTypes&& other) noexcept = def */ void XLContentTypes::addOverride(const std::string& path, XLContentType type) { + // Check if Override already exists to prevent duplicates + if (hasOverride(path)) { + return; // Don't add duplicate + } + const std::string typeString = GetStringFromType(type); XMLNode lastOverride = xmlDocument().document_element().last_child_of_type(pugi::node_element); // see if there's a last element @@ -309,6 +314,15 @@ void XLContentTypes::addOverride(const std::string& path, XLContentType type) node.append_attribute("ContentType").set_value(typeString.c_str()); } +/** + * @details Check if an override with the given path already exists + */ +bool XLContentTypes::hasOverride(const std::string& path) const +{ + XMLNode existingOverride = xmlDocument().document_element().find_child_by_attribute("PartName", path.c_str()); + return !existingOverride.empty(); +} + /** * @details */ @@ -344,3 +358,13 @@ std::vector XLContentTypes::getContentItems() return result; } + +int XLContentTypes::verifyData(std::string* dbgMsg) const +{ + int errorCount = 0; + + // Call base class verifyData() which calls m_xmlData->verifyData() + errorCount += XLXmlFile::verifyData(dbgMsg); + + return errorCount; +} diff --git a/OpenXLSX/sources/XLDocument.cpp b/OpenXLSX/sources/XLDocument.cpp index 5104d2f9..7c1f71ba 100644 --- a/OpenXLSX/sources/XLDocument.cpp +++ b/OpenXLSX/sources/XLDocument.cpp @@ -1504,6 +1504,56 @@ XLXmlData* XLDocument::getDrawingXmlData(const std::string& drawingFilename) return getXmlData(drawingFilename, true); // Force loading the XML data } +/** + * @details Get XML data for a relationships file + */ +XLXmlData* XLDocument::getRelationshipsXmlData(const std::string& relsFilename) +{ + XLXmlData* result = getXmlData(relsFilename, true); // Force loading the XML data + + if (!result) { + // Try to load from archive if not in cache + if (hasRelationshipsFile(relsFilename)) { + std::string relsContent = readRelationshipsFile(relsFilename); + if (!relsContent.empty()) { + // Add to m_data cache + m_data.emplace_back(this, relsFilename, relsContent, XLContentType::Relationships); + result = &m_data.back(); + } + } + } + + return result; +} + +/** + * @details Update relationships XML data in-memory without archive access + */ +bool XLDocument::updateRelationshipsXmlData(const std::string& relsFilename, const std::string& relsXml) +{ + try { + // Get existing XLXmlData or create new one + XLXmlData* relsXmlData = getRelationshipsXmlData(relsFilename); + + if (!relsXmlData) { + // Create new XLXmlData entry if it doesn't exist + m_data.emplace_back(this, relsFilename, "", XLContentType::Relationships); + relsXmlData = &m_data.back(); + } + + // Update the XML data using setRawData() + relsXmlData->setRawData(relsXml); + + // Add content type override if needed + m_contentTypes.addOverride("/" + relsFilename, XLContentType::Relationships); + + return true; + } + catch (const std::exception&) { + return false; + } +} + /** * @details Add a relationships file to the archive */ @@ -1583,6 +1633,30 @@ bool XLDocument::deleteEntry(const std::string& entryPath) return false; } +bool XLDocument::isBinaryFileReferenced(const std::string& imagePath) const +{ + try { + // Get all worksheet names + auto sheetNames = m_workbook.worksheetNames(); + + // Check each worksheet's relationships + for (const auto& sheetName : sheetNames) { + // Use const_cast to access non-const worksheet() method + auto worksheet = const_cast(this)->m_workbook.worksheet(sheetName); + + // Check if this worksheet references the binary file + if (worksheet.isBinaryFileReferenced(imagePath)) { + return true; // Found a reference, early exit + } + } + + return false; // No references found in any worksheet + } + catch (const std::exception&) { + return false; // Error occurred, assume not referenced + } +} + /** * @details return value defaults to true, false only where the XLCommandType implements it */ @@ -1919,7 +1993,7 @@ void XLDocument::loadAllXmlFilesFromArchive() if (path.length() > 0 && path[0] == '/') { path = path.substr(1); } - + // Skip if already loaded if (hasXmlData(path)) { continue; @@ -1929,7 +2003,14 @@ void XLDocument::loadAllXmlFilesFromArchive() bool shouldLoad = false; XLContentType contentType = item.type(); - if (path.length() >= 4 && path.substr(path.length() - 4) == ".xml") { + // Extract file extension + std::string extension = ""; + size_t dotPos = path.find_last_of('.'); + if (dotPos != std::string::npos) { + extension = path.substr(dotPos); + } + + if (extension == ".xml" || extension == ".rels") { // Load drawing files and other safe XML files switch (contentType) { case XLContentType::Drawing: @@ -1937,6 +2018,7 @@ void XLDocument::loadAllXmlFilesFromArchive() case XLContentType::Comments: case XLContentType::Table: case XLContentType::VMLDrawing: + case XLContentType::Relationships: shouldLoad = true; break; default: @@ -1969,7 +2051,6 @@ void XLDocument::loadAllXmlFilesFromArchive() */ XLXmlData* XLDocument::getXmlData(const std::string& path, bool doNotThrow) { - // avoid duplication of code: use const_cast to invoke the const function overload and return a non-const value return const_cast(const_cast(this)->getXmlData(path, doNotThrow)); } @@ -2143,14 +2224,6 @@ namespace OpenXLSX return ((result.length() > 1 || result.front() != '/') && path.back() == '/') ? result + "/" : result; } - /** - * @details Generate the next globally unique image ID for this document - */ - std::string XLDocument::generateNextImageId() const - { - return "img" + std::to_string(++m_globalImageCounter); - } - /** * @details Compare two documents for debugging */ @@ -2162,7 +2235,7 @@ namespace OpenXLSX std::string otherPath = other.path(); int pathCompare = thisPath.compare(otherPath); if (pathCompare != 0) { - appendDiff(diffMsg, "document path differs: '" + thisPath + "' vs '" + otherPath + "'"); + appendDbgMsg(diffMsg, "document path differs: '" + thisPath + "' vs '" + otherPath + "'"); return pathCompare; } @@ -2173,7 +2246,7 @@ namespace OpenXLSX auto otherSheetNames = other.m_workbook.sheetNames(); if (thisSheetNames.size() != otherSheetNames.size()) { - appendDiff(diffMsg, "worksheet count differs: " + std::to_string(thisSheetNames.size()) + + appendDbgMsg(diffMsg, "worksheet count differs: " + std::to_string(thisSheetNames.size()) + " vs " + std::to_string(otherSheetNames.size())); return thisSheetNames.size() < otherSheetNames.size() ? -1 : 1; } @@ -2181,29 +2254,28 @@ namespace OpenXLSX // Compare each worksheet for (size_t i = 0; i < thisSheetNames.size(); ++i) { if (thisSheetNames[i] != otherSheetNames[i]) { - appendDiff(diffMsg, "worksheet name differs at index " + std::to_string(i) + + appendDbgMsg(diffMsg, "worksheet name differs at index " + std::to_string(i) + ": '" + thisSheetNames[i] + "' vs '" + otherSheetNames[i] + "'"); return thisSheetNames[i].compare(otherSheetNames[i]); } // Compare worksheet content try { - // Use const_cast to access non-const worksheet method - XLWorksheet thisSheet = const_cast(this)->m_workbook.worksheet(thisSheetNames[i]); - XLWorksheet otherSheet = const_cast(&other)->m_workbook.worksheet(otherSheetNames[i]); + const XLWorksheet thisSheet = const_cast(this)->m_workbook.worksheet(thisSheetNames[i]); + const XLWorksheet otherSheet = const_cast(&other)->m_workbook.worksheet(otherSheetNames[i]); int sheetCompare = thisSheet.compare(otherSheet, diffMsg); if (sheetCompare != 0) { - appendDiff(diffMsg, "worksheet '" + thisSheetNames[i] + "' differs"); + appendDbgMsg(diffMsg, "worksheet '" + thisSheetNames[i] + "' differs"); return sheetCompare; } } catch (const std::exception&) { - appendDiff(diffMsg, "error comparing worksheet '" + thisSheetNames[i] + "'"); + appendDbgMsg(diffMsg, "error comparing worksheet '" + thisSheetNames[i] + "'"); return -1; } } } else if (m_workbook.valid() != other.m_workbook.valid()) { - appendDiff(diffMsg, "workbook validity differs: " + std::string(m_workbook.valid() ? "valid" : "invalid") + + appendDbgMsg(diffMsg, "workbook validity differs: " + std::string(m_workbook.valid() ? "valid" : "invalid") + " vs " + std::string(other.m_workbook.valid() ? "valid" : "invalid")); return m_workbook.valid() ? 1 : -1; } @@ -2214,7 +2286,7 @@ namespace OpenXLSX bool thisDocRelValid = m_docRelationships.valid(); bool otherDocRelValid = other.m_docRelationships.valid(); if (thisDocRelValid != otherDocRelValid) { - appendDiff(diffMsg, "document relationships validity differs: " + std::string(thisDocRelValid ? "valid" : "invalid") + + appendDbgMsg(diffMsg, "document relationships validity differs: " + std::string(thisDocRelValid ? "valid" : "invalid") + " vs " + std::string(otherDocRelValid ? "valid" : "invalid")); return thisDocRelValid ? 1 : -1; } @@ -2222,12 +2294,12 @@ namespace OpenXLSX bool thisWbkRelValid = m_wbkRelationships.valid(); bool otherWbkRelValid = other.m_wbkRelationships.valid(); if (thisWbkRelValid != otherWbkRelValid) { - appendDiff(diffMsg, "workbook relationships validity differs: " + std::string(thisWbkRelValid ? "valid" : "invalid") + + appendDbgMsg(diffMsg, "workbook relationships validity differs: " + std::string(thisWbkRelValid ? "valid" : "invalid") + " vs " + std::string(otherWbkRelValid ? "valid" : "invalid")); return thisWbkRelValid ? 1 : -1; } } catch (const std::exception&) { - appendDiff(diffMsg, "error comparing document relationships"); + appendDbgMsg(diffMsg, "error comparing document relationships"); return -1; } @@ -2237,12 +2309,12 @@ namespace OpenXLSX bool thisContentTypeValid = m_contentTypes.valid(); bool otherContentTypeValid = other.m_contentTypes.valid(); if (thisContentTypeValid != otherContentTypeValid) { - appendDiff(diffMsg, "content types validity differs: " + std::string(thisContentTypeValid ? "valid" : "invalid") + + appendDbgMsg(diffMsg, "content types validity differs: " + std::string(thisContentTypeValid ? "valid" : "invalid") + " vs " + std::string(otherContentTypeValid ? "valid" : "invalid")); return thisContentTypeValid ? 1 : -1; } } catch (const std::exception&) { - appendDiff(diffMsg, "error comparing content types"); + appendDbgMsg(diffMsg, "error comparing content types"); return -1; } @@ -2256,7 +2328,59 @@ namespace OpenXLSX // - Workbook relationships (m_wbkRelationships) // Documents are identical - return 0; + return 0; +} + +int XLDocument::verifyXLXmlData(std::string* dbgMsg) const +{ + int errorCount = 0; + + // Check each XLXmlData object for duplicates using verifyData() + for (const auto& xmlData : m_data) { + errorCount += xmlData.verifyData(dbgMsg); + } + + return errorCount; +} + +int XLDocument::verifyData(std::string* dbgMsg) const + { + int errorCount = 0; + + // Verify sub-objects that have verifyData() implemented + errorCount += m_contentTypes.verifyData(dbgMsg); + errorCount += m_docRelationships.verifyData(dbgMsg); + errorCount += m_wbkRelationships.verifyData(dbgMsg); + + // Verify all XML files in m_data (complete XML forest coverage) + errorCount += verifyXLXmlData(dbgMsg); + + // TODO: Add verifyData() methods to remaining classes + // errorCount += m_workbook.verifyData(dbgMsg); + // errorCount += m_coreProperties.verifyData(dbgMsg); + // errorCount += m_appProperties.verifyData(dbgMsg); + // errorCount += m_styles.verifyData(dbgMsg); + // errorCount += m_sharedStrings.verifyData(dbgMsg); + + + if (m_workbook.valid()) { + try { + auto sheetNames = m_workbook.worksheetNames(); + for (const auto& sheetName : sheetNames) { + const XLWorksheet worksheet = const_cast(this)->m_workbook.worksheet(sheetName); + int worksheetErrors = worksheet.verifyData(dbgMsg); + if (worksheetErrors > 0) { + appendDbgMsg(dbgMsg, "worksheet '" + sheetName + "' has " + std::to_string(worksheetErrors) + " errors"); + errorCount += worksheetErrors; + } + } + } catch (const std::exception&) { + appendDbgMsg(dbgMsg, "error accessing worksheets"); + errorCount++; + } + } + + return errorCount; } } // namespace OpenXLSX diff --git a/OpenXLSX/sources/XLDrawingML.cpp b/OpenXLSX/sources/XLDrawingML.cpp index 66448678..94c97726 100644 --- a/OpenXLSX/sources/XLDrawingML.cpp +++ b/OpenXLSX/sources/XLDrawingML.cpp @@ -49,8 +49,9 @@ namespace OpenXLSX // Create non-visual picture properties XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); - cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); // Start from 2 - cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); + // Use the actual image ID directly (now numeric to match Microsoft Excel) + cNvPr.append_attribute("id").set_value(image.id().c_str()); + cNvPr.append_attribute("name").set_value(("Picture " + image.id()).c_str()); // Add extension list with creation ID for better compatibility // XMLNode cNvPrExtLst = cNvPr.append_child("a:extLst"); @@ -199,8 +200,9 @@ namespace OpenXLSX // Create non-visual picture properties XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); - cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); - cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); + // Use the actual image ID directly (now numeric to match Microsoft Excel) + cNvPr.append_attribute("id").set_value(image.id().c_str()); + cNvPr.append_attribute("name").set_value(("Picture " + image.id()).c_str()); XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); @@ -280,8 +282,9 @@ namespace OpenXLSX // Create non-visual picture properties XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); - cNvPr.append_attribute("id").set_value(std::to_string(imageCount() + 2).c_str()); - cNvPr.append_attribute("name").set_value(("Picture " + std::to_string(imageCount() + 1)).c_str()); + // Use the actual image ID directly (now numeric to match Microsoft Excel) + cNvPr.append_attribute("id").set_value(image.id().c_str()); + cNvPr.append_attribute("name").set_value(("Picture " + image.id()).c_str()); XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); @@ -318,4 +321,72 @@ namespace OpenXLSX // Add clientData element (required for Excel compatibility) anchor.append_child("xdr:clientData"); } + + int XLDrawingML::verifyData(std::string* dbgMsg) const + { + int errorCount = 0; + + // Check if XML is valid + if (!valid()) { + appendDbgMsg(dbgMsg, "DrawingML XML is invalid"); + errorCount++; + return errorCount; + } + + try { + // Check XML structure + XMLNode rootNode = xmlDocument().document_element(); + if (rootNode.empty()) { + appendDbgMsg(dbgMsg, "DrawingML root node is empty"); + errorCount++; + } + + // Count images in XML and verify structure + size_t xmlImageCount = 0; + for (auto anchor : rootNode.children()) { + if (anchor.name() == std::string("xdr:oneCellAnchor") || + anchor.name() == std::string("xdr:twoCellAnchor")) { + xmlImageCount++; + + // Check for required elements + if (anchor.child("xdr:pic").empty()) { + appendDbgMsg(dbgMsg, "image anchor missing xdr:pic element"); + errorCount++; + } + + if (anchor.child("xdr:clientData").empty()) { + appendDbgMsg(dbgMsg, "image anchor missing xdr:clientData element"); + errorCount++; + } + } + } + + // Verify image count consistency + size_t reportedCount = imageCount(); + if (xmlImageCount != reportedCount) { + appendDbgMsg(dbgMsg, "XML image count (" + std::to_string(xmlImageCount) + + ") does not match reported count (" + std::to_string(reportedCount) + ")"); + errorCount++; + } + + } catch (const std::exception&) { + appendDbgMsg(dbgMsg, "error parsing DrawingML XML"); + errorCount++; + } + + return errorCount; + } + + XMLNode XLDrawingML::getRootNode() const + { + if (!valid()) { + return XMLNode(); // Return empty node if invalid + } + + try { + return xmlDocument().document_element(); + } catch (const std::exception&) { + return XMLNode(); // Return empty node on error + } + } } diff --git a/OpenXLSX/sources/XLImage.cpp b/OpenXLSX/sources/XLImage.cpp index aa092a75..081d2f77 100644 --- a/OpenXLSX/sources/XLImage.cpp +++ b/OpenXLSX/sources/XLImage.cpp @@ -475,53 +475,53 @@ namespace OpenXLSX // Compare image ID (most important for identification) int idCompare = imageId.compare(other.imageId); if (idCompare != 0) { - appendDiff(diffMsg, "image ID differs: '" + imageId + "' vs '" + other.imageId + "'"); + appendDbgMsg(diffMsg, "image ID differs: '" + imageId + "' vs '" + other.imageId + "'"); return idCompare; } // Compare relationship ID int relIdCompare = relationshipId.compare(other.relationshipId); if (relIdCompare != 0) { - appendDiff(diffMsg, "relationship ID differs: '" + relationshipId + "' vs '" + other.relationshipId + "'"); + appendDbgMsg(diffMsg, "relationship ID differs: '" + relationshipId + "' vs '" + other.relationshipId + "'"); return relIdCompare; } // Compare anchor cell int cellCompare = anchorCell.compare(other.anchorCell); if (cellCompare != 0) { - appendDiff(diffMsg, "anchor cell differs: '" + anchorCell + "' vs '" + other.anchorCell + "'"); + appendDbgMsg(diffMsg, "anchor cell differs: '" + anchorCell + "' vs '" + other.anchorCell + "'"); return cellCompare; } // Compare anchor type int typeCompare = anchorType.compare(other.anchorType); if (typeCompare != 0) { - appendDiff(diffMsg, "anchor type differs: '" + anchorType + "' vs '" + other.anchorType + "'"); + appendDbgMsg(diffMsg, "anchor type differs: '" + anchorType + "' vs '" + other.anchorType + "'"); return typeCompare; } // Compare dimensions if (widthPixels != other.widthPixels) { - appendDiff(diffMsg, "width differs: " + std::to_string(widthPixels) + + appendDbgMsg(diffMsg, "width differs: " + std::to_string(widthPixels) + " vs " + std::to_string(other.widthPixels) + " pixels"); return widthPixels < other.widthPixels ? -1 : 1; } if (heightPixels != other.heightPixels) { - appendDiff(diffMsg, "height differs: " + std::to_string(heightPixels) + + appendDbgMsg(diffMsg, "height differs: " + std::to_string(heightPixels) + " vs " + std::to_string(other.heightPixels) + " pixels"); return heightPixels < other.heightPixels ? -1 : 1; } // Compare display dimensions if (displayWidthEMUs != other.displayWidthEMUs) { - appendDiff(diffMsg, "display width differs: " + std::to_string(displayWidthEMUs) + + appendDbgMsg(diffMsg, "display width differs: " + std::to_string(displayWidthEMUs) + " vs " + std::to_string(other.displayWidthEMUs) + " EMUs"); return displayWidthEMUs < other.displayWidthEMUs ? -1 : 1; } if (displayHeightEMUs != other.displayHeightEMUs) { - appendDiff(diffMsg, "display height differs: " + std::to_string(displayHeightEMUs) + + appendDbgMsg(diffMsg, "display height differs: " + std::to_string(displayHeightEMUs) + " vs " + std::to_string(other.displayHeightEMUs) + " EMUs"); return displayHeightEMUs < other.displayHeightEMUs ? -1 : 1; } @@ -538,7 +538,7 @@ namespace OpenXLSX // Compare image data (most important for embedded images) if (m_imageData != other.m_imageData) { - appendDiff(diffMsg, "image data differs (size: " + std::to_string(m_imageData.size()) + + appendDbgMsg(diffMsg, "image data differs (size: " + std::to_string(m_imageData.size()) + " vs " + std::to_string(other.m_imageData.size()) + " bytes)"); return m_imageData.size() < other.m_imageData.size() ? -1 : 1; } @@ -546,46 +546,46 @@ namespace OpenXLSX // Compare MIME type int mimeCompare = m_mimeType.compare(other.m_mimeType); if (mimeCompare != 0) { - appendDiff(diffMsg, "MIME type differs: '" + m_mimeType + "' vs '" + other.m_mimeType + "'"); + appendDbgMsg(diffMsg, "MIME type differs: '" + m_mimeType + "' vs '" + other.m_mimeType + "'"); return mimeCompare; } // Compare extension int extCompare = m_extension.compare(other.m_extension); if (extCompare != 0) { - appendDiff(diffMsg, "extension differs: '" + m_extension + "' vs '" + other.m_extension + "'"); + appendDbgMsg(diffMsg, "extension differs: '" + m_extension + "' vs '" + other.m_extension + "'"); return extCompare; } // Compare ID int idCompare = m_id.compare(other.m_id); if (idCompare != 0) { - appendDiff(diffMsg, "image ID differs: '" + m_id + "' vs '" + other.m_id + "'"); + appendDbgMsg(diffMsg, "image ID differs: '" + m_id + "' vs '" + other.m_id + "'"); return idCompare; } // Compare dimensions if (m_widthPixels != other.m_widthPixels) { - appendDiff(diffMsg, "width differs: " + std::to_string(m_widthPixels) + + appendDbgMsg(diffMsg, "width differs: " + std::to_string(m_widthPixels) + " vs " + std::to_string(other.m_widthPixels) + " pixels"); return m_widthPixels < other.m_widthPixels ? -1 : 1; } if (m_heightPixels != other.m_heightPixels) { - appendDiff(diffMsg, "height differs: " + std::to_string(m_heightPixels) + + appendDbgMsg(diffMsg, "height differs: " + std::to_string(m_heightPixels) + " vs " + std::to_string(other.m_heightPixels) + " pixels"); return m_heightPixels < other.m_heightPixels ? -1 : 1; } // Compare display dimensions if (m_displayWidth != other.m_displayWidth) { - appendDiff(diffMsg, "display width differs: " + std::to_string(m_displayWidth) + + appendDbgMsg(diffMsg, "display width differs: " + std::to_string(m_displayWidth) + " vs " + std::to_string(other.m_displayWidth) + " EMUs"); return m_displayWidth < other.m_displayWidth ? -1 : 1; } if (m_displayHeight != other.m_displayHeight) { - appendDiff(diffMsg, "display height differs: " + std::to_string(m_displayHeight) + + appendDbgMsg(diffMsg, "display height differs: " + std::to_string(m_displayHeight) + " vs " + std::to_string(other.m_displayHeight) + " EMUs"); return m_displayHeight < other.m_displayHeight ? -1 : 1; } @@ -594,4 +594,71 @@ namespace OpenXLSX return 0; } + /** + * @details Verify internal data integrity and class invariants + */ + int XLImage::verifyData(std::string* dbgMsg) const + { + int errorCount = 0; + + // Check basic data integrity + if (m_imageData.empty()) { + appendDbgMsg(dbgMsg, "image data is empty"); + errorCount++; + } + + // Check MIME type consistency + if (m_mimeType.empty()) { + appendDbgMsg(dbgMsg, "MIME type is empty"); + errorCount++; + } + + // Check extension consistency + if (m_extension.empty()) { + appendDbgMsg(dbgMsg, "file extension is empty"); + errorCount++; + } + + // Check ID consistency + if (m_id.empty()) { + appendDbgMsg(dbgMsg, "image ID is empty"); + errorCount++; + } + + // Check dimension consistency + if (m_widthPixels == 0) { + appendDbgMsg(dbgMsg, "width in pixels is zero"); + errorCount++; + } + + if (m_heightPixels == 0) { + appendDbgMsg(dbgMsg, "height in pixels is zero"); + errorCount++; + } + + // Check display dimension consistency + if (m_displayWidth == 0) { + appendDbgMsg(dbgMsg, "display width in EMUs is zero"); + errorCount++; + } + + if (m_displayHeight == 0) { + appendDbgMsg(dbgMsg, "display height in EMUs is zero"); + errorCount++; + } + + // Check MIME type and extension consistency + if (!m_mimeType.empty() && !m_extension.empty()) { + if ((m_mimeType == "image/png" && m_extension != ".png") || + (m_mimeType == "image/jpeg" && m_extension != ".jpg" && m_extension != ".jpeg") || + (m_mimeType == "image/gif" && m_extension != ".gif") || + (m_mimeType == "image/bmp" && m_extension != ".bmp")) { + appendDbgMsg(dbgMsg, "MIME type '" + m_mimeType + "' does not match extension '" + m_extension + "'"); + errorCount++; + } + } + + return errorCount; + } + } // namespace OpenXLSX diff --git a/OpenXLSX/sources/XLRelationships.cpp b/OpenXLSX/sources/XLRelationships.cpp index fe37deb1..8fa2f3d1 100644 --- a/OpenXLSX/sources/XLRelationships.cpp +++ b/OpenXLSX/sources/XLRelationships.cpp @@ -466,3 +466,13 @@ bool XLRelationships::idExists(const std::string& id) const * @details Print the underlying XML using pugixml::xml_node::print */ void XLRelationships::print(std::basic_ostream& ostr) const { xmlDocument().document_element().print(ostr); } + +int XLRelationships::verifyData(std::string* dbgMsg) const +{ + int errorCount = 0; + + // Call base class verifyData() which calls m_xmlData->verifyData() + errorCount += XLXmlFile::verifyData(dbgMsg); + + return errorCount; +} diff --git a/OpenXLSX/sources/XLSheet.cpp b/OpenXLSX/sources/XLSheet.cpp index 5571dac9..f1ed1c7b 100644 --- a/OpenXLSX/sources/XLSheet.cpp +++ b/OpenXLSX/sources/XLSheet.cpp @@ -996,6 +996,8 @@ XLWorksheet::XLWorksheet(const XLWorksheet& other) : XLSheetBase(ot m_drawingML = other.m_drawingML; // " XLDrawingML m_comments = other.m_comments; // " XLComments " m_tables = other.m_tables; // " XLTables " + m_images = other.m_images; // " std::vector" + m_imageRegistry = other.m_imageRegistry; // " std::vector" } /** @@ -1009,6 +1011,8 @@ XLWorksheet::XLWorksheet(XLWorksheet&& other) : XLSheetBase< XLWorksheet >(other m_drawingML = std::move(other.m_drawingML); // " XLDrawingML m_comments = std::move(other.m_comments); // " XLComments " m_tables = std::move(other.m_tables); // " XLTables " + m_images = std::move(other.m_images); // " std::vector" + m_imageRegistry = std::move(other.m_imageRegistry); // " std::vector" } /** @@ -1023,6 +1027,8 @@ XLWorksheet& XLWorksheet::operator=(const XLWorksheet& other) m_drawingML = other.m_drawingML; m_comments = other.m_comments; m_tables = other.m_tables; + m_images = other.m_images; + m_imageRegistry = other.m_imageRegistry; return *this; } @@ -1038,6 +1044,8 @@ XLWorksheet& XLWorksheet::operator=(XLWorksheet&& other) m_drawingML = std::move(other.m_drawingML); m_comments = std::move(other.m_comments); m_tables = std::move(other.m_tables); + m_images = std::move(other.m_images); + m_imageRegistry = std::move(other.m_imageRegistry); return *this; } @@ -1786,6 +1794,11 @@ XLDrawingML& XLWorksheet::drawingML() return m_drawingML; } +const XLDrawingML& XLWorksheet::drawingML() const{ +XLDrawingML& result = const_cast(this)->drawingML(); +return const_cast(result); +} + /** * @details Create an empty DrawingML object for worksheets without images * This is needed when addImage() is called on a worksheet that doesn't have images yet @@ -1935,11 +1948,11 @@ bool XLWorksheet::addImage(const XLImage& image, uint32_t row, uint16_t column) try { // Create a copy of the image and set its ID XLImage imageWithId = image; - // Only generate a new ID if the image doesn't already have one - if (imageWithId.id().empty()) { - imageWithId.setId(generateNextImageId()); - } - + // Always generate a new worksheet-scoped unique ID (ignore existing ID) + std::string oldId = imageWithId.id(); + std::string newId = generateNextImageId(); + imageWithId.setId(newId); + // Store the image in the vector m_images.push_back(imageWithId); @@ -1962,8 +1975,12 @@ bool XLWorksheet::addImage(const XLImage& image, uint32_t row, uint16_t column) // Add image to DrawingML // Use sequential relationship ID that matches createDrawingRelationshipsFile std::string relationshipId = getRelationshipIdFromImageCount(); + addImageToDrawingML(imageWithId, row, column, relationshipId); + // invalidate registry + m_imageRegistry.clear(); + return true; } catch (const std::exception&) { @@ -2034,7 +2051,10 @@ bool XLWorksheet::addImageWithOffset(const XLImage& image, uint32_t row, uint16_ // Add image to DrawingML with offset addImageToDrawingMLWithOffset(imageWithId, row, column, relationshipId, rowOffset, colOffset); - + + // invalidate registry + m_imageRegistry.clear(); + return true; } @@ -2049,10 +2069,9 @@ bool XLWorksheet::addImageTwoCellAnchor(const XLImage& image, { // Create a copy of the image and set its ID XLImage imageWithId = image; - // Only generate a new ID if the image doesn't already have one - if (imageWithId.id().empty()) { - imageWithId.setId(generateNextImageId()); - } + // Always generate a new worksheet-scoped unique ID (ignore existing ID) + std::string oldId = imageWithId.id(); + imageWithId.setId(generateNextImageId()); // Store the image m_images.push_back(imageWithId); @@ -2076,6 +2095,9 @@ bool XLWorksheet::addImageTwoCellAnchor(const XLImage& image, addImageToDrawingMLTwoCellAnchor(imageWithId, fromRow, fromCol, toRow, toCol, relationshipId, fromRowOffset, fromColOffset, toRowOffset, toColOffset); + // invalidate registry + m_imageRegistry.clear(); + return true; } @@ -2131,10 +2153,22 @@ bool XLWorksheet::hasImagesInXML() const /** * @details Generate the next available image ID for this worksheet + * Ensures uniqueness within the worksheet scope */ std::string XLWorksheet::generateNextImageId() const { - return parentDoc().generateNextImageId(); + // Get all existing image IDs (sorted and unique) + std::vector existingIds = getImageIDs(); + + // Generate unique numeric ID using binary search for efficiency + // Microsoft Excel uses numeric IDs (e.g., "1", "2", "3") in DrawingML XML + int counter = 1; + std::string candidateId; + do { + candidateId = std::to_string(counter++); + } while (std::binary_search(existingIds.begin(), existingIds.end(), candidateId)); + + return candidateId; } /** @@ -2224,60 +2258,80 @@ void XLWorksheet::addImageRelationship(const XLImage& image, const std::string& if (parentDoc().hasRelationshipsFile(drawingRelsFilename)) { // Read existing content and add new relationship try { - // Get existing content from archive - std::string existingContent = parentDoc().readRelationshipsFile(drawingRelsFilename); - - // Parse existing XML - XMLDocument relsDoc; - relsDoc.load_string(existingContent.c_str()); - XMLNode relationshipsNode = relsDoc.document_element(); + // Get existing content from in-memory XML data + std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; - if (relationshipsNode) { - // Start building new XML + // Try to get the relationships XML data from in-memory cache + XLXmlData* relsXmlData = const_cast(parentDoc()).getRelationshipsXmlData(relsFilename); + if (!relsXmlData || !relsXmlData->valid()) { + // No existing relationships data, will create new file below relsXml = "\n" - "\n"; - - // Find the highest existing relationship ID to avoid duplicates - int maxId = 0; - std::set existingIds; - - for (auto relNode : relationshipsNode.children("Relationship")) { - std::string relId = relNode.attribute("Id").value(); - std::string relType = relNode.attribute("Type").value(); - std::string relTarget = relNode.attribute("Target").value(); + "\n" + "\n" + ""; + } else { + // Parse existing XML from in-memory data + XMLDocument* relsDoc = relsXmlData->getXmlDocument(); + if (!relsDoc || !relsDoc->document_element()) { + // Invalid XML data, create new file + relsXml = "\n" + "\n" + "\n" + ""; + } else { + XMLNode relationshipsNode = relsDoc->document_element(); + + // Start building new XML + relsXml = "\n" + "\n"; - // Extract numeric part of ID - if (relId.length() > 3 && relId.substr(0, 3) == "rId") { - try { - int idNum = std::stoi(relId.substr(3)); - maxId = std::max(maxId, idNum); - } catch (...) { - // Ignore non-numeric IDs + // Find the highest existing relationship ID to avoid duplicates + int maxId = 0; + std::set existingIds; + + for (auto relNode : relationshipsNode.children("Relationship")) { + std::string relId = relNode.attribute("Id").value(); + std::string relType = relNode.attribute("Type").value(); + std::string relTarget = relNode.attribute("Target").value(); + + // Copy existing relationships + relsXml += "\n"; + + // Track existing IDs for uniqueness check + existingIds.insert(relId); + + // Extract numeric part for max ID calculation + if (relId.substr(0, 3) == "rId") { + try { + int idNum = std::stoi(relId.substr(3)); + maxId = std::max(maxId, idNum); + } catch (...) { + // Ignore non-numeric IDs + } } } - existingIds.insert(relId); + // Generate unique relationship ID + std::string uniqueRelId = relationshipId; + int idNum = maxId + 1; + while (existingIds.find(uniqueRelId) != existingIds.end()) { + uniqueRelId = "rId" + std::to_string(idNum); + idNum++; + } - relsXml += "\n"; - } - - // Generate a unique relationship ID - std::string uniqueRelId = relationshipId; - int idNum = maxId + 1; - while (existingIds.find(uniqueRelId) != existingIds.end()) { - uniqueRelId = "rId" + std::to_string(idNum); - idNum++; + // Add new relationship with unique ID + std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); + relsXml += "\n"; + + relsXml += ""; } - - // Add new relationship with unique ID - std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); - relsXml += "\n"; - - relsXml += ""; } } catch (const std::exception&) { // If parsing fails, fall back to creating new file @@ -2300,7 +2354,7 @@ void XLWorksheet::addImageRelationship(const XLImage& image, const std::string& } // Update the relationships file - if (!parentDoc().addRelationshipsFile(drawingRelsFilename, relsXml)) { + if (!const_cast(parentDoc()).updateRelationshipsXmlData(drawingRelsFilename, relsXml)) { throw XLException("XLWorksheet::addImageRelationship(): could not update drawing relationships file"); } } @@ -2412,7 +2466,7 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const std::string otherName = other.name(); int nameCompare = thisName.compare(otherName); if (nameCompare != 0) { - appendDiff(diffMsg, "worksheet name differs: '" + thisName + "' vs '" + otherName + "'"); + appendDbgMsg(diffMsg, "worksheet name differs: '" + thisName + "' vs '" + otherName + "'"); return nameCompare; } @@ -2421,7 +2475,7 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const bool thisDrawingMLValid = m_drawingML.valid(); bool otherDrawingMLValid = other.m_drawingML.valid(); if (thisDrawingMLValid != otherDrawingMLValid) { - appendDiff(diffMsg, "DrawingML validity differs: " + std::string(thisDrawingMLValid ? "valid" : "invalid") + + appendDbgMsg(diffMsg, "DrawingML validity differs: " + std::string(thisDrawingMLValid ? "valid" : "invalid") + " vs " + std::string(otherDrawingMLValid ? "valid" : "invalid")); return thisDrawingMLValid ? 1 : -1; } @@ -2430,7 +2484,7 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const size_t thisImageCount = imageCount(); size_t otherImageCount = other.imageCount(); if (thisImageCount != otherImageCount) { - appendDiff(diffMsg, "image count differs: " + std::to_string(thisImageCount) + + appendDbgMsg(diffMsg, "image count differs: " + std::to_string(thisImageCount) + " vs " + std::to_string(otherImageCount)); return thisImageCount < otherImageCount ? -1 : 1; } @@ -2439,7 +2493,7 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const size_t thisRegistrySize = m_imageRegistry.size(); size_t otherRegistrySize = other.m_imageRegistry.size(); if (thisRegistrySize != otherRegistrySize) { - appendDiff(diffMsg, "image registry size differs: " + std::to_string(thisRegistrySize) + + appendDbgMsg(diffMsg, "image registry size differs: " + std::to_string(thisRegistrySize) + " vs " + std::to_string(otherRegistrySize)); return thisRegistrySize < otherRegistrySize ? -1 : 1; } @@ -2449,7 +2503,7 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const size_t otherImagesInMemory = other.m_images.size(); if (thisImagesInMemory > 0 || otherImagesInMemory > 0) { if (thisImagesInMemory != otherImagesInMemory) { - appendDiff(diffMsg, "images in memory count differs: " + std::to_string(thisImagesInMemory) + + appendDbgMsg(diffMsg, "images in memory count differs: " + std::to_string(thisImagesInMemory) + " vs " + std::to_string(otherImagesInMemory)); return thisImagesInMemory < otherImagesInMemory ? -1 : 1; } @@ -2458,7 +2512,7 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const for (size_t i = 0; i < thisImagesInMemory && i < otherImagesInMemory; ++i) { int imageCompare = m_images[i].compare(other.m_images[i], diffMsg); if (imageCompare != 0) { - appendDiff(diffMsg, "image " + std::to_string(i) + " differs"); + appendDbgMsg(diffMsg, "image " + std::to_string(i) + " differs"); return imageCompare; } } @@ -2469,7 +2523,7 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const for (size_t i = 0; i < thisRegistrySize && i < otherRegistrySize; ++i) { int registryCompare = m_imageRegistry[i].compare(other.m_imageRegistry[i], diffMsg); if (registryCompare != 0) { - appendDiff(diffMsg, "image registry entry " + std::to_string(i) + " differs"); + appendDbgMsg(diffMsg, "image registry entry " + std::to_string(i) + " differs"); return registryCompare; } } @@ -2481,7 +2535,7 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const bool thisRelValid = m_relationships.valid(); bool otherRelValid = other.m_relationships.valid(); if (thisRelValid != otherRelValid) { - appendDiff(diffMsg, "relationships validity differs: " + std::string(thisRelValid ? "valid" : "invalid") + + appendDbgMsg(diffMsg, "relationships validity differs: " + std::string(thisRelValid ? "valid" : "invalid") + " vs " + std::string(otherRelValid ? "valid" : "invalid")); return thisRelValid ? 1 : -1; } @@ -2489,7 +2543,7 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const // Skip DrawingML relationship check for now to avoid path issues // TODO: Implement safer DrawingML relationship comparison } catch (const std::exception&) { - appendDiff(diffMsg, "error comparing relationships"); + appendDbgMsg(diffMsg, "error comparing relationships"); return -1; } @@ -2504,3 +2558,455 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const // Worksheets are identical return 0; } + +int XLWorksheet::verifyData(std::string* dbgMsg) const +{ + int errorCount = 0; + std::string worksheetName = name(); + + // TODO: Verify sub-objects when verifyData() is implemented for them + // errorCount += m_relationships.verifyData(dbgMsg); + // errorCount += m_merges.verifyData(dbgMsg); + // errorCount += m_vmlDrawing.verifyData(dbgMsg); + + // Only verify DrawingML if the worksheet has images + // For worksheets without images, DrawingML can be invalid + if (!m_imageRegistry.empty() || !m_images.empty() || hasImagesInXML()) { + errorCount += m_drawingML.verifyData(dbgMsg); + } + // errorCount += m_comments.verifyData(dbgMsg); + // errorCount += m_tables.verifyData(dbgMsg); + + // Verify images vector + for (size_t i = 0; i < m_images.size(); ++i) { + int imageErrors = m_images[i].verifyData(dbgMsg); + if (imageErrors > 0) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image " + std::to_string(i) + " has " + std::to_string(imageErrors) + " errors"); + errorCount += imageErrors; + } + } + + // Verify image registry consistency + if (m_drawingML.valid()) { + try { + auto imageInfos = getImageInfos(); + if (imageInfos.size() != m_images.size()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image registry size (" + std::to_string(imageInfos.size()) + + ") does not match images vector size (" + std::to_string(m_images.size()) + ")"); + errorCount++; + } + } catch (const std::exception&) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] error accessing image registry"); + errorCount++; + } + } + + // Verify unique image IDs + errorCount += verifyUniqueImageIDs(dbgMsg); + + // Verify DrawingML XML consistency with registry + errorCount += verifyDrawingMLConsistency(dbgMsg); + + // Verify m_images vector consistency with XML + errorCount += verifyImagesVectorConsistency(dbgMsg); + + // Verify binary image data exists in archive + errorCount += verifyBinaryImageData(dbgMsg); + + return errorCount; +} + +int XLWorksheet::verifyUniqueImageIDs(std::string* dbgMsg) const +{ + int duplicateCount = 0; + std::string worksheetName = name(); + + // Check 1: Image registry for duplicate IDs + if (m_drawingML.valid()) { + try { + auto imageInfos = getImageInfos(); + std::set registryIds; + + for (const auto& info : imageInfos) { + if (registryIds.find(info.imageId) != registryIds.end()) { + // Duplicate found in registry + appendDbgMsg(dbgMsg, "[" + worksheetName + "] duplicate image ID in registry: " + info.imageId); + duplicateCount++; + } else { + registryIds.insert(info.imageId); + } + } + } catch (const std::exception&) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] error accessing image registry for ID verification"); + duplicateCount++; + } + } + + // Check 2: m_images vector for duplicate IDs + std::set imageIds; + for (size_t i = 0; i < m_images.size(); ++i) { + const std::string& imageId = m_images[i].id(); + if (imageIds.find(imageId) != imageIds.end()) { + // Duplicate found in m_images vector + appendDbgMsg(dbgMsg, "[" + worksheetName + "] duplicate image ID in m_images vector: " + imageId); + duplicateCount++; + } else { + imageIds.insert(imageId); + } + } + + return duplicateCount; +} + +int XLWorksheet::verifyDrawingMLConsistency(std::string* dbgMsg) const +{ + int inconsistencyCount = 0; + std::string worksheetName = name(); + + // Check if DrawingML is valid + if (!m_drawingML.valid()) { + // If DrawingML is invalid but we have images in registry or m_images, that's an inconsistency + if (!m_imageRegistry.empty() || !m_images.empty()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] DrawingML is invalid but has " + + std::to_string(m_imageRegistry.size()) + " registry entries and " + + std::to_string(m_images.size()) + " m_images entries"); + inconsistencyCount++; + } + return inconsistencyCount; // No further checks possible + } + + try { + // Get image registry data + auto imageInfos = getImageInfos(); + size_t registryCount = imageInfos.size(); + + // Get DrawingML XML data + XMLNode rootNode = m_drawingML.getRootNode(); + if (rootNode.empty()) { + // Empty root node is OK if there are no images + if (!m_imageRegistry.empty() || !m_images.empty()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] DrawingML root node is empty but has " + + std::to_string(m_imageRegistry.size()) + " registry entries and " + + std::to_string(m_images.size()) + " m_images entries"); + inconsistencyCount++; + } + return inconsistencyCount; + } + + // Count anchors in XML + size_t xmlAnchorCount = 0; + std::vector xmlImageIds; + std::vector xmlRelationshipIds; + + for (auto anchor : rootNode.children()) { + if (matchesElementName(anchor.name(), "oneCellAnchor") || + matchesElementName(anchor.name(), "twoCellAnchor")) { + xmlAnchorCount++; + + // Extract image ID from XML + XMLNode pic = anchor.child("xdr:pic"); + if (!pic.empty()) { + XMLNode nvPicPr = pic.child("xdr:nvPicPr"); + if (!nvPicPr.empty()) { + XMLNode cNvPr = nvPicPr.child("xdr:cNvPr"); + if (!cNvPr.empty()) { + XMLAttribute idAttr = cNvPr.attribute("id"); + if (!idAttr.empty()) { + xmlImageIds.push_back(idAttr.value()); + } + } + } + } + + // Extract relationship ID from XML + XMLNode blipFill = pic.child("xdr:blipFill"); + if (!blipFill.empty()) { + XMLNode blip = blipFill.child("a:blip"); + if (!blip.empty()) { + XMLAttribute embedAttr = blip.attribute("r:embed"); + if (!embedAttr.empty()) { + xmlRelationshipIds.push_back(embedAttr.value()); + } + } + } + } + } + + // Check 1: Anchor count consistency + if (xmlAnchorCount != registryCount) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] XML anchor count (" + std::to_string(xmlAnchorCount) + + ") does not match registry count (" + std::to_string(registryCount) + ")"); + inconsistencyCount++; + } + + // Check 2: Image ID consistency + std::set registryImageIds; + for (const auto& info : imageInfos) { + registryImageIds.insert(info.imageId); + } + + std::set xmlImageIdSet(xmlImageIds.begin(), xmlImageIds.end()); + + // Check for IDs in registry but not in XML + for (const auto& registryId : registryImageIds) { + if (xmlImageIdSet.find(registryId) == xmlImageIdSet.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + registryId + "' in registry but not in XML"); + inconsistencyCount++; + } + } + + // Check for IDs in XML but not in registry + for (const auto& xmlId : xmlImageIdSet) { + if (registryImageIds.find(xmlId) == registryImageIds.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + xmlId + "' in XML but not in registry"); + inconsistencyCount++; + } + } + + // Check 3: Relationship ID consistency + std::set registryRelIds; + for (const auto& info : imageInfos) { + registryRelIds.insert(info.relationshipId); + } + + std::set xmlRelIdSet(xmlRelationshipIds.begin(), xmlRelationshipIds.end()); + + // Check for relationship IDs in registry but not in XML + for (const auto& registryRelId : registryRelIds) { + if (xmlRelIdSet.find(registryRelId) == xmlRelIdSet.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] relationship ID '" + registryRelId + "' in registry but not in XML"); + inconsistencyCount++; + } + } + + // Check for relationship IDs in XML but not in registry + for (const auto& xmlRelId : xmlRelIdSet) { + if (registryRelIds.find(xmlRelId) == registryRelIds.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] relationship ID '" + xmlRelId + "' in XML but not in registry"); + inconsistencyCount++; + } + } + + } catch (const std::exception& e) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] error during DrawingML consistency check: " + std::string(e.what())); + inconsistencyCount++; + } + + return inconsistencyCount; +} + +int XLWorksheet::verifyImagesVectorConsistency(std::string* dbgMsg) const +{ + int inconsistencyCount = 0; + std::string worksheetName = name(); + + // Check if DrawingML is valid + if (!m_drawingML.valid()) { + // If DrawingML is invalid but we have images in m_images, that's an inconsistency + if (!m_images.empty()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] DrawingML is invalid but m_images has " + + std::to_string(m_images.size()) + " entries"); + inconsistencyCount++; + } + return inconsistencyCount; // No further checks possible + } + + try { + // Get DrawingML XML data + XMLNode rootNode = m_drawingML.getRootNode(); + if (rootNode.empty()) { + // Empty root node is OK if there are no images + if (!m_images.empty()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] DrawingML root node is empty but m_images has " + + std::to_string(m_images.size()) + " entries"); + inconsistencyCount++; + } + return inconsistencyCount; + } + + // Count anchors in XML + size_t xmlAnchorCount = 0; + std::vector xmlImageIds; + + for (auto anchor : rootNode.children()) { + if (matchesElementName(anchor.name(), "oneCellAnchor") || + matchesElementName(anchor.name(), "twoCellAnchor")) { + xmlAnchorCount++; + + // Extract image ID from XML + XMLNode pic = anchor.child("xdr:pic"); + if (!pic.empty()) { + XMLNode nvPicPr = pic.child("xdr:nvPicPr"); + if (!nvPicPr.empty()) { + XMLNode cNvPr = nvPicPr.child("xdr:cNvPr"); + if (!cNvPr.empty()) { + XMLAttribute idAttr = cNvPr.attribute("id"); + if (!idAttr.empty()) { + xmlImageIds.push_back(idAttr.value()); + } + } + } + } + } + } + + // Check 1: Anchor count consistency + if (xmlAnchorCount != m_images.size()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] XML anchor count (" + std::to_string(xmlAnchorCount) + + ") does not match m_images count (" + std::to_string(m_images.size()) + ")"); + inconsistencyCount++; + } + + // Check 2: Image ID consistency + std::set mImagesIds; + for (const auto& image : m_images) { + if (!image.id().empty()) { + mImagesIds.insert(image.id()); + } + } + + std::set xmlImageIdSet(xmlImageIds.begin(), xmlImageIds.end()); + + // Check for IDs in m_images but not in XML + for (const auto& mImageId : mImagesIds) { + if (xmlImageIdSet.find(mImageId) == xmlImageIdSet.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + mImageId + "' in m_images but not in XML"); + inconsistencyCount++; + } + } + + // Check for IDs in XML but not in m_images + for (const auto& xmlId : xmlImageIdSet) { + if (mImagesIds.find(xmlId) == mImagesIds.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + xmlId + "' in XML but not in m_images"); + inconsistencyCount++; + } + } + + } catch (const std::exception& e) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] error during m_images consistency check: " + std::string(e.what())); + inconsistencyCount++; + } + + return inconsistencyCount; +} + +int XLWorksheet::verifyBinaryImageData(std::string* dbgMsg) const +{ + int issueCount = 0; + std::string worksheetName = name(); + + // Only validate if we have images to check + if (m_imageRegistry.empty() && m_images.empty()) { + return issueCount; // No images to validate + } + + try { + // Get the drawing relationships filename + std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + + // Check if relationships file exists in archive + if (!parentDoc().hasRelationshipsFile(relsFilename)) { + // Check if we have images but no relationships file - this is a problem + if (m_images.size() > 0) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] has " + std::to_string(m_images.size()) + " images but no relationships file"); + issueCount++; + } + // No relationships file means no binary data to validate + return issueCount; + } + + // Read the relationships file + std::string relsContent = parentDoc().readRelationshipsFile(relsFilename); + if (relsContent.empty()) { + // Check if we have images but empty relationships file - this is a problem + if (m_images.size() > 0) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] has " + std::to_string(m_images.size()) + " images but relationships file is empty"); + issueCount++; + } + return issueCount; // Empty relationships file means no binary data to validate + } + + // Parse the relationships XML + pugi::xml_document relsDoc; + if (!relsDoc.load_string(relsContent.c_str())) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] failed to parse relationships XML"); + issueCount++; + return issueCount; + } + + // Collect unique image paths and verify binary files exist + std::set referencedImagePaths; + + for (auto rel : relsDoc.document_element().children("Relationship")) { + std::string target = rel.attribute("Target").value(); + std::string type = rel.attribute("Type").value(); + + // Only process image relationships + if (type == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image") { + referencedImagePaths.insert(target); + } + } + + // Verify that each referenced binary file actually exists in the archive + for (const auto& imagePath : referencedImagePaths) { + // Convert relative path to absolute path for archive check + std::string absolutePath = "xl/media/" + imagePath.substr(imagePath.find_last_of('/') + 1); + + try { + std::string imageContent = parentDoc().readFile(absolutePath); + if (imageContent.empty()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image file '" + absolutePath + "' referenced in relationships but binary file missing from archive"); + issueCount++; + } + } catch (const std::exception&) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] error checking image file '" + absolutePath + "' during relationships integrity verification"); + issueCount++; + } + } + + } catch (const std::exception& e) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] error during relationships integrity verification: " + std::string(e.what())); + issueCount++; + } + + return issueCount; +} + +std::vector XLWorksheet::getImageIDs() const +{ + std::vector imageIds; + + // Get image IDs from m_images vector + for (const auto& image : m_images) { + if (!image.id().empty()) { + imageIds.push_back(image.id()); + } + } + + // Get image IDs from the image registry + if (m_drawingML.valid()) { + try { + auto imageInfos = getImageInfos(); + for (const auto& info : imageInfos) { + if (!info.imageId.empty()) { + imageIds.push_back(info.imageId); + } + } + } catch (const std::exception&) { + // Registry access failed, continue with m_images IDs only + } + } + + // Sort the vector + std::sort(imageIds.begin(), imageIds.end()); + + // Remove duplicates using std::unique() and vector::erase() + auto it = std::unique(imageIds.begin(), imageIds.end()); + imageIds.erase(it, imageIds.end()); + + return imageIds; +} + +// Static const member definitions for XLWorksheet +const XLImageInfo XLWorksheet::m_emptyImageInfo{}; diff --git a/OpenXLSX/sources/XLWorkbook.cpp b/OpenXLSX/sources/XLWorkbook.cpp index b55e5ad4..7f8fb198 100644 --- a/OpenXLSX/sources/XLWorkbook.cpp +++ b/OpenXLSX/sources/XLWorkbook.cpp @@ -139,6 +139,8 @@ XLWorksheet XLWorkbook::worksheet(const std::string& sheetName) if (worksheet.hasImagesInXML()) { try { std::ignore = worksheet.drawingML(); // Initialize DrawingML + // Populate m_images vector from existing XML data + worksheet.populateImagesFromXML(); } catch (...) { // DrawingML initialization failed, but worksheet is still usable } @@ -160,6 +162,8 @@ XLWorksheet XLWorkbook::worksheet(uint16_t index) if (worksheet.hasImagesInXML()) { try { std::ignore = worksheet.drawingML(); // Initialize DrawingML + // Populate m_images vector from existing XML data + worksheet.populateImagesFromXML(); } catch (...) { // DrawingML initialization failed, but worksheet is still usable } diff --git a/OpenXLSX/sources/XLWorksheetImageQuery.cpp b/OpenXLSX/sources/XLWorksheetImageQuery.cpp index c0afa9a3..3a66484c 100644 --- a/OpenXLSX/sources/XLWorksheetImageQuery.cpp +++ b/OpenXLSX/sources/XLWorksheetImageQuery.cpp @@ -86,6 +86,16 @@ namespace OpenXLSX refreshImageRegistry(); } +#if (defined(_DEBUG) || !defined(NDEBUG)) + // Check for unique image IDs + std::string dbgMsg; + int duplicateCount = verifyUniqueImageIDs(&dbgMsg); + if (duplicateCount > 0) { + std::cout << "ERROR: getImageInfoByImageID() found " << duplicateCount + << " duplicate image ID(s): " << dbgMsg << std::endl; + } +#endif + // Linear search for the image ID for (const auto& imgInfo : m_imageRegistry) { if (imgInfo.imageId == imageId) { @@ -123,42 +133,40 @@ namespace OpenXLSX /** * @brief Get all image information at a specific cell * @param cellRef The cell reference (e.g., "A1", "B5") - * @return Const reference to vector of XLImageInfo objects at that cell + * @return Vector of XLImageInfo objects at that cell */ - const std::vector& XLWorksheet::getImageInfosAtCell(const std::string& cellRef) const + std::vector XLWorksheet::getImageInfosAtCell(const std::string& cellRef) const { // Auto-refresh registry if empty if (m_imageRegistry.empty()) { refreshImageRegistry(); } - // Clear the result vector - m_emptyImageInfoVector.clear(); + std::vector result; // Filter images by cell reference for (const auto& imgInfo : m_imageRegistry) { if (imgInfo.anchorCell == cellRef) { - m_emptyImageInfoVector.push_back(imgInfo); + result.push_back(imgInfo); } } - return m_emptyImageInfoVector; + return result; } /** * @brief Get all image information in a specific range * @param cellRange The cell range (e.g., "A1:B5", "C10:D20") - * @return Const reference to vector of XLImageInfo objects in that range + * @return Vector of XLImageInfo objects in that range */ - const std::vector& XLWorksheet::getImageInfosInRange(const std::string& cellRange) const + std::vector XLWorksheet::getImageInfosInRange(const std::string& cellRange) const { // Auto-refresh registry if empty if (m_imageRegistry.empty()) { refreshImageRegistry(); } - // Clear the result vector - m_emptyImageInfoVector.clear(); + std::vector result; // Parse the range size_t colonPos = cellRange.find(':'); @@ -182,7 +190,7 @@ namespace OpenXLSX // Check if image is within range if (imgRef.row() >= topLeftRef.row() && imgRef.row() <= bottomRightRef.row() && imgRef.column() >= topLeftRef.column() && imgRef.column() <= bottomRightRef.column()) { - m_emptyImageInfoVector.push_back(imgInfo); + result.push_back(imgInfo); } } } @@ -190,7 +198,7 @@ namespace OpenXLSX // Invalid range format, return empty vector } - return m_emptyImageInfoVector; + return result; } /** @@ -255,14 +263,21 @@ namespace OpenXLSX // Remove from relationships file (in-memory XML document) bool relsRemoved = removeImageFromRelationships(relationshipId); - // Remove image file from archive (delete binary file entry) - use the path we got earlier - bool fileRemoved = removeImageFileByPath(imagePath); + // Remove image file from archive (delete binary file entry if no other references) - use the path we got earlier + bool fileProcessed = removeImageFileIfUnreferenced(imagePath); // Remove from registry (in-memory cache) m_imageRegistry.erase(it); - // Return true if at least XML removal succeeded - return xmlRemoved; + // Remove from m_images vector (in-memory cache) + auto imagesIt = std::find_if(m_images.begin(), m_images.end(), + [&imageId](const XLImage& img) { return img.id() == imageId; }); + if (imagesIt != m_images.end()) { + m_images.erase(imagesIt); + } + + // Return true if all critical operations succeeded + return xmlRemoved && relsRemoved && fileProcessed; } /** @@ -285,35 +300,62 @@ namespace OpenXLSX return false; // Image not found } + // Store the image ID before removing from registry + std::string imageId = it->imageId; + + // Get image path BEFORE removing relationships (we need the relationship to find the file path) + std::string imagePath = getImagePathFromRelationship(relationshipId); + // Remove from DrawingML XML (in-memory XML document) bool xmlRemoved = removeImageFromDrawingXML(relationshipId); // Remove from relationships file (in-memory XML document) bool relsRemoved = removeImageFromRelationships(relationshipId); - // Remove image file from archive (delete binary file entry) - bool fileRemoved = removeImageFileFromArchive(relationshipId); + // Remove image file from archive (delete binary file entry if no other references) - use the path we got earlier + bool fileProcessed = removeImageFileIfUnreferenced(imagePath); // Remove from registry (in-memory cache) m_imageRegistry.erase(it); - // Return true if at least XML removal succeeded - return xmlRemoved; + // Remove from m_images vector (in-memory cache) + auto imagesIt = std::find_if(m_images.begin(), m_images.end(), + [&imageId](const XLImage& img) { return img.id() == imageId; }); + if (imagesIt != m_images.end()) { + m_images.erase(imagesIt); + } + + // Return true if all critical operations succeeded + return xmlRemoved && relsRemoved && fileProcessed; } /** * @brief Remove all images from the worksheet + * @return Number of images removed */ void XLWorksheet::clearImages() { - // Clear the registry - m_imageRegistry.clear(); - - // TODO: Implement XML clearing from DrawingML - // This would involve: - // 1. Clearing all image nodes from drawing.xml - // 2. Clearing the relationships file - // 3. Removing all image files from the archive + // Get all image IDs + std::vector imageIds = getImageIDs(); + + // Remove each image by ID + for (const auto& imageId : imageIds) { + if (removeImageByImageID(imageId)) { + } + } + + // Ensure m_images vector is cleared (safety check) + m_images.clear(); + + // TODO: Add verification checks to ensure complete cleanup: + // 1. Check that m_imageRegistry is empty + // 2. Check that DrawingML XML has no image nodes + // 3. Check that relationships file has no image relationships + // 4. Check that no image files remain in the archive + // 5. Verify that getImageIDs() returns empty vector + // 6. Verify that imageCount() returns 0 + + return; } //---------------------------------------------------------------------------------------------------------------------- @@ -325,43 +367,53 @@ namespace OpenXLSX */ void XLWorksheet::refreshImageRegistry() const { + bool done = false; + // Clear existing registry m_imageRegistry.clear(); // Check if DrawingML is valid if (!m_drawingML.valid()) { - return; // No drawing data available + done = true; // No drawing data available } try { - // Get the DrawingML XML content by reading directly from the archive + // Use standard sequential numbering approach (consistent with rest of codebase) std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNumber()) + ".xml"; - - std::string xmlContent = parentDoc().readRelationshipsFile(drawingFilename); - if (xmlContent.empty()) { - return; // No XML data available + // Get the existing XLXmlData object for the drawing file + // This is the proper way - use the existing XML data structure rather than bypassing it + XLXmlData* xmlData = const_cast(parentDoc()).getDrawingXmlData(drawingFilename); + if (!xmlData) { + done = true; } - // Parse the XML document - pugi::xml_document doc; - if (!doc.load_string(xmlContent.c_str())) { - return; // Failed to parse XML - } + XMLNode wsDr; + std::string rootName; + if( !done ){ + // Get the underlying XMLDocument (pugi::xml_document) + XMLDocument& xmlDoc = *xmlData->getXmlDocument(); + if (!xmlDoc.document_element()) { + done = true; + } - // Get the root element (xdr:wsDr) - auto wsDr = doc.document_element(); - std::string rootName = wsDr.name(); + // Get the root element (xdr:wsDr) + wsDr = xmlDoc.document_element(); + rootName = wsDr.name(); + } // Check for both "wsDr" and "xdr:wsDr" (with namespace prefix) + if( !done ){ if (rootName != "wsDr" && rootName != "xdr:wsDr") { - return; // Invalid root element + done = true; // Invalid root element + } } // Read the relationships file to get image mappings - std::map relationshipMap; - readDrawingRelationships(relationshipMap); + if( !done ){ + std::map relationshipMap; + readDrawingRelationships(relationshipMap); - // Process each anchor element + // Process each anchor element for (auto anchor : wsDr.children()) { std::string anchorName = anchor.name(); @@ -370,6 +422,7 @@ namespace OpenXLSX } else if (matchesElementName(anchorName, "twoCellAnchor")) { processTwoCellAnchor(anchor, relationshipMap); + } } } } @@ -395,20 +448,22 @@ namespace OpenXLSX return; // No relationships file } - // Read the relationships file - std::string relsContent = parentDoc().readRelationshipsFile(relsFilename); - if (relsContent.empty()) { - return; // Empty relationships file + // Alternate implementation using XLXmlData access + // Get relationships XML data from in-memory cache + XLXmlData* relsXmlData = const_cast(parentDoc()).getRelationshipsXmlData(relsFilename); + if (!relsXmlData || !relsXmlData->valid()) { + return; // No relationships data available } - // Parse the relationships XML - pugi::xml_document relsDoc; - if (!relsDoc.load_string(relsContent.c_str())) { - return; // Failed to parse relationships + // Parse the relationships XML from in-memory data + XMLDocument* relsDoc = relsXmlData->getXmlDocument(); + if (!relsDoc || !relsDoc->document_element()) { + return; // Invalid XML data } // Extract relationship mappings - for (auto rel : relsDoc.document_element().children("Relationship")) { + int relationshipCount = 0; + for (auto rel : relsDoc->document_element().children("Relationship")) { std::string id = rel.attribute("Id").value(); std::string target = rel.attribute("Target").value(); std::string type = rel.attribute("Type").value(); @@ -416,6 +471,7 @@ namespace OpenXLSX // Only process image relationships if (type == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image") { relationshipMap[id] = target; + relationshipCount++; } } } @@ -424,6 +480,82 @@ namespace OpenXLSX } } + /** + * @brief Populate m_images vector from existing XML data + */ + void XLWorksheet::populateImagesFromXML() + { + // Only populate if m_images is empty to avoid duplicates + if (!m_images.empty()) { + return; + } + + // First refresh the registry to get current XML data + refreshImageRegistry(); + + // Get relationship map to find image file paths + std::map relationshipMap; + readDrawingRelationships(relationshipMap); + + // Convert registry entries to XLImage objects + for (const auto& imgInfo : m_imageRegistry) { + try { + // Find the image file path using the relationship ID + auto it = relationshipMap.find(imgInfo.relationshipId); + if (it == relationshipMap.end()) { + continue; // Skip if relationship not found + } + + std::string imagePath = it->second; + + // Convert relative path to absolute path if needed + if (imagePath.substr(0, 3) == "../") { + imagePath = "xl/" + imagePath.substr(3); // Convert "../media/image_1.png" to "xl/media/image_1.png" + } else if (imagePath.substr(0, 4) != "xl/") { + imagePath = "xl/" + imagePath; // Add xl/ prefix if missing + } + + // Create XLImage object and load from archive + XLImage image; + + // Load binary data from archive + std::string binaryData = parentDoc().readFile(imagePath); + if (binaryData.empty()) { + continue; // Skip if binary data not found + } + + // Convert string to vector + std::vector imageData(binaryData.begin(), binaryData.end()); + + // Determine MIME type from file extension + std::string mimeType = "image/png"; // default + if (imagePath.find(".jpg") != std::string::npos || imagePath.find(".jpeg") != std::string::npos) { + mimeType = "image/jpeg"; + } else if (imagePath.find(".gif") != std::string::npos) { + mimeType = "image/gif"; + } else if (imagePath.find(".bmp") != std::string::npos) { + mimeType = "image/bmp"; + } + + // Load image data + if (!image.loadFromData(imageData, mimeType, imgInfo.imageId)) { + continue; // Skip if loading failed + } + + // Set display dimensions from registry + image.setDisplayWidth(imgInfo.displayWidthEMUs); + image.setDisplayHeight(imgInfo.displayHeightEMUs); + + // Add to m_images vector + m_images.push_back(image); + } + catch (const std::exception&) { + // If creating XLImage fails, skip this entry + // This ensures we don't crash on malformed data + } + } + } + /** * @brief Process a oneCellAnchor element and add to registry * @param anchor The oneCellAnchor XML node @@ -455,14 +587,36 @@ namespace OpenXLSX std::string cellRef = XLCellReference(row + 1, col + 1).address(); // Convert to 1-based // Extract dimensions - auto ext = anchor.child("xdr:ext"); + pugi::xml_node ext; + if (ext.empty()) { + // Try looking inside xdr:pic > xdr:spPr > a:xfrm + auto pic = anchor.child("xdr:pic"); + if (!pic.empty()) { + auto spPr = pic.child("xdr:spPr"); + if (!spPr.empty()) { + auto xfrm = spPr.child("a:xfrm"); + if (!xfrm.empty()) { + ext = xfrm.child("a:ext"); + } + } + } + if (ext.empty()) { + auto ext = anchor.child("xdr:ext"); + } + if (ext.empty()) { + ext = anchor.child("a:ext"); // Try with a namespace prefix + } if (ext.empty()) { ext = anchor.child("ext"); // Try without namespace prefix } + + } if (ext.empty()) { return; } + // TODO: Double check the proper interpretation of cx/cy values based on whether + // the ext element is (intrinsic shape size) or (anchored size) uint32_t widthEMUs = ext.attribute("cx").as_uint(); uint32_t heightEMUs = ext.attribute("cy").as_uint(); @@ -507,8 +661,8 @@ namespace OpenXLSX std::string imagePath = it->second; - // Extract image ID from filename (e.g., "../media/image_img1.png" -> "img1") - std::string imageId = extractImageIdFromPath(imagePath); + // Extract image ID from XML (not filename) - more reliable + std::string imageId = extractImageIdFromXML(pic); // Create XLImageInfo XLImageInfo imgInfo; @@ -539,6 +693,7 @@ namespace OpenXLSX void XLWorksheet::processTwoCellAnchor(const pugi::xml_node& anchor, const std::map& relationshipMap) const { try { + // Extract cell position (use 'from' cell as primary anchor) auto from = anchor.child("xdr:from"); if (from.empty()) { @@ -563,13 +718,31 @@ namespace OpenXLSX // Extract dimensions auto ext = anchor.child("xdr:ext"); + if (ext.empty()) { + ext = anchor.child("a:ext"); // Try with a namespace prefix + } if (ext.empty()) { ext = anchor.child("ext"); // Try without namespace prefix } + if (ext.empty()) { + // Try looking inside xdr:pic > xdr:spPr > a:xfrm (Excel-generated files) + auto pic = anchor.child("xdr:pic"); + if (!pic.empty()) { + auto spPr = pic.child("xdr:spPr"); + if (!spPr.empty()) { + auto xfrm = spPr.child("a:xfrm"); + if (!xfrm.empty()) { + ext = xfrm.child("a:ext"); + } + } + } + } if (ext.empty()) { return; } + // TODO: Double check the proper interpretation of cx/cy values based on whether + // the ext element is (intrinsic shape size) or (anchored size) uint32_t widthEMUs = ext.attribute("cx").as_uint(); uint32_t heightEMUs = ext.attribute("cy").as_uint(); @@ -614,8 +787,8 @@ namespace OpenXLSX std::string imagePath = it->second; - // Extract image ID from filename - std::string imageId = extractImageIdFromPath(imagePath); + // Extract image ID from XML (not filename) - more reliable + std::string imageId = extractImageIdFromXML(pic); // Create XLImageInfo XLImageInfo imgInfo; @@ -639,26 +812,38 @@ namespace OpenXLSX } /** - * @brief Extract image ID from image file path - * @param imagePath The image file path (e.g., "../media/image_img1.png") - * @return The image ID (e.g., "img1") + * @brief Extract image ID from XML pic element + * @param pic The pic XML node containing the image information + * @return The image ID from the cNvPr id attribute */ - std::string XLWorksheet::extractImageIdFromPath(const std::string& imagePath) const + std::string XLWorksheet::extractImageIdFromXML(const pugi::xml_node& pic) const { - // Extract filename from path - size_t lastSlash = imagePath.find_last_of('/'); - std::string filename = (lastSlash != std::string::npos) ? imagePath.substr(lastSlash + 1) : imagePath; - - // Remove "image_" prefix and file extension - if (filename.substr(0, 6) == "image_") { - std::string withoutPrefix = filename.substr(6); - size_t dotPos = withoutPrefix.find_last_of('.'); - if (dotPos != std::string::npos) { - return withoutPrefix.substr(0, dotPos); + try { + // Navigate to cNvPr element: pic -> nvPicPr -> cNvPr + auto nvPicPr = pic.child("xdr:nvPicPr"); + if (nvPicPr.empty()) { + nvPicPr = pic.child("nvPicPr"); // Try without namespace + } + + if (!nvPicPr.empty()) { + auto cNvPr = nvPicPr.child("xdr:cNvPr"); + if (cNvPr.empty()) { + cNvPr = nvPicPr.child("cNvPr"); // Try without namespace + } + + if (!cNvPr.empty()) { + auto idAttr = cNvPr.attribute("id"); + if (!idAttr.empty()) { + std::string imageId = idAttr.value(); + return imageId; + } + } } + } catch (const std::exception&) { + // If XML parsing fails, fall back to empty string } - - return filename; // Fallback to full filename + + return ""; // Fallback to empty string } /** @@ -748,18 +933,21 @@ namespace OpenXLSX if (!parentDoc().hasRelationshipsFile(relsFilename)) { return ""; } - - std::string relsContent = parentDoc().readRelationshipsFile(relsFilename); - if (relsContent.empty()) { - return ""; + // Alternate implementation using XLXmlData access + // Get relationships XML data from in-memory cache + XLXmlData* relsXmlData = const_cast(parentDoc()).getRelationshipsXmlData(relsFilename); + if (!relsXmlData || !relsXmlData->valid()) { + return ""; } - pugi::xml_document relsDoc; - if (!relsDoc.load_string(relsContent.c_str())) { + // Parse the relationships XML from in-memory data + XMLDocument* relsDoc = relsXmlData->getXmlDocument(); + if (!relsDoc || !relsDoc->document_element()) { return ""; } - for (auto rel : relsDoc.document_element().children("Relationship")) { + // Find the relationship with the specified ID + for (auto rel : relsDoc->document_element().children("Relationship")) { if (rel.attribute("Id").value() == relationshipId) { return rel.attribute("Target").value(); } @@ -773,22 +961,35 @@ namespace OpenXLSX } /** - * @brief Remove image file by direct path + * @brief Remove image file if no other relationships reference it * @param imagePath The relative image path (e.g., "../media/image_img3.png") - * @return True if the file was found and removed + * @return True if the file was processed (deleted if no references remain, preserved if still referenced) + * + * @note This function MUST be called AFTER the relationship has been removed from the relationships file. + * It checks if any other relationships still reference the binary file before deciding whether to delete it. + * If called before relationship removal, it will incorrectly preserve files that should be deleted. */ - bool XLWorksheet::removeImageFileByPath(const std::string& imagePath) const + bool XLWorksheet::removeImageFileIfUnreferenced(const std::string& imagePath) const { if (imagePath.empty()) { return false; } try { + // Check if binary file is still referenced before deleting + bool isReferenced = parentDoc().isBinaryFileReferenced(imagePath); + + if (!isReferenced) { + // Only delete binary file if no relationships still reference it // Convert relative path to absolute path within the archive std::string fullImagePath = "xl/media/" + imagePath.substr(imagePath.find_last_of('/') + 1); bool result = const_cast(parentDoc()).deleteEntry(fullImagePath); return result; + } else { + // Multiple references exist - don't delete binary file yet + return true; // Return true since we "processed" the request + } } catch (const std::exception&) { return false; @@ -897,33 +1098,39 @@ namespace OpenXLSX return false; } - // Read the relationships file from in-memory document - std::string relsContent = parentDoc().readRelationshipsFile(relsFilename); - if (relsContent.empty()) { + // Alternate implementation using XLXmlData access + // Get relationships XML data from in-memory cache + XLXmlData* relsXmlData = const_cast(parentDoc()).getRelationshipsXmlData(relsFilename); + if (!relsXmlData || !relsXmlData->valid()) { return false; } - // Parse the relationships XML - pugi::xml_document relsDoc; - if (!relsDoc.load_string(relsContent.c_str())) { + // Parse the relationships XML from in-memory data + XMLDocument* relsDoc = relsXmlData->getXmlDocument(); + if (!relsDoc || !relsDoc->document_element()) { return false; } // Find and remove the relationship - for (auto rel : relsDoc.document_element().children("Relationship")) { + bool found = false; + for (auto rel : relsDoc->document_element().children("Relationship")) { std::string relId = rel.attribute("Id").value(); if (relId == relationshipId) { - relsDoc.document_element().remove_child(rel); - - // Update the in-memory XML document - std::ostringstream oss; - relsDoc.save(oss); - const_cast(parentDoc()).addRelationshipsFile(relsFilename, oss.str()); - - return true; + relsDoc->document_element().remove_child(rel); + found = true; + break; } } + if (found) { + // Persist the changes to XLXmlData using setRawData() + // This avoids archive access and lets XLXmlData handle persistence + std::ostringstream oss; + relsDoc->save(oss); + relsXmlData->setRawData(oss.str()); + return true; + } + return false; // Relationship not found } catch (const std::exception&) { @@ -932,11 +1139,56 @@ namespace OpenXLSX } /** - * @brief Remove image file from archive (delete binary file entry) + * @brief Check if a binary file is referenced by any relationships in this worksheet + * @param imagePath The relative image path (e.g., "../media/image_3.gif") + * @return True if the file is referenced by at least one relationship in this worksheet + */ + bool XLWorksheet::isBinaryFileReferenced(const std::string& imagePath) const + { + try { + // Get the drawing relationships filename + std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + + // Check if relationships file exists in archive + if (!parentDoc().hasRelationshipsFile(relsFilename)) { + return false; // No relationships file + } + + // Alternate implementation using XLXmlData access + // Get relationships XML data from in-memory cache + XLXmlData* relsXmlData = const_cast(parentDoc()).getRelationshipsXmlData(relsFilename); + if (!relsXmlData || !relsXmlData->valid()) { + return false; // No relationships data available + } + + // Parse the relationships XML from in-memory data + XMLDocument* relsDoc = relsXmlData->getXmlDocument(); + if (!relsDoc || !relsDoc->document_element()) { + return false; // Invalid XML data + } + + // Check if any relationship references the image path + for (auto rel : relsDoc->document_element().children("Relationship")) { + std::string target = rel.attribute("Target").value(); + if (target == imagePath) { + return true; // Found at least one reference + } + } + + return false; // No references found + + } catch (const std::exception&) { + // If any error occurs, assume no references to be safe + return false; + } + } + + /** + * @brief Remove image file from archive if no other relationships reference it * @param relationshipId The relationship ID of the image file to remove - * @return True if the file was found and removed from archive + * @return True if the file was processed (deleted if no references remain, preserved if still referenced) */ - bool XLWorksheet::removeImageFileFromArchive(const std::string& relationshipId) const + bool XLWorksheet::removeImageFileFromArchiveIfUnreferenced(const std::string& relationshipId) const { try { // Use standard sequential numbering approach (consistent with rest of codebase) @@ -946,42 +1198,53 @@ namespace OpenXLSX return false; } - std::string relsContent = parentDoc().readRelationshipsFile(relsFilename); - if (relsContent.empty()) { - return false; + // Alternate implementation using XLXmlData access + // Get relationships XML data from in-memory cache + XLXmlData* relsXmlData = const_cast(parentDoc()).getRelationshipsXmlData(relsFilename); + if (!relsXmlData || !relsXmlData->valid()) { + return false; } - pugi::xml_document relsDoc; - if (!relsDoc.load_string(relsContent.c_str())) { + // Parse the relationships XML from in-memory data + XMLDocument* relsDoc = relsXmlData->getXmlDocument(); + if (!relsDoc || !relsDoc->document_element()) { return false; } - std::string imagePath = ""; - for (auto rel : relsDoc.document_element().children("Relationship")) { - std::string relId = rel.attribute("Id").value(); - if (relId == relationshipId) { + // Find the relationship and get the image path + std::string imagePath; + for (auto rel : relsDoc->document_element().children("Relationship")) { + if (rel.attribute("Id").value() == relationshipId) { imagePath = rel.attribute("Target").value(); break; } } if (!imagePath.empty()) { - // Convert relative path to absolute path within the archive - std::string fullImagePath = "xl/media/" + imagePath.substr(imagePath.find_last_of('/') + 1); + // Check if binary file is still referenced before deleting + bool isReferenced = isBinaryFileReferenced(imagePath); - // Remove the image file from the archive - // This deletes the binary file entry from the in-memory archive - try { - bool result = const_cast(parentDoc()).deleteEntry(fullImagePath); - return result; - } catch (const std::exception&) { - return false; + if (!isReferenced) { + // Only delete binary file if no relationships still reference it + // Convert relative path to absolute path within the archive + std::string fullImagePath = "xl/media/" + imagePath.substr(imagePath.find_last_of('/') + 1); + + // Remove the image file from the archive + // This deletes the binary file entry from the in-memory archive + try { + bool result = const_cast(parentDoc()).deleteEntry(fullImagePath); + return result; + } catch (const std::exception&) { + return false; + } + } else { + // Multiple references exist - don't delete binary file yet + // Just return true since the relationship was removed successfully + return true; } } else { return false; } - - return false; } catch (const std::exception&) { return false; diff --git a/OpenXLSX/sources/XLXmlData.cpp b/OpenXLSX/sources/XLXmlData.cpp index 11dcf704..f40440dc 100644 --- a/OpenXLSX/sources/XLXmlData.cpp +++ b/OpenXLSX/sources/XLXmlData.cpp @@ -46,6 +46,9 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. // ===== External Includes ===== // #include #include +#include +#include +#include // ===== OpenXLSX Includes ===== // #include "XLDocument.hpp" @@ -181,3 +184,232 @@ const XMLDocument* XLXmlData::getXmlDocument() const return m_xmlDoc.get(); } + +int XLXmlData::verifyData(std::string* dbgMsg) const +{ + int errorCount = 0; + + // Call verifyUniqueXMLRecords to check for duplicates + errorCount += verifyUniqueXMLRecords(dbgMsg); + + return errorCount; +} + +int XLXmlData::verifyUniqueXMLRecords(std::string* dbgMsg) const +{ + int errorCount = 0; + + // Get the XML document and call recursive verification on root node + const XMLDocument* xmlDoc = getXmlDocument(); + if (xmlDoc != nullptr) { + errorCount += verifyUniqueXMLRecordsRecursive(xmlDoc->document_element(), dbgMsg); + } + + return errorCount; +} + +/* +pugi::xml_node::operator<() only compares pointers. This function compares data. +*/ +int XLXmlData::compareXMLNode(const pugi::xml_node& x, const pugi::xml_node& y) +{ + // Handle empty nodes + if (x.empty() && !y.empty()) return -1; + if (y.empty() && !x.empty()) return 1; + if (x.empty() && y.empty()) return 0; + + // Compare node types + if (x.type() != y.type()) { + return (x.type() < y.type()) ? -1 : 1; + } + + // Compare names (handle null pointers) + const char* xName = x.name(); + const char* yName = y.name(); + + if (xName == nullptr && yName != nullptr) return -1; + if (yName == nullptr && xName != nullptr) return 1; + if (xName == nullptr && yName == nullptr) { + // Both null, continue to value comparison + } else { + int nameCompare = std::strcmp(xName, yName); + if (nameCompare != 0) return nameCompare; + } + + // Compare values + const char* xValue = x.value(); + const char* yValue = y.value(); + + if (xValue == nullptr && yValue != nullptr) return -1; + if (yValue == nullptr && xValue != nullptr) return 1; + if (xValue == nullptr && yValue == nullptr) { + // Both null, continue to attribute comparison + } else { + int valueCompare = std::strcmp(xValue, yValue); + if (valueCompare != 0) return valueCompare; + } + + // Compare attributes (this is crucial for distinguishing nodes) + auto xAttr = x.attributes_begin(); + auto yAttr = y.attributes_begin(); + auto xAttrEnd = x.attributes_end(); + auto yAttrEnd = y.attributes_end(); + + while (xAttr != xAttrEnd && yAttr != yAttrEnd) { + // Compare attribute names + int attrNameCompare = std::strcmp(xAttr->name(), yAttr->name()); + if (attrNameCompare != 0) return attrNameCompare; + + // Compare attribute values + int attrValueCompare = std::strcmp(xAttr->value(), yAttr->value()); + if (attrValueCompare != 0) return attrValueCompare; + + ++xAttr; + ++yAttr; + } + + // If one has more attributes than the other + if (xAttr != xAttrEnd) return 1; // x has more attributes + if (yAttr != yAttrEnd) return -1; // y has more attributes + + return 0; // All comparisons equal +} + +bool XLXmlData::lessXMLNode(const pugi::xml_node& x, const pugi::xml_node& y) +{ + const int compareResult = compareXMLNode(x, y); + return (compareResult < 0); +} + + +int XLXmlData::verifyUniqueXMLRecordsRecursive(const pugi::xml_node& rootNode, std::string* dbgMsg) +{ + int errorCount = 0; + + std::stack nodeStack; + if( !rootNode.empty() ){ + nodeStack.push(rootNode); + } + std::vector children; // Declare outside the loop + + while (!nodeStack.empty()) { + const pugi::xml_node currentNode = nodeStack.top(); + nodeStack.pop(); + + // Clear and collect children using first_child() and next_sibling() + // Only check element nodes for duplicate detection - ignore formatting nodes + children.resize(0); + for (pugi::xml_node child = currentNode.first_child(); !child.empty(); child = child.next_sibling()) { + // Only process element nodes (node_element) - these contain actual data + // Only process leaf nodes + if ((child.type() == pugi::node_element) && (child.first_child().empty()) ) { + children.push_back(child); + } + } + + // Sort children using lessXMLNode comparison function + std::sort(children.begin(), children.end(), &XLXmlData::lessXMLNode); + + // Iterate sorted children and detect duplicates + for (size_t i = 1; i < children.size(); ++i) { + if (compareXMLNode(children[i-1], children[i]) == 0) { + // Found duplicate nodes + errorCount++; + + // Count how many adjacent children are the same + size_t duplicateCount = 1; + while (i + duplicateCount < children.size() && + compareXMLNode(children[i-1], children[i + duplicateCount]) == 0) { + ++duplicateCount; + } + + if (dbgMsg != nullptr) { + // Create deluxe debug message with comprehensive node information + *dbgMsg += "=== DUPLICATE XML NODES DETECTED ===\n"; + + // Show root node information + *dbgMsg += "Root Node: "; + if (rootNode.empty()) { + *dbgMsg += "[EMPTY ROOT]\n"; + } else { + *dbgMsg += "Type=" + std::to_string(static_cast(rootNode.type())) + + ", Name='" + (rootNode.name() ? rootNode.name() : "[NULL]") + "'" + + ", Value='" + (rootNode.value() ? rootNode.value() : "[NULL]") + "'"; + + // Show root node attributes if any + if (!rootNode.attributes().empty()) { + *dbgMsg += ", Attributes: "; + bool firstAttr = true; + for (const pugi::xml_attribute& attr : rootNode.attributes()) { + std::string attrName = (attr.name()) ? attr.name() : ""; + std::string attrValue = (attr.value()) ? attr.value() : ""; + if (!firstAttr) *dbgMsg += ", "; + *dbgMsg += attrName + "='" + attrValue + "'"; + firstAttr = false; + } + } + *dbgMsg += "\n"; + } + + *dbgMsg += "Parent Node: "; + if (currentNode.empty()) { + *dbgMsg += "[ROOT/DOCUMENT]\n"; + } else { + *dbgMsg += "Type=" + std::to_string(static_cast(currentNode.type())) + + ", Name='" + (currentNode.name() ? currentNode.name() : "[NULL]") + "'" + + ", Value='" + (currentNode.value() ? currentNode.value() : "[NULL]") + "'"; + + // Show parent node attributes if any + if (!currentNode.attributes().empty()) { + *dbgMsg += ", Attributes: "; + bool firstAttr = true; + for (const pugi::xml_attribute& attr : currentNode.attributes()) { + std::string attrName = (attr.name()) ? attr.name() : ""; + std::string attrValue = (attr.value()) ? attr.value() : ""; + if (!firstAttr) *dbgMsg += ", "; + *dbgMsg += attrName + "='" + attrValue + "'"; + firstAttr = false; + } + } + *dbgMsg += "\n"; + } + + *dbgMsg += "Duplicate Count: " + std::to_string(duplicateCount + 1) + " identical nodes\n"; + *dbgMsg += "Duplicate Node Details:\n"; + + // Show details of the first duplicate node + const pugi::xml_node& duplicateNode = children[i-1]; + *dbgMsg += " - Type=" + std::to_string(static_cast(duplicateNode.type())) + + ", Name='" + (duplicateNode.name() ? duplicateNode.name() : "[NULL]") + "'" + + ", Value='" + (duplicateNode.value() ? duplicateNode.value() : "[NULL]") + "'\n"; + + // Show attributes if any + if (!duplicateNode.attributes().empty()) { + *dbgMsg += " - Attributes: "; + bool firstAttr = true; + for (const pugi::xml_attribute& attr : duplicateNode.attributes()) { + std::string attrName = (attr.name()) ? attr.name() : ""; + std::string attrValue = (attr.value()) ? attr.value() : ""; + if (!firstAttr) *dbgMsg += ", "; + *dbgMsg += attrName + "='" + attrValue + "'"; + firstAttr = false; + } + *dbgMsg += "\n"; + } + + *dbgMsg += "=====================================\n"; + } + + // Skip past all the duplicates + i += duplicateCount - 1; + } + } + + // Push children onto the stack for further processing + for (const pugi::xml_node& child : children) { + nodeStack.push(child); + } + } + + return errorCount; +} diff --git a/OpenXLSX/sources/XLXmlFile.cpp b/OpenXLSX/sources/XLXmlFile.cpp index 5b2d6575..cf279c6b 100644 --- a/OpenXLSX/sources/XLXmlFile.cpp +++ b/OpenXLSX/sources/XLXmlFile.cpp @@ -130,3 +130,15 @@ std::string XLXmlFile::getXmlPath() const { return m_xmlData == nullptr ? "" : m_xmlData->getXmlPath(); } + +int XLXmlFile::verifyData(std::string* dbgMsg) const +{ + int errorCount = 0; + + // Call m_xmlData->verifyData() if m_xmlData is not null + if (m_xmlData != nullptr) { + errorCount += m_xmlData->verifyData(dbgMsg); + } + + return errorCount; +} diff --git a/Tests/testXLCellValue.cpp b/Tests/testXLCellValue.cpp index 6ecc839f..f0014001 100644 --- a/Tests/testXLCellValue.cpp +++ b/Tests/testXLCellValue.cpp @@ -185,7 +185,7 @@ TEST_CASE("XLCellValue Tests", "[XLCellValue]") XLCellValue value; XLDocument doc; doc.create("./testXLCellValue.xlsx"); - XLWorksheet wks = doc.workbook().sheet(1); + XLWorksheet wks = doc.workbook().worksheet(1); wks.cell("A1").value() = "Hello OpenXLSX!"; value = wks.cell("A1").value(); diff --git a/Tests/testXLImage.cpp b/Tests/testXLImage.cpp new file mode 100644 index 00000000..f7997331 --- /dev/null +++ b/Tests/testXLImage.cpp @@ -0,0 +1,180 @@ +// +// Created by OpenXLSX Image Test Suite +// + +#include +#include +#include + +using namespace OpenXLSX; + +TEST_CASE("XLImage", "[XLImage]") +{ + SECTION("Basic Image Embedding Test") { + + // Create a new workbook + XLDocument doc; + doc.create("./testXLImage.xlsx"); + + // Get the first worksheet and rename it + XLWorksheet worksheet1 = doc.workbook().worksheet(1); + worksheet1.setName("Images_Sheet1"); + REQUIRE(worksheet1.name() == "Images_Sheet1"); + + // Add a second worksheet + doc.workbook().addWorksheet("Images_Sheet2"); + XLWorksheet worksheet2 = doc.workbook().worksheet("Images_Sheet2"); + REQUIRE(worksheet2.name() == "Images_Sheet2"); + + // Add some images to the first worksheet + try { + // Create a simple PNG image (1x1 pixel red PNG) + static const std::vector red1x1PNGData = { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, + 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0x0F, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + 0x42, 0x60, 0x82 + }; + + // Create XLImage objects and load the PNG data + XLImage image1, image2, image3; + REQUIRE(image1.loadFromData(red1x1PNGData, "image/png")); + REQUIRE(image2.loadFromData(red1x1PNGData, "image/png")); + REQUIRE(image3.loadFromData(red1x1PNGData, "image/png")); + + // Add image to first worksheet + REQUIRE(worksheet1.addImage(image1, 2, 2)); // Row 2, Column 2 + REQUIRE(worksheet1.addImage(image2, 5, 3)); // Row 5, Column 3 + + // Add image to second worksheet + REQUIRE(worksheet2.addImage(image3, 1, 1)); // Row 1, Column 1 + + // Verify the document data integrity + std::string dbgMsg; + int result = doc.verifyData(&dbgMsg); + REQUIRE(result == EXIT_SUCCESS); + + // Basic verification that images were added + REQUIRE(worksheet1.imageCount() > 0); + REQUIRE(worksheet2.imageCount() > 0); + + } catch (const std::exception& e) { + FAIL("Exception during image embedding: " << e.what()); + } + + // Save and close the document + doc.save(); + doc.close(); + + // Clean up + std::remove("./testXLImage.xlsx"); + } +} + +/* +COMPREHENSIVE XLImage UNIT TEST OUTLINE +======================================= + +This outline covers all functions related to embedded images in OpenXLSX: + +1. BASIC IMAGE CREATION AND EMBEDDING + - Test addImage() with different image formats (PNG, JPEG, GIF, BMP) + - Test addImage() with different data sources (vector, file path) + - Test addImage() with different positioning (various row/column combinations) + - Test addImage() with different anchor types (oneCellAnchor, twoCellAnchor) + - Test addImage() with different sizing strategies (original, aspect ratio, exact dimensions) + +2. IMAGE QUERY OPERATIONS + - Test getImageInfos() - retrieve all images in worksheet + - Test getImageInfosAtCell() - retrieve images at specific cell + - Test getImageInfosInRange() - retrieve images in cell range + - Test getImageInfoByImageID() - retrieve specific image by ID + - Test getImageInfoByRelationshipId() - retrieve specific image by relationship ID + - Test imageCount() - verify correct count of images + +3. IMAGE MODIFICATION OPERATIONS + - Test removeImageByImageID() - remove specific image by ID + - Test removeImageByRelationshipId() - remove specific image by relationship ID + - Test clearImages() - remove all images from worksheet + - Test image positioning changes + - Test image size modifications + +4. IMAGE REGISTRY OPERATIONS + - Test refreshImageRegistry() - populate registry from XML + - Test populateImagesFromXML() - populate m_images vector from registry + - Test registry consistency with XML data + - Test registry updates after image operations + +5. DRAWINGML INTEGRATION + - Test DrawingML XML generation for different anchor types + - Test DrawingML XML parsing for Excel-generated files + - Test DrawingML validation and error handling + - Test DrawingML consistency with image registry + +6. RELATIONSHIPS FILE OPERATIONS + - Test relationship file creation and updates + - Test relationship ID generation and uniqueness + - Test relationship file parsing and validation + - Test relationship consistency with image data + +7. BINARY DATA HANDLING + - Test binary image data storage and retrieval + - Test MIME type detection and validation + - Test file extension handling + - Test binary data integrity verification + +8. ERROR HANDLING AND EDGE CASES + - Test invalid image data handling + - Test invalid positioning parameters + - Test duplicate image ID handling + - Test missing relationship file handling + - Test corrupted XML data handling + +9. PERFORMANCE AND MEMORY + - Test memory usage with large images + - Test performance with many images + - Test memory cleanup after image removal + - Test efficient registry operations + +10. CROSS-WORKSHEET OPERATIONS + - Test image operations across multiple worksheets + - Test image ID uniqueness across worksheets + - Test relationship ID uniqueness across worksheets + - Test document-level image operations + +11. FILE I/O OPERATIONS + - Test loading images from existing Excel files + - Test saving images to new Excel files + - Test round-trip operations (save/load/verify) + - Test compatibility with Excel-generated files + +12. DATA INTEGRITY VERIFICATION + - Test verifyData() for various image scenarios + - Test verifyUniqueImageIDs() for duplicate detection + - Test verifyDrawingMLConsistency() for XML validation + - Test verifyImagesVectorConsistency() for vector validation + - Test verifyBinaryImageData() for binary data validation + +13. CONST CORRECTNESS + - Test const member function behavior + - Test const reference returns + - Test const_cast usage validation + - Test mutable member usage + +14. THREAD SAFETY + - Test concurrent image operations + - Test static const member usage + - Test registry access from multiple threads + - Test XML data access from multiple threads + +15. API COMPATIBILITY + - Test backward compatibility with existing code + - Test API consistency across different image types + - Test return type consistency + - Test parameter validation + +This comprehensive test suite would ensure robust image embedding functionality +across all supported scenarios and edge cases. +*/ From 145d762078b3bda1668882b3f1788b0d425f4afd Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Thu, 30 Oct 2025 09:08:07 -0700 Subject: [PATCH 11/13] Embedded Image Support -- phase X / reorganize code --- Examples/Demo11.cpp | 967 +++++------- OpenXLSX/headers/XLContentTypes.hpp | 10 - OpenXLSX/headers/XLDocument.hpp | 15 + OpenXLSX/headers/XLDrawingML.hpp | 271 +++- OpenXLSX/headers/XLImage.hpp | 1408 ++++++++++------- OpenXLSX/headers/XLSheet.hpp | 310 ++-- OpenXLSX/sources/XLDocument.cpp | 12 +- OpenXLSX/sources/XLDrawingML.cpp | 862 ++++++---- OpenXLSX/sources/XLImage.cpp | 1645 ++++++++++++-------- OpenXLSX/sources/XLImageUtils.cpp | 779 +++++++++ OpenXLSX/sources/XLSheet.cpp | 1007 +++++++----- OpenXLSX/sources/XLWorksheetImageQuery.cpp | 1008 +++--------- Tests/testXLImage.cpp | 41 +- 13 files changed, 4838 insertions(+), 3497 deletions(-) create mode 100644 OpenXLSX/sources/XLImageUtils.cpp diff --git a/Examples/Demo11.cpp b/Examples/Demo11.cpp index 89d9ef13..df769d3d 100644 --- a/Examples/Demo11.cpp +++ b/Examples/Demo11.cpp @@ -14,6 +14,7 @@ #include #include #include "OpenXLSX.hpp" +#include "XLImage.hpp" // For XLImageUtils using namespace OpenXLSX; @@ -47,6 +48,7 @@ std::string findDirectory(const std::string& relativePath) } + /* a. get image ID b. get relationshiop ID @@ -57,33 +59,43 @@ std::string findDirectory(const std::string& relativePath) Return image information in the following format "img_id:ABCD r_id:EFGH oneCellAnchor:C44 330x492 px 1249900x1700000 emu" */ -std::string imageInfoStr(const XLImageInfo& imageInfo) { +std::string embeddedImageStr(const XLEmbeddedImage& embImage) { std::string result; // a. get image ID - std::string imageId = imageInfo.imageId; + std::string imageId = embImage.id(); result += "img_id:" + imageId; // b. get relationship ID - result += " r_id:" + imageInfo.relationshipId; + result += " r_id:" + embImage.relationshipId(); + + // c. get anchor type and cell location + const XLImageAnchor& imageAnchor = embImage.getImageAnchor(); + std::string anchorCell = XLEmbeddedImage::rowColToCellRef(imageAnchor.fromRow, imageAnchor.fromCol); + result += " " + XLImageAnchorUtils::anchorTypeToString(imageAnchor.anchorType) + ":" + anchorCell; - // c. get anchor type - // d. get anchor cell location(s) - result += " " + imageInfo.anchorType + ":" + imageInfo.anchorCell; + // d. get image width & height in pixels + result += " " + std::to_string(embImage.widthPixels()) + "x" + std::to_string(embImage.heightPixels()) + " px"; - // e. get image width & height in pixels - result += " " + std::to_string(imageInfo.widthPixels) + "x" + std::to_string(imageInfo.heightPixels) + " px"; + // e. get image displayed width and displayed height in EMUs + result += " " + std::to_string(embImage.displayWidthEMUs()) + "x" + std::to_string(embImage.displayHeightEMUs()) + " EMU"; - // f. get image displayed width and displayed height in EMUs - result += " " + std::to_string(imageInfo.displayWidthEMUs) + "x" + std::to_string(imageInfo.displayHeightEMUs) + " EMU"; + // f. additional debugging info + if (embImage.getImage()) { + result += " package:" + embImage.getImage()->filePackagePath(); + result += " mime:" + XLImageUtils::mimeTypeToString(embImage.mimeType()); + } else { + result += " [NO_IMAGE_OBJECT]"; + } return result; } -// Helper function to print image registry information for a worksheet -void printImageRegistry(const XLWorksheet& worksheet, const std::string& sheetName, int* totalImages = nullptr) { +// Helper function to print embedded images for a worksheet +void printEmbeddedImages(const XLWorksheet& worksheet, const std::string& sheetName, int* totalImages = nullptr) { try { - size_t imageCount = worksheet.imageCount(); + const auto& embImages = worksheet.getEmbImages(); + size_t imageCount = embImages.size(); // Update total count if pointer provided if (totalImages != nullptr) { @@ -91,19 +103,18 @@ void printImageRegistry(const XLWorksheet& worksheet, const std::string& sheetNa } // Print header - auto imageInfos = worksheet.getImageInfos(); const OpenXLSX::XLDrawingML& drawing = worksheet.drawingML(); const size_t drawingImageCount = drawing.imageCount(); std::cout << " Worksheet '" << sheetName << "': " << imageCount << " image(s) " - << imageInfos.size() << " images in registry" + << imageCount << " embedded images" << " drawing valid:" << (drawing.valid() ? "Yes" : "No") << " drawing images:" << drawingImageCount << std::endl; // Print image details if any images exist if (imageCount > 0) { - for (size_t i = 0; i < imageInfos.size(); ++i) { - std::cout << " " << (i + 1) << ". " << imageInfoStr(imageInfos[i]) << std::endl; + for (size_t i = 0; i < embImages.size(); ++i) { + std::cout << " " << (i + 1) << ". " << embeddedImageStr(embImages[i]) << std::endl; } } } catch (const std::exception& e) { @@ -111,23 +122,112 @@ void printImageRegistry(const XLWorksheet& worksheet, const std::string& sheetNa } } -// Helper function to print image registry information for all worksheets -// TODO: Is it ok that image registry is empty? -// TODO: Should the image registry be refreshed as part of this query -void printAllImageRegistries(const XLDocument& doc, int* totalImages = nullptr) { - for (uint16_t sheet_idx = 1; sheet_idx <= doc.workbook().sheetCount(); ++sheet_idx) { - XLSheet sheet = doc.workbook().sheet(sheet_idx); - std::string sheetName = sheet.name(); - if (sheet.isType()) { - XLWorksheet worksheet = sheet.get(); - try { - const auto& imageInfos = worksheet.getImageInfos(); - printImageRegistry(worksheet, sheetName, totalImages); - } catch (const std::exception& e) { - std::cout << " ERROR: Failed to query registry: " << e.what() << std::endl; +// Helper function to print embedded image information for all worksheets +void printAllEmbeddedImages(const XLDocument& doc, int* totalImages = nullptr) { + std::cout << "\n=== EMBEDDED IMAGES ===" << std::endl; + + int totalCount = 0; + if (totalImages != nullptr) { + *totalImages = 0; + } + + // Iterate through all worksheets using workbook + auto workbook = doc.workbook(); + auto sheetNames = workbook.worksheetNames(); + + for (const auto& sheetName : sheetNames) { + try { + XLWorksheet worksheet = workbook.worksheet(sheetName); + printEmbeddedImages(worksheet, sheetName, &totalCount); + } catch (const std::exception& e) { + std::cout << "Error accessing worksheet '" << sheetName << "': " << e.what() << std::endl; + } + } + + std::cout << "\nTotal embedded images across all worksheets: " << totalCount << std::endl; + if (totalImages != nullptr) { + *totalImages = totalCount; + } +} + +// Comprehensive cross-reference verification function +void verifyImageConsistency(const XLDocument& doc) { + std::cout << "\n=== IMAGE CONSISTENCY VERIFICATION ===" << std::endl; + + int totalErrors = 0; + + // Iterate through all worksheets using workbook + auto workbook = doc.workbook(); + auto sheetNames = workbook.worksheetNames(); + + for (const auto& sheetName : sheetNames) { + try { + XLWorksheet worksheet = workbook.worksheet(sheetName); + std::cout << "\n--- Verifying worksheet: " << sheetName << " ---" << std::endl; + + // Get embedded images data + const auto& embImages = worksheet.getEmbImages(); + + std::cout << "EmbeddedImages count: " << embImages.size() << std::endl; + + // Check count consistency (no longer needed since we only have one source) + std::cout << "Embedded images verification complete." << std::endl; + + // Cross-reference each image + size_t minCount = embImages.size(); + for (size_t i = 0; i < minCount; ++i) { + const auto& embImage = embImages[i]; + + std::cout << " Image " << (i+1) << ":" << std::endl; + + // Check image ID consistency (no longer needed since we only have one source) + std::cout << " Image ID: " << embImage.id() << " ✓" << std::endl; + + // Check relationship ID consistency + std::cout << " Relationship ID: " << embImage.relationshipId() << " ✓" << std::endl; + + // Check anchor cell consistency + std::cout << " Anchor Cell: " << embImage.anchorCell() << " ✓" << std::endl; + + // Check anchor type consistency + std::cout << " Anchor Type: " << XLImageAnchorUtils::anchorTypeToString( + embImage.anchorType()) << " ✓" << std::endl; + + // Check dimensions consistency + std::cout << " Dimensions: " << embImage.widthPixels() << "x" + << embImage.heightPixels() << " px ✓" << std::endl; + + // Check display dimensions consistency + std::cout << " Display Dimensions: " << embImage.displayWidthEMUs() << "x" + << embImage.displayHeightEMUs() << " EMUs ✓" << std::endl; + + std::cout << " Summary: " << embeddedImageStr(embImage) << std::endl; + } + + // Run built-in verifyData + std::string verifyMsg; + int verifyErrors = worksheet.verifyData(&verifyMsg); + if (verifyErrors > 0) { + std::cout << "Built-in verifyData found " << verifyErrors << " errors:" << std::endl; + std::cout << verifyMsg << std::endl; + totalErrors += verifyErrors; + } else { + std::cout << "Built-in verifyData: No errors ✓" << std::endl; } + + } catch (const std::exception& e) { + std::cout << "Error verifying worksheet '" << sheetName << "': " << e.what() << std::endl; + totalErrors++; } } + + std::cout << "\n=== VERIFICATION SUMMARY ===" << std::endl; + std::cout << "Total errors found: " << totalErrors << std::endl; + if (totalErrors == 0) { + std::cout << "All image data is consistent! ✓" << std::endl; + } else { + std::cout << "Image data inconsistencies detected!" << std::endl; + } } // Helper function to print detailed image analysis for a worksheet @@ -137,29 +237,27 @@ void printDetailedImageAnalysis(const XLWorksheet& worksheet, const std::string& if (imageCount > 0) { try { - const auto& imageInfos = worksheet.getImageInfos(); + const auto& embImages = worksheet.getEmbImages(); std::vector anchorCells; std::vector imageIds; std::vector relationshipIds; - for (size_t i = 0; i < imageInfos.size(); ++i) { - std::cout << " " << (i + 1) << ". " << imageInfoStr(imageInfos[i]) << std::endl; - anchorCells.push_back(imageInfos[i].anchorCell); - imageIds.push_back(imageInfos[i].imageId); - relationshipIds.push_back(imageInfos[i].relationshipId); + for (size_t i = 0; i < embImages.size(); ++i) { + std::cout << " " << (i + 1) << ". " << embeddedImageStr(embImages[i]) << std::endl; + anchorCells.push_back(embImages[i].anchorCell()); + imageIds.push_back(embImages[i].id()); + relationshipIds.push_back(embImages[i].relationshipId()); } - // Test getImageInfosAtCell() + // Test getEmbImagesAtCell() for( const std::string& anchorCell : anchorCells ){ - const auto& cellImgs = worksheet.getImageInfosAtCell(anchorCell); - std::cout << " getImageInfosAtCell('" << anchorCell << "') returned " + const auto& cellImgs = worksheet.getEmbImagesAtCell(anchorCell); + std::cout << " getEmbImagesAtCell('" << anchorCell << "') returned " << cellImgs.size() << " image(s)" << std::endl; } // Test getImageInfoByImageID() for( const std::string& imageId : imageIds ){ - // TODO: Should imageId be unique, or is it ok to just get the first image that matches imageId? - // TODO: If imageId should be unique, getImageInfoByImageID() should, in DEBUG mode, check for uniqueness. - const auto& foundImg = worksheet.getImageInfoByImageID(imageId); + const auto& foundImg = worksheet.getEmbImageByImageID(imageId); if (foundImg.isValid()) { std::cout << " getImageInfoByImageID( " << imageId << ") found image" << std::endl; } else { @@ -169,8 +267,7 @@ void printDetailedImageAnalysis(const XLWorksheet& worksheet, const std::string& // Test getImageInfoByRelationshipId() for( const std::string& relationshipId : relationshipIds ){ - // TODO: If relationshipId should be unique, getImageInfoByRelationshipId() should, in DEBUG mode, check for uniqueness. - const auto& foundRel = worksheet.getImageInfoByRelationshipId(relationshipId); + const auto& foundRel = worksheet.getEmbImageByRelationshipId(relationshipId); if (foundRel.isValid()) { std::cout << " ✓ getImageInfoByRelationshipId( " << relationshipId << ") found the image" << std::endl; } else { @@ -178,9 +275,9 @@ void printDetailedImageAnalysis(const XLWorksheet& worksheet, const std::string& } } - // Test getImageInfosInRange() - const auto& rangeImgs = worksheet.getImageInfosInRange("A1:Z100"); - std::cout << " getImageInfosInRange('A1:Z100') returned " << rangeImgs.size() << " image(s)" << std::endl; + // Test getEmbImagesInRange() + const auto& rangeImgs = worksheet.getEmbImagesInRange("A1:Z100"); + std::cout << " getEmbImagesInRange('A1:Z100') returned " << rangeImgs.size() << " image(s)" << std::endl; // Test validation bool isRegstryValid = worksheet.validateImageRegistry(); @@ -214,393 +311,6 @@ void verifyWorksheetData(const XLWorksheet& worksheet) } } - // These contain the binary data from the tiny image files in the images directory - - // tiny_png.png - Small PNG image - static const std::vector pngData = { - 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0f, - 0x08, 0x02, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x83, 0xf5, 0x00, 0x00, 0x00, - 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, - 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, - 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, - 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, - 0x00, 0x01, 0xa8, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4f, 0x63, 0x7c, 0xff, - 0x64, 0x03, 0x03, 0xcd, 0x00, 0x13, 0x94, 0xa6, 0x0d, 0x00, 0xbb, 0xfd, - 0xf1, 0xfe, 0x7f, 0xa1, 0x5b, 0xa1, 0x02, 0x50, 0x20, 0xc5, 0xb8, 0xba, - 0x88, 0x51, 0x96, 0x81, 0xe1, 0xfb, 0x85, 0x98, 0x6b, 0x67, 0x18, 0xa4, - 0x42, 0x96, 0x48, 0x0a, 0x40, 0x65, 0x40, 0x22, 0xef, 0xd3, 0x8c, 0x1d, - 0xed, 0xa0, 0x7c, 0xce, 0x89, 0x29, 0xc2, 0xfb, 0xa1, 0x6c, 0x06, 0xc5, - 0xe0, 0x17, 0xfd, 0x9e, 0x7f, 0xa0, 0x1c, 0x06, 0x06, 0x16, 0x28, 0xcd, - 0x60, 0xcc, 0x78, 0x22, 0x92, 0x11, 0xca, 0x66, 0x60, 0x38, 0xb2, 0xfc, - 0x5f, 0x68, 0x1f, 0x03, 0xd0, 0x02, 0x11, 0x10, 0xef, 0xfa, 0xb3, 0xf3, - 0x87, 0x24, 0xe1, 0xc6, 0x21, 0x81, 0x17, 0x7c, 0x85, 0x35, 0x7c, 0x0a, - 0xd9, 0x4f, 0x36, 0x18, 0x42, 0xf8, 0x40, 0x9b, 0x24, 0x0a, 0x19, 0x10, - 0x16, 0xe0, 0x08, 0x19, 0x1b, 0x17, 0x46, 0x8d, 0x67, 0x0c, 0x0f, 0x21, - 0x1c, 0xe5, 0x7c, 0xa9, 0xf7, 0xb3, 0x9e, 0x7f, 0x80, 0x70, 0x90, 0x00, - 0xcb, 0xba, 0x99, 0x7c, 0x0c, 0xc1, 0x2f, 0xf2, 0xa1, 0x46, 0x03, 0xc1, - 0xf7, 0xfc, 0x96, 0x4f, 0x0c, 0x6b, 0xf9, 0x4e, 0x41, 0xb9, 0xc4, 0x85, - 0xbb, 0x82, 0xa4, 0x93, 0xdb, 0xfb, 0xf3, 0x87, 0xa0, 0x3c, 0x18, 0x78, - 0xc1, 0x75, 0xf8, 0xf1, 0xb7, 0x48, 0xa4, 0x70, 0x00, 0x01, 0x89, 0x4f, - 0xfd, 0x73, 0xde, 0x99, 0x41, 0x39, 0xb8, 0x4c, 0x3f, 0xb2, 0xe7, 0xff, - 0x0d, 0x63, 0x06, 0x1b, 0x28, 0x8f, 0x81, 0x41, 0x20, 0x4e, 0x8a, 0xa1, - 0xf0, 0x1e, 0xd4, 0x2f, 0x50, 0xf0, 0x9c, 0xe5, 0xbe, 0xec, 0x1f, 0x19, - 0x28, 0x07, 0x3b, 0x80, 0x87, 0xfb, 0xd9, 0xff, 0x16, 0x67, 0xff, 0x43, - 0xd9, 0x40, 0x00, 0x8e, 0x55, 0x28, 0x1b, 0x0c, 0x04, 0x0c, 0xf3, 0x9f, - 0xed, 0x5b, 0xf4, 0x5d, 0x3e, 0x0e, 0xca, 0x47, 0x07, 0xa7, 0xe6, 0xc8, - 0xb4, 0x9d, 0x80, 0x30, 0xff, 0xc4, 0xb5, 0xbc, 0x08, 0x92, 0x00, 0xb1, - 0x70, 0xc4, 0x2a, 0x16, 0x20, 0x10, 0xa7, 0xa8, 0x14, 0xf3, 0xfc, 0x61, - 0x9c, 0x24, 0x94, 0xcf, 0x20, 0xf9, 0x47, 0xf1, 0x31, 0xcb, 0x13, 0xa0, - 0x43, 0xc0, 0x3c, 0xb3, 0x94, 0x27, 0x1b, 0x52, 0x80, 0x34, 0x30, 0x62, - 0xf9, 0xc1, 0x02, 0x20, 0x40, 0x4a, 0x7a, 0xe7, 0x34, 0x48, 0x63, 0xd8, - 0xdd, 0x00, 0x8f, 0x5d, 0x89, 0x6f, 0xb6, 0xb2, 0x5c, 0xcb, 0xb7, 0xc3, - 0xdd, 0x87, 0x05, 0x90, 0x96, 0x9b, 0xec, 0x24, 0x4d, 0xee, 0x3c, 0x3b, - 0x73, 0x1d, 0xca, 0xfb, 0x13, 0x94, 0x0e, 0x4c, 0x21, 0x12, 0x85, 0x08, - 0x0b, 0x58, 0xd6, 0x35, 0x22, 0xd2, 0x3e, 0x10, 0xe0, 0xb3, 0x19, 0x0b, - 0xe0, 0x34, 0x68, 0x95, 0xba, 0x17, 0xf4, 0x0c, 0xca, 0x03, 0xa7, 0x90, - 0x6f, 0xeb, 0x1a, 0x25, 0x02, 0xd6, 0x42, 0x05, 0x18, 0x2c, 0xde, 0x6e, - 0xa8, 0xff, 0x0e, 0x65, 0x43, 0xf3, 0x2a, 0xcd, 0x00, 0x69, 0x21, 0x43, - 0x2a, 0xa0, 0xa5, 0xe9, 0x0c, 0x0c, 0x00, 0xe4, 0x3f, 0x87, 0xe1, 0xb6, - 0x42, 0x8a, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, - 0x42, 0x60, 0x82 - }; - - // tiny_jpeg.jpg - Small JPEG image (actual file data from xxd) - static const std::vector jpegData = { - 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, - 0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xe1, 0x00, 0x22, - 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, - 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, - 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, - 0x02, 0x02, 0x02, 0x03, 0x05, 0x03, 0x03, 0x03, 0x03, 0x03, 0x06, 0x04, - 0x04, 0x03, 0x05, 0x07, 0x06, 0x07, 0x07, 0x07, 0x06, 0x07, 0x07, 0x08, - 0x09, 0x0b, 0x09, 0x08, 0x08, 0x0a, 0x08, 0x07, 0x07, 0x0a, 0x0d, 0x0a, - 0x0a, 0x0b, 0x0c, 0x0c, 0x0c, 0x0c, 0x07, 0x09, 0x0e, 0x0f, 0x0d, 0x0c, - 0x0e, 0x0b, 0x0c, 0x0c, 0x0c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x02, 0x02, - 0x02, 0x03, 0x03, 0x03, 0x06, 0x03, 0x03, 0x06, 0x0c, 0x08, 0x07, 0x08, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, - 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x12, 0x00, 0x20, 0x03, - 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, - 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, - 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, - 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, - 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, - 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, - 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, - 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, - 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, - 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, - 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, - 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, - 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, - 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, - 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, - 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, - 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, - 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, - 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, - 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, - 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, - 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, - 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, - 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, - 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, - 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, - 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, - 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, - 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, - 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, - 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, - 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, - 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, - 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, - 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfd, - 0x5e, 0xf8, 0xcf, 0xf1, 0x17, 0xfe, 0x15, 0x17, 0xc2, 0x5f, 0x12, 0x78, - 0xa7, 0xec, 0x7f, 0xda, 0x1f, 0xf0, 0x8f, 0xe9, 0xd3, 0xea, 0x1f, 0x65, - 0xf3, 0x7c, 0x9f, 0xb4, 0x79, 0x68, 0x5b, 0x66, 0xfd, 0xad, 0xb7, 0x38, - 0xc6, 0x70, 0x71, 0xe8, 0x6b, 0x26, 0x4f, 0x88, 0xfe, 0x29, 0xd6, 0x3c, - 0x59, 0xe2, 0x0d, 0x3f, 0x43, 0xf0, 0xee, 0x83, 0x79, 0x6d, 0xa0, 0x5c, - 0xa5, 0xab, 0xcd, 0x7b, 0xae, 0x4b, 0x6b, 0x24, 0xce, 0xd0, 0x47, 0x37, - 0x08, 0xb6, 0xb2, 0x00, 0x31, 0x20, 0x1c, 0xbf, 0x51, 0xda, 0xb9, 0xdf, - 0xda, 0x7b, 0x4d, 0xf1, 0xaf, 0x8e, 0xbc, 0x3d, 0xe2, 0x3f, 0x06, 0xe9, - 0x3e, 0x1b, 0x5d, 0x4f, 0x45, 0xf1, 0x76, 0x86, 0x34, 0xdb, 0x6d, 0x4e, - 0x2b, 0xa8, 0x22, 0xfe, 0xc9, 0xba, 0x95, 0xe5, 0x8e, 0x79, 0x2e, 0x84, - 0x92, 0xab, 0xb4, 0x2b, 0x13, 0x42, 0xeb, 0xe4, 0x47, 0x23, 0xe5, 0x25, - 0x04, 0x72, 0x95, 0xa9, 0xa6, 0xfc, 0x0f, 0x8b, 0x5b, 0xf1, 0xe7, 0x8c, - 0x35, 0x1d, 0x5c, 0xeb, 0xd6, 0xd0, 0xea, 0x7a, 0x84, 0x52, 0x5a, 0x7d, - 0x8b, 0x5f, 0xbb, 0xb2, 0x8e, 0x78, 0x85, 0xac, 0x08, 0x49, 0x8e, 0xde, - 0x65, 0x5c, 0xef, 0x57, 0x19, 0x61, 0xb8, 0x81, 0xe9, 0x8a, 0xfc, 0x3f, - 0x56, 0xfe, 0x4f, 0xef, 0xba, 0xff, 0x00, 0x83, 0xf9, 0x9f, 0x53, 0xc4, - 0x58, 0x4a, 0xd4, 0xb0, 0x18, 0x47, 0x82, 0x92, 0xf6, 0xb5, 0x2a, 0x4f, - 0x9f, 0x96, 0x49, 0xb5, 0x4f, 0xd9, 0xa7, 0x1e, 0x6f, 0x76, 0x7c, 0x9e, - 0xfd, 0xd6, 0xb1, 0x4d, 0xbd, 0x2f, 0x6b, 0x15, 0x57, 0xf6, 0x8a, 0xbf, - 0xf1, 0x8c, 0xda, 0x3e, 0x9f, 0xe0, 0xdf, 0x0d, 0xc5, 0xab, 0xeb, 0xba, - 0x86, 0x9c, 0xba, 0xb5, 0xe4, 0x1a, 0x9e, 0xa3, 0xfd, 0x9f, 0x69, 0xa4, - 0xdb, 0x33, 0xbc, 0x6b, 0xe7, 0x4f, 0x1c, 0x53, 0x93, 0x23, 0xc9, 0x1c, - 0x8a, 0x88, 0x91, 0xb0, 0x61, 0x14, 0x8c, 0x59, 0x40, 0x1b, 0xba, 0xff, - 0x00, 0x86, 0x3e, 0x34, 0xd5, 0x3c, 0x61, 0xa5, 0xde, 0xae, 0xb7, 0xe1, - 0xfb, 0x8f, 0x0e, 0xea, 0xda, 0x5d, 0xe3, 0xd9, 0xdc, 0xdb, 0x99, 0x0c, - 0xf6, 0xd3, 0x10, 0x15, 0x96, 0x6b, 0x69, 0xca, 0x27, 0x9d, 0x0b, 0xab, - 0xa9, 0x0d, 0xb1, 0x48, 0x3b, 0x95, 0x95, 0x59, 0x58, 0x0e, 0x23, 0xc4, - 0x7e, 0x00, 0xd5, 0x3e, 0x14, 0x7c, 0x46, 0x9b, 0x5c, 0xf0, 0xcf, 0x86, - 0xae, 0x35, 0xff, 0x00, 0x0f, 0xeb, 0x1a, 0x1d, 0xb6, 0x83, 0x7d, 0xa4, - 0xe9, 0x57, 0x70, 0xd9, 0x5f, 0xd8, 0x0b, 0x66, 0x9d, 0xa0, 0x96, 0xdd, - 0xa6, 0x92, 0x28, 0xca, 0x15, 0xb8, 0x74, 0x71, 0xe6, 0xa3, 0xa9, 0x58, - 0xd9, 0x77, 0x1d, 0xc0, 0x5a, 0xfd, 0x98, 0xfc, 0x11, 0xe2, 0x0f, 0x09, - 0x5b, 0x78, 0x9a, 0xe3, 0x5a, 0x87, 0x5c, 0xd3, 0xb4, 0xfd, 0x5b, 0x52, - 0x59, 0xf4, 0x7d, 0x2b, 0x58, 0xf1, 0x0c, 0xda, 0xe5, 0xf6, 0x99, 0x6c, - 0xb0, 0xc7, 0x11, 0x59, 0x66, 0x92, 0x49, 0x42, 0xb3, 0xc8, 0x8f, 0x2e, - 0xc8, 0xe5, 0x91, 0x54, 0x48, 0x06, 0xec, 0x82, 0x07, 0xd7, 0x63, 0x30, - 0x79, 0x7f, 0xf6, 0x73, 0xab, 0x47, 0x92, 0xe9, 0x41, 0xa7, 0xcd, 0xef, - 0xca, 0x4d, 0xfb, 0xf1, 0x71, 0xf6, 0x8e, 0xdc, 0xad, 0xbb, 0x7e, 0xee, - 0xce, 0x31, 0x4f, 0x9e, 0xef, 0xde, 0xf2, 0xf0, 0xaf, 0x13, 0x1e, 0x48, - 0x57, 0x77, 0x95, 0x92, 0x93, 0x4b, 0x46, 0xf9, 0x55, 0xda, 0x76, 0x5b, - 0xca, 0xfd, 0xad, 0xb5, 0x8f, 0x52, 0xa2, 0x8a, 0x2b, 0xe4, 0x4f, 0x4c, - 0x28, 0xa2, 0x8a, 0x00, 0xff, 0xd9 - }; - - // tiny_bmp.bmp - Small BMP image (actual file data from xxd) - static const std::vector bmpData = { - 0x42, 0x4d, 0x76, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, - 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x10, 0x00, - 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, - 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, 0xb1, 0x22, 0xb0, 0xdb, 0x91, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x3e, 0x89, - 0xef, 0x6f, 0x44, 0xed, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x4c, 0xb1, - 0x22, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, - 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, - 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0x91, 0xe4, 0xef, 0x5e, - 0xb1, 0x6f, 0x80, 0xd4, 0x91, 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x24, 0x1c, 0xee, - 0xb0, 0xa8, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0x80, 0xdb, 0xef, 0x80, 0xba, 0x4b, 0x91, 0xe4, 0xd0, 0x6f, 0xb1, 0x6f, - 0xb0, 0xe4, 0xb1, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, - 0xef, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0x5e, 0xcb, 0xd0, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, - 0xef, 0x4c, 0xc2, 0xb1, 0xb0, 0xd4, 0x6f, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, - 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, - 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x24, - 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, - 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x9b, 0x89, 0xed, 0xb0, 0xe4, 0xef, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xa0, 0xe4, 0xb1, 0x5e, 0xba, 0x91, 0xb0, - 0xdb, 0x91, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x91, 0xc2, 0x22, 0x80, - 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc2, 0x48, 0x3f, 0xb0, 0xe4, 0xb9, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x85, 0x67, 0xed, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0x80, 0xdb, 0xb1, - 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0xb0, 0xe4, 0xef, 0x91, 0xe4, 0xef, - 0x6f, 0xb1, 0x6f, 0x80, 0xdb, 0xb1, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, 0xef, 0xcc, 0x65, 0x9d, - 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, - 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, - 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, - 0x4b, 0x5e, 0xcb, 0x91, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x80, 0xcb, 0x6f, 0x6f, 0xb1, - 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, - 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, - 0xef, 0xcc, 0x65, 0x9d, 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, - 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0x80, - 0xdb, 0xef, 0x5e, 0xb1, 0x4b, 0x4c, 0xba, 0x4b, 0xb0, 0xd4, 0x6f, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, - 0xba, 0x22, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, - 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, - 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, - 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x4c, 0xb1, 0x4b, 0x6f, 0xb1, 0x22, - 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0x91, 0xe4, 0xef, 0x4c, 0xb1, 0x6f, 0x6f, 0xb1, 0x22, 0xb0, 0xe4, 0xb1, - 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, - 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, - 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, - 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, - 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00 - }; - - // tiny_gif.gif - Small GIF image (actual file data from xxd) - static const std::vector gifData = { - 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x1a, 0x00, 0x10, 0x00, 0x70, 0x00, - 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x2c, 0x00, 0x00, - 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x33, 0x00, 0x00, 0x66, 0x00, 0x00, 0x99, 0x00, 0x00, 0xcc, 0x00, 0x00, - 0xff, 0x00, 0x2b, 0x00, 0x00, 0x2b, 0x33, 0x00, 0x2b, 0x66, 0x00, 0x2b, - 0x99, 0x00, 0x2b, 0xcc, 0x00, 0x2b, 0xff, 0x00, 0x55, 0x00, 0x00, 0x55, - 0x33, 0x00, 0x55, 0x66, 0x00, 0x55, 0x99, 0x00, 0x55, 0xcc, 0x00, 0x55, - 0xff, 0x00, 0x80, 0x00, 0x00, 0x80, 0x33, 0x00, 0x80, 0x66, 0x00, 0x80, - 0x99, 0x00, 0x80, 0xcc, 0x00, 0x80, 0xff, 0x00, 0xaa, 0x00, 0x00, 0xaa, - 0x33, 0x00, 0xaa, 0x66, 0x00, 0xaa, 0x99, 0x00, 0xaa, 0xcc, 0x00, 0xaa, - 0xff, 0x00, 0xd5, 0x00, 0x00, 0xd5, 0x33, 0x00, 0xd5, 0x66, 0x00, 0xd5, - 0x99, 0x00, 0xd5, 0xcc, 0x00, 0xd5, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, - 0x33, 0x00, 0xff, 0x66, 0x00, 0xff, 0x99, 0x00, 0xff, 0xcc, 0x00, 0xff, - 0xff, 0x33, 0x00, 0x00, 0x33, 0x00, 0x33, 0x33, 0x00, 0x66, 0x33, 0x00, - 0x99, 0x33, 0x00, 0xcc, 0x33, 0x00, 0xff, 0x33, 0x2b, 0x00, 0x33, 0x2b, - 0x33, 0x33, 0x2b, 0x66, 0x33, 0x2b, 0x99, 0x33, 0x2b, 0xcc, 0x33, 0x2b, - 0xff, 0x33, 0x55, 0x00, 0x33, 0x55, 0x33, 0x33, 0x55, 0x66, 0x33, 0x55, - 0x99, 0x33, 0x55, 0xcc, 0x33, 0x55, 0xff, 0x33, 0x80, 0x00, 0x33, 0x80, - 0x33, 0x33, 0x80, 0x66, 0x33, 0x80, 0x99, 0x33, 0x80, 0xcc, 0x33, 0x80, - 0xff, 0x33, 0xaa, 0x00, 0x33, 0xaa, 0x33, 0x33, 0xaa, 0x66, 0x33, 0xaa, - 0x99, 0x33, 0xaa, 0xcc, 0x33, 0xaa, 0xff, 0x33, 0xd5, 0x00, 0x33, 0xd5, - 0x33, 0x33, 0xd5, 0x66, 0x33, 0xd5, 0x99, 0x33, 0xd5, 0xcc, 0x33, 0xd5, - 0xff, 0x33, 0xff, 0x00, 0x33, 0xff, 0x33, 0x33, 0xff, 0x66, 0x33, 0xff, - 0x99, 0x33, 0xff, 0xcc, 0x33, 0xff, 0xff, 0x66, 0x00, 0x00, 0x66, 0x00, - 0x33, 0x66, 0x00, 0x66, 0x66, 0x00, 0x99, 0x66, 0x00, 0xcc, 0x66, 0x00, - 0xff, 0x66, 0x2b, 0x00, 0x66, 0x2b, 0x33, 0x66, 0x2b, 0x66, 0x66, 0x2b, - 0x99, 0x66, 0x2b, 0xcc, 0x66, 0x2b, 0xff, 0x66, 0x55, 0x00, 0x66, 0x55, - 0x33, 0x66, 0x55, 0x66, 0x66, 0x55, 0x99, 0x66, 0x55, 0xcc, 0x66, 0x55, - 0xff, 0x66, 0x80, 0x00, 0x66, 0x80, 0x33, 0x66, 0x80, 0x66, 0x66, 0x80, - 0x99, 0x66, 0x80, 0xcc, 0x66, 0x80, 0xff, 0x66, 0xaa, 0x00, 0x66, 0xaa, - 0x33, 0x66, 0xaa, 0x66, 0x66, 0xaa, 0x99, 0x66, 0xaa, 0xcc, 0x66, 0xaa, - 0xff, 0x66, 0xd5, 0x00, 0x66, 0xd5, 0x33, 0x66, 0xd5, 0x66, 0x66, 0xd5, - 0x99, 0x66, 0xd5, 0xcc, 0x66, 0xd5, 0xff, 0x66, 0xff, 0x00, 0x66, 0xff, - 0x33, 0x66, 0xff, 0x66, 0x66, 0xff, 0x99, 0x66, 0xff, 0xcc, 0x66, 0xff, - 0xff, 0x99, 0x00, 0x00, 0x99, 0x00, 0x33, 0x99, 0x00, 0x66, 0x99, 0x00, - 0x99, 0x99, 0x00, 0xcc, 0x99, 0x00, 0xff, 0x99, 0x2b, 0x00, 0x99, 0x2b, - 0x33, 0x99, 0x2b, 0x66, 0x99, 0x2b, 0x99, 0x99, 0x2b, 0xcc, 0x99, 0x2b, - 0xff, 0x99, 0x55, 0x00, 0x99, 0x55, 0x33, 0x99, 0x55, 0x66, 0x99, 0x55, - 0x99, 0x99, 0x55, 0xcc, 0x99, 0x55, 0xff, 0x99, 0x80, 0x00, 0x99, 0x80, - 0x33, 0x99, 0x80, 0x66, 0x99, 0x80, 0x99, 0x99, 0x80, 0xcc, 0x99, 0x80, - 0xff, 0x99, 0xaa, 0x00, 0x99, 0xaa, 0x33, 0x99, 0xaa, 0x66, 0x99, 0xaa, - 0x99, 0x99, 0xaa, 0xcc, 0x99, 0xaa, 0xff, 0x99, 0xd5, 0x00, 0x99, 0xd5, - 0x33, 0x99, 0xd5, 0x66, 0x99, 0xd5, 0x99, 0x99, 0xd5, 0xcc, 0x99, 0xd5, - 0xff, 0x99, 0xff, 0x00, 0x99, 0xff, 0x33, 0x99, 0xff, 0x66, 0x99, 0xff, - 0x99, 0x99, 0xff, 0xcc, 0x99, 0xff, 0xff, 0xcc, 0x00, 0x00, 0xcc, 0x00, - 0x33, 0xcc, 0x00, 0x66, 0xcc, 0x00, 0x99, 0xcc, 0x00, 0xcc, 0xcc, 0x00, - 0xff, 0xcc, 0x2b, 0x00, 0xcc, 0x2b, 0x33, 0xcc, 0x2b, 0x66, 0xcc, 0x2b, - 0x99, 0xcc, 0x2b, 0xcc, 0xcc, 0x2b, 0xff, 0xcc, 0x55, 0x00, 0xcc, 0x55, - 0x33, 0xcc, 0x55, 0x66, 0xcc, 0x55, 0x99, 0xcc, 0x55, 0xcc, 0xcc, 0x55, - 0xff, 0xcc, 0x80, 0x00, 0xcc, 0x80, 0x33, 0xcc, 0x80, 0x66, 0xcc, 0x80, - 0x99, 0xcc, 0x80, 0xcc, 0xcc, 0x80, 0xff, 0xcc, 0xaa, 0x00, 0xcc, 0xaa, - 0x33, 0xcc, 0xaa, 0x66, 0xcc, 0xaa, 0x99, 0xcc, 0xaa, 0xcc, 0xcc, 0xaa, - 0xff, 0xcc, 0xd5, 0x00, 0xcc, 0xd5, 0x33, 0xcc, 0xd5, 0x66, 0xcc, 0xd5, - 0x99, 0xcc, 0xd5, 0xcc, 0xcc, 0xd5, 0xff, 0xcc, 0xff, 0x00, 0xcc, 0xff, - 0x33, 0xcc, 0xff, 0x66, 0xcc, 0xff, 0x99, 0xcc, 0xff, 0xcc, 0xcc, 0xff, - 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0x33, 0xff, 0x00, 0x66, 0xff, 0x00, - 0x99, 0xff, 0x00, 0xcc, 0xff, 0x00, 0xff, 0xff, 0x2b, 0x00, 0xff, 0x2b, - 0x33, 0xff, 0x2b, 0x66, 0xff, 0x2b, 0x99, 0xff, 0x2b, 0xcc, 0xff, 0x2b, - 0xff, 0xff, 0x55, 0x00, 0xff, 0x55, 0x33, 0xff, 0x55, 0x66, 0xff, 0x55, - 0x99, 0xff, 0x55, 0xcc, 0xff, 0x55, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, - 0x33, 0xff, 0x80, 0x66, 0xff, 0x80, 0x99, 0xff, 0x80, 0xcc, 0xff, 0x80, - 0xff, 0xff, 0xaa, 0x00, 0xff, 0xaa, 0x33, 0xff, 0xaa, 0x66, 0xff, 0xaa, - 0x99, 0xff, 0xaa, 0xcc, 0xff, 0xaa, 0xff, 0xff, 0xd5, 0x00, 0xff, 0xd5, - 0x33, 0xff, 0xd5, 0x66, 0xff, 0xd5, 0x99, 0xff, 0xd5, 0xcc, 0xff, 0xd5, - 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x33, 0xff, 0xff, 0x66, 0xff, 0xff, - 0x99, 0xff, 0xff, 0xcc, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0x00, 0xe7, 0xe9, - 0x13, 0x48, 0x70, 0xa0, 0xc1, 0x82, 0x08, 0x0f, 0xea, 0xd3, 0x97, 0x8c, - 0x5e, 0x32, 0x86, 0x0e, 0x21, 0x3e, 0x6c, 0x38, 0x31, 0x22, 0xc5, 0x7c, - 0xca, 0x04, 0x26, 0xc3, 0x38, 0x8f, 0x23, 0xc6, 0x7c, 0x0d, 0x41, 0xd2, - 0x03, 0xc9, 0x50, 0xe3, 0x48, 0x88, 0x1d, 0x51, 0x4d, 0xc3, 0x96, 0xea, - 0x5c, 0x3e, 0x7a, 0xf2, 0x88, 0x98, 0x72, 0x47, 0x86, 0x86, 0x0e, 0x9b, - 0x3a, 0xde, 0x0c, 0xcc, 0x38, 0x0f, 0x9a, 0x3c, 0x54, 0xee, 0xf2, 0x41, - 0xcb, 0x37, 0xad, 0x17, 0x48, 0x37, 0x9f, 0xe6, 0xe9, 0x78, 0x36, 0xb2, - 0x69, 0xc8, 0x92, 0xdf, 0xce, 0x41, 0x9b, 0x37, 0x2f, 0x99, 0x3c, 0x55, - 0xfa, 0x62, 0x42, 0xa3, 0xf9, 0xc9, 0xe1, 0x4b, 0x93, 0x0f, 0xe5, 0x51, - 0x6b, 0xc8, 0x54, 0x9f, 0xb2, 0x7c, 0x54, 0x89, 0x80, 0x9a, 0x47, 0x06, - 0xd4, 0xd7, 0xa9, 0x4c, 0xbf, 0x0a, 0x03, 0xf7, 0x75, 0xe3, 0x3c, 0x65, - 0xfa, 0x90, 0xb9, 0x99, 0x59, 0x53, 0xc7, 0x4d, 0x20, 0xf3, 0xe8, 0x31, - 0x9d, 0xf7, 0x8c, 0x1d, 0xac, 0x88, 0xdf, 0x52, 0x65, 0x4b, 0x45, 0x15, - 0xa9, 0xd2, 0x67, 0x81, 0xd1, 0x42, 0xcc, 0x37, 0xf0, 0x2a, 0xe5, 0x8d, - 0x59, 0xa7, 0x29, 0xd3, 0xca, 0x15, 0x62, 0x5c, 0x8a, 0x0c, 0xa3, 0x26, - 0x0b, 0xbc, 0xf1, 0x55, 0xbb, 0x7c, 0x44, 0x3e, 0x29, 0x23, 0xd3, 0xb0, - 0x27, 0xe9, 0x93, 0x54, 0x91, 0x4d, 0x6b, 0x37, 0x35, 0xdf, 0xab, 0x69, - 0xf4, 0xf4, 0x6e, 0x1d, 0xe3, 0xf6, 0xac, 0x6b, 0x86, 0x64, 0x47, 0xd6, - 0x92, 0x96, 0x6a, 0x1a, 0xb8, 0xa1, 0xf3, 0x64, 0xce, 0xf3, 0x62, 0xca, - 0x24, 0xc7, 0x85, 0x81, 0x21, 0x8f, 0x1c, 0x9a, 0x51, 0x30, 0xbd, 0xbb, - 0x68, 0x23, 0x56, 0x1d, 0x38, 0xfa, 0x21, 0xf6, 0x8c, 0x03, 0xa7, 0xba, - 0x11, 0x7e, 0xf9, 0x79, 0x24, 0xf8, 0x9d, 0x5f, 0x4b, 0x86, 0x7f, 0x3d, - 0xf8, 0xac, 0x76, 0x65, 0x01, 0x01, 0x00, 0x3b - }; int main() @@ -620,8 +330,8 @@ int main() // Create a new workbook XLDocument doc; - std::string xlsxFileName = "Demo11.xlsx"; - doc.create(xlsxFileName); + const std::string xlsxFileNameA = "Demo11A.xlsx"; + doc.create(xlsxFileNameA); auto wb = doc.workbook(); // Create test worksheets @@ -644,9 +354,7 @@ int main() std::cout << "2. Sheet 2: Anchor Types (oneCellAnchor vs twoCellAnchor)" << std::endl; // Test 1: oneCellAnchor vs twoCellAnchor with same image - XLImage testImage1(pngData, ImageMimeTypes::PNG); - testImage1.setDisplaySizeWithAspectRatio(2 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); - + // Add headers anchorSheet.cell("A1").value() = "oneCellAnchor (PNG, preserve aspect ratio within 2x2 cell height bounding box)"; anchorSheet.cell("A7").value() = "twoCellAnchor (PNG, 4x3 cells)"; @@ -654,22 +362,37 @@ int main() anchorSheet.cell("A19").value() = "twoCellAnchor (JPEG, 3x2 cells)"; // Add images - bool success1 = anchorSheet.addImage(testImage1, "A2"); + XLImageAnchor imageAnchor1; + imageAnchor1.initOneCell( XLCellReference("A2") ); + imageAnchor1.setDisplaySizeWithAspectRatio(XLImageUtils::png31x15Data, XLMimeType::PNG, + 2 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); + const std::string imageId1 = anchorSheet.embedImageFromImageData(imageAnchor1, + XLImageUtils::png31x15Data, XLMimeType::PNG ); + bool success1 = !imageId1.empty(); std::cout << " Added oneCellAnchor PNG: " << (success1 ? "Success" : "Failed") << std::endl; if (!success1) ++errorCount; // Add twoCellAnchor image - bool success2 = anchorSheet.addImageTwoCellAnchor(testImage1, 8, 1, 10, 4); // A8 to D10 + XLImageAnchor imageAnchor2; + imageAnchor2.initTwoCell( XLCellReference("A8"), XLCellReference("E11") ); + const std::string imageId2 = anchorSheet.embedImageFromImageData(imageAnchor2, + XLImageUtils::png31x15Data, XLMimeType::PNG ); + bool success2 = !imageId2.empty(); std::cout << " Added twoCellAnchor PNG: " << (success2 ? "Success" : "Failed") << std::endl; if (!success2) ++errorCount; // Add more twoCellAnchor tests - XLImage testImage2(jpegData, ImageMimeTypes::JPEG); - bool success3 = anchorSheet.addImage(testImage2, "A14"); + const std::string imageId3 = anchorSheet.embedImageFromImageData(XLCellReference("A14" ), + XLImageUtils::jpeg32x18Data, XLMimeType::JPEG ); + bool success3 = !imageId3.empty(); std::cout << " Added oneCellAnchor JPEG: " << (success3 ? "Success" : "Failed") << std::endl; if (!success3) ++errorCount; - bool success4 = anchorSheet.addImageTwoCellAnchor(testImage2, 20, 1, 21, 3); // A20 to C21 + XLImageAnchor imageAnchor4; + imageAnchor4.initTwoCell( XLCellReference("A20"), XLCellReference("D22") ); + const std::string imageId4 = anchorSheet.embedImageFromImageData(imageAnchor4, + XLImageUtils::jpeg32x18Data, XLMimeType::JPEG ); + bool success4 = !imageId4.empty(); std::cout << " Added twoCellAnchor JPEG: " << (success4 ? "Success" : "Failed") << std::endl; if (!success4) ++errorCount; @@ -680,22 +403,24 @@ int main() std::cout << "3. Sheet 3: File Formats (PNG, JPEG, GIF, BMP)" << std::endl; // Test different file formats - static const std::vector, std::string, std::string>> formats = { - {pngData, "PNG", ImageMimeTypes::PNG}, - {jpegData, "JPEG", ImageMimeTypes::JPEG}, - {gifData, "GIF", ImageMimeTypes::GIF}, - {bmpData, "BMP", ImageMimeTypes::BMP} + static const std::vector, std::string, XLMimeType>> formats = { + {XLImageUtils::png31x15Data, "PNG", XLMimeType::PNG}, + {XLImageUtils::jpeg32x18Data, "JPEG", XLMimeType::JPEG}, + {XLImageUtils::gif26x16Data, "GIF", XLMimeType::GIF}, + {XLImageUtils::bmp33x16Data, "BMP", XLMimeType::BMP} }; - for (size_t i = 0; i < formats.size(); ++i) { - XLImage formatImage(std::get<0>(formats[i]), std::get<2>(formats[i])); - formatImage.setDisplaySizeWithAspectRatio(4 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); - + for (size_t i = 0; i < formats.size(); ++i) { std::string textCell = "A" + std::to_string(1 + i * 6); std::string imageCell = "A" + std::to_string(2 + i * 6); formatSheet.cell(textCell).value() = std::get<1>(formats[i]) + " Format Test"; - - bool success = formatSheet.addImage(formatImage, imageCell); + XLImageAnchor imageAnchor; + imageAnchor.initOneCell( XLCellReference(imageCell) ); + imageAnchor.setDisplaySizeWithAspectRatio(std::get<0>(formats[i]), std::get<2>(formats[i]), + 4 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); + const std::string imageId = formatSheet.embedImageFromImageData(imageAnchor, + std::get<0>(formats[i]), std::get<2>(formats[i]) ); + bool success = !imageId.empty(); std::cout << " Added " << std::get<1>(formats[i]) << " image: " << (success ? "Success" : "Failed") << std::endl; if (!success) ++errorCount; } @@ -707,29 +432,36 @@ int main() std::cout << "4. Sheet 4: Sizing Strategy (aspect ratio, original, exact)" << std::endl; // Test different sizing strategies - XLImage sizingImage(pngData, ImageMimeTypes::PNG); - // Strategy 1: Preserve aspect ratio + bounding box - sizingImage.setDisplaySizeWithAspectRatio(7 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); sizingSheet.cell("A1").value() = "Aspect Ratio + Bounding Box (7x3 cell height units)"; - bool success3a = sizingSheet.addImage(sizingImage, "A2"); + XLImageAnchor sizingImageAnchor; + sizingImageAnchor.initOneCell( XLCellReference("A2") ); + sizingImageAnchor.setDisplaySizeWithAspectRatio(XLImageUtils::png31x15Data, XLMimeType::PNG, + 7 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); + const std::string imageId3a = sizingSheet.embedImageFromImageData(sizingImageAnchor, + XLImageUtils::png31x15Data, XLMimeType::PNG ); + bool success3a = !imageId3a.empty(); std::cout << " Aspect ratio + bounding box: " << (success3a ? "Success" : "Failed") << std::endl; if (!success3a) ++errorCount; // Strategy 2: Original size (no scaling) - XLImage originalImage(pngData, ImageMimeTypes::PNG); // Don't call setDisplaySize - use original dimensions sizingSheet.cell("A7").value() = "Original Size (no scaling)"; - bool success3b = sizingSheet.addImage(originalImage, "A8"); + const std::string imageId3b = sizingSheet.embedImageFromImageData(XLCellReference("A8" ), + XLImageUtils::png31x15Data, XLMimeType::PNG ); + bool success3b = !imageId3b.empty(); std::cout << " Original size: " << (success3b ? "Success" : "Failed") << std::endl; if (!success3b) ++errorCount; // Strategy 3: Force exact dimensions - XLImage exactImage(pngData, ImageMimeTypes::PNG); - exactImage.setDisplayWidth(11 * EXCEL_CELL_Y_SPACING_EMUS); - exactImage.setDisplayHeight(3 * EXCEL_CELL_Y_SPACING_EMUS); sizingSheet.cell("A13").value() = "Exact Dimensions (11x3 cell height units, may distort)"; - bool success3c = sizingSheet.addImage(exactImage, "A14"); + XLImageAnchor exactImageAnchor; + exactImageAnchor.initOneCell( XLCellReference("A14") ); + exactImageAnchor.displayWidthEMUs = 11 * EXCEL_CELL_Y_SPACING_EMUS; + exactImageAnchor.displayHeightEMUs = 3 * EXCEL_CELL_Y_SPACING_EMUS; + const std::string imageId3c = sizingSheet.embedImageFromImageData(exactImageAnchor, + XLImageUtils::png31x15Data, XLMimeType::PNG ); + bool success3c = !imageId3c.empty(); std::cout << " Exact dimensions: " << (success3c ? "Success" : "Failed") << std::endl; if (!success3c) ++errorCount; @@ -739,30 +471,43 @@ int main() // 5. Sheet 5: Units & Scaling Testing (pixels, EMUs, cells) std::cout << "5. Sheet 5: Units & Scaling Testing" << std::endl; - // Test different unit systems - XLImage unitsImage(pngData, ImageMimeTypes::PNG); - + // Test different unit systems + // Pixels - unitsImage.setDisplaySizePixelsWithAspectRatio(100, 50); unitsSheet.cell("A1").value() = "Pixel-based (100x50 pixels)"; - bool success4a = unitsSheet.addImage(unitsImage, "A2"); + XLImageAnchor pixelUnitsImageAnchor; + pixelUnitsImageAnchor.initOneCell( XLCellReference("A2") ); + const uint32_t maxWidthEmus = XLImageUtils::pixelsToEmus(100); + const uint32_t maxHeightEmus = XLImageUtils::pixelsToEmus(50); + pixelUnitsImageAnchor.setDisplaySizeWithAspectRatio(XLImageUtils::png31x15Data, XLMimeType::PNG, + maxWidthEmus, maxHeightEmus); + const std::string imageId4a = unitsSheet.embedImageFromImageData(pixelUnitsImageAnchor, + XLImageUtils::png31x15Data, XLMimeType::PNG ); + bool success4a = !imageId4a.empty(); std::cout << " Pixel-based sizing: " << (success4a ? "Success" : "Failed") << std::endl; if (!success4a) ++errorCount; // EMUs - XLImage emuImage(pngData, ImageMimeTypes::PNG); - emuImage.setDisplayWidth(476250); // 50 pixels in EMUs - emuImage.setDisplayHeight(238125); // 25 pixels in EMUs unitsSheet.cell("A7").value() = "EMU-based (476250x238125 EMUs)"; - bool success4b = unitsSheet.addImage(emuImage, "A8"); + XLImageAnchor emuUnitsImageAnchor; + emuUnitsImageAnchor.initOneCell( XLCellReference("A8") ); + emuUnitsImageAnchor.displayWidthEMUs = 476250; + emuUnitsImageAnchor.displayHeightEMUs = 238125; + const std::string imageId4b = unitsSheet.embedImageFromImageData(emuUnitsImageAnchor, + XLImageUtils::png31x15Data, XLMimeType::PNG ); + bool success4b = !imageId4b.empty(); std::cout << " EMU-based sizing: " << (success4b ? "Success" : "Failed") << std::endl; if (!success4b) ++errorCount; // Cell spacing - XLImage cellImage(pngData, ImageMimeTypes::PNG); - cellImage.setDisplaySizeWithAspectRatio(19 * EXCEL_CELL_Y_SPACING_EMUS, 5 * EXCEL_CELL_Y_SPACING_EMUS); unitsSheet.cell("A13").value() = "Cell-based (19x5 cell height units) -- preserve aspect ratio"; - bool success4c = unitsSheet.addImage(cellImage, "A14"); + XLImageAnchor cellSpacingImageAnchor; + cellSpacingImageAnchor.initOneCell( XLCellReference("A14") ); + cellSpacingImageAnchor.setDisplaySizeWithAspectRatio(XLImageUtils::png31x15Data, XLMimeType::PNG, + 19 * EXCEL_CELL_Y_SPACING_EMUS, 5 * EXCEL_CELL_Y_SPACING_EMUS); + const std::string imageId4c = unitsSheet.embedImageFromImageData(cellSpacingImageAnchor, + XLImageUtils::png31x15Data, XLMimeType::PNG ); + bool success4c = !imageId4c.empty(); std::cout << " Cell-based sizing: " << (success4c ? "Success" : "Failed") << std::endl; if (!success4c) ++errorCount; @@ -788,95 +533,80 @@ int main() // Image 1: PNG - Original size std::string pngPath = imageDir + "tiny_png.png"; fileSheet.cell("A1").value() = "PNG - Original Size"; - bool fileSuccess1 = fileSheet.addImageFromFile(pngPath, "A2"); + const std::string imageIdFile1 = + fileSheet.embedImageFromFile(XLCellReference("A2"), pngPath ); + bool fileSuccess1 = !imageIdFile1.empty(); std::cout << " PNG original size: " << (fileSuccess1 ? "Success" : "Failed") << std::endl; if (!fileSuccess1) ++errorCount; // Image 2: JPEG - Aspect ratio preserved (2x1 cells) std::string jpegPath = imageDir + "tiny_jpeg.jpg"; - XLImage jpegImage; - std::string jpegId = fileSheet.generateNextImageId(); - if (jpegImage.loadFromFile(jpegPath, jpegId)) { - jpegImage.setDisplaySizeWithAspectRatio(2 * EXCEL_CELL_Y_SPACING_EMUS, 1 * EXCEL_CELL_Y_SPACING_EMUS); - fileSheet.cell("A7").value() = "JPEG - Aspect Ratio (2x1 cells)"; - bool fileSuccess2 = fileSheet.addImage(jpegImage, "A8"); - std::cout << " JPEG aspect ratio: " << (fileSuccess2 ? "Success" : "Failed") << std::endl; - if (!fileSuccess2) ++errorCount; - } else { - std::cout << " JPEG aspect ratio: Failed to load image" << std::endl; - ++errorCount; - } - + fileSheet.cell("A7").value() = "JPEG - Aspect Ratio (2x1 cells)"; + XLImageAnchor jpegImageAnchor; + jpegImageAnchor.initOneCell( XLCellReference("A8") ); + jpegImageAnchor.setDisplaySizeWithAspectRatio(jpegPath, XLMimeType::JPEG, + 2 * EXCEL_CELL_Y_SPACING_EMUS, 1 * EXCEL_CELL_Y_SPACING_EMUS); + const std::string imageIdFile2 = fileSheet.embedImageFromFile(jpegImageAnchor, + jpegPath, XLMimeType::JPEG ); + bool fileSuccess2 = !imageIdFile2.empty(); + std::cout << " JPEG aspect ratio: " << (fileSuccess2 ? "Success" : "Failed") << std::endl; + if (!fileSuccess2) ++errorCount; + // Image 3: GIF - Exact dimensions (may distort) std::string gifPath = imageDir + "tiny_gif.gif"; - XLImage gifImage; - std::string gifId = fileSheet.generateNextImageId(); - if (gifImage.loadFromFile(gifPath, gifId)) { - gifImage.setDisplayWidth(3 * EXCEL_CELL_Y_SPACING_EMUS); - gifImage.setDisplayHeight(2 * EXCEL_CELL_Y_SPACING_EMUS); - fileSheet.cell("A13").value() = "GIF - Exact Dimensions (3x2 cells)"; - bool fileSuccess3 = fileSheet.addImage(gifImage, "A14"); - std::cout << " GIF exact dimensions: " << (fileSuccess3 ? "Success" : "Failed") << std::endl; - if (!fileSuccess3) ++errorCount; - } else { - std::cout << " GIF exact dimensions: Failed to load image" << std::endl; - ++errorCount; - } + fileSheet.cell("A13").value() = "GIF - Exact Dimensions (3x2 cells)"; + XLImageAnchor gifImageAnchor; + gifImageAnchor.initOneCell( XLCellReference("A14") ); + gifImageAnchor.displayWidthEMUs = 3 * EXCEL_CELL_Y_SPACING_EMUS; + gifImageAnchor.displayHeightEMUs = 2 * EXCEL_CELL_Y_SPACING_EMUS; + const std::string imageIdFile3 = fileSheet.embedImageFromFile(gifImageAnchor, + gifPath, XLMimeType::GIF ); + bool fileSuccess3 = !imageIdFile3.empty(); + std::cout << " GIF exact dimensions: " << (fileSuccess3 ? "Success" : "Failed") << std::endl; + if (!fileSuccess3) ++errorCount; // Image 4: BMP - Two-cell anchor spanning multiple cells std::string bmpPath = imageDir + "tiny_bmp.bmp"; - XLImage bmpImage; - std::string bmpId = fileSheet.generateNextImageId(); - if (bmpImage.loadFromFile(bmpPath, bmpId)) { - bmpImage.setDisplaySizeWithAspectRatio(3 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); - fileSheet.cell("A19").value() = "BMP - Two-Cell Anchor (A20:C22)"; - bool fileSuccess4 = fileSheet.addImageTwoCellAnchor(bmpImage, 20, 1, 22, 3); // A20 to C22 - std::cout << " BMP two-cell anchor: " << (fileSuccess4 ? "Success" : "Failed") << std::endl; - if (!fileSuccess4) ++errorCount; - } else { - std::cout << " BMP two-cell anchor: Failed to load image" << std::endl; - ++errorCount; - } - - // Image 5: PNG - Precise positioning with offset (using previously unused addImageWithOffset) + fileSheet.cell("A19").value() = "BMP - Two-Cell Anchor (A20:D23)"; + XLImageAnchor bmpImageAnchor; + bmpImageAnchor.initTwoCell( XLCellReference("A20"), XLCellReference("D23") ); + bmpImageAnchor.setDisplaySizeWithAspectRatio(bmpPath, XLMimeType::BMP, + 3 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); + const std::string imageIdFile4 = fileSheet.embedImageFromFile(bmpImageAnchor, + bmpPath, XLMimeType::BMP ); + bool fileSuccess4 = !imageIdFile4.empty(); + std::cout << " BMP two-cell anchor: " << (fileSuccess4 ? "Success" : "Failed") << std::endl; + if (!fileSuccess4) ++errorCount; + + // Image 5: PNG - Precise positioning with offset (using previously unused embedImageWithOffset) std::string pngPath2 = imageDir + "tiny_png.png"; - XLImage offsetImage; - std::string imageId = fileSheet.generateNextImageId(); - if (offsetImage.loadFromFile(pngPath2, imageId)) { - // Set a specific size for the offset test - offsetImage.setDisplaySizeWithAspectRatio(3 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); - - fileSheet.cell("A25").value() = "PNG - Precise Offset (A26 + 50% cell width/height offset)"; - // Offset by 50% of cell width and height (100000 EMUs each) - int32_t colOffset = EXCEL_CELL_Y_SPACING_EMUS / 2; // 50% of cell width - int32_t rowOffset = EXCEL_CELL_Y_SPACING_EMUS / 2; // 50% of cell height - bool fileSuccess5 = fileSheet.addImageWithOffset(offsetImage, 26, 1, rowOffset, colOffset); - std::cout << " PNG with offset: " << (fileSuccess5 ? "Success" : "Failed") << std::endl; - if (!fileSuccess5) ++errorCount; - } else { - std::cout << " PNG with offset: Failed to load image" << std::endl; - ++errorCount; - } + fileSheet.cell("A25").value() = "PNG - Precise Offset (A26 + 50% cell height offset)"; + XLImageAnchor pngImageAnchor2; + pngImageAnchor2.initOneCell( XLCellReference("A26") ); + pngImageAnchor2.setDisplaySizeWithAspectRatio(pngPath2, XLMimeType::PNG, + 3 * EXCEL_CELL_Y_SPACING_EMUS, 2 * EXCEL_CELL_Y_SPACING_EMUS); + pngImageAnchor2.fromColOffset = EXCEL_CELL_Y_SPACING_EMUS / 2; // 50% of cell height + pngImageAnchor2.fromRowOffset = EXCEL_CELL_Y_SPACING_EMUS / 2; // 50% of cell height + const std::string imageIdFile5 = fileSheet.embedImageFromFile(pngImageAnchor2, + pngPath2, XLMimeType::PNG ); + bool fileSuccess5 = !imageIdFile5.empty(); + std::cout << " PNG with offset: " << (fileSuccess5 ? "Success" : "Failed") << std::endl; + if (!fileSuccess5) ++errorCount; // Image 6: JPEG - Larger size with offset (demonstrates variety in offset sizing) std::string jpegPath2 = imageDir + "tiny_jpeg.jpg"; - XLImage offsetImage2; - std::string imageId2 = fileSheet.generateNextImageId(); - if (offsetImage2.loadFromFile(jpegPath2, imageId2)) { - // Set a larger size for variety - offsetImage2.setDisplaySizeWithAspectRatio(4 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); - - fileSheet.cell("A31").value() = "JPEG - Large Size with Offset (A32 + 25% cell offset)"; - // Smaller offset for variety - int32_t colOffset2 = EXCEL_CELL_Y_SPACING_EMUS / 4; // 25% of cell width - int32_t rowOffset2 = EXCEL_CELL_Y_SPACING_EMUS / 4; // 25% of cell height - bool fileSuccess6 = fileSheet.addImageWithOffset(offsetImage2, 32, 1, rowOffset2, colOffset2); - std::cout << " JPEG large with offset: " << (fileSuccess6 ? "Success" : "Failed") << std::endl; - if (!fileSuccess6) ++errorCount; - } else { - std::cout << " JPEG large with offset: Failed to load image" << std::endl; - ++errorCount; - } + fileSheet.cell("A31").value() = "JPEG - Large Size with Offset (A32 + 25% cell offset)"; + XLImageAnchor jpegImageAnchor2; + jpegImageAnchor2.initOneCell( XLCellReference("A32") ); + jpegImageAnchor2.setDisplaySizeWithAspectRatio(jpegPath2, XLMimeType::JPEG, + 4 * EXCEL_CELL_Y_SPACING_EMUS, 3 * EXCEL_CELL_Y_SPACING_EMUS); + jpegImageAnchor2.fromColOffset = EXCEL_CELL_Y_SPACING_EMUS / 4; // 25% of cell height + jpegImageAnchor2.fromRowOffset = EXCEL_CELL_Y_SPACING_EMUS / 4; // 25% of cell height + const std::string imageIdFile6 = fileSheet.embedImageFromFile(jpegImageAnchor2, + jpegPath2, XLMimeType::JPEG ); + bool fileSuccess6 = !imageIdFile6.empty(); + std::cout << " JPEG large with offset: " << (fileSuccess6 ? "Success" : "Failed") << std::endl; + if (!fileSuccess6) ++errorCount; // Check data integrity -- typically only done for debugging verifyWorksheetData(fileSheet); @@ -885,9 +615,13 @@ int main() // Check data integrity -- typically only done for debugging verifyDocData(doc); - // 7. Inspect Image Registry - std::cout << "7. Inspect Image Registry" << std::endl; - printAllImageRegistries(doc); + // 7. Inspect Images + std::cout << "7. Inspect Embedded Images" << std::endl; + printAllEmbeddedImages(doc); + + // 7a. Verify Image Consistency (cross-reference old vs new system) + std::cout << "7a. Verify Image Consistency" << std::endl; + verifyImageConsistency(doc); // Check data integrity -- typically only done for debugging verifyDocData(doc); @@ -923,7 +657,7 @@ int main() // 9. Save the workbook std::cout << "9. Save" << std::endl; - std::cout << " Saving workbook as '" << xlsxFileName << "'..." << std::endl; + std::cout << " Saving workbook as '" << xlsxFileNameA << "'..." << std::endl; try { doc.save(); std::cout << " Workbook saved successfully." << std::endl; @@ -944,8 +678,8 @@ int main() std::cout << "1. Read back the .xlsx file" << std::endl; OpenXLSX::XLDocument readDoc; try { - readDoc.open(xlsxFileName); - std::cout << " Successfully opened: " << xlsxFileName << std::endl; + readDoc.open(xlsxFileNameA); + std::cout << " Successfully opened: " << xlsxFileNameA << std::endl; // Check data integrity -- typically only done for debugging verifyDocData(readDoc); @@ -965,16 +699,10 @@ int main() } // 3. Inspect Image Registry - std::cout << "3. Inspect Image Registry" << std::endl; + std::cout << "3. Inspect Embedded Images" << std::endl; - int totalImagesFound = 0; - std::vector worksheetsWithImages; - - for (const auto& sheetName : readDoc.workbook().worksheetNames()) { - OpenXLSX::XLWorksheet sheet = readDoc.workbook().worksheet(sheetName); - printImageRegistry(sheet, sheetName, &totalImagesFound); - } - std::cout << " Total images: " << totalImagesFound << std::endl; + int totalImagesFound = 0; + printAllEmbeddedImages(readDoc,&totalImagesFound); // 4. Compare to original, after search std::cout << "4. Compare to original, after search" << std::endl; @@ -996,10 +724,17 @@ int main() if (readDoc.workbook().worksheetExists("File Loading")) { fileLoadingSheet = readDoc.workbook().worksheet("File Loading"); + // Print embedded images BEFORE removal + std::cout << "5a. Embedded Images BEFORE Removal:" << std::endl; + printAllEmbeddedImages(readDoc); + + // Print XLImageManager reference counts BEFORE removal + readDoc.getImageManager().printReferenceCounts("XLImageManager Reference Counts BEFORE Removal"); + // Remove image 3 by Image ID and image 4 by Relationship ID (testing both methods) std::cout << " Removing image 3 by Image ID and image 4 by Relationship ID..." << std::endl; bool removedImage3 = fileLoadingSheet.removeImageByImageID("3"); // Third image (A14) - bool removedImage4 = fileLoadingSheet.removeImageByRelationshipId("rId5"); // Fourth image (A20) + bool removedImage4 = fileLoadingSheet.removeImageByRelationshipId("rId4"); // Fourth image (A20) std::cout << " Removed image 3 (by Image ID): " << (removedImage3 ? "Success" : "Failed") << std::endl; if (!removedImage3) ++errorCount; std::cout << " Removed image 4 (by Relationship ID): " << (removedImage4 ? "Success" : "Failed") << std::endl; @@ -1009,29 +744,29 @@ int main() fileLoadingSheet.cell("B13").value() = (removedImage3) ? "REMOVED" : "FAILED TO REMOVE"; // Near image 3 at A14 fileLoadingSheet.cell("B19").value() = (removedImage4) ? "REMOVED" : "FAILED TO REMOVE"; // Near image 4 at A20 + // Print embedded images AFTER removal + std::cout << "5. Embedded Images AFTER Removal:" << std::endl; + printAllEmbeddedImages(readDoc); + + // Print XLImageManager reference counts AFTER removal + readDoc.getImageManager().printReferenceCounts("XLImageManager Reference Counts AFTER Removal"); + // Check data integrity -- typically only done for debugging verifyWorksheetData(fileLoadingSheet); - // 6. Inspect Image Registry - std::cout << "6. Inspect Image Registry" << std::endl; - printImageRegistry(fileLoadingSheet, "File Loading"); - // 7. Add image std::cout << "7. Add image" << std::endl; - OpenXLSX::XLImage newImage; - // TODO: should generateNextImageId() return a unique image ID for the worksheet? - newImageId = fileLoadingSheet.generateNextImageId(); + XLImageAnchor newImageAnchor; + newImageAnchor.initOneCell( XLCellReference("A38") ); + newImageAnchor.setDisplaySizeWithAspectRatio(XLImageUtils::png31x15Data, XLMimeType::PNG, + 2 * EXCEL_CELL_Y_SPACING_EMUS, 1 * EXCEL_CELL_Y_SPACING_EMUS); + newImageId = fileLoadingSheet.embedImageFromImageData(newImageAnchor, + XLImageUtils::png31x15Data, XLMimeType::PNG ); std::cout << " Generated new image ID: " << newImageId << std::endl; - - // Use the same PNG data from our static constants - if (newImage.loadFromData(pngData, "image/png", newImageId)) { + OpenXLSX::XLEmbeddedImage newImage = fileSheet.getEmbImageByImageID(newImageId); + if(!newImageId.empty()){ std::cout << " Successfully loaded image data" << std::endl; - std::cout << " Image ID before addImage(): " << newImage.id() << std::endl; - - // Set a distinctive size for this new image - newImage.setDisplaySizeWithAspectRatio(2 * EXCEL_CELL_Y_SPACING_EMUS, 1 * EXCEL_CELL_Y_SPACING_EMUS); - std::cout << " Set image display size" << std::endl; - + // Add it to cell A37 (after the existing images) fileLoadingSheet.cell("A37").value() = "NEW IMAGE - Added via Read-Modify-Write Cycle"; std::cout << " Set cell A37 text" << std::endl; @@ -1039,31 +774,26 @@ int main() // Check if image is valid before adding std::cout << " Image valid: " << (newImage.isValid() ? "Yes" : "No") << std::endl; std::cout << " Image data size: " << newImage.data().size() << " bytes" << std::endl; - std::cout << " Image MIME type: " << newImage.mimeType() << std::endl; - - bool addSuccess = fileLoadingSheet.addImage(newImage, "A38"); - std::cout << " addImage() returned: " << (addSuccess ? "Success" : "Failed") << std::endl; - if (!addSuccess) ++errorCount; - std::cout << " Image ID after addImage(): " << newImage.id() << std::endl; - - if (addSuccess) { + std::cout << " Image MIME type: " << XLImageUtils::mimeTypeToString(newImage.mimeType()) << std::endl; + + if (newImage.isValid()) { std::cout << " Successfully added new image to 'File Loading' worksheet" << std::endl; std::cout << " New image ID: " << newImageId << std::endl; std::cout << " Image positioned at: A38" << std::endl; - // Verify the image was actually added by checking the registry + // Verify the image was actually added by checking the embedded images try { // Check if the new image was added - const auto& imageInfos = fileLoadingSheet.getImageInfos(); + const auto& embImages = fileLoadingSheet.getEmbImages(); bool foundNewImage = false; - for (const auto& imgInfo : imageInfos) { - if (imgInfo.imageId == newImageId) { + for (const auto& embImage : embImages) { + if (embImage.id() == newImageId) { foundNewImage = true; - std::cout << " Found new image in registry: " << imageInfoStr(imgInfo) << std::endl; + std::cout << " Found new image in embedded images: " << embeddedImageStr(embImage) << std::endl; } } if (!foundNewImage) { - std::cout << " WARNING: New image '" << newImageId << "' not found in registry after addition!" << std::endl; + std::cout << " WARNING: New image '" << newImageId << "' not found in embedded images after addition!" << std::endl; } } catch (const std::exception& e) { std::cout << " ERROR: Failed to verify image addition: " << e.what() << std::endl; @@ -1072,6 +802,11 @@ int main() std::cout << " Failed to add new image to worksheet" << std::endl; ++errorCount; } + + // Print embedded images AFTER addition + std::cout << "7a. Embedded Images AFTER Addition:" << std::endl; + printAllEmbeddedImages(readDoc); + } else { std::cout << " Failed to create new image from data" << std::endl; ++errorCount; @@ -1097,11 +832,11 @@ int main() std::cout << " Differences: " << diffMsgAfterModify << std::endl; } - // 9. Inspect Image Registry - std::cout << "9. Inspect Image Registry" << std::endl; + // 9. Inspect Embedded Images + std::cout << "9. Inspect Embedded Images" << std::endl; try { if (readDoc.workbook().worksheetExists("File Loading")) { - printImageRegistry(fileLoadingSheet, "File Loading"); + printEmbeddedImages(fileLoadingSheet, "File Loading"); std::cout << " Total images after modifications: " << fileLoadingSheet.imageCount() << std::endl; } else { std::cout << " 'File Loading' worksheet not found!" << std::endl; @@ -1137,11 +872,14 @@ int main() // Check data integrity -- typically only done for debugging verifyDocData(readDoc); + // print reference counts + readDoc.getImageManager().printReferenceCounts("XLImageManager Reference Counts before saving"); + // 11. Save modified file - std::string modifiedFileName = "Demo11_Modified.xlsx"; - std::cout << "11. Save modified file: " << modifiedFileName << std::endl; + const std::string xlsxFileNameB = "Demo11B.xlsx"; + std::cout << "11. Save modified file: " << xlsxFileNameB << std::endl; try { - readDoc.saveAs(modifiedFileName); + readDoc.saveAs(xlsxFileNameB); } catch (const std::exception& e) { std::cout << " Error saving modified workbook: " << e.what() << std::endl; } @@ -1182,11 +920,16 @@ int main() // Check data integrity -- typically only done for debugging verifyDocData(excelDoc); - // 2. Inspect Image Registry - std::cout << "2. Inspect Image Registry" << std::endl; - int totalImages = 0; - printAllImageRegistries(excelDoc, &totalImages); - std::cout << " Total images in Excel file: " << totalImages << std::endl; + + // 2. Inspect Embedded Images + std::cout << "2 Inspect Embedded Images" << std::endl; + int totalEmbeddedImages = 0; + printAllEmbeddedImages(excelDoc, &totalEmbeddedImages); + std::cout << " Total embedded images in Excel file: " << totalEmbeddedImages << std::endl; + + // 2c. Verify Image Consistency (cross-reference old vs new system) + std::cout << "2c. Verify Image Consistency" << std::endl; + verifyImageConsistency(excelDoc); } catch (const std::exception& e) { std::cout << " ERROR: Failed to open Excel file: " << e.what() << std::endl; ++errorCount; @@ -1217,8 +960,8 @@ int main() // Re-open the Demo11 file to test image queries XLDocument testDoc; try { - testDoc.open("Demo11.xlsx"); - std::cout << " Successfully opened Demo11.xlsx for query testing" << std::endl; + testDoc.open(xlsxFileNameA); + std::cout << " Successfully opened " << xlsxFileNameA << std::endl; // Check data integrity -- typically only done for debugging verifyDocData(testDoc); diff --git a/OpenXLSX/headers/XLContentTypes.hpp b/OpenXLSX/headers/XLContentTypes.hpp index 1c6355fa..18a6227e 100644 --- a/OpenXLSX/headers/XLContentTypes.hpp +++ b/OpenXLSX/headers/XLContentTypes.hpp @@ -98,16 +98,6 @@ namespace OpenXLSX Unknown }; - /** - * @brief Image MIME type constants - */ - namespace ImageMimeTypes { - static const std::string PNG = "image/png"; - static const std::string JPEG = "image/jpeg"; - static const std::string BMP = "image/bmp"; - static const std::string GIF = "image/gif"; - } - /** * @brief utility function: determine the name of an XLContentType value * @param type the XLContentType to get a name for diff --git a/OpenXLSX/headers/XLDocument.hpp b/OpenXLSX/headers/XLDocument.hpp index d2065274..8bd1a2bd 100644 --- a/OpenXLSX/headers/XLDocument.hpp +++ b/OpenXLSX/headers/XLDocument.hpp @@ -72,6 +72,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #include "XLTables.hpp" #include "XLWorkbook.hpp" #include "XLXmlData.hpp" +#include "XLImage.hpp" #include "XLZipArchive.hpp" namespace OpenXLSX @@ -682,6 +683,19 @@ namespace OpenXLSX */ void loadAllXmlFilesFromArchive(); + public: + /** + * @brief Get the image manager for this document + * @return Const reference to the XLImageManager + */ + const XLImageManager& getImageManager() const { return m_imageManager; } + + /** + * @brief Get the image manager for this document + * @return Reference to the XLImageManager + */ + XLImageManager& getImageManager() { return m_imageManager; } + /** * @brief * @param path @@ -711,6 +725,7 @@ namespace OpenXLSX XLProperties m_coreProperties {}; /**< A pointer to the Core properties object*/ XLStyles m_styles {}; /**< A pointer to the document styles object*/ XLWorkbook m_workbook {}; /**< A pointer to the workbook object */ + XLImageManager m_imageManager {this}; /**< A pointer to the image manager object */ IZipArchive m_archive {}; /**< */ }; diff --git a/OpenXLSX/headers/XLDrawingML.hpp b/OpenXLSX/headers/XLDrawingML.hpp index 423618c8..6c1b1c99 100644 --- a/OpenXLSX/headers/XLDrawingML.hpp +++ b/OpenXLSX/headers/XLDrawingML.hpp @@ -12,13 +12,48 @@ #include #include #include +#include // std::pair // ===== OpenXLSX Includes ===== // #include "XLXmlFile.hpp" -#include "XLImage.hpp" +#include "XLCellReference.hpp" namespace OpenXLSX { + // Forward declarations + class XLEmbeddedImage; + class XLImageAnchor; + enum class XLMimeType : uint8_t; + + /** + * @brief Image anchor type enumeration + * @details Defines the types of image anchors supported in Excel worksheets + */ + enum class XLImageAnchorType : uint8_t { + OneCellAnchor, /**< Single cell anchor - image positioned relative to one cell */ + TwoCellAnchor, /**< Two cell anchor - image spans between two cells */ + Unknown /**< Unknown or unsupported anchor type */ + }; + + /** + * @brief Utility functions for XLImageAnchorType enum + */ + namespace XLImageAnchorUtils { + /** + * @brief Convert anchor type string to XLImageAnchorType enum + * @param anchorTypeStr Anchor type string (e.g., "oneCellAnchor", "twoCellAnchor") + * @return Corresponding XLImageAnchorType enum value + */ + XLImageAnchorType stringToAnchorType(const std::string& anchorTypeStr); + + /** + * @brief Convert XLImageAnchorType enum to anchor type string + * @param anchorType XLImageAnchorType enum value + * @return Corresponding anchor type string (e.g., "oneCellAnchor", "twoCellAnchor") + */ + std::string anchorTypeToString(XLImageAnchorType anchorType); + } + /** * @brief Class for managing DrawingML drawings in worksheets */ @@ -36,49 +71,9 @@ namespace OpenXLSX XLDrawingML& operator=(const XLDrawingML& other) = default; XLDrawingML& operator=(XLDrawingML&& other) noexcept = default; - // ===== Public Methods ===== // - /** - * @brief Add an image to the drawing - * @param image The image to add - * @param row The row number (1-based) - * @param column The column number (1-based) - * @param relationshipId The relationship ID for the image - */ - void addImage(const XLImage& image, uint32_t row, uint16_t column, const std::string& relationshipId); - - /** - * @brief Add an image to the drawing with precise positioning - * @param image The image to add - * @param row The row number (1-based) - * @param column The column number (1-based) - * @param relationshipId The relationship ID for the image - * @param rowOffset Offset from the top-left of the cell in EMUs - * @param colOffset Offset from the top-left of the cell in EMUs - */ - void addImageWithOffset(const XLImage& image, uint32_t row, uint16_t column, - const std::string& relationshipId, - int32_t rowOffset = 0, int32_t colOffset = 0); - - /** - * @brief Add an image to the drawing with two-cell anchoring - * @param image The image to add - * @param fromRow Starting row number (1-based) - * @param fromCol Starting column number (1-based) - * @param toRow Ending row number (1-based) - * @param toCol Ending column number (1-based) - * @param relationshipId The relationship ID for the image - * @param fromRowOffset Offset from top-left of starting cell in EMUs - * @param fromColOffset Offset from top-left of starting cell in EMUs - * @param toRowOffset Offset from top-left of ending cell in EMUs - * @param toColOffset Offset from top-left of ending cell in EMUs - */ - void addImageTwoCellAnchor(const XLImage& image, - uint32_t fromRow, uint16_t fromCol, - uint32_t toRow, uint16_t toCol, - const std::string& relationshipId, - int32_t fromRowOffset = 0, int32_t fromColOffset = 0, - int32_t toRowOffset = 0, int32_t toColOffset = 0); + void addImage(const XLEmbeddedImage& embImage); + // ===== Public Methods ===== // /** * @brief Get the number of images in the drawing * @return Number of images @@ -98,21 +93,34 @@ namespace OpenXLSX */ XMLNode getRootNode() const; - private: - // ===== Private Methods ===== // + // ===== Static Utility Functions ===== // + /** - * @brief Convert EMUs to Excel units - * @param emus EMU value - * @return Excel unit value + * @brief Create XML anchor node from XLImageAnchor data + * @param rootNode The root XML node to append the anchor to + * @param imgAnchorInfo The anchor data to convert to XML + * @return The created anchor XML node */ - uint32_t emusToExcelUnits(uint32_t emus) const; - + static XMLNode createXMLNode(XMLNode rootNode, const XLImageAnchor& imgAnchorInfo); + /** - * @brief Convert points to EMUs - * @param points Point value - * @return EMU value + * @brief Delete XML anchor node by relationship ID + * @param rootNode The root XML node to search in + * @param relationshipId The relationship ID to find and delete + * @return True if the anchor was found and deleted, false otherwise */ - uint32_t pointsToEmus(double points) const; + static bool deleteXMLNode(XMLNode rootNode, const std::string& relationshipId); + + /** + * @brief Parse XML anchor node into XLImageAnchor data + * @param anchorXMLNode The XML node containing anchor data + * @param imgAnchorInfo Pointer to XLImageAnchor to populate + * @return True if parsing was successful, false otherwise + */ + static bool parseXMLNode(const pugi::xml_node& anchorXMLNode, XLImageAnchor* imgAnchorInfo); + + private: + // ===== Private Methods ===== // /** * @brief Calculate cell position in Excel units @@ -121,6 +129,161 @@ namespace OpenXLSX * @return Position in Excel units */ uint32_t calculateCellPosition(uint32_t row, uint16_t column) const; + + }; + + // ========== XLImageAnchor Class ========== // + + /** + * @brief Represents data from and XML elements + * + * This class directly maps to the XML structure found in Excel's DrawingML + * anchor elements. All data members correspond exactly to XML attributes and + * text content, with no automatic coordinate conversions. + */ + class OPENXLSX_EXPORT XLImageAnchor + { + public: + // ===== Anchor Type and Identification ===== // + XLImageAnchorType anchorType; /**< OneCellAnchor or TwoCellAnchor */ + std::string imageId; /**< Worksheet-level image identifier (e.g., "1", "2") + from xdr:cNvPr id attribute. Maps to XLEmbeddedImage::m_id. + XML: */ + std::string relationshipId; /**< Relationship ID from a:blip r:embed attribute */ + + // ===== Cell Positioning (From Element) - Direct XML mapping ===== // + uint32_t fromRow; /**< Row number from xdr:row (0-based in XML) */ + uint16_t fromCol; /**< Column number from xdr:col (0-based in XML) */ + int32_t fromRowOffset; /**< Row offset from xdr:rowOff (in EMUs) */ + int32_t fromColOffset; /**< Column offset from xdr:colOff (in EMUs) */ + + // ===== Cell Positioning (To Element) - for twoCellAnchor ===== // + uint32_t toRow; /**< To row number from xdr:row (0-based in XML) */ + uint16_t toCol; /**< To column number from xdr:col (0-based in XML) */ + int32_t toRowOffset; /**< To row offset from xdr:rowOff (in EMUs) */ + int32_t toColOffset; /**< To column offset from xdr:colOff (in EMUs) */ + + // ===== Display Dimensions (xdr:ext element) ===== // + uint32_t displayWidthEMUs; /**< Display width from xdr:ext cx attribute (in EMUs) */ + uint32_t displayHeightEMUs; /**< Display height from xdr:ext cy attribute (in EMUs) */ + + // ===== Actual Image Dimensions (a:xfrm/a:ext element) ===== // + uint32_t actualWidthEMUs; /**< Actual image width from a:xfrm/a:ext cx (in EMUs) */ + uint32_t actualHeightEMUs; /**< Actual image height from a:xfrm/a:ext cy (in EMUs) */ + + // ===== Constructors ===== // + XLImageAnchor() = default; + + /** + * @brief Construct from oneCellAnchor data + * @param imageId Image identifier + * @param relationshipId Relationship identifier + * @param row Row number (0-based, as in XML) + * @param col Column number (0-based, as in XML) + * @param rowOffset Row offset in EMUs + * @param colOffset Column offset in EMUs + * @param displayWidth Display width in EMUs + * @param displayHeight Display height in EMUs + */ + XLImageAnchor(const std::string& imageId, const std::string& relationshipId, + uint32_t row, uint16_t col, int32_t rowOffset, int32_t colOffset, + uint32_t displayWidth, uint32_t displayHeight); + + /** + * @brief Construct from twoCellAnchor data + * @param imageId Image identifier + * @param relationshipId Relationship identifier + * @param fromRow From row number (0-based, as in XML) + * @param fromCol From column number (0-based, as in XML) + * @param toRow To row number (0-based, as in XML) + * @param toCol To column number (0-based, as in XML) + * @param fromRowOffset From row offset in EMUs + * @param fromColOffset From column offset in EMUs + * @param toRowOffset To row offset in EMUs + * @param toColOffset To column offset in EMUs + * @param displayWidth Display width in EMUs + * @param displayHeight Display height in EMUs + */ + XLImageAnchor(const std::string& imageId, const std::string& relationshipId, + uint32_t fromRow, uint16_t fromCol, uint32_t toRow, uint16_t toCol, + int32_t fromRowOffset, int32_t fromColOffset, + int32_t toRowOffset, int32_t toColOffset, + uint32_t displayWidth, uint32_t displayHeight); + + void reset(); + + void initOneCell( const XLCellReference& cellRef, int32_t rowOffset = 0, + int32_t colOffset = 0); + + void initTwoCell( const XLCellReference& fromCellRef, + const XLCellReference& toCellRef, int32_t fromROffset = 0, + int32_t fromCOffset = 0, int32_t toROffset = 0, int32_t toCOffset = 0); + + + // ===== Utility Methods ===== // + + XLCellReference getFromCellReference() const; + + void setFromCellReference( const XLCellReference& fromCellRef ); + + XLCellReference getToCellReference() const; + + void setToCellReference( const XLCellReference& toCellRef ); + + void setDisplaySizeWithAspectRatio( + const uint32_t& widthPixels, const uint32_t& heightPixels, + const uint32_t& maxWidthEmus, const uint32_t& maxHeightEmus ); + + void setDisplaySizeWithAspectRatio( + const std::vector& imageData, const XLMimeType& mimeType, + const uint32_t& maxWidthEmus, const uint32_t& maxHeightEmus ); + + void setDisplaySizeWithAspectRatio( + const std::string& imageFileName, const XLMimeType& mimeType, + const uint32_t& maxWidthEmus, const uint32_t& maxHeightEmus ); + + /** + * @brief Get the primary anchor cell reference (converts 0-based to 1-based) + * @return Cell reference string (e.g., "A2") + */ + std::string getAnchorCellReference() const; + + /** + * @brief Check if this is a twoCellAnchor + * @return True if twoCellAnchor, false if oneCellAnchor + */ + bool isTwoCell() const; + + /** + * @brief Get display dimensions as a pair + * @return Pair of (width, height) in EMUs + */ + std::pair getDisplayDimensions() const; + + /** + * @brief Get actual image dimensions as a pair + * @return Pair of (width, height) in EMUs + */ + std::pair getActualDimensions() const; + + /** + * @brief Set actual image dimensions + * @param width Width in EMUs + * @param height Height in EMUs + */ + void setActualDimensions(uint32_t width, uint32_t height); + + /** + * @brief Convert EMUs to pixels for display dimensions + * @return Pair of (width, height) in pixels + */ + std::pair getDisplayDimensionsPixels() const; + + /** + * @brief Convert EMUs to pixels for actual dimensions + * @return Pair of (width, height) in pixels + */ + std::pair getActualDimensionsPixels() const; }; } diff --git a/OpenXLSX/headers/XLImage.hpp b/OpenXLSX/headers/XLImage.hpp index bbb9f26e..47d00803 100644 --- a/OpenXLSX/headers/XLImage.hpp +++ b/OpenXLSX/headers/XLImage.hpp @@ -1,551 +1,857 @@ -/* - - ____ ____ ___ ____ ____ ____ ___ - 6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M' - 8P Y8 `MM. d' MM 6M' ` `MM. d' -6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d' -MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d' -MM MM MM MM MM MM MM' MM `MMd MM YMMMMb `MMd -MM MM MM MM MMMMMMMM MM MM d'`MM. MM `Mb dMM. -YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. - 8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM. - YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_ - MM - MM - _MM_ - - Copyright (c) 2018, Kenneth Troldal Balslev - - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - Neither the name of the author nor the - names of any contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - */ - -#ifndef OPENXLSX_XLIMAGE_HPP -#define OPENXLSX_XLIMAGE_HPP - -// ===== External Includes ===== // -#include // uint8_t, uint16_t, uint32_t -#include // std::string -#include // std::vector - -// ===== OpenXLSX Includes ===== // -#include "OpenXLSX-Exports.hpp" -#include "XLContentTypes.hpp" - -/* - * ================================================================================ - * IMAGE EMBEDDING RELATIONSHIP CREATION IN OPENXLSX - * ================================================================================ - * - * When images are embedded in Excel worksheets, OpenXLSX creates TWO types of - * relationships that work together to link worksheets → drawings → images. - * Each relationship type uses different ID management strategies appropriate - * to their scope and purpose. - * - * ┌─────────────────────────────────────────────────────────────────────────────┐ - * │ RELATIONSHIP CREATION FLOW │ - * ├─────────────────────────────────────────────────────────────────────────────┤ - * │ │ - * │ 1. Worksheet.addImage() called │ - * │ ↓ │ - * │ 2. drawingML() ensures drawing.xml exists │ - * │ ↓ │ - * │ 3. Creates Worksheet → Drawing relationship │ - * │ ↓ │ - * │ 4. addImageToDrawingML() adds image to drawing.xml │ - * │ ↓ │ - * │ 5. createDrawingRelationshipsFile() creates Drawing → Image relationships │ - * │ │ - * └─────────────────────────────────────────────────────────────────────────────┘ - * - * RELATIONSHIP TYPE 1: WORKSHEET-LEVEL RELATIONSHIPS - * ────────────────────────────────────────────────────────────────────────────── - * - * Location: Lines 1735-1750 in XLWorksheet::drawingML() - * - * Code: - * std::string drawingRelativePath = getPathARelativeToPathB(drawingFilename, getXmlPath()); - * XLRelationshipItem drawingRelationship; - * if (!m_relationships.targetExists(drawingRelativePath)) - * drawingRelationship = m_relationships.addRelationship(XLRelationshipType::Drawing, drawingRelativePath); - * else - * drawingRelationship = m_relationships.relationshipByTarget(drawingRelativePath); - * - * XMLNode drawing = appendAndGetNode(docElement, "drawing", m_nodeOrder); - * appendAndSetAttribute(drawing, "r:id", drawingRelationship.id()); - * - * Creates: - * File: xl/worksheets/_rels/sheet1.xml.rels - * Content: - * - * Purpose: Links worksheet to drawing.xml file - * Uses: XLRelationships::addRelationship() with intelligent ID management - * - * ID Management Strategy: - * - Uses GetNewRelsID() function with intelligent scanning - * - Checks existing relationships to avoid conflicts - * - Handles complex scenarios with multiple relationship types - * - Ensures uniqueness within the worksheet's relationship file - * - * RELATIONSHIP TYPE 2: DRAWING-LEVEL RELATIONSHIPS - * ────────────────────────────────────────────────────────────────────────────── - * - * Location: Lines 2016-2045 in XLWorksheet::createDrawingRelationshipsFile() - * - * Code: - * int relationshipId = 1; - * for (const auto& image : m_images) { - * std::string imageFilename = "xl/media/image_" + image.id() + image.extension(); - * std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); - * - * relsXml += "\n"; - * relationshipId++; - * } - * - * Creates: - * File: xl/drawings/_rels/drawing1.xml.rels - * Content: - * - * Purpose: Links drawing.xml to image files - * Uses: Simple sequential counter (rId1, rId2, rId3...) - * - * ID Management Strategy: - * - Uses simple sequential numbering starting from 1 - * - Regenerates entire relationship file each time - * - No conflict checking needed (file is recreated) - * - Fast and predictable for drawing-specific relationships - * - * WHY TWO DIFFERENT STRATEGIES? - * ────────────────────────────────────────────────────────────────────────────── - * - * 1. SCOPE DIFFERENCES: - * - Worksheet relationships: Complex, shared namespace with other worksheet parts - * - Drawing relationships: Simple, isolated to drawing objects only - * - * 2. FREQUENCY OF CHANGES: - * - Worksheet relationships: Infrequent, stable (drawing, comments, tables) - * - Drawing relationships: Frequent, dynamic (images added/removed often) - * - * 3. COMPLEXITY REQUIREMENTS: - * - Worksheet relationships: Must avoid conflicts with existing relationships - * - Drawing relationships: Can use simple sequential approach - * - * 4. PERFORMANCE CONSIDERATIONS: - * - Worksheet relationships: Intelligent scanning for uniqueness - * - Drawing relationships: Fast regeneration of entire file - * - * RELATIONSHIP FILE STRUCTURE: - * ────────────────────────────────────────────────────────────────────────────── - * - * xl/worksheets/_rels/sheet1.xml.rels: - * - * - * - * - * - * - * xl/drawings/_rels/drawing1.xml.rels: - * - * - * - * - * - * - * KEY INSIGHT: - * Both relationship types can use the same ID (e.g., "rId1") because they exist - * in separate relationship files with independent namespaces. This is the - * fundamental principle of Excel's relationship scoping system. - * - * ================================================================================ - */ - -namespace OpenXLSX -{ - // ========== Global Helper Functions ========== // - - /** - * @brief Helper function to append debug messages for debugging - * @param dbgMsg Optional pointer to append debug message (up to 16384 characters) - * @param msg The message to append - */ - inline void appendDbgMsg(std::string* dbgMsg, const std::string& msg) - { - if (dbgMsg && dbgMsg->length() < 16000) { // Leave some buffer - if (!dbgMsg->empty()){ - dbgMsg->append("\n "); - } - dbgMsg->append(msg); - } - } - - // ========== Excel Constants ========== // - - /** - * @brief Standard Excel cell height in EMUs (15 points × 12700 EMUs/point) - */ - constexpr uint32_t EXCEL_CELL_HEIGHT_EMUS = 190500; - - /** - * @brief Excel cell vertical spacing in EMUs (includes cell height + divider) - * This is approximately 198000 EMUs (cell height × 26/25) - */ - constexpr uint32_t EXCEL_CELL_Y_SPACING_EMUS = 198000; - - /** - * @brief Standard EMU-to-pixel conversion ratio (approximately) - * Note: This may vary based on screen resolution - */ - constexpr uint32_t EMU_TO_PIXEL_RATIO = 9525; - - /** - * @brief Structure containing image metadata for query operations - * - * This structure holds all the essential information about an embedded image - * that can be queried from a worksheet. It provides a unified interface - * for accessing image properties without needing to parse XML directly. - * - * The structure is designed to be efficient for both storage and access, - * with all data pre-parsed and readily available for common operations. - */ - struct OPENXLSX_EXPORT XLImageInfo - { - std::string imageId; /**< Unique image identifier (e.g., "img1", "img2") */ - std::string relationshipId; /**< Relationship ID in drawing.xml.rels (e.g., "rId1", "rId2") */ - std::string anchorCell; /**< Primary anchor cell reference (e.g., "A1", "B5") */ - std::string anchorType; /**< Anchor type: "oneCellAnchor" or "twoCellAnchor" */ - uint32_t widthPixels{0}; /**< Original image width in pixels */ - uint32_t heightPixels{0}; /**< Original image height in pixels */ - uint32_t displayWidthEMUs{0}; /**< Display width in Excel units (EMUs) */ - uint32_t displayHeightEMUs{0}; /**< Display height in Excel units (EMUs) */ - - /** - * @brief Default constructor - */ - XLImageInfo() = default; - - /** - * @brief Constructor with all parameters - * @param id Image ID - * @param relId Relationship ID - * @param cell Anchor cell reference - * @param type Anchor type - * @param wPx Width in pixels - * @param hPx Height in pixels - * @param wEmu Display width in EMUs - * @param hEmu Display height in EMUs - */ - XLImageInfo(const std::string& id, const std::string& relId, const std::string& cell, - const std::string& type, uint32_t wPx, uint32_t hPx, uint32_t wEmu, uint32_t hEmu) - : imageId(id), relationshipId(relId), anchorCell(cell), anchorType(type), - widthPixels(wPx), heightPixels(hPx), displayWidthEMUs(wEmu), displayHeightEMUs(hEmu) {} - - /** - * @brief Check if this image info is valid (has non-empty imageId) - * @return True if valid, false otherwise - */ - bool isValid() const { return !imageId.empty(); } - - /** - * @brief Check if this image info is empty (default constructed) - * @return True if empty, false otherwise - */ - bool isEmpty() const { return imageId.empty(); } - - /** - * @brief Compare two image info structures for debugging - * @param other The other XLImageInfo to compare with - * @param diffMsg Optional pointer to append difference message (up to 16384 characters) - * @return 0 if identical, <0 if this precedes other, >0 if this follows other - */ - int compare(const XLImageInfo& other, std::string* diffMsg = nullptr) const; - }; - - /** - * @brief The XLImage class represents an image that can be embedded in a worksheet - */ - class OPENXLSX_EXPORT XLImage - { - public: - /** - * @brief Default constructor - */ - XLImage() = default; - - /** - * @brief Constructor from file path - * @param imagePath Path to the image file - */ - explicit XLImage(const std::string& imagePath); - - /** - * @brief Constructor from binary data - * @param imageData Binary image data - * @param mimeType MIME type of the image - */ - XLImage(const std::vector& imageData, const std::string& mimeType); - - /** - * @brief Copy constructor - * @param other The XLImage to copy - */ - XLImage(const XLImage& other) = default; - - /** - * @brief Move constructor - * @param other The XLImage to move - */ - XLImage(XLImage&& other) noexcept = default; - - /** - * @brief Destructor - */ - ~XLImage() = default; - - /** - * @brief Copy assignment operator - * @param other The XLImage to copy - * @return Reference to this XLImage - */ - XLImage& operator=(const XLImage& other) = default; - - /** - * @brief Move assignment operator - * @param other The XLImage to move - * @return Reference to this XLImage - */ - XLImage& operator=(XLImage&& other) noexcept = default; - - /** - * @brief Load image from file - * @param imagePath Path to the image file - * @return True if successful, false otherwise - */ - bool loadFromFile(const std::string& imagePath); - - /** - * @brief Load image from file - * @param imagePath Path to the image file - * @param imageId The unique ID for this image - * @return True if successful, false otherwise - */ - bool loadFromFile(const std::string& imagePath, const std::string& imageId); - - /** - * @brief Load image from binary data - * @param imageData Binary image data - * @param mimeType MIME type of the image - * @return True if successful, false otherwise - */ - bool loadFromData(const std::vector& imageData, const std::string& mimeType); - - /** - * @brief Load image from binary data - * @param imageData Binary image data - * @param mimeType MIME type of the image - * @param imageId The unique ID for this image - * @return True if successful, false otherwise - */ - bool loadFromData(const std::vector& imageData, const std::string& mimeType, const std::string& imageId); - - /** - * @brief Set the image ID - * @param imageId The unique ID for this image - */ - void setId(const std::string& imageId); - - /** - * @brief Get the image data - * @return Reference to the image data vector - */ - const std::vector& data() const; - - /** - * @brief Get the MIME type - * @return The MIME type string - */ - const std::string& mimeType() const; - - /** - * @brief Get the file extension - * @return The file extension (e.g., ".png", ".jpg") - */ - const std::string& extension() const; - - /** - * @brief Get the unique image ID - * @return The unique image ID - */ - const std::string& id() const; - - /** - * @brief Get the image width in pixels - * @return The width in pixels - */ - uint32_t widthPixels() const; - - /** - * @brief Get the image height in pixels - * @return The height in pixels - */ - uint32_t heightPixels() const; - - /** - * @brief Set the display width in Excel units (EMUs) - * @param width The display width in EMUs - */ - void setDisplayWidth(uint32_t width); - - /** - * @brief Set the display height in Excel units (EMUs) - * @param height The display height in EMUs - */ - void setDisplayHeight(uint32_t height); - - /** - * @brief Get the display width in Excel units (EMUs) - * @return The display width in EMUs - */ - uint32_t displayWidth() const; - - /** - * @brief Get the display height in Excel units (EMUs) - * @return The display height in EMUs - */ - uint32_t displayHeight() const; - - /** - * @brief Set display size in pixels (converts to EMUs automatically) - * @param widthPixels The display width in pixels - * @param heightPixels The display height in pixels - */ - void setDisplaySizePixels(uint32_t widthPixels, uint32_t heightPixels); - - /** - * @brief Set display size maintaining aspect ratio - * @param maxWidthEmus Maximum width in EMUs (0 = no limit) - * @param maxHeightEmus Maximum height in EMUs (0 = no limit) - */ - void setDisplaySizeWithAspectRatio(uint32_t maxWidthEmus = 0, uint32_t maxHeightEmus = 0); - - /** - * @brief Set display size maintaining aspect ratio - * @param maxWidthPixels Maximum width in pixels (0 = no limit) - * @param maxHeightPixels Maximum height in pixels (0 = no limit) - */ - void setDisplaySizePixelsWithAspectRatio(uint32_t maxWidthPixels = 0, uint32_t maxHeightPixels = 0); - - - /** - * @brief Convert pixels to EMUs (Excel units) - * @param pixels Number of pixels - * @return Equivalent EMUs - */ - static uint32_t pixelsToEmus(uint32_t pixels); - - /** - * @brief Convert EMUs to pixels - * @param emus Number of EMUs - * @return Equivalent pixels - */ - static uint32_t emusToPixels(uint32_t emus); - - /** - * @brief Check if the image is valid - * @return True if the image has valid data - */ - bool isValid() const; - - /** - * @brief Get the content type for this image - * @return The XLContentType enum value - */ - XLContentType contentType() const; - - /** - * @brief Compare two images for debugging - * @param other The other XLImage to compare with - * @param diffMsg Optional pointer to append difference message (up to 16384 characters) - * @return 0 if identical, <0 if this precedes other, >0 if this follows other - */ - int compare(const XLImage& other, std::string* diffMsg = nullptr) const; - - /** - * @brief Verify internal data integrity and class invariants - * @param dbgMsg Optional pointer to append debug message explaining any errors found - * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) - */ - int verifyData(std::string* dbgMsg = nullptr) const; - - private: - std::vector m_imageData; /**< Binary image data */ - std::string m_mimeType; /**< MIME type of the image */ - std::string m_extension; /**< File extension */ - std::string m_id; /**< Unique image ID */ - uint32_t m_widthPixels{0}; /**< Image width in pixels */ - uint32_t m_heightPixels{0}; /**< Image height in pixels */ - uint32_t m_displayWidth{0}; /**< Display width in EMUs */ - uint32_t m_displayHeight{0}; /**< Display height in EMUs */ - - /** - * @brief Generate a unique image ID - * @return A unique image ID string - */ - static std::string generateId(uint32_t imageNumber); - - /** - * @brief Determine MIME type from file extension - * @param extension File extension - * @return MIME type string - */ - static std::string mimeTypeFromExtension(const std::string& extension); - - /** - * @brief Determine file extension from MIME type - * @param mimeType MIME type string - * @return File extension string - */ - static std::string extensionFromMimeType(const std::string& mimeType); - - /** - * @brief Convert pixels to EMUs (Excel Measurement Units) - * @param pixels Number of pixels - * @return Number of EMUs - */ - static uint32_t pixelsToEMUs(uint32_t pixels); - - /** - * @brief Get image dimensions from binary data - * @param data Binary image data - * @param mimeType MIME type of the image - * @return Pair of (width, height) in pixels - */ - static std::pair getImageDimensions(const std::vector& data, const std::string& mimeType); - }; - -} // namespace OpenXLSX - -#endif // OPENXLSX_XLIMAGE_HPP +/* + + ____ ____ ___ ____ ____ ____ ___ + 6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M' + 8P Y8 `MM. d' MM 6M' ` `MM. d' +6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d' +MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d' +MM MM MM MM MM MM MM' MM `MMd MM YMMMMb `MMd +MM MM MM MM MMMMMMMM MM MM d'`MM. MM `Mb dMM. +YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. + 8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM. + YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_ + MM + MM + _MM_ + + Copyright (c) 2018, Kenneth Troldal Balslev + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + - Neither the name of the author nor the + names of any contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +#ifndef OPENXLSX_XLIMAGE_HPP +#define OPENXLSX_XLIMAGE_HPP + +// ===== External Includes ===== // +#include // uint8_t, uint16_t, uint32_t +#include // std::map +#include // std::string +#include // std::vector +#include // std::pair + +// ===== OpenXLSX Includes ===== // +#include "OpenXLSX-Exports.hpp" +#include "XLContentTypes.hpp" +#include "XLDrawingML.hpp" + +/* + * ================================================================================ + * IMAGE EMBEDDING RELATIONSHIP CREATION IN OPENXLSX + * ================================================================================ + * + * When images are embedded in Excel worksheets, OpenXLSX creates TWO types of + * relationships that work together to link worksheets → drawings → images. + * Each relationship type uses different ID management strategies appropriate + * to their scope and purpose. + * + * ┌─────────────────────────────────────────────────────────────────────────────┐ + * │ RELATIONSHIP CREATION FLOW │ + * ├─────────────────────────────────────────────────────────────────────────────┤ + * │ │ + * │ 1. Worksheet.addImage() called │ + * │ ↓ │ + * │ 2. drawingML() ensures drawing.xml exists │ + * │ ↓ │ + * │ 3. Creates Worksheet → Drawing relationship │ + * │ ↓ │ + * │ 4. addImageToDrawingML() adds image to drawing.xml │ + * │ ↓ │ + * │ 5. createDrawingRelationshipsFile() creates Drawing → Image relationships │ + * │ │ + * └─────────────────────────────────────────────────────────────────────────────┘ + * + * RELATIONSHIP TYPE 1: WORKSHEET-LEVEL RELATIONSHIPS + * ────────────────────────────────────────────────────────────────────────────── + * + * Location: Lines 1735-1750 in XLWorksheet::drawingML() + * + * Code: + * std::string drawingRelativePath = getPathARelativeToPathB(drawingFilename, getXmlPath()); + * XLRelationshipItem drawingRelationship; + * if (!m_relationships.targetExists(drawingRelativePath)) + * drawingRelationship = m_relationships.addRelationship(XLRelationshipType::Drawing, drawingRelativePath); + * else + * drawingRelationship = m_relationships.relationshipByTarget(drawingRelativePath); + * + * XMLNode drawing = appendAndGetNode(docElement, "drawing", m_nodeOrder); + * appendAndSetAttribute(drawing, "r:id", drawingRelationship.id()); + * + * Creates: + * File: xl/worksheets/_rels/sheet1.xml.rels + * Content: + * + * Purpose: Links worksheet to drawing.xml file + * Uses: XLRelationships::addRelationship() with intelligent ID management + * + * ID Management Strategy: + * - Uses GetNewRelsID() function with intelligent scanning + * - Checks existing relationships to avoid conflicts + * - Handles complex scenarios with multiple relationship types + * - Ensures uniqueness within the worksheet's relationship file + * + * RELATIONSHIP TYPE 2: DRAWING-LEVEL RELATIONSHIPS + * ────────────────────────────────────────────────────────────────────────────── + * + * Location: Lines 2016-2045 in XLWorksheet::createDrawingRelationshipsFile() + * + * Code: + * int relationshipId = 1; + * for (const auto& image : m_images) { + * std::string imageFilename = "xl/media/image_" + image.id() + image.extension(); + * std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); + * + * relsXml += "\n"; + * relationshipId++; + * } + * + * Creates: + * File: xl/drawings/_rels/drawing1.xml.rels + * Content: + * + * Purpose: Links drawing.xml to image files + * Uses: Simple sequential counter (rId1, rId2, rId3...) + * + * ID Management Strategy: + * - Uses simple sequential numbering starting from 1 + * - Regenerates entire relationship file each time + * - No conflict checking needed (file is recreated) + * - Fast and predictable for drawing-specific relationships + * + * WHY TWO DIFFERENT STRATEGIES? + * ────────────────────────────────────────────────────────────────────────────── + * + * 1. SCOPE DIFFERENCES: + * - Worksheet relationships: Complex, shared namespace with other worksheet parts + * - Drawing relationships: Simple, isolated to drawing objects only + * + * 2. FREQUENCY OF CHANGES: + * - Worksheet relationships: Infrequent, stable (drawing, comments, tables) + * - Drawing relationships: Frequent, dynamic (images added/removed often) + * + * 3. COMPLEXITY REQUIREMENTS: + * - Worksheet relationships: Must avoid conflicts with existing relationships + * - Drawing relationships: Can use simple sequential approach + * + * 4. PERFORMANCE CONSIDERATIONS: + * - Worksheet relationships: Intelligent scanning for uniqueness + * - Drawing relationships: Fast regeneration of entire file + * + * RELATIONSHIP FILE STRUCTURE: + * ────────────────────────────────────────────────────────────────────────────── + * + * xl/worksheets/_rels/sheet1.xml.rels: + * + * + * + * + * + * + * xl/drawings/_rels/drawing1.xml.rels: + * + * + * + * + * + * + * KEY INSIGHT: + * Both relationship types can use the same ID (e.g., "rId1") because they exist + * in separate relationship files with independent namespaces. This is the + * fundamental principle of Excel's relationship scoping system. + * + * ================================================================================ + */ + +namespace OpenXLSX +{ + // Forward declarations + class XLDocument; + + // ========== Global Helper Functions ========== // + + /** + * @brief Helper function to append debug messages for debugging + * @param dbgMsg Optional pointer to append debug message (up to 16384 characters) + * @param msg The message to append + */ + inline void appendDbgMsg(std::string* dbgMsg, const std::string& msg) + { + if (dbgMsg && dbgMsg->length() < 16000) { // Leave some buffer + if (!dbgMsg->empty()){ + dbgMsg->append("\n "); + } + dbgMsg->append(msg); + } + } + + // ========== Excel Constants ========== // + + /** + * @brief Standard Excel cell height in EMUs (15 points × 12700 EMUs/point) + */ + constexpr uint32_t EXCEL_CELL_HEIGHT_EMUS = 190500; + + /** + * @brief Excel cell vertical spacing in EMUs (includes cell height + divider) + * This is approximately 198000 EMUs (cell height × 26/25) + */ + constexpr uint32_t EXCEL_CELL_Y_SPACING_EMUS = 198000; + + /** + * @brief Standard EMU-to-pixel conversion ratio (approximately) + * Note: This may vary based on screen resolution + */ + constexpr uint32_t EMU_TO_PIXEL_RATIO = 9525; + + /** + * @brief MIME type enumeration for image formats + * @details This enum provides type-safe representation of image MIME types, + * replacing string-based MIME types for better performance and type safety. + */ + enum class XLMimeType : uint8_t { + PNG, /**< PNG image format */ + JPEG, /**< JPEG image format */ + BMP, /**< BMP image format */ + GIF, /**< GIF image format */ + Unknown /**< Unknown or unsupported image format */ + }; + + /** + * @brief Class representing shared binary data and metadata for an image file + * @details This class stores metadata about an image and provides access to shared binary data + * through the parent document. It uses a hash-based approach to avoid storing + * duplicate binary data in memory. + */ + class OPENXLSX_EXPORT XLImage { + public: + /** + * @brief Default constructor + */ + XLImage() = default; + + /** + * @brief Constructor with all parameters + * @param parentDoc Pointer to the parent XLDocument + * @param imageDataHash Hash of the image binary data + * @param mimeType MIME type of the image + * @param extension File extension of the image + * @param filePackagePath Package path in archive + * @param widthPixels Width in pixels + * @param heightPixels Height in pixels + */ + XLImage(XLDocument* parentDoc, size_t imageDataHash, XLMimeType mimeType, + const std::string& extension, const std::string& filePackagePath, + uint32_t widthPixels, uint32_t heightPixels); + + /** + * @brief Get the image binary data + * @return Vector containing the image binary data + */ + std::vector getImageData() const; + + /** + * @brief Verify the image data integrity and metadata consistency + * @param dbgMsg Optional pointer to append debug message + * @return Number of issues found (0 = no issues) + */ + int verifyData(std::string* dbgMsg = nullptr) const; + + // Getters + XLDocument* parentDoc() const { return m_parentDoc; } + size_t imageDataHash() const { return m_imageDataHash; } + XLMimeType mimeType() const { return m_mimeType; } + const std::string& extension() const { return m_extension; } + const std::string& filePackagePath() const { return m_filePackagePath; } + uint32_t widthPixels() const { return m_widthPixels; } + uint32_t heightPixels() const { return m_heightPixels; } + + // Setters + void setParentDoc(XLDocument* parentDoc) { m_parentDoc = parentDoc; } + void setImageDataHash(size_t imageDataHash) { m_imageDataHash = imageDataHash; } + void setMimeType(XLMimeType mimeType) { m_mimeType = mimeType; } + void setExtension(const std::string& extension) { m_extension = extension; } + void setFilePackagePath(const std::string& filePackagePath) { m_filePackagePath = filePackagePath; } + void setWidthPixels(uint32_t widthPixels) { m_widthPixels = widthPixels; } + void setHeightPixels(uint32_t heightPixels) { m_heightPixels = heightPixels; } + + private: + XLDocument* m_parentDoc = nullptr; /**< Pointer to parent document */ + size_t m_imageDataHash = 0; /**< Hash of the image binary data */ + XLMimeType m_mimeType = XLMimeType::Unknown; /**< MIME type of the image */ + std::string m_extension; /**< File extension of the image */ + std::string m_filePackagePath; /**< Package path in archive */ + uint32_t m_widthPixels = 0; /**< Width in pixels */ + uint32_t m_heightPixels = 0; /**< Height in pixels */ + }; + + /** + * @brief Shared pointer type for XLImage + */ + using XLImageShPtr = std::shared_ptr; + + /** + * @brief Vector type for embedded images + */ + using XLEmbImgVec = std::vector; + + /** + * @brief Shared pointer type for embedded image vector + */ + using XLEmbImgVecShPtr = std::shared_ptr; + + /** + * @brief Class managing a collection of XLImage objects and caching XLEmbImgVec objects + * @details This class manages a collection of shared pointers to XLImage objects, + * providing functionality to find, add, and prune images based on usage. + * In addition, it keeps XLEmbImgVec containers for XLWorksheet's when + * the XLWorksheet's go out of scope. + */ + class OPENXLSX_EXPORT XLImageManager { + public: + /** + * @brief Constructor + * @param parentDoc Pointer to the parent XLDocument + */ + explicit XLImageManager(XLDocument* parentDoc); + + /** + * @brief Destructor + */ + ~XLImageManager() = default; + + /** + * @brief Remove unused images from the collection and archive + * @details For each XLImageShPtr image in images, if image.get() == nullptr or + * image->use_count() <= 1, then remove image from images and remove + * image from archive by calling parentDoc->deleteEntry() + */ + void prune(); + + /** + * @brief Find or add an image to the collection + * @param packagePath The image package path (e.g., "xl/media/image1.png") or empty string to generate new + * @param imageData The image binary data + * @param contentType The content type of the image + * @return Shared pointer to the XLImage object + * @details If image already exists (findImageByImageData), return image from images. + * Otherwise, call parentDoc->addImageEntry() and add image to images. + */ + XLImageShPtr findOrAddImage(const std::string& packagePath, + const std::vector& imageData, + XLContentType contentType); + + /** + * @brief Find an image by its binary data hash + * @param imageData The image binary data to search for + * @return Shared pointer to the XLImage object, or nullptr if not found + */ + XLImageShPtr findImageByImageData(const std::vector& imageData) const; + + + /** + * @brief Generate a unique package filename for an image + * @param extension The file extension (e.g., ".png", ".jpg") + * @return A unique package filename (e.g., "xl/media/image1.png") + * @details Generates sequential filenames like Excel does, but ensures uniqueness + * across all extensions (image1.jpg, image2.png, image3.gif, etc.) + * Uses O(n log n) efficiency with sorted vector and binary search. + * Relies on XLImageManager being the authoritative source for all image paths. + */ + std::string generateUniquePackageFilename(const std::string& extension) const; + + + /** + * @brief Find an image by its file package path + * @param filePackagePath The file package path to search for + * @return Shared pointer to the XLImage object, or nullptr if not found + */ + XLImageShPtr findImageByFilePackagePath(const std::string& filePackagePath) const; + + /** + * @brief Get the number of images in the collection + * @return Number of images + */ + size_t getImageCount() const; + + /** + * @brief Get iterator to the beginning of the images collection + * @return Const iterator to the beginning + */ + std::vector::const_iterator begin() const; + + /** + * @brief Get iterator to the end of the images collection + * @return Const iterator to the end + */ + std::vector::const_iterator end() const; + + /** + * @brief Remove embedded image vector for worksheet + * @param sheetName The name of the worksheet + */ + void eraseEmbImgsForSheetName(const std::string& sheetName); + + /** + * @brief Find or create embedded image vector for worksheet + * @param sheetName The name of the worksheet + * @return Shared pointer to the embedded image vector + */ + XLEmbImgVecShPtr getEmbImgsForSheetName(const std::string& sheetName); + + /** + * @brief Find embedded image vector for worksheet, return null pointer if not found + * @param sheetName The name of the worksheet + * @return Shared pointer to the embedded image vector, or nullptr if not found + */ + XLEmbImgVecShPtr getEmbImgsForSheetName(const std::string& sheetName) const; + + /** + * @brief Verify data integrity for all images + * @param dbgMsg Optional pointer to append debug message + * @return Number of issues found (0 = no issues) + * @details Call verifyData() for each non-null image, check each image has same parentDoc, + * check each image has unique imageData + */ + int verifyData(std::string* dbgMsg = nullptr) const; + + /** + * @brief Debug function to print reference counts and brief info for all XLImageShPtr objects + * @param title Optional title for the debug output + */ + void printReferenceCounts(const std::string& title = "XLImageManager Reference Counts") const; + + private: + XLDocument* m_parentDoc; /**< Pointer to parent document */ + std::vector m_images; /**< Collection of image shared pointers */ + std::map m_sheetNmEmbImgVMap; /* cache embedded images for each worksheet */ + }; + + /** + * @brief The XLEmbeddedImage class represents an image that can be embedded in a worksheet + */ + class OPENXLSX_EXPORT XLEmbeddedImage + { + public: + /** + * @brief Default constructor + */ + XLEmbeddedImage() = default; + + /** + * @brief Copy constructor + * @param other The XLEmbeddedImage to copy + */ + XLEmbeddedImage(const XLEmbeddedImage& other) = default; + + /** + * @brief Move constructor + * @param other The XLEmbeddedImage to move + */ + XLEmbeddedImage(XLEmbeddedImage&& other) noexcept = default; + + /** + * @brief Destructor + */ + ~XLEmbeddedImage() = default; + + /** + * @brief Copy assignment operator + * @param other The XLEmbeddedImage to copy + * @return Reference to this XLEmbeddedImage + */ + XLEmbeddedImage& operator=(const XLEmbeddedImage& other) = default; + + /** + * @brief Move assignment operator + * @param other The XLEmbeddedImage to move + * @return Reference to this XLEmbeddedImage + */ + XLEmbeddedImage& operator=(XLEmbeddedImage&& other) noexcept = default; + + /** + * @brief Set the image ID + * @param imageId The unique ID for this image + */ + void setId(const std::string& imageId); + + /** + * @brief Get the image data + * @return Reference to the image data vector + */ + std::vector data() const; + + /** + * @brief Get the file extension + * @return The file extension (e.g., ".png", ".jpg") + */ + const std::string& extension() const; + + /** + * @brief Get the unique image ID + * @return The unique image ID + */ + const std::string& id() const; + + /** + * @brief Get the image width in pixels + * @return The width in pixels + */ + uint32_t widthPixels() const; + + /** + * @brief Get the image height in pixels + * @return The height in pixels + */ + uint32_t heightPixels() const; + + /** + * @brief Get the image MIME type + * @return The MIME type as enum + */ + XLMimeType mimeType() const; + + /** + * @brief Set the display width in Excel units (EMUs) + * @param width The display width in EMUs + */ + void setDisplayWidthEMUs(uint32_t width); + + /** + * @brief Set the display height in Excel units (EMUs) + * @param height The display height in EMUs + */ + void setDisplayHeightEMUs(uint32_t height); + + /** + * @brief Get the display width in Excel units (EMUs) + * @return The display width in EMUs + */ + uint32_t displayWidthEMUs() const; + + /** + * @brief Get the display height in Excel units (EMUs) + * @return The display height in EMUs + */ + uint32_t displayHeightEMUs() const; + + /** + * @brief Set display size in pixels (converts to EMUs automatically) + * @param widthPixels The display width in pixels + * @param heightPixels The display height in pixels + */ + void setDisplaySizePixels(uint32_t widthPixels, uint32_t heightPixels); + + /** + * @brief Check if the image is valid + * @return True if the image has valid data + */ + bool isValid() const; + + /** + * @brief Get the content type for this image + * @return The XLContentType enum value + */ + XLContentType contentType() const; + + /** + * @brief Compare two images for debugging + * @param other The other XLEmbeddedImage to compare with + * @param diffMsg Optional pointer to append difference message (up to 16384 characters) + * @return 0 if identical, <0 if this precedes other, >0 if this follows other + */ + int compare(const XLEmbeddedImage& other, std::string* diffMsg = nullptr) const; + + /** + * @brief Verify internal data integrity and class invariants + * @param dbgMsg Optional pointer to append debug message explaining any errors found + * @return Number of errors found (0 = EXIT_SUCCESS, data integrity OK) + */ + int verifyData(std::string* dbgMsg = nullptr) const; + + // Registry/relationship setter functions (for XLImageInfo compatibility) + /** + * @brief Set the relationship ID + * @param relationshipId The relationship ID in drawing.xml.rels + */ + void setRelationshipId(const std::string& relationshipId); + + /** + * @brief Set the anchor cell reference + * @param anchorCell The primary anchor cell reference (e.g., "A1", "B5") + */ + void setAnchorCell(const std::string& anchorCell); + + + /** + * @brief Set the anchor type + * @param anchorType The anchor type enum + */ + void setAnchorType(XLImageAnchorType anchorType); + + /** + * @brief Convert row/column coordinates to Excel cell reference + * @param row Row number (0-based) + * @param col Column number (0-based) + * @return Excel cell reference (e.g., "A1", "B5") + */ + static std::string rowColToCellRef(uint32_t row, uint16_t col); + + /** + * @brief Convert Excel cell reference to row/column coordinates + * @param cellRef Excel cell reference (e.g., "A1", "B5") + * @return Pair of (row, col) coordinates (0-based) + */ + static std::pair cellRefToRowCol(const std::string& cellRef); + + // Registry/relationship getter functions (for XLImageInfo compatibility) + /** + * @brief Get the relationship ID + * @return The relationship ID string + */ + const std::string& relationshipId() const; + + /** + * @brief Get the anchor cell reference + * @return The anchor cell reference string + */ + std::string anchorCell() const; + + /** + * @brief Get the anchor type + * @return anchor type + */ + const XLImageAnchorType& anchorType() const; + + /** + * @brief Get the shared pointer to the image data + * @return Shared pointer to the XLImage + */ + XLImageShPtr getImage() const; + + /** + * @brief Set the shared pointer to the image data + * @param image Shared pointer to the XLImage + */ + void setImage(XLImageShPtr image); + + const XLImageAnchor& getImageAnchor() const{ return m_imageAnchor; } + + void setImageAnchor(const XLImageAnchor& a){ m_imageAnchor = a; } + + /** + * @brief Check if this image is empty (default constructed) + * @return True if empty, false otherwise + */ + bool isEmpty() const; + + private: + XLImageShPtr m_image; /**< Shared pointer to the image data */ + XLImageAnchor m_imageAnchor; /**< Image anchor information (positioning, IDs, display dimensions) */ + + /** + * @brief Generate a unique image ID + * @return A unique image ID string + */ + static std::string generateId(uint32_t imageNumber); + }; + + /** + * @brief Utility class for image operations and test data + */ + class OPENXLSX_EXPORT XLImageUtils + { + public: + // Static const sample binary data for testing + static const std::vector png31x15Data; + static const std::vector jpeg32x18Data; + static const std::vector bmp33x16Data; + static const std::vector gif26x16Data; + + /** + * @brief Detect MIME type from binary image data + * @param data Binary image data + * @return MIME type string (e.g., "image/png") + */ + static std::string detectMimeType(const std::vector& data); + + /** + * @brief Get file extension from MIME type + * @param mime MIME type string + * @return File extension (e.g., ".png") + */ + static std::string extensionFromMime(const std::string& mime); + + /** + * @brief Determine MIME type from file extension + * @param extension File extension + * @return MIME type + */ + static XLMimeType mimeTypeFromExtension(const std::string& extension); + + /** + * @brief Determine file extension from MIME type + * @param mimeType MIME type string + * @return File extension string + */ + static std::string extensionFromMimeType(XLMimeType mimeType); + + /** + * @brief Convert pixels to EMUs (Excel Measurement Units) + * @param pixels Number of pixels + * @return Number of EMUs + */ + static uint32_t pixelsToEMUs(uint32_t pixels); + + /** + * @brief Get image dimensions from binary data + * @param data Binary image data + * @param mimeType MIME type of the image + * @return Pair of (width, height) in pixels + */ + static std::pair getImageDimensions(const std::vector& data, XLMimeType mimeType); + + /** + * @brief Convert pixels to EMUs (Excel units) + * @param pixels Number of pixels + * @return Equivalent EMUs + @deprecated -- duplicate of pixelsToEMUs() + */ + static uint32_t pixelsToEmus(uint32_t pixels); + + /** + * @brief Convert EMUs to pixels + * @param emus Number of EMUs + * @return Equivalent pixels + */ + static uint32_t emusToPixels(uint32_t emus); + + /** + * @brief Convert EMUs to Excel units + * @param emus EMU value + * @return Excel unit value + */ + static uint32_t emusToExcelUnits(uint32_t emus); + + /** + * @brief Convert points to EMUs + * @param points Point value + * @return EMU value + */ + static uint32_t pointsToEmus(double points); + + /** + * @brief Validate image data integrity + * @param data Binary image data + * @param mimeType MIME type of the image + * @return True if image data is valid + */ + static bool validateImageData(const std::vector& data, const XLMimeType& mimeType); + + /** + * @brief Calculate aspect ratio from dimensions + * @param width Width in pixels + * @param height Height in pixels + * @return Aspect ratio (width/height) + */ + static double calculateAspectRatio(uint32_t width, uint32_t height); + + /** + * @brief Get image dimensions from binary data (auto-detect MIME type) + * @param data Binary image data + * @return Pair of (width, height) in pixels, or {0,0} if detection fails + */ + static std::pair getImageDimensions(const std::vector& data); + + /** + * @brief Check if image dimensions are reasonable + * @param width Width in pixels + * @param height Height in pixels + * @return True if dimensions are within reasonable bounds + */ + static bool isValidImageSize(uint32_t width, uint32_t height); + + /** + * @brief Calculate hash value for binary image data + * @param imageData Binary image data as vector + * @return Hash value for the image data + */ + static size_t imageDataHash(const std::vector& imageData); + + /** + * @brief Calculate hash value for image data stored as string + * @param imageDataStr Binary image data as string + * @return Hash value for the image data (same as imageDataHash for equivalent data) + */ + static size_t imageDataStrHash(const std::string& imageDataStr); + + /** + * @brief Match XML element name with target name (handles namespace prefixes) + * @param elementName The XML element name (e.g., "xdr:oneCellAnchor") + * @param targetName The target name to match (e.g., "oneCellAnchor") + * @return True if the element name matches the target name + */ + static bool matchesElementName(const std::string& elementName, const std::string& targetName); + + /** + * @brief Convert MIME type to XLContentType enum + * @param mimeType MIME type enum + * @return Corresponding XLContentType enum value + */ + static XLContentType mimeTypeToContentType(const XLMimeType& mimeType); + + /** + * @brief Convert XLContentType enum to MIME type + * @param contentType XLContentType enum value + * @return Corresponding MIME type + */ + static XLMimeType contentTypeToMimeType(XLContentType contentType); + + /** + * @brief Convert MIME type string to XLMimeType enum + * @param mimeType MIME type string (e.g., "image/png") + * @return Corresponding XLMimeType enum value + */ + static XLMimeType stringToMimeType(const std::string& mimeType); + + /** + * @brief Convert XLMimeType enum to MIME type string + * @param mimeType XLMimeType enum value + * @return Corresponding MIME type string (e.g., "image/png") + */ + static std::string mimeTypeToString(XLMimeType mimeType); + + /** + * @brief Detect MIME type from binary image data and return as enum + * @param data Binary image data + * @return Corresponding XLMimeType enum value + */ + static XLMimeType detectMimeTypeEnum(const std::vector& data); + }; + +} // namespace OpenXLSX + +#endif // OPENXLSX_XLIMAGE_HPP diff --git a/OpenXLSX/headers/XLSheet.hpp b/OpenXLSX/headers/XLSheet.hpp index edd4eeef..a6e874cc 100644 --- a/OpenXLSX/headers/XLSheet.hpp +++ b/OpenXLSX/headers/XLSheet.hpp @@ -1287,92 +1287,39 @@ namespace OpenXLSX // Image Methods //---------------------------------------------------------------------------------------------------------------------- - /** - * @brief Add an image to the worksheet at the specified cell - * @param image The XLImage object to add - * @param cellRef The cell reference where to place the image (e.g., "A1") - * @return True if successful, false otherwise - * @note Will fail if an image with the same ID already exists - */ - bool addImage(const XLImage& image, const std::string& cellRef); - - /** - * @brief Add an image to the worksheet at the specified cell - * @param image The XLImage object to add - * @param cellRef The XLCellReference where to place the image - * @return True if successful, false otherwise - */ - bool addImage(const XLImage& image, const XLCellReference& cellRef); - - /** - * @brief Add an image to the worksheet at the specified cell - * @param image The XLImage object to add - * @param row The row number (1-based) - * @param column The column number (1-based) - * @return True if successful, false otherwise - */ - bool addImage(const XLImage& image, uint32_t row, uint16_t column); - - /** - * @brief Add an image to the worksheet with precise positioning - * @param image The XLImage object to add - * @param row The row number (1-based) - * @param column The column number (1-based) - * @param rowOffset Offset from the top-left of the cell in EMUs - * @param colOffset Offset from the top-left of the cell in EMUs - * @return True if successful, false otherwise - * @note Images with offset positioning are constrained to fit within a single cell - * by Excel/OpenOffice. This is the expected behavior - the offset positions - * the image within the cell, but the image size is automatically scaled to - * fit the cell dimensions. This is different from regular addImage() which - * can span multiple cells. - */ - bool addImageWithOffset(const XLImage& image, uint32_t row, uint16_t column, - int32_t rowOffset = 0, int32_t colOffset = 0); - - /** - * @brief Add an image to the worksheet with two-cell anchoring - * @param image The XLImage object to add - * @param fromRow Starting row number (1-based) - * @param fromCol Starting column number (1-based) - * @param toRow Ending row number (1-based) - * @param toCol Ending column number (1-based) - * @param fromRowOffset Offset from top-left of starting cell in EMUs - * @param fromColOffset Offset from top-left of starting cell in EMUs - * @param toRowOffset Offset from top-left of ending cell in EMUs - * @param toColOffset Offset from top-left of ending cell in EMUs - * @return True if successful, false otherwise - */ - bool addImageTwoCellAnchor(const XLImage& image, - uint32_t fromRow, uint16_t fromCol, - uint32_t toRow, uint16_t toCol, - int32_t fromRowOffset = 0, int32_t fromColOffset = 0, - int32_t toRowOffset = 0, int32_t toColOffset = 0); - - /** - * @brief Add an image from file to the worksheet at the specified cell - * @param imagePath Path to the image file - * @param cellRef The cell reference where to place the image (e.g., "A1") - * @return True if successful, false otherwise - */ - bool addImageFromFile(const std::string& imagePath, const std::string& cellRef); - - /** - * @brief Add an image from file to the worksheet at the specified cell - * @param imagePath Path to the image file - * @param cellRef The XLCellReference where to place the image - * @return True if successful, false otherwise - */ - bool addImageFromFile(const std::string& imagePath, const XLCellReference& cellRef); - - /** - * @brief Add an image from file to the worksheet at the specified cell - * @param imagePath Path to the image file - * @param row The row number (1-based) - * @param column The column number (1-based) - * @return True if successful, false otherwise - */ - bool addImageFromFile(const std::string& imagePath, uint32_t row, uint16_t column); + /* + Construct oneCellAnchor XLImageAnchor from cellRef. + Construct XLImage from imageData and mimeType + return Image ID + */ + std::string embedImageFromImageData( const XLCellReference& cellRef, + const std::vector& imageData, + XLMimeType mimeType = XLMimeType::Unknown ); + + /* + Construct XLImage from imageData and mimeType + return Image ID + */ + std::string embedImageFromImageData( const XLImageAnchor& imageAnchor, + const std::vector& imageData, + XLMimeType mimeType = XLMimeType::Unknown ); + + /* + Construct oneCellAnchor XLImageAnchor from cellRef. + Construct XLImage from imageFileName and mimeType + return Image ID + */ + std::string embedImageFromFile( const XLCellReference& cellRef, + const std::string& imageFileName, + XLMimeType mimeType = XLMimeType::Unknown ); + + /* + Construct XLImage from imageFileName and mimeType + return Image ID + */ + std::string embedImageFromFile( const XLImageAnchor& imageAnchor, + const std::string& imageFileName, + XLMimeType mimeType = XLMimeType::Unknown ); /** * @brief Get the number of images in the worksheet @@ -1405,62 +1352,69 @@ namespace OpenXLSX * @return A relationship ID string (e.g., "rId1", "rId2", etc.) * @note This generates IDs for DrawingML XML based on the current number of images */ - std::string getRelationshipIdFromImageCount() const; + std::string getNextUnusedRelationshipId() const; //---------------------------------------------------------------------------------------------------------------------- // Image Query Methods //---------------------------------------------------------------------------------------------------------------------- /** - * @brief Get all image information in the worksheet - * @return Const reference to vector of XLImageInfo objects - * @note Returns a const reference for efficiency - no copying of the vector + * @brief Get specific embedded image by its image ID + * @param imageId The unique image ID (e.g., "img1", "img2") + * @return Const reference to XLEmbeddedImage object, or empty XLEmbeddedImage if not found + * @note Returns a const reference for efficiency - no copying of the object */ - const std::vector& getImageInfos() const; + const XLEmbeddedImage& getEmbImageByImageID(const std::string& imageId) const; /** - * @brief Get specific image information by its ID - * @param imageId The unique image identifier (e.g., "img1", "img2") - * @return Const reference to XLImageInfo object, or empty XLImageInfo if not found - * @note Returns a const reference for efficiency - no copying of the struct - */ - const XLImageInfo& getImageInfoByImageID(const std::string& imageId) const; - - /** - * @brief Get specific image information by its relationship ID + * @brief Get specific embedded image by its relationship ID * @param relationshipId The relationship ID in drawing.xml.rels (e.g., "rId1", "rId2") - * @return Const reference to XLImageInfo object, or empty XLImageInfo if not found - * @note Returns a const reference for efficiency - no copying of the struct + * @return Const reference to XLEmbeddedImage object, or empty XLEmbeddedImage if not found + * @note Returns a const reference for efficiency - no copying of the object */ - const XLImageInfo& getImageInfoByRelationshipId(const std::string& relationshipId) const; + const XLEmbeddedImage& getEmbImageByRelationshipId(const std::string& relationshipId) const; /** - * @brief Get all image information at a specific cell + * @brief Get all embedded images at a specific cell * @param cellRef The cell reference (e.g., "A1", "B5") - * @return Vector of XLImageInfo objects at that cell + * @return Vector of XLEmbeddedImage objects at that cell */ - std::vector getImageInfosAtCell(const std::string& cellRef) const; + std::vector getEmbImagesAtCell(const std::string& cellRef) const; /** - * @brief Get all image information in a specific range + * @brief Get all embedded images in a specific range * @param cellRange The cell range (e.g., "A1:B5", "C10:D20") - * @return Vector of XLImageInfo objects in that range + * @return Vector of XLEmbeddedImage objects in that range */ - std::vector getImageInfosInRange(const std::string& cellRange) const; + std::vector getEmbImagesInRange(const std::string& cellRange) const; /** - * @brief Get iterator to the beginning of the image information collection - * @return Const iterator to the first image info + * @brief Get the embedded images vector, ensuring it's initialized + * @return Reference to the embedded images vector + * @details This function ensures m_embImages is properly initialized from XLImageManager + */ + std::vector& getEmbImages(); + + /** + * @brief Get the embedded images vector, ensuring it's initialized (const version) + * @return Const reference to the embedded images vector + * @details This function ensures m_embImages is properly initialized from XLImageManager + */ + const std::vector& getEmbImages() const; + + /** + * @brief Get iterator to the beginning of the embedded images collection + * @return Const iterator to the first embedded image * @note Useful for range-based for loops and STL algorithms */ - std::vector::const_iterator imageInfosBegin() const; + std::vector::const_iterator embImagesBegin() const; /** - * @brief Get iterator to the end of the image information collection - * @return Const iterator to the end of the image information collection + * @brief Get iterator to the end of the embedded images collection + * @return Const iterator to the end of the embedded images collection * @note Useful for range-based for loops and STL algorithms */ - std::vector::const_iterator imageInfosEnd() const; + std::vector::const_iterator embImagesEnd() const; //---------------------------------------------------------------------------------------------------------------------- // Image Modification Methods @@ -1493,6 +1447,9 @@ namespace OpenXLSX //---------------------------------------------------------------------------------------------------------------------- private: + /* add Embedded Image, assuming imageAnchor and image are ready to add */ + std::string embedImage( const XLImageAnchor& imageAnchor, XLImageShPtr image ); + /** * @brief Remove image node from DrawingML XML (in-memory document) * @param relationshipId The relationship ID of the image to remove @@ -1512,7 +1469,6 @@ namespace OpenXLSX * @param relationshipId The relationship ID of the image file to remove * @return True if the file was found and removed from archive */ - bool removeImageFileFromArchiveIfUnreferenced(const std::string& relationshipId) const; /** * @brief Get image path from relationship ID @@ -1526,7 +1482,6 @@ namespace OpenXLSX * @param imagePath The relative image path (e.g., "../media/image_img3.png") * @return True if the file was found and removed */ - bool removeImageFileIfUnreferenced(const std::string& imagePath) const; public: //---------------------------------------------------------------------------------------------------------------------- @@ -1561,7 +1516,7 @@ namespace OpenXLSX * @param imageId The image ID to check * @return True if the ID exists, false otherwise */ - bool hasImageWithId(const std::string& imageId) const; + bool hasEmbImageWithId(const std::string& imageId) const; /** * @brief Check if a relationship ID already exists @@ -1570,14 +1525,6 @@ namespace OpenXLSX */ bool hasImageWithRelationshipId(const std::string& relationshipId) const; - /** - * @brief Robust XML element name matching that handles namespace prefixes - * @param elementName The actual element name (may include namespace prefix) - * @param targetName The target element name to match - * @return True if the element name matches the target (with or without namespace) - */ - bool matchesElementName(const std::string& elementName, const std::string& targetName) const; - //---------------------------------------------------------------------------------------------------------------------- // Private Helper Methods for Image Registry //---------------------------------------------------------------------------------------------------------------------- @@ -1590,18 +1537,11 @@ namespace OpenXLSX void readDrawingRelationships(std::map& relationshipMap) const; /** - * @brief Process a oneCellAnchor element and add to registry - * @param anchor The oneCellAnchor XML node - * @param relationshipMap Map of relationship IDs to image files - */ - void processOneCellAnchor(const pugi::xml_node& anchor, const std::map& relationshipMap) const; - - /** - * @brief Process a twoCellAnchor element and add to registry - * @param anchor The twoCellAnchor XML node + * @brief Process a single XLImageAnchor into an XLEmbeddedImage and add to m_embImages + * @param anchorInfo The parsed anchor information * @param relationshipMap Map of relationship IDs to image files */ - void processTwoCellAnchor(const pugi::xml_node& anchor, const std::map& relationshipMap) const; + void processAnchorToEmbeddedImage(const XLImageAnchor& anchorInfo, const std::map& relationshipMap); /** * @brief Extract image ID from XML pic element @@ -1614,49 +1554,9 @@ namespace OpenXLSX * @brief Convert EMUs to pixels (approximate conversion) * @param emus EMUs value * @return Approximate pixel value + @deprected -> move to XLImageUtils */ uint32_t emusToPixels(uint32_t emus) const; - /** - * @brief Add an image to the DrawingML - * @param image The image to add - * @param row The row number (1-based) - * @param column The column number (1-based) - * @param relationshipId The relationship ID for the image - */ - void addImageToDrawingML(const XLImage& image, uint32_t row, uint16_t column, const std::string& relationshipId); - - /** - * @brief Add an image to the DrawingML with precise positioning - * @param image The image to add - * @param row The row number (1-based) - * @param column The column number (1-based) - * @param relationshipId The relationship ID for the image - * @param rowOffset Offset from the top-left of the cell in EMUs - * @param colOffset Offset from the top-left of the cell in EMUs - */ - void addImageToDrawingMLWithOffset(const XLImage& image, uint32_t row, uint16_t column, - const std::string& relationshipId, - int32_t rowOffset = 0, int32_t colOffset = 0); - - /** - * @brief Add an image to the DrawingML with two-cell anchoring - * @param image The image to add - * @param fromRow Starting row number (1-based) - * @param fromCol Starting column number (1-based) - * @param toRow Ending row number (1-based) - * @param toCol Ending column number (1-based) - * @param relationshipId The relationship ID for the image - * @param fromRowOffset Offset from top-left of starting cell in EMUs - * @param fromColOffset Offset from top-left of starting cell in EMUs - * @param toRowOffset Offset from top-left of ending cell in EMUs - * @param toColOffset Offset from top-left of ending cell in EMUs - */ - void addImageToDrawingMLTwoCellAnchor(const XLImage& image, - uint32_t fromRow, uint16_t fromCol, - uint32_t toRow, uint16_t toCol, - const std::string& relationshipId, - int32_t fromRowOffset = 0, int32_t fromColOffset = 0, - int32_t toRowOffset = 0, int32_t toColOffset = 0); /** * @brief Create the drawing relationships file @@ -1666,17 +1566,29 @@ namespace OpenXLSX /** * @brief Create an empty DrawingML object for worksheets without images - * This is needed when addImage() is called on a worksheet that doesn't have images yet + * This is needed when embedImage() is called on a worksheet that doesn't have images yet * @return Reference to the newly created DrawingML object */ XLDrawingML& createEmptyDrawingML(); - + + void addImageRelationship(const XLEmbeddedImage& image); + + /** * @brief Add a relationship for a new image to the existing relationships file * @param image The image to add a relationship for * @param relationshipId The relationship ID to use +* @deprecated + */ + void addImageRelationship(const XLEmbeddedImage& image, const std::string& relationshipId); + + /** + * @brief Helper function to create new relationships XML with a single relationship + * @param relationshipId The relationship ID to use + * @param imageRelativePath The relative path to the image file + * @return The complete XML string for the relationships file */ - void addImageRelationship(const XLImage& image, const std::string& relationshipId); + std::string createNewRelationshipsXml(const std::string& relationshipId, const std::string& imageRelativePath); /** * @brief fetch the # number from the xml path xl/worksheets/sheet#.xml @@ -1767,6 +1679,40 @@ namespace OpenXLSX int verifyBinaryImageData(std::string* dbgMsg = nullptr) const; bool isBinaryFileReferenced(const std::string& imagePath) const; + /** + * @brief Get the drawing relationships filename for this worksheet + * @return The relationships filename (e.g., "xl/drawings/_rels/drawing1.xml.rels") + */ + std::string getDrawingRelsFilename() const; + + /** + * @brief Get the drawing filename for this worksheet + * @return The drawing filename (e.g., "xl/drawings/drawing1.xml") + */ + std::string getDrawingFilename() const; + + /** + * @brief Get the image media filename for an embedded image + * @param imageId The image ID + * @param extension The image file extension (e.g., ".png", ".jpg") + * @return The image media filename (e.g., "xl/media/image_1.png") + */ + static std::string getImageMediaFilename(const std::string& imageId, const std::string& extension); + + /** + * @brief Convert a relative image path to absolute archive path + * @param relativePath The relative path (e.g., "../media/image_1.png" or "media/image_1.png") + * @return The absolute archive path (e.g., "xl/media/image_1.png") + */ + static std::string getAbsoluteImagePath(const std::string& relativePath); + + /** + * @brief Extract filename from a full image path + * @param imagePath The full image path + * @return The absolute archive path with extracted filename (e.g., "xl/media/image_1.png") + */ + static std::string getImageMediaPathFromFilename(const std::string& imagePath); + private: // ---------- Private Member Variables ---------- // XLRelationships m_relationships{}; /**< class handling the worksheet relationships */ XLMergeCells m_merges{}; /**< class handling the */ @@ -1774,9 +1720,7 @@ namespace OpenXLSX XLDrawingML m_drawingML{}; /**< class handling the worksheet DrawingML object */ XLComments m_comments{}; /**< class handling the worksheet comments */ XLTables m_tables{}; /**< class handling the worksheet table settings */ - std::vector m_images{}; /**< vector storing images in the worksheet */ - mutable std::vector m_imageRegistry{}; /**< vector storing parsed image metadata for query operations */ - static const XLImageInfo m_emptyImageInfo; /**< empty image info for returning when image not found */ + XLEmbImgVecShPtr m_embImages; /**< shared pointer to vector storing images in the worksheet */ const std::vector< std::string_view >& m_nodeOrder = XLWorksheetNodeOrder; // worksheet XML root node required child sequence }; diff --git a/OpenXLSX/sources/XLDocument.cpp b/OpenXLSX/sources/XLDocument.cpp index 7c1f71ba..10d1ae69 100644 --- a/OpenXLSX/sources/XLDocument.cpp +++ b/OpenXLSX/sources/XLDocument.cpp @@ -435,13 +435,13 @@ namespace } // namespace -XLDocument::XLDocument(const IZipArchive& zipArchive) : m_xmlSavingDeclaration{}, m_archive(zipArchive) {} +XLDocument::XLDocument(const IZipArchive& zipArchive) : m_xmlSavingDeclaration{}, m_archive(zipArchive), m_imageManager(this) {} /** * @details An alternative constructor, taking a std::string with the path to the .xlsx package as an argument. */ XLDocument::XLDocument(const std::string& docPath, const IZipArchive& zipArchive) - : m_xmlSavingDeclaration{}, m_archive(zipArchive) + : m_xmlSavingDeclaration{}, m_archive(zipArchive), m_imageManager(this) { open(docPath); } @@ -764,6 +764,10 @@ void XLDocument::saveAs(const std::string& fileName, bool forceOverwrite) m_archive.addEntry(item.getXmlPath(), item.getRawData(XLXmlSavingDeclaration(m_xmlSavingDeclaration.version(), m_xmlSavingDeclaration.encoding(),xmlIsStandalone))); } + + // Prune unused images before saving + m_imageManager.prune(); + m_archive.save(m_filePath); } @@ -1774,6 +1778,10 @@ bool XLDocument::execCommand(const XLCommand& command) m_appProperties.deleteSheetName(command.getParam("sheetName")); std::string sheetPath = m_wbkRelationships.relationshipById(command.getParam("sheetID")).target(); if (sheetPath.substr(0, 4) != "/xl/") sheetPath = "/xl/" + sheetPath; // 2024-12-15: respect absolute sheet path + + // Clean up embedded images for this worksheet before deleting the sheet + m_imageManager.eraseEmbImgsForSheetName(command.getParam("sheetName")); + m_archive.deleteEntry(sheetPath.substr(1)); m_contentTypes.deleteOverride(sheetPath); m_wbkRelationships.deleteRelationship(command.getParam("sheetID")); diff --git a/OpenXLSX/sources/XLDrawingML.cpp b/OpenXLSX/sources/XLDrawingML.cpp index 94c97726..657a562f 100644 --- a/OpenXLSX/sources/XLDrawingML.cpp +++ b/OpenXLSX/sources/XLDrawingML.cpp @@ -13,6 +13,7 @@ #include "XLDrawingML.hpp" #include "XLImage.hpp" #include "XLXmlData.hpp" +#include "XLCellReference.hpp" #include "utilities/XLUtilities.hpp" namespace OpenXLSX @@ -22,105 +23,13 @@ namespace OpenXLSX { } - // ===== Public Methods ===== // - void XLDrawingML::addImage(const XLImage& image, uint32_t row, uint16_t column, const std::string& relationshipId) - { - // Get the root element + void XLDrawingML::addImage(const XLEmbeddedImage& embImage){ + const XLImageAnchor& imageAnchor = embImage.getImageAnchor(); XMLNode rootNode = xmlDocument().document_element(); - - // Create a oneCellAnchor element for single-cell images - XMLNode anchor = rootNode.append_child("xdr:oneCellAnchor"); - - // Create the 'from' element - XMLNode from = anchor.append_child("xdr:from"); - from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column - 1).c_str()); - from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value("0"); - from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row - 1).c_str()); - from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); - - // Create the 'ext' element - this is crucial for oneCellAnchor! - XMLNode ext = anchor.append_child("xdr:ext"); - ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); - ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); - - // Create the picture element - XMLNode pic = anchor.append_child("xdr:pic"); - - // Create non-visual picture properties - XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); - XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); - // Use the actual image ID directly (now numeric to match Microsoft Excel) - cNvPr.append_attribute("id").set_value(image.id().c_str()); - cNvPr.append_attribute("name").set_value(("Picture " + image.id()).c_str()); - - // Add extension list with creation ID for better compatibility - // XMLNode cNvPrExtLst = cNvPr.append_child("a:extLst"); - // XMLNode cNvPrExt = cNvPrExtLst.append_child("a:ext"); - // cNvPrExt.append_attribute("uri").set_value("{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}"); - // XMLNode creationId = cNvPrExt.append_child("a16:creationId"); - // std::string creationIdValue = "{00000000-0008-0000-0000-00000" + std::to_string(imageCount() + 1) + "00000}"; - // creationId.append_attribute("id").set_value(creationIdValue.c_str()); - - XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); - XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); - picLocks.append_attribute("noChangeAspect").set_value("1"); - picLocks.append_attribute("noChangeArrowheads").set_value("1"); - - // Create blip fill - XMLNode blipFill = pic.append_child("xdr:blipFill"); - XMLNode blip = blipFill.append_child("a:blip"); - blip.append_attribute("r:embed").set_value(relationshipId.c_str()); - - // Add extension list for better compatibility - // XMLNode extLst = blip.append_child("a:extLst"); - // XMLNode blipExt = extLst.append_child("a:ext"); - // blipExt.append_attribute("uri").set_value("{28A0092B-C50C-407E-A947-70E740481C1C}"); - // XMLNode useLocalDpi = blipExt.append_child("a14:useLocalDpi"); - // useLocalDpi.append_attribute("val").set_value("0"); - - XMLNode srcRect = blipFill.append_child("a:srcRect"); - XMLNode stretch = blipFill.append_child("a:stretch"); - stretch.append_child("a:fillRect"); - - // Create shape properties - XMLNode spPr = pic.append_child("xdr:spPr"); - spPr.append_attribute("bwMode").set_value("auto"); - - XMLNode xfrm = spPr.append_child("a:xfrm"); - XMLNode off = xfrm.append_child("a:off"); - off.append_attribute("x").set_value("0"); - off.append_attribute("y").set_value("0"); - - // Calculate image size that preserves aspect ratio within the bounding box - uint32_t imageWidth = image.displayWidth(); - uint32_t imageHeight = image.displayHeight(); - - // Get the image's natural dimensions - uint32_t naturalWidth = image.widthPixels(); - uint32_t naturalHeight = image.heightPixels(); - - // Calculate scaling factor to fit within bounding box while preserving aspect ratio - double scaleX = static_cast(imageWidth) / naturalWidth; - double scaleY = static_cast(imageHeight) / naturalHeight; - double scale = std::min(scaleX, scaleY); // Use smaller scale to fit within box - - // Calculate actual image size (preserving aspect ratio) - uint32_t actualWidth = static_cast(naturalWidth * scale); - uint32_t actualHeight = static_cast(naturalHeight * scale); - - XMLNode xfrmExt = xfrm.append_child("a:ext"); - xfrmExt.append_attribute("cx").set_value(std::to_string(actualWidth).c_str()); - xfrmExt.append_attribute("cy").set_value(std::to_string(actualHeight).c_str()); - - XMLNode prstGeom = spPr.append_child("a:prstGeom"); - prstGeom.append_attribute("prst").set_value("rect"); - prstGeom.append_child("a:avLst"); - - spPr.append_child("a:noFill"); - - // Create client data - simple empty element as per Google example - anchor.append_child("xdr:clientData"); - } + (void)XLDrawingML::createXMLNode(rootNode, imageAnchor); + } + + // ===== Public Methods ===== // size_t XLDrawingML::imageCount() const { @@ -142,17 +51,6 @@ namespace OpenXLSX } // ===== Private Methods ===== // - uint32_t XLDrawingML::emusToExcelUnits(uint32_t emus) const - { - // Convert EMUs to Excel units (1 EMU = 1/9525 Excel units) - return emus / 9525; - } - - uint32_t XLDrawingML::pointsToEmus(double points) const - { - // Convert points to EMUs (1 point = 12700 EMUs) - return static_cast(points * 12700); - } uint32_t XLDrawingML::calculateCellPosition(uint32_t row, uint16_t column) const { @@ -161,167 +59,6 @@ namespace OpenXLSX return (row - 1) * 15 + (column - 1) * 64; } - /** - * @details Add an image to the drawing with precise positioning - */ - void XLDrawingML::addImageWithOffset(const XLImage& image, uint32_t row, uint16_t column, - const std::string& relationshipId, - int32_t rowOffset, int32_t colOffset) - { - // Get the root element - XMLNode rootNode = xmlDocument().document_element(); - - // Create a twoCellAnchor element with proper oneCell editing (Excel standard) - XMLNode anchor = rootNode.append_child("xdr:twoCellAnchor"); - anchor.append_attribute("editAs").set_value("oneCell"); - - // Create the 'from' element with offset - XMLNode from = anchor.append_child("xdr:from"); - from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column - 1).c_str()); - from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(colOffset).c_str()); - from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row - 1).c_str()); - from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(rowOffset).c_str()); - - // Create the 'to' element (same as from for oneCell anchoring) - XMLNode to = anchor.append_child("xdr:to"); - to.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(column).c_str()); - to.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value("0"); - to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(row).c_str()); - to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value("0"); - - // Create the 'ext' element (required for twoCellAnchor) - XMLNode ext = anchor.append_child("xdr:ext"); - ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); - ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); - - // Create the picture element (same as original addImage method) - XMLNode pic = anchor.append_child("xdr:pic"); - - // Create non-visual picture properties - XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); - XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); - // Use the actual image ID directly (now numeric to match Microsoft Excel) - cNvPr.append_attribute("id").set_value(image.id().c_str()); - cNvPr.append_attribute("name").set_value(("Picture " + image.id()).c_str()); - - XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); - XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); - picLocks.append_attribute("noChangeAspect").set_value("1"); - picLocks.append_attribute("noChangeArrowheads").set_value("1"); - - // Create blip fill - XMLNode blipFill = pic.append_child("xdr:blipFill"); - XMLNode blip = blipFill.append_child("a:blip"); - blip.append_attribute("r:embed").set_value(relationshipId.c_str()); - - XMLNode srcRect = blipFill.append_child("a:srcRect"); - XMLNode stretch = blipFill.append_child("a:stretch"); - stretch.append_child("a:fillRect"); - - // Create shape properties - XMLNode spPr = pic.append_child("xdr:spPr"); - XMLNode xfrm = spPr.append_child("a:xfrm"); - XMLNode off = xfrm.append_child("a:off"); - off.append_attribute("x").set_value("0"); - off.append_attribute("y").set_value("0"); - - XMLNode xfrmExt = xfrm.append_child("a:ext"); - xfrmExt.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); - xfrmExt.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); - - XMLNode prstGeom = spPr.append_child("a:prstGeom"); - prstGeom.append_attribute("prst").set_value("rect"); - prstGeom.append_child("a:avLst"); - - // Use noFill to match other methods (no borders) - spPr.append_child("a:noFill"); - - // Add clientData element (required for Excel compatibility) - anchor.append_child("xdr:clientData"); - } - - /** - * @details Add an image to the drawing with two-cell anchoring - */ - void XLDrawingML::addImageTwoCellAnchor(const XLImage& image, - uint32_t fromRow, uint16_t fromCol, - uint32_t toRow, uint16_t toCol, - const std::string& relationshipId, - int32_t fromRowOffset, int32_t fromColOffset, - int32_t toRowOffset, int32_t toColOffset) - { - // Get the root element - XMLNode rootNode = xmlDocument().document_element(); - - // Create a twoCellAnchor element - XMLNode anchor = rootNode.append_child("xdr:twoCellAnchor"); - anchor.append_attribute("editAs").set_value("oneCell"); - - // Create the 'from' element - XMLNode from = anchor.append_child("xdr:from"); - from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(fromCol - 1).c_str()); - from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(fromColOffset).c_str()); - from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(fromRow - 1).c_str()); - from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(fromRowOffset).c_str()); - - // Create the 'to' element - XMLNode to = anchor.append_child("xdr:to"); - to.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(toCol).c_str()); - to.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(toColOffset).c_str()); - to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(toRow).c_str()); - to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(toRowOffset).c_str()); - - // Create the 'ext' element (required for twoCellAnchor) - XMLNode ext = anchor.append_child("xdr:ext"); - ext.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); - ext.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); - - // Create the picture element (same as original addImage method) - XMLNode pic = anchor.append_child("xdr:pic"); - - // Create non-visual picture properties - XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); - XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); - // Use the actual image ID directly (now numeric to match Microsoft Excel) - cNvPr.append_attribute("id").set_value(image.id().c_str()); - cNvPr.append_attribute("name").set_value(("Picture " + image.id()).c_str()); - - XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); - XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); - picLocks.append_attribute("noChangeAspect").set_value("1"); - picLocks.append_attribute("noChangeArrowheads").set_value("1"); - - // Create blip fill - XMLNode blipFill = pic.append_child("xdr:blipFill"); - XMLNode blip = blipFill.append_child("a:blip"); - blip.append_attribute("r:embed").set_value(relationshipId.c_str()); - - XMLNode srcRect = blipFill.append_child("a:srcRect"); - XMLNode stretch = blipFill.append_child("a:stretch"); - stretch.append_child("a:fillRect"); - - // Create shape properties - XMLNode spPr = pic.append_child("xdr:spPr"); - XMLNode xfrm = spPr.append_child("a:xfrm"); - XMLNode off = xfrm.append_child("a:off"); - off.append_attribute("x").set_value("0"); - off.append_attribute("y").set_value("0"); - - XMLNode xfrmExt = xfrm.append_child("a:ext"); - xfrmExt.append_attribute("cx").set_value(std::to_string(image.displayWidth()).c_str()); - xfrmExt.append_attribute("cy").set_value(std::to_string(image.displayHeight()).c_str()); - - XMLNode prstGeom = spPr.append_child("a:prstGeom"); - prstGeom.append_attribute("prst").set_value("rect"); - prstGeom.append_child("a:avLst"); - - // Use noFill to match oneCellAnchor behavior (no borders) - spPr.append_child("a:noFill"); - - // Add clientData element (required for Excel compatibility) - anchor.append_child("xdr:clientData"); - } - int XLDrawingML::verifyData(std::string* dbgMsg) const { int errorCount = 0; @@ -344,8 +81,9 @@ namespace OpenXLSX // Count images in XML and verify structure size_t xmlImageCount = 0; for (auto anchor : rootNode.children()) { - if (anchor.name() == std::string("xdr:oneCellAnchor") || - anchor.name() == std::string("xdr:twoCellAnchor")) { + // Check anchor type using enum conversion + XLImageAnchorType anchorType = XLImageAnchorUtils::stringToAnchorType(anchor.name()); + if (anchorType != XLImageAnchorType::Unknown) { xmlImageCount++; // Check for required elements @@ -389,4 +127,584 @@ namespace OpenXLSX return XMLNode(); // Return empty node on error } } + + // ========== XLImageAnchor Implementation ========== // + + XLImageAnchor::XLImageAnchor(const std::string& imageId, const std::string& relationshipId, + uint32_t row, uint16_t col, int32_t rowOffset, int32_t colOffset, + uint32_t displayWidth, uint32_t displayHeight) + : anchorType(XLImageAnchorType::OneCellAnchor) + , imageId(imageId) + , relationshipId(relationshipId) + , fromRow(row) // Store as-is (0-based from XML) + , fromCol(col) // Store as-is (0-based from XML) + , fromRowOffset(rowOffset) + , fromColOffset(colOffset) + , toRow(row) // For oneCellAnchor, to = from + , toCol(col) // For oneCellAnchor, to = from + , toRowOffset(0) // No offset for 'to' in oneCellAnchor + , toColOffset(0) // No offset for 'to' in oneCellAnchor + , displayWidthEMUs(displayWidth) + , displayHeightEMUs(displayHeight) + , actualWidthEMUs(displayWidth) + , actualHeightEMUs(displayHeight) + { + } + + XLImageAnchor::XLImageAnchor(const std::string& imageId, const std::string& relationshipId, + uint32_t fromRow, uint16_t fromCol, uint32_t toRow, uint16_t toCol, + int32_t fromRowOffset, int32_t fromColOffset, + int32_t toRowOffset, int32_t toColOffset, + uint32_t displayWidth, uint32_t displayHeight) + : anchorType(XLImageAnchorType::TwoCellAnchor) + , imageId(imageId) + , relationshipId(relationshipId) + , fromRow(fromRow) // Store as-is (0-based from XML) + , fromCol(fromCol) // Store as-is (0-based from XML) + , fromRowOffset(fromRowOffset) + , fromColOffset(fromColOffset) + , toRow(toRow) // Store as-is (0-based from XML) + , toCol(toCol) // Store as-is (0-based from XML) + , toRowOffset(toRowOffset) + , toColOffset(toColOffset) + , displayWidthEMUs(displayWidth) + , displayHeightEMUs(displayHeight) + , actualWidthEMUs(displayWidth) + , actualHeightEMUs(displayHeight) + { + } + + void XLImageAnchor::reset(){ + anchorType = XLImageAnchorType::Unknown; + imageId.erase(); + relationshipId.erase(); + fromRow = 0; + fromCol = 0; + fromRowOffset = 0; + fromColOffset = 0; + toRow = 0; + toCol = 0; + toRowOffset = 0; + toColOffset = 0; + displayWidthEMUs = 0; + displayHeightEMUs = 0; + actualWidthEMUs = 0; + actualHeightEMUs = 0; + } + + void XLImageAnchor::initOneCell( const XLCellReference& cellRef, int32_t rowOffset, + int32_t colOffset){ + reset(); + anchorType = XLImageAnchorType::OneCellAnchor; + setFromCellReference(cellRef); + fromRowOffset = rowOffset; + fromColOffset = colOffset; + } + + void XLImageAnchor::initTwoCell( const XLCellReference& fromCellRef, + const XLCellReference& toCellRef, int32_t fromROffset, + int32_t fromCOffset, int32_t toROffset, int32_t toCOffset){ + reset(); + anchorType = XLImageAnchorType::TwoCellAnchor; + setFromCellReference(fromCellRef); + setToCellReference(toCellRef); + fromRowOffset = fromROffset; + fromColOffset = fromCOffset; + toRowOffset = toROffset; + toColOffset = toCOffset; + } + + XLCellReference XLImageAnchor::getFromCellReference() const{ + const XLCellReference fromCellReference( fromRow + 1, fromCol + 1 ); + return fromCellReference; + } + + void XLImageAnchor::setFromCellReference( const XLCellReference& fromCellRef ){ + const uint32_t cellRefRow = fromCellRef.row(); + const uint16_t cellRefColumn = fromCellRef.column(); + fromRow = ( cellRefRow > 0 ) ? (cellRefRow - 1) : 0; + fromCol = ( cellRefColumn > 0 ) ? (cellRefColumn - 1) : 0; + } + + XLCellReference XLImageAnchor::getToCellReference() const{ + const XLCellReference toCellReference( toRow + 1, toCol + 1 ); + return toCellReference; + } + + void XLImageAnchor::setToCellReference( const XLCellReference& toCellRef ){ + const uint32_t cellRefRow = toCellRef.row(); + const uint16_t cellRefColumn = toCellRef.column(); + toRow = ( cellRefRow > 0 ) ? (cellRefRow - 1) : 0; + toCol = ( cellRefColumn > 0 ) ? (cellRefColumn - 1) : 0; + } + + void XLImageAnchor::setDisplaySizeWithAspectRatio( + const uint32_t& widthPixels, const uint32_t& heightPixels, + const uint32_t& maxWidthEmus, const uint32_t& maxHeightEmus ){ + uint32_t targetWidth = XLImageUtils::pixelsToEmus(widthPixels); + uint32_t targetHeight = XLImageUtils::pixelsToEmus(heightPixels); + + if (widthPixels > 0 || heightPixels > 0) { + double aspectRatio = XLImageUtils::calculateAspectRatio(widthPixels, heightPixels); + assert(aspectRatio > 0.0); + + const double maxWidth = static_cast(maxWidthEmus); + const double maxHeight = static_cast(maxHeightEmus); + const double widthA = maxHeight * aspectRatio; + + if (widthA <= maxWidth) { + /* + +------------+--------+ + | | | + | | | + | | | + | | |maxHeight + | | | + | | | + | | | + | widthA | | + +------------+--------+ + maxWidth + */ + targetWidth = static_cast(round(widthA)); + targetHeight = maxHeightEmus; + } + else { + /* + +---------------------+ + | | + | | + +---------------------+ + | |maxHeight + | | + | heightB| + | | + | | + +---------------------+ + maxWidth + */ + const double heightB = maxWidth / aspectRatio; + assert(heightB <= (1.00000001 * maxHeight)); + targetWidth = maxWidthEmus; + targetHeight = static_cast(round(heightB)); + } + + assert(fabs(aspectRatio - (static_cast(targetWidth) / + static_cast(targetHeight))) < 1e-4); + } + + displayWidthEMUs = targetWidth; + displayHeightEMUs = targetHeight; + } + + void XLImageAnchor::setDisplaySizeWithAspectRatio( + const std::vector& imageData, const XLMimeType& mimeType, + const uint32_t& maxWidthEmus, const uint32_t& maxHeightEmus ){ + + // Convert string mimeType to enum, auto-detect if unknown + XLMimeType mimeTypeEnum = mimeType; + if (mimeTypeEnum == XLMimeType::Unknown) { + mimeTypeEnum = XLImageUtils::detectMimeTypeEnum(imageData); + } + + const std::pair imageDims = + XLImageUtils::getImageDimensions(imageData, mimeType); + + setDisplaySizeWithAspectRatio( imageDims.first, imageDims.second, + maxWidthEmus, maxHeightEmus ); + } + + void XLImageAnchor::setDisplaySizeWithAspectRatio( + const std::string& imageFileName, const XLMimeType& mimeType, + const uint32_t& maxWidthEmus, const uint32_t& maxHeightEmus ){ + + std::ifstream file(imageFileName, std::ios::binary); + if (file.is_open()) { + // Read the entire file into a vector + file.seekg(0, std::ios::end); + size_t fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector imageData(fileSize); + file.read(reinterpret_cast(imageData.data()), fileSize); + file.close(); + + setDisplaySizeWithAspectRatio( imageData, mimeType, maxWidthEmus, + maxHeightEmus ); + } + } + + + std::string XLImageAnchor::getAnchorCellReference() const + { + // Convert 0-based coordinates to 1-based for cell reference + return XLCellReference(fromRow + 1, fromCol + 1).address(); + } + + bool XLImageAnchor::isTwoCell() const + { + return anchorType == XLImageAnchorType::TwoCellAnchor; + } + + std::pair XLImageAnchor::getDisplayDimensions() const + { + return std::make_pair(displayWidthEMUs, displayHeightEMUs); + } + + std::pair XLImageAnchor::getActualDimensions() const + { + return std::make_pair(actualWidthEMUs, actualHeightEMUs); + } + + void XLImageAnchor::setActualDimensions(uint32_t width, uint32_t height) + { + actualWidthEMUs = width; + actualHeightEMUs = height; + } + + std::pair XLImageAnchor::getDisplayDimensionsPixels() const + { + return std::make_pair( + XLImageUtils::emusToPixels(displayWidthEMUs), + XLImageUtils::emusToPixels(displayHeightEMUs) + ); + } + + std::pair XLImageAnchor::getActualDimensionsPixels() const + { + return std::make_pair( + XLImageUtils::emusToPixels(actualWidthEMUs), + XLImageUtils::emusToPixels(actualHeightEMUs) + ); + } + + // ========== XLDrawingML Static Utility Functions ========== // + + XMLNode XLDrawingML::createXMLNode(XMLNode rootNode, const XLImageAnchor& imgAnchorInfo) + { + if (rootNode.empty()) { + return XMLNode(); + } + + // Create the appropriate anchor element + XMLNode anchor; + if (imgAnchorInfo.isTwoCell()) { + anchor = rootNode.append_child("xdr:twoCellAnchor"); + anchor.append_attribute("editAs").set_value("oneCell"); + } else { + anchor = rootNode.append_child("xdr:oneCellAnchor"); + } + + // Create the 'from' element + XMLNode from = anchor.append_child("xdr:from"); + from.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(imgAnchorInfo.fromCol).c_str()); + from.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(imgAnchorInfo.fromColOffset).c_str()); + from.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(imgAnchorInfo.fromRow).c_str()); + from.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(imgAnchorInfo.fromRowOffset).c_str()); + + // Create the 'to' element (for twoCellAnchor) + if (imgAnchorInfo.isTwoCell()) { + XMLNode to = anchor.append_child("xdr:to"); + to.append_child("xdr:col").append_child(pugi::node_pcdata).set_value(std::to_string(imgAnchorInfo.toCol).c_str()); + to.append_child("xdr:colOff").append_child(pugi::node_pcdata).set_value(std::to_string(imgAnchorInfo.toColOffset).c_str()); + to.append_child("xdr:row").append_child(pugi::node_pcdata).set_value(std::to_string(imgAnchorInfo.toRow).c_str()); + to.append_child("xdr:rowOff").append_child(pugi::node_pcdata).set_value(std::to_string(imgAnchorInfo.toRowOffset).c_str()); + } + + // Create the 'ext' element (required for both anchor types) + XMLNode ext = anchor.append_child("xdr:ext"); + ext.append_attribute("cx").set_value(std::to_string(imgAnchorInfo.displayWidthEMUs).c_str()); + ext.append_attribute("cy").set_value(std::to_string(imgAnchorInfo.displayHeightEMUs).c_str()); + + // Create the picture element + XMLNode pic = anchor.append_child("xdr:pic"); + + // Create non-visual picture properties + XMLNode nvPicPr = pic.append_child("xdr:nvPicPr"); + XMLNode cNvPr = nvPicPr.append_child("xdr:cNvPr"); + cNvPr.append_attribute("id").set_value(imgAnchorInfo.imageId.c_str()); + cNvPr.append_attribute("name").set_value(("Picture " + imgAnchorInfo.imageId).c_str()); + + XMLNode cNvPicPr = nvPicPr.append_child("xdr:cNvPicPr"); + XMLNode picLocks = cNvPicPr.append_child("a:picLocks"); + picLocks.append_attribute("noChangeAspect").set_value("1"); + picLocks.append_attribute("noChangeArrowheads").set_value("1"); + + // Create blip fill + XMLNode blipFill = pic.append_child("xdr:blipFill"); + XMLNode blip = blipFill.append_child("a:blip"); + blip.append_attribute("r:embed").set_value(imgAnchorInfo.relationshipId.c_str()); + + XMLNode srcRect = blipFill.append_child("a:srcRect"); + XMLNode stretch = blipFill.append_child("a:stretch"); + stretch.append_child("a:fillRect"); + + // Create shape properties + XMLNode spPr = pic.append_child("xdr:spPr"); + XMLNode xfrm = spPr.append_child("a:xfrm"); + XMLNode off = xfrm.append_child("a:off"); + off.append_attribute("x").set_value("0"); + off.append_attribute("y").set_value("0"); + + XMLNode xfrmExt = xfrm.append_child("a:ext"); + xfrmExt.append_attribute("cx").set_value(std::to_string(imgAnchorInfo.actualWidthEMUs).c_str()); + xfrmExt.append_attribute("cy").set_value(std::to_string(imgAnchorInfo.actualHeightEMUs).c_str()); + + XMLNode prstGeom = spPr.append_child("a:prstGeom"); + prstGeom.append_attribute("prst").set_value("rect"); + prstGeom.append_child("a:avLst"); + + // Use noFill to match other methods (no borders) + spPr.append_child("a:noFill"); + + // Add clientData element (required for Excel compatibility) + anchor.append_child("xdr:clientData"); + + return anchor; + } + + bool XLDrawingML::deleteXMLNode(XMLNode rootNode, const std::string& relationshipId) + { + if (rootNode.empty() || relationshipId.empty()) { + return false; + } + + // Find and remove the image node + for (auto anchor : rootNode.children()) { + // Check both oneCellAnchor and twoCellAnchor using enum conversion + XLImageAnchorType anchorType = XLImageAnchorUtils::stringToAnchorType(anchor.name()); + if (anchorType != XLImageAnchorType::Unknown) { + + // Look for the pic element with the matching relationship ID + XMLNode pic = anchor.child("xdr:pic"); + if (pic.empty()) { + pic = anchor.child("pic"); + } + + if (!pic.empty()) { + // Check blipFill -> blip -> r:embed + XMLNode blipFill = pic.child("xdr:blipFill"); + if (blipFill.empty()) { + blipFill = pic.child("a:blipFill"); + } + if (blipFill.empty()) { + blipFill = pic.child("blipFill"); + } + + if (!blipFill.empty()) { + XMLNode blip = blipFill.child("a:blip"); + if (blip.empty()) { + blip = blipFill.child("blip"); + } + + if (!blip.empty()) { + std::string embedId = blip.attribute("r:embed").value(); + if (embedId == relationshipId) { + // Found the matching image, remove the entire anchor + rootNode.remove_child(anchor); + return true; + } + } + } + } + } + } + + return false; // Image not found + } + + bool XLDrawingML::parseXMLNode(const pugi::xml_node& anchorXMLNode, XLImageAnchor* imgAnchorInfo) + { + if (anchorXMLNode.empty() || !imgAnchorInfo) { + return false; + } + + try { + // Determine anchor type + std::string nodeName = anchorXMLNode.name(); + // Determine anchor type using utility function + imgAnchorInfo->anchorType = XLImageAnchorUtils::stringToAnchorType(nodeName); + if (imgAnchorInfo->anchorType == XLImageAnchorType::Unknown) { + return false; // Unknown anchor type + } + + // Extract cell position from 'from' element + XMLNode from = anchorXMLNode.child("xdr:from"); + if (from.empty()) { + from = anchorXMLNode.child("from"); + } + if (from.empty()) { + return false; + } + + XMLNode colNode = from.child("xdr:col"); + if (colNode.empty()) { + colNode = from.child("col"); + } + XMLNode rowNode = from.child("xdr:row"); + if (rowNode.empty()) { + rowNode = from.child("row"); + } + + if (colNode.empty() || rowNode.empty()) { + return false; + } + + imgAnchorInfo->fromCol = colNode.text().as_uint(); + imgAnchorInfo->fromRow = rowNode.text().as_uint(); + + // Extract offsets + XMLNode colOffNode = from.child("xdr:colOff"); + if (colOffNode.empty()) { + colOffNode = from.child("colOff"); + } + XMLNode rowOffNode = from.child("xdr:rowOff"); + if (rowOffNode.empty()) { + rowOffNode = from.child("rowOff"); + } + + imgAnchorInfo->fromColOffset = colOffNode.empty() ? 0 : colOffNode.text().as_int(); + imgAnchorInfo->fromRowOffset = rowOffNode.empty() ? 0 : rowOffNode.text().as_int(); + + // Extract 'to' element for twoCellAnchor + if (imgAnchorInfo->isTwoCell()) { + XMLNode to = anchorXMLNode.child("xdr:to"); + if (to.empty()) { + to = anchorXMLNode.child("to"); + } + if (!to.empty()) { + XMLNode toColNode = to.child("xdr:col"); + if (toColNode.empty()) { + toColNode = to.child("col"); + } + XMLNode toRowNode = to.child("xdr:row"); + if (toRowNode.empty()) { + toRowNode = to.child("row"); + } + + if (!toColNode.empty() && !toRowNode.empty()) { + imgAnchorInfo->toCol = toColNode.text().as_uint(); + imgAnchorInfo->toRow = toRowNode.text().as_uint(); + } + + // Extract 'to' offsets + XMLNode toColOffNode = to.child("xdr:colOff"); + if (toColOffNode.empty()) { + toColOffNode = to.child("colOff"); + } + XMLNode toRowOffNode = to.child("xdr:rowOff"); + if (toRowOffNode.empty()) { + toRowOffNode = to.child("rowOff"); + } + + imgAnchorInfo->toColOffset = toColOffNode.empty() ? 0 : toColOffNode.text().as_int(); + imgAnchorInfo->toRowOffset = toRowOffNode.empty() ? 0 : toRowOffNode.text().as_int(); + } + } else { + // For oneCellAnchor, to = from + imgAnchorInfo->toCol = imgAnchorInfo->fromCol; + imgAnchorInfo->toRow = imgAnchorInfo->fromRow; + imgAnchorInfo->toColOffset = 0; + imgAnchorInfo->toRowOffset = 0; + } + + // Extract dimensions from xdr:ext element + XMLNode ext = anchorXMLNode.child("xdr:ext"); + if (ext.empty()) { + ext = anchorXMLNode.child("ext"); + } + if (!ext.empty()) { + imgAnchorInfo->displayWidthEMUs = ext.attribute("cx").as_uint(); + imgAnchorInfo->displayHeightEMUs = ext.attribute("cy").as_uint(); + } + + // Extract image information from pic element + XMLNode pic = anchorXMLNode.child("xdr:pic"); + if (pic.empty()) { + pic = anchorXMLNode.child("pic"); + } + if (!pic.empty()) { + // Extract image ID + XMLNode nvPicPr = pic.child("xdr:nvPicPr"); + if (nvPicPr.empty()) { + nvPicPr = pic.child("nvPicPr"); + } + if (!nvPicPr.empty()) { + XMLNode cNvPr = nvPicPr.child("xdr:cNvPr"); + if (cNvPr.empty()) { + cNvPr = nvPicPr.child("cNvPr"); + } + if (!cNvPr.empty()) { + imgAnchorInfo->imageId = cNvPr.attribute("id").value(); + } + } + + // Extract relationship ID + XMLNode blipFill = pic.child("xdr:blipFill"); + if (blipFill.empty()) { + blipFill = pic.child("a:blipFill"); + } + if (blipFill.empty()) { + blipFill = pic.child("blipFill"); + } + if (!blipFill.empty()) { + XMLNode blip = blipFill.child("a:blip"); + if (blip.empty()) { + blip = blipFill.child("blip"); + } + if (!blip.empty()) { + imgAnchorInfo->relationshipId = blip.attribute("r:embed").value(); + } + } + + // Extract actual dimensions from shape properties + XMLNode spPr = pic.child("xdr:spPr"); + if (spPr.empty()) { + spPr = pic.child("spPr"); + } + if (!spPr.empty()) { + XMLNode xfrm = spPr.child("a:xfrm"); + if (!xfrm.empty()) { + XMLNode xfrmExt = xfrm.child("a:ext"); + if (!xfrmExt.empty()) { + imgAnchorInfo->actualWidthEMUs = xfrmExt.attribute("cx").as_uint(); + imgAnchorInfo->actualHeightEMUs = xfrmExt.attribute("cy").as_uint(); + } + } + } + } + + // If actual dimensions weren't found, use display dimensions + if (imgAnchorInfo->actualWidthEMUs == 0) { + imgAnchorInfo->actualWidthEMUs = imgAnchorInfo->displayWidthEMUs; + } + if (imgAnchorInfo->actualHeightEMUs == 0) { + imgAnchorInfo->actualHeightEMUs = imgAnchorInfo->displayHeightEMUs; + } + + return true; + } + catch (const std::exception&) { + return false; + } + } +} + +// ========== XLImageAnchorUtils Implementation ========== // + +OpenXLSX::XLImageAnchorType OpenXLSX::XLImageAnchorUtils::stringToAnchorType(const std::string& anchorTypeStr) +{ + if (anchorTypeStr.find("oneCellAnchor") != std::string::npos) return XLImageAnchorType::OneCellAnchor; + if (anchorTypeStr.find("twoCellAnchor") != std::string::npos) return XLImageAnchorType::TwoCellAnchor; + return XLImageAnchorType::Unknown; +} + +std::string OpenXLSX::XLImageAnchorUtils::anchorTypeToString(XLImageAnchorType anchorType) +{ + switch (anchorType) { + case XLImageAnchorType::OneCellAnchor: return "oneCellAnchor"; + case XLImageAnchorType::TwoCellAnchor: return "twoCellAnchor"; + case XLImageAnchorType::Unknown: + default: return ""; + } } diff --git a/OpenXLSX/sources/XLImage.cpp b/OpenXLSX/sources/XLImage.cpp index 081d2f77..dbad9df5 100644 --- a/OpenXLSX/sources/XLImage.cpp +++ b/OpenXLSX/sources/XLImage.cpp @@ -1,664 +1,981 @@ -/* - - ____ ____ ___ ____ ____ ____ ___ - 6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M' - 8P Y8 `MM. d' MM 6M' ` `MM. d' -6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d' -MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d' -MM MM MM MM MM MM MM' MM `MMd MM YMMMMb `MMd -MM MM MM MM MMMMMMMM MM MM d'`MM. MM `Mb dMM. -YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. - 8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM. - YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_ - MM - MM - _MM_ - - Copyright (c) 2018, Kenneth Troldal Balslev - - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - Neither the name of the author nor the - names of any contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY - DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - */ - -// ===== External Includes ===== // -#include // std::ifstream -#include // std::stringstream -#include // std::find -#include // std::round, std::fabs - -// ===== OpenXLSX Includes ===== // -#include "XLImage.hpp" -#include "XLException.hpp" - -using namespace OpenXLSX; - -namespace OpenXLSX -{ - // ========== XLImage Member Functions ========== // - - /** - * @details Constructor from file path - */ - XLImage::XLImage(const std::string& imagePath) - { - loadFromFile(imagePath); - } - - /** - * @details Constructor from binary data - */ - XLImage::XLImage(const std::vector& imageData, const std::string& mimeType) - { - loadFromData(imageData, mimeType); - } - - /** - * @details Load image from file (backward compatibility) - */ - bool XLImage::loadFromFile(const std::string& imagePath) - { - // Generate a proper sequential ID instead of temp_id - static int counter = 1; - std::string imageId = "img" + std::to_string(counter++); - return loadFromFile(imagePath, imageId); - } - - /** - * @details Load image from file - */ - bool XLImage::loadFromFile(const std::string& imagePath, const std::string& imageId) - { - std::ifstream file(imagePath, std::ios::binary); - if (!file.is_open()) { - return false; - } - - // Read the entire file into a vector - file.seekg(0, std::ios::end); - size_t fileSize = file.tellg(); - file.seekg(0, std::ios::beg); - - m_imageData.resize(fileSize); - file.read(reinterpret_cast(m_imageData.data()), fileSize); - file.close(); - - // Determine file extension and MIME type - size_t dotPos = imagePath.find_last_of('.'); - if (dotPos != std::string::npos) { - m_extension = imagePath.substr(dotPos); - std::transform(m_extension.begin(), m_extension.end(), m_extension.begin(), ::tolower); - } - - m_mimeType = mimeTypeFromExtension(m_extension); - - // Get image dimensions - auto dimensions = getImageDimensions(m_imageData, m_mimeType); - m_widthPixels = dimensions.first; - m_heightPixels = dimensions.second; - - // Set default display size (same as original size) - m_displayWidth = pixelsToEMUs(m_widthPixels); - m_displayHeight = pixelsToEMUs(m_heightPixels); - - // Set the provided ID - m_id = imageId; - - return true; - } - - /** - * @details Load image from binary data (backward compatibility) - */ - bool XLImage::loadFromData(const std::vector& imageData, const std::string& mimeType) - { - // Generate a proper sequential ID instead of temp_id - static int counter = 1; - std::string imageId = "img" + std::to_string(counter++); - return loadFromData(imageData, mimeType, imageId); - } - - /** - * @details Load image from binary data - */ - bool XLImage::loadFromData(const std::vector& imageData, const std::string& mimeType, const std::string& imageId) - { - m_imageData = imageData; - m_mimeType = mimeType; - m_extension = extensionFromMimeType(mimeType); - - // Get image dimensions - auto dimensions = getImageDimensions(m_imageData, m_mimeType); - m_widthPixels = dimensions.first; - m_heightPixels = dimensions.second; - - // Set default display size (same as original size) - m_displayWidth = pixelsToEMUs(m_widthPixels); - m_displayHeight = pixelsToEMUs(m_heightPixels); - - // Set the provided ID - m_id = imageId; - - return true; - } - - /** - * @details Set the image ID - */ - void XLImage::setId(const std::string& imageId) - { - m_id = imageId; - } - - /** - * @details Get the image data - */ - const std::vector& XLImage::data() const - { - return m_imageData; - } - - /** - * @details Get the MIME type - */ - const std::string& XLImage::mimeType() const - { - return m_mimeType; - } - - /** - * @details Get the file extension - */ - const std::string& XLImage::extension() const - { - return m_extension; - } - - /** - * @details Get the unique image ID - */ - const std::string& XLImage::id() const - { - return m_id; - } - - /** - * @details Get the image width in pixels - */ - uint32_t XLImage::widthPixels() const - { - return m_widthPixels; - } - - /** - * @details Get the image height in pixels - */ - uint32_t XLImage::heightPixels() const - { - return m_heightPixels; - } - - /** - * @details Set the display width in Excel units (EMUs) - */ - void XLImage::setDisplayWidth(uint32_t width) - { - m_displayWidth = width; - } - - /** - * @details Set the display height in Excel units (EMUs) - */ - void XLImage::setDisplayHeight(uint32_t height) - { - m_displayHeight = height; - } - - /** - * @details Get the display width in Excel units (EMUs) - */ - uint32_t XLImage::displayWidth() const - { - return m_displayWidth; - } - - /** - * @details Get the display height in Excel units (EMUs) - */ - uint32_t XLImage::displayHeight() const - { - return m_displayHeight; - } - - /** - * @details Check if the image is valid - */ - bool XLImage::isValid() const - { - return !m_imageData.empty() && !m_mimeType.empty() && !m_id.empty(); - } - - /** - * @details Get the content type for this image - */ - XLContentType XLImage::contentType() const - { - if (m_mimeType == ImageMimeTypes::PNG) return XLContentType::ImagePNG; - if (m_mimeType == ImageMimeTypes::JPEG) return XLContentType::ImageJPEG; - if (m_mimeType == ImageMimeTypes::BMP) return XLContentType::ImageBMP; - if (m_mimeType == ImageMimeTypes::GIF) return XLContentType::ImageGIF; - return XLContentType::Unknown; - } - - // ========== XLImage Private Member Functions ========== // - - /** - * @details Generate a unique image ID - */ - std::string XLImage::generateId(uint32_t imageNumber) - { - return "img" + std::to_string(imageNumber); - } - - /** - * @details Determine MIME type from file extension - */ - std::string XLImage::mimeTypeFromExtension(const std::string& extension) - { - if (extension == ".png") return ImageMimeTypes::PNG; - if (extension == ".jpg" || extension == ".jpeg") return ImageMimeTypes::JPEG; - if (extension == ".bmp") return ImageMimeTypes::BMP; - if (extension == ".gif") return ImageMimeTypes::GIF; - return ""; - } - - /** - * @details Determine file extension from MIME type - */ - std::string XLImage::extensionFromMimeType(const std::string& mimeType) - { - if (mimeType == ImageMimeTypes::PNG) return ".png"; - if (mimeType == ImageMimeTypes::JPEG) return ".jpg"; - if (mimeType == ImageMimeTypes::BMP) return ".bmp"; - if (mimeType == ImageMimeTypes::GIF) return ".gif"; - return ""; - } - - /** - * @details Convert pixels to EMUs (Excel Measurement Units) - * 1 pixel = 9525 EMUs (approximately) - */ - uint32_t XLImage::pixelsToEMUs(uint32_t pixels) - { - return pixels * EMU_TO_PIXEL_RATIO; - } - - /** - * @details Get image dimensions from binary data - * This is a simplified implementation that works for basic cases - * For production use, consider using a proper image library like libpng, libjpeg, etc. - */ - std::pair XLImage::getImageDimensions(const std::vector& data, const std::string& mimeType) - { - if (data.size() < 8) return {0, 0}; - - // PNG signature check and dimension extraction - if (mimeType == ImageMimeTypes::PNG) { - if (data.size() >= 24 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47) { - // PNG IHDR chunk contains width and height at bytes 16-23 - uint32_t width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19]; - uint32_t height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23]; - return {width, height}; - } - } - // JPEG signature check and dimension extraction - else if (mimeType == ImageMimeTypes::JPEG) { - if (data.size() >= 2 && data[0] == 0xFF && data[1] == 0xD8) { - // For JPEG, we need to find the SOF0 marker (0xFFC0) and extract dimensions - for (size_t i = 2; i < data.size() - 8; ++i) { - if (data[i] == 0xFF && data[i + 1] == 0xC0) { - uint32_t height = (data[i + 5] << 8) | data[i + 6]; - uint32_t width = (data[i + 7] << 8) | data[i + 8]; - return {width, height}; - } - } - } - } - // BMP signature check and dimension extraction - else if (mimeType == ImageMimeTypes::BMP) { - if (data.size() >= 26 && data[0] == 0x42 && data[1] == 0x4D) { - // BMP header contains width and height at bytes 18-25 - uint32_t width = data[18] | (data[19] << 8) | (data[20] << 16) | (data[21] << 24); - uint32_t height = data[22] | (data[23] << 8) | (data[24] << 16) | (data[25] << 24); - return {width, height}; - } - } - // GIF signature check and dimension extraction - else if (mimeType == ImageMimeTypes::GIF) { - if (data.size() >= 10 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46) { - // GIF header contains width and height at bytes 6-9 - uint32_t width = data[6] | (data[7] << 8); - uint32_t height = data[8] | (data[9] << 8); - return {width, height}; - } - } - - // Default fallback - assume square image - return {100, 100}; - } - - /** - * @details Set display size in pixels (converts to EMUs automatically) - */ - void XLImage::setDisplaySizePixels(uint32_t widthPixels, uint32_t heightPixels) - { - m_displayWidth = pixelsToEmus(widthPixels); - m_displayHeight = pixelsToEmus(heightPixels); - } - - /** - * @details Set display size maintaining aspect ratio - */ - void XLImage::setDisplaySizeWithAspectRatio(uint32_t maxWidthEmus, uint32_t maxHeightEmus) - { - uint32_t targetWidth = pixelsToEmus(m_widthPixels); - uint32_t targetHeight = pixelsToEmus(m_heightPixels); - - if (m_widthPixels > 0 || m_heightPixels > 0) { - double aspectRatio = static_cast(m_widthPixels) / static_cast(m_heightPixels); - assert(aspectRatio > 0.0); - - const double maxWidth = static_cast(maxWidthEmus); - const double maxHeight = static_cast(maxHeightEmus); - const double widthA = maxHeight * aspectRatio; - - if (widthA <= maxWidth) { - /* - +------------+--------+ - | | | - | | | - | | | - | | |maxHeight - | | | - | | | - | | | - | widthA | | - +------------+--------+ - maxWidth - */ - targetWidth = static_cast(round(widthA)); - targetHeight = maxHeightEmus; - } - else { - /* - +---------------------+ - | | - | | - +---------------------+ - | |maxHeight - | | - | heightB| - | | - | | - +---------------------+ - maxWidth - */ - const double heightB = maxWidth / aspectRatio; - assert(heightB <= (1.00000001 * maxHeight)); - targetWidth = maxWidthEmus; - targetHeight = static_cast(round(heightB)); - } - - assert(fabs(aspectRatio - (static_cast(targetWidth) / - static_cast(targetHeight))) < 1e-4); - } - - m_displayWidth = targetWidth; - m_displayHeight = targetHeight; - } - - - /** - * @details Set display size maintaining aspect ratio - */ - void XLImage::setDisplaySizePixelsWithAspectRatio(uint32_t maxWidthPixels, uint32_t maxHeightPixels) - { - const uint32_t maxWidthEmus = pixelsToEmus(maxWidthPixels); - const uint32_t maxHeightEmus = pixelsToEmus(maxHeightPixels); - setDisplaySizeWithAspectRatio(maxWidthEmus, maxHeightEmus); - } - - /** - * @details Convert pixels to EMUs (Excel units) - * 1 pixel = 9525 EMUs (approximately) - */ - uint32_t XLImage::pixelsToEmus(uint32_t pixels) - { - return pixels * EMU_TO_PIXEL_RATIO; - } - - /** - * @details Convert EMUs to pixels - */ - uint32_t XLImage::emusToPixels(uint32_t emus) - { - return emus / EMU_TO_PIXEL_RATIO; - } - - /** - * @details Compare two image info structures for debugging - */ - int XLImageInfo::compare(const XLImageInfo& other, std::string* diffMsg) const - { - - // Compare image ID (most important for identification) - int idCompare = imageId.compare(other.imageId); - if (idCompare != 0) { - appendDbgMsg(diffMsg, "image ID differs: '" + imageId + "' vs '" + other.imageId + "'"); - return idCompare; - } - - // Compare relationship ID - int relIdCompare = relationshipId.compare(other.relationshipId); - if (relIdCompare != 0) { - appendDbgMsg(diffMsg, "relationship ID differs: '" + relationshipId + "' vs '" + other.relationshipId + "'"); - return relIdCompare; - } - - // Compare anchor cell - int cellCompare = anchorCell.compare(other.anchorCell); - if (cellCompare != 0) { - appendDbgMsg(diffMsg, "anchor cell differs: '" + anchorCell + "' vs '" + other.anchorCell + "'"); - return cellCompare; - } - - // Compare anchor type - int typeCompare = anchorType.compare(other.anchorType); - if (typeCompare != 0) { - appendDbgMsg(diffMsg, "anchor type differs: '" + anchorType + "' vs '" + other.anchorType + "'"); - return typeCompare; - } - - // Compare dimensions - if (widthPixels != other.widthPixels) { - appendDbgMsg(diffMsg, "width differs: " + std::to_string(widthPixels) + - " vs " + std::to_string(other.widthPixels) + " pixels"); - return widthPixels < other.widthPixels ? -1 : 1; - } - - if (heightPixels != other.heightPixels) { - appendDbgMsg(diffMsg, "height differs: " + std::to_string(heightPixels) + - " vs " + std::to_string(other.heightPixels) + " pixels"); - return heightPixels < other.heightPixels ? -1 : 1; - } - - // Compare display dimensions - if (displayWidthEMUs != other.displayWidthEMUs) { - appendDbgMsg(diffMsg, "display width differs: " + std::to_string(displayWidthEMUs) + - " vs " + std::to_string(other.displayWidthEMUs) + " EMUs"); - return displayWidthEMUs < other.displayWidthEMUs ? -1 : 1; - } - - if (displayHeightEMUs != other.displayHeightEMUs) { - appendDbgMsg(diffMsg, "display height differs: " + std::to_string(displayHeightEMUs) + - " vs " + std::to_string(other.displayHeightEMUs) + " EMUs"); - return displayHeightEMUs < other.displayHeightEMUs ? -1 : 1; - } - - // Image info structures are identical - return 0; - } - - /** - * @details Compare two images for debugging - */ - int XLImage::compare(const XLImage& other, std::string* diffMsg) const - { - - // Compare image data (most important for embedded images) - if (m_imageData != other.m_imageData) { - appendDbgMsg(diffMsg, "image data differs (size: " + std::to_string(m_imageData.size()) + - " vs " + std::to_string(other.m_imageData.size()) + " bytes)"); - return m_imageData.size() < other.m_imageData.size() ? -1 : 1; - } - - // Compare MIME type - int mimeCompare = m_mimeType.compare(other.m_mimeType); - if (mimeCompare != 0) { - appendDbgMsg(diffMsg, "MIME type differs: '" + m_mimeType + "' vs '" + other.m_mimeType + "'"); - return mimeCompare; - } - - // Compare extension - int extCompare = m_extension.compare(other.m_extension); - if (extCompare != 0) { - appendDbgMsg(diffMsg, "extension differs: '" + m_extension + "' vs '" + other.m_extension + "'"); - return extCompare; - } - - // Compare ID - int idCompare = m_id.compare(other.m_id); - if (idCompare != 0) { - appendDbgMsg(diffMsg, "image ID differs: '" + m_id + "' vs '" + other.m_id + "'"); - return idCompare; - } - - // Compare dimensions - if (m_widthPixels != other.m_widthPixels) { - appendDbgMsg(diffMsg, "width differs: " + std::to_string(m_widthPixels) + - " vs " + std::to_string(other.m_widthPixels) + " pixels"); - return m_widthPixels < other.m_widthPixels ? -1 : 1; - } - - if (m_heightPixels != other.m_heightPixels) { - appendDbgMsg(diffMsg, "height differs: " + std::to_string(m_heightPixels) + - " vs " + std::to_string(other.m_heightPixels) + " pixels"); - return m_heightPixels < other.m_heightPixels ? -1 : 1; - } - - // Compare display dimensions - if (m_displayWidth != other.m_displayWidth) { - appendDbgMsg(diffMsg, "display width differs: " + std::to_string(m_displayWidth) + - " vs " + std::to_string(other.m_displayWidth) + " EMUs"); - return m_displayWidth < other.m_displayWidth ? -1 : 1; - } - - if (m_displayHeight != other.m_displayHeight) { - appendDbgMsg(diffMsg, "display height differs: " + std::to_string(m_displayHeight) + - " vs " + std::to_string(other.m_displayHeight) + " EMUs"); - return m_displayHeight < other.m_displayHeight ? -1 : 1; - } - - // Images are identical - return 0; - } - - /** - * @details Verify internal data integrity and class invariants - */ - int XLImage::verifyData(std::string* dbgMsg) const - { - int errorCount = 0; - - // Check basic data integrity - if (m_imageData.empty()) { - appendDbgMsg(dbgMsg, "image data is empty"); - errorCount++; - } - - // Check MIME type consistency - if (m_mimeType.empty()) { - appendDbgMsg(dbgMsg, "MIME type is empty"); - errorCount++; - } - - // Check extension consistency - if (m_extension.empty()) { - appendDbgMsg(dbgMsg, "file extension is empty"); - errorCount++; - } - - // Check ID consistency - if (m_id.empty()) { - appendDbgMsg(dbgMsg, "image ID is empty"); - errorCount++; - } - - // Check dimension consistency - if (m_widthPixels == 0) { - appendDbgMsg(dbgMsg, "width in pixels is zero"); - errorCount++; - } - - if (m_heightPixels == 0) { - appendDbgMsg(dbgMsg, "height in pixels is zero"); - errorCount++; - } - - // Check display dimension consistency - if (m_displayWidth == 0) { - appendDbgMsg(dbgMsg, "display width in EMUs is zero"); - errorCount++; - } - - if (m_displayHeight == 0) { - appendDbgMsg(dbgMsg, "display height in EMUs is zero"); - errorCount++; - } - - // Check MIME type and extension consistency - if (!m_mimeType.empty() && !m_extension.empty()) { - if ((m_mimeType == "image/png" && m_extension != ".png") || - (m_mimeType == "image/jpeg" && m_extension != ".jpg" && m_extension != ".jpeg") || - (m_mimeType == "image/gif" && m_extension != ".gif") || - (m_mimeType == "image/bmp" && m_extension != ".bmp")) { - appendDbgMsg(dbgMsg, "MIME type '" + m_mimeType + "' does not match extension '" + m_extension + "'"); - errorCount++; - } - } - - return errorCount; - } - -} // namespace OpenXLSX +/* + + ____ ____ ___ ____ ____ ____ ___ + 6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M' + 8P Y8 `MM. d' MM 6M' ` `MM. d' +6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d' +MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d' +MM MM MM MM MM MM MM' MM `MMd MM YMMMMb `MMd +MM MM MM MM MMMMMMMM MM MM d'`MM. MM `Mb dMM. +YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. + 8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM. + YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_ + MM + MM + _MM_ + + Copyright (c) 2018, Kenneth Troldal Balslev + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + - Neither the name of the author nor the + names of any contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +// ===== External Includes ===== // +#include // std::find +#include // std::round, std::fabs +#include // std::ifstream +#include // std::ifstream +#include // std::stringstream +#include // std::stringstream + +// ===== OpenXLSX Includes ===== // +#include "XLImage.hpp" +#include "XLDocument.hpp" +#include "XLException.hpp" +#include "XLCellReference.hpp" +#include +#include + +using namespace OpenXLSX; + +namespace OpenXLSX +{ + //============================================================================= + // XLImage Implementation + //============================================================================= + + XLImage::XLImage(XLDocument* parentDoc, size_t imageDataHash, XLMimeType mimeType, + const std::string& extension, const std::string& filePackagePath, + uint32_t widthPixels, uint32_t heightPixels) + : m_parentDoc(parentDoc) + , m_imageDataHash(imageDataHash) + , m_mimeType(mimeType) + , m_extension(extension) + , m_filePackagePath(filePackagePath) + , m_widthPixels(widthPixels) + , m_heightPixels(heightPixels) + { + } + + std::vector XLImage::getImageData() const + { + if (!m_parentDoc || m_filePackagePath.empty()) { + return {}; + } + + try { + std::string dataStr = m_parentDoc->readFile(m_filePackagePath); + return std::vector(dataStr.begin(), dataStr.end()); + } catch (...) { + return {}; + } + } + + int XLImage::verifyData(std::string* dbgMsg) const + { + int issueCount = 0; + + if (!m_parentDoc) { + appendDbgMsg(dbgMsg, "XLImage: parentDoc is null"); + issueCount++; + return issueCount; + } + + if (m_filePackagePath.empty()) { + appendDbgMsg(dbgMsg, "XLImage: filePackagePath is empty"); + issueCount++; + return issueCount; + } + + // Get image data from archive + std::vector imageData = getImageData(); + if (imageData.empty()) { + appendDbgMsg(dbgMsg, "XLImage: image data is empty"); + issueCount++; + return issueCount; + } + + // Verify hash matches + size_t computedHash = XLImageUtils::imageDataHash(imageData); + if (computedHash != m_imageDataHash) { + appendDbgMsg(dbgMsg, "XLImage: hash mismatch - stored: " + std::to_string(m_imageDataHash) + + ", computed: " + std::to_string(computedHash)); + issueCount++; + } + + // Compute metadata from image data + XLMimeType computedMimeType = XLImageUtils::mimeTypeFromExtension(m_extension); + if (computedMimeType != m_mimeType) { + std::string storedMimeTypeStr = XLImageUtils::mimeTypeToString(m_mimeType); + std::string computedMimeTypeStr = XLImageUtils::mimeTypeToString(computedMimeType); + appendDbgMsg(dbgMsg, "XLImage: MIME type mismatch - stored: " + storedMimeTypeStr + + ", computed: " + computedMimeTypeStr); + issueCount++; + } + + // Get dimensions from image data + auto dimensions = XLImageUtils::getImageDimensions(imageData); + uint32_t computedWidth = dimensions.first; + uint32_t computedHeight = dimensions.second; + + if (computedWidth != m_widthPixels) { + appendDbgMsg(dbgMsg, "XLImage: width mismatch - stored: " + std::to_string(m_widthPixels) + + ", computed: " + std::to_string(computedWidth)); + issueCount++; + } + if (computedHeight != m_heightPixels) { + appendDbgMsg(dbgMsg, "XLImage: height mismatch - stored: " + std::to_string(m_heightPixels) + + ", computed: " + std::to_string(computedHeight)); + issueCount++; + } + + return issueCount; + } + + //============================================================================= + // XLImageManager Implementation + //============================================================================= + + XLImageManager::XLImageManager(XLDocument* parentDoc) + : m_parentDoc(parentDoc) + { + } + + void XLImageManager::prune() + { + auto it = m_images.begin(); + while (it != m_images.end()) { + if (!it->get() || it->use_count() <= 1) { + // Remove from archive if it has a file package path + if (it->get() && !it->get()->filePackagePath().empty()) { + try { + m_parentDoc->deleteEntry(it->get()->filePackagePath()); + } catch (...) { + // Ignore deletion errors + } + } + it = m_images.erase(it); + } else { + ++it; + } + } + } + + XLImageShPtr XLImageManager::findOrAddImage(const std::string& packagePath, + const std::vector& imageData, + XLContentType contentType) + { + // First, try to find existing image by data hash + XLImageShPtr existingImage = findImageByImageData(imageData); + if (existingImage) { + return existingImage; + } + + // Image doesn't exist, create new one + size_t imageDataHash = XLImageUtils::imageDataHash(imageData); + XLMimeType mimeType = XLImageUtils::contentTypeToMimeType(contentType); + std::string extension = XLImageUtils::extensionFromMimeType(mimeType); + + // Generate package filename + std::string packageImageFilename; + + if (!packagePath.empty()) { + // Use the provided package path (from XML when loading existing files) + packageImageFilename = packagePath; + } else { + // Generate unique package filename using efficient algorithm + packageImageFilename = generateUniquePackageFilename(extension); + } + + // Get image dimensions + auto dimensions = XLImageUtils::getImageDimensions(imageData); + uint32_t widthPixels = dimensions.first; + uint32_t heightPixels = dimensions.second; + + // Add image entry to document + if (!m_parentDoc->addImageEntry(packageImageFilename, imageData, contentType)) { + return nullptr; + } + + // Create new XLImage object + auto newImage = std::make_shared(m_parentDoc, imageDataHash, mimeType, extension, + packageImageFilename, widthPixels, heightPixels); + + // Add to collection + m_images.push_back(newImage); + + return newImage; + } + + XLImageShPtr XLImageManager::findImageByImageData(const std::vector& imageData) const + { + size_t targetHash = XLImageUtils::imageDataHash(imageData); + + for (const auto& image : m_images) { + if (image && image->imageDataHash() == targetHash) { + return image; + } + } + + return nullptr; + } + + + std::string XLImageManager::generateUniquePackageFilename(const std::string& extension) const + { + // Collect all existing package filenames and extract base names (without extension) + std::vector existingBaseNames; + for (const auto& image : m_images) { + if (image && !image->filePackagePath().empty()) { + std::string packagePath = image->filePackagePath(); + // Extract filename from "xl/media/image1.png" -> "image1.png" + size_t lastSlash = packagePath.find_last_of('/'); + if (lastSlash != std::string::npos) { + std::string filename = packagePath.substr(lastSlash + 1); + // Extract base name without extension: "image1.png" -> "image1" + size_t lastDot = filename.find_last_of('.'); + if (lastDot != std::string::npos) { + std::string baseName = filename.substr(0, lastDot); + existingBaseNames.push_back(baseName); + } + } + } + } + + // Sort the base names for efficient binary search + std::sort(existingBaseNames.begin(), existingBaseNames.end()); + + // Find the next available image number + int imageNumber = 1; + std::string candidateBaseName; + + do { + candidateBaseName = "image" + std::to_string(imageNumber); + imageNumber++; + } while (std::binary_search(existingBaseNames.begin(), existingBaseNames.end(), candidateBaseName)); + + // Return the unique package filename + return "xl/media/" + candidateBaseName + extension; + } + + + XLImageShPtr XLImageManager::findImageByFilePackagePath(const std::string& filePackagePath) const + { + for (const auto& image : m_images) { + if (image && image->filePackagePath() == filePackagePath) { + return image; + } + } + + return nullptr; + } + + size_t XLImageManager::getImageCount() const + { + return m_images.size(); + } + + std::vector::const_iterator XLImageManager::begin() const + { + return m_images.begin(); + } + + std::vector::const_iterator XLImageManager::end() const + { + return m_images.end(); + } + + + /** + * @brief Remove embedded image vector for worksheet + * @param sheetName The name of the worksheet + */ + void XLImageManager::eraseEmbImgsForSheetName(const std::string& sheetName) + { + m_sheetNmEmbImgVMap.erase(sheetName); + } + + /** + * @brief Find or create embedded image vector for worksheet + * @param sheetName The name of the worksheet + * @return Shared pointer to the embedded image vector + */ + XLEmbImgVecShPtr XLImageManager::getEmbImgsForSheetName(const std::string& sheetName) + { + auto itr = m_sheetNmEmbImgVMap.lower_bound(sheetName); + if ((itr != m_sheetNmEmbImgVMap.end()) && (sheetName == itr->first)) { + return itr->second; + } else { + auto result = std::make_shared(); + m_sheetNmEmbImgVMap.insert(itr, std::make_pair(sheetName, result)); + return result; + } + } + + /** + * @brief Find embedded image vector for worksheet, return null pointer if not found + * @param sheetName The name of the worksheet + * @return Shared pointer to the embedded image vector, or nullptr if not found + */ + XLEmbImgVecShPtr XLImageManager::getEmbImgsForSheetName(const std::string& sheetName) const + { + auto itr = m_sheetNmEmbImgVMap.find(sheetName); + if (itr != m_sheetNmEmbImgVMap.end()) { + return itr->second; + } + return nullptr; + } + + int XLImageManager::verifyData(std::string* dbgMsg) const + { + int totalIssues = 0; + + // Check each image has same parentDoc + for (const auto& image : m_images) { + if (image && image->parentDoc() != m_parentDoc) { + appendDbgMsg(dbgMsg, "XLImageManager: image has different parentDoc"); + totalIssues++; + } + } + + + // Check each image has unique imageData hash + // TODO: allow for hash collision -- compare binary image data when hash values are same. + std::set imageDataHashes; + for (const auto& image : m_images) { + if (image) { + if (imageDataHashes.find(image->imageDataHash()) != imageDataHashes.end()) { + appendDbgMsg(dbgMsg, "XLImageManager: duplicate imageDataHash: " + std::to_string(image->imageDataHash())); + totalIssues++; + } else { + imageDataHashes.insert(image->imageDataHash()); + } + } + } + + // Call verifyData() for each non-null image + for (const auto& image : m_images) { + if (image) { + totalIssues += image->verifyData(dbgMsg); + } + } + + // Check package path uniqueness and format + std::set packagePaths; + for (const auto& image : m_images) { + if (image && !image->filePackagePath().empty()) { + std::string packagePath = image->filePackagePath(); + + // Check format + if (packagePath.find("xl/media/") != 0) { + appendDbgMsg(dbgMsg, "XLImageManager: package path '" + packagePath + "' does not start with 'xl/media/'"); + totalIssues++; + } + + // Check uniqueness + if (packagePaths.find(packagePath) != packagePaths.end()) { + appendDbgMsg(dbgMsg, "XLImageManager: duplicate package path: " + packagePath); + totalIssues++; + } else { + packagePaths.insert(packagePath); + } + } + } + + // Check that all images have valid parent document + for (const auto& image : m_images) { + if (image && !image->parentDoc()) { + appendDbgMsg(dbgMsg, "XLImageManager: image has null parentDoc"); + totalIssues++; + } + } + + // Check that all images have valid image data + for (const auto& image : m_images) { + if (image && image->getImageData().empty()) { + appendDbgMsg(dbgMsg, "XLImageManager: image has empty image data"); + totalIssues++; + } + } + + // Check consistency between m_images vector and archive + if (m_parentDoc) { + // Check that all images in m_images actually exist in the archive + for (const auto& image : m_images) { + if (image && !image->filePackagePath().empty()) { + std::string archiveContent = m_parentDoc->readFile(image->filePackagePath()); + if (archiveContent.empty()) { + appendDbgMsg(dbgMsg, "XLImageManager: image '" + image->filePackagePath() + "' not found in archive"); + totalIssues++; + } + } + } + + // Check for orphaned files in archive (files that exist in archive but not in m_images) + // This is more complex and would require iterating through all archive entries + // For now, we'll skip this check as it's expensive and the above check is more critical + } + + std::set uniqueEmbImgPtrs; + // Check that each ptr in m_sheetNmEmbImgVMap is unique + std::set uniquePtrs; + for (const auto& pair : m_sheetNmEmbImgVMap) { + if (pair.second) { + if (uniquePtrs.find(pair.second) != uniquePtrs.end()) { + appendDbgMsg(dbgMsg, "XLImageManager: duplicate XLEmbImgVecShPtr for worksheet: " + pair.first); + totalIssues++; + } else { + uniquePtrs.insert(pair.second); + } + } + } + + // Check that each ptr in m_sheetNmEmbImgVMap is non-NULL + for (const auto& pair : m_sheetNmEmbImgVMap) { + if (!pair.second) { + appendDbgMsg(dbgMsg, "XLImageManager: NULL XLEmbImgVecShPtr for worksheet: " + pair.first); + totalIssues++; + } + } + + // For each ptr in m_sheetNmEmbImgVMap, call ptr->verifyData() + for (const auto& pair : m_sheetNmEmbImgVMap) { + if (pair.second) { + for (const auto& embImage : *pair.second) { + std::string embImageDbgMsg; + int embImageIssues = embImage.verifyData(&embImageDbgMsg); + if (embImageIssues > 0) { + appendDbgMsg(dbgMsg, "XLImageManager: worksheet '" + pair.first + "' has embedded image issues: " + embImageDbgMsg); + totalIssues += embImageIssues; + } + } + } + } + + return totalIssues; + } + + void XLImageManager::printReferenceCounts(const std::string& title) const + { + std::cout << "\n=== " << title << " ===" << std::endl; + std::cout << "Total XLImageShPtr objects: " << m_images.size() << std::endl; + + for (size_t i = 0; i < m_images.size(); ++i) { + const auto& image = m_images[i]; + if (image) { + std::cout << " [" << i << "] refs=" << image.use_count() + << " package=" << image->filePackagePath() + << " mime=" << XLImageUtils::mimeTypeToString(image->mimeType()) + << " size=" << image->widthPixels() << "x" << image->heightPixels() + << " hash=" << image->imageDataHash() << std::endl; + } else { + std::cout << " [" << i << "] NULL pointer" << std::endl; + } + } + std::cout << "=== End " << title << " ===" << std::endl; + } + + + // ========== XLEmbeddedImage Member Functions ========== // + + /** + * @details Set the image ID + */ + void XLEmbeddedImage::setId(const std::string& imageId) + { + m_imageAnchor.imageId = imageId; + } + + /** + * @details Get the image data + */ + std::vector XLEmbeddedImage::data() const + { + static const std::vector empty; + if (!m_image) return empty; + return m_image->getImageData(); + } + + /** + * @details Get the MIME type + */ + /** + * @details Get the file extension + */ + const std::string& XLEmbeddedImage::extension() const + { + static std::string empty; + if (!m_image) return empty; + return m_image->extension(); + } + + /** + * @details Get the unique image ID + */ + const std::string& XLEmbeddedImage::id() const + { + return m_imageAnchor.imageId; + } + + /** + * @details Get the image width in pixels + */ + uint32_t XLEmbeddedImage::widthPixels() const + { + if (!m_image) return 0; + return m_image->widthPixels(); + } + + /** + * @details Get the image height in pixels + */ + uint32_t XLEmbeddedImage::heightPixels() const + { + if (!m_image) return 0; + return m_image->heightPixels(); + } + + /** + * @details Get the image MIME type + */ + XLMimeType XLEmbeddedImage::mimeType() const + { + if (!m_image) return XLMimeType::Unknown; + return m_image->mimeType(); + } + + /** + * @details Set the display width in Excel units (EMUs) + */ + void XLEmbeddedImage::setDisplayWidthEMUs(uint32_t width) + { + m_imageAnchor.displayWidthEMUs = width; + } + + /** + * @details Set the display height in Excel units (EMUs) + */ + void XLEmbeddedImage::setDisplayHeightEMUs(uint32_t height) + { + m_imageAnchor.displayHeightEMUs = height; + } + + /** + * @details Get the display width in Excel units (EMUs) + */ + uint32_t XLEmbeddedImage::displayWidthEMUs() const + { + return m_imageAnchor.displayWidthEMUs; + } + + /** + * @details Get the display height in Excel units (EMUs) + */ + uint32_t XLEmbeddedImage::displayHeightEMUs() const + { + return m_imageAnchor.displayHeightEMUs; + } + + /** + * @details Check if the image is valid + */ + bool XLEmbeddedImage::isValid() const + { + return m_image && !m_imageAnchor.imageId.empty(); + } + + /** + * @details Get the content type for this image + */ + XLContentType XLEmbeddedImage::contentType() const + { + if (!m_image) return XLContentType::Unknown; + return XLImageUtils::mimeTypeToContentType(m_image->mimeType()); + } + + // ========== XLEmbeddedImage Private Member Functions ========== // + + /** + * @details Generate a unique image ID + */ + std::string XLEmbeddedImage::generateId(uint32_t imageNumber) + { + return "img" + std::to_string(imageNumber); + } + + /** + * @details Set display size in pixels (converts to EMUs automatically) + */ + void XLEmbeddedImage::setDisplaySizePixels(uint32_t widthPixels, uint32_t heightPixels) + { + m_imageAnchor.displayWidthEMUs = XLImageUtils::pixelsToEmus(widthPixels); + m_imageAnchor.displayHeightEMUs = XLImageUtils::pixelsToEmus(heightPixels); + } + + /** + * @details Compare two images for debugging + */ + int XLEmbeddedImage::compare(const XLEmbeddedImage& other, std::string* diffMsg) const + { + + // Compare image data (most important for embedded images) + if (m_image != other.m_image) { + auto thisData = m_image ? m_image->getImageData() : std::vector(); + auto otherData = other.m_image ? other.m_image->getImageData() : std::vector(); + if (thisData != otherData) { + appendDbgMsg(diffMsg, "image data differs (size: " + std::to_string(thisData.size()) + + " vs " + std::to_string(otherData.size()) + " bytes)"); + return thisData.size() < otherData.size() ? -1 : 1; + } + } + + // Compare MIME type + const XLMimeType thisMimeType = mimeType(); + const XLMimeType otherMimeType = other.mimeType(); + int mimeCompare = static_cast(thisMimeType) - static_cast(otherMimeType); + if (mimeCompare != 0) { + std::string thisMimeTypeStr = m_image ? XLImageUtils::mimeTypeToString(m_image->mimeType()) : ""; + std::string otherMimeTypeStr = other.m_image ? XLImageUtils::mimeTypeToString(other.m_image->mimeType()) : ""; + appendDbgMsg(diffMsg, "MIME type differs: '" + thisMimeTypeStr + "' vs '" + otherMimeTypeStr + "'"); + return mimeCompare; + } + + // Compare extension + std::string thisExtension = m_image ? m_image->extension() : ""; + std::string otherExtension = other.m_image ? other.m_image->extension() : ""; + int extCompare = thisExtension.compare(otherExtension); + if (extCompare != 0) { + appendDbgMsg(diffMsg, "extension differs: '" + thisExtension + "' vs '" + otherExtension + "'"); + return extCompare; + } + + // Compare ID + int idCompare = m_imageAnchor.imageId.compare(other.m_imageAnchor.imageId); + if (idCompare != 0) { + appendDbgMsg(diffMsg, "image ID differs: '" + m_imageAnchor.imageId + "' vs '" + other.m_imageAnchor.imageId + "'"); + return idCompare; + } + + // Compare dimensions + uint32_t thisWidth = m_image ? m_image->widthPixels() : 0; + uint32_t otherWidth = other.m_image ? other.m_image->widthPixels() : 0; + if (thisWidth != otherWidth) { + appendDbgMsg(diffMsg, "width differs: " + std::to_string(thisWidth) + + " vs " + std::to_string(otherWidth) + " pixels"); + return thisWidth < otherWidth ? -1 : 1; + } + + uint32_t thisHeight = m_image ? m_image->heightPixels() : 0; + uint32_t otherHeight = other.m_image ? other.m_image->heightPixels() : 0; + if (thisHeight != otherHeight) { + appendDbgMsg(diffMsg, "height differs: " + std::to_string(thisHeight) + + " vs " + std::to_string(otherHeight) + " pixels"); + return thisHeight < otherHeight ? -1 : 1; + } + + // Compare display dimensions + if (m_imageAnchor.displayWidthEMUs != other.m_imageAnchor.displayWidthEMUs) { + appendDbgMsg(diffMsg, "display width differs: " + std::to_string(m_imageAnchor.displayWidthEMUs) + + " vs " + std::to_string(other.m_imageAnchor.displayWidthEMUs) + " EMUs"); + return m_imageAnchor.displayWidthEMUs < other.m_imageAnchor.displayWidthEMUs ? -1 : 1; + } + + if (m_imageAnchor.displayHeightEMUs != other.m_imageAnchor.displayHeightEMUs) { + appendDbgMsg(diffMsg, "display height differs: " + std::to_string(m_imageAnchor.displayHeightEMUs) + + " vs " + std::to_string(other.m_imageAnchor.displayHeightEMUs) + " EMUs"); + return m_imageAnchor.displayHeightEMUs < other.m_imageAnchor.displayHeightEMUs ? -1 : 1; + } + + // Compare registry/relationship fields + int relIdCompare = m_imageAnchor.relationshipId.compare(other.m_imageAnchor.relationshipId); + if (relIdCompare != 0) { + appendDbgMsg(diffMsg, "relationship ID differs: '" + m_imageAnchor.relationshipId + "' vs '" + other.m_imageAnchor.relationshipId + "'"); + return relIdCompare; + } + + int cellCompare = anchorCell().compare(other.anchorCell()); + if (cellCompare != 0) { + appendDbgMsg(diffMsg, "anchor cell differs: '" + anchorCell() + "' vs '" + other.anchorCell() + "'"); + return cellCompare; + } + + int typeCompare = static_cast(m_imageAnchor.anchorType) - static_cast(other.m_imageAnchor.anchorType); + if (typeCompare != 0) { + std::string thisAnchorTypeStr = XLImageAnchorUtils::anchorTypeToString(m_imageAnchor.anchorType); + std::string otherAnchorTypeStr = XLImageAnchorUtils::anchorTypeToString(other.m_imageAnchor.anchorType); + appendDbgMsg(diffMsg, "anchor type differs: '" + thisAnchorTypeStr + "' vs '" + otherAnchorTypeStr + "'"); + return typeCompare; + } + + // Images are identical + return 0; + } + + /** + * @details Verify internal data integrity and class invariants + */ + int XLEmbeddedImage::verifyData(std::string* dbgMsg) const + { + int errorCount = 0; + + // Check basic data integrity + if (!m_image || m_image->getImageData().empty()) { + appendDbgMsg(dbgMsg, "image data is empty"); + errorCount++; + } + + // MIME type + const XLMimeType thisMimeType = mimeType(); + const std::string mimeTypeStr = XLImageUtils::mimeTypeToString(thisMimeType); + + // Check extension consistency + std::string extension = m_image ? m_image->extension() : ""; + if (extension.empty()) { + appendDbgMsg(dbgMsg, "file extension is empty"); + errorCount++; + } + + // Check ID consistency + if (m_imageAnchor.imageId.empty()) { + appendDbgMsg(dbgMsg, "image ID is empty"); + errorCount++; + } + + // Check relationship ID consistency + if (m_imageAnchor.relationshipId.empty()) { + appendDbgMsg(dbgMsg, "relationship ID is empty"); + errorCount++; + } + + // Check anchor type consistency + if (m_imageAnchor.anchorType == XLImageAnchorType::Unknown) { + appendDbgMsg(dbgMsg, "anchor type is unknown"); + errorCount++; + } + + // Check dimension consistency + uint32_t widthPixels = m_image ? m_image->widthPixels() : 0; + uint32_t heightPixels = m_image ? m_image->heightPixels() : 0; + if (widthPixels == 0) { + appendDbgMsg(dbgMsg, "width in pixels is zero"); + errorCount++; + } + + if (heightPixels == 0) { + appendDbgMsg(dbgMsg, "height in pixels is zero"); + errorCount++; + } + + // Check display dimension consistency + if (m_imageAnchor.displayWidthEMUs == 0) { + appendDbgMsg(dbgMsg, "display width in EMUs is zero"); + errorCount++; + } + + if (m_imageAnchor.displayHeightEMUs == 0) { + appendDbgMsg(dbgMsg, "display height in EMUs is zero"); + errorCount++; + } + + // Check MIME type and extension consistency + if ( (thisMimeType != XLMimeType::Unknown) && !extension.empty()) { + if (( XLMimeType::PNG == thisMimeType && extension != ".png") || + ( XLMimeType::JPEG == thisMimeType && extension != ".jpg" && extension != ".jpeg") || + ( XLMimeType::GIF == thisMimeType && extension != ".gif") || + ( XLMimeType::BMP == thisMimeType && extension != ".bmp")) { + appendDbgMsg(dbgMsg, "MIME type '" + mimeTypeStr + + "' does not match extension '" + extension + "'"); + errorCount++; + } + } + + // Check package path consistency + if (m_image && !m_image->filePackagePath().empty()) { + std::string packagePath = m_image->filePackagePath(); + if (packagePath.find("xl/media/") != 0) { + appendDbgMsg(dbgMsg, "package path '" + packagePath + "' does not start with 'xl/media/'"); + errorCount++; + } + } + + return errorCount; + } + + // Registry/relationship setter functions (for XLImageInfo compatibility) + /** + * @details Set the relationship ID + */ + void XLEmbeddedImage::setRelationshipId(const std::string& relationshipId) + { + m_imageAnchor.relationshipId = relationshipId; + } + + /** + * @details Set the anchor cell reference + */ + void XLEmbeddedImage::setAnchorCell(const std::string& anchorCell) + { + auto [row, col] = cellRefToRowCol(anchorCell); + m_imageAnchor.fromRow = row; + m_imageAnchor.fromCol = col; + } + + /** + * @details Set the anchor type + */ + void XLEmbeddedImage::setAnchorType(XLImageAnchorType anchorType) + { + m_imageAnchor.anchorType = anchorType; + } + + // Registry/relationship getter functions (for XLImageInfo compatibility) + /** + * @details Get the relationship ID + */ + const std::string& XLEmbeddedImage::relationshipId() const + { + return m_imageAnchor.relationshipId; + } + + /** + * @details Get the anchor cell reference + */ + std::string XLEmbeddedImage::anchorCell() const + { + return rowColToCellRef(m_imageAnchor.fromRow, m_imageAnchor.fromCol); + } + + /** + * @details Get the anchor type + */ + const XLImageAnchorType& XLEmbeddedImage::anchorType() const + { + return m_imageAnchor.anchorType; + } + + /** + * @details Check if this image is empty (default constructed) + */ + bool XLEmbeddedImage::isEmpty() const + { + return m_imageAnchor.imageId.empty(); + } + + /** + * @details Get the shared pointer to the image data + */ + XLImageShPtr XLEmbeddedImage::getImage() const + { + return m_image; + } + + /** + * @details Set the shared pointer to the image data + */ + void XLEmbeddedImage::setImage(XLImageShPtr image) + { + m_image = image; + } + + /* TODO: add unit test */ + std::string XLEmbeddedImage::rowColToCellRef(uint32_t row, uint16_t col) + { + std::string result; + + // Convert column to letters (A, B, C, ..., Z, AA, AB, ...) + ++col; + while (col > 0) { + col--; // Convert to 0-based first + result = static_cast('A' + (col % 26)) + result; + col = col / 26; + } + + // Add row number (1-based) + result += std::to_string(row + 1); + + return result; + } + + /* TODO: add unit test */ + std::pair XLEmbeddedImage::cellRefToRowCol(const std::string& cellRef) + { + uint32_t row = 0; + uint16_t col = 0; + + // Parse the cell reference (e.g., "A1", "B5", "AA10") + size_t i = 0; + while (i < cellRef.length() && std::isalpha(cellRef[i])) { + col = col * 26 + (cellRef[i] - 'A' + 1); + i++; + } + col--; // Convert to 0-based + + if (i < cellRef.length()) { + row = std::stoul(cellRef.substr(i)) - 1; // Convert to 0-based + } + + return {row, col}; + } + + /** + * @brief Robust XML element name matching that handles namespace prefixes + * @param elementName The actual element name (may include namespace prefix) + * @param targetName The target element name to match + * @return True if the element name matches the target (with or without namespace) + * + * @example + * Exact match: "oneCellAnchor" matches "oneCellAnchor" + * Namespace prefix: "xdr:oneCellAnchor" matches "oneCellAnchor" + * Different namespace: "a:oneCellAnchor" matches "oneCellAnchor" + */ + bool XLImageUtils::matchesElementName(const std::string& elementName, const std::string& targetName) + { + // Exact match + if (elementName == targetName) { + return true; + } + + // Check if element name ends with target name (handles namespace prefixes) + if (elementName.length() > targetName.length() + 1) { + size_t pos = elementName.find_last_of(':'); + if (pos != std::string::npos && pos + 1 < elementName.length()) { + std::string localName = elementName.substr(pos + 1); + if (localName == targetName) { + return true; + } + } + } + + // Check if element name starts with target name followed by colon + if (elementName.length() == targetName.length() + 1 && elementName[targetName.length()] == ':') { + if (elementName.substr(0, targetName.length()) == targetName) { + return true; + } + } + + return false; + } + +} // namespace OpenXLSX diff --git a/OpenXLSX/sources/XLImageUtils.cpp b/OpenXLSX/sources/XLImageUtils.cpp new file mode 100644 index 00000000..114d5b31 --- /dev/null +++ b/OpenXLSX/sources/XLImageUtils.cpp @@ -0,0 +1,779 @@ +/* + + ____ ____ ___ ____ ____ ____ ___ + 6MMMMb `MM( )M' `MM' 6MMMMb\`MM( )M' + 8P Y8 `MM. d' MM 6M' ` `MM. d' +6M Mb __ ____ ____ ___ __ `MM. d' MM MM `MM. d' +MM MM `M6MMMMb 6MMMMb `MM 6MMb `MM. d' MM YM. `MM. d' +MM MM MM MM MM MM MM' MM `MMd MM YMMMMb `MMd +MM MM MM MM MMMMMMMM MM MM d'`MM. MM `Mb dMM. +YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. + 8b d8 MM. ,M9 YM d9 MM MM d' `MM. MM / L ,M9 d' `MM. + YMMMM9 MMYMMM9 YMMMM9 _MM_ _MM_M(_ _)MM_ _MMMMMMM MYMMMM9 _M(_ _)MM_ + MM + MM + _MM_ + + Copyright (c) 2018, Kenneth Troldal Balslev + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + - Neither the name of the author nor the + names of any contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +// ===== External Includes ===== // +#include +#include + +// ===== OpenXLSX Includes ===== // +#include "XLImage.hpp" + +using namespace OpenXLSX; + +// ========== XLImageUtils Static Data ========== // + +// These contain the binary data from the tiny image files in the images directory + +// tiny_png.png - Small PNG image +const std::vector XLImageUtils::png31x15Data = { + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x0f, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x93, 0x1a, 0x83, 0xf5, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, + 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, + 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, + 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, + 0x00, 0x01, 0xa8, 0x49, 0x44, 0x41, 0x54, 0x38, 0x4f, 0x63, 0x7c, 0xff, + 0x64, 0x03, 0x03, 0xcd, 0x00, 0x13, 0x94, 0xa6, 0x0d, 0x00, 0xbb, 0xfd, + 0xf1, 0xfe, 0x7f, 0xa1, 0x5b, 0xa1, 0x02, 0x50, 0x20, 0xc5, 0xb8, 0xba, + 0x88, 0x51, 0x96, 0x81, 0xe1, 0xfb, 0x85, 0x98, 0x6b, 0x67, 0x18, 0xa4, + 0x42, 0x96, 0x48, 0x0a, 0x40, 0x65, 0x40, 0x22, 0xef, 0xd3, 0x8c, 0x1d, + 0xed, 0xa0, 0x7c, 0xce, 0x89, 0x29, 0xc2, 0xfb, 0xa1, 0x6c, 0x06, 0xc5, + 0xe0, 0x17, 0xfd, 0x9e, 0x7f, 0xa0, 0x1c, 0x06, 0x06, 0x16, 0x28, 0xcd, + 0x60, 0xcc, 0x78, 0x22, 0x92, 0x11, 0xca, 0x66, 0x60, 0x38, 0xb2, 0xfc, + 0x5f, 0x68, 0x1f, 0x03, 0xd0, 0x02, 0x11, 0x10, 0xef, 0xfa, 0xb3, 0xf3, + 0x87, 0x24, 0xe1, 0xc6, 0x21, 0x81, 0x17, 0x7c, 0x85, 0x35, 0x7c, 0x0a, + 0xd9, 0x4f, 0x36, 0x18, 0x42, 0xf8, 0x40, 0x9b, 0x24, 0x0a, 0x19, 0x10, + 0x16, 0xe0, 0x08, 0x19, 0x1b, 0x17, 0x46, 0x8d, 0x67, 0x0c, 0x0f, 0x21, + 0x1c, 0xe5, 0x7c, 0xa9, 0xf7, 0xb3, 0x9e, 0x7f, 0x80, 0x70, 0x90, 0x00, + 0xcb, 0xba, 0x99, 0x7c, 0x0c, 0xc1, 0x2f, 0xf2, 0xa1, 0x46, 0x03, 0xc1, + 0xf7, 0xfc, 0x96, 0x4f, 0x0c, 0x6b, 0xf9, 0x4e, 0x41, 0xb9, 0xc4, 0x85, + 0xbb, 0x82, 0xa4, 0x93, 0xdb, 0xfb, 0xf3, 0x87, 0xa0, 0x3c, 0x18, 0x78, + 0xc1, 0x75, 0xf8, 0xf1, 0xb7, 0x48, 0xa4, 0x70, 0x00, 0x01, 0x89, 0x4f, + 0xfd, 0x73, 0xde, 0x99, 0x41, 0x39, 0xb8, 0x4c, 0x3f, 0xb2, 0xe7, 0xff, + 0x0d, 0x63, 0x06, 0x1b, 0x28, 0x8f, 0x81, 0x41, 0x20, 0x4e, 0x8a, 0xa1, + 0xf0, 0x1e, 0xd4, 0x2f, 0x50, 0xf0, 0x9c, 0xe5, 0xbe, 0xec, 0x1f, 0x19, + 0x28, 0x07, 0x3b, 0x80, 0x87, 0xfb, 0xd9, 0xff, 0x16, 0x67, 0xff, 0x43, + 0xd9, 0x40, 0x00, 0x8e, 0x55, 0x28, 0x1b, 0x0c, 0x04, 0x0c, 0xf3, 0x9f, + 0xed, 0x5b, 0xf4, 0x5d, 0x3e, 0x0e, 0xca, 0x47, 0x07, 0xa7, 0xe6, 0xc8, + 0xb4, 0x9d, 0x80, 0x30, 0xff, 0xc4, 0xb5, 0xbc, 0x08, 0x92, 0x00, 0xb1, + 0x70, 0xc4, 0x2a, 0x16, 0x20, 0x10, 0xa7, 0xa8, 0x14, 0xf3, 0xfc, 0x61, + 0x9c, 0x24, 0x94, 0xcf, 0x20, 0xf9, 0x47, 0xf1, 0x31, 0xcb, 0x13, 0xa0, + 0x43, 0xc0, 0x3c, 0xb3, 0x94, 0x27, 0x1b, 0x52, 0x80, 0x34, 0x30, 0x62, + 0xf9, 0xc1, 0x02, 0x20, 0x40, 0x4a, 0x7a, 0xe7, 0x34, 0x48, 0x63, 0xd8, + 0xdd, 0x00, 0x8f, 0x5d, 0x89, 0x6f, 0xb6, 0xb2, 0x5c, 0xcb, 0xb7, 0xc3, + 0xdd, 0x87, 0x05, 0x90, 0x96, 0x9b, 0xec, 0x24, 0x4d, 0xee, 0x3c, 0x3b, + 0x73, 0x1d, 0xca, 0xfb, 0x13, 0x94, 0x0e, 0x4c, 0x21, 0x12, 0x85, 0x08, + 0x0b, 0x58, 0xd6, 0x35, 0x22, 0xd2, 0x3e, 0x10, 0xe0, 0xb3, 0x19, 0x0b, + 0xe0, 0x34, 0x68, 0x95, 0xba, 0x17, 0xf4, 0x0c, 0xca, 0x03, 0xa7, 0x90, + 0x6f, 0xeb, 0x1a, 0x25, 0x02, 0xd6, 0x42, 0x05, 0x18, 0x2c, 0xde, 0x6e, + 0xa8, 0xff, 0x0e, 0x65, 0x43, 0xf3, 0x2a, 0xcd, 0x00, 0x69, 0x21, 0x43, + 0x2a, 0xa0, 0xa5, 0xe9, 0x0c, 0x0c, 0x00, 0xe4, 0x3f, 0x87, 0xe1, 0xb6, + 0x42, 0x8a, 0xcd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, + 0x42, 0x60, 0x82 + }; + + // tiny_jpeg.jpg - Small JPEG image (actual file data) + const std::vector XLImageUtils::jpeg32x18Data = { + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, + 0x01, 0x01, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0xff, 0xe1, 0x00, 0x22, + 0x45, 0x78, 0x69, 0x66, 0x00, 0x00, 0x4d, 0x4d, 0x00, 0x2a, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x01, 0x01, 0x12, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, + 0x00, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, + 0x02, 0x02, 0x02, 0x03, 0x05, 0x03, 0x03, 0x03, 0x03, 0x03, 0x06, 0x04, + 0x04, 0x03, 0x05, 0x07, 0x06, 0x07, 0x07, 0x07, 0x06, 0x07, 0x07, 0x08, + 0x09, 0x0b, 0x09, 0x08, 0x08, 0x0a, 0x08, 0x07, 0x07, 0x0a, 0x0d, 0x0a, + 0x0a, 0x0b, 0x0c, 0x0c, 0x0c, 0x0c, 0x07, 0x09, 0x0e, 0x0f, 0x0d, 0x0c, + 0x0e, 0x0b, 0x0c, 0x0c, 0x0c, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x02, 0x02, + 0x02, 0x03, 0x03, 0x03, 0x06, 0x03, 0x03, 0x06, 0x0c, 0x08, 0x07, 0x08, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, + 0x0c, 0x0c, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x12, 0x00, 0x20, 0x03, + 0x01, 0x22, 0x00, 0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, + 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, + 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, + 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, + 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, + 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, + 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, + 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, + 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, + 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, + 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, + 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, + 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, + 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, + 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, + 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, + 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, + 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05, 0x04, 0x04, 0x00, + 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, + 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, + 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, + 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, + 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, + 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, + 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, + 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, + 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, + 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00, 0x3f, 0x00, 0xfd, + 0x5e, 0xf8, 0xcf, 0xf1, 0x17, 0xfe, 0x15, 0x17, 0xc2, 0x5f, 0x12, 0x78, + 0xa7, 0xec, 0x7f, 0xda, 0x1f, 0xf0, 0x8f, 0xe9, 0xd3, 0xea, 0x1f, 0x65, + 0xf3, 0x7c, 0x9f, 0xb4, 0x79, 0x68, 0x5b, 0x66, 0xfd, 0xad, 0xb7, 0x38, + 0xc6, 0x70, 0x71, 0xe8, 0x6b, 0x26, 0x4f, 0x88, 0xfe, 0x29, 0xd6, 0x3c, + 0x59, 0xe2, 0x0d, 0x3f, 0x43, 0xf0, 0xee, 0x83, 0x79, 0x6d, 0xa0, 0x5c, + 0xa5, 0xab, 0xcd, 0x7b, 0xae, 0x4b, 0x6b, 0x24, 0xce, 0xd0, 0x47, 0x37, + 0x08, 0xb6, 0xb2, 0x00, 0x31, 0x20, 0x1c, 0xbf, 0x51, 0xda, 0xb9, 0xdf, + 0xda, 0x7b, 0x4d, 0xf1, 0xaf, 0x8e, 0xbc, 0x3d, 0xe2, 0x3f, 0x06, 0xe9, + 0x3e, 0x1b, 0x5d, 0x4f, 0x45, 0xf1, 0x76, 0x86, 0x34, 0xdb, 0x6d, 0x4e, + 0x2b, 0xa8, 0x22, 0xfe, 0xc9, 0xba, 0x95, 0xe5, 0x8e, 0x79, 0x2e, 0x84, + 0x92, 0xab, 0xb4, 0x2b, 0x13, 0x42, 0xeb, 0xe4, 0x47, 0x23, 0xe5, 0x25, + 0x04, 0x72, 0x95, 0xa9, 0xa6, 0xfc, 0x0f, 0x8b, 0x5b, 0xf1, 0xe7, 0x8c, + 0x35, 0x1d, 0x5c, 0xeb, 0xd6, 0xd0, 0xea, 0x7a, 0x84, 0x52, 0x5a, 0x7d, + 0x8b, 0x5f, 0xbb, 0xb2, 0x8e, 0x78, 0x85, 0xac, 0x08, 0x49, 0x8e, 0xde, + 0x65, 0x5c, 0xef, 0x57, 0x19, 0x61, 0xb8, 0x81, 0xe9, 0x8a, 0xfc, 0x3f, + 0x56, 0xfe, 0x4f, 0xef, 0xba, 0xff, 0x00, 0x83, 0xf9, 0x9f, 0x53, 0xc4, + 0x58, 0x4a, 0xd4, 0xb0, 0x18, 0x47, 0x82, 0x92, 0xf6, 0xb5, 0x2a, 0x4f, + 0x9f, 0x96, 0x49, 0xb5, 0x4f, 0xd9, 0xa7, 0x1e, 0x6f, 0x76, 0x7c, 0x9e, + 0xfd, 0xd6, 0xb1, 0x4d, 0xbd, 0x2f, 0x6b, 0x15, 0x57, 0xf6, 0x8a, 0xbf, + 0xf1, 0x8c, 0xda, 0x3e, 0x9f, 0xe0, 0xdf, 0x0d, 0xc5, 0xab, 0xeb, 0xba, + 0x86, 0x9c, 0xba, 0xb5, 0xe4, 0x1a, 0x9e, 0xa3, 0xfd, 0x9f, 0x69, 0xa4, + 0xdb, 0x33, 0xbc, 0x6b, 0xe7, 0x4f, 0x1c, 0x53, 0x93, 0x23, 0xc9, 0x1c, + 0x8a, 0x88, 0x91, 0xb0, 0x61, 0x14, 0x8c, 0x59, 0x40, 0x1b, 0xba, 0xff, + 0x00, 0x86, 0x3e, 0x34, 0xd5, 0x3c, 0x61, 0xa5, 0xde, 0xae, 0xb7, 0xe1, + 0xfb, 0x8f, 0x0e, 0xea, 0xda, 0x5d, 0xe3, 0xd9, 0xdc, 0xdb, 0x99, 0x0c, + 0xf6, 0xd3, 0x10, 0x15, 0x96, 0x6b, 0x69, 0xca, 0x27, 0x9d, 0x0b, 0xab, + 0xa9, 0x0d, 0xb1, 0x48, 0x3b, 0x95, 0x95, 0x59, 0x58, 0x0e, 0x23, 0xc4, + 0x7e, 0x00, 0xd5, 0x3e, 0x14, 0x7c, 0x46, 0x9b, 0x5c, 0xf0, 0xcf, 0x86, + 0xae, 0x35, 0xff, 0x00, 0x0f, 0xeb, 0x1a, 0x1d, 0xb6, 0x83, 0x7d, 0xa4, + 0xe9, 0x57, 0x70, 0xd9, 0x5f, 0xd8, 0x0b, 0x66, 0x9d, 0xa0, 0x96, 0xdd, + 0xa6, 0x92, 0x28, 0xca, 0x15, 0xb8, 0x74, 0x71, 0xe6, 0xa3, 0xa9, 0x58, + 0xd9, 0x77, 0x1d, 0xc0, 0x5a, 0xfd, 0x98, 0xfc, 0x11, 0xe2, 0x0f, 0x09, + 0x5b, 0x78, 0x9a, 0xe3, 0x5a, 0x87, 0x5c, 0xd3, 0xb4, 0xfd, 0x5b, 0x52, + 0x59, 0xf4, 0x7d, 0x2b, 0x58, 0xf1, 0x0c, 0xda, 0xe5, 0xf6, 0x99, 0x6c, + 0xb0, 0xc7, 0x11, 0x59, 0x66, 0x92, 0x49, 0x42, 0xb3, 0xc8, 0x8f, 0x2e, + 0xc8, 0xe5, 0x91, 0x54, 0x48, 0x06, 0xec, 0x82, 0x07, 0xd7, 0x63, 0x30, + 0x79, 0x7f, 0xf6, 0x73, 0xab, 0x47, 0x92, 0xe9, 0x41, 0xa7, 0xcd, 0xef, + 0xca, 0x4d, 0xfb, 0xf1, 0x71, 0xf6, 0x8e, 0xdc, 0xad, 0xbb, 0x7e, 0xee, + 0xce, 0x31, 0x4f, 0x9e, 0xef, 0xde, 0xf2, 0xf0, 0xaf, 0x13, 0x1e, 0x48, + 0x57, 0x77, 0x95, 0x92, 0x93, 0x4b, 0x46, 0xf9, 0x55, 0xda, 0x76, 0x5b, + 0xca, 0xfd, 0xad, 0xb5, 0x8f, 0x52, 0xa2, 0x8a, 0x2b, 0xe4, 0x4f, 0x4c, + 0x28, 0xa2, 0x8a, 0x00, 0xff, 0xd9 + }; + + // tiny_bmp.bmp - Small BMP image (actual file data) + const std::vector XLImageUtils::bmp33x16Data = { + 0x42, 0x4d, 0x76, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, + 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x10, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x06, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, 0xb1, 0x22, 0xb0, 0xdb, 0x91, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x3e, 0x89, + 0xef, 0x6f, 0x44, 0xed, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x4c, 0xb1, + 0x22, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0x91, 0xe4, 0xef, 0x5e, + 0xb1, 0x6f, 0x80, 0xd4, 0x91, 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, 0xef, 0x24, 0x1c, 0xee, + 0xb0, 0xa8, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0x80, 0xdb, 0xef, 0x80, 0xba, 0x4b, 0x91, 0xe4, 0xd0, 0x6f, 0xb1, 0x6f, + 0xb0, 0xe4, 0xb1, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, + 0xef, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0x5e, 0xcb, 0xd0, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, + 0xef, 0x4c, 0xc2, 0xb1, 0xb0, 0xd4, 0x6f, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, + 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, + 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, + 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x9b, 0x89, 0xed, 0xb0, 0xe4, 0xef, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xa0, 0xe4, 0xb1, 0x5e, 0xba, 0x91, 0xb0, + 0xdb, 0x91, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x91, 0xc2, 0x22, 0x80, + 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc2, 0x48, 0x3f, 0xb0, 0xe4, 0xb9, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x85, 0x67, 0xed, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x6f, 0xb1, 0x4b, 0x80, 0xdb, 0xb1, + 0x80, 0xba, 0x4b, 0xb0, 0xe4, 0xd0, 0xb0, 0xe4, 0xef, 0x91, 0xe4, 0xef, + 0x6f, 0xb1, 0x6f, 0x80, 0xdb, 0xb1, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, 0xef, 0xcc, 0x65, 0x9d, + 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, + 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x85, 0xe4, + 0xef, 0x3e, 0x1c, 0xee, 0xb0, 0xc7, 0xee, 0x80, 0xdb, 0xef, 0x6f, 0xb1, + 0x4b, 0x5e, 0xcb, 0x91, 0xa0, 0xcb, 0x4b, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0x4c, 0xc2, 0xb1, 0x80, 0xcb, 0x6f, 0x6f, 0xb1, + 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, 0x48, 0x60, 0xb0, 0xe4, + 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb4, 0xe4, + 0xef, 0xcc, 0x65, 0x9d, 0xb0, 0xb3, 0x80, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xc7, 0xef, 0x57, + 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0x57, 0xa8, 0xef, 0x57, 0x1c, 0xed, 0xb0, 0xe4, 0xee, 0x80, + 0xdb, 0xef, 0x5e, 0xb1, 0x4b, 0x4c, 0xba, 0x4b, 0xb0, 0xd4, 0x6f, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x6f, 0xd4, 0xef, 0x5e, + 0xba, 0x22, 0x6f, 0xb1, 0x4b, 0xb0, 0xe4, 0xb1, 0xbe, 0xcc, 0xef, 0xc2, + 0x48, 0x60, 0xb0, 0xe4, 0xb9, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xc2, 0xb3, 0xef, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x6f, 0xc7, 0xef, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, + 0x24, 0x1c, 0xed, 0x24, 0x1c, 0xed, 0x3e, 0x1c, 0xed, 0xb0, 0xc7, 0xee, + 0xb0, 0xe4, 0xef, 0x80, 0xdb, 0xef, 0x4c, 0xb1, 0x4b, 0x6f, 0xb1, 0x22, + 0xb0, 0xe4, 0xb1, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0x91, 0xe4, 0xef, 0x4c, 0xb1, 0x6f, 0x6f, 0xb1, 0x22, 0xb0, 0xe4, 0xb1, + 0xbe, 0xcc, 0xef, 0xcc, 0x48, 0x60, 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, + 0xcc, 0x48, 0x3f, 0xcc, 0x48, 0x3f, 0xc8, 0x48, 0x3f, 0xb0, 0xcc, 0x9d, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, + 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, + 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, + 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0xb0, 0xe4, 0xef, 0x00 + }; + + // tiny_gif.gif - Small GIF image (actual file data) + const std::vector XLImageUtils::gif26x16Data = { + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x1a, 0x00, 0x10, 0x00, 0x70, 0x00, + 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0xfc, 0x00, 0x2c, 0x00, 0x00, + 0x00, 0x00, 0x1a, 0x00, 0x10, 0x00, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x33, 0x00, 0x00, 0x66, 0x00, 0x00, 0x99, 0x00, 0x00, 0xcc, 0x00, 0x00, + 0xff, 0x00, 0x2b, 0x00, 0x00, 0x2b, 0x33, 0x00, 0x2b, 0x66, 0x00, 0x2b, + 0x99, 0x00, 0x2b, 0xcc, 0x00, 0x2b, 0xff, 0x00, 0x55, 0x00, 0x00, 0x55, + 0x33, 0x00, 0x55, 0x66, 0x00, 0x55, 0x99, 0x00, 0x55, 0xcc, 0x00, 0x55, + 0xff, 0x00, 0x80, 0x00, 0x00, 0x80, 0x33, 0x00, 0x80, 0x66, 0x00, 0x80, + 0x99, 0x00, 0x80, 0xcc, 0x00, 0x80, 0xff, 0x00, 0xaa, 0x00, 0x00, 0xaa, + 0x33, 0x00, 0xaa, 0x66, 0x00, 0xaa, 0x99, 0x00, 0xaa, 0xcc, 0x00, 0xaa, + 0xff, 0x00, 0xd5, 0x00, 0x00, 0xd5, 0x33, 0x00, 0xd5, 0x66, 0x00, 0xd5, + 0x99, 0x00, 0xd5, 0xcc, 0x00, 0xd5, 0xff, 0x00, 0xff, 0x00, 0x00, 0xff, + 0x33, 0x00, 0xff, 0x66, 0x00, 0xff, 0x99, 0x00, 0xff, 0xcc, 0x00, 0xff, + 0xff, 0x33, 0x00, 0x00, 0x33, 0x00, 0x33, 0x33, 0x00, 0x66, 0x33, 0x00, + 0x99, 0x33, 0x00, 0xcc, 0x33, 0x00, 0xff, 0x33, 0x2b, 0x00, 0x33, 0x2b, + 0x33, 0x33, 0x2b, 0x66, 0x33, 0x2b, 0x99, 0x33, 0x2b, 0xcc, 0x33, 0x2b, + 0xff, 0x33, 0x55, 0x00, 0x33, 0x55, 0x33, 0x33, 0x55, 0x66, 0x33, 0x55, + 0x99, 0x33, 0x55, 0xcc, 0x33, 0x55, 0xff, 0x33, 0x80, 0x00, 0x33, 0x80, + 0x33, 0x33, 0x80, 0x66, 0x33, 0x80, 0x99, 0x33, 0x80, 0xcc, 0x33, 0x80, + 0xff, 0x33, 0xaa, 0x00, 0x33, 0xaa, 0x33, 0x33, 0xaa, 0x66, 0x33, 0xaa, + 0x99, 0x33, 0xaa, 0xcc, 0x33, 0xaa, 0xff, 0x33, 0xd5, 0x00, 0x33, 0xd5, + 0x33, 0x33, 0xd5, 0x66, 0x33, 0xd5, 0x99, 0x33, 0xd5, 0xcc, 0x33, 0xd5, + 0xff, 0x33, 0xff, 0x00, 0x33, 0xff, 0x33, 0x33, 0xff, 0x66, 0x33, 0xff, + 0x99, 0x33, 0xff, 0xcc, 0x33, 0xff, 0xff, 0x66, 0x00, 0x00, 0x66, 0x00, + 0x33, 0x66, 0x00, 0x66, 0x66, 0x00, 0x99, 0x66, 0x00, 0xcc, 0x66, 0x00, + 0xff, 0x66, 0x2b, 0x00, 0x66, 0x2b, 0x33, 0x66, 0x2b, 0x66, 0x66, 0x2b, + 0x99, 0x66, 0x2b, 0xcc, 0x66, 0x2b, 0xff, 0x66, 0x55, 0x00, 0x66, 0x55, + 0x33, 0x66, 0x55, 0x66, 0x66, 0x55, 0x99, 0x66, 0x55, 0xcc, 0x66, 0x55, + 0xff, 0x66, 0x80, 0x00, 0x66, 0x80, 0x33, 0x66, 0x80, 0x66, 0x66, 0x80, + 0x99, 0x66, 0x80, 0xcc, 0x66, 0x80, 0xff, 0x66, 0xaa, 0x00, 0x66, 0xaa, + 0x33, 0x66, 0xaa, 0x66, 0x66, 0xaa, 0x99, 0x66, 0xaa, 0xcc, 0x66, 0xaa, + 0xff, 0x66, 0xd5, 0x00, 0x66, 0xd5, 0x33, 0x66, 0xd5, 0x66, 0x66, 0xd5, + 0x99, 0x66, 0xd5, 0xcc, 0x66, 0xd5, 0xff, 0x66, 0xff, 0x00, 0x66, 0xff, + 0x33, 0x66, 0xff, 0x66, 0x66, 0xff, 0x99, 0x66, 0xff, 0xcc, 0x66, 0xff, + 0xff, 0x99, 0x00, 0x00, 0x99, 0x00, 0x33, 0x99, 0x00, 0x66, 0x99, 0x00, + 0x99, 0x99, 0x00, 0xcc, 0x99, 0x00, 0xff, 0x99, 0x2b, 0x00, 0x99, 0x2b, + 0x33, 0x99, 0x2b, 0x66, 0x99, 0x2b, 0x99, 0x99, 0x2b, 0xcc, 0x99, 0x2b, + 0xff, 0x99, 0x55, 0x00, 0x99, 0x55, 0x33, 0x99, 0x55, 0x66, 0x99, 0x55, + 0x99, 0x99, 0x55, 0xcc, 0x99, 0x55, 0xff, 0x99, 0x80, 0x00, 0x99, 0x80, + 0x33, 0x99, 0x80, 0x66, 0x99, 0x80, 0x99, 0x99, 0x80, 0xcc, 0x99, 0x80, + 0xff, 0x99, 0xaa, 0x00, 0x99, 0xaa, 0x33, 0x99, 0xaa, 0x66, 0x99, 0xaa, + 0x99, 0x99, 0xaa, 0xcc, 0x99, 0xaa, 0xff, 0x99, 0xd5, 0x00, 0x99, 0xd5, + 0x33, 0x99, 0xd5, 0x66, 0x99, 0xd5, 0x99, 0x99, 0xd5, 0xcc, 0x99, 0xd5, + 0xff, 0x99, 0xff, 0x00, 0x99, 0xff, 0x33, 0x99, 0xff, 0x66, 0x99, 0xff, + 0x99, 0x99, 0xff, 0xcc, 0x99, 0xff, 0xff, 0xcc, 0x00, 0x00, 0xcc, 0x00, + 0x33, 0xcc, 0x00, 0x66, 0xcc, 0x00, 0x99, 0xcc, 0x00, 0xcc, 0xcc, 0x00, + 0xff, 0xcc, 0x2b, 0x00, 0xcc, 0x2b, 0x33, 0xcc, 0x2b, 0x66, 0xcc, 0x2b, + 0x99, 0xcc, 0x2b, 0xcc, 0xcc, 0x2b, 0xff, 0xcc, 0x55, 0x00, 0xcc, 0x55, + 0x33, 0xcc, 0x55, 0x66, 0xcc, 0x55, 0x99, 0xcc, 0x55, 0xcc, 0xcc, 0x55, + 0xff, 0xcc, 0x80, 0x00, 0xcc, 0x80, 0x33, 0xcc, 0x80, 0x66, 0xcc, 0x80, + 0x99, 0xcc, 0x80, 0xcc, 0xcc, 0x80, 0xff, 0xcc, 0xaa, 0x00, 0xcc, 0xaa, + 0x33, 0xcc, 0xaa, 0x66, 0xcc, 0xaa, 0x99, 0xcc, 0xaa, 0xcc, 0xcc, 0xaa, + 0xff, 0xcc, 0xd5, 0x00, 0xcc, 0xd5, 0x33, 0xcc, 0xd5, 0x66, 0xcc, 0xd5, + 0x99, 0xcc, 0xd5, 0xcc, 0xcc, 0xd5, 0xff, 0xcc, 0xff, 0x00, 0xcc, 0xff, + 0x33, 0xcc, 0xff, 0x66, 0xcc, 0xff, 0x99, 0xcc, 0xff, 0xcc, 0xcc, 0xff, + 0xff, 0xff, 0x00, 0x00, 0xff, 0x00, 0x33, 0xff, 0x00, 0x66, 0xff, 0x00, + 0x99, 0xff, 0x00, 0xcc, 0xff, 0x00, 0xff, 0xff, 0x2b, 0x00, 0xff, 0x2b, + 0x33, 0xff, 0x2b, 0x66, 0xff, 0x2b, 0x99, 0xff, 0x2b, 0xcc, 0xff, 0x2b, + 0xff, 0xff, 0x55, 0x00, 0xff, 0x55, 0x33, 0xff, 0x55, 0x66, 0xff, 0x55, + 0x99, 0xff, 0x55, 0xcc, 0xff, 0x55, 0xff, 0xff, 0x80, 0x00, 0xff, 0x80, + 0x33, 0xff, 0x80, 0x66, 0xff, 0x80, 0x99, 0xff, 0x80, 0xcc, 0xff, 0x80, + 0xff, 0xff, 0xaa, 0x00, 0xff, 0xaa, 0x33, 0xff, 0xaa, 0x66, 0xff, 0xaa, + 0x99, 0xff, 0xaa, 0xcc, 0xff, 0xaa, 0xff, 0xff, 0xd5, 0x00, 0xff, 0xd5, + 0x33, 0xff, 0xd5, 0x66, 0xff, 0xd5, 0x99, 0xff, 0xd5, 0xcc, 0xff, 0xd5, + 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x33, 0xff, 0xff, 0x66, 0xff, 0xff, + 0x99, 0xff, 0xff, 0xcc, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xff, 0x00, 0xe7, 0xe9, + 0x13, 0x48, 0x70, 0xa0, 0xc1, 0x82, 0x08, 0x0f, 0xea, 0xd3, 0x97, 0x8c, + 0x5e, 0x32, 0x86, 0x0e, 0x21, 0x3e, 0x6c, 0x38, 0x31, 0x22, 0xc5, 0x7c, + 0xca, 0x04, 0x26, 0xc3, 0x38, 0x8f, 0x23, 0xc6, 0x7c, 0x0d, 0x41, 0xd2, + 0x03, 0xc9, 0x50, 0xe3, 0x48, 0x88, 0x1d, 0x51, 0x4d, 0xc3, 0x96, 0xea, + 0x5c, 0x3e, 0x7a, 0xf2, 0x88, 0x98, 0x72, 0x47, 0x86, 0x86, 0x0e, 0x9b, + 0x3a, 0xde, 0x0c, 0xcc, 0x38, 0x0f, 0x9a, 0x3c, 0x54, 0xee, 0xf2, 0x41, + 0xcb, 0x37, 0xad, 0x17, 0x48, 0x37, 0x9f, 0xe6, 0xe9, 0x78, 0x36, 0xb2, + 0x69, 0xc8, 0x92, 0xdf, 0xce, 0x41, 0x9b, 0x37, 0x2f, 0x99, 0x3c, 0x55, + 0xfa, 0x62, 0x42, 0xa3, 0xf9, 0xc9, 0xe1, 0x4b, 0x93, 0x0f, 0xe5, 0x51, + 0x6b, 0xc8, 0x54, 0x9f, 0xb2, 0x7c, 0x54, 0x89, 0x80, 0x9a, 0x47, 0x06, + 0xd4, 0xd7, 0xa9, 0x4c, 0xbf, 0x0a, 0x03, 0xf7, 0x75, 0xe3, 0x3c, 0x65, + 0xfa, 0x90, 0xb9, 0x99, 0x59, 0x53, 0xc7, 0x4d, 0x20, 0xf3, 0xe8, 0x31, + 0x9d, 0xf7, 0x8c, 0x1d, 0xac, 0x88, 0xdf, 0x52, 0x65, 0x4b, 0x45, 0x15, + 0xa9, 0xd2, 0x67, 0x81, 0xd1, 0x42, 0xcc, 0x37, 0xf0, 0x2a, 0xe5, 0x8d, + 0x59, 0xa7, 0x29, 0xd3, 0xca, 0x15, 0x62, 0x5c, 0x8a, 0x0c, 0xa3, 0x26, + 0x0b, 0xbc, 0xf1, 0x55, 0xbb, 0x7c, 0x44, 0x3e, 0x29, 0x23, 0xd3, 0xb0, + 0x27, 0xe9, 0x93, 0x54, 0x91, 0x4d, 0x6b, 0x37, 0x35, 0xdf, 0xab, 0x69, + 0xf4, 0xf4, 0x6e, 0x1d, 0xe3, 0xf6, 0xac, 0x6b, 0x86, 0x64, 0x47, 0xd6, + 0x92, 0x96, 0x6a, 0x1a, 0xb8, 0xa1, 0xf3, 0x64, 0xce, 0xf3, 0x62, 0xca, + 0x24, 0xc7, 0x85, 0x81, 0x21, 0x8f, 0x1c, 0x9a, 0x51, 0x30, 0xbd, 0xbb, + 0x68, 0x23, 0x56, 0x1d, 0x38, 0xfa, 0x21, 0xf6, 0x8c, 0x03, 0xa7, 0xba, + 0x11, 0x7e, 0xf9, 0x79, 0x24, 0xf8, 0x9d, 0x5f, 0x4b, 0x86, 0x7f, 0x3d, + 0xf8, 0xac, 0x76, 0x65, 0x01, 0x01, 0x00, 0x3b + }; + + + + +// ========== XLImageUtils Member Functions ========== // + +/** + * @brief Detect MIME type from binary image data + * @param data Binary image data + * @return MIME type string (e.g., "image/png") + */ +std::string XLImageUtils::detectMimeType(const std::vector& data) +{ + if (data.size() >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4e && data[3] == 0x47) { + return "image/png"; + } + if (data.size() >= 2 && data[0] == 0xFF && data[1] == 0xD8) { + return "image/jpeg"; + } + if (data.size() >= 2 && data[0] == 0x42 && data[1] == 0x4D) { + return "image/bmp"; + } + if (data.size() >= 3 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46) { + return "image/gif"; + } + return ""; +} + +/** + * @brief Get file extension from MIME type + * @param mime MIME type string + * @return File extension (e.g., ".png") + */ +std::string XLImageUtils::extensionFromMime(const std::string& mime) +{ + if (mime == "image/png") return ".png"; + if (mime == "image/jpeg") return ".jpg"; + if (mime == "image/bmp") return ".bmp"; + if (mime == "image/gif") return ".gif"; + return ""; +} + +/** + * @brief Determine MIME type from file extension + * @param extension File extension + * @return MIME type + */ +XLMimeType XLImageUtils::mimeTypeFromExtension(const std::string& extension) +{ + if (extension.find("png") != std::string::npos ) return XLMimeType::PNG; + if ((extension.find("jpg") != std::string::npos ) || + (extension.find("jpeg") != std::string::npos )) return XLMimeType::JPEG; + if (extension.find("bmp") != std::string::npos ) return XLMimeType::BMP; + if (extension.find("gif") != std::string::npos ) return XLMimeType::GIF; + return XLMimeType::Unknown; +} + + + +std::string XLImageUtils::extensionFromMimeType(XLMimeType mimeType) +{ + switch (mimeType) { + case XLMimeType::PNG: return ".png"; + case XLMimeType::JPEG: return ".jpg"; + case XLMimeType::BMP: return ".bmp"; + case XLMimeType::GIF: return ".gif"; + case XLMimeType::Unknown: + default: return ""; + } +} + +/** + * @brief Convert pixels to EMUs (Excel Measurement Units) + * @param pixels Number of pixels + * @return Number of EMUs + */ +uint32_t XLImageUtils::pixelsToEMUs(uint32_t pixels) +{ + return pixels * 9525; // EMU_TO_PIXEL_RATIO +} + +/** + * @brief Get image dimensions from binary data + * @param data Binary image data + * @param mimeType MIME type of the image + * @return Pair of (width, height) in pixels + */ +std::pair XLImageUtils::getImageDimensions(const std::vector& data, XLMimeType mimeType) +{ + if (data.size() < 8) return {0, 0}; + + // PNG signature check and dimension extraction + if (mimeType == XLMimeType::PNG) { + if (data.size() >= 24 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47) { + // PNG IHDR chunk contains width and height at bytes 16-23 + uint32_t width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19]; + uint32_t height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23]; + return {width, height}; + } + } + // JPEG signature check and dimension extraction + else if (mimeType == XLMimeType::JPEG) { + if (data.size() >= 2 && data[0] == 0xFF && data[1] == 0xD8) { + // For JPEG, we need to find the SOF0 marker (0xFFC0) and extract dimensions + for (size_t i = 2; i < data.size() - 8; ++i) { + if (data[i] == 0xFF && data[i + 1] == 0xC0) { + uint32_t height = (data[i + 5] << 8) | data[i + 6]; + uint32_t width = (data[i + 7] << 8) | data[i + 8]; + return {width, height}; + } + } + } + } + // BMP signature check and dimension extraction + else if (mimeType == XLMimeType::BMP) { + if (data.size() >= 26 && data[0] == 0x42 && data[1] == 0x4D) { + // BMP header contains width and height at bytes 18-25 + uint32_t width = data[18] | (data[19] << 8) | (data[20] << 16) | (data[21] << 24); + uint32_t height = data[22] | (data[23] << 8) | (data[24] << 16) | (data[25] << 24); + return {width, height}; + } + } + // GIF signature check and dimension extraction + else if (mimeType == XLMimeType::GIF) { + if (data.size() >= 10 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46) { + // GIF header contains width and height at bytes 6-9 + uint32_t width = data[6] | (data[7] << 8); + uint32_t height = data[8] | (data[9] << 8); + return {width, height}; + } + } + + // Default fallback - assume square image + return {100, 100}; +} + +/** + * @brief Convert pixels to EMUs (Excel units) + * @param pixels Number of pixels + * @return Equivalent EMUs + */ +uint32_t XLImageUtils::pixelsToEmus(uint32_t pixels) +{ + return pixels * 9525; // EMU_TO_PIXEL_RATIO +} + +/** + * @brief Convert EMUs to pixels + * @param emus Number of EMUs + * @return Equivalent pixels + */ +uint32_t XLImageUtils::emusToPixels(uint32_t emus) +{ + return emus / 9525; // EMU_TO_PIXEL_RATIO +} + +/** + * @brief Convert EMUs to Excel units + * @param emus EMU value + * @return Excel unit value + */ +uint32_t XLImageUtils::emusToExcelUnits(uint32_t emus) +{ + // Convert EMUs to Excel units (1 EMU = 1/9525 Excel units) + return emus / 9525; +} + +/** + * @brief Convert points to EMUs + * @param points Point value + * @return EMU value + */ +uint32_t XLImageUtils::pointsToEmus(double points) +{ + // Convert points to EMUs (1 point = 12700 EMUs) + return static_cast(points * 12700); +} + +/** + * @brief Validate image data integrity + * @param data Binary image data + * @param mimeType MIME type of the image + * @return True if image data is valid + */ +bool XLImageUtils::validateImageData(const std::vector& data, const XLMimeType& mimeType) +{ + if (data.empty()) return false; + + // Check basic signature matches MIME type + if (XLMimeType::PNG == mimeType) { + return data.size() >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47; + } + if (XLMimeType::JPEG == mimeType) { + return data.size() >= 2 && data[0] == 0xFF && data[1] == 0xD8; + } + if (XLMimeType::BMP == mimeType) { + return data.size() >= 2 && data[0] == 0x42 && data[1] == 0x4D; + } + if (XLMimeType::GIF == mimeType) { + return data.size() >= 3 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46; + } + + return false; +} + +/** + * @brief Calculate aspect ratio from dimensions + * @param width Width in pixels + * @param height Height in pixels + * @return Aspect ratio (width/height) + */ +double XLImageUtils::calculateAspectRatio(uint32_t width, uint32_t height) +{ + if (height == 0) return 0.0; + return static_cast(width) / static_cast(height); +} + +/** + * @brief Get image dimensions from binary data (auto-detect MIME type) + * @param data Binary image data + * @return Pair of (width, height) in pixels, or {0,0} if detection fails + */ +std::pair XLImageUtils::getImageDimensions(const std::vector& data) +{ + // Auto-detect MIME type first + XLMimeType mimeType = detectMimeTypeEnum(data); + if (mimeType == XLMimeType::Unknown) { + return {0, 0}; + } + + // Use the existing function with detected MIME type + return getImageDimensions(data, mimeType); +} + +/** + * @brief Check if image dimensions are reasonable + * @param width Width in pixels + * @param height Height in pixels + * @return True if dimensions are within reasonable bounds + */ +bool XLImageUtils::isValidImageSize(uint32_t width, uint32_t height) +{ + // Reasonable bounds: 1x1 to 10000x10000 pixels + return width > 0 && height > 0 && width <= 10000 && height <= 10000; +} + +/** + * @brief Calculate hash value for binary image data + * @param imageData Binary image data as vector + * @return Hash value for the image data + */ +size_t XLImageUtils::imageDataHash(const std::vector& imageData) +{ + std::string imageDataStr; + imageDataStr.reserve(imageData.size()); + imageDataStr.insert(imageDataStr.end(), imageData.begin(), imageData.end()); + const size_t h = imageDataStrHash(imageDataStr); + return h; +} + +/** + * @brief Calculate hash value for image data stored as string + * @param imageDataStr Binary image data as string + * @return Hash value for the image data (same as imageDataHash for equivalent data) + */ +size_t XLImageUtils::imageDataStrHash(const std::string& imageDataStr) +{ + size_t h = 0; + if (!imageDataStr.empty()) { + std::hash hasher; + h = hasher(imageDataStr); + } + return h; +} + +/** + * @brief Convert MIME type string to XLContentType enum + * @param mimeType MIME type string (e.g., "image/png") + * @return Corresponding XLContentType enum value + */ +XLContentType XLImageUtils::mimeTypeToContentType(const XLMimeType& mimeType) +{ + if (mimeType == XLMimeType::PNG) return XLContentType::ImagePNG; + if (mimeType == XLMimeType::JPEG) return XLContentType::ImageJPEG; + if (mimeType == XLMimeType::BMP) return XLContentType::ImageBMP; + if (mimeType == XLMimeType::GIF) return XLContentType::ImageGIF; + return XLContentType::Unknown; +} + +XLMimeType XLImageUtils::contentTypeToMimeType(XLContentType contentType) +{ + switch (contentType) { + case XLContentType::ImagePNG: return XLMimeType::PNG; + case XLContentType::ImageJPEG: return XLMimeType::JPEG; + case XLContentType::ImageBMP: return XLMimeType::BMP; + case XLContentType::ImageGIF: return XLMimeType::GIF; + default: return XLMimeType::Unknown; + } +} + +XLMimeType XLImageUtils::stringToMimeType(const std::string& mimeTypeStr) +{ + if (mimeTypeStr == "image/png") return XLMimeType::PNG; + if (mimeTypeStr == "image/jpeg") return XLMimeType::JPEG; + if (mimeTypeStr == "image/bmp") return XLMimeType::BMP; + if (mimeTypeStr == "image/gif") return XLMimeType::GIF; + return XLMimeType::Unknown; +} + +std::string XLImageUtils::mimeTypeToString(XLMimeType mimeType) +{ + switch (mimeType) { + case XLMimeType::PNG: return "image/png"; + case XLMimeType::JPEG: return "image/jpeg"; + case XLMimeType::BMP: return "image/bmp"; + case XLMimeType::GIF: return "image/gif"; + case XLMimeType::Unknown: + default: return ""; + } +} + +XLMimeType XLImageUtils::detectMimeTypeEnum(const std::vector& data) +{ + if (data.size() >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47) { + return XLMimeType::PNG; + } + if (data.size() >= 2 && data[0] == 0xFF && data[1] == 0xD8) { + return XLMimeType::JPEG; + } + if (data.size() >= 2 && data[0] == 0x42 && data[1] == 0x4D) { + return XLMimeType::BMP; + } + if (data.size() >= 3 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46) { + return XLMimeType::GIF; + } + return XLMimeType::Unknown; +} diff --git a/OpenXLSX/sources/XLSheet.cpp b/OpenXLSX/sources/XLSheet.cpp index f1ed1c7b..8251518a 100644 --- a/OpenXLSX/sources/XLSheet.cpp +++ b/OpenXLSX/sources/XLSheet.cpp @@ -46,6 +46,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. // ===== External Includes ===== // #include // std::max #include // std::isdigit (issue #330) +#include // std::cout #include // std::numeric_limits #include // std::multimap #include // std::set @@ -996,8 +997,7 @@ XLWorksheet::XLWorksheet(const XLWorksheet& other) : XLSheetBase(ot m_drawingML = other.m_drawingML; // " XLDrawingML m_comments = other.m_comments; // " XLComments " m_tables = other.m_tables; // " XLTables " - m_images = other.m_images; // " std::vector" - m_imageRegistry = other.m_imageRegistry; // " std::vector" + m_embImages = other.m_embImages; // " XLEmbImgVecShPtr" } /** @@ -1011,8 +1011,7 @@ XLWorksheet::XLWorksheet(XLWorksheet&& other) : XLSheetBase< XLWorksheet >(other m_drawingML = std::move(other.m_drawingML); // " XLDrawingML m_comments = std::move(other.m_comments); // " XLComments " m_tables = std::move(other.m_tables); // " XLTables " - m_images = std::move(other.m_images); // " std::vector" - m_imageRegistry = std::move(other.m_imageRegistry); // " std::vector" + m_embImages = std::move(other.m_embImages); // " XLEmbImgVecShPtr" } /** @@ -1027,8 +1026,7 @@ XLWorksheet& XLWorksheet::operator=(const XLWorksheet& other) m_drawingML = other.m_drawingML; m_comments = other.m_comments; m_tables = other.m_tables; - m_images = other.m_images; - m_imageRegistry = other.m_imageRegistry; + m_embImages = other.m_embImages; return *this; } @@ -1044,8 +1042,7 @@ XLWorksheet& XLWorksheet::operator=(XLWorksheet&& other) m_drawingML = std::move(other.m_drawingML); m_comments = std::move(other.m_comments); m_tables = std::move(other.m_tables); - m_images = std::move(other.m_images); - m_imageRegistry = std::move(other.m_imageRegistry); + m_embImages = std::move(other.m_embImages); return *this; } @@ -1722,7 +1719,7 @@ XLDrawingML& XLWorksheet::drawingML() // ===== Check if DrawingML XML file already exists uint16_t sheetXmlNo = sheetXmlNumber(); - std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; + std::string drawingFilename = getDrawingFilename(); // Try to load existing DrawingML data first try { @@ -1781,9 +1778,7 @@ XLDrawingML& XLWorksheet::drawingML() appendAndSetAttribute(drawing, "r:id", drawingRelationship.id()); // Create drawing relationships file only if it doesn't exist - std::string drawingRelsFilename = drawingFilename.substr(0, drawingFilename.find_last_of('/') + 1) + - "_rels/" + - drawingFilename.substr(drawingFilename.find_last_of('/') + 1) + ".rels"; + std::string drawingRelsFilename = getDrawingRelsFilename(); // Only create relationships file if it doesn't exist if (!parentDoc().hasRelationshipsFile(drawingRelsFilename)) { @@ -1823,7 +1818,7 @@ XLDrawingML& XLWorksheet::createEmptyDrawingML() // Add to archive using public method uint16_t sheetXmlNo = sheetXmlNumber(); - std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; + std::string drawingFilename = getDrawingFilename(); if (!parentDoc().addDrawingFile(drawingFilename, drawingXml)) { throw XLException("XLWorksheet::createEmptyDrawingML(): could not create drawing file"); @@ -1859,7 +1854,7 @@ XLDrawingML& XLWorksheet::createEmptyDrawingML() appendAndSetAttribute(drawing, "r:id", drawingRelationship.id()); // Create drawing relationships file - std::string drawingRelsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNo) + ".xml.rels"; + std::string drawingRelsFilename = getDrawingRelsFilename(); if (!parentDoc().hasRelationshipsFile(drawingRelsFilename)) { createDrawingRelationshipsFile(drawingFilename); } @@ -1921,184 +1916,103 @@ XLTables& XLWorksheet::tables() //---------------------------------------------------------------------------------------------------------------------- /** - * @details Add an image to the worksheet at the specified cell + * @brief Embed an image from image data at the specified cell location + * @details Constructs a oneCellAnchor XLImageAnchor from cellRef and creates XLImage from imageData and mimeType + * @param cellRef Cell reference where the image should be anchored + * @param imageData Vector containing the image binary data + * @param mimeType MIME type of the image (PNG, JPEG, etc.) + * @return Image ID string */ -bool XLWorksheet::addImage(const XLImage& image, const std::string& cellRef) +std::string XLWorksheet::embedImageFromImageData(const XLCellReference& cellRef, + const std::vector& imageData, + XLMimeType mimeType) { - return addImage(image, XLCellReference(cellRef)); + XLImageAnchor imageAnchor; + imageAnchor.initOneCell(cellRef, 0, 0); + const std::string imageId = embedImageFromImageData(imageAnchor, imageData, mimeType); + return imageId; } /** - * @details Add an image to the worksheet at the specified cell + * @brief Embed an image from image data using the specified anchor + * @details Creates XLImage from imageData and mimeType using the provided anchor configuration + * @param imageAnchor Anchor configuration specifying position and dimensions + * @param imageData Vector containing the image binary data + * @param mimeType MIME type of the image (PNG, JPEG, etc.) + * @return Image ID string */ -bool XLWorksheet::addImage(const XLImage& image, const XLCellReference& cellRef) +std::string XLWorksheet::embedImageFromImageData(const XLImageAnchor& imageAnchor, + const std::vector& imageData, + XLMimeType mimeType) { - return addImage(image, cellRef.row(), cellRef.column()); -} - -/** - * @details Add an image to the worksheet at the specified cell - */ -bool XLWorksheet::addImage(const XLImage& image, uint32_t row, uint16_t column) -{ - if (!image.isValid()) { - return false; - } - - try { - // Create a copy of the image and set its ID - XLImage imageWithId = image; - // Always generate a new worksheet-scoped unique ID (ignore existing ID) - std::string oldId = imageWithId.id(); - std::string newId = generateNextImageId(); - imageWithId.setId(newId); - - // Store the image in the vector - m_images.push_back(imageWithId); - - // Get the parent document - XLDocument& doc = parentDoc(); - - // Create the image file path in the archive - std::string imageFilename = "xl/media/image_" + imageWithId.id() + imageWithId.extension(); - - // Store the image binary data in the archive - if (!doc.addImageEntry(imageFilename, imageWithId.data(), imageWithId.contentType())) { - return false; - } - - // Ensure we have a DrawingML drawing - create it if needed - if (!m_drawingML.valid()) { - createEmptyDrawingML(); - } - - // Add image to DrawingML - // Use sequential relationship ID that matches createDrawingRelationshipsFile - std::string relationshipId = getRelationshipIdFromImageCount(); - - addImageToDrawingML(imageWithId, row, column, relationshipId); - - // invalidate registry - m_imageRegistry.clear(); - - return true; - } - catch (const std::exception&) { - return false; - } -} - -/** - * @details Add an image from file to the worksheet at the specified cell - */ -bool XLWorksheet::addImageFromFile(const std::string& imagePath, const std::string& cellRef) -{ - return addImageFromFile(imagePath, XLCellReference(cellRef)); -} - -/** - * @details Add an image from file to the worksheet at the specified cell - */ -bool XLWorksheet::addImageFromFile(const std::string& imagePath, const XLCellReference& cellRef) -{ - return addImageFromFile(imagePath, cellRef.row(), cellRef.column()); -} - -/** - * @details Add an image from file to the worksheet at the specified cell - */ -bool XLWorksheet::addImageFromFile(const std::string& imagePath, uint32_t row, uint16_t column) -{ - XLImage image; - std::string imageId = generateNextImageId(); - if (!image.loadFromFile(imagePath, imageId)) { - return false; + // Auto-detect MIME type if unknown + XLMimeType detectedMimeType = mimeType; + if (mimeType == XLMimeType::Unknown) { + detectedMimeType = XLImageUtils::detectMimeTypeEnum(imageData); } - return addImage(image, row, column); + XLImageShPtr image = parentDoc().getImageManager().findOrAddImage("", + imageData, + XLImageUtils::mimeTypeToContentType(detectedMimeType)); + const std::string imageId = embedImage(imageAnchor, image); + return imageId; } /** - * @details Add an image to the worksheet with precise positioning + * @brief Embed an image from file at the specified cell location + * @details Constructs a oneCellAnchor XLImageAnchor from cellRef and creates XLImage from file and mimeType + * @param cellRef Cell reference where the image should be anchored + * @param imageFileName Path to the image file on disk + * @param mimeType MIME type of the image (PNG, JPEG, etc.) + * @return Image ID string */ -bool XLWorksheet::addImageWithOffset(const XLImage& image, uint32_t row, uint16_t column, - int32_t rowOffset, int32_t colOffset) +std::string XLWorksheet::embedImageFromFile(const XLCellReference& cellRef, + const std::string& imageFileName, + XLMimeType mimeType) { - // Create a copy of the image and set its ID - XLImage imageWithId = image; - // Only generate a new ID if the image doesn't already have one - if (imageWithId.id().empty()) { - imageWithId.setId(generateNextImageId()); - } - - // Store the image - m_images.push_back(imageWithId); - - // Add image entry to document - std::string imageFilename = "xl/media/image_" + imageWithId.id() + imageWithId.extension(); - if (!parentDoc().addImageEntry(imageFilename, imageWithId.data(), imageWithId.contentType())) { - throw XLException("XLWorksheet::addImageWithOffset(): could not add image entry to document"); - } - - // Get or create DrawingML - if (!m_drawingML.valid()) { - createEmptyDrawingML(); - } - XLDrawingML& drawing = m_drawingML; - - // Generate relationship ID - std::string relationshipId = getRelationshipIdFromImageCount(); - - // Add image to DrawingML with offset - addImageToDrawingMLWithOffset(imageWithId, row, column, relationshipId, rowOffset, colOffset); - - // invalidate registry - m_imageRegistry.clear(); - - return true; + XLImageAnchor imageAnchor; + imageAnchor.initOneCell(cellRef, 0, 0); + const std::string imageId = embedImageFromFile(imageAnchor, imageFileName, mimeType); + return imageId; } /** - * @details Add an image to the worksheet with two-cell anchoring + * @brief Embed an image from file using the specified anchor + * @details Creates XLImage from file and mimeType using the provided anchor configuration + * @param imageAnchor Anchor configuration specifying position and dimensions + * @param imageFileName Path to the image file on disk + * @param mimeType MIME type of the image (PNG, JPEG, etc.) + * @return Image ID string */ -bool XLWorksheet::addImageTwoCellAnchor(const XLImage& image, - uint32_t fromRow, uint16_t fromCol, - uint32_t toRow, uint16_t toCol, - int32_t fromRowOffset, int32_t fromColOffset, - int32_t toRowOffset, int32_t toColOffset) +std::string XLWorksheet::embedImageFromFile(const XLImageAnchor& imageAnchor, + const std::string& imageFileName, + XLMimeType mimeType) { - // Create a copy of the image and set its ID - XLImage imageWithId = image; - // Always generate a new worksheet-scoped unique ID (ignore existing ID) - std::string oldId = imageWithId.id(); - imageWithId.setId(generateNextImageId()); - - // Store the image - m_images.push_back(imageWithId); + std::string imageId; - // Add image entry to document - std::string imageFilename = "xl/media/image_" + imageWithId.id() + imageWithId.extension(); - if (!parentDoc().addImageEntry(imageFilename, imageWithId.data(), imageWithId.contentType())) { - throw XLException("XLWorksheet::addImageTwoCellAnchor(): could not add image entry to document"); - } - - // Get or create DrawingML - if (!m_drawingML.valid()) { - createEmptyDrawingML(); - } - XLDrawingML& drawing = m_drawingML; - - // Generate relationship ID - std::string relationshipId = getRelationshipIdFromImageCount(); - - // Add image to DrawingML with two-cell anchoring - addImageToDrawingMLTwoCellAnchor(imageWithId, fromRow, fromCol, toRow, toCol, relationshipId, - fromRowOffset, fromColOffset, toRowOffset, toColOffset); - - // invalidate registry - m_imageRegistry.clear(); + std::ifstream file(imageFileName, std::ios::binary); + if (file.is_open()) { + // Read the entire file into a vector + file.seekg(0, std::ios::end); + size_t fileSize = file.tellg(); + file.seekg(0, std::ios::beg); + + std::vector imageData(fileSize); + file.read(reinterpret_cast(imageData.data()), fileSize); + file.close(); + + // Auto-detect MIME type if unknown + XLMimeType detectedMimeType = mimeType; + if (mimeType == XLMimeType::Unknown) { + detectedMimeType = XLImageUtils::detectMimeTypeEnum(imageData); + } + + XLImageShPtr image = parentDoc().getImageManager().findOrAddImage("", + imageData, XLImageUtils::mimeTypeToContentType(detectedMimeType)); - return true; + imageId = embedImage(imageAnchor, image); + } + return imageId; } /** @@ -2106,7 +2020,7 @@ bool XLWorksheet::addImageTwoCellAnchor(const XLImage& image, */ size_t XLWorksheet::imageCount() const { - if (m_images.empty()) { + if (getEmbImages().empty()) { // Check if DrawingML is valid (was initialized because images exist) if (m_drawingML.valid()) { size_t count = m_drawingML.imageCount(); @@ -2127,7 +2041,7 @@ size_t XLWorksheet::imageCount() const } } else { // Use the vector count for newly added images - return m_images.size(); + return getEmbImages().size(); } } @@ -2136,7 +2050,7 @@ size_t XLWorksheet::imageCount() const */ bool XLWorksheet::hasImages() const { - return !m_images.empty(); + return !getEmbImages().empty(); } /** @@ -2160,7 +2074,6 @@ std::string XLWorksheet::generateNextImageId() const // Get all existing image IDs (sorted and unique) std::vector existingIds = getImageIDs(); - // Generate unique numeric ID using binary search for efficiency // Microsoft Excel uses numeric IDs (e.g., "1", "2", "3") in DrawingML XML int counter = 1; std::string candidateId; @@ -2172,36 +2085,60 @@ std::string XLWorksheet::generateNextImageId() const } /** - * @details Generate a relationship ID based on the current image count + * @details Generate a unique relationship ID */ -std::string XLWorksheet::getRelationshipIdFromImageCount() const +std::string XLWorksheet::getNextUnusedRelationshipId() const { - // For existing files, count from DrawingML; for new files, use m_images - if (m_images.empty()) { - // Try to get count from existing DrawingML + // Collect all existing relationship IDs + std::set existingRelIds; + + // Get relationship IDs from getEmbImages() + for (const auto& embImage : getEmbImages()) { + if (!embImage.relationshipId().empty()) { + existingRelIds.insert(embImage.relationshipId()); + } + } + + // Get relationship IDs from DrawingML XML (for existing files) + // TODO: this is not needed - we should only be using the m_embImages vector for relationship IDs + if (m_drawingML.valid()) { try { - if (m_drawingML.valid()) { - return "rId" + std::to_string(m_drawingML.imageCount() + 1); + XMLNode rootNode = m_drawingML.getRootNode(); + if (!rootNode.empty()) { + for (auto anchor : rootNode.children()) { + // Check anchor type using enum conversion + XLImageAnchorType anchorType = XLImageAnchorUtils::stringToAnchorType(anchor.name()); + if (anchorType != XLImageAnchorType::Unknown) { + XMLNode pic = anchor.child("xdr:pic"); + if (!pic.empty()) { + XMLNode blipFill = pic.child("xdr:blipFill"); + if (!blipFill.empty()) { + XMLNode blip = blipFill.child("a:blip"); + if (!blip.empty()) { + XMLAttribute embedAttr = blip.attribute("r:embed"); + if (!embedAttr.empty()) { + // TODO: add check that the relationship ID is not already in existingRelIds + existingRelIds.insert(embedAttr.value()); + } + } + } + } + } + } } } catch (...) { // Fall through to default } - return "rId1"; // Default for first image - } else { - return "rId" + std::to_string(m_images.size() + 1); } -} - -/** - * @details Add an image to the DrawingML - */ -void XLWorksheet::addImageToDrawingML(const XLImage& image, uint32_t row, uint16_t column, const std::string& relationshipId) -{ - // Add image to DrawingML - m_drawingML.addImage(image, row, column, relationshipId); - // Add relationship for the new image - addImageRelationship(image, relationshipId); + // Find the next available relationship ID + int counter = 1; + std::string candidateId; + do { + candidateId = "rId" + std::to_string(counter++); + } while (existingRelIds.find(candidateId) != existingRelIds.end()); + + return candidateId; } /** @@ -2210,9 +2147,7 @@ void XLWorksheet::addImageToDrawingML(const XLImage& image, uint32_t row, uint16 void XLWorksheet::createDrawingRelationshipsFile(const std::string& drawingFilename) { // Create the relationships filename - std::string drawingRelsFilename = drawingFilename.substr(0, drawingFilename.find_last_of('/') + 1) + - "_rels/" + - drawingFilename.substr(drawingFilename.find_last_of('/') + 1) + ".rels"; + std::string drawingRelsFilename = getDrawingRelsFilename(); // Create the relationships XML content std::string relsXml = "\n" @@ -2220,9 +2155,10 @@ void XLWorksheet::createDrawingRelationshipsFile(const std::string& drawingFilen // Add relationships for each image int relationshipId = 1; - for (const auto& image : m_images) { - std::string imageFilename = "xl/media/image_" + image.id() + image.extension(); - std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); + for (const auto& embImage : getEmbImages()) { + // Use the actual package filename from the XLImage object, not constructed from ID/extension + std::string packageFilename = embImage.getImage()->filePackagePath(); + std::string imageRelativePath = "../media/" + packageFilename.substr(packageFilename.find_last_of('/') + 1); relsXml += "\n" + "\n" + "\n" + ""; +} + /** * @details Add a relationship for a new image to the existing relationships file */ -void XLWorksheet::addImageRelationship(const XLImage& image, const std::string& relationshipId) +void XLWorksheet::addImageRelationship(const XLEmbeddedImage& embImage) { - // Get the drawing filename for this worksheet - uint16_t sheetXmlNo = sheetXmlNumber(); - std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNo) + ".xml"; + // Get the drawing relationships filename + std::string drawingRelsFilename = getDrawingRelsFilename(); - // Create the relationships filename - std::string drawingRelsFilename = drawingFilename.substr(0, drawingFilename.find_last_of('/') + 1) + - "_rels/" + - drawingFilename.substr(drawingFilename.find_last_of('/') + 1) + ".rels"; + // Get the image's package path and convert to relative path for relationships + std::string packageFilename = embImage.getImage()->filePackagePath(); + std::string imageRelativePath = "../media/" + packageFilename.substr(packageFilename.find_last_of('/') + 1); + /* get relationship ID */ + const std::string& relationshipId = embImage.relationshipId(); + std::string relsXml; // Try to read existing relationships file - if (parentDoc().hasRelationshipsFile(drawingRelsFilename)) { + if (this->parentDoc().hasRelationshipsFile(drawingRelsFilename)) { // Read existing content and add new relationship try { // Get existing content from in-memory XML data - std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + std::string relsFilename = getDrawingRelsFilename(); // Try to get the relationships XML data from in-memory cache - XLXmlData* relsXmlData = const_cast(parentDoc()).getRelationshipsXmlData(relsFilename); + XLXmlData* relsXmlData = const_cast(this->parentDoc()).getRelationshipsXmlData(relsFilename); if (!relsXmlData || !relsXmlData->valid()) { - // No existing relationships data, will create new file below - relsXml = "\n" - "\n" - "\n" - ""; + // No existing relationships data, create new file + relsXml = createNewRelationshipsXml(relationshipId, imageRelativePath); } else { // Parse existing XML from in-memory data XMLDocument* relsDoc = relsXmlData->getXmlDocument(); if (!relsDoc || !relsDoc->document_element()) { // Invalid XML data, create new file - relsXml = "\n" - "\n" - "\n" - ""; + relsXml = createNewRelationshipsXml(relationshipId, imageRelativePath); } else { XMLNode relationshipsNode = relsDoc->document_element(); @@ -2303,6 +2243,7 @@ void XLWorksheet::addImageRelationship(const XLImage& image, const std::string& "\" Type=\"" + relType + "\" Target=\"" + relTarget + "\"/>\n"; // Track existing IDs for uniqueness check + // TODO: this should not be needed and could cause a problem if different than relationhipId in embImage existingIds.insert(relId); // Extract numeric part for max ID calculation @@ -2317,6 +2258,7 @@ void XLWorksheet::addImageRelationship(const XLImage& image, const std::string& } // Generate unique relationship ID + // TODO: this should not be needed and could cause a problem if different than relationhipId in embImage std::string uniqueRelId = relationshipId; int idNum = maxId + 1; while (existingIds.find(uniqueRelId) != existingIds.end()) { @@ -2325,8 +2267,7 @@ void XLWorksheet::addImageRelationship(const XLImage& image, const std::string& } // Add new relationship with unique ID - std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); - relsXml += "\n"; @@ -2335,22 +2276,11 @@ void XLWorksheet::addImageRelationship(const XLImage& image, const std::string& } } catch (const std::exception&) { // If parsing fails, fall back to creating new file - relsXml = "\n" - "\n" - "\n" - ""; + relsXml = createNewRelationshipsXml(relationshipId, imageRelativePath); } } else { // Create new relationships file - std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); - relsXml = "\n" - "\n" - "\n" - ""; + relsXml = createNewRelationshipsXml(relationshipId, imageRelativePath); } // Update the relationships file @@ -2360,35 +2290,100 @@ void XLWorksheet::addImageRelationship(const XLImage& image, const std::string& } /** - * @details Add an image to the DrawingML with precise positioning + * @details Add a relationship for a new image to the existing relationships file */ -void XLWorksheet::addImageToDrawingMLWithOffset(const XLImage& image, uint32_t row, uint16_t column, - const std::string& relationshipId, - int32_t rowOffset, int32_t colOffset) +void XLWorksheet::addImageRelationship(const XLEmbeddedImage& embImage, const std::string& relationshipId) { - // Add image to DrawingML - m_drawingML.addImageWithOffset(image, row, column, relationshipId, rowOffset, colOffset); + // Get the drawing relationships filename + std::string drawingRelsFilename = getDrawingRelsFilename(); - // Add relationship for the new image - addImageRelationship(image, relationshipId); -} - -/** - * @details Add an image to the DrawingML with two-cell anchoring - */ -void XLWorksheet::addImageToDrawingMLTwoCellAnchor(const XLImage& image, - uint32_t fromRow, uint16_t fromCol, - uint32_t toRow, uint16_t toCol, - const std::string& relationshipId, - int32_t fromRowOffset, int32_t fromColOffset, - int32_t toRowOffset, int32_t toColOffset) -{ - // Add image to DrawingML - m_drawingML.addImageTwoCellAnchor(image, fromRow, fromCol, toRow, toCol, relationshipId, - fromRowOffset, fromColOffset, toRowOffset, toColOffset); + // Get the image's package path and convert to relative path for relationships + std::string packageFilename = embImage.getImage()->filePackagePath(); + std::string imageRelativePath = "../media/" + packageFilename.substr(packageFilename.find_last_of('/') + 1); + + std::string relsXml; + + // Try to read existing relationships file + if (this->parentDoc().hasRelationshipsFile(drawingRelsFilename)) { + // Read existing content and add new relationship + try { + // Get existing content from in-memory XML data + std::string relsFilename = getDrawingRelsFilename(); + + // Try to get the relationships XML data from in-memory cache + XLXmlData* relsXmlData = const_cast(this->parentDoc()).getRelationshipsXmlData(relsFilename); + if (!relsXmlData || !relsXmlData->valid()) { + // No existing relationships data, create new file + relsXml = createNewRelationshipsXml(relationshipId, imageRelativePath); + } else { + // Parse existing XML from in-memory data + XMLDocument* relsDoc = relsXmlData->getXmlDocument(); + if (!relsDoc || !relsDoc->document_element()) { + // Invalid XML data, create new file + relsXml = createNewRelationshipsXml(relationshipId, imageRelativePath); + } else { + XMLNode relationshipsNode = relsDoc->document_element(); + + // Start building new XML + relsXml = "\n" + "\n"; + + // Find the highest existing relationship ID to avoid duplicates + int maxId = 0; + std::set existingIds; + + for (auto relNode : relationshipsNode.children("Relationship")) { + std::string relId = relNode.attribute("Id").value(); + std::string relType = relNode.attribute("Type").value(); + std::string relTarget = relNode.attribute("Target").value(); + + // Copy existing relationships + relsXml += "\n"; + + // Track existing IDs for uniqueness check + existingIds.insert(relId); + + // Extract numeric part for max ID calculation + if (relId.substr(0, 3) == "rId") { + try { + int idNum = std::stoi(relId.substr(3)); + maxId = std::max(maxId, idNum); + } catch (...) { + // Ignore non-numeric IDs + } + } + } + + // Generate unique relationship ID + std::string uniqueRelId = relationshipId; + int idNum = maxId + 1; + while (existingIds.find(uniqueRelId) != existingIds.end()) { + uniqueRelId = "rId" + std::to_string(idNum); + idNum++; + } + + // Add new relationship with unique ID + relsXml += "\n"; + + relsXml += ""; + } + } + } catch (const std::exception&) { + // If parsing fails, fall back to creating new file + relsXml = createNewRelationshipsXml(relationshipId, imageRelativePath); + } + } else { + // Create new relationships file + relsXml = createNewRelationshipsXml(relationshipId, imageRelativePath); + } - // Add relationship for the new image - addImageRelationship(image, relationshipId); + // Update the relationships file + if (!const_cast(parentDoc()).updateRelationshipsXmlData(drawingRelsFilename, relsXml)) { + throw XLException("XLWorksheet::addImageRelationship(): could not update drawing relationships file"); + } } /** @@ -2489,18 +2484,18 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const return thisImageCount < otherImageCount ? -1 : 1; } - // Compare image registry (parsed image metadata) - size_t thisRegistrySize = m_imageRegistry.size(); - size_t otherRegistrySize = other.m_imageRegistry.size(); - if (thisRegistrySize != otherRegistrySize) { - appendDbgMsg(diffMsg, "image registry size differs: " + std::to_string(thisRegistrySize) + - " vs " + std::to_string(otherRegistrySize)); - return thisRegistrySize < otherRegistrySize ? -1 : 1; + // Compare embedded images (parsed image metadata) + size_t thisEmbImagesSize = getEmbImages().size(); + size_t otherEmbImagesSize = other.getEmbImages().size(); + if (thisEmbImagesSize != otherEmbImagesSize) { + appendDbgMsg(diffMsg, "embedded images size differs: " + std::to_string(thisEmbImagesSize) + + " vs " + std::to_string(otherEmbImagesSize)); + return thisEmbImagesSize < otherEmbImagesSize ? -1 : 1; } // Compare images if both worksheets have images in memory - size_t thisImagesInMemory = m_images.size(); - size_t otherImagesInMemory = other.m_images.size(); + size_t thisImagesInMemory = getEmbImages().size(); + size_t otherImagesInMemory = other.getEmbImages().size(); if (thisImagesInMemory > 0 || otherImagesInMemory > 0) { if (thisImagesInMemory != otherImagesInMemory) { appendDbgMsg(diffMsg, "images in memory count differs: " + std::to_string(thisImagesInMemory) + @@ -2510,7 +2505,7 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const // Compare each image in memory for (size_t i = 0; i < thisImagesInMemory && i < otherImagesInMemory; ++i) { - int imageCompare = m_images[i].compare(other.m_images[i], diffMsg); + int imageCompare = getEmbImages()[i].compare(other.getEmbImages()[i], diffMsg); if (imageCompare != 0) { appendDbgMsg(diffMsg, "image " + std::to_string(i) + " differs"); return imageCompare; @@ -2518,13 +2513,13 @@ int XLWorksheet::compare(const XLWorksheet& other, std::string* diffMsg) const } } - // Compare image registry entries if both have registry entries - if (thisRegistrySize > 0) { - for (size_t i = 0; i < thisRegistrySize && i < otherRegistrySize; ++i) { - int registryCompare = m_imageRegistry[i].compare(other.m_imageRegistry[i], diffMsg); - if (registryCompare != 0) { - appendDbgMsg(diffMsg, "image registry entry " + std::to_string(i) + " differs"); - return registryCompare; + // Compare embedded images entries if both have embedded images entries + if (thisEmbImagesSize > 0) { + for (size_t i = 0; i < thisEmbImagesSize && i < otherEmbImagesSize; ++i) { + int embImagesCompare = getEmbImages()[i].compare(other.getEmbImages()[i], diffMsg); + if (embImagesCompare != 0) { + appendDbgMsg(diffMsg, "embedded images entry " + std::to_string(i) + " differs"); + return embImagesCompare; } } } @@ -2571,32 +2566,31 @@ int XLWorksheet::verifyData(std::string* dbgMsg) const // Only verify DrawingML if the worksheet has images // For worksheets without images, DrawingML can be invalid - if (!m_imageRegistry.empty() || !m_images.empty() || hasImagesInXML()) { + if (!getEmbImages().empty() || hasImagesInXML()) { errorCount += m_drawingML.verifyData(dbgMsg); } // errorCount += m_comments.verifyData(dbgMsg); // errorCount += m_tables.verifyData(dbgMsg); // Verify images vector - for (size_t i = 0; i < m_images.size(); ++i) { - int imageErrors = m_images[i].verifyData(dbgMsg); + for (size_t i = 0; i < getEmbImages().size(); ++i) { + int imageErrors = getEmbImages()[i].verifyData(dbgMsg); if (imageErrors > 0) { appendDbgMsg(dbgMsg, "[" + worksheetName + "] image " + std::to_string(i) + " has " + std::to_string(imageErrors) + " errors"); errorCount += imageErrors; } } - // Verify image registry consistency + // Verify embedded images consistency if (m_drawingML.valid()) { try { - auto imageInfos = getImageInfos(); - if (imageInfos.size() != m_images.size()) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] image registry size (" + std::to_string(imageInfos.size()) + - ") does not match images vector size (" + std::to_string(m_images.size()) + ")"); + // Check that embedded images vector is populated + if (getEmbImages().empty()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] embedded images vector is empty but drawing is valid"); errorCount++; } } catch (const std::exception&) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] error accessing image registry"); + appendDbgMsg(dbgMsg, "[" + worksheetName + "] error accessing embedded images"); errorCount++; } } @@ -2607,7 +2601,7 @@ int XLWorksheet::verifyData(std::string* dbgMsg) const // Verify DrawingML XML consistency with registry errorCount += verifyDrawingMLConsistency(dbgMsg); - // Verify m_images vector consistency with XML + // Verify m_embImages vector consistency with XML errorCount += verifyImagesVectorConsistency(dbgMsg); // Verify binary image data exists in archive @@ -2621,19 +2615,18 @@ int XLWorksheet::verifyUniqueImageIDs(std::string* dbgMsg) const int duplicateCount = 0; std::string worksheetName = name(); - // Check 1: Image registry for duplicate IDs + // Check 1: Embedded images for duplicate IDs if (m_drawingML.valid()) { try { - auto imageInfos = getImageInfos(); std::set registryIds; - for (const auto& info : imageInfos) { - if (registryIds.find(info.imageId) != registryIds.end()) { - // Duplicate found in registry - appendDbgMsg(dbgMsg, "[" + worksheetName + "] duplicate image ID in registry: " + info.imageId); + for (const auto& embImage : getEmbImages()) { + if (registryIds.find(embImage.id()) != registryIds.end()) { + // Duplicate found in embedded images + appendDbgMsg(dbgMsg, "[" + worksheetName + "] duplicate image ID in embedded images: " + embImage.id()); duplicateCount++; } else { - registryIds.insert(info.imageId); + registryIds.insert(embImage.id()); } } } catch (const std::exception&) { @@ -2642,13 +2635,13 @@ int XLWorksheet::verifyUniqueImageIDs(std::string* dbgMsg) const } } - // Check 2: m_images vector for duplicate IDs + // Check 2: m_embImages vector for duplicate IDs std::set imageIds; - for (size_t i = 0; i < m_images.size(); ++i) { - const std::string& imageId = m_images[i].id(); + for (size_t i = 0; i < getEmbImages().size(); ++i) { + const std::string& imageId = getEmbImages()[i].id(); if (imageIds.find(imageId) != imageIds.end()) { - // Duplicate found in m_images vector - appendDbgMsg(dbgMsg, "[" + worksheetName + "] duplicate image ID in m_images vector: " + imageId); + // Duplicate found in m_embImages vector + appendDbgMsg(dbgMsg, "[" + worksheetName + "] duplicate image ID in m_embImages vector: " + imageId); duplicateCount++; } else { imageIds.insert(imageId); @@ -2665,29 +2658,26 @@ int XLWorksheet::verifyDrawingMLConsistency(std::string* dbgMsg) const // Check if DrawingML is valid if (!m_drawingML.valid()) { - // If DrawingML is invalid but we have images in registry or m_images, that's an inconsistency - if (!m_imageRegistry.empty() || !m_images.empty()) { + // If DrawingML is invalid but we have embedded images, that's an inconsistency + if (!getEmbImages().empty()) { appendDbgMsg(dbgMsg, "[" + worksheetName + "] DrawingML is invalid but has " + - std::to_string(m_imageRegistry.size()) + " registry entries and " + - std::to_string(m_images.size()) + " m_images entries"); + std::to_string(getEmbImages().size()) + " embedded images"); inconsistencyCount++; } return inconsistencyCount; // No further checks possible } try { - // Get image registry data - auto imageInfos = getImageInfos(); - size_t registryCount = imageInfos.size(); + // Get embedded images data + size_t embeddedCount = getEmbImages().size(); // Get DrawingML XML data XMLNode rootNode = m_drawingML.getRootNode(); if (rootNode.empty()) { // Empty root node is OK if there are no images - if (!m_imageRegistry.empty() || !m_images.empty()) { + if (!getEmbImages().empty()) { appendDbgMsg(dbgMsg, "[" + worksheetName + "] DrawingML root node is empty but has " + - std::to_string(m_imageRegistry.size()) + " registry entries and " + - std::to_string(m_images.size()) + " m_images entries"); + std::to_string(getEmbImages().size()) + " embedded images"); inconsistencyCount++; } return inconsistencyCount; @@ -2699,8 +2689,9 @@ int XLWorksheet::verifyDrawingMLConsistency(std::string* dbgMsg) const std::vector xmlRelationshipIds; for (auto anchor : rootNode.children()) { - if (matchesElementName(anchor.name(), "oneCellAnchor") || - matchesElementName(anchor.name(), "twoCellAnchor")) { + // Check anchor type using enum conversion + XLImageAnchorType anchorType = XLImageAnchorUtils::stringToAnchorType(anchor.name()); + if (anchorType != XLImageAnchorType::Unknown) { xmlAnchorCount++; // Extract image ID from XML @@ -2733,56 +2724,57 @@ int XLWorksheet::verifyDrawingMLConsistency(std::string* dbgMsg) const } // Check 1: Anchor count consistency - if (xmlAnchorCount != registryCount) { + if (xmlAnchorCount != embeddedCount) { appendDbgMsg(dbgMsg, "[" + worksheetName + "] XML anchor count (" + std::to_string(xmlAnchorCount) + - ") does not match registry count (" + std::to_string(registryCount) + ")"); + ") does not match embedded images count (" + std::to_string(embeddedCount) + ")"); inconsistencyCount++; } // Check 2: Image ID consistency - std::set registryImageIds; - for (const auto& info : imageInfos) { - registryImageIds.insert(info.imageId); + std::set embeddedImageIds; + for (const auto& embImage : getEmbImages()) { + embeddedImageIds.insert(embImage.id()); } std::set xmlImageIdSet(xmlImageIds.begin(), xmlImageIds.end()); // Check for IDs in registry but not in XML - for (const auto& registryId : registryImageIds) { - if (xmlImageIdSet.find(registryId) == xmlImageIdSet.end()) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + registryId + "' in registry but not in XML"); + // Check for IDs in embedded images but not in XML + for (const auto& embeddedId : embeddedImageIds) { + if (xmlImageIdSet.find(embeddedId) == xmlImageIdSet.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + embeddedId + "' in embedded images but not in XML"); inconsistencyCount++; } } - // Check for IDs in XML but not in registry + // Check for IDs in XML but not in embedded images for (const auto& xmlId : xmlImageIdSet) { - if (registryImageIds.find(xmlId) == registryImageIds.end()) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + xmlId + "' in XML but not in registry"); + if (embeddedImageIds.find(xmlId) == embeddedImageIds.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + xmlId + "' in XML but not in embedded images"); inconsistencyCount++; } } // Check 3: Relationship ID consistency - std::set registryRelIds; - for (const auto& info : imageInfos) { - registryRelIds.insert(info.relationshipId); + std::set embeddedRelIds; + for (const auto& embImage : getEmbImages()) { + embeddedRelIds.insert(embImage.relationshipId()); } std::set xmlRelIdSet(xmlRelationshipIds.begin(), xmlRelationshipIds.end()); - // Check for relationship IDs in registry but not in XML - for (const auto& registryRelId : registryRelIds) { - if (xmlRelIdSet.find(registryRelId) == xmlRelIdSet.end()) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] relationship ID '" + registryRelId + "' in registry but not in XML"); + // Check for relationship IDs in embedded images but not in XML + for (const auto& embeddedRelId : embeddedRelIds) { + if (xmlRelIdSet.find(embeddedRelId) == xmlRelIdSet.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] relationship ID '" + embeddedRelId + "' in embedded images but not in XML"); inconsistencyCount++; } } - // Check for relationship IDs in XML but not in registry + // Check for relationship IDs in XML but not in embedded images for (const auto& xmlRelId : xmlRelIdSet) { - if (registryRelIds.find(xmlRelId) == registryRelIds.end()) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] relationship ID '" + xmlRelId + "' in XML but not in registry"); + if (embeddedRelIds.find(xmlRelId) == embeddedRelIds.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] relationship ID '" + xmlRelId + "' in XML but not in embedded images"); inconsistencyCount++; } } @@ -2802,10 +2794,10 @@ int XLWorksheet::verifyImagesVectorConsistency(std::string* dbgMsg) const // Check if DrawingML is valid if (!m_drawingML.valid()) { - // If DrawingML is invalid but we have images in m_images, that's an inconsistency - if (!m_images.empty()) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] DrawingML is invalid but m_images has " + - std::to_string(m_images.size()) + " entries"); + // If DrawingML is invalid but we have images in m_embImages, that's an inconsistency + if (!getEmbImages().empty()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] DrawingML is invalid but m_embImages has " + + std::to_string(getEmbImages().size()) + " entries"); inconsistencyCount++; } return inconsistencyCount; // No further checks possible @@ -2816,24 +2808,31 @@ int XLWorksheet::verifyImagesVectorConsistency(std::string* dbgMsg) const XMLNode rootNode = m_drawingML.getRootNode(); if (rootNode.empty()) { // Empty root node is OK if there are no images - if (!m_images.empty()) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] DrawingML root node is empty but m_images has " + - std::to_string(m_images.size()) + " entries"); + if (!getEmbImages().empty()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] DrawingML root node is empty but m_embImages has " + + std::to_string(getEmbImages().size()) + " entries"); inconsistencyCount++; } return inconsistencyCount; } - // Count anchors in XML + // Count anchors in XML and extract detailed information size_t xmlAnchorCount = 0; std::vector xmlImageIds; + std::vector xmlRelationshipIds; + std::vector xmlAnchorTypes; for (auto anchor : rootNode.children()) { - if (matchesElementName(anchor.name(), "oneCellAnchor") || - matchesElementName(anchor.name(), "twoCellAnchor")) { + // Check anchor type using enum conversion + XLImageAnchorType anchorType = XLImageAnchorUtils::stringToAnchorType(anchor.name()); + if (anchorType != XLImageAnchorType::Unknown) { xmlAnchorCount++; + // Extract anchor type + xmlAnchorTypes.push_back(anchorType); + // Extract image ID from XML + std::string xmlImageId; XMLNode pic = anchor.child("xdr:pic"); if (!pic.empty()) { XMLNode nvPicPr = pic.child("xdr:nvPicPr"); @@ -2842,7 +2841,24 @@ int XLWorksheet::verifyImagesVectorConsistency(std::string* dbgMsg) const if (!cNvPr.empty()) { XMLAttribute idAttr = cNvPr.attribute("id"); if (!idAttr.empty()) { - xmlImageIds.push_back(idAttr.value()); + xmlImageId = idAttr.value(); + xmlImageIds.push_back(xmlImageId); + } + } + } + } + + // Extract relationship ID from XML + std::string xmlRelId; + if (!pic.empty()) { + XMLNode blipFill = pic.child("xdr:blipFill"); + if (!blipFill.empty()) { + XMLNode blip = blipFill.child("a:blip"); + if (!blip.empty()) { + XMLAttribute embedAttr = blip.attribute("r:embed"); + if (!embedAttr.empty()) { + xmlRelId = embedAttr.value(); + xmlRelationshipIds.push_back(xmlRelId); } } } @@ -2851,40 +2867,208 @@ int XLWorksheet::verifyImagesVectorConsistency(std::string* dbgMsg) const } // Check 1: Anchor count consistency - if (xmlAnchorCount != m_images.size()) { + if (xmlAnchorCount != getEmbImages().size()) { appendDbgMsg(dbgMsg, "[" + worksheetName + "] XML anchor count (" + std::to_string(xmlAnchorCount) + - ") does not match m_images count (" + std::to_string(m_images.size()) + ")"); + ") does not match m_embImages count (" + std::to_string(getEmbImages().size()) + ")"); inconsistencyCount++; } // Check 2: Image ID consistency std::set mImagesIds; - for (const auto& image : m_images) { - if (!image.id().empty()) { - mImagesIds.insert(image.id()); + for (const auto& embImage : getEmbImages()) { + if (!embImage.id().empty()) { + mImagesIds.insert(embImage.id()); } } std::set xmlImageIdSet(xmlImageIds.begin(), xmlImageIds.end()); - // Check for IDs in m_images but not in XML + // Check for IDs in m_embImages but not in XML for (const auto& mImageId : mImagesIds) { if (xmlImageIdSet.find(mImageId) == xmlImageIdSet.end()) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + mImageId + "' in m_images but not in XML"); + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + mImageId + "' in m_embImages but not in XML"); inconsistencyCount++; } } - // Check for IDs in XML but not in m_images + // Check for IDs in XML but not in m_embImages for (const auto& xmlId : xmlImageIdSet) { if (mImagesIds.find(xmlId) == mImagesIds.end()) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + xmlId + "' in XML but not in m_images"); + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image ID '" + xmlId + "' in XML but not in m_embImages"); inconsistencyCount++; } } + // Check 3: Detailed cross-reference validation for each embedded image + for (size_t i = 0; i < getEmbImages().size() && i < xmlImageIds.size(); ++i) { + const auto& embImage = getEmbImages()[i]; + const std::string& xmlImageId = xmlImageIds[i]; + const std::string& xmlRelId = (i < xmlRelationshipIds.size()) ? xmlRelationshipIds[i] : ""; + XLImageAnchorType xmlAnchorType = (i < xmlAnchorTypes.size()) ? xmlAnchorTypes[i] : XLImageAnchorType::Unknown; + + // Check image ID match + if (embImage.id() != xmlImageId) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image " + std::to_string(i) + + " ID mismatch: m_embImages='" + embImage.id() + "', XML='" + xmlImageId + "'"); + inconsistencyCount++; + } + + // Check relationship ID match + if (!xmlRelId.empty() && embImage.relationshipId() != xmlRelId) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image " + std::to_string(i) + + " relationship ID mismatch: m_embImages='" + embImage.relationshipId() + "', XML='" + xmlRelId + "'"); + inconsistencyCount++; + } + + // Check anchor type match + if (xmlAnchorType != XLImageAnchorType::Unknown && embImage.getImageAnchor().anchorType != xmlAnchorType) { + std::string embAnchorTypeStr = XLImageAnchorUtils::anchorTypeToString(embImage.getImageAnchor().anchorType); + std::string xmlAnchorTypeStr = XLImageAnchorUtils::anchorTypeToString(xmlAnchorType); + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image " + std::to_string(i) + + " anchor type mismatch: m_embImages='" + embAnchorTypeStr + "', XML='" + xmlAnchorTypeStr + "'"); + inconsistencyCount++; + } + + // Check if embedded image has valid XLImage object + if (!embImage.getImage()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image " + std::to_string(i) + + " has no associated XLImage object"); + inconsistencyCount++; + } else { + // Check package path format + std::string packagePath = embImage.getImage()->filePackagePath(); + if (!packagePath.empty() && packagePath.find("xl/media/") != 0) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image " + std::to_string(i) + + " package path '" + packagePath + "' does not start with 'xl/media/'"); + inconsistencyCount++; + } + } + } + + // Check 4: Relationship ID uniqueness + std::set uniqueRelIds; + for (const auto& relId : xmlRelationshipIds) { + if (!relId.empty()) { + if (uniqueRelIds.find(relId) != uniqueRelIds.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] duplicate relationship ID '" + relId + "' in XML"); + inconsistencyCount++; + } else { + uniqueRelIds.insert(relId); + } + } + } + + // Check 5: Image ID uniqueness + std::set uniqueImageIds; + for (const auto& imgId : xmlImageIds) { + if (!imgId.empty()) { + if (uniqueImageIds.find(imgId) != uniqueImageIds.end()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] duplicate image ID '" + imgId + "' in XML"); + inconsistencyCount++; + } else { + uniqueImageIds.insert(imgId); + } + } + } + + // Check 6: Package path consistency with relationship XML + try { + // Get the drawing relationships filename + std::string relsFilename = getDrawingRelsFilename(); + + // Check if relationships file exists + if (parentDoc().hasRelationshipsFile(relsFilename)) { + // Use in-memory XML data instead of parsing file content + XLXmlData* relsXmlData = const_cast(parentDoc()).getRelationshipsXmlData(relsFilename); + if (relsXmlData && relsXmlData->valid()) { + // Parse the relationships XML from in-memory data + XMLDocument* relsDoc = relsXmlData->getXmlDocument(); + if (relsDoc && relsDoc->document_element()) { + // Create a map of relationship ID to target path + std::map relIdToTarget; + for (auto rel : relsDoc->document_element().children("Relationship")) { + std::string relId = rel.attribute("Id").value(); + std::string target = rel.attribute("Target").value(); + std::string type = rel.attribute("Type").value(); + + // Only process image relationships + if (type == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image") { + if (!relId.empty() && !target.empty()) { + relIdToTarget[relId] = target; + } + } + } + + // Check each embedded image's package path against XML target + for (size_t i = 0; i < getEmbImages().size() && i < xmlRelationshipIds.size(); ++i) { + const auto& embImage = getEmbImages()[i]; + const std::string& xmlRelId = xmlRelationshipIds[i]; + + if (!xmlRelId.empty() && embImage.getImage()) { + auto targetIt = relIdToTarget.find(xmlRelId); + if (targetIt != relIdToTarget.end()) { + std::string xmlTarget = targetIt->second; + + // Convert XML target path to expected package path + // XML target is like "../media/image1.png", package path should be "xl/media/image1.png" + std::string expectedPackagePath; + if (xmlTarget.find("../media/") == 0) { + expectedPackagePath = "xl/media/" + xmlTarget.substr(9); // Remove "../media/" + } else if (xmlTarget.find("media/") == 0) { + expectedPackagePath = "xl/media/" + xmlTarget.substr(6); // Remove "media/" + } else { + // Handle other possible formats + size_t lastSlash = xmlTarget.find_last_of('/'); + if (lastSlash != std::string::npos) { + expectedPackagePath = "xl/media/" + xmlTarget.substr(lastSlash + 1); + } else { + expectedPackagePath = "xl/media/" + xmlTarget; + } + } + + std::string actualPackagePath = embImage.getImage()->filePackagePath(); + + if (actualPackagePath != expectedPackagePath) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image " + std::to_string(i) + + " package path mismatch: XLImage='" + actualPackagePath + + "', XML target='" + xmlTarget + "' (expected='" + expectedPackagePath + "')"); + inconsistencyCount++; + } + } else { + if (relIdToTarget.empty()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image " + std::to_string(i) + + " relationship ID '" + xmlRelId + "' not found in relationships XML (file is empty)"); + } else { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] image " + std::to_string(i) + + " relationship ID '" + xmlRelId + "' not found in relationships XML"); + } + inconsistencyCount++; + } + } + } + } else { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] relationships XML document is invalid"); + inconsistencyCount++; + } + } else { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] relationships XML data is not available"); + inconsistencyCount++; + } + } else { + // No relationships file - this might be OK if there are no images + if (!getEmbImages().empty()) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] has " + std::to_string(getEmbImages().size()) + + " images but no relationships file"); + inconsistencyCount++; + } + } + } catch (const std::exception& e) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] error during package path consistency check: " + std::string(e.what())); + inconsistencyCount++; + } + } catch (const std::exception& e) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] error during m_images consistency check: " + std::string(e.what())); + appendDbgMsg(dbgMsg, "[" + worksheetName + "] error during m_embImages consistency check: " + std::string(e.what())); inconsistencyCount++; } @@ -2897,19 +3081,19 @@ int XLWorksheet::verifyBinaryImageData(std::string* dbgMsg) const std::string worksheetName = name(); // Only validate if we have images to check - if (m_imageRegistry.empty() && m_images.empty()) { + if (getEmbImages().empty()) { return issueCount; // No images to validate } try { // Get the drawing relationships filename - std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + std::string relsFilename = getDrawingRelsFilename(); // Check if relationships file exists in archive if (!parentDoc().hasRelationshipsFile(relsFilename)) { // Check if we have images but no relationships file - this is a problem - if (m_images.size() > 0) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] has " + std::to_string(m_images.size()) + " images but no relationships file"); + if (getEmbImages().size() > 0) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] has " + std::to_string(getEmbImages().size()) + " images but no relationships file"); issueCount++; } // No relationships file means no binary data to validate @@ -2920,8 +3104,8 @@ int XLWorksheet::verifyBinaryImageData(std::string* dbgMsg) const std::string relsContent = parentDoc().readRelationshipsFile(relsFilename); if (relsContent.empty()) { // Check if we have images but empty relationships file - this is a problem - if (m_images.size() > 0) { - appendDbgMsg(dbgMsg, "[" + worksheetName + "] has " + std::to_string(m_images.size()) + " images but relationships file is empty"); + if (getEmbImages().size() > 0) { + appendDbgMsg(dbgMsg, "[" + worksheetName + "] has " + std::to_string(getEmbImages().size()) + " images but relationships file is empty"); issueCount++; } return issueCount; // Empty relationships file means no binary data to validate @@ -2951,7 +3135,7 @@ int XLWorksheet::verifyBinaryImageData(std::string* dbgMsg) const // Verify that each referenced binary file actually exists in the archive for (const auto& imagePath : referencedImagePaths) { // Convert relative path to absolute path for archive check - std::string absolutePath = "xl/media/" + imagePath.substr(imagePath.find_last_of('/') + 1); + std::string absolutePath = getImageMediaPathFromFilename(imagePath); try { std::string imageContent = parentDoc().readFile(absolutePath); @@ -2977,24 +3161,23 @@ std::vector XLWorksheet::getImageIDs() const { std::vector imageIds; - // Get image IDs from m_images vector - for (const auto& image : m_images) { - if (!image.id().empty()) { - imageIds.push_back(image.id()); + // Get image IDs from m_embImages vector + for (const auto& embImage : getEmbImages()) { + if (!embImage.id().empty()) { + imageIds.push_back(embImage.id()); } } - // Get image IDs from the image registry + // Get image IDs from embedded images if (m_drawingML.valid()) { try { - auto imageInfos = getImageInfos(); - for (const auto& info : imageInfos) { - if (!info.imageId.empty()) { - imageIds.push_back(info.imageId); + for (const auto& embImage : getEmbImages()) { + if (!embImage.id().empty()) { + imageIds.push_back(embImage.id()); } } } catch (const std::exception&) { - // Registry access failed, continue with m_images IDs only + // Embedded images access failed, continue with empty list } } @@ -3008,5 +3191,75 @@ std::vector XLWorksheet::getImageIDs() const return imageIds; } +/** + * @brief Get the drawing relationships filename for this worksheet + * @return The relationships filename (e.g., "xl/drawings/_rels/drawing1.xml.rels") + */ +std::string XLWorksheet::getDrawingRelsFilename() const +{ + return "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; +} + +/** + * @brief Get the drawing filename for this worksheet + * @return The drawing filename (e.g., "xl/drawings/drawing1.xml") + */ +std::string XLWorksheet::getDrawingFilename() const +{ + return "xl/drawings/drawing" + std::to_string(sheetXmlNumber()) + ".xml"; +} + +/** + * @brief Get the image media filename for an embedded image + * @param imageId The image ID + * @param extension The image file extension (e.g., ".png", ".jpg") + * @return The image media filename (e.g., "xl/media/image_1.png") + */ +std::string XLWorksheet::getImageMediaFilename(const std::string& imageId, const std::string& extension) +{ + return "xl/media/image_" + imageId + extension; +} + +/** + * @brief Convert a relative image path to absolute archive path + * @param relativePath The relative path (e.g., "../media/image_1.png" or "media/image_1.png") + * @return The absolute archive path (e.g., "xl/media/image_1.png") + */ +std::string XLWorksheet::getAbsoluteImagePath(const std::string& relativePath) +{ + if (relativePath.substr(0, 3) == "../") { + return "xl/" + relativePath.substr(3); // Convert "../media/image_1.png" to "xl/media/image_1.png" + } else if (relativePath.substr(0, 4) != "xl/") { + return "xl/" + relativePath; // Add xl/ prefix if missing + } + return relativePath; // Already absolute +} + +/** + * @brief Extract filename from a full image path + * @param imagePath The full image path + * @return The absolute archive path with extracted filename (e.g., "xl/media/image_1.png") + */ +std::string XLWorksheet::getImageMediaPathFromFilename(const std::string& imagePath) +{ + return "xl/media/" + imagePath.substr(imagePath.find_last_of('/') + 1); +} + +const std::vector& XLWorksheet::getEmbImages() const +{ + const std::vector& embImages = const_cast(this)->getEmbImages(); + return embImages; +} + +std::vector& XLWorksheet::getEmbImages() +{ + if (!m_embImages) { + m_embImages = parentDoc().getImageManager().getEmbImgsForSheetName(name()); + if(!m_embImages){ + throw XLInternalError( std::string("failed to initialize m_embImages for") + name() ); + } + } + return *m_embImages; +} + // Static const member definitions for XLWorksheet -const XLImageInfo XLWorksheet::m_emptyImageInfo{}; diff --git a/OpenXLSX/sources/XLWorksheetImageQuery.cpp b/OpenXLSX/sources/XLWorksheetImageQuery.cpp index 3a66484c..7133ca6b 100644 --- a/OpenXLSX/sources/XLWorksheetImageQuery.cpp +++ b/OpenXLSX/sources/XLWorksheetImageQuery.cpp @@ -50,9 +50,11 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM. #include // ===== OpenXLSX Includes ===== // +#include "XLImage.hpp" #include "XLSheet.hpp" #include "XLCellReference.hpp" #include "XLDocument.hpp" +#include "XLDrawingML.hpp" namespace OpenXLSX { @@ -61,93 +63,84 @@ namespace OpenXLSX //---------------------------------------------------------------------------------------------------------------------- /** - * @brief Get all image information in the worksheet - * @return Const reference to vector of XLImageInfo objects + * @brief Get specific embedded image by its image ID */ - const std::vector& XLWorksheet::getImageInfos() const + const XLEmbeddedImage& XLWorksheet::getEmbImageByImageID(const std::string& imageId) const { - // Auto-refresh registry if empty - if (m_imageRegistry.empty()) { - refreshImageRegistry(); - } - - return m_imageRegistry; - } - - /** - * @brief Get specific image information by its ID - * @param imageId The unique image identifier - * @return Const reference to XLImageInfo object, or empty XLImageInfo if not found - */ - const XLImageInfo& XLWorksheet::getImageInfoByImageID(const std::string& imageId) const - { - // Auto-refresh registry if empty - if (m_imageRegistry.empty()) { - refreshImageRegistry(); + const std::vector& embImages = getEmbImages(); + const XLEmbeddedImage *result = nullptr; + std::vector::const_iterator embImgItr = embImages.begin(); + for( ; (embImages.end() != embImgItr) && (nullptr == result); ++embImgItr ){ + const XLEmbeddedImage& embImage = *embImgItr; + if( embImage.id() == imageId ){ + result = &embImage; + } } #if (defined(_DEBUG) || !defined(NDEBUG)) - // Check for unique image IDs - std::string dbgMsg; - int duplicateCount = verifyUniqueImageIDs(&dbgMsg); - if (duplicateCount > 0) { - std::cout << "ERROR: getImageInfoByImageID() found " << duplicateCount - << " duplicate image ID(s): " << dbgMsg << std::endl; + /* check for duplicate ID */ + for( ; embImages.end() != embImgItr; ++embImgItr ){ + const XLEmbeddedImage& embImage = *embImgItr; + if( embImage.id() == imageId ){ + std::cerr << "WARNING: Worksheet '" << name() + << "' has duplicate imageId = '" << imageId << std::endl; + } } #endif - - // Linear search for the image ID - for (const auto& imgInfo : m_imageRegistry) { - if (imgInfo.imageId == imageId) { - return imgInfo; + // Return empty embedded image if not found + static const XLEmbeddedImage emptyEmbImage{}; + if( nullptr == result ){ + result = &emptyEmbImage; } - } - - // Return empty image info if not found - return m_emptyImageInfo; + + return *result; } /** - * @brief Get specific image information by its relationship ID - * @param relationshipId The relationship ID in drawing.xml.rels - * @return Const reference to XLImageInfo object, or empty XLImageInfo if not found + * @brief Get specific embedded image by its relationship ID */ - const XLImageInfo& XLWorksheet::getImageInfoByRelationshipId(const std::string& relationshipId) const + const XLEmbeddedImage& XLWorksheet::getEmbImageByRelationshipId(const std::string& relationshipId) const { - // Auto-refresh registry if empty - if (m_imageRegistry.empty()) { - refreshImageRegistry(); + const std::vector& embImages = getEmbImages(); + const XLEmbeddedImage *result = nullptr; + std::vector::const_iterator embImgItr = embImages.begin(); + for( ; (embImages.end() != embImgItr) && (nullptr == result); ++embImgItr ){ + const XLEmbeddedImage& embImage = *embImgItr; + if( embImage.relationshipId() == relationshipId ){ + result = &embImage; + } } - // Linear search for the relationship ID - for (const auto& imgInfo : m_imageRegistry) { - if (imgInfo.relationshipId == relationshipId) { - return imgInfo; +#if (defined(_DEBUG) || !defined(NDEBUG)) + /* check for duplicate ID */ + for( ; embImages.end() != embImgItr; ++embImgItr ){ + const XLEmbeddedImage& embImage = *embImgItr; + if( embImage.relationshipId() == relationshipId ){ + std::cerr << "WARNING: Worksheet '" << name() + << "' has duplicate relationshipId = '" << relationshipId << std::endl; } } - - // Return empty image info if not found - return m_emptyImageInfo; +#endif + // Return empty embedded image if not found + static const XLEmbeddedImage emptyEmbImage{}; + if( nullptr == result ){ + result = &emptyEmbImage; + } + + return *result; } /** - * @brief Get all image information at a specific cell - * @param cellRef The cell reference (e.g., "A1", "B5") - * @return Vector of XLImageInfo objects at that cell + * @brief Get all embedded images at a specific cell */ - std::vector XLWorksheet::getImageInfosAtCell(const std::string& cellRef) const + std::vector XLWorksheet::getEmbImagesAtCell(const std::string& cellRef) const { - // Auto-refresh registry if empty - if (m_imageRegistry.empty()) { - refreshImageRegistry(); - } - - std::vector result; + std::vector result; - // Filter images by cell reference - for (const auto& imgInfo : m_imageRegistry) { - if (imgInfo.anchorCell == cellRef) { - result.push_back(imgInfo); + // Filter embedded images by cell reference + for (const auto& embImage : getEmbImages()) { + if (embImage.anchorCell() == cellRef) { + result.push_back(embImage); } } @@ -155,42 +148,34 @@ namespace OpenXLSX } /** - * @brief Get all image information in a specific range - * @param cellRange The cell range (e.g., "A1:B5", "C10:D20") - * @return Vector of XLImageInfo objects in that range + * @brief Get all embedded images in a specific range */ - std::vector XLWorksheet::getImageInfosInRange(const std::string& cellRange) const + std::vector XLWorksheet::getEmbImagesInRange(const std::string& cellRange) const { - // Auto-refresh registry if empty - if (m_imageRegistry.empty()) { - refreshImageRegistry(); - } - - std::vector result; - - // Parse the range - size_t colonPos = cellRange.find(':'); - if (colonPos == std::string::npos) { - // Single cell range - return getImageInfosAtCell(cellRange); - } - - std::string topLeft = cellRange.substr(0, colonPos); - std::string bottomRight = cellRange.substr(colonPos + 1); + std::vector result; try { - XLCellReference topLeftRef(topLeft); - XLCellReference bottomRightRef(bottomRight); - - // Filter images by range - for (const auto& imgInfo : m_imageRegistry) { - if (!imgInfo.anchorCell.empty()) { - XLCellReference imgRef(imgInfo.anchorCell); + // Parse the range + size_t colonPos = cellRange.find(':'); + if (colonPos == std::string::npos) { + // Single cell range + return getEmbImagesAtCell(cellRange); + } else { + // Range format + std::string topLeftStr = cellRange.substr(0, colonPos); + std::string bottomRightStr = cellRange.substr(colonPos + 1); + + XLCellReference topLeftRef(topLeftStr); + XLCellReference bottomRightRef(bottomRightStr); + + // Filter embedded images by range + for (const auto& embImage : getEmbImages()) { + XLCellReference imgRef(embImage.anchorCell()); // Check if image is within range if (imgRef.row() >= topLeftRef.row() && imgRef.row() <= bottomRightRef.row() && imgRef.column() >= topLeftRef.column() && imgRef.column() <= bottomRightRef.column()) { - result.push_back(imgInfo); + result.push_back(embImage); } } } @@ -202,29 +187,12 @@ namespace OpenXLSX } /** - * @brief Get iterator to the beginning of the image information collection - * @return Const iterator to the first image info + * @brief Get iterator to the end of the embedded images collection + * @return Const iterator to the end of the embedded images collection */ - std::vector::const_iterator XLWorksheet::imageInfosBegin() const + std::vector::const_iterator XLWorksheet::embImagesEnd() const { - // Auto-refresh registry if empty - if (m_imageRegistry.empty()) { - refreshImageRegistry(); - } - return m_imageRegistry.begin(); - } - - /** - * @brief Get iterator to the end of the image information collection - * @return Const iterator to the end of the image information collection - */ - std::vector::const_iterator XLWorksheet::imageInfosEnd() const - { - // Auto-refresh registry if empty - if (m_imageRegistry.empty()) { - refreshImageRegistry(); - } - return m_imageRegistry.end(); + return getEmbImages().end(); } //---------------------------------------------------------------------------------------------------------------------- @@ -238,46 +206,31 @@ namespace OpenXLSX */ bool XLWorksheet::removeImageByImageID(const std::string& imageId) { - // Auto-refresh registry if empty - if (m_imageRegistry.empty()) { - refreshImageRegistry(); - } - - // Find the image in the registry - auto it = std::find_if(m_imageRegistry.begin(), m_imageRegistry.end(), - [&imageId](const XLImageInfo& img) { return img.imageId == imageId; }); + // Find the image in embedded images + auto it = std::find_if(getEmbImages().begin(), getEmbImages().end(), + [&imageId](const XLEmbeddedImage& img) { return img.id() == imageId; }); - if (it == m_imageRegistry.end()) { + if (it == getEmbImages().end()) { return false; // Image not found } - // Store the relationship ID before removing from registry - std::string relationshipId = it->relationshipId; + // Store the relationship ID before removing + std::string relationshipId = it->relationshipId(); - // Get image path BEFORE removing relationships (we need the relationship to find the file path) - std::string imagePath = getImagePathFromRelationship(relationshipId); - // Remove from DrawingML XML (in-memory XML document) bool xmlRemoved = removeImageFromDrawingXML(relationshipId); // Remove from relationships file (in-memory XML document) bool relsRemoved = removeImageFromRelationships(relationshipId); - // Remove image file from archive (delete binary file entry if no other references) - use the path we got earlier - bool fileProcessed = removeImageFileIfUnreferenced(imagePath); - // Remove from registry (in-memory cache) - m_imageRegistry.erase(it); + // Remove from m_embImages vector (in-memory cache) + getEmbImages().erase(it); - // Remove from m_images vector (in-memory cache) - auto imagesIt = std::find_if(m_images.begin(), m_images.end(), - [&imageId](const XLImage& img) { return img.id() == imageId; }); - if (imagesIt != m_images.end()) { - m_images.erase(imagesIt); - } + // Note: XLDocument.getImageManager()->prune() will delete binary image data files from archive as needed. // Return true if all critical operations succeeded - return xmlRemoved && relsRemoved && fileProcessed; + return xmlRemoved && relsRemoved; } /** @@ -287,46 +240,28 @@ namespace OpenXLSX */ bool XLWorksheet::removeImageByRelationshipId(const std::string& relationshipId) { - // Auto-refresh registry if empty - if (m_imageRegistry.empty()) { - refreshImageRegistry(); - } + // Find the image in embedded images + auto it = std::find_if(getEmbImages().begin(), getEmbImages().end(), + [&relationshipId](const XLEmbeddedImage& img) { return img.relationshipId() == relationshipId; }); - // Find the image in the registry - auto it = std::find_if(m_imageRegistry.begin(), m_imageRegistry.end(), - [&relationshipId](const XLImageInfo& img) { return img.relationshipId == relationshipId; }); - - if (it == m_imageRegistry.end()) { + if (it == getEmbImages().end()) { return false; // Image not found } - // Store the image ID before removing from registry - std::string imageId = it->imageId; - - // Get image path BEFORE removing relationships (we need the relationship to find the file path) - std::string imagePath = getImagePathFromRelationship(relationshipId); - // Remove from DrawingML XML (in-memory XML document) bool xmlRemoved = removeImageFromDrawingXML(relationshipId); // Remove from relationships file (in-memory XML document) bool relsRemoved = removeImageFromRelationships(relationshipId); - // Remove image file from archive (delete binary file entry if no other references) - use the path we got earlier - bool fileProcessed = removeImageFileIfUnreferenced(imagePath); - // Remove from registry (in-memory cache) - m_imageRegistry.erase(it); + // Remove from m_embImages vector (in-memory cache) + getEmbImages().erase(it); - // Remove from m_images vector (in-memory cache) - auto imagesIt = std::find_if(m_images.begin(), m_images.end(), - [&imageId](const XLImage& img) { return img.id() == imageId; }); - if (imagesIt != m_images.end()) { - m_images.erase(imagesIt); - } + // Note: XLDocument.getImageManager()->prune() will delete binary image data files from archive as needed. // Return true if all critical operations succeeded - return xmlRemoved && relsRemoved && fileProcessed; + return xmlRemoved && relsRemoved; } /** @@ -335,23 +270,13 @@ namespace OpenXLSX */ void XLWorksheet::clearImages() { - // Get all image IDs - std::vector imageIds = getImageIDs(); - - // Remove each image by ID - for (const auto& imageId : imageIds) { - if (removeImageByImageID(imageId)) { - } - } - - // Ensure m_images vector is cleared (safety check) - m_images.clear(); + // Ensure m_embImages vector is cleared (safety check) + getEmbImages().clear(); // TODO: Add verification checks to ensure complete cleanup: - // 1. Check that m_imageRegistry is empty - // 2. Check that DrawingML XML has no image nodes - // 3. Check that relationships file has no image relationships - // 4. Check that no image files remain in the archive + // 1. Check that DrawingML XML has no image nodes + // 2. Check that relationships file has no image relationships + // 3. Check that no image files remain in the archive // 5. Verify that getImageIDs() returns empty vector // 6. Verify that imageCount() returns 0 @@ -362,76 +287,6 @@ namespace OpenXLSX // Image Registry Management Implementation //---------------------------------------------------------------------------------------------------------------------- - /** - * @brief Refresh the image registry from DrawingML XML - */ - void XLWorksheet::refreshImageRegistry() const - { - bool done = false; - - // Clear existing registry - m_imageRegistry.clear(); - - // Check if DrawingML is valid - if (!m_drawingML.valid()) { - done = true; // No drawing data available - } - - try { - // Use standard sequential numbering approach (consistent with rest of codebase) - std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNumber()) + ".xml"; - // Get the existing XLXmlData object for the drawing file - // This is the proper way - use the existing XML data structure rather than bypassing it - XLXmlData* xmlData = const_cast(parentDoc()).getDrawingXmlData(drawingFilename); - if (!xmlData) { - done = true; - } - - XMLNode wsDr; - std::string rootName; - if( !done ){ - // Get the underlying XMLDocument (pugi::xml_document) - XMLDocument& xmlDoc = *xmlData->getXmlDocument(); - if (!xmlDoc.document_element()) { - done = true; - } - - // Get the root element (xdr:wsDr) - wsDr = xmlDoc.document_element(); - rootName = wsDr.name(); - } - - // Check for both "wsDr" and "xdr:wsDr" (with namespace prefix) - if( !done ){ - if (rootName != "wsDr" && rootName != "xdr:wsDr") { - done = true; // Invalid root element - } - } - - // Read the relationships file to get image mappings - if( !done ){ - std::map relationshipMap; - readDrawingRelationships(relationshipMap); - - // Process each anchor element - for (auto anchor : wsDr.children()) { - std::string anchorName = anchor.name(); - - if (matchesElementName(anchorName, "oneCellAnchor")) { - processOneCellAnchor(anchor, relationshipMap); - } - else if (matchesElementName(anchorName, "twoCellAnchor")) { - processTwoCellAnchor(anchor, relationshipMap); - } - } - } - } - catch (const std::exception& ) { - // If parsing fails, leave registry empty - m_imageRegistry.clear(); - } - } - /** * @brief Read the drawing relationships file to map relationship IDs to image files * @param relationshipMap Output map of relationship ID to image file path @@ -440,8 +295,8 @@ namespace OpenXLSX { try { // Get the drawing filename - std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNumber()) + ".xml"; - std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + std::string drawingFilename = getDrawingFilename(); + std::string relsFilename = getDrawingRelsFilename(); // Check if relationships file exists if (!parentDoc().hasRelationshipsFile(relsFilename)) { @@ -481,333 +336,127 @@ namespace OpenXLSX } /** - * @brief Populate m_images vector from existing XML data + * @brief Populate m_embImages vector from existing XML data */ void XLWorksheet::populateImagesFromXML() { - // Only populate if m_images is empty to avoid duplicates - if (!m_images.empty()) { + // Only populate if m_embImages is empty to avoid duplicates + if (!getEmbImages().empty()) { return; } - // First refresh the registry to get current XML data - refreshImageRegistry(); - - // Get relationship map to find image file paths - std::map relationshipMap; - readDrawingRelationships(relationshipMap); - - // Convert registry entries to XLImage objects - for (const auto& imgInfo : m_imageRegistry) { - try { - // Find the image file path using the relationship ID - auto it = relationshipMap.find(imgInfo.relationshipId); - if (it == relationshipMap.end()) { - continue; // Skip if relationship not found - } - - std::string imagePath = it->second; - - // Convert relative path to absolute path if needed - if (imagePath.substr(0, 3) == "../") { - imagePath = "xl/" + imagePath.substr(3); // Convert "../media/image_1.png" to "xl/media/image_1.png" - } else if (imagePath.substr(0, 4) != "xl/") { - imagePath = "xl/" + imagePath; // Add xl/ prefix if missing - } - - // Create XLImage object and load from archive - XLImage image; - - // Load binary data from archive - std::string binaryData = parentDoc().readFile(imagePath); - if (binaryData.empty()) { - continue; // Skip if binary data not found - } - - // Convert string to vector - std::vector imageData(binaryData.begin(), binaryData.end()); - - // Determine MIME type from file extension - std::string mimeType = "image/png"; // default - if (imagePath.find(".jpg") != std::string::npos || imagePath.find(".jpeg") != std::string::npos) { - mimeType = "image/jpeg"; - } else if (imagePath.find(".gif") != std::string::npos) { - mimeType = "image/gif"; - } else if (imagePath.find(".bmp") != std::string::npos) { - mimeType = "image/bmp"; - } - - // Load image data - if (!image.loadFromData(imageData, mimeType, imgInfo.imageId)) { - continue; // Skip if loading failed - } - - // Set display dimensions from registry - image.setDisplayWidth(imgInfo.displayWidthEMUs); - image.setDisplayHeight(imgInfo.displayHeightEMUs); - - // Add to m_images vector - m_images.push_back(image); - } - catch (const std::exception&) { - // If creating XLImage fails, skip this entry - // This ensures we don't crash on malformed data - } + // Check if DrawingML is valid (was initialized because images exist) + if (!m_drawingML.valid()) { + return; // No images to populate } - } - /** - * @brief Process a oneCellAnchor element and add to registry - * @param anchor The oneCellAnchor XML node - * @param relationshipMap Map of relationship IDs to image files - */ - void XLWorksheet::processOneCellAnchor(const pugi::xml_node& anchor, const std::map& relationshipMap) const - { try { - // Extract cell position - auto from = anchor.child("xdr:from"); - if (from.empty()) { - from = anchor.child("from"); // Try without namespace prefix - } - if (from.empty()) { - return; - } - - auto colNode = from.child("xdr:col"); - if (colNode.empty()) { - colNode = from.child("col"); // Try without namespace prefix - } - auto rowNode = from.child("xdr:row"); - if (rowNode.empty()) { - rowNode = from.child("row"); // Try without namespace prefix - } - - int col = colNode.text().as_int(); - int row = rowNode.text().as_int(); - std::string cellRef = XLCellReference(row + 1, col + 1).address(); // Convert to 1-based - - // Extract dimensions - pugi::xml_node ext; - if (ext.empty()) { - // Try looking inside xdr:pic > xdr:spPr > a:xfrm - auto pic = anchor.child("xdr:pic"); - if (!pic.empty()) { - auto spPr = pic.child("xdr:spPr"); - if (!spPr.empty()) { - auto xfrm = spPr.child("a:xfrm"); - if (!xfrm.empty()) { - ext = xfrm.child("a:ext"); - } - } - } - if (ext.empty()) { - auto ext = anchor.child("xdr:ext"); - } - if (ext.empty()) { - ext = anchor.child("a:ext"); // Try with a namespace prefix - } - if (ext.empty()) { - ext = anchor.child("ext"); // Try without namespace prefix - } - - } - if (ext.empty()) { - return; - } - - // TODO: Double check the proper interpretation of cx/cy values based on whether - // the ext element is (intrinsic shape size) or (anchored size) - uint32_t widthEMUs = ext.attribute("cx").as_uint(); - uint32_t heightEMUs = ext.attribute("cy").as_uint(); - - // Extract image information - auto pic = anchor.child("xdr:pic"); - if (pic.empty()) { - pic = anchor.child("pic"); // Try without namespace prefix - } - if (pic.empty()) { + // Use standard sequential numbering approach (consistent with rest of codebase) + std::string drawingFilename = getDrawingFilename(); + // Get the existing XLXmlData object for the drawing file + // This is the proper way - use the existing XML data structure rather than bypassing it + XLXmlData* xmlData = const_cast(parentDoc()).getDrawingXmlData(drawingFilename); + if (!xmlData) { return; } - auto blipFill = pic.child("xdr:blipFill"); - if (blipFill.empty()) { - blipFill = pic.child("a:blipFill"); // Try with a namespace prefix - } - if (blipFill.empty()) { - blipFill = pic.child("blipFill"); // Try without namespace prefix - } - if (blipFill.empty()) { + XMLNode wsDr; + std::string rootName; + // Get the underlying XMLDocument (pugi::xml_document) + XMLDocument& xmlDoc = *xmlData->getXmlDocument(); + if (!xmlDoc.document_element()) { return; } - auto blip = blipFill.child("a:blip"); - if (blip.empty()) { - blip = blipFill.child("blip"); // Try without namespace prefix - } - if (blip.empty()) { - return; + // Get the root element (xdr:wsDr) + wsDr = xmlDoc.document_element(); + rootName = wsDr.name(); + + // Check for both "wsDr" and "xdr:wsDr" (with namespace prefix) + if (rootName != "wsDr" && rootName != "xdr:wsDr") { + return; // Invalid root element } - std::string relationshipId = blip.attribute("r:embed").value(); - if (relationshipId.empty()) { - return; - } + // Read the relationships file to get image mappings + std::map relationshipMap; + readDrawingRelationships(relationshipMap); - // Get image file path from relationship - auto it = relationshipMap.find(relationshipId); - if (it == relationshipMap.end()) { - return; + // Process each anchor element directly + for (auto anchor : wsDr.children()) { + std::string anchorName = anchor.name(); + // Check anchor type using enum conversion + XLImageAnchorType anchorType = XLImageAnchorUtils::stringToAnchorType(anchorName); + if (anchorType != XLImageAnchorType::Unknown) { + // Parse anchor using XLDrawingML utility function + XLImageAnchor anchorInfo; + if (XLDrawingML::parseXMLNode(anchor, &anchorInfo)) { + // Process this anchor directly into XLEmbeddedImage + processAnchorToEmbeddedImage(anchorInfo, relationshipMap); + } + } } - - std::string imagePath = it->second; - - // Extract image ID from XML (not filename) - more reliable - std::string imageId = extractImageIdFromXML(pic); - - // Create XLImageInfo - XLImageInfo imgInfo; - imgInfo.imageId = imageId; - imgInfo.relationshipId = relationshipId; - imgInfo.anchorCell = cellRef; - imgInfo.anchorType = "oneCellAnchor"; - imgInfo.displayWidthEMUs = widthEMUs; - imgInfo.displayHeightEMUs = heightEMUs; - - // Convert EMUs to pixels (approximate) - imgInfo.widthPixels = emusToPixels(widthEMUs); - imgInfo.heightPixels = emusToPixels(heightEMUs); - - // Add to registry - m_imageRegistry.push_back(imgInfo); } - catch (const std::exception& ) { - // If processing fails, skip this anchor + catch (const std::exception&) { + // If any error occurs, just return without populating + return; } } /** - * @brief Process a twoCellAnchor element and add to registry - * @param anchor The twoCellAnchor XML node + * @brief Process a single XLImageAnchor into an XLEmbeddedImage and add to m_embImages + * @param anchorInfo The parsed anchor information * @param relationshipMap Map of relationship IDs to image files */ - void XLWorksheet::processTwoCellAnchor(const pugi::xml_node& anchor, const std::map& relationshipMap) const + void XLWorksheet::processAnchorToEmbeddedImage(const XLImageAnchor& anchorInfo, const std::map& relationshipMap) { try { - - // Extract cell position (use 'from' cell as primary anchor) - auto from = anchor.child("xdr:from"); - if (from.empty()) { - from = anchor.child("from"); // Try without namespace prefix - } - if (from.empty()) { - return; + // Find the image file path using the relationship ID + auto it = relationshipMap.find(anchorInfo.relationshipId); + if (it == relationshipMap.end()) { + return; // Skip if relationship not found } - auto colNode = from.child("xdr:col"); - if (colNode.empty()) { - colNode = from.child("col"); // Try without namespace prefix - } - auto rowNode = from.child("xdr:row"); - if (rowNode.empty()) { - rowNode = from.child("row"); // Try without namespace prefix - } + std::string packagePath = it->second; - int col = colNode.text().as_int(); - int row = rowNode.text().as_int(); - std::string cellRef = XLCellReference(row + 1, col + 1).address(); // Convert to 1-based - - // Extract dimensions - auto ext = anchor.child("xdr:ext"); - if (ext.empty()) { - ext = anchor.child("a:ext"); // Try with a namespace prefix - } - if (ext.empty()) { - ext = anchor.child("ext"); // Try without namespace prefix - } - if (ext.empty()) { - // Try looking inside xdr:pic > xdr:spPr > a:xfrm (Excel-generated files) - auto pic = anchor.child("xdr:pic"); - if (!pic.empty()) { - auto spPr = pic.child("xdr:spPr"); - if (!spPr.empty()) { - auto xfrm = spPr.child("a:xfrm"); - if (!xfrm.empty()) { - ext = xfrm.child("a:ext"); - } - } - } - } - if (ext.empty()) { - return; - } - - // TODO: Double check the proper interpretation of cx/cy values based on whether - // the ext element is (intrinsic shape size) or (anchored size) - uint32_t widthEMUs = ext.attribute("cx").as_uint(); - uint32_t heightEMUs = ext.attribute("cy").as_uint(); - - // Extract image information - auto pic = anchor.child("xdr:pic"); - if (pic.empty()) { - pic = anchor.child("pic"); // Try without namespace prefix - } - if (pic.empty()) { - return; - } - - auto blipFill = pic.child("xdr:blipFill"); - if (blipFill.empty()) { - blipFill = pic.child("a:blipFill"); // Try with a namespace prefix - } - if (blipFill.empty()) { - blipFill = pic.child("blipFill"); // Try without namespace prefix - } - if (blipFill.empty()) { - return; - } + // Convert relative path to absolute path if needed + packagePath = getAbsoluteImagePath(packagePath); - auto blip = blipFill.child("a:blip"); - if (blip.empty()) { - blip = blipFill.child("blip"); // Try without namespace prefix - } - if (blip.empty()) { - return; + // Load binary data from archive + std::string binaryData = parentDoc().readFile(packagePath); + if (binaryData.empty()) { + return; // Skip if binary data not found } - - std::string relationshipId = blip.attribute("r:embed").value(); - if (relationshipId.empty()) { - return; + + // Convert string to vector + std::vector imageData(binaryData.begin(), binaryData.end()); + + // Determine MIME type from file extension ot imageData + std::string extension = packagePath.substr(packagePath.find_last_of('.')); + XLMimeType mimeType = XLImageUtils::mimeTypeFromExtension(extension); + if (XLMimeType::Unknown == mimeType) { + mimeType = XLImageUtils::detectMimeTypeEnum(imageData); } - - // Get image file path from relationship - auto it = relationshipMap.find(relationshipId); - if (it == relationshipMap.end()) { + + // Convert enum back to XLContentType for findOrAddImage + XLContentType contentType = XLImageUtils::mimeTypeToContentType(mimeType); + + // Use XLImageManager to find or add the image with existing package path + XLImageShPtr image = parentDoc().getImageManager().findOrAddImage(packagePath, + imageData, contentType); + + if (!image.get()) { return; } - std::string imagePath = it->second; - - // Extract image ID from XML (not filename) - more reliable - std::string imageId = extractImageIdFromXML(pic); - - // Create XLImageInfo - XLImageInfo imgInfo; - imgInfo.imageId = imageId; - imgInfo.relationshipId = relationshipId; - imgInfo.anchorCell = cellRef; - imgInfo.anchorType = "twoCellAnchor"; - imgInfo.displayWidthEMUs = widthEMUs; - imgInfo.displayHeightEMUs = heightEMUs; - - // Convert EMUs to pixels (approximate) - imgInfo.widthPixels = emusToPixels(widthEMUs); - imgInfo.heightPixels = emusToPixels(heightEMUs); - - // Add to registry - m_imageRegistry.push_back(imgInfo); + // Add to embedded image register + XLEmbeddedImage embImage; + embImage.setImage(image); + embImage.setImageAnchor( anchorInfo ); + getEmbImages().push_back(embImage); } - catch (const std::exception& ) { - // If processing fails, skip this anchor + catch (const std::exception&) { + // If creating XLEmbeddedImage fails, skip this entry + // This ensures we don't crash on malformed data } } @@ -867,21 +516,21 @@ namespace OpenXLSX std::set imageIds; std::set relationshipIds; - for (const auto& imgInfo : m_imageRegistry) { + for (const auto& embImage : getEmbImages()) { // Check for duplicate image IDs - if (!imgInfo.imageId.empty()) { - if (imageIds.find(imgInfo.imageId) != imageIds.end()) { + if (!embImage.id().empty()) { + if (imageIds.find(embImage.id()) != imageIds.end()) { return false; // Duplicate image ID found } - imageIds.insert(imgInfo.imageId); + imageIds.insert(embImage.id()); } // Check for duplicate relationship IDs - if (!imgInfo.relationshipId.empty()) { - if (relationshipIds.find(imgInfo.relationshipId) != relationshipIds.end()) { + if (!embImage.relationshipId().empty()) { + if (relationshipIds.find(embImage.relationshipId()) != relationshipIds.end()) { return false; // Duplicate relationship ID found } - relationshipIds.insert(imgInfo.relationshipId); + relationshipIds.insert(embImage.relationshipId()); } } @@ -893,15 +542,10 @@ namespace OpenXLSX * @param imageId The image ID to check * @return True if the ID exists, false otherwise */ - bool XLWorksheet::hasImageWithId(const std::string& imageId) const + bool XLWorksheet::hasEmbImageWithId(const std::string& imageId) const { - // Auto-refresh registry if empty - if (m_imageRegistry.empty()) { - refreshImageRegistry(); - } - - return std::any_of(m_imageRegistry.begin(), m_imageRegistry.end(), - [&imageId](const XLImageInfo& img) { return img.imageId == imageId; }); + return std::any_of(getEmbImages().begin(), getEmbImages().end(), + [&imageId](const XLEmbeddedImage& img) { return img.id() == imageId; }); } /** @@ -911,13 +555,8 @@ namespace OpenXLSX */ bool XLWorksheet::hasImageWithRelationshipId(const std::string& relationshipId) const { - // Auto-refresh registry if empty - if (m_imageRegistry.empty()) { - refreshImageRegistry(); - } - - return std::any_of(m_imageRegistry.begin(), m_imageRegistry.end(), - [&relationshipId](const XLImageInfo& img) { return img.relationshipId == relationshipId; }); + return std::any_of(getEmbImages().begin(), getEmbImages().end(), + [&relationshipId](const XLEmbeddedImage& img) { return img.relationshipId() == relationshipId; }); } /** @@ -928,7 +567,7 @@ namespace OpenXLSX std::string XLWorksheet::getImagePathFromRelationship(const std::string& relationshipId) const { try { - std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + std::string relsFilename = getDrawingRelsFilename(); if (!parentDoc().hasRelationshipsFile(relsFilename)) { return ""; @@ -960,41 +599,59 @@ namespace OpenXLSX } } - /** - * @brief Remove image file if no other relationships reference it - * @param imagePath The relative image path (e.g., "../media/image_img3.png") - * @return True if the file was processed (deleted if no references remain, preserved if still referenced) - * - * @note This function MUST be called AFTER the relationship has been removed from the relationships file. - * It checks if any other relationships still reference the binary file before deciding whether to delete it. - * If called before relationship removal, it will incorrectly preserve files that should be deleted. - */ - bool XLWorksheet::removeImageFileIfUnreferenced(const std::string& imagePath) const - { - if (imagePath.empty()) { - return false; - } - - try { - // Check if binary file is still referenced before deleting - bool isReferenced = parentDoc().isBinaryFileReferenced(imagePath); - - if (!isReferenced) { - // Only delete binary file if no relationships still reference it - // Convert relative path to absolute path within the archive - std::string fullImagePath = "xl/media/" + imagePath.substr(imagePath.find_last_of('/') + 1); - - bool result = const_cast(parentDoc()).deleteEntry(fullImagePath); - return result; - } else { - // Multiple references exist - don't delete binary file yet - return true; // Return true since we "processed" the request - } +/** + * @brief Add an embedded image, assuming image is fully created and imageAnchor has all user-specified data + * @details This function generates unique IDs, completes the anchor initialization, and adds the image to the worksheet + * @param imageAnchor Anchor configuration (may be partially initialized) + * @param image Shared pointer to the XLImage object + * @return Image ID string + */ +std::string XLWorksheet::embedImage(const XLImageAnchor& imageAnchor, XLImageShPtr image) +{ + std::string imageId; + if (image.get()) { + // Get unique IDs + imageId = generateNextImageId(); + const std::string relationshipId = getNextUnusedRelationshipId(); + + // Fully initialize image anchor + XLImageAnchor anchor = imageAnchor; + anchor.imageId = imageId; + anchor.relationshipId = relationshipId; + if (XLImageAnchorType::Unknown == anchor.anchorType) { + anchor.anchorType = ((0 == anchor.toCol) && (0 == anchor.toRow)) ? + XLImageAnchorType::OneCellAnchor : XLImageAnchorType::TwoCellAnchor; + } + if ((0 == anchor.displayWidthEMUs) || (0 == anchor.displayHeightEMUs)) { + const uint32_t widthPixels = image->widthPixels(); + const uint32_t heightPixels = image->heightPixels(); + anchor.displayWidthEMUs = XLImageUtils::pixelsToEMUs(widthPixels); + anchor.displayHeightEMUs = XLImageUtils::pixelsToEMUs(heightPixels); + } + if ((0 == anchor.actualWidthEMUs) || (0 == anchor.actualHeightEMUs)) { + anchor.actualWidthEMUs = anchor.displayWidthEMUs; + anchor.actualHeightEMUs = anchor.displayHeightEMUs; } - catch (const std::exception&) { - return false; + + // Construct embedded image + XLEmbeddedImage embImage; + embImage.setImageAnchor(anchor); + embImage.setImage(image); + + // Add anchor to drawing file XML (drawing3.xml) + if (!m_drawingML.valid()) { + createEmptyDrawingML(); } + m_drawingML.addImage(embImage); + + // Add relationship to relationship file XML (drawing3.xml.rels) + addImageRelationship(embImage); + + // Add to register + getEmbImages().push_back(embImage); } + return imageId; +} /** * @brief Remove image node from DrawingML XML (in-memory document) @@ -1008,7 +665,7 @@ namespace OpenXLSX { try { // Use standard sequential numbering approach (consistent with rest of codebase) - std::string drawingFilename = "xl/drawings/drawing" + std::to_string(sheetXmlNumber()) + ".xml"; + std::string drawingFilename = getDrawingFilename(); // Get the existing XLXmlData object for the drawing file // This is the proper way - use the existing XML data structure rather than bypassing it XLXmlData* xmlData = const_cast(parentDoc()).getDrawingXmlData(drawingFilename); @@ -1031,51 +688,8 @@ namespace OpenXLSX return false; } - // Find and remove the image node - for (auto anchor : wsDr.children()) { - // Check both oneCellAnchor and twoCellAnchor - if (matchesElementName(anchor.name(), "oneCellAnchor") || - matchesElementName(anchor.name(), "twoCellAnchor")) { - - // Look for the pic element with the matching relationship ID - auto pic = anchor.child("xdr:pic"); - if (pic.empty()) { - pic = anchor.child("pic"); - } - - if (!pic.empty()) { - // Check blipFill -> blip -> r:embed - auto blipFill = pic.child("xdr:blipFill"); - if (blipFill.empty()) { - blipFill = pic.child("a:blipFill"); - } - if (blipFill.empty()) { - blipFill = pic.child("blipFill"); - } - - if (!blipFill.empty()) { - auto blip = blipFill.child("a:blip"); - if (blip.empty()) { - blip = blipFill.child("blip"); - } - - if (!blip.empty()) { - std::string embedId = blip.attribute("r:embed").value(); - if (embedId == relationshipId) { - // Found the matching image, remove the entire anchor - wsDr.remove_child(anchor); - - // The XMLDocument is automatically updated - no need to save/restore - // The XLXmlData will persist the changes when the document is saved - return true; - } - } - } - } - } - } - - return false; // Image not found + // Delegate to XLDrawingML utility function + return XLDrawingML::deleteXMLNode(wsDr, relationshipId); } catch (const std::exception&) { return false; @@ -1091,7 +705,7 @@ namespace OpenXLSX { try { // Use standard sequential numbering approach (consistent with rest of codebase) - std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + std::string relsFilename = getDrawingRelsFilename(); // Check if relationships file exists if (!parentDoc().hasRelationshipsFile(relsFilename)) { @@ -1142,12 +756,13 @@ namespace OpenXLSX * @brief Check if a binary file is referenced by any relationships in this worksheet * @param imagePath The relative image path (e.g., "../media/image_3.gif") * @return True if the file is referenced by at least one relationship in this worksheet +* TODO: imagePath should be imagePackagePath */ bool XLWorksheet::isBinaryFileReferenced(const std::string& imagePath) const { try { // Get the drawing relationships filename - std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; + std::string relsFilename = getDrawingRelsFilename(); // Check if relationships file exists in archive if (!parentDoc().hasRelationshipsFile(relsFilename)) { @@ -1183,111 +798,4 @@ namespace OpenXLSX } } - /** - * @brief Remove image file from archive if no other relationships reference it - * @param relationshipId The relationship ID of the image file to remove - * @return True if the file was processed (deleted if no references remain, preserved if still referenced) - */ - bool XLWorksheet::removeImageFileFromArchiveIfUnreferenced(const std::string& relationshipId) const - { - try { - // Use standard sequential numbering approach (consistent with rest of codebase) - std::string relsFilename = "xl/drawings/_rels/drawing" + std::to_string(sheetXmlNumber()) + ".xml.rels"; - - if (!parentDoc().hasRelationshipsFile(relsFilename)) { - return false; - } - - // Alternate implementation using XLXmlData access - // Get relationships XML data from in-memory cache - XLXmlData* relsXmlData = const_cast(parentDoc()).getRelationshipsXmlData(relsFilename); - if (!relsXmlData || !relsXmlData->valid()) { - return false; - } - - // Parse the relationships XML from in-memory data - XMLDocument* relsDoc = relsXmlData->getXmlDocument(); - if (!relsDoc || !relsDoc->document_element()) { - return false; - } - - // Find the relationship and get the image path - std::string imagePath; - for (auto rel : relsDoc->document_element().children("Relationship")) { - if (rel.attribute("Id").value() == relationshipId) { - imagePath = rel.attribute("Target").value(); - break; - } - } - - if (!imagePath.empty()) { - // Check if binary file is still referenced before deleting - bool isReferenced = isBinaryFileReferenced(imagePath); - - if (!isReferenced) { - // Only delete binary file if no relationships still reference it - // Convert relative path to absolute path within the archive - std::string fullImagePath = "xl/media/" + imagePath.substr(imagePath.find_last_of('/') + 1); - - // Remove the image file from the archive - // This deletes the binary file entry from the in-memory archive - try { - bool result = const_cast(parentDoc()).deleteEntry(fullImagePath); - return result; - } catch (const std::exception&) { - return false; - } - } else { - // Multiple references exist - don't delete binary file yet - // Just return true since the relationship was removed successfully - return true; - } - } else { - return false; - } - } - catch (const std::exception&) { - return false; - } - } - - /** - * @brief Robust XML element name matching that handles namespace prefixes - * @param elementName The actual element name (may include namespace prefix) - * @param targetName The target element name to match - * @return True if the element name matches the target (with or without namespace) - * - * @example - * Exact match: "oneCellAnchor" matches "oneCellAnchor" - * Namespace prefix: "xdr:oneCellAnchor" matches "oneCellAnchor" - * Different namespace: "a:oneCellAnchor" matches "oneCellAnchor" - */ - bool XLWorksheet::matchesElementName(const std::string& elementName, const std::string& targetName) const - { - // Exact match - if (elementName == targetName) { - return true; - } - - // Check if element name ends with target name (handles namespace prefixes) - if (elementName.length() > targetName.length() + 1) { - size_t pos = elementName.find_last_of(':'); - if (pos != std::string::npos && pos + 1 < elementName.length()) { - std::string localName = elementName.substr(pos + 1); - if (localName == targetName) { - return true; - } - } - } - - // Check if element name starts with target name followed by colon - if (elementName.length() == targetName.length() + 1 && elementName[targetName.length()] == ':') { - if (elementName.substr(0, targetName.length()) == targetName) { - return true; - } - } - - return false; - } - } // namespace OpenXLSX diff --git a/Tests/testXLImage.cpp b/Tests/testXLImage.cpp index f7997331..7faeda41 100644 --- a/Tests/testXLImage.cpp +++ b/Tests/testXLImage.cpp @@ -8,13 +8,13 @@ using namespace OpenXLSX; -TEST_CASE("XLImage", "[XLImage]") +TEST_CASE("XLEmbeddedImage", "[XLEmbeddedImage]") { SECTION("Basic Image Embedding Test") { // Create a new workbook XLDocument doc; - doc.create("./testXLImage.xlsx"); + doc.create("./testXLEmbeddedImage.xlsx"); // Get the first worksheet and rename it XLWorksheet worksheet1 = doc.workbook().worksheet(1); @@ -38,19 +38,16 @@ TEST_CASE("XLImage", "[XLImage]") 0x42, 0x60, 0x82 }; - // Create XLImage objects and load the PNG data - XLImage image1, image2, image3; - REQUIRE(image1.loadFromData(red1x1PNGData, "image/png")); - REQUIRE(image2.loadFromData(red1x1PNGData, "image/png")); - REQUIRE(image3.loadFromData(red1x1PNGData, "image/png")); - - // Add image to first worksheet - REQUIRE(worksheet1.addImage(image1, 2, 2)); // Row 2, Column 2 - REQUIRE(worksheet1.addImage(image2, 5, 3)); // Row 5, Column 3 - - // Add image to second worksheet - REQUIRE(worksheet2.addImage(image3, 1, 1)); // Row 1, Column 1 - + const std::string imageId1 = worksheet1.embedImageFromImageData( + XLCellReference(2,2), red1x1PNGData, XLMimeType::PNG ); + const std::string imageId2 = worksheet1.embedImageFromImageData( + XLCellReference(5,3), red1x1PNGData, XLMimeType::PNG ); + const std::string imageId3 = worksheet2.embedImageFromImageData( + XLCellReference(1,1), red1x1PNGData, XLMimeType::PNG ); + REQUIRE(!imageId1.empty()); + REQUIRE(!imageId3.empty()); + REQUIRE(!imageId3.empty()); + // Verify the document data integrity std::string dbgMsg; int result = doc.verifyData(&dbgMsg); @@ -69,22 +66,22 @@ TEST_CASE("XLImage", "[XLImage]") doc.close(); // Clean up - std::remove("./testXLImage.xlsx"); + std::remove("./testXLEmbeddedImage.xlsx"); } } /* -COMPREHENSIVE XLImage UNIT TEST OUTLINE +COMPREHENSIVE XLEmbeddedImage UNIT TEST OUTLINE ======================================= This outline covers all functions related to embedded images in OpenXLSX: 1. BASIC IMAGE CREATION AND EMBEDDING - - Test addImage() with different image formats (PNG, JPEG, GIF, BMP) - - Test addImage() with different data sources (vector, file path) - - Test addImage() with different positioning (various row/column combinations) - - Test addImage() with different anchor types (oneCellAnchor, twoCellAnchor) - - Test addImage() with different sizing strategies (original, aspect ratio, exact dimensions) + - Test embedImage() with different image formats (PNG, JPEG, GIF, BMP) + - Test embedImage() with different data sources (vector, file path) + - Test embedImage() with different positioning (various row/column combinations) + - Test embedImage() with different anchor types (oneCellAnchor, twoCellAnchor) + - Test embedImage() with different sizing strategies (original, aspect ratio, exact dimensions) 2. IMAGE QUERY OPERATIONS - Test getImageInfos() - retrieve all images in worksheet From 4befaa39ba80e7cdc92fd553f5ee8f417e620e9e Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Wed, 12 Nov 2025 12:49:51 -0800 Subject: [PATCH 12/13] Embedded Image Support -- phase XI / unit test --- OpenXLSX/CMakeLists.txt | 2 + OpenXLSX/headers/XLDrawingML.hpp | 44 +- OpenXLSX/headers/XLImage.hpp | 130 ++- OpenXLSX/headers/XLSheet.hpp | 19 +- OpenXLSX/sources/XLDrawingML.cpp | 53 +- OpenXLSX/sources/XLImage.cpp | 12 +- OpenXLSX/sources/XLImageUtils.cpp | 312 +++---- OpenXLSX/sources/XLSheet.cpp | 17 +- OpenXLSX/sources/XLWorksheetImageQuery.cpp | 81 +- Tests/CMakeLists.txt | 1 + Tests/testXLImage.cpp | 965 ++++++++++++++++++--- 11 files changed, 1143 insertions(+), 493 deletions(-) diff --git a/OpenXLSX/CMakeLists.txt b/OpenXLSX/CMakeLists.txt index c44285cb..6fe203bd 100644 --- a/OpenXLSX/CMakeLists.txt +++ b/OpenXLSX/CMakeLists.txt @@ -112,7 +112,9 @@ set(OPENXLSX_SOURCES ${CMAKE_CURRENT_LIST_DIR}/sources/XLStyles.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLTables.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLImage.cpp + ${CMAKE_CURRENT_LIST_DIR}/sources/XLImageUtils.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLDrawingML.cpp + ${CMAKE_CURRENT_LIST_DIR}/sources/XLWorksheetImageQuery.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLWorkbook.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLXmlData.cpp ${CMAKE_CURRENT_LIST_DIR}/sources/XLXmlFile.cpp diff --git a/OpenXLSX/headers/XLDrawingML.hpp b/OpenXLSX/headers/XLDrawingML.hpp index 6c1b1c99..74dcf0b0 100644 --- a/OpenXLSX/headers/XLDrawingML.hpp +++ b/OpenXLSX/headers/XLDrawingML.hpp @@ -173,43 +173,9 @@ namespace OpenXLSX // ===== Constructors ===== // XLImageAnchor() = default; - - /** - * @brief Construct from oneCellAnchor data - * @param imageId Image identifier - * @param relationshipId Relationship identifier - * @param row Row number (0-based, as in XML) - * @param col Column number (0-based, as in XML) - * @param rowOffset Row offset in EMUs - * @param colOffset Column offset in EMUs - * @param displayWidth Display width in EMUs - * @param displayHeight Display height in EMUs - */ - XLImageAnchor(const std::string& imageId, const std::string& relationshipId, - uint32_t row, uint16_t col, int32_t rowOffset, int32_t colOffset, - uint32_t displayWidth, uint32_t displayHeight); - - /** - * @brief Construct from twoCellAnchor data - * @param imageId Image identifier - * @param relationshipId Relationship identifier - * @param fromRow From row number (0-based, as in XML) - * @param fromCol From column number (0-based, as in XML) - * @param toRow To row number (0-based, as in XML) - * @param toCol To column number (0-based, as in XML) - * @param fromRowOffset From row offset in EMUs - * @param fromColOffset From column offset in EMUs - * @param toRowOffset To row offset in EMUs - * @param toColOffset To column offset in EMUs - * @param displayWidth Display width in EMUs - * @param displayHeight Display height in EMUs - */ - XLImageAnchor(const std::string& imageId, const std::string& relationshipId, - uint32_t fromRow, uint16_t fromCol, uint32_t toRow, uint16_t toCol, - int32_t fromRowOffset, int32_t fromColOffset, - int32_t toRowOffset, int32_t toColOffset, - uint32_t displayWidth, uint32_t displayHeight); + bool operator==( const XLImageAnchor& other ) const = default; + void reset(); void initOneCell( const XLCellReference& cellRef, int32_t rowOffset = 0, @@ -241,12 +207,6 @@ namespace OpenXLSX void setDisplaySizeWithAspectRatio( const std::string& imageFileName, const XLMimeType& mimeType, const uint32_t& maxWidthEmus, const uint32_t& maxHeightEmus ); - - /** - * @brief Get the primary anchor cell reference (converts 0-based to 1-based) - * @return Cell reference string (e.g., "A2") - */ - std::string getAnchorCellReference() const; /** * @brief Check if this is a twoCellAnchor diff --git a/OpenXLSX/headers/XLImage.hpp b/OpenXLSX/headers/XLImage.hpp index 47d00803..b56b9332 100644 --- a/OpenXLSX/headers/XLImage.hpp +++ b/OpenXLSX/headers/XLImage.hpp @@ -86,7 +86,7 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. * RELATIONSHIP TYPE 1: WORKSHEET-LEVEL RELATIONSHIPS * ────────────────────────────────────────────────────────────────────────────── * - * Location: Lines 1735-1750 in XLWorksheet::drawingML() + * Location: Lines 1764-1778 in XLWorksheet::drawingML() * * Code: * std::string drawingRelativePath = getPathARelativeToPathB(drawingFilename, getXmlPath()); @@ -115,13 +115,13 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d'`MM. * RELATIONSHIP TYPE 2: DRAWING-LEVEL RELATIONSHIPS * ────────────────────────────────────────────────────────────────────────────── * - * Location: Lines 2016-2045 in XLWorksheet::createDrawingRelationshipsFile() + * Location: Lines 2147-2175 in XLWorksheet::createDrawingRelationshipsFile() * * Code: * int relationshipId = 1; - * for (const auto& image : m_images) { - * std::string imageFilename = "xl/media/image_" + image.id() + image.extension(); - * std::string imageRelativePath = "../media/image_" + image.id() + image.extension(); + * for (const auto& embImage : getEmbImages()) { + * std::string packageFilename = embImage.getImage()->filePackagePath(); + * std::string imageRelativePath = "../media/" + packageFilename.substr(packageFilename.find_last_of('/') + 1); * * relsXml += " red1x1PNGData; static const std::vector png31x15Data; static const std::vector jpeg32x18Data; static const std::vector bmp33x16Data; static const std::vector gif26x16Data; /** - * @brief Detect MIME type from binary image data + * @brief Detect MIME type from binary image data and return as enum * @param data Binary image data - * @return MIME type string (e.g., "image/png") + * @return Corresponding XLMimeType enum value */ - static std::string detectMimeType(const std::vector& data); + static XLMimeType detectMimeType(const std::vector& data); /** - * @brief Get file extension from MIME type - * @param mime MIME type string - * @return File extension (e.g., ".png") + * @brief Convert XLMimeType enum to MIME type string + * @param mimeType XLMimeType enum value + * @return Corresponding MIME type string (e.g., "image/png") */ - static std::string extensionFromMime(const std::string& mime); + static std::string mimeTypeToString(XLMimeType mimeType); /** - * @brief Determine MIME type from file extension - * @param extension File extension - * @return MIME type + * @brief Convert MIME type string to XLMimeType enum + * @param mimeType MIME type string (e.g., "image/png") + * @return Corresponding XLMimeType enum value */ - static XLMimeType mimeTypeFromExtension(const std::string& extension); + static XLMimeType stringToMimeType(const std::string& mimeType); /** * @brief Determine file extension from MIME type * @param mimeType MIME type string * @return File extension string */ - static std::string extensionFromMimeType(XLMimeType mimeType); + static std::string mimeTypeToExtension(XLMimeType mimeType); /** - * @brief Convert pixels to EMUs (Excel Measurement Units) - * @param pixels Number of pixels - * @return Number of EMUs + * @brief Determine MIME type from file extension + * @param extension File extension + * @return MIME type */ - static uint32_t pixelsToEMUs(uint32_t pixels); + static XLMimeType extensionToMimeType(const std::string& extension); /** - * @brief Get image dimensions from binary data - * @param data Binary image data - * @param mimeType MIME type of the image - * @return Pair of (width, height) in pixels + * @brief Convert MIME type to XLContentType enum + * @param mimeType MIME type enum + * @return Corresponding XLContentType enum value */ - static std::pair getImageDimensions(const std::vector& data, XLMimeType mimeType); + static XLContentType mimeTypeToContentType(const XLMimeType& mimeType); + + /** + * @brief Convert XLContentType enum to MIME type + * @param contentType XLContentType enum value + * @return Corresponding MIME type + */ + static XLMimeType contentTypeToMimeType(XLContentType contentType); /** * @brief Convert pixels to EMUs (Excel units) @@ -749,13 +758,6 @@ namespace OpenXLSX */ static uint32_t emusToPixels(uint32_t emus); - /** - * @brief Convert EMUs to Excel units - * @param emus EMU value - * @return Excel unit value - */ - static uint32_t emusToExcelUnits(uint32_t emus); - /** * @brief Convert points to EMUs * @param points Point value @@ -763,14 +765,6 @@ namespace OpenXLSX */ static uint32_t pointsToEmus(double points); - /** - * @brief Validate image data integrity - * @param data Binary image data - * @param mimeType MIME type of the image - * @return True if image data is valid - */ - static bool validateImageData(const std::vector& data, const XLMimeType& mimeType); - /** * @brief Calculate aspect ratio from dimensions * @param width Width in pixels @@ -779,6 +773,14 @@ namespace OpenXLSX */ static double calculateAspectRatio(uint32_t width, uint32_t height); + /** + * @brief Get image dimensions from binary data + * @param data Binary image data + * @param mimeType MIME type of the image + * @return Pair of (width, height) in pixels + */ + static std::pair getImageDimensions(const std::vector& data, XLMimeType mimeType); + /** * @brief Get image dimensions from binary data (auto-detect MIME type) * @param data Binary image data @@ -794,6 +796,14 @@ namespace OpenXLSX */ static bool isValidImageSize(uint32_t width, uint32_t height); + /** + * @brief Validate image data integrity + * @param data Binary image data + * @param mimeType MIME type of the image + * @return True if image data is valid + */ + static bool validateImageData(const std::vector& data, const XLMimeType& mimeType); + /** * @brief Calculate hash value for binary image data * @param imageData Binary image data as vector @@ -817,39 +827,25 @@ namespace OpenXLSX static bool matchesElementName(const std::string& elementName, const std::string& targetName); /** - * @brief Convert MIME type to XLContentType enum - * @param mimeType MIME type enum - * @return Corresponding XLContentType enum value - */ - static XLContentType mimeTypeToContentType(const XLMimeType& mimeType); - - /** - * @brief Convert XLContentType enum to MIME type - * @param contentType XLContentType enum value - * @return Corresponding MIME type - */ - static XLMimeType contentTypeToMimeType(XLContentType contentType); - - /** - * @brief Convert MIME type string to XLMimeType enum - * @param mimeType MIME type string (e.g., "image/png") - * @return Corresponding XLMimeType enum value + * @brief Get file extension from MIME type + * @param mime MIME type string + * @return File extension (e.g., ".png") */ - static XLMimeType stringToMimeType(const std::string& mimeType); + static std::string extensionFromMime(const std::string& mime); /** - * @brief Convert XLMimeType enum to MIME type string - * @param mimeType XLMimeType enum value - * @return Corresponding MIME type string (e.g., "image/png") + * @brief Convert pixels to EMUs (Excel Measurement Units) + * @param pixels Number of pixels + * @return Number of EMUs */ - static std::string mimeTypeToString(XLMimeType mimeType); + static uint32_t pixelsToEMUs(uint32_t pixels); /** - * @brief Detect MIME type from binary image data and return as enum - * @param data Binary image data - * @return Corresponding XLMimeType enum value + * @brief Convert EMUs to Excel units + * @param emus EMU value + * @return Excel unit value */ - static XLMimeType detectMimeTypeEnum(const std::vector& data); + static uint32_t emusToExcelUnits(uint32_t emus); }; } // namespace OpenXLSX diff --git a/OpenXLSX/headers/XLSheet.hpp b/OpenXLSX/headers/XLSheet.hpp index a6e874cc..bd45c3a5 100644 --- a/OpenXLSX/headers/XLSheet.hpp +++ b/OpenXLSX/headers/XLSheet.hpp @@ -1402,19 +1402,6 @@ namespace OpenXLSX */ const std::vector& getEmbImages() const; - /** - * @brief Get iterator to the beginning of the embedded images collection - * @return Const iterator to the first embedded image - * @note Useful for range-based for loops and STL algorithms - */ - std::vector::const_iterator embImagesBegin() const; - - /** - * @brief Get iterator to the end of the embedded images collection - * @return Const iterator to the end of the embedded images collection - * @note Useful for range-based for loops and STL algorithms - */ - std::vector::const_iterator embImagesEnd() const; //---------------------------------------------------------------------------------------------------------------------- // Image Modification Methods @@ -1464,6 +1451,12 @@ namespace OpenXLSX */ bool removeImageFromRelationships(const std::string& relationshipId) const; + /** + * @brief Remove the element from worksheet XML if there are no more images + * @note This is a helper function called after removing images to clean up the XML + */ + void removeDrawingElementIfEmpty(); + /** * @brief Remove image file from archive (delete binary file entry) * @param relationshipId The relationship ID of the image file to remove diff --git a/OpenXLSX/sources/XLDrawingML.cpp b/OpenXLSX/sources/XLDrawingML.cpp index 657a562f..0791ac5e 100644 --- a/OpenXLSX/sources/XLDrawingML.cpp +++ b/OpenXLSX/sources/XLDrawingML.cpp @@ -130,50 +130,6 @@ namespace OpenXLSX // ========== XLImageAnchor Implementation ========== // - XLImageAnchor::XLImageAnchor(const std::string& imageId, const std::string& relationshipId, - uint32_t row, uint16_t col, int32_t rowOffset, int32_t colOffset, - uint32_t displayWidth, uint32_t displayHeight) - : anchorType(XLImageAnchorType::OneCellAnchor) - , imageId(imageId) - , relationshipId(relationshipId) - , fromRow(row) // Store as-is (0-based from XML) - , fromCol(col) // Store as-is (0-based from XML) - , fromRowOffset(rowOffset) - , fromColOffset(colOffset) - , toRow(row) // For oneCellAnchor, to = from - , toCol(col) // For oneCellAnchor, to = from - , toRowOffset(0) // No offset for 'to' in oneCellAnchor - , toColOffset(0) // No offset for 'to' in oneCellAnchor - , displayWidthEMUs(displayWidth) - , displayHeightEMUs(displayHeight) - , actualWidthEMUs(displayWidth) - , actualHeightEMUs(displayHeight) - { - } - - XLImageAnchor::XLImageAnchor(const std::string& imageId, const std::string& relationshipId, - uint32_t fromRow, uint16_t fromCol, uint32_t toRow, uint16_t toCol, - int32_t fromRowOffset, int32_t fromColOffset, - int32_t toRowOffset, int32_t toColOffset, - uint32_t displayWidth, uint32_t displayHeight) - : anchorType(XLImageAnchorType::TwoCellAnchor) - , imageId(imageId) - , relationshipId(relationshipId) - , fromRow(fromRow) // Store as-is (0-based from XML) - , fromCol(fromCol) // Store as-is (0-based from XML) - , fromRowOffset(fromRowOffset) - , fromColOffset(fromColOffset) - , toRow(toRow) // Store as-is (0-based from XML) - , toCol(toCol) // Store as-is (0-based from XML) - , toRowOffset(toRowOffset) - , toColOffset(toColOffset) - , displayWidthEMUs(displayWidth) - , displayHeightEMUs(displayHeight) - , actualWidthEMUs(displayWidth) - , actualHeightEMUs(displayHeight) - { - } - void XLImageAnchor::reset(){ anchorType = XLImageAnchorType::Unknown; imageId.erase(); @@ -304,7 +260,7 @@ namespace OpenXLSX // Convert string mimeType to enum, auto-detect if unknown XLMimeType mimeTypeEnum = mimeType; if (mimeTypeEnum == XLMimeType::Unknown) { - mimeTypeEnum = XLImageUtils::detectMimeTypeEnum(imageData); + mimeTypeEnum = XLImageUtils::detectMimeType(imageData); } const std::pair imageDims = @@ -334,13 +290,6 @@ namespace OpenXLSX } } - - std::string XLImageAnchor::getAnchorCellReference() const - { - // Convert 0-based coordinates to 1-based for cell reference - return XLCellReference(fromRow + 1, fromCol + 1).address(); - } - bool XLImageAnchor::isTwoCell() const { return anchorType == XLImageAnchorType::TwoCellAnchor; diff --git a/OpenXLSX/sources/XLImage.cpp b/OpenXLSX/sources/XLImage.cpp index dbad9df5..c5ec5b3e 100644 --- a/OpenXLSX/sources/XLImage.cpp +++ b/OpenXLSX/sources/XLImage.cpp @@ -126,7 +126,7 @@ namespace OpenXLSX } // Compute metadata from image data - XLMimeType computedMimeType = XLImageUtils::mimeTypeFromExtension(m_extension); + XLMimeType computedMimeType = XLImageUtils::extensionToMimeType(m_extension); if (computedMimeType != m_mimeType) { std::string storedMimeTypeStr = XLImageUtils::mimeTypeToString(m_mimeType); std::string computedMimeTypeStr = XLImageUtils::mimeTypeToString(computedMimeType); @@ -196,7 +196,7 @@ namespace OpenXLSX // Image doesn't exist, create new one size_t imageDataHash = XLImageUtils::imageDataHash(imageData); XLMimeType mimeType = XLImageUtils::contentTypeToMimeType(contentType); - std::string extension = XLImageUtils::extensionFromMimeType(mimeType); + std::string extension = XLImageUtils::mimeTypeToExtension(mimeType); // Generate package filename std::string packageImageFilename; @@ -795,8 +795,12 @@ namespace OpenXLSX } if (m_imageAnchor.displayHeightEMUs == 0) { - appendDbgMsg(dbgMsg, "display height in EMUs is zero"); - errorCount++; + // For twoCellAnchor, Excel may legitimately store zero values in the ext element + // Excel handles the sizing based on cell positions, so zero is acceptable + if (!m_imageAnchor.isTwoCell()) { + appendDbgMsg(dbgMsg, "display height in EMUs is zero"); + errorCount++; + } } // Check MIME type and extension consistency diff --git a/OpenXLSX/sources/XLImageUtils.cpp b/OpenXLSX/sources/XLImageUtils.cpp index 114d5b31..f1b21d4f 100644 --- a/OpenXLSX/sources/XLImageUtils.cpp +++ b/OpenXLSX/sources/XLImageUtils.cpp @@ -55,6 +55,16 @@ using namespace OpenXLSX; // These contain the binary data from the tiny image files in the images directory +// 1x1 red PNG (minimal) +const std::vector XLImageUtils::red1x1PNGData = { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, + 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0x0F, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + 0x42, 0x60, 0x82 +}; + // tiny_png.png - Small PNG image const std::vector XLImageUtils::png31x15Data = { 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, @@ -447,39 +457,73 @@ const std::vector XLImageUtils::png31x15Data = { // ========== XLImageUtils Member Functions ========== // /** - * @brief Detect MIME type from binary image data + * @brief Detect MIME type from binary image data and return as enum * @param data Binary image data - * @return MIME type string (e.g., "image/png") + * @return Corresponding XLMimeType enum value */ -std::string XLImageUtils::detectMimeType(const std::vector& data) +XLMimeType XLImageUtils::detectMimeType(const std::vector& data) { - if (data.size() >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4e && data[3] == 0x47) { - return "image/png"; + if (data.size() >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47) { + return XLMimeType::PNG; } if (data.size() >= 2 && data[0] == 0xFF && data[1] == 0xD8) { - return "image/jpeg"; + return XLMimeType::JPEG; } if (data.size() >= 2 && data[0] == 0x42 && data[1] == 0x4D) { - return "image/bmp"; + return XLMimeType::BMP; } if (data.size() >= 3 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46) { - return "image/gif"; + return XLMimeType::GIF; } - return ""; + return XLMimeType::Unknown; +} + +/** + * @brief Convert XLMimeType enum to MIME type string + * @param mimeType XLMimeType enum value + * @return Corresponding MIME type string (e.g., "image/png") + */ +std::string XLImageUtils::mimeTypeToString(XLMimeType mimeType) +{ + switch (mimeType) { + case XLMimeType::PNG: return "image/png"; + case XLMimeType::JPEG: return "image/jpeg"; + case XLMimeType::BMP: return "image/bmp"; + case XLMimeType::GIF: return "image/gif"; + case XLMimeType::Unknown: + default: return ""; + } +} + +/** + * @brief Convert MIME type string to XLMimeType enum + * @param mimeType MIME type string (e.g., "image/png") + * @return Corresponding XLMimeType enum value + */ +XLMimeType XLImageUtils::stringToMimeType(const std::string& mimeTypeStr) +{ + if (mimeTypeStr == "image/png") return XLMimeType::PNG; + if (mimeTypeStr == "image/jpeg") return XLMimeType::JPEG; + if (mimeTypeStr == "image/bmp") return XLMimeType::BMP; + if (mimeTypeStr == "image/gif") return XLMimeType::GIF; + return XLMimeType::Unknown; } /** - * @brief Get file extension from MIME type - * @param mime MIME type string - * @return File extension (e.g., ".png") + * @brief Determine file extension from MIME type + * @param mimeType MIME type string + * @return File extension string */ -std::string XLImageUtils::extensionFromMime(const std::string& mime) +std::string XLImageUtils::mimeTypeToExtension(XLMimeType mimeType) { - if (mime == "image/png") return ".png"; - if (mime == "image/jpeg") return ".jpg"; - if (mime == "image/bmp") return ".bmp"; - if (mime == "image/gif") return ".gif"; - return ""; + switch (mimeType) { + case XLMimeType::PNG: return ".png"; + case XLMimeType::JPEG: return ".jpg"; + case XLMimeType::BMP: return ".bmp"; + case XLMimeType::GIF: return ".gif"; + case XLMimeType::Unknown: + default: return ""; + } } /** @@ -487,7 +531,7 @@ std::string XLImageUtils::extensionFromMime(const std::string& mime) * @param extension File extension * @return MIME type */ -XLMimeType XLImageUtils::mimeTypeFromExtension(const std::string& extension) +XLMimeType XLImageUtils::extensionToMimeType(const std::string& extension) { if (extension.find("png") != std::string::npos ) return XLMimeType::PNG; if ((extension.find("jpg") != std::string::npos ) || @@ -497,28 +541,77 @@ XLMimeType XLImageUtils::mimeTypeFromExtension(const std::string& extension) return XLMimeType::Unknown; } +/** + * @brief Convert MIME type to XLContentType enum + * @param mimeType MIME type enum + * @return Corresponding XLContentType enum value + */ +XLContentType XLImageUtils::mimeTypeToContentType(const XLMimeType& mimeType) +{ + if (mimeType == XLMimeType::PNG) return XLContentType::ImagePNG; + if (mimeType == XLMimeType::JPEG) return XLContentType::ImageJPEG; + if (mimeType == XLMimeType::BMP) return XLContentType::ImageBMP; + if (mimeType == XLMimeType::GIF) return XLContentType::ImageGIF; + return XLContentType::Unknown; +} - -std::string XLImageUtils::extensionFromMimeType(XLMimeType mimeType) +/** + * @brief Convert XLContentType enum to MIME type + * @param contentType XLContentType enum value + * @return Corresponding MIME type + */ +XLMimeType XLImageUtils::contentTypeToMimeType(XLContentType contentType) { - switch (mimeType) { - case XLMimeType::PNG: return ".png"; - case XLMimeType::JPEG: return ".jpg"; - case XLMimeType::BMP: return ".bmp"; - case XLMimeType::GIF: return ".gif"; - case XLMimeType::Unknown: - default: return ""; + switch (contentType) { + case XLContentType::ImagePNG: return XLMimeType::PNG; + case XLContentType::ImageJPEG: return XLMimeType::JPEG; + case XLContentType::ImageBMP: return XLMimeType::BMP; + case XLContentType::ImageGIF: return XLMimeType::GIF; + default: return XLMimeType::Unknown; } } /** - * @brief Convert pixels to EMUs (Excel Measurement Units) + * @brief Convert pixels to EMUs (Excel units) * @param pixels Number of pixels - * @return Number of EMUs + * @return Equivalent EMUs + */ +uint32_t XLImageUtils::pixelsToEmus(uint32_t pixels) +{ + return pixels * EMU_TO_PIXEL_RATIO; +} + +/** + * @brief Convert EMUs to pixels + * @param emus Number of EMUs + * @return Equivalent pixels + */ +uint32_t XLImageUtils::emusToPixels(uint32_t emus) +{ + return emus / EMU_TO_PIXEL_RATIO; +} + +/** + * @brief Convert points to EMUs + * @param points Point value + * @return EMU value + */ +uint32_t XLImageUtils::pointsToEmus(double points) +{ + // Convert points to EMUs (1 point = 12700 EMUs) + return static_cast(points * 12700); +} + +/** + * @brief Calculate aspect ratio from dimensions + * @param width Width in pixels + * @param height Height in pixels + * @return Aspect ratio (width/height) */ -uint32_t XLImageUtils::pixelsToEMUs(uint32_t pixels) +double XLImageUtils::calculateAspectRatio(uint32_t width, uint32_t height) { - return pixels * 9525; // EMU_TO_PIXEL_RATIO + if (height == 0) return 0.0; + return static_cast(width) / static_cast(height); } /** @@ -577,45 +670,32 @@ std::pair XLImageUtils::getImageDimensions(const std::vector } /** - * @brief Convert pixels to EMUs (Excel units) - * @param pixels Number of pixels - * @return Equivalent EMUs - */ -uint32_t XLImageUtils::pixelsToEmus(uint32_t pixels) -{ - return pixels * 9525; // EMU_TO_PIXEL_RATIO -} - -/** - * @brief Convert EMUs to pixels - * @param emus Number of EMUs - * @return Equivalent pixels - */ -uint32_t XLImageUtils::emusToPixels(uint32_t emus) -{ - return emus / 9525; // EMU_TO_PIXEL_RATIO -} - -/** - * @brief Convert EMUs to Excel units - * @param emus EMU value - * @return Excel unit value + * @brief Get image dimensions from binary data (auto-detect MIME type) + * @param data Binary image data + * @return Pair of (width, height) in pixels, or {0,0} if detection fails */ -uint32_t XLImageUtils::emusToExcelUnits(uint32_t emus) +std::pair XLImageUtils::getImageDimensions(const std::vector& data) { - // Convert EMUs to Excel units (1 EMU = 1/9525 Excel units) - return emus / 9525; + // Auto-detect MIME type first + const XLMimeType mimeType = detectMimeType(data); + if (mimeType == XLMimeType::Unknown) { + return {0, 0}; + } + + // Use the existing function with detected MIME type + return getImageDimensions(data, mimeType); } /** - * @brief Convert points to EMUs - * @param points Point value - * @return EMU value + * @brief Check if image dimensions are reasonable + * @param width Width in pixels + * @param height Height in pixels + * @return True if dimensions are within reasonable bounds */ -uint32_t XLImageUtils::pointsToEmus(double points) +bool XLImageUtils::isValidImageSize(uint32_t width, uint32_t height) { - // Convert points to EMUs (1 point = 12700 EMUs) - return static_cast(points * 12700); + // Reasonable bounds: 1x1 to 10000x10000 pixels + return width > 0 && height > 0 && width <= 10000 && height <= 10000; } /** @@ -645,47 +725,6 @@ bool XLImageUtils::validateImageData(const std::vector& data, const XLM return false; } -/** - * @brief Calculate aspect ratio from dimensions - * @param width Width in pixels - * @param height Height in pixels - * @return Aspect ratio (width/height) - */ -double XLImageUtils::calculateAspectRatio(uint32_t width, uint32_t height) -{ - if (height == 0) return 0.0; - return static_cast(width) / static_cast(height); -} - -/** - * @brief Get image dimensions from binary data (auto-detect MIME type) - * @param data Binary image data - * @return Pair of (width, height) in pixels, or {0,0} if detection fails - */ -std::pair XLImageUtils::getImageDimensions(const std::vector& data) -{ - // Auto-detect MIME type first - XLMimeType mimeType = detectMimeTypeEnum(data); - if (mimeType == XLMimeType::Unknown) { - return {0, 0}; - } - - // Use the existing function with detected MIME type - return getImageDimensions(data, mimeType); -} - -/** - * @brief Check if image dimensions are reasonable - * @param width Width in pixels - * @param height Height in pixels - * @return True if dimensions are within reasonable bounds - */ -bool XLImageUtils::isValidImageSize(uint32_t width, uint32_t height) -{ - // Reasonable bounds: 1x1 to 10000x10000 pixels - return width > 0 && height > 0 && width <= 10000 && height <= 10000; -} - /** * @brief Calculate hash value for binary image data * @param imageData Binary image data as vector @@ -714,66 +753,3 @@ size_t XLImageUtils::imageDataStrHash(const std::string& imageDataStr) } return h; } - -/** - * @brief Convert MIME type string to XLContentType enum - * @param mimeType MIME type string (e.g., "image/png") - * @return Corresponding XLContentType enum value - */ -XLContentType XLImageUtils::mimeTypeToContentType(const XLMimeType& mimeType) -{ - if (mimeType == XLMimeType::PNG) return XLContentType::ImagePNG; - if (mimeType == XLMimeType::JPEG) return XLContentType::ImageJPEG; - if (mimeType == XLMimeType::BMP) return XLContentType::ImageBMP; - if (mimeType == XLMimeType::GIF) return XLContentType::ImageGIF; - return XLContentType::Unknown; -} - -XLMimeType XLImageUtils::contentTypeToMimeType(XLContentType contentType) -{ - switch (contentType) { - case XLContentType::ImagePNG: return XLMimeType::PNG; - case XLContentType::ImageJPEG: return XLMimeType::JPEG; - case XLContentType::ImageBMP: return XLMimeType::BMP; - case XLContentType::ImageGIF: return XLMimeType::GIF; - default: return XLMimeType::Unknown; - } -} - -XLMimeType XLImageUtils::stringToMimeType(const std::string& mimeTypeStr) -{ - if (mimeTypeStr == "image/png") return XLMimeType::PNG; - if (mimeTypeStr == "image/jpeg") return XLMimeType::JPEG; - if (mimeTypeStr == "image/bmp") return XLMimeType::BMP; - if (mimeTypeStr == "image/gif") return XLMimeType::GIF; - return XLMimeType::Unknown; -} - -std::string XLImageUtils::mimeTypeToString(XLMimeType mimeType) -{ - switch (mimeType) { - case XLMimeType::PNG: return "image/png"; - case XLMimeType::JPEG: return "image/jpeg"; - case XLMimeType::BMP: return "image/bmp"; - case XLMimeType::GIF: return "image/gif"; - case XLMimeType::Unknown: - default: return ""; - } -} - -XLMimeType XLImageUtils::detectMimeTypeEnum(const std::vector& data) -{ - if (data.size() >= 8 && data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47) { - return XLMimeType::PNG; - } - if (data.size() >= 2 && data[0] == 0xFF && data[1] == 0xD8) { - return XLMimeType::JPEG; - } - if (data.size() >= 2 && data[0] == 0x42 && data[1] == 0x4D) { - return XLMimeType::BMP; - } - if (data.size() >= 3 && data[0] == 0x47 && data[1] == 0x49 && data[2] == 0x46) { - return XLMimeType::GIF; - } - return XLMimeType::Unknown; -} diff --git a/OpenXLSX/sources/XLSheet.cpp b/OpenXLSX/sources/XLSheet.cpp index 8251518a..b3d5c7fc 100644 --- a/OpenXLSX/sources/XLSheet.cpp +++ b/OpenXLSX/sources/XLSheet.cpp @@ -1948,7 +1948,7 @@ std::string XLWorksheet::embedImageFromImageData(const XLImageAnchor& imageAncho // Auto-detect MIME type if unknown XLMimeType detectedMimeType = mimeType; if (mimeType == XLMimeType::Unknown) { - detectedMimeType = XLImageUtils::detectMimeTypeEnum(imageData); + detectedMimeType = XLImageUtils::detectMimeType(imageData); } XLImageShPtr image = parentDoc().getImageManager().findOrAddImage("", @@ -2004,7 +2004,7 @@ std::string XLWorksheet::embedImageFromFile(const XLImageAnchor& imageAnchor, // Auto-detect MIME type if unknown XLMimeType detectedMimeType = mimeType; if (mimeType == XLMimeType::Unknown) { - detectedMimeType = XLImageUtils::detectMimeTypeEnum(imageData); + detectedMimeType = XLImageUtils::detectMimeType(imageData); } XLImageShPtr image = parentDoc().getImageManager().findOrAddImage("", @@ -3245,12 +3245,6 @@ std::string XLWorksheet::getImageMediaPathFromFilename(const std::string& imageP return "xl/media/" + imagePath.substr(imagePath.find_last_of('/') + 1); } -const std::vector& XLWorksheet::getEmbImages() const -{ - const std::vector& embImages = const_cast(this)->getEmbImages(); - return embImages; -} - std::vector& XLWorksheet::getEmbImages() { if (!m_embImages) { @@ -3262,4 +3256,9 @@ std::vector& XLWorksheet::getEmbImages() return *m_embImages; } -// Static const member definitions for XLWorksheet +const std::vector& XLWorksheet::getEmbImages() const +{ + const std::vector& embImages = const_cast(this)->getEmbImages(); + return embImages; +} + diff --git a/OpenXLSX/sources/XLWorksheetImageQuery.cpp b/OpenXLSX/sources/XLWorksheetImageQuery.cpp index 7133ca6b..cee212ac 100644 --- a/OpenXLSX/sources/XLWorksheetImageQuery.cpp +++ b/OpenXLSX/sources/XLWorksheetImageQuery.cpp @@ -186,14 +186,6 @@ namespace OpenXLSX return result; } - /** - * @brief Get iterator to the end of the embedded images collection - * @return Const iterator to the end of the embedded images collection - */ - std::vector::const_iterator XLWorksheet::embImagesEnd() const - { - return getEmbImages().end(); - } //---------------------------------------------------------------------------------------------------------------------- // Image Modification Methods Implementation @@ -227,6 +219,11 @@ namespace OpenXLSX // Remove from m_embImages vector (in-memory cache) getEmbImages().erase(it); + // If this was the last image, remove the element from worksheet XML + if (getEmbImages().empty()) { + removeDrawingElementIfEmpty(); + } + // Note: XLDocument.getImageManager()->prune() will delete binary image data files from archive as needed. // Return true if all critical operations succeeded @@ -258,6 +255,11 @@ namespace OpenXLSX // Remove from m_embImages vector (in-memory cache) getEmbImages().erase(it); + // If this was the last image, remove the element from worksheet XML + if (getEmbImages().empty()) { + removeDrawingElementIfEmpty(); + } + // Note: XLDocument.getImageManager()->prune() will delete binary image data files from archive as needed. // Return true if all critical operations succeeded @@ -270,17 +272,54 @@ namespace OpenXLSX */ void XLWorksheet::clearImages() { - // Ensure m_embImages vector is cleared (safety check) - getEmbImages().clear(); + const std::vector embImgs = getEmbImages(); + std::vector::const_iterator embImgItr = embImgs.begin(); + for( ; embImgs.end() != embImgItr; ++embImgItr ){ + const XLEmbeddedImage& embImg = *embImgItr; + const std::string& relationshipId = embImg.relationshipId(); + + // Remove from DrawingML XML (in-memory XML document) + removeImageFromDrawingXML(relationshipId); - // TODO: Add verification checks to ensure complete cleanup: - // 1. Check that DrawingML XML has no image nodes - // 2. Check that relationships file has no image relationships - // 3. Check that no image files remain in the archive - // 5. Verify that getImageIDs() returns empty vector - // 6. Verify that imageCount() returns 0 + // Remove from relationships file (in-memory XML document) + removeImageFromRelationships(relationshipId); + } + + getEmbImages().clear(); - return; + // After clearing all images, remove the element from worksheet XML + removeDrawingElementIfEmpty(); + } + + /** + * @brief Remove the element from worksheet XML if there are no more images + * @note This is a helper function called after removing images to clean up the XML + */ + void XLWorksheet::removeDrawingElementIfEmpty() + { + // Only remove if there are no images left and the drawing element exists + if (getEmbImages().empty() && hasImagesInXML()) { + XMLNode docElement = xmlDocument().document_element(); + XMLNode drawingNode = docElement.child("drawing"); + if (!drawingNode.empty()) { + // Get the relationship ID from the drawing element + XMLAttribute rIdAttr = drawingNode.attribute("r:id"); + if (!rIdAttr.empty()) { + std::string drawingRelId = rIdAttr.value(); + // Remove the relationship from worksheet relationships + try { + relationships().deleteRelationship(drawingRelId); + } catch (...) { + // Relationship might not exist, ignore + } + } + // Remove the element from worksheet XML + docElement.remove_child(drawingNode); + + // Invalidate m_drawingML since the drawing element no longer exists + m_drawingML = XLDrawingML(nullptr); + } + } } //---------------------------------------------------------------------------------------------------------------------- @@ -432,9 +471,9 @@ namespace OpenXLSX // Determine MIME type from file extension ot imageData std::string extension = packagePath.substr(packagePath.find_last_of('.')); - XLMimeType mimeType = XLImageUtils::mimeTypeFromExtension(extension); + XLMimeType mimeType = XLImageUtils::extensionToMimeType(extension); if (XLMimeType::Unknown == mimeType) { - mimeType = XLImageUtils::detectMimeTypeEnum(imageData); + mimeType = XLImageUtils::detectMimeType(imageData); } // Convert enum back to XLContentType for findOrAddImage @@ -625,8 +664,8 @@ std::string XLWorksheet::embedImage(const XLImageAnchor& imageAnchor, XLImageShP if ((0 == anchor.displayWidthEMUs) || (0 == anchor.displayHeightEMUs)) { const uint32_t widthPixels = image->widthPixels(); const uint32_t heightPixels = image->heightPixels(); - anchor.displayWidthEMUs = XLImageUtils::pixelsToEMUs(widthPixels); - anchor.displayHeightEMUs = XLImageUtils::pixelsToEMUs(heightPixels); + anchor.displayWidthEMUs = XLImageUtils::pixelsToEmus(widthPixels); + anchor.displayHeightEMUs = XLImageUtils::pixelsToEmus(heightPixels); } if ((0 == anchor.actualWidthEMUs) || (0 == anchor.actualHeightEMUs)) { anchor.actualWidthEMUs = anchor.displayWidthEMUs; diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index b49767fe..2666810f 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -26,6 +26,7 @@ target_sources(OpenXLSXTests testXLColor.cpp testXLDateTime.cpp testXLFormula.cpp + testXLImage.cpp testXLRow.cpp testXLSheet.cpp ) diff --git a/Tests/testXLImage.cpp b/Tests/testXLImage.cpp index 7faeda41..10cd17d7 100644 --- a/Tests/testXLImage.cpp +++ b/Tests/testXLImage.cpp @@ -5,6 +5,10 @@ #include #include #include +#include +#include +#include +#include using namespace OpenXLSX; @@ -28,22 +32,13 @@ TEST_CASE("XLEmbeddedImage", "[XLEmbeddedImage]") // Add some images to the first worksheet try { - // Create a simple PNG image (1x1 pixel red PNG) - static const std::vector red1x1PNGData = { - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, - 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, - 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0x0F, 0x00, 0x00, - 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, - 0x42, 0x60, 0x82 - }; - + // Use shared tiny PNG image data from XLImageUtils const std::string imageId1 = worksheet1.embedImageFromImageData( - XLCellReference(2,2), red1x1PNGData, XLMimeType::PNG ); + XLCellReference(2,2), XLImageUtils::red1x1PNGData, XLMimeType::PNG ); const std::string imageId2 = worksheet1.embedImageFromImageData( - XLCellReference(5,3), red1x1PNGData, XLMimeType::PNG ); + XLCellReference(5,3), XLImageUtils::red1x1PNGData, XLMimeType::PNG ); const std::string imageId3 = worksheet2.embedImageFromImageData( - XLCellReference(1,1), red1x1PNGData, XLMimeType::PNG ); + XLCellReference(1,1), XLImageUtils::red1x1PNGData, XLMimeType::PNG ); REQUIRE(!imageId1.empty()); REQUIRE(!imageId3.empty()); REQUIRE(!imageId3.empty()); @@ -68,110 +63,846 @@ TEST_CASE("XLEmbeddedImage", "[XLEmbeddedImage]") // Clean up std::remove("./testXLEmbeddedImage.xlsx"); } -} + // ----------------------------------------------------------------- + // Additional sections consolidated under the XLEmbeddedImage test + // ----------------------------------------------------------------- + + SECTION("XLImageUtils - brief test") { + // Detect and validate + const XLMimeType mt = XLImageUtils::detectMimeType(XLImageUtils::red1x1PNGData); + REQUIRE(mt == XLMimeType::PNG); + REQUIRE(XLImageUtils::validateImageData(XLImageUtils::red1x1PNGData, mt)); + + // MIME/content-type round-trip + const XLContentType ct = XLImageUtils::mimeTypeToContentType(mt); + const XLMimeType mt2 = XLImageUtils::contentTypeToMimeType(ct); + REQUIRE(mt2 == XLMimeType::PNG); + + // Extension mapping + REQUIRE(XLImageUtils::extensionToMimeType(".png") == XLMimeType::PNG); + REQUIRE(XLImageUtils::mimeTypeToExtension(XLMimeType::PNG) == ".png"); + + // EMU conversion sanity + const uint32_t px = 31; + const uint32_t emu = XLImageUtils::pixelsToEmus(px); + REQUIRE(emu > 0u); + REQUIRE(XLImageUtils::emusToPixels(emu) >= 1u); + } + + SECTION("XLImageUtils - full test") { + // Test data - using pointers to avoid vector of references + struct TestImage { + const std::vector* data; + XLMimeType mimeType; + }; + const std::vector testImages = { + {&XLImageUtils::red1x1PNGData, XLMimeType::PNG}, + {&XLImageUtils::png31x15Data, XLMimeType::PNG}, + {&XLImageUtils::jpeg32x18Data, XLMimeType::JPEG}, + {&XLImageUtils::bmp33x16Data, XLMimeType::BMP}, + {&XLImageUtils::gif26x16Data, XLMimeType::GIF} + }; + + // Test detectMimeType() - Convert each test image data vector to mimetype + REQUIRE(XLImageUtils::detectMimeType(XLImageUtils::red1x1PNGData) == XLMimeType::PNG); + REQUIRE(XLImageUtils::detectMimeType(XLImageUtils::png31x15Data) == XLMimeType::PNG); + REQUIRE(XLImageUtils::detectMimeType(XLImageUtils::jpeg32x18Data) == XLMimeType::JPEG); + REQUIRE(XLImageUtils::detectMimeType(XLImageUtils::bmp33x16Data) == XLMimeType::BMP); + REQUIRE(XLImageUtils::detectMimeType(XLImageUtils::gif26x16Data) == XLMimeType::GIF); + + // Test with invalid data + const std::vector invalidData = {0x00, 0x01, 0x02, 0x03}; + REQUIRE(XLImageUtils::detectMimeType(invalidData) == XLMimeType::Unknown); + + // Test mimeTypeToString() and stringToMimeType() - Convert each to and from MIME type string + REQUIRE(XLImageUtils::mimeTypeToString(XLMimeType::PNG) == "image/png"); + REQUIRE(XLImageUtils::mimeTypeToString(XLMimeType::JPEG) == "image/jpeg"); + REQUIRE(XLImageUtils::mimeTypeToString(XLMimeType::BMP) == "image/bmp"); + REQUIRE(XLImageUtils::mimeTypeToString(XLMimeType::GIF) == "image/gif"); + REQUIRE(XLImageUtils::mimeTypeToString(XLMimeType::Unknown) == ""); + + REQUIRE(XLImageUtils::stringToMimeType("image/png") == XLMimeType::PNG); + REQUIRE(XLImageUtils::stringToMimeType("image/jpeg") == XLMimeType::JPEG); + REQUIRE(XLImageUtils::stringToMimeType("image/bmp") == XLMimeType::BMP); + REQUIRE(XLImageUtils::stringToMimeType("image/gif") == XLMimeType::GIF); + + // Round-trip tests + for (const auto& img : testImages) { + const std::string mimeStr = XLImageUtils::mimeTypeToString(img.mimeType); + REQUIRE(!mimeStr.empty()); + REQUIRE(XLImageUtils::stringToMimeType(mimeStr) == img.mimeType); + } + + // Convert random garbage MIME type string to mime type + REQUIRE(XLImageUtils::stringToMimeType("garbage/string") == XLMimeType::Unknown); + REQUIRE(XLImageUtils::stringToMimeType("") == XLMimeType::Unknown); + REQUIRE(XLImageUtils::stringToMimeType("image/unknown") == XLMimeType::Unknown); + REQUIRE(XLImageUtils::stringToMimeType("text/plain") == XLMimeType::Unknown); + + // Test mimeTypeToExtension() and extensionToMimeType() - Convert each to and from file extension + REQUIRE(XLImageUtils::mimeTypeToExtension(XLMimeType::PNG) == ".png"); + REQUIRE(XLImageUtils::mimeTypeToExtension(XLMimeType::JPEG) == ".jpg"); + REQUIRE(XLImageUtils::mimeTypeToExtension(XLMimeType::BMP) == ".bmp"); + REQUIRE(XLImageUtils::mimeTypeToExtension(XLMimeType::GIF) == ".gif"); + REQUIRE(XLImageUtils::mimeTypeToExtension(XLMimeType::Unknown) == ""); + + REQUIRE(XLImageUtils::extensionToMimeType(".png") == XLMimeType::PNG); + REQUIRE(XLImageUtils::extensionToMimeType(".jpg") == XLMimeType::JPEG); + REQUIRE(XLImageUtils::extensionToMimeType(".jpeg") == XLMimeType::JPEG); + REQUIRE(XLImageUtils::extensionToMimeType(".bmp") == XLMimeType::BMP); + REQUIRE(XLImageUtils::extensionToMimeType(".gif") == XLMimeType::GIF); + + // Round-trip tests + for (const auto& img : testImages) { + const std::string ext = XLImageUtils::mimeTypeToExtension(img.mimeType); + REQUIRE(!ext.empty()); + REQUIRE(XLImageUtils::extensionToMimeType(ext) == img.mimeType); + } + + // Convert random garbage extension to mime type + REQUIRE(XLImageUtils::extensionToMimeType(".xyz") == XLMimeType::Unknown); + REQUIRE(XLImageUtils::extensionToMimeType(".txt") == XLMimeType::Unknown); + REQUIRE(XLImageUtils::extensionToMimeType("") == XLMimeType::Unknown); + REQUIRE(XLImageUtils::extensionToMimeType("noextension") == XLMimeType::Unknown); + REQUIRE(XLImageUtils::extensionToMimeType("png") == XLMimeType::PNG); // without dot should still work + + // Test mimeTypeToContentType() and contentTypeToMimeType() - Convert each to and from XLContentType + REQUIRE(XLImageUtils::mimeTypeToContentType(XLMimeType::PNG) == XLContentType::ImagePNG); + REQUIRE(XLImageUtils::mimeTypeToContentType(XLMimeType::JPEG) == XLContentType::ImageJPEG); + REQUIRE(XLImageUtils::mimeTypeToContentType(XLMimeType::BMP) == XLContentType::ImageBMP); + REQUIRE(XLImageUtils::mimeTypeToContentType(XLMimeType::GIF) == XLContentType::ImageGIF); + REQUIRE(XLImageUtils::mimeTypeToContentType(XLMimeType::Unknown) == XLContentType::Unknown); + + REQUIRE(XLImageUtils::contentTypeToMimeType(XLContentType::ImagePNG) == XLMimeType::PNG); + REQUIRE(XLImageUtils::contentTypeToMimeType(XLContentType::ImageJPEG) == XLMimeType::JPEG); + REQUIRE(XLImageUtils::contentTypeToMimeType(XLContentType::ImageBMP) == XLMimeType::BMP); + REQUIRE(XLImageUtils::contentTypeToMimeType(XLContentType::ImageGIF) == XLMimeType::GIF); + REQUIRE(XLImageUtils::contentTypeToMimeType(XLContentType::Unknown) == XLMimeType::Unknown); + + // Round-trip tests + for (const auto& img : testImages) { + const XLContentType ct = XLImageUtils::mimeTypeToContentType(img.mimeType); + REQUIRE(ct != XLContentType::Unknown); + REQUIRE(XLImageUtils::contentTypeToMimeType(ct) == img.mimeType); + } + + // Test pixelsToEmus() and emusToPixels() - Use Catch RNG to pick random pixel value, convert back and forth + auto& rng = Catch::rng(); + std::uniform_int_distribution pixelDist(1, 1000); + + for (int i = 0; i < 10; ++i) { + // Generate random pixel value between 1 and 1000 + const uint32_t pixels = pixelDist(rng); + + const uint32_t emus = XLImageUtils::pixelsToEmus(pixels); + REQUIRE(emus > 0u); + + const uint32_t pixelsBack = XLImageUtils::emusToPixels(emus); + // Allow for some rounding error (within 1 pixel) + REQUIRE((pixelsBack == pixels || pixelsBack == pixels - 1 || pixelsBack == pixels + 1)); + } + + // Use Catch RNG to pick random EMU value, convert back and forth + std::uniform_int_distribution emuDist(9525, 9525000); // 1-1000 pixels + + for (int i = 0; i < 10; ++i) { + // Generate random EMU value between 9525 and 9525000 (1-1000 pixels) + const uint32_t emus = emuDist(rng); + + const uint32_t pixels = XLImageUtils::emusToPixels(emus); + REQUIRE(pixels >= 1u); + + const uint32_t emusBack = XLImageUtils::pixelsToEmus(pixels); + // Allow for some rounding error + const uint32_t diff = (emusBack > emus) ? (emusBack - emus) : (emus - emusBack); + REQUIRE(diff <= 9525u); // Within 1 pixel + } + + // Test pointsToEmus() + REQUIRE(XLImageUtils::pointsToEmus(1.0) == 12700u); + REQUIRE(XLImageUtils::pointsToEmus(0.0) == 0u); + REQUIRE(XLImageUtils::pointsToEmus(12.0) == 152400u); // 12 * 12700 + + // Use Catch RNG to pick random point values + std::uniform_real_distribution pointDist(0.0, 100.0); + + for (int i = 0; i < 10; ++i) { + const double points = pointDist(rng); // 0 to 100 points + const uint32_t emus = XLImageUtils::pointsToEmus(points); + REQUIRE(emus == static_cast(points * 12700)); + } + + // Test calculateAspectRatio() - Use Catch RNG to pick random width and height values + std::uniform_int_distribution dimensionDist(1, 999); + + for (int i = 0; i < 10; ++i) { + const uint32_t width = dimensionDist(rng); + const uint32_t height = dimensionDist(rng); + + const double aspectRatio = XLImageUtils::calculateAspectRatio(width, height); + REQUIRE(aspectRatio > 0.0); + REQUIRE(aspectRatio == static_cast(width) / static_cast(height)); + } + + // Test edge cases for calculateAspectRatio() + REQUIRE(XLImageUtils::calculateAspectRatio(100, 100) == 1.0); + REQUIRE(XLImageUtils::calculateAspectRatio(200, 100) == 2.0); + REQUIRE(XLImageUtils::calculateAspectRatio(100, 200) == 0.5); + REQUIRE(XLImageUtils::calculateAspectRatio(0, 100) == 0.0); // height == 0 case + + // Test getImageDimensions() - Get image dimensions from each of the test image data vectors + const auto dims1x1 = XLImageUtils::getImageDimensions(XLImageUtils::red1x1PNGData, XLMimeType::PNG); + REQUIRE(dims1x1.first == 1u); + REQUIRE(dims1x1.second == 1u); + + const auto dims31x15 = XLImageUtils::getImageDimensions(XLImageUtils::png31x15Data, XLMimeType::PNG); + REQUIRE(dims31x15.first == 31u); + REQUIRE(dims31x15.second == 15u); + + const auto dims32x18 = XLImageUtils::getImageDimensions(XLImageUtils::jpeg32x18Data, XLMimeType::JPEG); + REQUIRE(dims32x18.first == 32u); + REQUIRE(dims32x18.second == 18u); + + const auto dims33x16 = XLImageUtils::getImageDimensions(XLImageUtils::bmp33x16Data, XLMimeType::BMP); + REQUIRE(dims33x16.first == 33u); + REQUIRE(dims33x16.second == 16u); + + const auto dims26x16 = XLImageUtils::getImageDimensions(XLImageUtils::gif26x16Data, XLMimeType::GIF); + REQUIRE(dims26x16.first == 26u); + REQUIRE(dims26x16.second == 16u); + + // Test getImageDimensions() with auto-detection + const auto dims1x1Auto = XLImageUtils::getImageDimensions(XLImageUtils::red1x1PNGData); + REQUIRE(dims1x1Auto.first == 1u); + REQUIRE(dims1x1Auto.second == 1u); + + const auto dims31x15Auto = XLImageUtils::getImageDimensions(XLImageUtils::png31x15Data); + REQUIRE(dims31x15Auto.first == 31u); + REQUIRE(dims31x15Auto.second == 15u); + + const auto dims32x18Auto = XLImageUtils::getImageDimensions(XLImageUtils::jpeg32x18Data); + REQUIRE(dims32x18Auto.first == 32u); + REQUIRE(dims32x18Auto.second == 18u); + + // Test with invalid data + const auto dimsInvalid = XLImageUtils::getImageDimensions(invalidData); + REQUIRE(dimsInvalid.first == 0u); + REQUIRE(dimsInvalid.second == 0u); + + // Test isValidImageSize() + REQUIRE(XLImageUtils::isValidImageSize(1, 1) == true); + REQUIRE(XLImageUtils::isValidImageSize(100, 100) == true); + REQUIRE(XLImageUtils::isValidImageSize(10000, 10000) == true); + REQUIRE(XLImageUtils::isValidImageSize(10001, 10000) == false); + REQUIRE(XLImageUtils::isValidImageSize(10000, 10001) == false); + REQUIRE(XLImageUtils::isValidImageSize(0, 100) == false); + REQUIRE(XLImageUtils::isValidImageSize(100, 0) == false); + REQUIRE(XLImageUtils::isValidImageSize(0, 0) == false); + + // Test validateImageData() + for (const auto& img : testImages) { + REQUIRE(XLImageUtils::validateImageData(*img.data, img.mimeType) == true); + // Test with wrong MIME type + if (img.mimeType != XLMimeType::PNG) { + REQUIRE(XLImageUtils::validateImageData(*img.data, XLMimeType::PNG) == false); + } + } + + // Test with invalid data + REQUIRE(XLImageUtils::validateImageData(invalidData, XLMimeType::PNG) == false); + REQUIRE(XLImageUtils::validateImageData(invalidData, XLMimeType::JPEG) == false); + REQUIRE(XLImageUtils::validateImageData(std::vector(), XLMimeType::PNG) == false); + + // Test imageDataHash() + const size_t hash1 = XLImageUtils::imageDataHash(XLImageUtils::red1x1PNGData); + const size_t hash2 = XLImageUtils::imageDataHash(XLImageUtils::red1x1PNGData); + REQUIRE(hash1 == hash2); // Same data should produce same hash + + const size_t hash3 = XLImageUtils::imageDataHash(XLImageUtils::png31x15Data); + REQUIRE(hash1 != hash3); // Different data should produce different hash + + // Test imageDataStrHash() + std::string imageDataStr; + imageDataStr.insert(imageDataStr.end(), XLImageUtils::red1x1PNGData.begin(), XLImageUtils::red1x1PNGData.end()); + const size_t hashStr = XLImageUtils::imageDataStrHash(imageDataStr); + REQUIRE(hash1 == hashStr); // Same data in different formats should produce same hash + + // Test with empty data + REQUIRE(XLImageUtils::imageDataHash(std::vector()) == XLImageUtils::imageDataStrHash("")); + + // Test matchesElementName() + REQUIRE(XLImageUtils::matchesElementName("xdr:oneCellAnchor", "oneCellAnchor") == true); + REQUIRE(XLImageUtils::matchesElementName("oneCellAnchor", "oneCellAnchor") == true); + REQUIRE(XLImageUtils::matchesElementName("xdr:twoCellAnchor", "twoCellAnchor") == true); + REQUIRE(XLImageUtils::matchesElementName("twoCellAnchor", "twoCellAnchor") == true); + REQUIRE(XLImageUtils::matchesElementName("xdr:oneCellAnchor", "twoCellAnchor") == false); + REQUIRE(XLImageUtils::matchesElementName("oneCellAnchor", "twoCellAnchor") == false); + REQUIRE(XLImageUtils::matchesElementName("", "") == true); + REQUIRE(XLImageUtils::matchesElementName("prefix:name", "name") == true); + REQUIRE(XLImageUtils::matchesElementName("prefix:name", "prefix") == false); + } + + SECTION("XLImageManager - brief test") { + XLDocument doc; doc.create("./ut_mgr.xlsx"); + auto &mgr = doc.getImageManager(); + + const size_t before = mgr.getImageCount(); + auto img1 = mgr.findOrAddImage("", + XLImageUtils::red1x1PNGData, + XLImageUtils::mimeTypeToContentType(XLMimeType::PNG)); + auto img2 = mgr.findOrAddImage("", + XLImageUtils::red1x1PNGData, + XLImageUtils::mimeTypeToContentType(XLMimeType::PNG)); + REQUIRE(img1.get() != nullptr); + REQUIRE(img1.get() == img2.get()); + REQUIRE(mgr.getImageCount() == before + 1); + + doc.save(); doc.close(); std::remove("./ut_mgr.xlsx"); + } + + SECTION("XLImageManager/XLImage - full test") { + // Create empty document + XLDocument doc; + doc.create("./ut_mgr_full.xlsx"); + auto &mgr = doc.getImageManager(); + + // Test initial state + REQUIRE(mgr.getImageCount() == 0); + REQUIRE(mgr.begin() == mgr.end()); + std::string dbgMsg; + int verifyDataResult = mgr.verifyData(&dbgMsg); + REQUIRE(verifyDataResult== EXIT_SUCCESS); + + if(verifyDataResult== EXIT_SUCCESS){ + // Add two worksheets "SheetA", "SheetB" + doc.workbook().worksheet(1).setName("SheetA"); + doc.workbook().addWorksheet("SheetB"); + XLWorksheet sheetA = doc.workbook().worksheet("SheetA"); + XLWorksheet sheetB = doc.workbook().worksheet("SheetB"); + + // Map to track image hash -> image data from XLImageUtils + std::map*> hashToImageData; + + // Check vectors for each worksheet + XLEmbImgVec sheetAImagesCheck; + XLEmbImgVec sheetBImagesCheck; + + // Add two images to Worksheet "SheetA" + // Image 1: red1x1PNGData (will be shared with SheetB) + const std::string imageIdA1 = sheetA.embedImageFromImageData( + XLCellReference(1, 1), XLImageUtils::red1x1PNGData, XLMimeType::PNG); + REQUIRE(!imageIdA1.empty()); + const auto& embA1 = sheetA.getEmbImageByImageID(imageIdA1); + sheetAImagesCheck.push_back(embA1); + size_t hashA1 = XLImageUtils::imageDataHash(XLImageUtils::red1x1PNGData); + hashToImageData[hashA1] = &XLImageUtils::red1x1PNGData; + + // Image 2: png31x15Data (unique to SheetA) + const std::string imageIdA2 = sheetA.embedImageFromImageData( + XLCellReference(2, 2), XLImageUtils::png31x15Data, XLMimeType::PNG); + REQUIRE(!imageIdA2.empty()); + const auto& embA2 = sheetA.getEmbImageByImageID(imageIdA2); + sheetAImagesCheck.push_back(embA2); + size_t hashA2 = XLImageUtils::imageDataHash(XLImageUtils::png31x15Data); + hashToImageData[hashA2] = &XLImageUtils::png31x15Data; + + // Add three images to Worksheet "SheetB" + // Image 1: red1x1PNGData (shared with SheetA) + const std::string imageIdB1 = sheetB.embedImageFromImageData( + XLCellReference(1, 1), XLImageUtils::red1x1PNGData, XLMimeType::PNG); + REQUIRE(!imageIdB1.empty()); + const auto& embB1 = sheetB.getEmbImageByImageID(imageIdB1); + sheetBImagesCheck.push_back(embB1); + + // Image 2: jpeg32x18Data (unique to SheetB) + const std::string imageIdB2 = sheetB.embedImageFromImageData( + XLCellReference(3, 3), XLImageUtils::jpeg32x18Data, XLMimeType::JPEG); + REQUIRE(!imageIdB2.empty()); + const auto& embB2 = sheetB.getEmbImageByImageID(imageIdB2); + sheetBImagesCheck.push_back(embB2); + size_t hashB2 = XLImageUtils::imageDataHash(XLImageUtils::jpeg32x18Data); + hashToImageData[hashB2] = &XLImageUtils::jpeg32x18Data; + + // Image 3: bmp33x16Data (unique to SheetB) + const std::string imageIdB3 = sheetB.embedImageFromImageData( + XLCellReference(4, 4), XLImageUtils::bmp33x16Data, XLMimeType::BMP); + REQUIRE(!imageIdB3.empty()); + const auto& embB3 = sheetB.getEmbImageByImageID(imageIdB3); + sheetBImagesCheck.push_back(embB3); + size_t hashB3 = XLImageUtils::imageDataHash(XLImageUtils::bmp33x16Data); + hashToImageData[hashB3] = &XLImageUtils::bmp33x16Data; + + // Test: mgr.getImageCount() == 4 (four unique images) + REQUIRE(mgr.getImageCount() == 4); + REQUIRE(mgr.begin() != mgr.end()); + + // Get embedded images for each sheet + auto sheetAImagesPtr = mgr.getEmbImgsForSheetName("SheetA"); + auto sheetBImagesPtr = mgr.getEmbImgsForSheetName("SheetB"); + REQUIRE(sheetAImagesPtr != nullptr); + REQUIRE(sheetBImagesPtr != nullptr); + + XLEmbImgVec sheetAImages = *sheetAImagesPtr; + XLEmbImgVec sheetBImages = *sheetBImagesPtr; + + // Sort vectors by image ID for comparison + auto sortByImageId = [](const XLEmbeddedImage& a, const XLEmbeddedImage& b) { + return a.id() < b.id(); + }; + std::sort(sheetAImages.begin(), sheetAImages.end(), sortByImageId); + std::sort(sheetBImages.begin(), sheetBImages.end(), sortByImageId); + std::sort(sheetAImagesCheck.begin(), sheetAImagesCheck.end(), sortByImageId); + std::sort(sheetBImagesCheck.begin(), sheetBImagesCheck.end(), sortByImageId); + + // Test: sheetAImages == sheetAImagesCheck + REQUIRE(sheetAImages.size() == sheetAImagesCheck.size()); + for (size_t i = 0; i < sheetAImages.size(); ++i) { + REQUIRE(sheetAImages[i].id() == sheetAImagesCheck[i].id()); + } + + // Test: sheetBImages == sheetBImagesCheck + REQUIRE(sheetBImages.size() == sheetBImagesCheck.size()); + for (size_t i = 0; i < sheetBImages.size(); ++i) { + REQUIRE(sheetBImages[i].id() == sheetBImagesCheck[i].id()); + } + + // Test: mgr.verifyData() == EXIT_SUCCESS + dbgMsg.clear(); + REQUIRE(mgr.verifyData(&dbgMsg) == EXIT_SUCCESS); + + // Iterate through all images and test properties + for (auto imageItr = mgr.begin(); imageItr != mgr.end(); ++imageItr) { + XLImageShPtr image = *imageItr; + REQUIRE(image != nullptr); + + // Test: mgr.findImageByImageData(image->getImageData()) + std::vector imageData = image->getImageData(); + XLImageShPtr foundByData = mgr.findImageByImageData(imageData); + REQUIRE(foundByData != nullptr); + REQUIRE(foundByData.get() == image.get()); + + // Test: mgr.findImageByFilePackagePath(image->filePackagePath()) + XLImageShPtr foundByPath = mgr.findImageByFilePackagePath(image->filePackagePath()); + REQUIRE(foundByPath != nullptr); + REQUIRE(foundByPath.get() == image.get()); + + // Test: image->parentDoc() == doc + REQUIRE(image->parentDoc() == &doc); + + // Get hash and look up image data from map + size_t hash = image->imageDataHash(); + auto it = hashToImageData.find(hash); + REQUIRE(it != hashToImageData.end()); + const std::vector* expectedData = it->second; + + // Test: image->imageDataHash() + REQUIRE(image->imageDataHash() == hash); + REQUIRE(image->imageDataHash() == XLImageUtils::imageDataHash(*expectedData)); + + // Test: image->mimeType() + XLMimeType expectedMimeType = XLImageUtils::detectMimeType(*expectedData); + REQUIRE(image->mimeType() == expectedMimeType); + + // Test: image->extension() + std::string expectedExt = XLImageUtils::mimeTypeToExtension(expectedMimeType); + REQUIRE(image->extension() == expectedExt); + + // Test: image->filePackagePath() + REQUIRE(!image->filePackagePath().empty()); + REQUIRE(image->filePackagePath().find("xl/media/") == 0); + + // Test: image->widthPixels() and image->heightPixels() + auto expectedDims = XLImageUtils::getImageDimensions(*expectedData, expectedMimeType); + REQUIRE(image->widthPixels() == expectedDims.first); + REQUIRE(image->heightPixels() == expectedDims.second); + + // Test: image->verifyData() == EXIT_SUCCESS + dbgMsg.clear(); + REQUIRE(image->verifyData(&dbgMsg) == EXIT_SUCCESS); + } + + // Remove sheetB + doc.workbook().deleteSheet("SheetB"); + + // Test: mgr.getImageCount() == 4 (images still exist until prune) + REQUIRE(mgr.getImageCount() == 4); + } + + // Save document, triggering prune() + doc.save(); + + // Test: mgr.getImageCount() == 2 after prune + REQUIRE(mgr.getImageCount() == 2); // red1x1PNGData and png31x15Data + + doc.close(); + std::remove("./ut_mgr_full.xlsx"); + } + + + SECTION("XLWorksheet - embed image -- brief test") { + XLDocument doc; doc.create("./ut_ws.xlsx"); + auto ws = doc.workbook().worksheet(1); + + const size_t before = ws.imageCount(); + const std::string id = ws.embedImageFromImageData(XLCellReference(2, 2), + XLImageUtils::red1x1PNGData, + XLMimeType::PNG); + REQUIRE(!id.empty()); + REQUIRE(ws.imageCount() == before + 1); + const auto &emb = ws.getEmbImageByImageID(id); + REQUIRE(!emb.isEmpty()); + // XLCellReference(2,2) -> row 2, col 2 => "B2" + REQUIRE(emb.anchorCell() == std::string("B2")); + + // Remove and verify count restored + REQUIRE(ws.removeImageByImageID(id)); + REQUIRE(ws.imageCount() == before); + + doc.save(); doc.close(); std::remove("./ut_ws.xlsx"); + } + + + SECTION("XLImageAnchor -- embed image -- full test") { /* -COMPREHENSIVE XLEmbeddedImage UNIT TEST OUTLINE -======================================= - -This outline covers all functions related to embedded images in OpenXLSX: - -1. BASIC IMAGE CREATION AND EMBEDDING - - Test embedImage() with different image formats (PNG, JPEG, GIF, BMP) - - Test embedImage() with different data sources (vector, file path) - - Test embedImage() with different positioning (various row/column combinations) - - Test embedImage() with different anchor types (oneCellAnchor, twoCellAnchor) - - Test embedImage() with different sizing strategies (original, aspect ratio, exact dimensions) - -2. IMAGE QUERY OPERATIONS - - Test getImageInfos() - retrieve all images in worksheet - - Test getImageInfosAtCell() - retrieve images at specific cell - - Test getImageInfosInRange() - retrieve images in cell range - - Test getImageInfoByImageID() - retrieve specific image by ID - - Test getImageInfoByRelationshipId() - retrieve specific image by relationship ID - - Test imageCount() - verify correct count of images - -3. IMAGE MODIFICATION OPERATIONS - - Test removeImageByImageID() - remove specific image by ID - - Test removeImageByRelationshipId() - remove specific image by relationship ID - - Test clearImages() - remove all images from worksheet - - Test image positioning changes - - Test image size modifications - -4. IMAGE REGISTRY OPERATIONS - - Test refreshImageRegistry() - populate registry from XML - - Test populateImagesFromXML() - populate m_images vector from registry - - Test registry consistency with XML data - - Test registry updates after image operations - -5. DRAWINGML INTEGRATION - - Test DrawingML XML generation for different anchor types - - Test DrawingML XML parsing for Excel-generated files - - Test DrawingML validation and error handling - - Test DrawingML consistency with image registry - -6. RELATIONSHIPS FILE OPERATIONS - - Test relationship file creation and updates - - Test relationship ID generation and uniqueness - - Test relationship file parsing and validation - - Test relationship consistency with image data - -7. BINARY DATA HANDLING - - Test binary image data storage and retrieval - - Test MIME type detection and validation - - Test file extension handling - - Test binary data integrity verification - -8. ERROR HANDLING AND EDGE CASES - - Test invalid image data handling - - Test invalid positioning parameters - - Test duplicate image ID handling - - Test missing relationship file handling - - Test corrupted XML data handling - -9. PERFORMANCE AND MEMORY - - Test memory usage with large images - - Test performance with many images - - Test memory cleanup after image removal - - Test efficient registry operations - -10. CROSS-WORKSHEET OPERATIONS - - Test image operations across multiple worksheets - - Test image ID uniqueness across worksheets - - Test relationship ID uniqueness across worksheets - - Test document-level image operations - -11. FILE I/O OPERATIONS - - Test loading images from existing Excel files - - Test saving images to new Excel files - - Test round-trip operations (save/load/verify) - - Test compatibility with Excel-generated files - -12. DATA INTEGRITY VERIFICATION - - Test verifyData() for various image scenarios - - Test verifyUniqueImageIDs() for duplicate detection - - Test verifyDrawingMLConsistency() for XML validation - - Test verifyImagesVectorConsistency() for vector validation - - Test verifyBinaryImageData() for binary data validation - -13. CONST CORRECTNESS - - Test const member function behavior - - Test const reference returns - - Test const_cast usage validation - - Test mutable member usage - -14. THREAD SAFETY - - Test concurrent image operations - - Test static const member usage - - Test registry access from multiple threads - - Test XML data access from multiple threads - -15. API COMPATIBILITY - - Test backward compatibility with existing code - - Test API consistency across different image types - - Test return type consistency - - Test parameter validation - -This comprehensive test suite would ensure robust image embedding functionality -across all supported scenarios and edge cases. + XLImageAnchor::XLImageAnchor() + XLImageAnchor::reset(); + XLImageAnchor::initOneCell(); + XLImageAnchor::initTwoCell(); + XLImageAnchor::getFromCellReference() const; + XLImageAnchor::setFromCellReference( const XLCellReference& fromCellRef ); + XLImageAnchor::getToCellReference() const; + XLImageAnchor::setToCellReference(); + XLImageAnchor::setDisplaySizeWithAspectRatio(); + XLImageAnchor::setDisplaySizeWithAspectRatio(); + XLImageAnchor::setDisplaySizeWithAspectRatio(); + XLImageAnchor::isTwoCell() + XLImageAnchor::getDisplayDimensions() + XLImageAnchor::getActualDimensions() + XLImageAnchor::setActualDimensions() + XLImageAnchor::getDisplayDimensionsPixels() + XLImageAnchor::getActualDimensionsPixels() */ + auto& rng = Catch::rng(); + std::uniform_int_distribution rowDist(1, 200); + std::uniform_int_distribution colDist(1, 20); + std::uniform_int_distribution rowOffsetDist(1, 10000); + std::uniform_int_distribution colOffsetDist(1, 10000); + + const uint32_t fromRowA = rowDist(rng); + const uint16_t fromColA = colDist(rng); + const int32_t rowOffsetA = rowOffsetDist(rng); + const int32_t colOffsetA = colOffsetDist(rng); + + XLCellReference fromCellRefA( fromRowA, fromColA ); + + XLImageAnchor anchorA; + anchorA.initOneCell( fromCellRefA, rowOffsetA, colOffsetA ); + + REQUIRE(anchorA.fromRow == fromRowA - 1); + REQUIRE(anchorA.fromCol == fromColA - 1); + REQUIRE(anchorA.fromRowOffset == rowOffsetA); + REQUIRE(anchorA.fromColOffset == colOffsetA); + REQUIRE(anchorA.getFromCellReference() == fromCellRefA); + REQUIRE(!anchorA.isTwoCell()); + + const uint32_t fromRowB = rowDist(rng); + const uint16_t fromColB = colDist(rng); + const uint32_t toRowB = fromRowB + rowDist(rng); + const uint16_t toColB = fromColB + colDist(rng); + + XLCellReference fromCellRefB( fromRowB, fromColB ); + XLCellReference toCellRefB( toRowB, toColB ); + + XLImageAnchor anchorB; + anchorB.initTwoCell( fromCellRefB, toCellRefB ); + + REQUIRE(anchorB.fromRow == fromRowB - 1); + REQUIRE(anchorB.fromCol == fromColB - 1); + REQUIRE(anchorB.fromRowOffset == 0); + REQUIRE(anchorB.fromColOffset == 0); + REQUIRE(anchorB.getFromCellReference() == fromCellRefB); + REQUIRE(anchorB.toRow == toRowB - 1); + REQUIRE(anchorB.toCol == toColB - 1); + REQUIRE(anchorB.toRowOffset == 0); + REQUIRE(anchorB.toColOffset == 0); + REQUIRE(anchorB.getToCellReference() == toCellRefB); + REQUIRE(anchorB.isTwoCell()); + + const uint32_t fromRowC = rowDist(rng); + const uint16_t fromColC = colDist(rng); + const int32_t fromRowOffsetC = rowOffsetDist(rng); + const int32_t fromColOffsetC = colOffsetDist(rng); + const uint32_t toRowC = fromRowC + rowDist(rng); + const uint16_t toColC = fromColC + colDist(rng); + const int32_t toRowOffsetC = rowOffsetDist(rng); + const int32_t toColOffsetC = colOffsetDist(rng); + + XLCellReference fromCellRefC( fromRowC, fromColC ); + XLCellReference toCellRefC( toRowC, toColC ); + + XLImageAnchor anchorC; + anchorC.initTwoCell( fromCellRefC, toCellRefC, + fromRowOffsetC, fromColOffsetC, toRowOffsetC, toColOffsetC ); + + REQUIRE(anchorC.fromRow == fromRowC - 1); + REQUIRE(anchorC.fromCol == fromColC - 1); + REQUIRE(anchorC.fromRowOffset == fromRowOffsetC); + REQUIRE(anchorC.fromColOffset == fromColOffsetC); + REQUIRE(anchorC.getFromCellReference() == fromCellRefC); + REQUIRE(anchorC.toRow == toRowC - 1); + REQUIRE(anchorC.toCol == toColC - 1); + REQUIRE(anchorC.toRowOffset == toRowOffsetC); + REQUIRE(anchorC.toColOffset == toColOffsetC); + REQUIRE(anchorC.getToCellReference() == toCellRefC); + REQUIRE(anchorC.isTwoCell()); + + // Test setFromCellReference() and getFromCellReference() standalone + anchorC.setFromCellReference(fromCellRefB); + REQUIRE(anchorC.fromRow == fromRowB-1); + REQUIRE(anchorC.fromCol == fromColB-1); + REQUIRE(anchorC.getFromCellReference() == fromCellRefB); + anchorC.setFromCellReference(fromCellRefC); + REQUIRE(anchorC.fromRow == fromRowC-1); + REQUIRE(anchorC.fromCol == fromColC-1); + REQUIRE(anchorC.getFromCellReference() == fromCellRefC); + + // Test setToCellReference() and getToCellReference() standalone + anchorC.setToCellReference(toCellRefB); + REQUIRE(anchorC.toRow == toRowB-1); + REQUIRE(anchorC.toCol == toColB-1); + REQUIRE(anchorC.getToCellReference() == toCellRefB); + anchorC.setToCellReference(toCellRefC); + REQUIRE(anchorC.toRow == toRowC-1); + REQUIRE(anchorC.toCol == toColC-1); + REQUIRE(anchorC.getToCellReference() == toCellRefC); + + // Test setDisplaySizeWithAspectRatio() with pixel dimensions + const uint32_t testWidthPx = 100 + rowDist(rng) % 400; // 100-500 pixels + const uint32_t testHeightPx = 100 + rowDist(rng) % 400; // 100-500 pixels + const uint32_t maxWidthEmus = XLImageUtils::pixelsToEmus(300); + const uint32_t maxHeightEmus = XLImageUtils::pixelsToEmus(200); + anchorA.setDisplaySizeWithAspectRatio(testWidthPx, testHeightPx, maxWidthEmus, maxHeightEmus); + auto displayDims = anchorA.getDisplayDimensions(); + REQUIRE(displayDims.first > 0); + REQUIRE(displayDims.second > 0); + REQUIRE(displayDims.first <= maxWidthEmus); + REQUIRE(displayDims.second <= maxHeightEmus); + // Verify aspect ratio is maintained (within tolerance) + double expectedAspectRatio = XLImageUtils::calculateAspectRatio(testWidthPx, testHeightPx); + double actualAspectRatio = static_cast(displayDims.first) / static_cast(displayDims.second); + REQUIRE(std::abs(expectedAspectRatio - actualAspectRatio) < 0.01); + + // Test setDisplaySizeWithAspectRatio() with image data + const uint32_t maxWidthEmusH = XLImageUtils::pixelsToEmus(500); + const uint32_t maxHeightEmusH = XLImageUtils::pixelsToEmus(300); + anchorA.setDisplaySizeWithAspectRatio(XLImageUtils::png31x15Data, XLMimeType::PNG, + maxWidthEmusH, maxHeightEmusH); + auto displayDimsH = anchorA.getDisplayDimensions(); + REQUIRE(displayDimsH.first > 0); + REQUIRE(displayDimsH.second > 0); + REQUIRE(displayDimsH.first <= maxWidthEmusH); + REQUIRE(displayDimsH.second <= maxHeightEmusH); + // Verify aspect ratio matches image dimensions (31x15) + double expectedAspectRatioH = XLImageUtils::calculateAspectRatio(31, 15); + double actualAspectRatioH = static_cast(displayDimsH.first) / static_cast(displayDimsH.second); + REQUIRE(std::abs(expectedAspectRatioH - actualAspectRatioH) < 0.01); + + // Test getDisplayDimensions() + const uint32_t testDisplayWidth = 100000; // EMUs + const uint32_t testDisplayHeight = 50000; // EMUs + anchorA.displayWidthEMUs = testDisplayWidth; + anchorA.displayHeightEMUs = testDisplayHeight; + auto displayDimsI = anchorA.getDisplayDimensions(); + REQUIRE(displayDimsI.first == testDisplayWidth); + REQUIRE(displayDimsI.second == testDisplayHeight); + + // Test getActualDimensions() and setActualDimensions() + const uint32_t testActualWidth = 200000; // EMUs + const uint32_t testActualHeight = 100000; // EMUs + anchorA.setActualDimensions(testActualWidth, testActualHeight); + auto actualDimsJ = anchorA.getActualDimensions(); + REQUIRE(actualDimsJ.first == testActualWidth); + REQUIRE(actualDimsJ.second == testActualHeight); + + // Test getDisplayDimensionsPixels() + const uint32_t testDisplayWidthK = XLImageUtils::pixelsToEmus(150); + const uint32_t testDisplayHeightK = XLImageUtils::pixelsToEmus(75); + anchorA.displayWidthEMUs = testDisplayWidthK; + anchorA.displayHeightEMUs = testDisplayHeightK; + auto displayDimsPxK = anchorA.getDisplayDimensionsPixels(); + REQUIRE(displayDimsPxK.first >= 149); // Allow for rounding + REQUIRE(displayDimsPxK.first <= 151); + REQUIRE(displayDimsPxK.second >= 74); + REQUIRE(displayDimsPxK.second <= 76); + + // Test getActualDimensionsPixels() + const uint32_t testActualWidthL = XLImageUtils::pixelsToEmus(200); + const uint32_t testActualHeightL = XLImageUtils::pixelsToEmus(100); + anchorA.setActualDimensions(testActualWidthL, testActualHeightL); + auto actualDimsPxL = anchorA.getActualDimensionsPixels(); + REQUIRE(actualDimsPxL.first >= 199); // Allow for rounding + REQUIRE(actualDimsPxL.first <= 201); + REQUIRE(actualDimsPxL.second >= 99); + REQUIRE(actualDimsPxL.second <= 101); + + // Test setDisplaySizeWithAspectRatio() with various aspect ratios using Catch RNG + for (int i = 0; i < 5; ++i) { + const uint32_t widthPx = 50 + rowDist(rng) % 450; // 50-500 pixels + const uint32_t heightPx = 50 + rowDist(rng) % 450; // 50-500 pixels + const uint32_t maxWidthEmusM = XLImageUtils::pixelsToEmus(300); + const uint32_t maxHeightEmusM = XLImageUtils::pixelsToEmus(200); + anchorA.setDisplaySizeWithAspectRatio(widthPx, heightPx, maxWidthEmusM, maxHeightEmusM); + auto displayDimsM = anchorA.getDisplayDimensions(); + REQUIRE(displayDimsM.first > 0); + REQUIRE(displayDimsM.second > 0); + REQUIRE(displayDimsM.first <= maxWidthEmusM); + REQUIRE(displayDimsM.second <= maxHeightEmusM); + } + + // Test reset() + anchorC.reset(); + REQUIRE(anchorC.anchorType == XLImageAnchorType::Unknown); + REQUIRE(anchorC.fromRow == 0); + REQUIRE(anchorC.fromCol == 0); + REQUIRE(anchorC.displayWidthEMUs == 0); + REQUIRE(anchorC.displayHeightEMUs == 0); + } + + + SECTION("XLWorksheet - full test") { + + XLDocument doc; doc.create("./ut_ws.xlsx"); + auto ws = doc.workbook().worksheet(1); + + REQUIRE(ws.imageCount() == 0); + REQUIRE(!ws.hasImages()); + REQUIRE(!ws.hasImagesInXML()); + + std::string errMsg; + auto& rng = Catch::rng(); + std::uniform_int_distribution rowDist(1, 20); + std::uniform_int_distribution colDist(1, 5); + + const uint32_t fromRowA = rowDist(rng); + const uint16_t fromColA = colDist(rng); + XLCellReference fromCellRefA( fromRowA, fromColA ); + + const uint32_t fromRowB = rowDist(rng) + fromRowA; + const uint16_t fromColB = colDist(rng); + XLCellReference fromCellRefB( fromRowB, fromColB ); + XLImageAnchor anchorB; + anchorB.initOneCell( fromCellRefB ); + + const uint32_t fromRowC = fromRowB + rowDist(rng); + const uint16_t fromColC = colDist(rng); + XLCellReference fromCellRefC( fromRowC, fromColC ); + + const uint32_t fromRowD = fromRowC + rowDist(rng); + const uint16_t fromColD = colDist(rng); + const uint32_t toRowD = fromRowD + rowDist(rng); + const uint16_t toColD = fromColD + colDist(rng); + XLCellReference fromCellRefD( fromRowD, fromColD ); + XLCellReference toCellRefD( toRowD, toColD ); + XLImageAnchor anchorD; + const uint32_t maxWidthEmusD = XLImageUtils::pixelsToEmus(500); + const uint32_t maxHeightEmusD = XLImageUtils::pixelsToEmus(300); + anchorD.setDisplaySizeWithAspectRatio(XLImageUtils::gif26x16Data, + XLMimeType::GIF, maxWidthEmusD, maxHeightEmusD); + anchorD.initTwoCell( fromCellRefD, toCellRefD ); + + const std::string embImgIdA = ws.embedImageFromImageData( + fromCellRefA, XLImageUtils::png31x15Data, XLMimeType::PNG ); + const XLEmbeddedImage embImgA = ws.getEmbImageByImageID(embImgIdA); + const XLEmbeddedImage embImgAByRelId = + ws.getEmbImageByRelationshipId(embImgA.relationshipId()); + REQUIRE( !embImgIdA.empty() ); + REQUIRE( embImgA.id() == embImgIdA ); + REQUIRE( embImgA == embImgAByRelId ); + REQUIRE(embImgA.anchorCell() == fromCellRefA.address()); + REQUIRE(embImgA.verifyData(&errMsg) == EXIT_SUCCESS); + + const std::string embImgIdB = ws.embedImageFromImageData( + anchorB, XLImageUtils::jpeg32x18Data ); + const XLEmbeddedImage embImgB = ws.getEmbImageByImageID(embImgIdB); + const XLEmbeddedImage embImgBByRelId = + ws.getEmbImageByRelationshipId(embImgB.relationshipId()); + REQUIRE( !embImgIdB.empty() ); + REQUIRE( embImgB.id() == embImgIdB ); + REQUIRE( embImgB == embImgBByRelId ); + REQUIRE(embImgB.anchorCell() == fromCellRefB.address()); + REQUIRE(embImgB.verifyData(&errMsg) == EXIT_SUCCESS); + + const std::string bmp33x16fileName = "bmp33x16.bmp"; + { + std::ofstream file(bmp33x16fileName, std::ios::binary); + file.write(reinterpret_cast(XLImageUtils::bmp33x16Data.data()), + XLImageUtils::bmp33x16Data.size()); + } + const std::string embImgIdC = ws.embedImageFromFile( + fromCellRefC, bmp33x16fileName, XLMimeType::BMP ); + std::remove(bmp33x16fileName.c_str()); + const XLEmbeddedImage embImgC = ws.getEmbImageByImageID(embImgIdC); + const XLEmbeddedImage embImgCByRelId = + ws.getEmbImageByRelationshipId(embImgC.relationshipId()); + REQUIRE( !embImgIdC.empty() ); + REQUIRE( embImgC.id() == embImgIdC ); + REQUIRE( embImgC == embImgCByRelId ); + REQUIRE(embImgC.anchorCell() == fromCellRefC.address()); + REQUIRE(embImgC.verifyData(&errMsg) == EXIT_SUCCESS); + + const std::string gif26x16fileName = "gif26x16.gif"; + { + std::ofstream file(gif26x16fileName, std::ios::binary); + file.write(reinterpret_cast(XLImageUtils::gif26x16Data.data()), + XLImageUtils::gif26x16Data.size()); + } + const std::string embImgIdD = ws.embedImageFromFile( + anchorD, gif26x16fileName ); + std::remove(gif26x16fileName.c_str()); + const XLEmbeddedImage embImgD = ws.getEmbImageByImageID(embImgIdD); + const XLEmbeddedImage embImgDByRelId = + ws.getEmbImageByRelationshipId(embImgD.relationshipId()); + REQUIRE( !embImgIdD.empty() ); + REQUIRE( embImgD.id() == embImgIdD ); + REQUIRE( embImgD == embImgDByRelId ); + REQUIRE(embImgD.anchorCell() == fromCellRefD.address()); + REQUIRE(embImgD.verifyData(&errMsg) == EXIT_SUCCESS); + + REQUIRE(ws.imageCount() == 4); + REQUIRE(ws.hasImages()); + REQUIRE(ws.hasImagesInXML()); + REQUIRE(ws.verifyData(&errMsg) == EXIT_SUCCESS); + REQUIRE(doc.verifyData(&errMsg) == EXIT_SUCCESS); + + std::vector embImagesA = + ws.getEmbImagesAtCell(fromCellRefA.address()); + REQUIRE( !embImagesA.empty() ); + const std::string cellRangeAD = fromCellRefA.address() + ":" + + toCellRefD.address(); + std::vector embImagesAD = + ws.getEmbImagesInRange( cellRangeAD ); + REQUIRE( !embImagesAD.empty() ); + std::vector embImages = ws.getEmbImages(); + REQUIRE( !embImages.empty() ); + + doc.save(); + + ws.removeImageByImageID(embImgIdB); + REQUIRE(ws.imageCount() == 3); + ws.removeImageByRelationshipId(embImgD.relationshipId()); + REQUIRE(ws.imageCount() == 2); + REQUIRE(ws.verifyData(&errMsg) == EXIT_SUCCESS); + REQUIRE(doc.verifyData(&errMsg) == EXIT_SUCCESS); + ws.clearImages(); + REQUIRE(ws.imageCount() == 0); + REQUIRE(!ws.hasImages()); + REQUIRE(!ws.hasImagesInXML()); + REQUIRE(ws.verifyData(&errMsg) == EXIT_SUCCESS); + REQUIRE(doc.verifyData(&errMsg) == EXIT_SUCCESS); + + doc.close(); + std::remove("./ut_ws.xlsx"); + } +} + From d4603bb8937490df428c3fe4b0323b626cf024e8 Mon Sep 17 00:00:00 2001 From: Mac Stevens Date: Mon, 17 Nov 2025 12:59:14 -0800 Subject: [PATCH 13/13] Embedded Image Support -- bug fix --- OpenXLSX/sources/XLDrawingML.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenXLSX/sources/XLDrawingML.cpp b/OpenXLSX/sources/XLDrawingML.cpp index 0791ac5e..d04cc99d 100644 --- a/OpenXLSX/sources/XLDrawingML.cpp +++ b/OpenXLSX/sources/XLDrawingML.cpp @@ -264,7 +264,7 @@ namespace OpenXLSX } const std::pair imageDims = - XLImageUtils::getImageDimensions(imageData, mimeType); + XLImageUtils::getImageDimensions(imageData, mimeTypeEnum); setDisplaySizeWithAspectRatio( imageDims.first, imageDims.second, maxWidthEmus, maxHeightEmus );