diff --git a/CHANGELOG.md b/CHANGELOG.md index 98f259e2e..eca30cd19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- Fitting the width states a `minimum-scale` where the content is more than + four screens wide, which the browser's own floor of 0.25 cannot reach. An A0 + pdf page now zooms out to fit; narrower content is unchanged. + - **Fix**: selecting text in a pdf view showed the invisible layer that carries it, as a second set of glyphs over the drawn ones. That layer now stays transparent under `::selection`, and states the highlight background, which diff --git a/src/odr/internal/html/common.cpp b/src/odr/internal/html/common.cpp index 26192001d..9d2c10a81 100644 --- a/src/odr/internal/html/common.cpp +++ b/src/odr/internal/html/common.cpp @@ -27,9 +27,35 @@ namespace odr::internal { +namespace { + +/// The narrowest screen a floor is computed for, in css px. +constexpr double narrowest_viewport_pixels = 320.0; + +/// What the browser floors the page scale at on its own. +constexpr double browser_minimum_scale = 0.25; + +/// `minimum-scale=,` where the browser's own floor cannot fit the content, +/// empty where it can. +std::string minimum_scale_for(const std::optional content_pixels) { + if (!content_pixels.has_value() || *content_pixels <= 0) { + return {}; + } + const double fit = narrowest_viewport_pixels / *content_pixels; + if (fit >= browser_minimum_scale) { + return {}; + } + // three decimals reach a page 320000px wide, and the string stays short + const double floored = std::max(std::floor(fit * 1000.0) / 1000.0, 0.001); + return "minimum-scale=" + Measure(floored, DynamicUnit()).to_string() + ","; +} + +} // namespace + void html::write_viewport_meta( HtmlWriter &out, const HtmlConfig &config, const bool fit_width_by_default, - const std::optional mode_override) { + const std::optional mode_override, + const std::optional content_pixels) { if (config.viewport_content.has_value()) { out.write_header_viewport( xml::escape_attribute(config.viewport_content.value())); @@ -44,7 +70,9 @@ void html::write_viewport_meta( switch (mode) { case HtmlViewportMode::fit_width: - out.write_header_viewport("width=device-width,user-scalable=yes"); + out.write_header_viewport("width=device-width," + + minimum_scale_for(content_pixels) + + "user-scalable=yes"); break; case HtmlViewportMode::actual_size: // A stated scale is what turns the browser's own fitting off. diff --git a/src/odr/internal/html/common.hpp b/src/odr/internal/html/common.hpp index 6606639cc..a1411346f 100644 --- a/src/odr/internal/html/common.hpp +++ b/src/odr/internal/html/common.hpp @@ -86,9 +86,13 @@ struct WritingState { /// `config.viewport_mode`. `fit_width_by_default` resolves /// `HtmlViewportMode::automatic`: true for fixed-size paged content, false for /// content that reflows to the screen width. +/// +/// `content_pixels` is the width the content wants, as `write_zoom_style` +/// takes it: fitting the width states a `minimum-scale` that reaches it. void write_viewport_meta(HtmlWriter &out, const HtmlConfig &config, bool fit_width_by_default, - std::optional mode_override = {}); + std::optional mode_override = {}, + std::optional content_pixels = {}); /// Who fits the output's width to the viewport. enum class WidthFit { diff --git a/src/odr/internal/html/document.cpp b/src/odr/internal/html/document.cpp index c1a99470c..1bc28c5e2 100644 --- a/src/odr/internal/html/document.cpp +++ b/src/odr/internal/html/document.cpp @@ -172,7 +172,8 @@ void write_head(const Document &document, const WritingState &state, : "odr"); const std::optional mode_override = viewport_mode_override(document, state.config()); - write_viewport_meta(out, state.config(), paged_content, mode_override); + write_viewport_meta(out, state.config(), paged_content, mode_override, + content_pixels); write_zoom_style(out, state.config(), paged_content ? width_fit(state.config(), paged_content, mode_override) diff --git a/src/odr/internal/html/pdf_file.cpp b/src/odr/internal/html/pdf_file.cpp index 13d358f95..0c7ca87ca 100644 --- a/src/odr/internal/html/pdf_file.cpp +++ b/src/odr/internal/html/pdf_file.cpp @@ -2685,7 +2685,7 @@ class HtmlServiceImpl final : public HtmlService { out.write_header_begin(); out.write_header_charset("UTF-8"); out.write_header_title("odr"); - write_viewport_meta(out, config(), true); + write_viewport_meta(out, config(), true, {}, content); write_zoom_style(out, config(), width_fit(config(), true), content); write_content_margin_style(out, config()); out.write_header_style_begin(); diff --git a/test/src/html_test.cpp b/test/src/html_test.cpp index f57e5e9c9..3d9f6fd8d 100644 --- a/test/src/html_test.cpp +++ b/test/src/html_test.cpp @@ -307,8 +307,38 @@ std::optional fit_of(const std::string &page) { return std::stod(page.substr(begin)); } +/// The `content` of the page's viewport meta, or "" where it states none. +std::string viewport_of(const std::string &page) { + constexpr std::string_view key = "name=\"viewport\" content=\""; + const std::size_t at = page.find(key); + if (at == std::string::npos) { + return {}; + } + const std::size_t begin = at + key.length(); + return page.substr(begin, page.find('"', begin) - begin); +} + } // namespace +// A browser floors the page scale at 0.25, so a page more than four screens +// wide cannot be zoomed out to. A wider page states a floor of its own. +TEST(html, fitting_the_width_states_a_floor_a_wide_page_needs) { + HtmlConfig config; + config.text_document_margin = true; + + EXPECT_EQ(viewport_of(render_odt(config)), + "width=device-width,user-scalable=yes"); + + // the gutter is part of the width, so this is a page column four screens + // and more wide + config.min_content_margin.left = Measure("800px"); + config.min_content_margin.right = Measure("800px"); + + const std::string wide = viewport_of(render_odt(config)); + EXPECT_TRUE(wide.starts_with("width=device-width,minimum-scale=0.")) << wide; + EXPECT_TRUE(wide.ends_with(",user-scalable=yes")) << wide; +} + // The gutter around the page column is part of the width the view is fitted // to, so raising it fits the pages smaller. TEST(html, min_content_margin_widens_the_page_column_fit) {