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
36 changes: 35 additions & 1 deletion src/odr/internal/html/document_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ const char *html::translate_font_style(const FontStyle font_style) {
}
}

const char *html::translate_font_position(const FontPosition font_position) {
switch (font_position) {
case FontPosition::normal:
return "baseline";
case FontPosition::super:
return "super";
case FontPosition::sub:
return "sub";
default:
return ""; // TODO log
}
}

std::string html::translate_outer_page_style(const PageLayout &page_layout) {
std::string result;
if (const std::optional<Measure> width = page_layout.width;
Expand Down Expand Up @@ -153,6 +166,13 @@ std::string html::translate_text_style(const TextStyle &text_style) {
.append(color(*background_color))
.append(";");
}
if (const std::optional<FontPosition> font_position =
text_style.font_position;
font_position.has_value()) {
result.append("vertical-align:")
.append(translate_font_position(*font_position))
.append(";");
}
return result;
}

Expand Down Expand Up @@ -191,7 +211,21 @@ html::translate_paragraph_style(const ParagraphStyle &paragraph_style) {
}
if (const std::optional<Measure> line_height = paragraph_style.line_height;
line_height.has_value()) {
result.append("line-height:").append(line_height->to_string()).append(";");
// a percent line height is relative to the text font size, which is
// emitted on nested text spans; a CSS percentage would resolve against
// this block's font size, while a unitless ratio inherits per element
if (line_height->unit().name() == "%") {
const Measure ratio(line_height->magnitude() * 1e-2, DynamicUnit(""));
result.append("line-height:").append(ratio.to_string()).append(";");
} else {
result.append("line-height:")
.append(line_height->to_string())
.append(";");
}
}
if (const std::optional<Measure> text_indent = paragraph_style.text_indent;
text_indent.has_value()) {
result.append("text-indent:").append(text_indent->to_string()).append(";");
}
return result;
}
Expand Down
2 changes: 2 additions & 0 deletions src/odr/internal/html/document_style.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ enum class HorizontalAlign;
enum class VerticalAlign;
enum class FontWeight;
enum class FontStyle;
enum class FontPosition;

class Frame;
class Rect;
Expand All @@ -31,6 +32,7 @@ const char *translate_horizontal_align(HorizontalAlign horizontal_align);
const char *translate_vertical_align(VerticalAlign vertical_align);
const char *translate_font_weight(FontWeight font_weight);
const char *translate_font_style(FontStyle font_style);
const char *translate_font_position(FontPosition font_position);

std::string translate_outer_page_style(const PageLayout &page_layout);
std::string translate_inner_page_style(const PageLayout &page_layout);
Expand Down
8 changes: 5 additions & 3 deletions src/odr/internal/odf/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ flatten-at-build, not a runtime walk. Parent beats family as the copy source. A
Font names are indirected through `style:font-face` → `svg:font-family`.
Master pages are parsed into the element tree *and* the style index.

**Percent units are special-cased**: percent font-size multiplies the inherited
size; percent margins/line-height are currently **dropped** (open work).
**Percent units are special-cased**: percent font-size (and the relative size
of `style:text-position`) multiplies the inherited size; percent line-height
passes through (the HTML renderer emits it as a unitless CSS ratio); percent
margins are currently **dropped** (open work).

**Decryption is manifest-driven, two layouts.** `odf_crypto.cpp` supports either
a single `encrypted-package` blob (decrypt → inflate → new ZIP filesystem) or
Expand Down Expand Up @@ -112,7 +114,7 @@ The structural/foundational gaps, roughly by value:
5. **Repeated / covered spreadsheet cells are heuristic.** Several
`// TODO covered cells` / `// TODO mark as repeated` in `odf_parser.cpp`;
empty-row/cell detection is approximate.
6. **Style gaps beyond missing properties**: percent margins/line-height dropped;
6. **Style gaps beyond missing properties**: percent margins dropped;
`transparent`/alpha colours → `nullopt`; the Style-vs-element cascade layering
is provisional (`// TODO use override?`). List/outline numbering is indexed but
not rendered as numbers.
Expand Down
8 changes: 5 additions & 3 deletions src/odr/internal/odf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ Roughly ordered by importance.
- [x] underline, strike through
- [x] color, background color
- [x] shadow
- [ ] superscript, subscript (`style:text-position`)
- [x] superscript, subscript (`style:text-position`, incl. relative font
size)
- [x] paragraph
- [x] alignment
- [x] margins
- [x] line height
- [x] margins (percentages are dropped)
- [x] line height (absolute and percentage)
- [x] first line indent (`fo:text-indent`)
- [x] links
- [x] images
- [x] internal and external references
Expand Down
58 changes: 54 additions & 4 deletions src/odr/internal/odf/odf_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <odr/internal/odf/odf_document.hpp>
#include <odr/internal/odf/odf_parser.hpp>

#include <cstdlib>
#include <cstring>
#include <unordered_map>
#include <utility>
Expand Down Expand Up @@ -59,6 +60,34 @@ std::optional<FontStyle> read_font_style(const pugi::xml_attribute attribute) {
return {};
}

/// first component of `style:text-position`: "super", "sub" or a signed
/// percent offset from the baseline
std::optional<FontPosition>
read_font_position(const pugi::xml_attribute attribute) {
if (!attribute) {
return {};
}
const char *value = attribute.value();
if (std::strncmp("super", value, 5) == 0) {
return FontPosition::super;
}
if (std::strncmp("sub", value, 3) == 0) {
return FontPosition::sub;
}
char *end{nullptr};
const double offset = std::strtod(value, &end);
if (end == value) {
return {};
}
if (offset > 0) {
return FontPosition::super;
}
if (offset < 0) {
return FontPosition::sub;
}
return FontPosition::normal;
}

std::optional<TextAlign> read_text_align(const pugi::xml_attribute attribute) {
if (!attribute) {
return {};
Expand Down Expand Up @@ -294,6 +323,24 @@ void Style::resolve_text_style_(const StyleRegistry *registry,
read_color(text_properties.attribute("fo:background-color"))) {
result.background_color = background_color;
}
if (const pugi::xml_attribute text_position =
text_properties.attribute("style:text-position")) {
if (const std::optional<FontPosition> font_position =
read_font_position(text_position)) {
result.font_position = font_position;
}
// optional second component: relative font size in percent
if (const char *scale = std::strchr(text_position.value(), ' ');
scale != nullptr && result.font_size.has_value()) {
char *end{nullptr};
if (const double magnitude = std::strtod(scale + 1, &end);
end != scale + 1 && magnitude > 0) {
result.font_size =
Measure(result.font_size->magnitude() * magnitude * 1e-2,
result.font_size->unit());
}
}
}
}

void Style::resolve_paragraph_style_(const pugi::xml_node node,
Expand Down Expand Up @@ -342,10 +389,13 @@ void Style::resolve_paragraph_style_(const pugi::xml_node node,
}
if (const std::optional<Measure> line_height =
read_measure(paragraph_properties.attribute("fo:line-height"))) {
// TODO
if (line_height->unit().name() != "%") {
result.line_height = line_height;
}
// percent line height is relative to the text font size; the HTML
// translation emits it as a unitless ratio
result.line_height = line_height;
Comment thread
andiwand marked this conversation as resolved.
}
if (const std::optional<Measure> text_indent =
read_measure(paragraph_properties.attribute("fo:text-indent"))) {
result.text_indent = text_indent;
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/odr/style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ void TextStyle::override(const TextStyle &other) {
if (other.background_color.has_value()) {
background_color = other.background_color;
}
if (other.font_position.has_value()) {
font_position = other.font_position;
}
}

void ParagraphStyle::override(const ParagraphStyle &other) {
Expand All @@ -78,6 +81,9 @@ void ParagraphStyle::override(const ParagraphStyle &other) {
if (other.line_height.has_value()) {
line_height = other.line_height;
}
if (other.text_indent.has_value()) {
text_indent = other.text_indent;
}
}

void TableStyle::override(const TableStyle &other) {
Expand Down
9 changes: 9 additions & 0 deletions src/odr/style.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ enum class FontStyle {
italic,
};

/// @brief Collection of vertical font positions (sub/superscript).
enum class FontPosition {
normal,
super,
sub,
};

/// @brief Collection of text alignments.
enum class TextAlign {
left,
Expand Down Expand Up @@ -133,6 +140,7 @@ struct TextStyle final {
std::optional<std::string> font_shadow;
std::optional<Color> font_color;
std::optional<Color> background_color;
std::optional<FontPosition> font_position;

void override(const TextStyle &other);
};
Expand All @@ -142,6 +150,7 @@ struct ParagraphStyle final {
std::optional<TextAlign> text_align;
DirectionalStyle<Measure> margin;
std::optional<Measure> line_height;
std::optional<Measure> text_indent;

void override(const ParagraphStyle &other);
};
Expand Down
2 changes: 1 addition & 1 deletion test/data/reference-output/odr-private
69 changes: 69 additions & 0 deletions test/src/document_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@
using namespace odr;
using namespace odr::test;

namespace {

Element find_paragraph_with_text_prefix(const Element root,
const std::string &prefix) {
for (const Element child : root.children()) {
if (child.type() == ElementType::paragraph) {
if (const Element first_child = child.first_child();
first_child && first_child.type() == ElementType::text &&
first_child.as_text().content().starts_with(prefix)) {
return child;
}
}
if (const Element match = find_paragraph_with_text_prefix(child, prefix)) {
return match;
}
}
return {};
}

} // namespace

TEST(Document, odt) {
const std::unique_ptr logger =
Logger::create_stdio("odr-test", LogLevel::verbose);
Expand Down Expand Up @@ -81,6 +102,54 @@ TEST(Document, odt_element_path2) {
EXPECT_EQ(cell_via_path.as_text().content(), "B1");
}

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

const DocumentFile document_file(
TestData::test_file_path("odr-public/odt/style-various-1.odt"), *logger);
const Document document = document_file.document();
const Element root = document.root_element();

const Element superscript =
find_paragraph_with_text_prefix(root, "Superscript");
ASSERT_TRUE(superscript);
const TextStyle superscript_style = superscript.as_paragraph().text_style();
EXPECT_EQ(FontPosition::super, superscript_style.font_position);
// `style:text-position="super 58%"` scales the inherited 12pt font
ASSERT_TRUE(superscript_style.font_size.has_value());
EXPECT_NEAR(6.96, superscript_style.font_size->magnitude(), 1e-9);

const Element subscript = find_paragraph_with_text_prefix(root, "Subscript");
ASSERT_TRUE(subscript);
const TextStyle subscript_style = subscript.as_paragraph().text_style();
EXPECT_EQ(FontPosition::sub, subscript_style.font_position);

const Element normal = find_paragraph_with_text_prefix(root, "Default");
ASSERT_TRUE(normal);
EXPECT_FALSE(normal.as_paragraph().text_style().font_position.has_value());
}

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

const DocumentFile document_file(
TestData::test_file_path("odr-public/odt/file-sample_100kB.odt"),
*logger);
const Document document = document_file.document();
const Element root = document.root_element();

const Element heading =
find_paragraph_with_text_prefix(root, "Lorem ipsum dolor");
ASSERT_TRUE(heading);
EXPECT_EQ(Measure("0.25in"), heading.as_paragraph().style().text_indent);

const Element body = find_paragraph_with_text_prefix(root, "Morbi viverra");
ASSERT_TRUE(body);
EXPECT_EQ(Measure("120%"), body.as_paragraph().style().line_height);
}

TEST(Document, odg) {
const std::unique_ptr logger =
Logger::create_stdio("odr-test", LogLevel::verbose);
Expand Down
Loading