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

- 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
Expand Down
32 changes: 30 additions & 2 deletions src/odr/internal/html/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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=<n>,` where the browser's own floor cannot fit the content,
/// empty where it can.
std::string minimum_scale_for(const std::optional<double> 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<HtmlViewportMode> mode_override) {
const std::optional<HtmlViewportMode> mode_override,
const std::optional<double> content_pixels) {
if (config.viewport_content.has_value()) {
out.write_header_viewport(
xml::escape_attribute(config.viewport_content.value()));
Expand All @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion src/odr/internal/html/common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<HtmlViewportMode> mode_override = {});
std::optional<HtmlViewportMode> mode_override = {},
std::optional<double> content_pixels = {});

/// Who fits the output's width to the viewport.
enum class WidthFit {
Expand Down
3 changes: 2 additions & 1 deletion src/odr/internal/html/document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ void write_head(const Document &document, const WritingState &state,
: "odr");
const std::optional<HtmlViewportMode> 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)
Expand Down
2 changes: 1 addition & 1 deletion src/odr/internal/html/pdf_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
30 changes: 30 additions & 0 deletions test/src/html_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,38 @@ std::optional<double> 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) {
Expand Down
Loading