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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- `HtmlConfig::min_content_margin` puts a floor under the distance the
generated content keeps from the view's border, per side. A set side raises
the inset a view already has, never lowers it; a sheet is never inset. Bound
in python, jni, wasm and apple as `minContentMargin`.
- **Breaking** Bytes that do not read as text no longer come back as
`text_file` and render as nonsense - `decode` throws `UnknownFileType` and
`list_file_types` comes back empty. A file is text when it is empty, or its
Expand Down
5 changes: 5 additions & 0 deletions apple/include/OdrCoreObjC/ODRHtml.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import <Foundation/Foundation.h>

#import <OdrCoreObjC/ODRFilesystem.h>
#import <OdrCoreObjC/ODRStyle.h>
#import <OdrCoreObjC/ODRTable.h>

NS_ASSUME_NONNULL_BEGIN
Expand Down Expand Up @@ -101,6 +102,10 @@ NS_SWIFT_NAME(HtmlConfig)
/// The zoom the view opens at, 1 being actual size; `nil` follows the fit.
@property(nonatomic, strong, nullable) NSNumber *initialZoom;

/// The least distance the generated content keeps from the view's border. A
/// set side raises the inset the view already has, never lowers it.
@property(nonatomic, strong) ODRDirectionalMeasure *minContentMargin;

@property(nonatomic) BOOL formatHtml;
/// Repeated `htmlIndentString` per nesting level; 0 disables indentation.
@property(nonatomic) uint8_t htmlIndent;
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 @@ -79,6 +79,11 @@ NS_SWIFT_NAME(Measure)
/// Magnitude and unit as odrcore writes them, e.g. `12pt`.
@property(nonatomic, readonly, copy) NSString *stringValue;

/// A css length as odrcore writes one, e.g. `3mm`. A magnitude with no unit
/// reads as unitless.
- (instancetype)initWithString:(NSString *)string;
- (instancetype)initWithMagnitude:(double)magnitude unit:(NSString *)unit;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
@end
Expand All @@ -92,6 +97,12 @@ NS_SWIFT_NAME(DirectionalMeasure)
@property(nonatomic, readonly, nullable) ODRMeasure *left;
@property(nonatomic, readonly, nullable) ODRMeasure *bottom;

/// For the sides a caller states itself, e.g. `ODRHtmlConfig.minContentMargin`.
- (instancetype)initWithRight:(nullable ODRMeasure *)right
top:(nullable ODRMeasure *)top
left:(nullable ODRMeasure *)left
bottom:(nullable ODRMeasure *)bottom;

- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
@end
Expand Down
7 changes: 7 additions & 0 deletions apple/src/ODRHtml.mm
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ - (instancetype)initWithNativeConfig:(const odr::HtmlConfig &)config {
_viewportContent = config.viewport_content.has_value()
? to_nsstring(*config.viewport_content)
: nil;
_minContentMargin =
[ODRDirectionalMeasure directionalWithHandle:config.min_content_margin];
_viewportWidth = config.viewport_width.has_value()
? @(static_cast<unsigned int>(*config.viewport_width))
: nil;
Expand Down Expand Up @@ -187,6 +189,11 @@ - (instancetype)initWithNativeConfig:(const odr::HtmlConfig &)config {
} else {
config.initial_zoom.reset();
}
if (_minContentMargin != nil) {
config.min_content_margin = _minContentMargin.handle;
} else {
config.min_content_margin = odr::DirectionalStyle<odr::Measure>();
}
config.format_html = _formatHtml == YES;
config.html_indent = _htmlIndent;
config.html_indent_string = to_string(_htmlIndentString);
Expand Down
4 changes: 4 additions & 0 deletions apple/src/ODRPrivate.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#include <odr/logger.hpp>
#include <odr/style.hpp>

#include <optional>

/// Cross-translation-unit access to the C++ value each wrapper owns.
///
/// Each `@implementation` holds its handle as an ivar, destroyed by ARC's
Expand Down Expand Up @@ -85,11 +87,13 @@ NS_ASSUME_NONNULL_BEGIN

@interface ODRMeasure (Private)
+ (instancetype)measureWithHandle:(const odr::Measure &)handle;
- (const std::optional<odr::Measure> &)handle;
@end

@interface ODRDirectionalMeasure (Private)
+ (instancetype)directionalWithHandle:
(const odr::DirectionalStyle<odr::Measure> &)handle;
- (odr::DirectionalStyle<odr::Measure>)handle;
@end

@interface ODRDirectionalString (Private)
Expand Down
44 changes: 44 additions & 0 deletions apple/src/ODRStyle.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

using odr::apple::guarded_value;
using odr::apple::to_nsstring;
using odr::apple::to_string;

ODR_SAME_ENUM(ODRFontWeightNormal, odr::FontWeight::normal);
ODR_SAME_ENUM(ODRFontWeightBold, odr::FontWeight::bold);
Expand Down Expand Up @@ -107,6 +108,26 @@ - (NSString *)stringValue {
return guarded_value([&] { return to_nsstring(_handle->to_string()); }, @"");
}

- (instancetype)initWithString:(NSString *)string {
if ((self = [super init]) == nil) {
return nil;
}
_handle = odr::Measure(to_string(string));
return self;
}

- (instancetype)initWithMagnitude:(double)magnitude unit:(NSString *)unit {
if ((self = [super init]) == nil) {
return nil;
}
_handle = odr::Measure(magnitude, odr::DynamicUnit(to_string(unit)));
return self;
}

- (const std::optional<odr::Measure> &)handle {
return _handle;
}

- (NSString *)description {
return self.stringValue;
}
Expand All @@ -127,6 +148,29 @@ + (instancetype)directionalWithHandle:
return result;
}

- (instancetype)initWithRight:(ODRMeasure *)right
top:(ODRMeasure *)top
left:(ODRMeasure *)left
bottom:(ODRMeasure *)bottom {
if ((self = [super init]) == nil) {
return nil;
}
_right = right;
_top = top;
_left = left;
_bottom = bottom;
return self;
}

- (odr::DirectionalStyle<odr::Measure>)handle {
odr::DirectionalStyle<odr::Measure> result;
result.right = _right != nil ? _right.handle : std::nullopt;
result.top = _top != nil ? _top.handle : std::nullopt;
result.left = _left != nil ? _left.handle : std::nullopt;
result.bottom = _bottom != nil ? _bottom.handle : std::nullopt;
return result;
}

@end

@implementation ODRDirectionalString
Expand Down
20 changes: 20 additions & 0 deletions apple/tests/OdrCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,26 @@ final class HtmlTests: XCTestCase {
XCTAssertTrue(html.contains("<style"), "the html has no stylesheet")
}

/// The C++ suite covers where the floor lands; this only proves the margin
/// crosses the binding, `nil` sides and all.
func testMinContentMarginReachesTheHtml() throws {
let config = HtmlConfig()
config.minContentMargin = DirectionalMeasure(
right: nil, top: Measure(string: "12px"), left: Measure(string: "1cm"),
bottom: nil)

let file = try DecodedFile.decode(path: try Fixture.odt())
let service = try HtmlTranslator.translate(
file: file, cachePath: try temporaryDirectory(), config: config)
var resources: NSArray?
let html = try XCTUnwrap(service.views.first).writeHtml(resources: &resources)

XCTAssertTrue(
html.contains(":root{--odr-min-margin-top:12px;--odr-min-margin-left:1cm;}"),
"the margin did not reach the html")
XCTAssertFalse(html.contains("--odr-min-margin-right:"), "an unset side was written")
}

/// A view's impl points into its service without owning it, so the view has
/// to keep the service alive itself — the analogue of
/// `ElementTreeTests.testElementsKeepTheirDocumentAlive`. Rendering off a
Expand Down
7 changes: 7 additions & 0 deletions jni/java/app/opendocument/core/HtmlConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ public final class HtmlConfig {
/** The zoom the view opens at, 1 being actual size; {@code null} follows the fit. */
public Double initialZoom;

/**
* The least distance the generated content keeps from the view's border. A set side raises the
* inset the view already has, never lowers it.
*/
public DirectionalMeasure minContentMargin =
new DirectionalMeasure(null, null, null, null);

public boolean formatHtml = false;
public int htmlIndent = 1;
public String htmlIndentString = "\t";
Expand Down
46 changes: 46 additions & 0 deletions jni/src/jni_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,44 @@ make_directional_measure(JNIEnv *env,
make_measure(env, style.left), make_measure(env, style.bottom));
}

std::optional<odr::Measure> measure_from_java(JNIEnv *env, jobject value) {
if (value == nullptr) {
return std::nullopt;
}
jclass cls = env->GetObjectClass(value);
const double magnitude =
env->GetDoubleField(value, env->GetFieldID(cls, "magnitude", "D"));
auto unit = static_cast<jstring>(env->GetObjectField(
value, env->GetFieldID(cls, "unit", "Ljava/lang/String;")));
const odr::Measure result(magnitude, odr::DynamicUnit(to_string(env, unit)));
env->DeleteLocalRef(unit);
env->DeleteLocalRef(cls);
return result;
}

odr::DirectionalStyle<odr::Measure>
directional_measure_from_java(JNIEnv *env, jobject value) {
odr::DirectionalStyle<odr::Measure> result;
if (value == nullptr) {
return result;
}

jclass cls = env->GetObjectClass(value);
const auto side = [&](const char *name) {
jobject measure = env->GetObjectField(
value, env->GetFieldID(cls, name, "Lapp/opendocument/core/Measure;"));
std::optional<odr::Measure> parsed = measure_from_java(env, measure);
env->DeleteLocalRef(measure);
return parsed;
};
result.right = side("right");
result.top = side("top");
result.left = side("left");
result.bottom = side("bottom");
env->DeleteLocalRef(cls);
return result;
}

jobject
make_directional_string(JNIEnv *env,
const odr::DirectionalStyle<std::string> &style) {
Expand Down Expand Up @@ -374,6 +412,8 @@ jobject html_config_to_java(JNIEnv *env, const odr::HtmlConfig &config) {
box_integer(env, config.viewport_width));
set_object("initialZoom", "Ljava/lang/Double;",
box_double(env, config.initial_zoom));
set_object("minContentMargin", "Lapp/opendocument/core/DirectionalMeasure;",
make_directional_measure(env, config.min_content_margin));
set_boolean("formatHtml", config.format_html);
set_int("htmlIndent", config.html_indent);
set_string("htmlIndentString", config.html_indent_string);
Expand Down Expand Up @@ -540,6 +580,12 @@ odr::HtmlConfig html_config_from_java(JNIEnv *env, jobject config) {
}
env->DeleteLocalRef(zoom);
}
{
jobject margin = get_object("minContentMargin",
"Lapp/opendocument/core/DirectionalMeasure;");
result.min_content_margin = directional_measure_from_java(env, margin);
env->DeleteLocalRef(margin);
}
result.format_html = get_boolean("formatHtml");
result.html_indent = static_cast<std::uint8_t>(get_int("htmlIndent"));
result.html_indent_string = get_string("htmlIndentString");
Expand Down
25 changes: 25 additions & 0 deletions jni/tests/app/opendocument/core/HtmlTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,31 @@ void viewportConfigRoundTrips() throws IOException {
assertEquals(Double.valueOf(1.5), readBack.initialZoom);
}

/** The C++ suite covers where the floor lands; this only proves it crosses JNI. */
@Test
void minContentMarginReachesTheHtml() throws IOException {
assertNull(new HtmlConfig().minContentMargin.top);
assertTrue(!renderOdt(new HtmlConfig()).contains(":root{--odr-min-margin"));

// the field is the host's to null, and reading one is not a crash
HtmlConfig cleared = new HtmlConfig();
cleared.minContentMargin = null;
assertTrue(!renderOdt(cleared).contains(":root{--odr-min-margin"));

HtmlConfig config = new HtmlConfig();
config.minContentMargin =
new DirectionalMeasure(null, new Measure(12, "px"), new Measure(1, "cm"), null);
assertTrue(
renderOdt(config).contains(":root{--odr-min-margin-top:12px;--odr-min-margin-left:1cm;}"));

Path cache = Files.createDirectories(tempDir.resolve("margin"));
DecodedFile file = Odr.open(TestFiles.odtFile(tempDir).toString());
HtmlConfig readBack = Html.translate(file, cache.toString(), config).config();
assertEquals(new Measure(12, "px"), readBack.minContentMargin.top);
assertEquals(new Measure(1, "cm"), readBack.minContentMargin.left);
assertNull(readBack.minContentMargin.right);
}

/** The C++ suite covers the mode matrix; this only proves the config crosses JNI. */
@Test
void viewportModeReachesTheHtml() throws IOException {
Expand Down
1 change: 1 addition & 0 deletions python/src/bind_html.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ void odr_python::bind_html(py::module_ &m) {
.def_readwrite("viewport_content", &odr::HtmlConfig::viewport_content)
.def_readwrite("viewport_width", &odr::HtmlConfig::viewport_width)
.def_readwrite("initial_zoom", &odr::HtmlConfig::initial_zoom)
.def_readwrite("min_content_margin", &odr::HtmlConfig::min_content_margin)
.def_readwrite("format_html", &odr::HtmlConfig::format_html)
.def_readwrite("html_indent", &odr::HtmlConfig::html_indent)
.def_readwrite("html_indent_string", &odr::HtmlConfig::html_indent_string)
Expand Down
23 changes: 23 additions & 0 deletions python/tests/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,29 @@ def render(name, config):
assert '<meta name="viewport" content="width=420"/>' in render("raw", raw)


def test_min_content_margin_reaches_the_html(odt_path, tmp_path):
# The C++ suite covers where the floor lands; this only proves it crosses
# the binding, unset sides and all.
def render(name, config):
cache = tmp_path / name
cache.mkdir()
file = pyodr.open(str(odt_path))
service = pyodr.html.translate(file, str(cache), config)
content, _ = service.list_views()[0].write_html()
return content

default = pyodr.HtmlConfig()
assert default.min_content_margin.top is None
assert ":root{--odr-min-margin" not in render("default", default)

config = pyodr.HtmlConfig()
config.min_content_margin.top = pyodr.Measure("12px")
config.min_content_margin.left = pyodr.Measure("1cm")
html = render("margin", config)
assert ":root{--odr-min-margin-top:12px;--odr-min-margin-left:1cm;}" in html
assert "--odr-min-margin-right:" not in html


def test_translate_text(txt_path, tmp_path):
html = translate_offline(txt_path, tmp_path)
pages = html.pages()
Expand Down
6 changes: 6 additions & 0 deletions src/odr/html.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <odr/document.hpp>
#include <odr/file.hpp>
#include <odr/logger.hpp>
#include <odr/style.hpp>
#include <odr/table_dimension.hpp>

#include <functional>
Expand Down Expand Up @@ -156,6 +157,11 @@ struct HtmlConfig {
/// The zoom the view opens at, 1 being actual size; unset follows the fit.
std::optional<double> initial_zoom;

/// The least distance the generated content keeps from the view's border. A
/// set side raises the inset the view already has, never lowers it; a unit
/// css cannot read as a length is ignored. A sheet is never inset.
DirectionalStyle<Measure> min_content_margin;

/// Indent and break the output into lines rather than writing one stream.
bool format_html{false};
/// Repeated @ref html_indent_string per nesting level; 0 disables indenting.
Expand Down
Loading
Loading