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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ The release run heads these entries with the version and opens a fresh
- A pdf page is the size of its crop box and shows what is on it and no more,
as a viewer shows it.
- A pdf's JPEG 2000 images render. New dependency: `openjpeg`.
- A frame anchored to the page sits where the page says, whatever the text
around it wraps like - a letter's address and date boxes land in their fields
instead of in the running text.
- A text document is laid out on the master page it names, so a letter template
keeps the margins that leave room for its letterhead.

## v6.5.0 - 2026-08-10

Expand Down
15 changes: 10 additions & 5 deletions src/odr/internal/html/document_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -422,13 +422,18 @@ std::string html::translate_frame_properties(const Frame &frame) {
horizontal_position = *style.horizontal_position;
}

// Only a frame anchored in the text takes its place from the wrap. Text does
// not flow around a page-anchored one: css cannot place a box away from where
// it sits in the flow and still have the flow react to it.
const AnchorType anchor_type = frame.anchor_type();
const bool in_text = anchor_type != AnchorType::at_page;
Comment thread
andiwand marked this conversation as resolved.

// The frame is what its image sizes against.
std::string result;
if (const AnchorType anchor_type = frame.anchor_type();
anchor_type == AnchorType::as_char) {
if (anchor_type == AnchorType::as_char) {
result += "position:relative;";
result += "display:inline-block;";
} else if (text_wrap == TextWrap::before) {
} else if (in_text && text_wrap == TextWrap::before) {
result += "position:relative;";
result += "display:block;";
result += "float:right;clear:both;";
Expand All @@ -446,7 +451,7 @@ std::string html::translate_frame_properties(const Frame &frame) {
result += width->to_string();
result += ");";
}
} else if (text_wrap == TextWrap::after) {
} else if (in_text && text_wrap == TextWrap::after) {
result += "position:relative;";
result += "display:block;";
result += "float:left;clear:both;";
Expand All @@ -457,7 +462,7 @@ std::string html::translate_frame_properties(const Frame &frame) {
if (const std::optional<Measure> y = frame.y(); y.has_value()) {
result += "margin-top:" + y->to_string() + ";";
}
} else if (text_wrap == TextWrap::none) {
} else if (in_text && text_wrap == TextWrap::none) {
result += "position:relative;";
result += "display:block;";
if (const std::optional<Measure> x = frame.x(); x.has_value()) {
Expand Down
37 changes: 36 additions & 1 deletion src/odr/internal/odf/odf_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,43 @@ class ElementAdapter final : public abstract::ElementAdapter,
}
return {};
}
static bool is_anchored_frame(const pugi::xml_node node) {
return std::strcmp(node.name(), "draw:frame") == 0;
}

/// Whether laying this out would put a word on the page; a frame is anchored,
/// not written.
static bool writes_text(const pugi::xml_node node) {
for (const pugi::xml_node child : node.children()) {
if (child.type() == pugi::node_pcdata &&
!std::string_view(child.value()).empty()) {
return true;
}
if (!is_anchored_frame(child) && writes_text(child)) {
return true;
}
}
return false;
}

[[nodiscard]] ElementIdentifier text_root_first_master_page(
[[maybe_unused]] const ElementIdentifier element_id) const override {
const ElementIdentifier element_id) const override {
// A paragraph may name the master page its page uses (20.283). One page box
// is all we lay out, so only a name ahead of every written word counts.
for (const pugi::xml_node child : get_node(element_id).children()) {
if (is_anchored_frame(child)) {
continue;
}
if (const ElementIdentifier master_page_id =
m_document->style_registry().master_page_of_style(
child.attribute("text:style-name").value());
master_page_id != null_element_id) {
return master_page_id;
}
if (writes_text(child)) {
break;
}
}
return m_document->style_registry().first_master_page();
}

Expand Down
19 changes: 19 additions & 0 deletions src/odr/internal/odf/odf_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -688,4 +688,23 @@ ElementIdentifier StyleRegistry::first_master_page() const {
return m_first_master_page_element;
}

ElementIdentifier
StyleRegistry::master_page_of_style(const char *style_name) const {
// Bounded against a style naming itself as its own parent.
for (std::uint32_t depth = 0; depth < 16 && style_name[0] != '\0'; ++depth) {
const auto style_it = m_index_style.find(style_name);
if (style_it == std::end(m_index_style)) {
break;
}
const pugi::xml_node node = style_it->second;
if (const pugi::xml_attribute master_page_name =
node.attribute("style:master-page-name");
master_page_name && master_page_name.value()[0] != '\0') {
return master_page(master_page_name.value());
}
style_name = node.attribute("style:parent-style-name").value();
}
return {};
}

} // namespace odr::internal::odf
3 changes: 3 additions & 0 deletions src/odr/internal/odf/odf_style.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ class StyleRegistry final {

[[nodiscard]] ElementIdentifier master_page(const std::string &name) const;
[[nodiscard]] ElementIdentifier first_master_page() const;
/// The master page a style or one of its bases names (20.283); null if none.
[[nodiscard]] ElementIdentifier
master_page_of_style(const char *style_name) const;

private:
std::unordered_map<std::string, pugi::xml_node> m_index_font_face;
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 "b8d4a6de30ba901dc120573e7ceb9ac4ecef71b7")
REVISION "7e3b094d53590374a19aa9cd9219c418740b72b5")

odr_test_data(
PATH "reference-output/odr-private"
URL "https://github.com/opendocument-app/OpenDocument.test-private.output.git"
REVISION "6d22a2435767a97f51fab53b2a6ac9f11ebd8198")
REVISION "3cd78a400b871854e1affb7b9209704b49a9c1d4")
Loading