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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ The release run heads these entries with the version and opens a fresh
string, renders that text instead of dropping it.
- An embedded font that will not re-encode says so in the log rather than being
swapped for a substitute in silence.
- Paged output rendered **into a frame** fits the viewport itself. A viewport
meta tag is honoured for the top-level document only, so an embedder got no
fit at all. Only ever down, and a top-level document is left to the meta tag.
- **New** `HtmlConfig::viewport_width`: the width the output will be shown at,
in css pixels. The fit is then a factor in the emitted css β€” no script, framed
or not. Bound in the python, wasm, jni and apple bindings as `viewportWidth`.
- An image view fits the viewport too: `img{max-width:100%}`, so a scan wider
than the frame stops overflowing. `actual_size` still shows it 1:1.
- Output that fits itself keeps the reader's place when the viewport changes.
The browser's own guess is wrong here because the scale changes with the
width, and a long document came back a page or more from where it was.

## v6.9.0 - 2026-08-18

Expand Down
2 changes: 2 additions & 0 deletions apple/include/OdrCoreObjC/ODRHtml.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ NS_SWIFT_NAME(HtmlConfig)
@property(nonatomic, strong, nullable) NSNumber *spreadsheetViewportMode;
/// Raw `content` for the viewport meta tag; overrides the modes above.
@property(nonatomic, copy, nullable) NSString *viewportContent;
/// The width the output is shown at, in css pixels; fits paged content to it.
@property(nonatomic, strong, nullable) NSNumber *viewportWidth;

@property(nonatomic) BOOL formatHtml;
/// Repeated `htmlIndentString` per nesting level; 0 disables indentation.
Expand Down
9 changes: 9 additions & 0 deletions apple/src/ODRHtml.mm
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ - (instancetype)initWithNativeConfig:(const odr::HtmlConfig &)config {
_viewportContent = config.viewport_content.has_value()
? to_nsstring(*config.viewport_content)
: nil;
_viewportWidth = config.viewport_width.has_value()
? @(static_cast<unsigned int>(*config.viewport_width))
: nil;
_formatHtml = config.format_html ? YES : NO;
_htmlIndent = config.html_indent;
_htmlIndentString = to_nsstring(config.html_indent_string);
Expand Down Expand Up @@ -169,6 +172,12 @@ - (instancetype)initWithNativeConfig:(const odr::HtmlConfig &)config {
} else {
config.viewport_content.reset();
}
if (_viewportWidth != nil) {
config.viewport_width =
static_cast<std::uint32_t>(_viewportWidth.unsignedIntValue);
} else {
config.viewport_width.reset();
}
config.format_html = _formatHtml == YES;
config.html_indent = _htmlIndent;
config.html_indent_string = to_string(_htmlIndentString);
Expand Down
2 changes: 2 additions & 0 deletions jni/java/app/opendocument/core/HtmlConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ public final class HtmlConfig {
public HtmlViewportMode spreadsheetViewportMode;
/** Raw {@code content} for the viewport meta tag; overrides the modes above when set. */
public String viewportContent;
/** The width the output is shown at, in css pixels; fits paged content to it. */
public Integer viewportWidth;

public boolean formatHtml = false;
public int htmlIndent = 1;
Expand Down
15 changes: 15 additions & 0 deletions jni/src/jni_style.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,8 @@ jobject html_config_to_java(JNIEnv *env, const odr::HtmlConfig &config) {
: -1));
set_object("viewportContent", "Ljava/lang/String;",
make_string_opt(env, config.viewport_content));
set_object("viewportWidth", "Ljava/lang/Integer;",
box_integer(env, config.viewport_width));
set_boolean("formatHtml", config.format_html);
set_int("htmlIndent", config.html_indent);
set_string("htmlIndentString", config.html_indent_string);
Expand Down Expand Up @@ -508,6 +510,19 @@ odr::HtmlConfig html_config_from_java(JNIEnv *env, jobject config) {
: std::make_optional(static_cast<odr::HtmlViewportMode>(code));
}
result.viewport_content = get_string_opt("viewportContent");
{
jobject width = get_object("viewportWidth", "Ljava/lang/Integer;");
if (width == nullptr) {
result.viewport_width = std::nullopt;
} else {
jclass integer_cls = env->GetObjectClass(width);
jmethodID int_value = env->GetMethodID(integer_cls, "intValue", "()I");
result.viewport_width =
static_cast<std::uint32_t>(env->CallIntMethod(width, int_value));
env->DeleteLocalRef(integer_cls);
}
env->DeleteLocalRef(width);
}
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
3 changes: 3 additions & 0 deletions jni/tests/app/opendocument/core/HtmlTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ void htmlConfigDefaults() {
assertEquals(HtmlViewportMode.AUTOMATIC, config.viewportMode);
assertNull(config.spreadsheetViewportMode);
assertNull(config.viewportContent);
assertNull(config.viewportWidth);
}

@Test
Expand All @@ -46,6 +47,7 @@ void viewportConfigRoundTrips() throws IOException {
config.viewportMode = HtmlViewportMode.FIT_WIDTH;
config.spreadsheetViewportMode = HtmlViewportMode.ACTUAL_SIZE;
config.viewportContent = "width=420";
config.viewportWidth = 420;

Path cache = Files.createDirectories(tempDir.resolve("cache"));
DecodedFile file = Odr.open(TestFiles.odtFile(tempDir).toString());
Expand All @@ -54,6 +56,7 @@ void viewportConfigRoundTrips() throws IOException {
assertEquals(HtmlViewportMode.FIT_WIDTH, readBack.viewportMode);
assertEquals(HtmlViewportMode.ACTUAL_SIZE, readBack.spreadsheetViewportMode);
assertEquals("width=420", readBack.viewportContent);
assertEquals(Integer.valueOf(420), readBack.viewportWidth);
}

/** The C++ suite covers the mode matrix; this only proves the config crosses JNI. */
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 @@ -90,6 +90,7 @@ void odr_python::bind_html(py::module_ &m) {
.def_readwrite("spreadsheet_viewport_mode",
&odr::HtmlConfig::spreadsheet_viewport_mode)
.def_readwrite("viewport_content", &odr::HtmlConfig::viewport_content)
.def_readwrite("viewport_width", &odr::HtmlConfig::viewport_width)
.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
3 changes: 3 additions & 0 deletions python/tests/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@ def test_html_config_viewport_defaults():
assert config.viewport_mode == pyodr.HtmlViewportMode.automatic
assert config.spreadsheet_viewport_mode is None
assert config.viewport_content is None
assert config.viewport_width is None

config.viewport_mode = pyodr.HtmlViewportMode.fit_width
config.spreadsheet_viewport_mode = pyodr.HtmlViewportMode.actual_size
config.viewport_content = "width=420"
config.viewport_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"
assert config.viewport_width == 420


def test_viewport_mode_reaches_the_html(odt_path, tmp_path):
Expand Down
49 changes: 24 additions & 25 deletions src/odr/html.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,54 +104,55 @@ enum class PdfTextMode {

/// @brief HTML configuration.
struct HtmlConfig {
// document output file names
/// File name for the view that writes the whole document.
std::string document_output_file_name{"document.html"};

// document element output file names
// per-element view file names; `{index}` is the element's 0-based number
std::string slide_output_file_name{"slide{index}.html"};
std::string sheet_output_file_name{"sheet{index}.html"};
std::string page_output_file_name{"page{index}.html"};

// embedding
/// Embed images as data urls rather than writing them beside the document.
bool embed_images{true};
/// Write the renderer's own css and js into every document rather than beside
/// it as one shared file the documents link.
bool embed_shipped_resources{true};

// resources
/// Where linked shipped resources go, relative to the output path unless
/// named absolutely. Empty puts them beside the document.
std::string resource_path;
/// Link an absolute @ref resource_path relative to the document, so the
/// output stays movable.
bool relative_resource_paths{true};

// create editable output
/// Write `contenteditable` output, which back-translation reads edits from.
bool editable{false};

// text document margin
/// Render a text document as fixed-size pages rather than reflowing text.
bool text_document_margin{false};

// colors the output renders against
/// The colors the output renders against.
HtmlColorScheme color_scheme{HtmlColorScheme::light};

// spreadsheet table limit
/// Largest sheet region written; cells past it are dropped.
std::optional<TableDimensions> spreadsheet_limit{TableDimensions(10000, 500)};
/// Trim a sheet to the cells it uses before @ref spreadsheet_limit applies.
bool spreadsheet_limit_by_content{true};
// spreadsheet gridlines
/// Which gridlines a sheet paints.
HtmlTableGridlines spreadsheet_gridlines{HtmlTableGridlines::soft};

// initial zoom on mobile
/// Initial zoom on mobile; see @ref HtmlViewportMode.
HtmlViewportMode viewport_mode{HtmlViewportMode::automatic};
// overrides `viewport_mode` for spreadsheet content when set
/// Overrides @ref viewport_mode for spreadsheet content when set.
std::optional<HtmlViewportMode> spreadsheet_viewport_mode;
// raw `content` for the viewport meta tag; overrides the modes above
/// Raw `content` for the viewport meta tag; overrides the modes above.
std::optional<std::string> viewport_content;
/// The width the output is shown at, in css pixels; fits paged content to it.
std::optional<std::uint32_t> viewport_width;

// formatting
/// Indent and break the output into lines rather than writing one stream.
bool format_html{false};
// Indentation when `format_html` is set: `html_indent_string` is repeated
// `html_indent` times per nesting level (0 disables indentation entirely).
/// Repeated @ref html_indent_string per nesting level; 0 disables indenting.
std::uint8_t html_indent{1};
std::string html_indent_string{"\t"};

Expand All @@ -160,22 +161,20 @@ struct HtmlConfig {
/// @deprecated See @ref background_image_format.
double background_image_dpi{144.0};

// Paged-document page range (currently honored by the PDF pipeline): render
// only pages with 0-based index in `[page_range_begin, page_range_end)`.
// Page views and `#pN` anchors keep their document-global page numbers.
/// Renders only the pages with 0-based index in `[page_range_begin,
/// page_range_end)`; page views and `#pN` anchors keep their document-global
/// numbers. Honored by the pdf pipeline.
std::uint32_t page_range_begin{0};
std::optional<std::uint32_t> page_range_end;

// PDF text mode
/// How pdf text is written; see @ref PdfTextMode.
PdfTextMode pdf_text_mode{PdfTextMode::dual_layer};
// `dual_layer` renders its invisible selection layer in a local system font
// (first of these that resolves), whose natural width rarely matches the
// PDF-derived box CSS justify has to fill β€” and justify can only add spacing.
// The size-adjust (0-1, written as the @font-face percent) shrinks the
// fallback's metrics toward the PDF's to close that gap. Safe to
// underestimate, not to overestimate: the excess is clipped, not shrunk.
/// System fonts `dual_layer` sets its selection layer in, first that
/// resolves.
std::vector<std::string> pdf_dual_layer_fallback_fonts{
"Arial", "Helvetica", "Liberation Sans", "DejaVu Sans", "Nimbus Sans"};
/// Shrinks the fallback's metrics toward the pdf's (0-1) so css justify can
/// fill the box. Safe to underestimate: the excess is clipped, not shrunk.
double pdf_dual_layer_fallback_font_size_adjust{0.5};

/// @deprecated Inert: no output carries a restriction to lift.
Expand Down
61 changes: 61 additions & 0 deletions src/odr/internal/html/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
#include <odr/internal/util/string_util.hpp>

#include <odr/html.hpp>
#include <odr/quantity.hpp>
#include <odr/style.hpp>

#include <fstream>
#include <iomanip>
#include <sstream>
#include <string>
#include <unordered_map>

namespace odr::internal {

Expand Down Expand Up @@ -44,6 +47,64 @@ void html::write_viewport_meta(
}
}

bool html::fits_width(const HtmlConfig &config, const bool fit_width_by_default,
const std::optional<HtmlViewportMode> mode_override) {
// A raw `viewport_content` is the caller taking the question over.
if (config.viewport_content.has_value()) {
return false;
}

const HtmlViewportMode mode = mode_override.value_or(config.viewport_mode);
if (mode == HtmlViewportMode::automatic) {
return fit_width_by_default;
}
return mode == HtmlViewportMode::fit_width;
}

std::optional<double> html::css_pixels(const std::optional<Measure> &measure) {
if (!measure.has_value()) {
return {};
}

// css absolute lengths, all defined against the inch (css values 3, 5.2).
static const std::unordered_map<std::string, double> per_unit{
{"px", 1.0}, {"in", 96.0}, {"pt", 96.0 / 72.0},
{"pc", 96.0 / 6}, {"cm", 96.0 / 2.54}, {"mm", 96.0 / 25.4},
};

const auto it = per_unit.find(std::string(measure->unit().name()));
if (it == std::end(per_unit)) {
return {};
}
const double pixels = measure->magnitude() * it->second;
return pixels > 0 ? std::optional(pixels) : std::nullopt;
}

bool html::write_viewport_fit_style(
HtmlWriter &out, const HtmlConfig &config, const bool fits,
const std::optional<double> content_pixels) {
if (!fits || !config.viewport_width.has_value() ||
!content_pixels.has_value()) {
return false;
}

const double factor =
static_cast<double>(config.viewport_width.value()) / *content_pixels;
// only ever down: a page narrower than the viewport is shown at its size
if (factor >= 1) {
return true;
}

out.write_header_style_begin();
// `zoom` scales the layout, so the page scrolls against the scaled size
// instead of overflowing beside it; `Measure` renders no exponent form
out.out() << "body{zoom:" << Measure(factor, DynamicUnit()).to_string()
<< "}";
out.write_header_style_end();

return true;
}

std::string html::escape_text(std::string text) {
if (text.empty()) {
return text;
Expand Down
18 changes: 18 additions & 0 deletions src/odr/internal/html/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include <odr/html.hpp>
#include <odr/internal/abstract/html_service.hpp>
#include <odr/quantity.hpp>

namespace odr {
struct Color;
Expand Down Expand Up @@ -45,6 +46,23 @@ void write_viewport_meta(HtmlWriter &out, const HtmlConfig &config,
bool fit_width_by_default,
std::optional<HtmlViewportMode> mode_override = {});

/// Whether the output is meant to fit its width to the viewport.
[[nodiscard]] bool
fits_width(const HtmlConfig &config, bool fit_width_by_default,
std::optional<HtmlViewportMode> mode_override = {});

/// @p measure in css pixels, or nothing without an absolute unit.
[[nodiscard]] std::optional<double>
css_pixels(const std::optional<Measure> &measure);

/// The side gutters the page column puts around its pages, in css pixels.
constexpr double page_column_gutter_pixels = 32;

/// Scales the body so @p content_pixels fits `config.viewport_width`. Writes
/// nothing unless @p fits and both widths are known.
bool write_viewport_fit_style(HtmlWriter &out, const HtmlConfig &config,
bool fits, std::optional<double> content_pixels);

std::string escape_text(std::string text);

/// Escape a string for use as an HTML double-quoted attribute value (`&`, `"`,
Expand Down
Loading
Loading