Skip to content
Open
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
52 changes: 39 additions & 13 deletions src/odr/internal/oldms/spreadsheet/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,23 @@
The **why** and the roadmap; what is concretely done lives in the code. Shared
`oldms/` conventions are in [`../AGENTS.md`](../AGENTS.md).

**Scope.** Extract the **visible cell text** of every worksheet through the
abstract model so the generic HTML renderer produces a plain table per sheet.
Every cell value is rendered as a *string* — no styles, number/date formats,
merged cells, drawings, or charts.
**Scope.** Extract the **visible cell text** of every worksheet, plus each
cell's **font formatting and fill color**, through the abstract model so the
generic HTML renderer produces a styled table per sheet. Every cell value is
rendered as a *string* — no number/date formats, merged cells, borders,
drawings, or charts.

**Specs.** `[MS-XLS]` (record stream, SST, cell records) + `[MS-CFB]` container.
Section numbers cited inline in code.
**Specs.** `[MS-XLS]` (record stream, SST, cell records, Font/XF/Palette) +
`[MS-CFB]` container. Section numbers cited inline in code.

## Module layout (sibling of `../text`, `../presentation`)

| File (`oldms/spreadsheet/`) | Role |
|---|---|
| `xls_structs.hpp` | `#pragma pack(1)` PODs for record bodies + `static_assert` sizes + record-type enum |
| `xls_io.{hpp,cpp}` | `BiffReader` (record walker with transparent `CONTINUE` hopping; the `[MS-XLS]` string readers + `expect_bof`), RK decoding, number formatting |
| `xls_parser.{hpp,cpp}` | `parse_tree` → globals (BoundSheet8 + SST) then one pass per sheet substream |
| `xls_element_registry.{hpp,cpp}` | Flat element store + `Sheet` (name, dimensions, cell-position map) and `SheetCell` payloads |
| `xls_parser.{hpp,cpp}` | `parse_tree` → globals (BoundSheet8 + SST + Font/XF/Palette) then one pass per sheet substream |
| `xls_element_registry.{hpp,cpp}` | Flat element store + `Sheet` (name, dimensions, cell-position map), `SheetCell` payloads, and the resolved per-XF `CellStyle` table |
| `xls_document.{hpp,cpp}` | `internal::Document` subclass + the `ElementAdapter` |

Tree shape: `sheet → sheet_cell → paragraph → text`, one `sheet_cell` per
Expand Down Expand Up @@ -58,9 +59,27 @@ bit-field structs (`RkNumber`, `UnicodeStringFlags`, flags of
`BoundSheet8Fixed`/`FormulaFixed`) — little-endian, LSB-first hosts only; shared
`oldms/` assumption, see [`../AGENTS.md`](../AGENTS.md).

**Adapters** expose `ValueType::string` for every cell, `sheet_cell_span` →
`{1,1}`, all `*_style` → `{}`, and the `font_size = 11pt` placeholder hack (same as
`.doc`/`.ppt`). `Document::is_editable()` → `false`; `save` throws.
**Cell formatting is resolved at parse time, per XF.** Each cell record's
`ixfe` is kept on the `SheetCell`; the globals pass collects `Font` (0x0031),
`XF` (0x00E0) and `Palette` (0x0092) and resolves every XF into a `CellStyle`
(a `TextStyle` from the font + a `TableCellStyle` fill), stored once in the
registry and indexed by `ixfe`. The adapters only look up: `text_style`/
`paragraph_text_style` walk up to the `sheet_cell` ancestor and return its
XF's `TextStyle`; `sheet_cell_style` returns the fill. The non-obvious bits:
- **`FontIndex` 4 does not exist** (§2.5.129): `ifnt` < 4 is zero-based,
\> 4 is one-based into the Font records in file order.
- **Colors are `Icv` indexes** (§2.5.161): 0x00–0x07 built-in constants,
0x08–0x3F the `Palette` record (or the spec's default palette when absent),
0x40/0x41/0x7FFF system/automatic → left unset.
- **Fills** (§2.5.20): `fls` 0 = none; solid (1) renders `icvFore`; the other
patterns are *approximated* by their foreground color.
- A `dyHeight` of 0 (allowed by §2.4.122) leaves `font_size` unset instead of
emitting invisible 0pt text. Font names are interned in a `std::deque` so
`TextStyle::font_name` (`const char *`) stays valid.

**Adapters** expose `ValueType::string` for every cell and `sheet_cell_span` →
`{1,1}`; sheet/column/row styles are still `{}`.
`Document::is_editable()` → `false`; `save` throws.

### Value formatting (the non-obvious bits)

Expand All @@ -79,6 +98,10 @@ bit-field structs (`RkNumber`, `UnicodeStringFlags`, flags of
(no flags byte there) + correct next-string position.
- `xls_decode_rk` — all four RK flag combinations + number formatting (raw on-disk
encodings, so it also pins the `RkNumber` bit-field layout).
- `xls_cell_styles` — synthetic one-sheet workbook (inline bytes): XF → Font
resolution incl. the skipped index 4, weight/italic/underline/strike, the
default palette, automatic colors, solid fill, unstyled empty positions.
- `xls_palette_record` — a `Palette` record overriding the default palette.
- `xls_empty` / `xls_file_example_10` / `xls_file_example_5000` — real fixtures
(names, dimensions, extents, string/number cells; the 5000-row file exercises
SST `CONTINUE` on real data).
Expand Down Expand Up @@ -108,8 +131,11 @@ ignore their format codes. Fix by following the format chain:

- **Merged cells**: `MergeCells` (0x00E5) → `sheet_cell_span`/
`sheet_cell_is_covered` (adapter stubs in place).
- **Styles**: fonts (`Font` 0x0031), fills/borders from `XF`; column widths
(`ColInfo` 0x007D) and row heights (`Row` 0x0208).
- **Remaining styles**: borders and alignment from `XF` (fields already
parsed, unused); column widths (`ColInfo` 0x007D) and row heights (`Row`
0x0208). `Blank`/`MulBlank` cells are skipped entirely, so a fill on an
empty cell is lost. Non-solid fill patterns render as their foreground
color instead of a pattern.
- **Hidden rows/columns** (`Row.fDyZero`, `ColInfo.fHidden`).
- **Typed cell values**: expose numeric/bool/date `ValueType`s instead of
pre-rendered strings.
Expand Down
49 changes: 30 additions & 19 deletions src/odr/internal/oldms/spreadsheet/xls_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,17 @@ class ElementAdapter final : public abstract::ElementAdapter,
return {};
}
[[nodiscard]] TableCellStyle
sheet_cell_style([[maybe_unused]] const ElementIdentifier element_id,
[[maybe_unused]] const std::uint32_t column,
[[maybe_unused]] const std::uint32_t row) const override {
(void)element_id;
(void)column;
(void)row;
return {};
sheet_cell_style(const ElementIdentifier element_id,
const std::uint32_t column,
const std::uint32_t row) const override {
const ElementIdentifier cell_id =
m_registry->sheet_element_at(element_id).cell(column, row);
if (cell_id == null_element_id) {
return {};
}
const ElementRegistry::SheetCell &cell =
m_registry->sheet_cell_element_at(cell_id);
return m_registry->cell_style_at(cell.ixfe).cell_style;
}

[[nodiscard]] TablePosition
Expand Down Expand Up @@ -213,12 +217,7 @@ class ElementAdapter final : public abstract::ElementAdapter,
}
[[nodiscard]] TextStyle
paragraph_text_style(const ElementIdentifier element_id) const override {
(void)element_id;
// TODO setting font size otherwise the text will be invisible. upstream
// this is used to make empty paragraphs works correctly
TextStyle style{};
style.font_size = Measure("11pt");
return style;
return cell_text_style(element_id);
}

[[nodiscard]] std::string
Expand All @@ -233,19 +232,31 @@ class ElementAdapter final : public abstract::ElementAdapter,
}
[[nodiscard]] TextStyle
text_style(const ElementIdentifier element_id) const override {
(void)element_id;
// TODO setting font size otherwise the text will be invisible. upstream
// this is used to make empty paragraphs works correctly
TextStyle style{};
style.font_size = Measure("11pt");
return style;
return cell_text_style(element_id);
Comment thread
andiwand marked this conversation as resolved.
}

private:
// TODO remove maybe_unused
[[maybe_unused]]
const Document *m_document{nullptr};
ElementRegistry *m_registry{nullptr};

/// The font style of the sheet_cell ancestor (paragraph and text elements
/// only exist inside cells).
[[nodiscard]] TextStyle
cell_text_style(const ElementIdentifier element_id) const {
ElementIdentifier id = element_id;
while (id != null_element_id &&
element_type(id) != ElementType::sheet_cell) {
id = m_registry->element_at(id).parent_id;
}
if (id == null_element_id) {
return {};
}
const ElementRegistry::SheetCell &cell =
m_registry->sheet_cell_element_at(id);
return m_registry->cell_style_at(cell.ixfe).text_style;
}
};

std::unique_ptr<abstract::ElementAdapter>
Expand Down
23 changes: 23 additions & 0 deletions src/odr/internal/oldms/spreadsheet/xls_element_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ void ElementRegistry::clear() noexcept {
m_texts.clear();
m_sheets.clear();
m_sheet_cells.clear();
m_cell_styles.clear();
m_font_names.clear();
}

[[nodiscard]] std::size_t ElementRegistry::size() const noexcept {
Expand Down Expand Up @@ -140,6 +142,27 @@ void ElementRegistry::append_sheet_cell(const ElementIdentifier sheet_id,
std::max(sheet.content.columns, cell.position.column + 1);
}

const char *ElementRegistry::intern_font_name(const std::string &name) {
if (const auto it = std::ranges::find(m_font_names, name);
it != m_font_names.end()) {
return it->c_str();
}
return m_font_names.emplace_back(name).c_str();
}

void ElementRegistry::set_cell_styles(std::vector<CellStyle> styles) noexcept {
m_cell_styles = std::move(styles);
}

const ElementRegistry::CellStyle &
ElementRegistry::cell_style_at(const std::uint16_t ixfe) const {
if (ixfe >= m_cell_styles.size()) {
throw std::out_of_range(
"ElementRegistry::cell_style_at: XF index out of range");
}
return m_cell_styles[ixfe];
}

void ElementRegistry::check_element_id(const ElementIdentifier id) const {
if (id == null_element_id) {
throw std::out_of_range("ElementRegistry::check_id: null identifier");
Expand Down
23 changes: 23 additions & 0 deletions src/odr/internal/oldms/spreadsheet/xls_element_registry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

#include <odr/definitions.hpp>
#include <odr/document_element.hpp>
#include <odr/style.hpp>
#include <odr/table_dimension.hpp>
#include <odr/table_position.hpp>

#include <deque>
#include <string>
#include <tuple>
#include <unordered_map>
Expand Down Expand Up @@ -43,6 +45,15 @@ class ElementRegistry final {

struct SheetCell final {
TablePosition position;
/// Index into the workbook's cell styles (the cell record's ixfe).
std::uint16_t ixfe{0};
};

/// Display properties resolved from an XF record and its Font: the text
/// (font) side and the cell (fill) side.
struct CellStyle final {
TextStyle text_style;
TableCellStyle cell_style;
};

void clear() noexcept;
Expand Down Expand Up @@ -72,11 +83,23 @@ class ElementRegistry final {
/// they are addressed via `Sheet::cell` (like the ooxml/spreadsheet module).
void append_sheet_cell(ElementIdentifier sheet_id, ElementIdentifier cell_id);

/// Stores a font name and returns a pointer that stays valid for the
/// registry's lifetime (`TextStyle::font_name` is a `const char *`).
const char *intern_font_name(const std::string &name);

void set_cell_styles(std::vector<CellStyle> styles) noexcept;
/// The resolved style of an XF record, by XF index (a cell's ixfe).
/// Throws if the index has no XF record.
[[nodiscard]] const CellStyle &cell_style_at(std::uint16_t ixfe) const;

private:
std::vector<Element> m_elements;
std::unordered_map<ElementIdentifier, Text> m_texts;
std::unordered_map<ElementIdentifier, Sheet> m_sheets;
std::unordered_map<ElementIdentifier, SheetCell> m_sheet_cells;
std::vector<CellStyle> m_cell_styles;
/// Deque: elements never move, so interned `c_str()`s stay valid.
std::deque<std::string> m_font_names;

void check_element_id(ElementIdentifier id) const;
void check_text_id(ElementIdentifier id) const;
Expand Down
Loading
Loading