2025-01-17 comments support is now working - testing stage

This commit is contained in:
Lars Uffmann 2025-01-17 18:37:03 +01:00
parent d5ec0ce78e
commit 6476b030f4
9 changed files with 506 additions and 111 deletions

View File

@ -121,6 +121,52 @@ int main()
auto tmo = result.tm();
cout << "Cell G1: (" << G1.typeAsString() << ") " << std::asctime(&tmo);
std::cout << std::endl;
std::cout << "XLWorksheet comments demo" << std::endl;
std::cout << "=========================" << std::endl;
std::cout << "wks.hasComments() is " << (wks.hasComments() ? " TRUE" : "FALSE")
<< ", wks.hasVmlDrawing() is " << (wks.hasVmlDrawing() ? " TRUE" : "FALSE")
<< ", wks.hasTables() is " << (wks.hasTables() ? " TRUE" : "FALSE") << std::endl;
XLComments &wksComments = wks.comments(); // fetch comments (and create if missing)
std::cout << "wks.comments() is " << (wksComments.valid() ? "valid" : "not valid") << std::endl;
XLTables &wksTables = wks.tables(); // fetch tables (and create if missing)
std::cout << "wks.tables() is " << (wksTables.valid() ? "valid" : "not valid") << std::endl;
std::cout << "wks.hasComments() is " << (wks.hasComments() ? " TRUE" : "FALSE")
<< ", wks.hasVmlDrawing() is " << (wks.hasVmlDrawing() ? " TRUE" : "FALSE")
<< ", wks.hasTables() is " << (wks.hasTables() ? " TRUE" : "FALSE") << std::endl;
// 2025-01-17: LibreOffice displays all comments with the same, LibreOffice, author - regardless of author id - to be tested with MS Office
std::string authors[10] { "Edgar Allan Poe", "Agatha Christie", "J.R.R. Tolkien", "David Eddings", "Daniel Suarez",
/**/ "Mary Shelley", "George Orwell", "Stanislaw Lem", "Ray Bradbury", "William Shakespeare" };
for( std::string author : authors ) {
wksComments.addAuthor( author );
uint16_t authorCount = wksComments.authorCount();
std::cout << "wksComments.authorCount is " << authorCount << std::endl;
std::cout << "wksComments.author(" << (authorCount - 1) << ") is " << wksComments.author(authorCount - 1) << std::endl;
}
wksComments.deleteAuthor(7);
wksComments.deleteAuthor(5);
wksComments.deleteAuthor(3);
uint16_t authorCount = wksComments.authorCount();
std::cout << "wksComments.authorCount is " << authorCount << " after deleting authors 7, 5 and 3" << std::endl;
for( uint16_t index = 0; index < authorCount; ++index )
std::cout << "wksComments.author(" << (index) << ") is " << wksComments.author(index) << std::endl;
wksComments.set("A1", "this comment is for author #1", 0);
wksComments.set("B2", "this comment is for author #2", 1);
wksComments.shape("B2").style().show(); // bit cumbersome to access, but it reflects the XML: <v:shape style="[..];visibility=visible">[..]</v:shape>
wksComments.set("C3", "this comment is for author #3", 2);
wksComments.set("C3", "this is an updated comment for author #3", 2); // overwrite a comment
wksComments.shape("C3").style().show();
wksComments.set("D1", "this comment is for author #5", 4);
std::cout << std::endl;
std::cout << "the comment in cell C4 is \"" << wksComments.get("C4") << "\"" << std::endl;
std::cout << "the comment in cell B2 is \"" << wksComments.get("B2") << "\"" << std::endl;
std::cout << std::endl;
std::cout << "XLWorksheet protection settings demo" << std::endl;
std::cout << "====================================" << std::endl;
@ -152,46 +198,9 @@ int main()
// wks.cell("D1").setCellFormat(newCellFormatIndex);
wks.range("C1:D1").setFormat(newCellFormatIndex); // unlock two cells
std::cout << "====================================" << std::endl;
std::cout << "==> the worksheet " << wks.name() << " is now protected with the password " << password << std::endl;
std::cout << std::endl;
std::cout << "XLWorksheet comments demo" << std::endl;
std::cout << "=========================" << std::endl;
std::cout << "wks.hasComments() is " << (wks.hasComments() ? " TRUE" : "FALSE")
<< ", wks.hasVmlDrawing() is " << (wks.hasVmlDrawing() ? " TRUE" : "FALSE")
<< ", wks.hasTables() is " << (wks.hasTables() ? " TRUE" : "FALSE") << std::endl;
XLComments &wksComments = wks.comments(); // fetch comments (and create if missing)
std::cout << "wks.comments() is " << (wksComments.valid() ? "valid" : "not valid") << std::endl;
XLTables &wksTables = wks.tables(); // fetch tables (and create if missing)
std::cout << "wks.tables() is " << (wksTables.valid() ? "valid" : "not valid") << std::endl;
std::cout << "wks.hasComments() is " << (wks.hasComments() ? " TRUE" : "FALSE")
<< ", wks.hasVmlDrawing() is " << (wks.hasVmlDrawing() ? " TRUE" : "FALSE")
<< ", wks.hasTables() is " << (wks.hasTables() ? " TRUE" : "FALSE") << std::endl;
// // 2025-01-13: XLComments in preparation: needs ^%$%#^ vmlDrawing support next :(
// std::string authors[10] { "Edgar Allan Poe", "Agatha Christie", "J.R.R. Tolkien", "David Eddings", "Daniel Suarez",
// /**/ "Mary Shelley", "George Orwell", "Stanislaw Lem", "Ray Bradbury", "William Shakespeare" };
//
// for( std::string author : authors ) {
// wksComments.addAuthor( author );
// uint16_t authorCount = wksComments.authorCount();
// std::cout << "wksComments.authorCount is " << authorCount << std::endl;
// std::cout << "wksComments.author(" << (authorCount - 1) << ") is " << wksComments.author(authorCount - 1) << std::endl;
// }
//
// wksComments.deleteAuthor(7);
// wksComments.deleteAuthor(5);
// wksComments.deleteAuthor(3);
// uint16_t authorCount = wksComments.authorCount();
// std::cout << "wksComments.authorCount is " << authorCount << " after deleting authors 7, 5 and 3" << std::endl;
// for( uint16_t index = 0; index < authorCount; ++index )
// std::cout << "wksComments.author(" << (index) << ") is " << wksComments.author(index) << std::endl;
//
// wksComments.set("A1", "this comment is for author #1", 0);
// wksComments.set("B2", "this comment is for author #2", 1);
// wksComments.set("C3", "this comment is for author #3", 2);
// wksComments.set("D1", "this comment is for author #5", 4);
doc.save();
doc.close();

View File

@ -58,19 +58,9 @@ YM M9 MM MM MM MM MM d' `MM. MM MM d' `MM.
#include "XLXmlData.hpp"
#include "XLXmlFile.hpp"
// workbook XML: <sheets><sheet>: get sheetId where name == <worksheet name>
// --> sheetId should be equal comment ID ## in xl/comments##.xml
// new comments XML:
// TODO:
// add to [Content_Types].xml:
// <Override PartName="/xl/comments1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"/>
// create file xl/worksheets/_rels/sheet1.xml.rels, including directory, if it does not exist
// add to sheet relationships:
// <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
// <Relationship Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" Target="/xl/comments1.xml" Id="R31ace4858b334db2"/>
// </Relationships>
// Id seems to be unused, so... whatever :) but should use GetNewRelsID
// TBD if the XLRelationships functionality can be re-used, or GetNewRelsID needs to be exported (currently local to the module in
// an anonymous namespace)
namespace OpenXLSX
{
@ -154,14 +144,12 @@ namespace OpenXLSX
bool deleteComment(const std::string& cellRef);
std::string get(const std::string& cellRef) const;
/**
* @brief get the comment (if any) for the referenced cell
* @param cellRef the cell address to check
* @return the comment for this cell - an empty string if no comment is set
*/
std::string get(std::string cellRef) const;
std::string get(std::string const& cellRef) const;
/**
* @brief set the comment for the referenced cell
@ -170,7 +158,12 @@ namespace OpenXLSX
* @param authorId set this author
* @return true upon success, false on failure
*/
bool set(std::string cellRef, std::string comment, uint16_t authorId = 0);
bool set(std::string const& cellRef, std::string const& comment, uint16_t authorId = 0);
/**
* @brief get the XLShape object for this comment
*/
XLShape shape(std::string const& cellRef);
/**
* @brief Print the XML contents of this XLComments instance using the underlying XMLNode print function

View File

@ -9,6 +9,10 @@ namespace OpenXLSX
{
inline constexpr uint16_t MAX_COLS = 16'384;
inline constexpr uint32_t MAX_ROWS = 1'048'576;
// anchoring a comment shape below these values was not possible in LibreOffice - TBC with MS Office
inline constexpr uint16_t MAX_SHAPE_ANCHOR_COLUMN = 13067; // column "SHO"
inline constexpr uint32_t MAX_SHAPE_ANCHOR_ROW = 852177;
} // namespace OpenXLSX
#endif // OPENXLSX_XLCONSTANTS_HPP

View File

@ -68,6 +68,9 @@ namespace OpenXLSX
// <div style="text-align:left;"/>
// </v:textbox>
extern const std::string ShapeNodeName; // = "v:shape"
extern const std::string ShapeTypeNodeName; // = "v:shapetype"
// NOTE: numerical values of XLShapeTextVAlign and XLShapeTextHAlign are shared with the same alignments from XLAlignmentStyle (XLStyles.hpp)
enum class XLShapeTextVAlign : uint8_t {
Center = 3, // value="center", both
@ -181,6 +184,91 @@ namespace OpenXLSX
};
struct XLShapeStyleAttribute {
std::string name;
std::string value;
};
class OPENXLSX_EXPORT XLShapeStyle {
public:
/**
* @brief
*/
XLShapeStyle();
/**
* @brief Constructor. Init XLShapeStyle properties from styleAttribute
* @param styleAttribute a string with the value of the style attribute of a v:shape element
*/
explicit XLShapeStyle(const std::string& styleAttribute);
/**
* @brief Constructor. Init XLShapeStyle properties from styleAttribute and link to the attribute so that setter functions directly modify it
* @param styleAttribute an XMLAttribute constructed with the style attribute of a v:shape element
*/
explicit XLShapeStyle(const XMLAttribute& styleAttribute);
private:
/**
* @brief get index of an attribute name within m_nodeOrder
* @return index of attribute in m_nodeOrder
* @return -1 if not found
*/
int16_t attributeOrderIndex(std::string const& attributeName) const;
/**
* @brief XLShapeStyle internal generic getter & setter functions
*/
XLShapeStyleAttribute getAttribute(std::string const& attributeName, std::string const& valIfNotFound = "") const;
bool setAttribute(std::string const& attributeName, std::string const& attributeValue);
// bool setAttribute(XLShapeStyleAttribute const& attribute);
public:
/**
* @brief XLShapeStyle getter functions
*/
std::string position() const;
uint16_t marginLeft() const;
uint16_t marginTop() const;
uint16_t width() const;
uint16_t height() const;
std::string msoWrapStyle() const;
std::string vTextAnchor() const;
bool hidden() const;
bool visible() const;
std::string raw() const { return m_style; }
/**
* @brief XLShapeStyle setter functions
*/
bool setPosition(std::string newPosition);
bool setMarginLeft(uint16_t newMarginLeft);
bool setMarginTop(uint16_t newMarginTop);
bool setWidth(uint16_t newWidth);
bool setHeight(uint16_t newHeight);
bool setMsoWrapStyle(std::string newMsoWrapStyle);
bool setVTextAnchor(std::string newVTextAnchor);
bool hide(); // set visibility:hidden
bool show(); // set visibility:visible
bool setRaw(std::string newStyle) { m_style = newStyle; return true; }
private:
mutable std::string m_style; // mutable so getter functions can update it from m_styleAttribute if the latter is not empty
std::unique_ptr<XMLAttribute> m_styleAttribute;
inline static const std::vector< std::string_view > m_nodeOrder = {
"position",
"margin-left",
"margin-top",
"width",
"height",
"mso-wrap-style",
"v-text-anchor",
"visibility"
};
};
class OPENXLSX_EXPORT XLShape {
friend class XLVmlDrawing; // for access to m_shapeNode in XLVmlDrawing::addShape
public: // ---------- Public Member Functions ---------- //
@ -234,9 +322,7 @@ namespace OpenXLSX
bool stroked() const; // v:shape attribute stroked "t" ("f"?)
std::string type() const; // v:shape attribute type, link to v:shapetype attribute id
bool allowInCell() const; // v:shape attribute o:allowincell "f"
std::string style() const; // v:shape attribute style
// // Example: style="position:absolute;margin-left:258pt;margin-top:0pt;width:81.65pt;height:50.9pt;mso-wrap-style:none;v-text-anchor:middle;visibility:hidden">
// // ==> should probably create a subclass XLShapeStyle to further disassemble this attribute
XLShapeStyle style(); // v:shape attribute style, but constructed from the XMLAttribute
// XLShapeShadow& shadow(); // v:shape subnode v:shadow
// XLShapeFill& fill(); // v:shape subnode v:fill
@ -251,11 +337,12 @@ namespace OpenXLSX
* @return true for success, false for failure
*/
// NOTE: setShapeId is not available because shape id is managed by the parent class in createShape
bool setFillColor(std::string newFillColor);
bool setFillColor(std::string const& newFillColor);
bool setStroked(bool set);
bool setType(std::string newType);
bool setType(std::string const& newType);
bool setAllowInCell(bool set);
bool setStyle(std::string newStyle);
bool setStyle(std::string const& newStyle);
bool setStyle(XLShapeStyle const& newStyle);
private: // ---------- Private Member Variables ---------- //
std::unique_ptr<XMLNode> m_shapeNode; /**< An XMLNode object with the v:shape item */
@ -275,6 +362,7 @@ namespace OpenXLSX
class OPENXLSX_EXPORT XLVmlDrawing : public XLXmlFile
{
friend class XLWorksheet; // for access to XLXmlFile::getXmlPath
friend class XLComments; // for access to firstShapeNode
public:
/**
* @brief Constructor

View File

@ -327,11 +327,12 @@ std::string XLComments::get(const std::string& cellRef) const
/**
* @details TODO: write doxygen headers for functions in this module
*/
bool XLComments::set(std::string cellRef, std::string commentText, uint16_t authorId)
bool XLComments::set(std::string const& cellRef, std::string const& commentText, uint16_t authorId)
{
XLCellReference destRef(cellRef);
uint32_t destRow = destRef.row();
uint16_t destCol = destRef.column();
bool newCommentCreated = false; // if false, try to find an existing shape before creating one
using namespace std::literals::string_literals;
XMLNode comment = m_commentList.first_child_of_type(pugi::node_element);
@ -353,12 +354,14 @@ bool XLComments::set(std::string cellRef, std::string commentText, uint16_t auth
comment = m_commentList.insert_child_after("comment", comment); // insert new comment at end of list
copyLeadingWhitespaces(m_commentList, comment.previous_sibling(), comment); // and copy whitespaces prefix from previous comment
}
newCommentCreated = true;
}
else {
XLCellReference ref(comment.attribute("ref").value());
if(ref.row() != destRow || ref.column() != destCol) { // if node has to be inserted *before* this one
comment = m_commentList.insert_child_before("comment", comment); // insert new comment
copyLeadingWhitespaces(m_commentList, comment, comment.next_sibling()); // and copy whitespaces prefix from next node
newCommentCreated = true;
}
else // node exists / was found
comment.remove_children(); // clear node content
@ -373,22 +376,82 @@ bool XLComments::set(std::string cellRef, std::string commentText, uint16_t auth
tNode.prepend_child(pugi::node_pcdata).set_value(commentText.c_str()); // finally, insert <t> node_pcdata value
if (m_vmlDrawing->valid()) {
std::cout << "XLWorksheet::vmlDrawing: m_vmlDrawing is accessible and valid!" << std::endl;
XLShape cShape{};
bool newShapeNeeded = newCommentCreated; // on new comments: create new shape
if (!newCommentCreated) {
try {
cShape = shape(cellRef); // for existing comments, try to access existing shape
}
catch (XLException const& e) {
newShapeNeeded = true; // not found: create fresh
}
}
if (newShapeNeeded)
cShape = m_vmlDrawing->createShape();
XLShape cShape = m_vmlDrawing->createShape();
std::cout << "created a new comment shape with id " << cShape.shapeId() << std::endl;
cShape.setFillColor("#ffffc0");
cShape.setStroked(true);
// setType: already done by XLVmlDrawing::createShape
cShape.setAllowInCell(false);
cShape.setStyle(
"position:absolute;margin-left:258pt;margin-top:0pt;width:81.65pt;height:50.9pt;mso-wrap-style:none;v-text-anchor:middle;visibility:hidden"
);
{
// XLShapeStyle shapeStyle("position:absolute;margin-left:100pt;margin-top:0pt;width:50pt;height:50.0pt;mso-wrap-style:none;v-text-anchor:middle;visibility:hidden");
XLShapeStyle shapeStyle{}; // default construct with meaningful values
cShape.setStyle(shapeStyle);
}
XLShapeClientData clientData = cShape.clientData();
clientData.setObjectType("Note");
clientData.setMoveWithCells();
clientData.setSizeWithCells();
clientData.setAnchor("3, 23, 0, 0, 4, 25, 3, 5");
{
constexpr const uint16_t leftColOffset = 1;
constexpr const uint16_t widthCols = 2;
constexpr const uint16_t topRowOffset = 1;
constexpr const uint16_t heightRows = 2;
uint16_t anchorLeftCol, anchorRightCol;
if( OpenXLSX::MAX_COLS - destCol > leftColOffset + widthCols ) {
anchorLeftCol = (destCol - 1) + leftColOffset;
anchorRightCol = (destCol - 1) + leftColOffset + widthCols;
}
else { // if anchor would overflow MAX_COLS: move column anchor to the left of destCol
anchorLeftCol = (destCol - 1) - leftColOffset - widthCols;
anchorRightCol = (destCol - 1) - leftColOffset;
}
uint32_t anchorTopRow, anchorBottomRow;
if( OpenXLSX::MAX_ROWS - destRow > topRowOffset + heightRows ) {
anchorTopRow = (destRow - 1) + topRowOffset;
anchorBottomRow = (destRow - 1) + topRowOffset + heightRows;
}
else { // if anchor would overflow MAX_ROWS: move row anchor to the top of destCol
anchorTopRow = (destRow - 1) - topRowOffset - heightRows;
anchorBottomRow = (destRow - 1) - topRowOffset;
}
if (anchorRightCol > MAX_SHAPE_ANCHOR_COLUMN)
std::cout << "XLComments::set WARNING: anchoring comment shapes beyond column "s
/**/ + XLCellReference::columnAsString(MAX_SHAPE_ANCHOR_COLUMN) + " may not get displayed correctly (LO Calc, TBD in Excel)"s << std::endl;
if (anchorBottomRow > MAX_SHAPE_ANCHOR_ROW)
std::cout << "XLComments::set WARNING: anchoring comment shapes beyond row "s
/**/ + std::to_string(MAX_SHAPE_ANCHOR_ROW ) + " may not get displayed correctly (LO Calc, TBD in Excel)"s << std::endl;
uint16_t anchorLeftOffsetInCell = 10, anchorRightOffsetInCell = 10;
uint16_t anchorTopOffsetInCell = 5, anchorBottomOffsetInCell = 5;
// clientData.setAnchor("3, 23, 0, 0, 4, 25, 3, 5");
using namespace std::literals::string_literals;
clientData.setAnchor(
std::to_string(anchorLeftCol) + ","s
+ std::to_string(anchorLeftOffsetInCell) + ","s
+ std::to_string(anchorTopRow) + ","s
+ std::to_string(anchorTopOffsetInCell) + ","s
+ std::to_string(anchorRightCol) + ","s
+ std::to_string(anchorRightOffsetInCell) + ","s
+ std::to_string(anchorBottomRow) + ","s
+ std::to_string(anchorBottomOffsetInCell)
);
}
clientData.setAutoFill(false);
clientData.setTextVAlign(XLShapeTextVAlign::Top);
clientData.setTextHAlign(XLShapeTextHAlign::Left);
@ -416,6 +479,33 @@ bool XLComments::set(std::string cellRef, std::string commentText, uint16_t auth
return true;
}
/**
* @details
*/
XLShape XLComments::shape(std::string const& cellRef)
{
if (!m_vmlDrawing->valid())
throw XLException("XLComments::shape: can not access any shapes when VML Drawing object is invalid");
XLCellReference destRef(cellRef);
uint32_t destRow = destRef.row() - 1; // for accessing a shape: x:Row and x:Column are zero-indexed
uint16_t destCol = destRef.column() - 1; // ..
XMLNode shape = m_vmlDrawing->firstShapeNode();
while (not shape.empty()) {
if ((destRow == shape.child("x:ClientData").child("x:Row").text().as_uint())
&&(destCol == shape.child("x:ClientData").child("x:Column").text().as_uint()))
return XLShape(shape);
do {
shape = shape.next_sibling_of_type(pugi::node_element);
} while (not shape.empty() && shape.name() != OpenXLSX::ShapeNodeName);
}
using namespace std::literals::string_literals;
throw XLException("XLComments::shape: not found for cell "s + cellRef + " - was XLComment::set invoked first?"s);
return XLShape();
}
/**
* @details Print the underlying XML using pugixml::xml_node::print
*/

View File

@ -516,19 +516,60 @@ void XLDocument::open(const std::string& fileName)
bool isWorkbookPath = (item.path().substr(1) == workbookPath); // determine once, use thrice
if (!isWorkbookPath && item.path().substr(0, 4) == "/xl/") {
if (item.path().substr(4, 7) == "comment") {
if( !m_suppressWarnings )
std::cout << "XLDocument::" << __func__ << ": ignoring comment xml file " << item.path() << std::endl;
}
else if ((item.path().substr(4, 16) == "worksheets/sheet")
if ((item.path().substr(4, 16) == "worksheets/sheet")
||(item.path().substr(4) == "sharedStrings.xml")
||(item.path().substr(4) == "styles.xml")
||(item.path().substr(4, 7) == "comment")
||(item.path().substr(4, 18) == "drawing/vmlDrawing")
||(item.path().substr(4, 12) == "tables/table")
||(item.path().substr(4, 11) == "theme/theme"))
{
m_data.emplace_back(/* parentDoc */ this,
/* xmlPath */ item.path().substr(1),
/* xmlID */ m_wbkRelationships.relationshipByTarget(item.path().substr(4)).id(),
/* xmlType */ item.type());
/*** BEGIN: dirty workaround to load worksheet dependencies ***/
if (item.path().substr(4, 16) == "worksheets/sheet") {
// TODO: implement actual content type overrides (and/or check sheet dependencies from sheet relationships)
size_t pos = item.path().rfind(".xml");
if (pos != std::string::npos) {
uint16_t sheetXmlNo = static_cast<uint16_t>(std::stoi(
item.path().substr(20, pos - 20) // determine number between /xl/worksheets/sheet and .xml
));
// std::cout << "loaded a worksheet with sheetXmlNo " << sheetXmlNo << std::endl;
using namespace std::literals::string_literals;
std::string relsFilename = "xl/worksheets/_rels/sheet"s + std::to_string(sheetXmlNo) + ".xml.rels"s;
if (m_archive.hasEntry(relsFilename)) {
// only add to m_data if not already contained
if (m_data.end() == std::find_if(m_data.begin(), m_data.end(), [&](const XLXmlData& theItem) { return theItem.getXmlPath() == relsFilename; }))
m_data.emplace_back(this, relsFilename, "", XLContentType::CustomProperties);
}
std::string vmlDrawingFilename = "xl/drawings/vmlDrawing"s + std::to_string(sheetXmlNo) + ".vml"s;
if (m_archive.hasEntry(vmlDrawingFilename)) {
// only add to m_data if not already contained
if (m_data.end() == std::find_if(m_data.begin(), m_data.end(), [&](const XLXmlData& theItem) { return theItem.getXmlPath() == vmlDrawingFilename; }))
m_data.emplace_back(this, vmlDrawingFilename, "", XLContentType::VMLDrawing);
}
std::string commentsFilename = "xl/comments"s + std::to_string(sheetXmlNo) + ".xml"s;
if (m_archive.hasEntry(commentsFilename)) {
// only add to m_data if not already contained
if (m_data.end() == std::find_if(m_data.begin(), m_data.end(), [&](const XLXmlData& theItem) { return theItem.getXmlPath() == commentsFilename; }))
m_data.emplace_back(this, commentsFilename, "", XLContentType::Comments);
}
std::string tablesFilename = "xl/tables/table"s + std::to_string(sheetXmlNo) + ".xml"s;
if (m_archive.hasEntry(tablesFilename)) {
// only add to m_data if not already contained
if (m_data.end() == std::find_if(m_data.begin(), m_data.end(), [&](const XLXmlData& theItem) { return theItem.getXmlPath() == tablesFilename; }))
m_data.emplace_back(this, tablesFilename, "", XLContentType::Table);
}
}
}
/*** END: dirty workaround to load worksheet dependencies ***/
}
else {
if( !m_suppressWarnings )

View File

@ -234,6 +234,166 @@ bool XLShapeClientData::setColumn (uint16_t newColumn)
{ return appendAndGetNode(*m_clientDataNode, "x:Column", m_nodeOrder, XLForceNamespace).text().set(newColumn); }
// ========== XLShape Member Functions
/**
* @details
*/
XLShapeStyle::XLShapeStyle()
: m_style(""),
m_styleAttribute(std::make_unique<XMLAttribute>())
{
setPosition("absolute");
setMarginLeft(100);
setMarginTop(0);
setWidth(80);
setHeight(50);
setMsoWrapStyle("none");
setVTextAnchor("middle");
hide();
}
/**
* @details
*/
XLShapeStyle::XLShapeStyle(const std::string& styleAttribute)
: m_style(styleAttribute),
m_styleAttribute(std::make_unique<XMLAttribute>())
{}
/**
* @details
*/
XLShapeStyle::XLShapeStyle(const XMLAttribute& styleAttribute)
: m_style(""),
m_styleAttribute(std::make_unique<XMLAttribute>(styleAttribute))
{}
/**
* @details find attributeName in m_nodeOrder, then return index
*/
int16_t XLShapeStyle::attributeOrderIndex(std::string const& attributeName) const
{
// std::string m_style{"position:absolute;margin-left:100pt;margin-top:0pt;width:50pt;height:50.0pt;mso-wrap-style:none;v-text-anchor:middle;visibility:hidden"};
auto attributeIterator = std::find(m_nodeOrder.begin(), m_nodeOrder.end(), attributeName);
if (attributeIterator == m_nodeOrder.end())
return -1;
return attributeIterator - m_nodeOrder.begin();
}
/**
* @details
*/
XLShapeStyleAttribute XLShapeStyle::getAttribute(std::string const& attributeName, std::string const& valIfNotFound) const
{
if (attributeOrderIndex(attributeName) == -1) {
using namespace std::literals::string_literals;
throw XLInternalError("XLShapeStyle.getAttribute: attribute "s + attributeName + " is not defined in class"s);
}
// if attribute is linked, re-read m_style each time in case the underlying XML has changed
if (not m_styleAttribute->empty()) m_style = std::string(m_styleAttribute->value());
size_t lastPos = 0;
XLShapeStyleAttribute result;
result.name = ""; // indicates "not found"
result.value = ""; // default in case attribute name is found but has no value
do {
size_t pos = m_style.find(';', lastPos);
std::string attrPair = m_style.substr(lastPos, pos - lastPos);
if (attrPair.length() > 0) {
size_t separatorPos = attrPair.find(':');
if (attributeName == attrPair.substr(0, separatorPos)) { // found!
result.name = attributeName;
if( separatorPos != std::string::npos )
result.value = attrPair.substr(separatorPos + 1);
break;
}
}
lastPos = pos+1;
} while (lastPos < m_style.length());
if (lastPos >= m_style.length()) // attribute was not found
result.value = valIfNotFound; // -> return default value
return result;
}
/**
* @details
*/
bool XLShapeStyle::setAttribute(std::string const& attributeName, std::string const& attributeValue)
{
int16_t attrIndex = attributeOrderIndex(attributeName);
if (attrIndex == -1) {
using namespace std::literals::string_literals;
throw XLInternalError("XLShapeStyle.setAttribute: attribute "s + attributeName + " is not defined in class"s);
}
// if attribute is linked, re-read m_style each time in case the underlying XML has changed
if (not m_styleAttribute->empty()) m_style = std::string(m_styleAttribute->value());
size_t lastPos = 0;
size_t appendPos = 0;
do {
size_t pos = m_style.find(';', lastPos);
std::string attrPair = m_style.substr(lastPos, pos - lastPos);
if (attrPair.length() > 0) {
size_t separatorPos = attrPair.find(':');
int16_t thisAttrIndex = attributeOrderIndex(attrPair.substr(0, separatorPos));
if (thisAttrIndex >= attrIndex) { // can insert or update
appendPos = (thisAttrIndex == attrIndex ? pos : lastPos); // if match: append from following attribute, if not found, append from current (insert)
break;
}
}
if(pos == std::string::npos)
lastPos = pos;
else
lastPos = pos + 1;
} while (lastPos < m_style.length());
if (lastPos >= m_style.length() || appendPos > m_style.length()) { // if attribute needs to be appended or was found at last position
appendPos = m_style.length(); // then nothing remains from m_style to follow (appended) attribute
// for semi-colon logic, lastPos needs to be capped at string length (so that it can be 0 for empty string)
if (lastPos > m_style.length()) lastPos = m_style.length();
}
using namespace std::literals::string_literals;
m_style = m_style.substr(0, lastPos) + ((lastPos != 0 && appendPos == m_style.length()) ? ";"s : ""s) // prepend ';' if attribute is appended to non-empty string
/**/ + attributeName + ":"s + attributeValue // insert attribute:value pair
/**/ + (appendPos < m_style.length() ? ";"s : ""s) + m_style.substr(appendPos); // append ';' if attribute is inserted before other data
// if attribute is linked, update it with the new style value
if (not m_styleAttribute->empty()) m_styleAttribute->set_value(m_style.c_str());
return true;
}
/**
* @details XLShapeStyle getter functions
*/
std::string XLShapeStyle::position () const { return getAttribute("position" ).value ; }
uint16_t XLShapeStyle::marginLeft () const { return static_cast<uint16_t>(std::stoi(getAttribute("margin-left" ).value)); }
uint16_t XLShapeStyle::marginTop () const { return static_cast<uint16_t>(std::stoi(getAttribute("margin-top" ).value)); }
uint16_t XLShapeStyle::width () const { return static_cast<uint16_t>(std::stoi(getAttribute("width" ).value)); }
uint16_t XLShapeStyle::height () const { return static_cast<uint16_t>(std::stoi(getAttribute("height" ).value)); }
std::string XLShapeStyle::msoWrapStyle() const { return getAttribute("mso-wrap-style").value ; }
std::string XLShapeStyle::vTextAnchor () const { return getAttribute("v-text-anchor" ).value ; }
bool XLShapeStyle::hidden () const { return ("hidden" == getAttribute("visibility" ).value ); }
bool XLShapeStyle::visible () const { return !hidden(); }
/**
* @details XLShapeStyle setter functions
*/
bool XLShapeStyle::setPosition (std::string newPosition) { return setAttribute("position", newPosition ); }
bool XLShapeStyle::setMarginLeft (uint16_t newMarginLeft) { return setAttribute("margin-left", std::to_string(newMarginLeft) + std::string("pt")); }
bool XLShapeStyle::setMarginTop (uint16_t newMarginTop) { return setAttribute("margin-top", std::to_string(newMarginTop) + std::string("pt")); }
bool XLShapeStyle::setWidth (uint16_t newWidth) { return setAttribute("width", std::to_string(newWidth) + std::string("pt")); }
bool XLShapeStyle::setHeight (uint16_t newHeight) { return setAttribute("height", std::to_string(newHeight) + std::string("pt")); }
bool XLShapeStyle::setMsoWrapStyle(std::string newMsoWrapStyle) { return setAttribute("mso-wrap-style", newMsoWrapStyle ); }
bool XLShapeStyle::setVTextAnchor (std::string newVTextAnchor) { return setAttribute("v-text-anchor", newVTextAnchor ); }
bool XLShapeStyle::hide () { return setAttribute("visibility", "hidden" ); }
bool XLShapeStyle::show () { return setAttribute("visibility", "visible" ); }
// ========== XLShape Member Functions
XLShape::XLShape() : m_shapeNode(std::make_unique<XMLNode>(XMLNode())) {}
@ -242,12 +402,12 @@ XLShape::XLShape(const XMLNode& node) : m_shapeNode(std::make_unique<XMLNode>(no
/**
* @details getter functions: return the shape's attributes
*/
std::string XLShape::shapeId() const { return m_shapeNode->attribute("id").value(); } // const for the user, managed by parent class XLVmlDrawing
std::string XLShape::fillColor() const { return appendAndGetAttribute(*m_shapeNode, "fillcolor", "#ffffc0").value(); }
bool XLShape::stroked() const { return appendAndGetAttribute(*m_shapeNode, "stroked", "t" ).as_bool(); }
std::string XLShape::type() const { return appendAndGetAttribute(*m_shapeNode, "type", "" ).value(); }
bool XLShape::allowInCell() const { return appendAndGetAttribute(*m_shapeNode, "o:allowincell", "f" ).as_bool(); }
std::string XLShape::style() const { return appendAndGetAttribute(*m_shapeNode, "style", "" ).value(); }
std::string XLShape::shapeId() const { return m_shapeNode->attribute("id").value() ; } // const for user, managed by parent
std::string XLShape::fillColor() const { return appendAndGetAttribute(*m_shapeNode, "fillcolor", "#ffffc0").value() ; }
bool XLShape::stroked() const { return appendAndGetAttribute(*m_shapeNode, "stroked", "t" ).as_bool() ; }
std::string XLShape::type() const { return appendAndGetAttribute(*m_shapeNode, "type", "" ).value() ; }
bool XLShape::allowInCell() const { return appendAndGetAttribute(*m_shapeNode, "o:allowincell", "f" ).as_bool() ; }
XLShapeStyle XLShape::style() { return XLShapeStyle(appendAndGetAttribute(*m_shapeNode, "style", "" ) ); }
XLShapeClientData XLShape::clientData() {
return XLShapeClientData(appendAndGetNode(*m_shapeNode, "x:ClientData", m_nodeOrder, XLForceNamespace));
@ -256,11 +416,12 @@ XLShapeClientData XLShape::clientData() {
/**
* @details setter functions: assign the shape's attributes
*/
bool XLShape::setFillColor (std::string newFillColor) { return appendAndSetAttribute(*m_shapeNode, "fillcolor", newFillColor ).empty() == false; }
bool XLShape::setStroked (bool set) { return appendAndSetAttribute(*m_shapeNode, "stroked", (set ? "t" : "f")).empty() == false; }
bool XLShape::setType (std::string newType) { return appendAndSetAttribute(*m_shapeNode, "type", newType ).empty() == false; }
bool XLShape::setAllowInCell(bool set) { return appendAndSetAttribute(*m_shapeNode, "o:allowincell", (set ? "t" : "f")).empty() == false; }
bool XLShape::setStyle (std::string newStyle) { return appendAndSetAttribute(*m_shapeNode, "style", newStyle ).empty() == false; }
bool XLShape::setFillColor (std::string const& newFillColor) { return appendAndSetAttribute(*m_shapeNode, "fillcolor", newFillColor ).empty() == false; }
bool XLShape::setStroked (bool set) { return appendAndSetAttribute(*m_shapeNode, "stroked", (set ? "t" : "f")).empty() == false; }
bool XLShape::setType (std::string const& newType) { return appendAndSetAttribute(*m_shapeNode, "type", newType ).empty() == false; }
bool XLShape::setAllowInCell(bool set) { return appendAndSetAttribute(*m_shapeNode, "o:allowincell", (set ? "t" : "f")).empty() == false; }
bool XLShape::setStyle (std::string const& newStyle) { return appendAndSetAttribute(*m_shapeNode, "style", newStyle ).empty() == false; }
bool XLShape::setStyle (XLShapeStyle const& newStyle) { return setStyle( newStyle.raw() ); }
// ========== XLVmlDrawing Member Functions
@ -353,8 +514,6 @@ XLVmlDrawing::XLVmlDrawing(XLXmlData* xmlData)
}
appendAndGetAttribute(pathNode, "gradientshapeok", "t");
appendAndGetAttribute(pathNode, "o:connecttype", "rect");
std::cout << "m_defaultShapeTypeId is " << m_defaultShapeTypeId << std::endl;
}

View File

@ -1640,19 +1640,25 @@ XLVmlDrawing& XLWorksheet::vmlDrawing()
XMLAttribute xdrNamespace = appendAndGetAttribute(docElement, "xmlns:xdr", "");
xdrNamespace = "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing";
uint16_t sheetXmlNo = sheetXmlNumber();
if (!parentDoc().hasSheetRelationships(sheetXmlNo))
std::ignore = relationships(); // create sheet relationships if not existing
std::cout << "worksheet drawing for sheetId " << sheetXmlNo << std::endl;
std::ignore = relationships(); // create sheet relationships if not existing
// ===== Trigger parentDoc to create drawing XML file and return it
uint16_t sheetXmlNo = sheetXmlNumber();
m_vmlDrawing = parentDoc().sheetVmlDrawing(sheetXmlNo); // fetch drawing for this worksheet
if (!m_vmlDrawing.valid())
throw XLException("XLWorksheet::vmlDrawing(): could not create drawing XML");
std::string drawingRelativePath = getPathARelativeToPathB( m_vmlDrawing.getXmlPath(), getXmlPath() );
XLRelationshipItem vmlDrawingRelationship;
if (!m_relationships.targetExists(drawingRelativePath))
m_relationships.addRelationship(XLRelationshipType::VMLDrawing, drawingRelativePath);
vmlDrawingRelationship = m_relationships.addRelationship(XLRelationshipType::VMLDrawing, drawingRelativePath);
else
vmlDrawingRelationship = m_relationships.relationshipByTarget(drawingRelativePath);
if (vmlDrawingRelationship.empty())
throw XLException("XLWorksheet::vmlDrawing(): could not add determine sheet relationship for VML Drawing");
XMLNode legacyDrawing = appendAndGetNode(docElement, "legacyDrawing", m_nodeOrder);
if (legacyDrawing.empty())
throw XLException("XLWorksheet::vmlDrawing(): could not add <legacyDrawing> element to worksheet XML");
appendAndSetAttribute(legacyDrawing, "r:id", vmlDrawingRelationship.id());
}
return m_vmlDrawing;
@ -1664,24 +1670,14 @@ XLVmlDrawing& XLWorksheet::vmlDrawing()
XLComments& XLWorksheet::comments()
{
if (!m_comments.valid()) {
std::ignore = relationships(); // create sheet relationships if not existing
// ===== Unfortunately, xl/vmlDrawing#.vml is needed to support comments: append xdr namespace attribute to worksheet if not present
std::ignore = vmlDrawing(); // create sheet VML drawing if not existing
if (!m_vmlDrawing.valid()) {
// ===== Unfortunately, xl/vmlDrawing#.vml is needed to support comments: append xdr namespace attribute to worksheet if not present
std::ignore = vmlDrawing(); // create sheet VML drawing if not existing
}
// test code for new shape object:
// uint32_t newShapeIndex = m_vmlDrawing.createShape();
// std::cout << "created a new XLShape with index " << newShapeIndex << std::endl;
// XLShape shape = m_vmlDrawing.shape(newShapeIndex);
uint16_t sheetXmlNo = sheetXmlNumber();
if (!parentDoc().hasSheetRelationships(sheetXmlNo))
std::ignore = relationships(); // create sheet relationships if not existing
std::cout << "worksheet comments for sheetId " << sheetXmlNo << std::endl;
// ===== Trigger parentDoc to create comment XML file and return it
uint16_t sheetXmlNo = sheetXmlNumber();
// std::cout << "worksheet comments for sheetId " << sheetXmlNo << std::endl;
m_comments = parentDoc().sheetComments(sheetXmlNo); // fetch comments for this worksheet
if (!m_comments.valid())
throw XLException("XLWorksheet::comments(): could not create comments XML");
@ -1700,13 +1696,12 @@ XLComments& XLWorksheet::comments()
XLTables& XLWorksheet::tables()
{
if (!m_tables.valid()) {
uint16_t sheetXmlNo = sheetXmlNumber();
if (!parentDoc().hasSheetRelationships(sheetXmlNo))
std::ignore = relationships(); // create sheet relationships if not existing
std::ignore = relationships(); // create sheet relationships if not existing
std::cout << "worksheet tables for sheetId " << sheetXmlNo << std::endl;
// ===== Trigger parentDoc to create tables XML file and return it
uint16_t sheetXmlNo = sheetXmlNumber();
// std::cout << "worksheet tables for sheetId " << sheetXmlNo << std::endl;
m_tables = parentDoc().sheetTables(sheetXmlNo); // fetch tables for this worksheet
if (!m_tables.valid())
throw XLException("XLWorksheet::tables(): could not create tables XML");

View File

@ -7,6 +7,22 @@ Microsoft Excel® files, with the .xlsx format.
As the heading says - the latest "Release" that is shown on https://github.com/troldal/OpenXLSX/releases is from 2021-11-06, and severely outdated - please pull / download the latest SW version directly from the repository in its current state. Link for those that do not want to use ```git```: https://github.com/troldal/OpenXLSX/archive/refs/heads/master.zip
## (aral-matrix) 17 January 2025 - Basic support for comments is implemented - testing stage
The support for comments is now in a stage where it can be used - please refer to Demo01 line 124ff for a quick "howto".
This feature is still in alpha stage, with a few open TBDs:
* the library currently assumes that comment entries are well sorted, first by row, then by column - if this is not the case, existing comments might not be readable / be found
* the VML Drawing support is basic at best, for lack of documentation. I have made most features available through ```XLShape XLComments::shape(std::string const& cellRef)``` - for methods of XLShape please refer to the ```XLDrawing``` header file
* missing support in XLShape for now:
```
// XLShapeShadow& shadow(); // v:shape subnode v:shadow
// XLShapeFill& fill(); // v:shape subnode v:fill
// XLShapeStroke& stroke(); // v:shape subnode v:stroke
// XLShapePath& path(); // v:shape subnode v:path
// XLShapeTextbox& textbox(); // v:shape subnode v:textbox
```
* missing: library still needs to add ```Override``` entries for the worksheet dependencies (comments, vmlDrawing, table)
* to be replaced with proper logic: library (XLDocument) currently loads worksheets dependencies in a workaround
## (aral-matrix) 14 January 2025 - Support for comments is work in progress - unstable :)
As the title says - I am working on comments support and will do the occasional commit to save my progress (aside from local backups). I will only submit patches that should not affect existing code stability, but if I break something (last patch I forgot to add a new module dependency to the cmake instructions), apologies for that - feel free to open an issue right away & I will try to fix it ASAP.
I hope to finalize comments support (and possibly tables / filters) by the weekend.