From 0115053a08718f4b92c5636c84ee916eedf47f73 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Fri, 31 Jul 2026 23:19:15 +0200 Subject: [PATCH 1/2] feat: bind the html viewport config for java and python `HtmlConfig::viewport_mode`, `spreadsheet_viewport_mode` and `viewport_content` shipped without bindings, so the mobile initial zoom was whatever `automatic` resolved to - not reachable from OpenDocument.droid, which is the consumer the option exists for. Java gets `HtmlViewportMode` (ordinals matching the C++ enum, `null` for the absent optional, as everywhere else in the marshalling) plus the three `HtmlConfig` fields; python gets the enum and three `def_readwrite`s. The C++ suite already covers the mode matrix, so the new tests only prove the config crosses the binding: a round trip through `HtmlService.config()`, and the meta tag an odt renders with. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qis7KBzd1YHZqPa9ZEd6WV --- jni/CMakeLists.txt | 1 + .../app/opendocument/core/HtmlConfig.java | 7 +++ .../opendocument/core/HtmlViewportMode.java | 14 +++++ jni/src/jni_style.cpp | 28 ++++++++++ jni/tests/app/opendocument/core/HtmlTest.java | 53 +++++++++++++++++++ python/src/bind_html.cpp | 10 ++++ python/tests/test_html.py | 44 +++++++++++++++ 7 files changed, 157 insertions(+) create mode 100644 jni/java/app/opendocument/core/HtmlViewportMode.java diff --git a/jni/CMakeLists.txt b/jni/CMakeLists.txt index 46890195..07c210b2 100644 --- a/jni/CMakeLists.txt +++ b/jni/CMakeLists.txt @@ -112,6 +112,7 @@ add_jar(odr_java "java/app/opendocument/core/HtmlService.java" "java/app/opendocument/core/HtmlTableGridlines.java" "java/app/opendocument/core/HtmlView.java" + "java/app/opendocument/core/HtmlViewportMode.java" "java/app/opendocument/core/HttpServer.java" "java/app/opendocument/core/Image.java" "java/app/opendocument/core/ImageFile.java" diff --git a/jni/java/app/opendocument/core/HtmlConfig.java b/jni/java/app/opendocument/core/HtmlConfig.java index fe926bc9..7621ef19 100644 --- a/jni/java/app/opendocument/core/HtmlConfig.java +++ b/jni/java/app/opendocument/core/HtmlConfig.java @@ -28,6 +28,13 @@ public final class HtmlConfig { public boolean spreadsheetLimitByContent = true; public HtmlTableGridlines spreadsheetGridlines = HtmlTableGridlines.SOFT; + /** Initial zoom on mobile. */ + public HtmlViewportMode viewportMode = HtmlViewportMode.AUTOMATIC; + /** Overrides {@link #viewportMode} for spreadsheet content; {@code null} keeps it. */ + public HtmlViewportMode spreadsheetViewportMode; + /** Raw {@code content} for the viewport meta tag; overrides the modes above when set. */ + public String viewportContent; + public boolean formatHtml = false; public int htmlIndent = 1; public String htmlIndentString = "\t"; diff --git a/jni/java/app/opendocument/core/HtmlViewportMode.java b/jni/java/app/opendocument/core/HtmlViewportMode.java new file mode 100644 index 00000000..e391fda1 --- /dev/null +++ b/jni/java/app/opendocument/core/HtmlViewportMode.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::HtmlViewportMode}; constant order must match the C++ declaration. */ +public enum HtmlViewportMode { + AUTOMATIC, FIT_WIDTH, ACTUAL_SIZE, NONE; + + static HtmlViewportMode fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/src/jni_style.cpp b/jni/src/jni_style.cpp index 3107e325..a5f1eec3 100644 --- a/jni/src/jni_style.cpp +++ b/jni/src/jni_style.cpp @@ -350,6 +350,17 @@ jobject html_config_to_java(JNIEnv *env, const odr::HtmlConfig &config) { "Lapp/opendocument/core/HtmlTableGridlines;", enum_from_code(env, "app/opendocument/core/HtmlTableGridlines", static_cast(config.spreadsheet_gridlines))); + set_object("viewportMode", "Lapp/opendocument/core/HtmlViewportMode;", + enum_from_code(env, "app/opendocument/core/HtmlViewportMode", + static_cast(config.viewport_mode))); + set_object( + "spreadsheetViewportMode", "Lapp/opendocument/core/HtmlViewportMode;", + enum_from_code(env, "app/opendocument/core/HtmlViewportMode", + config.spreadsheet_viewport_mode.has_value() + ? static_cast(*config.spreadsheet_viewport_mode) + : -1)); + set_object("viewportContent", "Ljava/lang/String;", + make_string_opt(env, config.viewport_content)); set_boolean("formatHtml", config.format_html); set_int("htmlIndent", config.html_indent); set_string("htmlIndentString", config.html_indent_string); @@ -464,6 +475,23 @@ odr::HtmlConfig html_config_from_java(JNIEnv *env, jobject config) { result.spreadsheet_gridlines = static_cast(code); } } + { + const jint code = enum_ordinal( + env, + get_object("viewportMode", "Lapp/opendocument/core/HtmlViewportMode;")); + if (code >= 0) { + result.viewport_mode = static_cast(code); + } + } + { + const jint code = enum_ordinal( + env, get_object("spreadsheetViewportMode", + "Lapp/opendocument/core/HtmlViewportMode;")); + result.spreadsheet_viewport_mode = + code < 0 ? std::optional() + : static_cast(code); + } + result.viewport_content = get_string_opt("viewportContent"); result.format_html = get_boolean("formatHtml"); result.html_indent = static_cast(get_int("htmlIndent")); result.html_indent_string = get_string("htmlIndentString"); diff --git a/jni/tests/app/opendocument/core/HtmlTest.java b/jni/tests/app/opendocument/core/HtmlTest.java index 5dad7837..33b1f60e 100644 --- a/jni/tests/app/opendocument/core/HtmlTest.java +++ b/jni/tests/app/opendocument/core/HtmlTest.java @@ -1,6 +1,7 @@ package app.opendocument.core; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -22,12 +23,64 @@ private Html translateOffline(Path input) throws IOException { return service.bringOffline(output.toString()); } + private String renderOdt(HtmlConfig config) throws IOException { + Path cache = Files.createTempDirectory(tempDir, "render"); + DecodedFile file = Odr.open(TestFiles.odtFile(tempDir).toString()); + HtmlService service = Html.translate(file, cache.toString(), config); + return service.listViews().get(0).writeHtml().html; + } + @Test void htmlConfigDefaults() { HtmlConfig config = new HtmlConfig(); assertTrue(config.embedImages); assertTrue(!config.editable); assertEquals(HtmlTableGridlines.SOFT, config.spreadsheetGridlines); + assertEquals(HtmlViewportMode.AUTOMATIC, config.viewportMode); + assertNull(config.spreadsheetViewportMode); + assertNull(config.viewportContent); + } + + @Test + void viewportConfigRoundTrips() throws IOException { + assumeTrue(TestFiles.hasCoreData(), "odr core data path not available"); + HtmlConfig config = new HtmlConfig(); + config.viewportMode = HtmlViewportMode.FIT_WIDTH; + config.spreadsheetViewportMode = HtmlViewportMode.ACTUAL_SIZE; + config.viewportContent = "width=420"; + + Path cache = Files.createDirectories(tempDir.resolve("cache")); + DecodedFile file = Odr.open(TestFiles.odtFile(tempDir).toString()); + HtmlConfig readBack = Html.translate(file, cache.toString(), config).config(); + + assertEquals(HtmlViewportMode.FIT_WIDTH, readBack.viewportMode); + assertEquals(HtmlViewportMode.ACTUAL_SIZE, readBack.spreadsheetViewportMode); + assertEquals("width=420", readBack.viewportContent); + } + + /** The C++ suite covers the mode matrix; this only proves the config crosses JNI. */ + @Test + void viewportModeReachesTheHtml() throws IOException { + assumeTrue(TestFiles.hasCoreData(), "odr core data path not available"); + + // A text document without margins is reflowing content, so `AUTOMATIC` + // resolves to `ACTUAL_SIZE`. + String automatic = renderOdt(new HtmlConfig()); + assertTrue( + automatic.contains( + "")); + + HtmlConfig fitWidth = new HtmlConfig(); + fitWidth.viewportMode = HtmlViewportMode.FIT_WIDTH; + assertTrue( + renderOdt(fitWidth) + .contains( + "")); + + HtmlConfig raw = new HtmlConfig(); + raw.viewportContent = "width=420"; + assertTrue(renderOdt(raw).contains("")); } @Test diff --git a/python/src/bind_html.cpp b/python/src/bind_html.cpp index 76e7e34f..10b913fc 100644 --- a/python/src/bind_html.cpp +++ b/python/src/bind_html.cpp @@ -28,6 +28,12 @@ void odr_python::bind_html(py::module_ &m) { .value("soft", odr::HtmlTableGridlines::soft) .value("hard", odr::HtmlTableGridlines::hard); + py::enum_(m, "HtmlViewportMode") + .value("automatic", odr::HtmlViewportMode::automatic) + .value("fit_width", odr::HtmlViewportMode::fit_width) + .value("actual_size", odr::HtmlViewportMode::actual_size) + .value("none", odr::HtmlViewportMode::none); + py::enum_(m, "PdfTextMode") .value("dual_layer", odr::PdfTextMode::dual_layer) .value("single_layer", odr::PdfTextMode::single_layer); @@ -72,6 +78,10 @@ void odr_python::bind_html(py::module_ &m) { &odr::HtmlConfig::spreadsheet_limit_by_content) .def_readwrite("spreadsheet_gridlines", &odr::HtmlConfig::spreadsheet_gridlines) + .def_readwrite("viewport_mode", &odr::HtmlConfig::viewport_mode) + .def_readwrite("spreadsheet_viewport_mode", + &odr::HtmlConfig::spreadsheet_viewport_mode) + .def_readwrite("viewport_content", &odr::HtmlConfig::viewport_content) .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) diff --git a/python/tests/test_html.py b/python/tests/test_html.py index 89bf4264..f84b9f05 100644 --- a/python/tests/test_html.py +++ b/python/tests/test_html.py @@ -26,6 +26,50 @@ def test_html_config_defaults(): assert config.spreadsheet_limit.rows == 100 +def test_html_config_viewport_defaults(): + config = pyodr.HtmlConfig() + assert config.viewport_mode == pyodr.HtmlViewportMode.automatic + assert config.spreadsheet_viewport_mode is None + assert config.viewport_content is None + + config.viewport_mode = pyodr.HtmlViewportMode.fit_width + config.spreadsheet_viewport_mode = pyodr.HtmlViewportMode.actual_size + config.viewport_content = "width=420" + assert config.viewport_mode == pyodr.HtmlViewportMode.fit_width + assert config.spreadsheet_viewport_mode == pyodr.HtmlViewportMode.actual_size + assert config.viewport_content == "width=420" + + +def test_viewport_mode_reaches_the_html(core_data_path, odt_path, tmp_path): + # The C++ suite covers the mode matrix; this only proves the config crosses + # the binding. A text document without margins is reflowing content, so + # `automatic` resolves to `actual_size`. + 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 + + assert ( + '' + in render("automatic", pyodr.HtmlConfig()) + ) + + fit_width = pyodr.HtmlConfig() + fit_width.viewport_mode = pyodr.HtmlViewportMode.fit_width + assert ( + '' + in render("fit_width", fit_width) + ) + + raw = pyodr.HtmlConfig() + raw.viewport_content = "width=420" + assert '' in render("raw", raw) + + def test_translate_text(core_data_path, txt_path, tmp_path): html = translate_offline(txt_path, tmp_path) pages = html.pages() From 50551186c3440f9cda3bddbfdd0bcef9bcd74e59 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Fri, 31 Jul 2026 23:25:36 +0200 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- jni/src/jni_style.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jni/src/jni_style.cpp b/jni/src/jni_style.cpp index a5f1eec3..c7695207 100644 --- a/jni/src/jni_style.cpp +++ b/jni/src/jni_style.cpp @@ -488,8 +488,8 @@ odr::HtmlConfig html_config_from_java(JNIEnv *env, jobject config) { env, get_object("spreadsheetViewportMode", "Lapp/opendocument/core/HtmlViewportMode;")); result.spreadsheet_viewport_mode = - code < 0 ? std::optional() - : static_cast(code); + code < 0 ? std::nullopt + : std::make_optional(static_cast(code)); } result.viewport_content = get_string_opt("viewportContent"); result.format_html = get_boolean("formatHtml");