Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- A cell of an ods file takes a style: `Sheet::set_cell_style` and the
`setCellStyle` op write the fill, the horizontal alignment, and bold,
- A cell of an ods or xlsx file takes a style: `Sheet::set_cell_style` and
the `setCellStyle` op write the fill, the horizontal alignment, and bold,
italic, underline, strikethrough, colour and size.

- **Fix**: in an ods file, a value written into a covered cell landed outside
the merge, and one written into an empty cell right of a merge failed. The
first now refuses, and the second lands in its cell.

- **Fix**: an xlsx column in a gap between `<col>` ranges took the width and
the style of the next range. It now has neither.

- **Fix**: many xlsx fills showed a wrong colour. A cell now shows its fill,
theme colours and tints, italic, underline, strikethrough and every
alignment.
Expand Down
3 changes: 2 additions & 1 deletion docs/design/spreadsheet-editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ separate scripts. The coordinates are the ones an op names, never a DOM index.

## Cell formatting

Status: `.ods` writes a cell style. The steps land as a stack, in this order:
Status: `.ods` and `.xlsx` write a cell style. The steps land as a stack, in
this order:

1. The xlsx reader reads what the writer writes: a solid fill from `fgColor`,
theme colours with their `tint`, italic, underline and strikethrough, and
Expand Down
28 changes: 11 additions & 17 deletions src/odr/internal/odf/odf_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <odr/internal/odf/odf_document.hpp>
#include <odr/internal/odf/odf_parser.hpp>
#include <odr/internal/xml/xml_util.hpp>

#include <algorithm>
#include <array>
Expand Down Expand Up @@ -749,19 +750,11 @@ std::string color_value(const Color &color) {
return fmt::format("#{:06x}", color.rgb());
}

void set_attribute(pugi::xml_node node, const char *name, const char *value) {
pugi::xml_attribute attribute = node.attribute(name);
if (!attribute) {
attribute = node.append_attribute(name);
}
attribute.set_value(value);
}

/// Writes the set fields of @p style as attributes of @p properties, the
/// asian and complex variants beside each western one.
void write_text_properties(pugi::xml_node properties, const TextStyle &style) {
const auto set = [&](const char *name, const std::string &value) {
set_attribute(properties, name, value.c_str());
xml::set_attribute(properties, name, value.c_str());
};

if (style.font_size.has_value()) {
Expand Down Expand Up @@ -874,7 +867,7 @@ std::string StyleRegistry::create_cell_style(pugi::xml_node automatic_styles,
if (base_node &&
std::strcmp(base_node.parent().name(), "office:automatic-styles") == 0) {
node = automatic_styles.append_copy(base_node);
set_attribute(node, "style:name", name.c_str());
xml::set_attribute(node, "style:name", name.c_str());
} else {
node = automatic_styles.append_child("style:style");
node.append_attribute("style:name").set_value(name.c_str());
Expand All @@ -885,15 +878,16 @@ std::string StyleRegistry::create_cell_style(pugi::xml_node automatic_styles,
}

if (cell.background_color.has_value()) {
set_attribute(properties_of(node, "style:table-cell-properties"),
"fo:background-color",
color_value(*cell.background_color).c_str());
xml::set_attribute(properties_of(node, "style:table-cell-properties"),
"fo:background-color",
color_value(*cell.background_color).c_str());
}
if (cell.horizontal_align.has_value()) {
set_attribute(properties_of(node, "style:table-cell-properties"),
"style:text-align-source", "fix");
set_attribute(properties_of(node, "style:paragraph-properties"),
"fo:text-align", text_align_value(*cell.horizontal_align));
xml::set_attribute(properties_of(node, "style:table-cell-properties"),
"style:text-align-source", "fix");
xml::set_attribute(properties_of(node, "style:paragraph-properties"),
"fo:text-align",
text_align_value(*cell.horizontal_align));
}
TextStyle text_properties = text;
text_properties.background_color.reset();
Expand Down
11 changes: 10 additions & 1 deletion src/odr/internal/ooxml/spreadsheet/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The design of the xlsx module. The feature checklist is in

Scope: read `xl/workbook.xml`, its sheets, the shared strings and the drawings
into the abstract model, one table per sheet. Cell styles resolve from
`xl/styles.xml`. Write a cell value, and save.
`xl/styles.xml`. Write a cell value and a cell style, and save.

## Design decisions

Expand Down Expand Up @@ -60,6 +60,15 @@ their ids and stop being reachable. A shared string is never written back into
it, so the cell becomes `t="inlineStr"`. A covered cell, a cell holding an
`f`, and a date, time or error value throw `UnsupportedOperation`.

**A cell style is a new `xf`, never an edit of the one the cell names.**
`sheet_set_cell_style` starts from the cell's `s`, else its row's where the
row states `customFormat`, else its column's `style`. `create_cell_format`
copies that `xf` and its `font`, applies the delta, and appends a `font`, a
`fill` and an `xf` only where no equal one exists, with `applyFont`,
`applyFill` and `applyAlignment` set. A `font` keeps the child order of
`CT_Font`. An empty `styleSheet` first gets the entries every `xf` needs.
`styles.xml` is a written part, so every save writes it from its dom.

A position the file states no `c` for is stated by `insert_cell`: the `c` goes
into its row in column order, a missing `row` into `sheetData` in row order,
and `dimension` widens around it. The cell map is keyed by position, so an
Expand Down
60 changes: 55 additions & 5 deletions src/odr/internal/ooxml/spreadsheet/ooxml_spreadsheet_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ namespace odr::internal::ooxml::spreadsheet {

namespace {
std::unique_ptr<abstract::ElementAdapter>
create_element_adapter(const Document &document, ElementRegistry &registry);
create_element_adapter(Document &document, ElementRegistry &registry);

/// The `workbook` `calcPr`, appended where it is missing. ECMA-376 18.2.27
/// orders the children, so a new one goes before the first that must follow it.
Expand Down Expand Up @@ -60,7 +60,9 @@ Document::Document(std::shared_ptr<abstract::ReadableFilesystem> files)
const AbsPath workbook_path("/xl/workbook.xml");
const auto [workbook_xml, workbook_relations] = parse_xml_(workbook_path);
m_written_parts.push_back(workbook_path);
const auto [styles_xml, _] = parse_xml_(AbsPath("/xl/styles.xml"));
const AbsPath styles_path("/xl/styles.xml");
const auto [styles_xml, _] = parse_xml_(styles_path);
m_written_parts.push_back(styles_path);

for (pugi::xml_node sheet_node :
workbook_xml.document_element().child("sheets").children("sheet")) {
Expand Down Expand Up @@ -112,6 +114,8 @@ const StyleRegistry &Document::style_registry() const {
return m_style_registry;
}

StyleRegistry &Document::style_registry() { return m_style_registry; }

bool Document::is_editable() const noexcept { return true; }

bool Document::is_savable(const bool encrypted) const noexcept {
Expand Down Expand Up @@ -184,7 +188,7 @@ using AdapterBase = internal::RegistryElementAdapter<

class ElementAdapter final : public AdapterBase {
public:
ElementAdapter(const Document &document, ElementRegistry &registry)
ElementAdapter(Document &document, ElementRegistry &registry)
: AdapterBase(registry), m_document(&document) {}

[[nodiscard]] std::string
Expand Down Expand Up @@ -347,6 +351,52 @@ class ElementAdapter final : public AdapterBase {
}
return result;
}
/// [ECMA-376] 18.3.1.4: a `c` without `s` shows its row's `s` where the
/// row states `customFormat`, else its column's `style`.
void sheet_set_cell_style(const ElementIdentifier element_id,
const std::uint32_t column, const std::uint32_t row,
const TableCellStyle &cell_style,
const TextStyle &text_style) const override {
const ElementRegistry::Sheet &sheet =
m_registry->sheet_element_at(element_id);
const ElementRegistry::Sheet::Cell *cell = sheet.cell(column, row);

ElementIdentifier cell_id = null_element_id;
if (cell == nullptr) {
cell_id = insert_cell(element_id, column, row);
} else {
cell_id = cell->element_id;
if (m_registry->sheet_cell_element_at(cell_id).is_covered) {
throw UnsupportedOperation();
}
}

pugi::xml_node node = get_node(cell_id);
std::uint32_t base = 0;
if (const pugi::xml_attribute style = node.attribute("s")) {
base = style.as_uint();
} else if (const pugi::xml_node row_node = node.parent();
row_node.attribute("customFormat").as_bool()) {
base = row_node.attribute("s").as_uint();
} else {
base = m_registry->sheet_element_at(element_id)
.column_node(column)
.attribute("style")
.as_uint();
}

const std::uint32_t format =
m_document->style_registry().create_cell_format(base, cell_style,
text_style);
pugi::xml_attribute attribute = node.attribute("s");
if (!attribute) {
const pugi::xml_attribute reference = node.attribute("r");
attribute = reference ? node.insert_attribute_after("s", reference)
: node.prepend_attribute("s");
}
attribute.set_value(format);
}

[[nodiscard]] TableCellStyle
sheet_cell_style(const ElementIdentifier element_id,
const std::uint32_t column,
Expand Down Expand Up @@ -593,7 +643,7 @@ class ElementAdapter final : public AdapterBase {
}

private:
const Document *m_document{nullptr};
Document *m_document{nullptr};

[[nodiscard]] pugi::xml_node
get_node(const ElementIdentifier element_id) const {
Expand Down Expand Up @@ -763,7 +813,7 @@ class ElementAdapter final : public AdapterBase {
};

std::unique_ptr<abstract::ElementAdapter>
create_element_adapter(const Document &document, ElementRegistry &registry) {
create_element_adapter(Document &document, ElementRegistry &registry) {
return std::make_unique<ElementAdapter>(document, registry);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class Document final : public internal::Document {

[[nodiscard]] const ElementRegistry &element_registry() const;
[[nodiscard]] const StyleRegistry &style_registry() const;
StyleRegistry &style_registry();

[[nodiscard]] bool is_editable() const noexcept override;
[[nodiscard]] bool is_savable(bool encrypted) const noexcept override;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ void ElementRegistry::append_sheet_cell(const ElementIdentifier sheet_id,
element_at(cell_id).parent_id = sheet_id;
}

void ElementRegistry::Sheet::register_column(
[[maybe_unused]] const std::uint32_t column_min,
const std::uint32_t column_max, const pugi::xml_node element) {
columns[column_max] = {.node = element};
void ElementRegistry::Sheet::register_column(const std::uint32_t column_min,
const std::uint32_t column_max,
const pugi::xml_node element) {
columns[column_max] = {.min = column_min, .node = element};
}

void ElementRegistry::Sheet::register_row(const std::uint32_t row,
Expand All @@ -93,8 +93,10 @@ void ElementRegistry::Sheet::register_cell(const std::uint32_t column,

const ElementRegistry::Sheet::Column *
ElementRegistry::Sheet::column(const std::uint32_t column) const {
// the ranges leave gaps, so the one ending at or after `column` may start
// past it
if (const auto it = util::map::lookup_greater_or_equals(columns, column);
it != std::end(columns)) {
it != std::end(columns) && it->second.min <= column) {
return &it->second;
}
return nullptr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class ElementRegistry final

struct Sheet final {
struct Column final {
std::uint32_t min{0};
pugi::xml_node node;
};

Expand Down
Loading
Loading