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
5 changes: 5 additions & 0 deletions src/odr/internal/oldms/text/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ WordDocument stream
(`PlcBtePapx` β†’ `PapxFkp`) belongs here too.
- **Fields show only the cached result** (Β§2.8.25) β€” never evaluated. Acceptable
for "visible text".
- **No page layout** β€” `text_root_page_layout` returns `{}`, so the paginated
HTML renders a page box that hugs the text instead of a sheet of paper with
margins. Section properties (`PlcfSed` β†’ `Sepx`, `sprmSXaPage`/`sprmSYaPage`
for the size, `sprmSDxaLeft`/`Right`, `sprmSDyaTop`/`sprmSDyaBottom` for the
margins, Β§2.6.4) are unparsed.
- **Images / OLE / drawn objects** β€” anchor chars dropped; would need `PlcfSpa` /
Office Art (`dggInfo`).
- **Encrypted / obfuscated** β€” `fEncrypted`/`fObfuscated` parsed but not acted on;
Expand Down
7 changes: 7 additions & 0 deletions src/odr/internal/ooxml/text/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ interleaved list. `w:lvlText` is the template, `%N` naming a level's counter;
the shared expansion and the number formats are in `common/list_numbering.*`,
where ODF lowers to the same shape.

**Page layout comes from the first `w:sectPr` in document order.** The model
carries one `PageLayout` per text root, while Word carries one per section β€”
and `w:body/w:sectPr` describes the *last* section, a section being closed by
the `w:pPr/w:sectPr` of its final paragraph. Taking the first one in document
order therefore gives the layout the document opens with, and collapses to the
body's own for the single-section documents that are the norm.

**Style resolution mixes a static hierarchy with a runtime cascade.**
`StyleRegistry` indexes `w:style` by `w:styleId` and pre-flattens the
`w:basedOn` chain: each `Style` recursively resolves its parent, copies the
Expand Down
3 changes: 2 additions & 1 deletion src/odr/internal/ooxml/text/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ Roughly ordered by importance.
- [x] cell vertical alignment, borders
- [ ] cell width (parsed but not applied)
- [ ] table row styles
- [x] page layout (via master page)
- [x] page layout (`w:sectPr`: size, orientation, margins)
- [ ] one layout per section; the first section's applies to the document
- [ ] graphic / drawing styles

## References
Expand Down
44 changes: 43 additions & 1 deletion src/odr/internal/ooxml/text/ooxml_text_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,45 @@ namespace odr::internal::ooxml::text {
namespace {
std::unique_ptr<abstract::ElementAdapter>
create_element_adapter(const Document &document, ElementRegistry &registry);

/// `w:top` and `w:bottom` are signed: a negative margin lets the header flow
/// into the body, which as a CSS margin would push the text off the page.
std::optional<Measure> read_page_margin(const pugi::xml_attribute attribute) {
const std::optional<Measure> margin = read_twips_attribute(attribute);
if (margin.has_value() && margin->magnitude() < 0) {
return Measure(0, margin->unit());
}
return margin;
}

/// [ECMA-376] 17.6.17 `w:sectPr`. The body's own are the *last* section's, so
/// the first in document order is the one the document opens with.
PageLayout read_page_layout(const pugi::xml_node body) {
const pugi::xml_node section_properties =
body.select_node(".//w:sectPr").node();

PageLayout result;

const pugi::xml_node page_size = section_properties.child("w:pgSz");
result.width = read_twips_attribute(page_size.attribute("w:w"));
result.height = read_twips_attribute(page_size.attribute("w:h"));
if (const pugi::xml_attribute orientation = page_size.attribute("w:orient")) {
result.print_orientation =
std::strcmp("landscape", orientation.value()) == 0
? PrintOrientation::landscape
: PrintOrientation::portrait;
}

const pugi::xml_node page_margin = section_properties.child("w:pgMar");
result.margin.right = read_page_margin(page_margin.attribute("w:right"));
result.margin.top = read_page_margin(page_margin.attribute("w:top"));
result.margin.left = read_page_margin(page_margin.attribute("w:left"));
result.margin.bottom = read_page_margin(page_margin.attribute("w:bottom"));

return result;
}
} // namespace

Document::Document(std::shared_ptr<abstract::ReadableFilesystem> files)
: internal::Document(FileType::office_open_xml_document, DocumentType::text,
std::move(files)) {
Expand All @@ -40,6 +77,9 @@ Document::Document(std::shared_ptr<abstract::ReadableFilesystem> files)
m_document_relations =
parse_relationships(*m_files, AbsPath("/word/document.xml"));

m_page_layout =
read_page_layout(m_document_xml.document_element().child("w:body"));

m_root_element = parse_tree(
m_element_registry, m_document_xml.document_element().child("w:body"));

Expand Down Expand Up @@ -69,6 +109,8 @@ const Relations &Document::document_relations() const {
return m_document_relations;
}

const PageLayout &Document::page_layout() const { return m_page_layout; }

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

bool Document::is_savable(const bool encrypted) const noexcept {
Expand Down Expand Up @@ -242,7 +284,7 @@ class ElementAdapter final : public abstract::ElementAdapter,

[[nodiscard]] PageLayout text_root_page_layout(
[[maybe_unused]] const ElementIdentifier element_id) const override {
return {};
return m_document->page_layout();
}
[[nodiscard]] ElementIdentifier text_root_first_master_page(
[[maybe_unused]] const ElementIdentifier element_id) const override {
Expand Down
5 changes: 5 additions & 0 deletions src/odr/internal/ooxml/text/ooxml_text_document.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include <odr/style.hpp>

#include <odr/internal/common/document.hpp>
#include <odr/internal/ooxml/ooxml_util.hpp>
#include <odr/internal/ooxml/text/ooxml_text_element_registry.hpp>
Expand All @@ -23,6 +25,7 @@ class Document final : public internal::Document {
[[nodiscard]] const ElementRegistry &element_registry() const;
[[nodiscard]] const StyleRegistry &style_registry() const;
[[nodiscard]] const Relations &document_relations() const;
[[nodiscard]] const PageLayout &page_layout() const;

[[nodiscard]] bool is_editable() const noexcept override;
[[nodiscard]] bool is_savable(bool encrypted) const noexcept override;
Expand All @@ -37,6 +40,8 @@ class Document final : public internal::Document {

Relations m_document_relations;

PageLayout m_page_layout;

ElementRegistry m_element_registry;
StyleRegistry m_style_registry;
NumberingRegistry m_numbering_registry;
Expand Down
4 changes: 2 additions & 2 deletions test/data.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ odr_test_data(
odr_test_data(
PATH "reference-output/odr-public"
URL "https://github.com/opendocument-app/OpenDocument.test.output.git"
REVISION "0a1114a9a50417db31cb91193b356cf56b745b8e")
REVISION "40c457831835a8e9437bfe6494cb96b4c6159fab")

odr_test_data(
PATH "reference-output/odr-private"
URL "https://github.com/opendocument-app/OpenDocument.test-private.output.git"
REVISION "20a86c3be30a879ff6c4be21a246c4a17f95ff2e")
REVISION "8f7383f64c795602793a8c20a9c76bea197ad9c1")
20 changes: 20 additions & 0 deletions test/src/document_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,26 @@ TEST(Document, odt) {
EXPECT_EQ(Measure("0.7874in"), page_layout.margin.top);
}

TEST(Document, docx_page_layout) {
const Logger logger = Logger::create_stdio("odr-test", LogLevel::verbose);

const DocumentFile document_file(
TestData::test_file_path("odr-public/docx/sample3.docx"), logger);

const Document document = document_file.document();

const PageLayout page_layout =
document.root_element().as_text_root().page_layout();
// Two sections; the 0.5in bottom margin is the first one's, the body's own
// `w:sectPr` says 1in.
EXPECT_EQ(Measure("8.5in"), page_layout.width);
EXPECT_EQ(Measure("11in"), page_layout.height);
EXPECT_EQ(Measure("1in"), page_layout.margin.top);
EXPECT_EQ(Measure("1in"), page_layout.margin.right);
EXPECT_EQ(Measure("0.5in"), page_layout.margin.bottom);
EXPECT_EQ(Measure("1in"), page_layout.margin.left);
}

TEST(Document, xlsx_sheet_names) {
const Logger logger = Logger::create_stdio("odr-test", LogLevel::verbose);

Expand Down
Loading