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..c7695207 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::nullopt + : std::make_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()