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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ bytes ─▢ magic/open_strategy ─▢ DecodedFile ─▢ Document ─▢ Eleme

`ElementType` is the shared enum in `src/odr/document_element.hpp`.

A **manual page break** reaches the renderer two ways, as the formats state it
two ways: `ParagraphStyle::break_before`/`break_after` for odf and ooxml, where
it is a style property, and `ElementType::page_break` for rtf and `.doc`, where
it is a node. Both split the text view's page box. The *automatic* breaks a
producer's layout recorded β€” odf's `text:soft-page-break` β€” are not parsed.

## Directory map

| Path | What |
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- A manual page break starts a new page box, and prints as `break-before:page`.
New `BreakType` and `ParagraphStyle::break_before`/`break_after`, mirrored in
the JNI, Apple and Python bindings. Towards #174.

- ODF's `text:soft-page-break` is no longer part of the element tree, so a
`DocumentPath` past one shifts.

- New `DocumentFile::thumbnail()`, the preview the package carries or
`nullopt`, mirrored in the JNI, Apple and Python bindings. Closes #21.

Expand Down
11 changes: 11 additions & 0 deletions apple/include/OdrCoreObjC/ODRStyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ typedef NS_ENUM(NSInteger, ODRFontPosition) {
ODRFontPositionSub,
} NS_SWIFT_NAME(FontPosition);

/// A manual break, `ODRBreakTypeNone` being one the document turns off.
typedef NS_ENUM(NSInteger, ODRBreakType) {
ODRBreakTypeNone = 0,
ODRBreakTypePage,
ODRBreakTypeColumn,
} NS_SWIFT_NAME(BreakType);

typedef NS_ENUM(NSInteger, ODRTextAlign) {
ODRTextAlignLeft = 0,
ODRTextAlignRight,
Expand Down Expand Up @@ -157,6 +164,10 @@ NS_SWIFT_NAME(ParagraphStyle)
@property(nonatomic, readonly) ODRDirectionalMeasure *margin;
@property(nonatomic, readonly, nullable) ODRMeasure *lineHeight;
@property(nonatomic, readonly, nullable) ODRMeasure *textIndent;
/// `ODRBreakType`, boxed; `nil` where the style says nothing.
@property(nonatomic, readonly, nullable) NSNumber *breakBefore;
/// `ODRBreakType`, boxed; `nil` where the style says nothing.
@property(nonatomic, readonly, nullable) NSNumber *breakAfter;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
Expand Down
5 changes: 5 additions & 0 deletions apple/src/ODRStyle.mm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
ODR_SAME_ENUM(ODRFontPositionNormal, odr::FontPosition::normal);
ODR_SAME_ENUM(ODRFontPositionSuper, odr::FontPosition::super);
ODR_SAME_ENUM(ODRFontPositionSub, odr::FontPosition::sub);
ODR_SAME_ENUM(ODRBreakTypeNone, odr::BreakType::none);
ODR_SAME_ENUM(ODRBreakTypePage, odr::BreakType::page);
ODR_SAME_ENUM(ODRBreakTypeColumn, odr::BreakType::column);
ODR_SAME_ENUM(ODRTextAlignLeft, odr::TextAlign::left);
ODR_SAME_ENUM(ODRTextAlignRight, odr::TextAlign::right);
ODR_SAME_ENUM(ODRTextAlignCenter, odr::TextAlign::center);
Expand Down Expand Up @@ -216,6 +219,8 @@ + (instancetype)styleWithHandle:(const odr::ParagraphStyle &)handle {
result->_margin = [ODRDirectionalMeasure directionalWithHandle:handle.margin];
result->_lineHeight = box(handle.line_height);
result->_textIndent = box(handle.text_indent);
result->_breakBefore = box_enum(handle.break_before);
result->_breakAfter = box_enum(handle.break_after);
return result;
}

Expand Down
1 change: 1 addition & 0 deletions jni/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ add_jar(odr_java
"java/app/opendocument/core/Archive.java"
"java/app/opendocument/core/ArchiveFile.java"
"java/app/opendocument/core/Bookmark.java"
"java/app/opendocument/core/BreakType.java"
"java/app/opendocument/core/Circle.java"
"java/app/opendocument/core/Color.java"
"java/app/opendocument/core/CustomShape.java"
Expand Down
14 changes: 14 additions & 0 deletions jni/java/app/opendocument/core/BreakType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package app.opendocument.core;

/** Mirrors {@code odr::BreakType}; constant order must match the C++ declaration. */
public enum BreakType {
NONE, PAGE, COLUMN;

static BreakType fromNative(int code) {
return code < 0 ? null : values()[code];
}

int toNative() {
return ordinal();
}
}
14 changes: 13 additions & 1 deletion jni/java/app/opendocument/core/ParagraphStyle.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,23 @@ public final class ParagraphStyle {
public final DirectionalMeasure margin;
public final Measure lineHeight;
public final Measure textIndent;
/** A break the author put before the paragraph; {@code null} if the style says nothing. */
public final BreakType breakBefore;
/** A break the author put after the paragraph; {@code null} if the style says nothing. */
public final BreakType breakAfter;

ParagraphStyle(int textAlign, DirectionalMeasure margin, Measure lineHeight, Measure textIndent) {
ParagraphStyle(
int textAlign,
DirectionalMeasure margin,
Measure lineHeight,
Measure textIndent,
int breakBefore,
int breakAfter) {
this.textAlign = TextAlign.fromNative(textAlign);
this.margin = margin;
this.lineHeight = lineHeight;
this.textIndent = textIndent;
this.breakBefore = BreakType.fromNative(breakBefore);
this.breakAfter = BreakType.fromNative(breakAfter);
}
}
5 changes: 3 additions & 2 deletions jni/src/jni_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,11 @@ jobject make_paragraph_style(JNIEnv *env, const odr::ParagraphStyle &style) {
return new_object(
env, "app/opendocument/core/ParagraphStyle",
"(ILapp/opendocument/core/DirectionalMeasure;"
"Lapp/opendocument/core/Measure;Lapp/opendocument/core/Measure;)V",
"Lapp/opendocument/core/Measure;Lapp/opendocument/core/Measure;II)V",
enum_code(style.text_align), make_directional_measure(env, style.margin),
make_measure(env, style.line_height),
make_measure(env, style.text_indent));
make_measure(env, style.text_indent), enum_code(style.break_before),
enum_code(style.break_after));
}

jobject make_table_style(JNIEnv *env, const odr::TableStyle &style) {
Expand Down
9 changes: 8 additions & 1 deletion python/src/bind_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ void odr_python::bind_style(py::module_ &m) {
.value("super", odr::FontPosition::super)
.value("sub", odr::FontPosition::sub);

py::enum_<odr::BreakType>(m, "BreakType")
.value("none", odr::BreakType::none)
.value("page", odr::BreakType::page)
.value("column", odr::BreakType::column);

py::enum_<odr::TextAlign>(m, "TextAlign")
.value("left", odr::TextAlign::left)
.value("right", odr::TextAlign::right)
Expand Down Expand Up @@ -146,7 +151,9 @@ void odr_python::bind_style(py::module_ &m) {
.def_readwrite("text_align", &odr::ParagraphStyle::text_align)
.def_readwrite("margin", &odr::ParagraphStyle::margin)
.def_readwrite("line_height", &odr::ParagraphStyle::line_height)
.def_readwrite("text_indent", &odr::ParagraphStyle::text_indent);
.def_readwrite("text_indent", &odr::ParagraphStyle::text_indent)
.def_readwrite("break_before", &odr::ParagraphStyle::break_before)
.def_readwrite("break_after", &odr::ParagraphStyle::break_after);

py::class_<odr::TableStyle>(m, "TableStyle")
.def(py::init<>())
Expand Down
67 changes: 58 additions & 9 deletions src/odr/internal/html/document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,22 @@ class HtmlServiceImpl final : public HtmlService {
mutable HtmlResources m_resources;
};

/// Only a paragraph carries a break: one inside a table or a frame could not
/// split the page box.
bool breaks_page(const std::optional<BreakType> &break_type) {
return break_type == BreakType::page;
}

bool breaks_page_before(const Element &element) {
return element.type() == ElementType::paragraph &&
breaks_page(element.as_paragraph().style().break_before);
}

bool breaks_page_after(const Element &element) {
return element.type() == ElementType::paragraph &&
breaks_page(element.as_paragraph().style().break_after);
}

class TextHtmlFragment final : public HtmlFragmentBase {
public:
explicit TextHtmlFragment(std::string name, const std::size_t index,
Expand All @@ -459,8 +475,24 @@ class TextHtmlFragment final : public HtmlFragmentBase {
const TextRoot element = root.as_text_root();

if (state.config().text_document_margin) {
const PageLayout page_layout = element.page_layout();
write_pages(out, state, element);
} else {
out.write_element_begin("div",
HtmlElementOptions().set_class("odr-text-flow"));
translate_children(element.children(), state);
out.write_element_end("div");
}
}

private:
/// One page box per run of content between the author's manual breaks. Not
/// pagination: nothing computes where a page ends, so the boxes differ in
/// height. `.odr-pages` stacks them.
static void write_pages(HtmlWriter &out, const WritingState &state,
const TextRoot &element) {
const PageLayout page_layout = element.page_layout();

const auto begin_page = [&] {
out.write_element_begin(
"div",
HtmlElementOptions()
Expand All @@ -470,17 +502,34 @@ class TextHtmlFragment final : public HtmlFragmentBase {
"div", HtmlElementOptions()
.set_class("odr-page-inner")
.set_style(translate_inner_page_style(page_layout)));

translate_children(element.children(), state);

out.write_element_end("div");
};
const auto end_page = [&] {
out.write_element_end("div");
} else {
out.write_element_begin("div",
HtmlElementOptions().set_class("odr-text-flow"));
translate_children(element.children(), state);
out.write_element_end("div");
};

begin_page();
// Otherwise a leading or doubled break opens an empty sheet.
bool empty = true;
bool pending_break = false;

for (const Element child : element.children()) {
if (child.type() == ElementType::page_break) {
// The node is the break, not content.
pending_break = true;
continue;
}
if ((pending_break || breaks_page_before(child)) && !empty) {
end_page();
begin_page();
empty = true;
}
pending_break = breaks_page_after(child);
translate_element(child, state);
empty = false;
}

end_page();
}

protected:
Expand Down
16 changes: 13 additions & 3 deletions src/odr/internal/html/document_element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,14 @@ void html::translate_element(const Element &element,
case ElementType::custom_shape:
translate_custom_shape(element, state);
break;
case ElementType::page_break:
translate_page_break(element, state);
break;
case ElementType::group:
translate_children(element.children(), state);
break;
default:
// The element and its whole subtree are dropped. Logged rather than
// thrown: a renderer shows what it can, and the corpus run is what ranks
// the gaps by how often they actually occur.
// Dropped with its whole subtree; a renderer shows what it can.
ODR_WARNING(state.logger(), "html: dropped unhandled element "
<< element_type_name(element.type()));
break;
Expand Down Expand Up @@ -436,6 +437,15 @@ bool has_content(const ElementRange &children) {

} // namespace

void html::translate_page_break(const Element & /*element*/,
const WritingState &state) {
// Reached only where the page box cannot be split; `TextHtmlFragment` takes
// the breaks among the root's children.
state.out().write_element_begin(
"div", HtmlElementOptions().set_style("break-before:page"));
state.out().write_element_end("div");
}

void html::translate_paragraph(const Element &element,
const WritingState &state,
const std::string &marker) {
Expand Down
2 changes: 2 additions & 0 deletions src/odr/internal/html/document_element.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ void translate_master_page(const MasterPage &masterPage,

void translate_text(const Element &element, const WritingState &state);
void translate_line_break(const Element &element, const WritingState &state);
/// A manual page break carried as an element, which only rtf and `.doc` do.
void translate_page_break(const Element &element, const WritingState &state);
/// `marker`, when set, is written inside the paragraph ahead of its content.
void translate_paragraph(const Element &element, const WritingState &state,
const std::string &marker = "");
Expand Down
27 changes: 27 additions & 0 deletions src/odr/internal/html/document_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ const char *html::translate_vertical_align(const VerticalAlign vertical_align) {
}
}

const char *html::translate_break(const BreakType break_type) {
switch (break_type) {
case BreakType::page:
return "page";
case BreakType::column:
return "column";
case BreakType::none:
// The css initial value; the model needs it only for style inheritance.
return nullptr;
}
return nullptr;
}

const char *html::translate_font_weight(const FontWeight font_weight) {
switch (font_weight) {
case FontWeight::normal:
Expand Down Expand Up @@ -269,6 +282,20 @@ html::translate_paragraph_style(const ParagraphStyle &paragraph_style) {
text_indent.has_value()) {
result.append("text-indent:").append(text_indent->to_string()).append(";");
}
// Paged media only; the sheet it starts is `TextHtmlFragment`'s business.
if (const std::optional<BreakType> break_before =
paragraph_style.break_before;
break_before.has_value()) {
if (const char *value = translate_break(*break_before); value != nullptr) {
result.append("break-before:").append(value).append(";");
}
}
if (const std::optional<BreakType> break_after = paragraph_style.break_after;
break_after.has_value()) {
if (const char *value = translate_break(*break_after); value != nullptr) {
result.append("break-after:").append(value).append(";");
}
}
return result;
}

Expand Down
3 changes: 3 additions & 0 deletions src/odr/internal/html/document_style.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ enum class VerticalAlign;
enum class FontWeight;
enum class FontStyle;
enum class FontPosition;
enum class BreakType;

class Frame;
class Rect;
Expand All @@ -29,6 +30,8 @@ struct PageLayout;

namespace odr::internal::html {

/// `nullptr` for a break turned off: the css default already says it.
const char *translate_break(BreakType break_type);
const char *translate_text_align(TextAlign text_align);
const char *translate_horizontal_align(HorizontalAlign horizontal_align);
const char *translate_vertical_align(VerticalAlign vertical_align);
Expand Down
5 changes: 3 additions & 2 deletions src/odr/internal/odf/odf_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,9 @@ parse_any_element_tree(ElementRegistry &registry, const pugi::xml_node node) {
{"text:index-body", create_default_tree_parser(ElementType::group)},
// A `draw:measure` writes its label as `text:measure` runs.
{"text:measure", create_default_tree_parser(ElementType::group)},
{"text:soft-page-break",
create_default_tree_parser(ElementType::page_break)},
// No `text:soft-page-break`: [OpenDocument] 5.1.1 records the
// producer's own layout there, not an authored break, which is
// `fo:break-before` on the paragraph style.
{"text:date", create_default_tree_parser(ElementType::group)},
{"text:time", create_default_tree_parser(ElementType::group)},
{"text:section", create_default_tree_parser(ElementType::group)},
Expand Down
24 changes: 24 additions & 0 deletions src/odr/internal/odf/odf_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@ read_font_position(const pugi::xml_attribute attribute) {
return FontPosition::normal;
}

/// [OpenDocument] 20.86/20.87.
std::optional<BreakType> read_break(const pugi::xml_attribute attribute) {
if (!attribute) {
return {};
}
const char *value = attribute.value();
if (std::strcmp("page", value) == 0) {
return BreakType::page;
}
if (std::strcmp("column", value) == 0) {
return BreakType::column;
}
// `auto`: a style overriding an inherited break has to say so
return BreakType::none;
}

std::optional<TextAlign> read_text_align(const pugi::xml_attribute attribute) {
if (!attribute) {
return {};
Expand Down Expand Up @@ -417,6 +433,14 @@ void Style::resolve_paragraph_style_(const pugi::xml_node node,
read_measure(paragraph_properties.attribute("fo:text-indent"))) {
result.text_indent = text_indent;
}
if (const std::optional<BreakType> break_before =
read_break(paragraph_properties.attribute("fo:break-before"))) {
result.break_before = break_before;
}
if (const std::optional<BreakType> break_after =
read_break(paragraph_properties.attribute("fo:break-after"))) {
result.break_after = break_after;
}
}

void Style::resolve_table_style_(const pugi::xml_node node,
Expand Down
Loading
Loading