diff --git a/CHANGELOG.md b/CHANGELOG.md index e404ba67..d3a744bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/odr/internal/html/document_style.cpp b/src/odr/internal/html/document_style.cpp index 86d42071..b4fa4ed1 100644 --- a/src/odr/internal/html/document_style.cpp +++ b/src/odr/internal/html/document_style.cpp @@ -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; + // 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;"; @@ -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;"; @@ -457,7 +462,7 @@ std::string html::translate_frame_properties(const Frame &frame) { if (const std::optional 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 x = frame.x(); x.has_value()) { diff --git a/src/odr/internal/odf/odf_document.cpp b/src/odr/internal/odf/odf_document.cpp index bd70201a..14f76fec 100644 --- a/src/odr/internal/odf/odf_document.cpp +++ b/src/odr/internal/odf/odf_document.cpp @@ -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(); } diff --git a/src/odr/internal/odf/odf_style.cpp b/src/odr/internal/odf/odf_style.cpp index 7f2fda60..c5a0370b 100644 --- a/src/odr/internal/odf/odf_style.cpp +++ b/src/odr/internal/odf/odf_style.cpp @@ -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 diff --git a/src/odr/internal/odf/odf_style.hpp b/src/odr/internal/odf/odf_style.hpp index 3f6620ea..c9d15971 100644 --- a/src/odr/internal/odf/odf_style.hpp +++ b/src/odr/internal/odf/odf_style.hpp @@ -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 m_index_font_face; diff --git a/test/data.cmake b/test/data.cmake index 63114a56..feb0c849 100644 --- a/test/data.cmake +++ b/test/data.cmake @@ -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")