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
50 changes: 31 additions & 19 deletions src/odr/internal/oldms/presentation/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ The **why** and the roadmap; what is concretely done lives in the code. Shared

**Scope.** Extract the **visible text of each slide, positioned in its text
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
underline, color) as styled spans, plus **pictures** (JPEG/PNG BLIPs
referenced by slide shapes), through the abstract model so the generic HTML
renderer lays each slide out as positioned frames. No paragraph styles or
master-inherited formatting/pictures, master/notes pages, charts, tables, or
animations.

**Specs.** `[MS-PPT]` (the PowerPoint stream) + `[MS-ODRAW]` (the Office Art /
Expand Down Expand Up @@ -87,12 +88,13 @@ unmodelled/optional: an absent slide list (0 slides), a shape with no anchor
(unpositioned frame), nested groups and non-`Sp` records in a group, any
unrecognised child.

**First cut: top-level shapes only.** Only direct children of the root
`OfficeArtSpgrContainer`, whose anchors are already in the slide's master-unit
system (master units = 1/576 inch → inches via `/576`). Nested-group transforms,
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.
**First cut: top-level shapes only.** Direct children of the root
`OfficeArtSpgrContainer` plus the drawing's optional non-grouped shape
([MS-ODRAW] 2.2.13), whose anchors are already in the slide's master-unit
system (master units = 1/576 inch → inches via `/576`). Nested-group
transforms and master-placeholder geometry inheritance are deferred (open
work §1). Shapes with neither text nor a picture are dropped, so the group
shape disappears.

**Direct character formatting only, resolved to styled spans.** Each text atom
is kept raw (undecoded) until the **`StyleTextPropAtom`** (0x0FA1, §2.9.44)
Expand All @@ -115,7 +117,19 @@ empty-paragraph height). The non-obvious bits:
styles (`TxMasterStyleAtom`, open work). This replaces the old flat `11pt`
placeholder.

**Slide size hardcoded 10"×7.5"** (`slide_page_layout`) — open work.
**Pictures resolve through the BLIP store.** A shape references its picture
via the `OfficeArtFOPT` `pib` property (real picture shapes) or `fillBlip`
(picture fills — how LibreOffice-exported `.ppt` places pictures), a one-based
index into the `OfficeArtBStoreContainer` of the document's drawing group.
Each `OfficeArtFBSE` locates the BLIP in the `/Pictures` (delay) stream at
`foDelay` (or embeds it); **JPEG and PNG** BLIPs become an `image` element
under the shape's frame, served to the renderer as an in-memory `odr::File`.
WMF/EMF/PICT/DIB/TIFF BLIPs are skipped (open work). A shape flagged
`fBackground` (`OfficeArtFSP`) gets a full-slide anchor when it has none and
is moved before the slide's other shapes so it renders underneath.

**Slide size** comes from the `DocumentAtom` (master units), with 10"×7.5" as
fallback; `slide_name` returns "Slide N" in presentation order.
`Document::is_editable()` → `false`; `save` throws.

**Endianness.** Host byte order / LSB-first bit-fields assumed; shared `oldms/`
Expand All @@ -135,8 +149,9 @@ break (split like `doc_parser`); `0x09` tab kept; other controls dropped
color.
- `ppt_empty` (`empty.ppt`): 1 slide.
- `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).
per-box text, style assertions (44pt Arial black title; underlined blue
32pt link text), and the slide-6 background picture (leading full-slide
frame with the PNG bytes from `/Pictures`).

Fixture-commit / reference-HTML wiring / `OutlineTextRefAtom` fixture are open
(§2 below).
Expand Down Expand Up @@ -201,9 +216,6 @@ and is independent:
and map each descendant's anchor from `[xLeft..xRight]×[yTop..yBottom]` onto the
group shape's own anchor rect in the parent, composing transforms down the
nesting, before the `/576` conversion.
- **1.2 Non-grouped shapes.** `OfficeArtDgContainer` (0xF002) also has an optional
direct `shape` (§2.2.13) for a shape not in a group — the current walk only
iterates the `OfficeArtSpgrContainer`. Rare; read that child too.
- **1.3 Optional / inherited anchor.** A shape without an `OfficeArtClientAnchor`
currently yields a frame with no position. PowerPoint placeholders often omit
it and inherit geometry from the matching placeholder on the **master slide**
Expand All @@ -214,9 +226,10 @@ and is independent:

## 2. Smaller shortcomings

- **2.1 Slide size hardcoded.** Real size is `DocumentAtom.slideSize`
(`RT_DocumentAtom` 0x03E9, first child of `DocumentContainer`) — a `PointStruct`
in master units. Read it; fall back to 10"×7.5" only if absent.
- **2.0 Picture formats and locations.** WMF/EMF/PICT/DIB/TIFF BLIPs are
skipped (no converter; DIB would need a synthesized BMP header). Pictures on
**master slides** are not rendered (masters are skipped entirely) — e.g.
LibreOffice photo decks put each photo on a per-slide master.
- **2.2 Reference-output HTML not wired.** `html_output_test` has no `ppt` case;
add reference HTML under `test/data/reference-output/…/output/ppt/` (needs the
`OpenDocument.test.output` submodule).
Expand All @@ -234,5 +247,4 @@ and is independent:
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.
- **2.7 Endianness** — shared `oldms/` shortcoming; see [`../AGENTS.md`](../AGENTS.md).
48 changes: 43 additions & 5 deletions src/odr/internal/oldms/presentation/ppt_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

#include <odr/document_path.hpp>
#include <odr/exceptions.hpp>
#include <odr/file.hpp>
#include <odr/style.hpp>

#include <odr/internal/abstract/filesystem.hpp>
#include <odr/internal/common/file.hpp>
#include <odr/internal/oldms/presentation/ppt_parser.hpp>
#include <odr/internal/util/document_util.hpp>

#include <memory>
#include <optional>
#include <string>

Expand Down Expand Up @@ -59,7 +61,8 @@ class ElementAdapter final : public abstract::ElementAdapter,
public abstract::LineBreakAdapter,
public abstract::ParagraphAdapter,
public abstract::SpanAdapter,
public abstract::TextAdapter {
public abstract::TextAdapter,
public abstract::ImageAdapter {
public:
ElementAdapter(const Document &document, ElementRegistry &registry)
: m_document(&document), m_registry(&registry) {}
Expand Down Expand Up @@ -136,10 +139,23 @@ class ElementAdapter final : public abstract::ElementAdapter,
text_adapter(const ElementIdentifier element_id) const override {
return element_type(element_id) == ElementType::text ? this : nullptr;
}
[[nodiscard]] const ImageAdapter *
image_adapter(const ElementIdentifier element_id) const override {
return element_type(element_id) == ElementType::image ? this : nullptr;
}

[[nodiscard]] PageLayout slide_page_layout(
[[maybe_unused]] const ElementIdentifier element_id) const override {
// Default 4:3 slide so the renderer has non-empty page dimensions.
// The DocumentAtom's slide size (master units = 1/576 inch), with the
// default 4:3 slide as fallback so the renderer has page dimensions.
if (const auto size = m_registry->slide_size(); size.has_value()) {
return {
.width = Measure(size->first / 576.0, DynamicUnit("in")),
.height = Measure(size->second / 576.0, DynamicUnit("in")),
.print_orientation = {},
.margin = {},
};
}
return {
.width = Measure("10in"),
.height = Measure("7.5in"),
Expand All @@ -151,9 +167,17 @@ class ElementAdapter final : public abstract::ElementAdapter,
[[maybe_unused]] const ElementIdentifier element_id) const override {
return null_element_id;
}
[[nodiscard]] std::string slide_name(
[[maybe_unused]] const ElementIdentifier element_id) const override {
return {};
[[nodiscard]] std::string
slide_name(const ElementIdentifier element_id) const override {
// Slides carry no names in the file; number them in presentation order.
std::size_t index = 1;
for (ElementIdentifier id =
m_registry->element_at(element_id).previous_sibling_id;
id != null_element_id;
id = m_registry->element_at(id).previous_sibling_id) {
++index;
}
return "Slide " + std::to_string(index);
}

[[nodiscard]] AnchorType frame_anchor_type(
Expand Down Expand Up @@ -221,6 +245,20 @@ class ElementAdapter final : public abstract::ElementAdapter,
return {};
}

[[nodiscard]] bool image_is_internal(
[[maybe_unused]] const ElementIdentifier element_id) const override {
return true;
}
[[nodiscard]] std::optional<odr::File>
image_file(const ElementIdentifier element_id) const override {
return odr::File(std::make_shared<MemoryFile>(
m_registry->image_element_at(element_id).data));
}
[[nodiscard]] std::string
image_href(const ElementIdentifier element_id) const override {
return m_registry->image_element_at(element_id).href;
}

private:
/// The character style stored for a paragraph or span element.
[[nodiscard]] TextStyle
Expand Down
40 changes: 40 additions & 0 deletions src/odr/internal/oldms/presentation/ppt_element_registry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ void ElementRegistry::clear() noexcept {
m_elements.clear();
m_texts.clear();
m_frames.clear();
m_images.clear();
m_styles.clear();
m_font_names.clear();
m_slide_size.reset();
}

[[nodiscard]] std::size_t ElementRegistry::size() const noexcept {
Expand Down Expand Up @@ -41,6 +43,14 @@ ElementRegistry::create_frame_element() {
return {element_id, element, it->second};
}

std::tuple<ElementIdentifier, ElementRegistry::Element &,
ElementRegistry::Image &>
ElementRegistry::create_image_element() {
const auto &[element_id, element] = create_element(ElementType::image);
auto [it, success] = m_images.emplace(element_id, Image{});
return {element_id, element, it->second};
}

ElementRegistry::Element &
ElementRegistry::element_at(const ElementIdentifier id) {
check_element_id(id);
Expand Down Expand Up @@ -77,6 +87,18 @@ ElementRegistry::frame_element_at(const ElementIdentifier id) const {
return m_frames.at(id);
}

ElementRegistry::Image &
ElementRegistry::image_element_at(const ElementIdentifier id) {
check_image_id(id);
return m_images.at(id);
}

const ElementRegistry::Image &
ElementRegistry::image_element_at(const ElementIdentifier id) const {
check_image_id(id);
return m_images.at(id);
}

void ElementRegistry::append_child(const ElementIdentifier parent_id,
const ElementIdentifier child_id) {
check_element_id(parent_id);
Expand All @@ -100,6 +122,16 @@ void ElementRegistry::append_child(const ElementIdentifier parent_id,
element_at(parent_id).last_child_id = child_id;
}

void ElementRegistry::set_slide_size(const std::int32_t width,
const std::int32_t height) {
m_slide_size = {width, height};
}

std::optional<std::pair<std::int32_t, std::int32_t>>
ElementRegistry::slide_size() const {
return m_slide_size;
}

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()) {
Expand Down Expand Up @@ -144,4 +176,12 @@ void ElementRegistry::check_frame_id(const ElementIdentifier id) const {
}
}

void ElementRegistry::check_image_id(const ElementIdentifier id) const {
check_element_id(id);
if (!m_images.contains(id)) {
throw std::out_of_range(
"ElementRegistry::check_id: image identifier not found");
}
}

} // namespace odr::internal::oldms::presentation
18 changes: 18 additions & 0 deletions src/odr/internal/oldms/presentation/ppt_element_registry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

#include <odr/internal/oldms/presentation/ppt_structs.hpp>

#include <cstdint>
#include <deque>
#include <optional>
#include <string>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>

namespace odr::internal::oldms::presentation {
Expand All @@ -35,21 +37,29 @@ class ElementRegistry final {
std::optional<Anchor> anchor;
};

struct Image final {
std::string data; //< the raw image file bytes (JPEG/PNG)
std::string href; //< pseudo-path naming the BLIP (no real container path)
};

void clear() noexcept;

[[nodiscard]] std::size_t size() const noexcept;

std::tuple<ElementIdentifier, Element &> create_element(ElementType type);
std::tuple<ElementIdentifier, Element &, Text &> create_text_element();
std::tuple<ElementIdentifier, Element &, Frame &> create_frame_element();
std::tuple<ElementIdentifier, Element &, Image &> create_image_element();

[[nodiscard]] Element &element_at(ElementIdentifier id);
[[nodiscard]] Text &text_element_at(ElementIdentifier id);
[[nodiscard]] Frame &frame_element_at(ElementIdentifier id);
[[nodiscard]] Image &image_element_at(ElementIdentifier id);

[[nodiscard]] const Element &element_at(ElementIdentifier id) const;
[[nodiscard]] const Text &text_element_at(ElementIdentifier id) const;
[[nodiscard]] const Frame &frame_element_at(ElementIdentifier id) const;
[[nodiscard]] const Image &image_element_at(ElementIdentifier id) const;

void append_child(ElementIdentifier parent_id, ElementIdentifier child_id);

Expand All @@ -61,17 +71,25 @@ class ElementRegistry final {
void set_element_style(ElementIdentifier id, TextStyle style);
[[nodiscard]] const TextStyle *element_style(ElementIdentifier id) const;

/// Slide dimensions from the DocumentAtom, in master units (1/576 inch).
void set_slide_size(std::int32_t width, std::int32_t height);
[[nodiscard]] std::optional<std::pair<std::int32_t, std::int32_t>>
slide_size() 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, Image> m_images;
std::unordered_map<ElementIdentifier, TextStyle> m_styles;
std::optional<std::pair<std::int32_t, std::int32_t>> m_slide_size;
/// 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;
void check_frame_id(ElementIdentifier id) const;
void check_image_id(ElementIdentifier id) const;
};

} // namespace odr::internal::oldms::presentation
Loading
Loading