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
54 changes: 43 additions & 11 deletions src/odr/internal/oldms/presentation/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ 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 text of each slide, positioned in its text
boxes**, through the abstract model so the generic HTML renderer lays each slide
out as positioned frames. No character/paragraph styles, master/notes pages,
images, charts, tables, or animations.
boxes**, plus **direct character formatting** (font, size, bold, italic,
underline, color) as styled spans, through the abstract model so the generic
HTML renderer lays each slide out as positioned frames. No paragraph styles or
master-inherited formatting, master/notes pages, images, charts, tables, or
animations.

**Specs.** `[MS-PPT]` (the PowerPoint stream) + `[MS-ODRAW]` (the Office Art /
Escher drawing records) + `[MS-CFB]` container. Section numbers cited inline in
Expand All @@ -17,13 +19,14 @@ code and in the drawing-tree map below.
| File (`oldms/presentation/`) | Role |
|---|---|
| `ppt_structs.hpp` | `#pragma pack(1)` PODs (`RecordHeader`, atom bodies, `Anchor`) + `static_assert` sizes + `RecordType` / `SlideListInstance` enums |
| `ppt_io.{hpp,cpp}` | `read(...)` helpers over `std::istream` |
| `ppt_io.{hpp,cpp}` | `read(...)` helpers over `std::istream` + `parse_style_text_prop_atom` (StyleTextPropAtom β†’ `TextCFRun`s) |
| `ppt_parser.{hpp,cpp}` | `parse_tree(registry, files)` β†’ walks the stream, builds the element tree |
| `ppt_element_registry.{hpp,cpp}` | Flat element store + text & frame side-payloads |
| `ppt_element_registry.{hpp,cpp}` | Flat element store + text & frame side-payloads + per-element `TextStyle` map + font-name intern store |
| `ppt_document.{hpp,cpp}` | `internal::Document` subclass + the `ElementAdapter` |

Element tree shape: `root β†’ slide β†’ frame (one per text box) β†’ paragraph (split on
0x0D) β†’ text / line_break (0x0B)`.
Element tree shape: `root β†’ slide β†’ frame (one per text box) β†’ paragraph (split
on 0x0D) β†’ span (one per formatting run) β†’ text`, with `line_break` (0x0B)
elements between spans.

## Design decisions

Expand Down Expand Up @@ -91,8 +94,28 @@ non-grouped shapes, and master-placeholder geometry inheritance are deferred
(open work Β§1). Shapes with no text are dropped, so the group shape and pictures
disappear.

**Slide size hardcoded 10"Γ—7.5"** (`slide_page_layout`) and the `font_size = 11pt`
in the adapters is a placeholder hack (same as `.doc`/`.xls`) β€” both open work.
**Direct character formatting only, resolved to styled spans.** Each text atom
is kept raw (undecoded) until the **`StyleTextPropAtom`** (0x0FA1, Β§2.9.44)
that most closely follows it; its character runs (`TextCFRun`, counts in
UTF-16 units covering the text plus one implicit final paragraph mark) split
the text into spans. Each span and paragraph stores a resolved `TextStyle` in
a registry side-map (paragraphs keep their first run's style for
empty-paragraph height). The non-obvious bits:
- The **paragraph-level runs precede the character runs** in the atom and are
mask-skipped field by field (`TextPFException`, incl. the variable
`tabStops`).
- **Font names** come from the `FontCollection` (0x07D5, inside
`RT_Environment` 0x03F2), indexed by each `FontEntityAtom`'s `recInstance`,
interned in the registry (`TextStyle::font_name` is a `const char *`).
- **Colors** are `ColorIndexStruct`s: only explicit sRGB values (index `0xFE`)
are used; **scheme indexes are left unset** (they need the slide's color
scheme β€” open work).
- Text without a `StyleTextPropAtom`, or characters past the last run, get a
**default style of 18pt** β€” an approximation of the unread master text
styles (`TxMasterStyleAtom`, open work). This replaces the old flat `11pt`
placeholder.

**Slide size hardcoded 10"Γ—7.5"** (`slide_page_layout`) β€” open work.
`Document::is_editable()` β†’ `false`; `save` throws.

**Endianness.** Host byte order / LSB-first bit-fields assumed; shared `oldms/`
Expand All @@ -107,9 +130,13 @@ break (split like `doc_parser`); `0x09` tab kept; other controls dropped

## Tests

- `ppt_parse_style_text_prop_atom` β€” inline bytes: PF-run skipping (incl.
mask-dependent fields), CFStyle bold/italic, fontRef/size, explicit sRGB
color.
- `ppt_empty` (`empty.ppt`): 1 slide.
- `ppt_slides` (`slides.ppt`): 2 slides, 2 positioned frames each (all `at_page`
with `x/y/width/height`), distinct positions, exact per-box text.
- `ppt_style_various` (`style-various-1.ppt`): 8 slides, positioned frames,
per-box text, plus style assertions (44pt Arial black title; underlined blue
32pt link text).

Fixture-commit / reference-HTML wiring / `OutlineTextRefAtom` fixture are open
(Β§2 below).
Expand Down Expand Up @@ -200,6 +227,11 @@ and is independent:
unexercised β€” all current `.ppt` files are LibreOffice-authored (empty outline).
A PowerPoint-authored file using the outline indirection is needed. Pairs with
Β§2.3.
- **2.5a Master-inherited formatting.** Placeholder text without direct
formatting falls back to the 18pt default instead of the master's
`TxMasterStyleAtom` styles; scheme-indexed colors (`ColorIndexStruct`
index < 0xFE) are dropped for the same reason. Read the main master's text
styles + color scheme to resolve both.
- **2.5 Auto-field metacharacters dropped.** Slide-number/date/header/footer
placeholders (`RT_*MetaCharAtom`) are ignored. Low priority.
- **2.6 `slide_name` is empty.** Could return `"Slide N"` for the HTML page label.
Expand Down
32 changes: 22 additions & 10 deletions src/odr/internal/oldms/presentation/ppt_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class ElementAdapter final : public abstract::ElementAdapter,
public abstract::FrameAdapter,
public abstract::LineBreakAdapter,
public abstract::ParagraphAdapter,
public abstract::SpanAdapter,
public abstract::TextAdapter {
public:
ElementAdapter(const Document &document, ElementRegistry &registry)
Expand Down Expand Up @@ -127,6 +128,10 @@ class ElementAdapter final : public abstract::ElementAdapter,
paragraph_adapter(const ElementIdentifier element_id) const override {
return element_type(element_id) == ElementType::paragraph ? this : nullptr;
}
[[nodiscard]] const SpanAdapter *
span_adapter(const ElementIdentifier element_id) const override {
return element_type(element_id) == ElementType::span ? this : nullptr;
}
[[nodiscard]] const TextAdapter *
text_adapter(const ElementIdentifier element_id) const override {
return element_type(element_id) == ElementType::text ? this : nullptr;
Expand Down Expand Up @@ -191,12 +196,14 @@ class ElementAdapter final : public abstract::ElementAdapter,
[[maybe_unused]] const ElementIdentifier element_id) const override {
return {}; // TODO
}
[[nodiscard]] TextStyle paragraph_text_style(
[[maybe_unused]] const ElementIdentifier element_id) const override {
// Set a font size so empty paragraphs still have height.
TextStyle style{};
style.font_size = Measure("11pt");
return style;
[[nodiscard]] TextStyle
paragraph_text_style(const ElementIdentifier element_id) const override {
return stored_style(element_id);
}

[[nodiscard]] TextStyle
span_style(const ElementIdentifier element_id) const override {
return stored_style(element_id);
}

[[nodiscard]] std::string
Expand All @@ -210,13 +217,18 @@ class ElementAdapter final : public abstract::ElementAdapter,
}
[[nodiscard]] TextStyle text_style(
[[maybe_unused]] const ElementIdentifier element_id) const override {
// Set a font size so the text is not invisible.
TextStyle style{};
style.font_size = Measure("11pt");
return style;
// The enclosing span carries the character style.
return {};
}

private:
/// The character style stored for a paragraph or span element.
[[nodiscard]] TextStyle
stored_style(const ElementIdentifier element_id) const {
const TextStyle *style = m_registry->element_style(element_id);
return style != nullptr ? *style : TextStyle{};
}

// Converts one field of a frame's anchor (master units = 1/576 inch) to a
// Measure string, or nullopt when the frame has no anchor.
template <typename Selector>
Expand Down
23 changes: 23 additions & 0 deletions src/odr/internal/oldms/presentation/ppt_element_registry.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <odr/internal/oldms/presentation/ppt_element_registry.hpp>

#include <algorithm>
#include <stdexcept>

namespace odr::internal::oldms::presentation {
Expand All @@ -8,6 +9,8 @@ void ElementRegistry::clear() noexcept {
m_elements.clear();
m_texts.clear();
m_frames.clear();
m_styles.clear();
m_font_names.clear();
}

[[nodiscard]] std::size_t ElementRegistry::size() const noexcept {
Expand Down Expand Up @@ -97,6 +100,26 @@ void ElementRegistry::append_child(const ElementIdentifier parent_id,
element_at(parent_id).last_child_id = child_id;
}

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_element_style(const ElementIdentifier id,
TextStyle style) {
check_element_id(id);
m_styles[id] = std::move(style);
}

const TextStyle *
ElementRegistry::element_style(const ElementIdentifier id) const {
const auto it = m_styles.find(id);
return it != m_styles.end() ? &it->second : nullptr;
}

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
13 changes: 13 additions & 0 deletions src/odr/internal/oldms/presentation/ppt_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/internal/oldms/presentation/ppt_structs.hpp>

#include <deque>
#include <optional>
#include <string>
#include <tuple>
Expand Down Expand Up @@ -51,10 +53,21 @@ class ElementRegistry final {

void append_child(ElementIdentifier parent_id, ElementIdentifier child_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);

/// Character style of a span or paragraph element.
void set_element_style(ElementIdentifier id, TextStyle style);
[[nodiscard]] const TextStyle *element_style(ElementIdentifier id) const;

private:
std::vector<Element> m_elements;
std::unordered_map<ElementIdentifier, Text> m_texts;
std::unordered_map<ElementIdentifier, Frame> m_frames;
std::unordered_map<ElementIdentifier, TextStyle> m_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