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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ The release run heads these entries with the version and opens a fresh
~50ms.
- A pdf image placed on several pages is one image, and the pdf view honors
`HtmlConfig::embed_images`, which it used to ignore.
- Text in a pdf no longer comes out in giant overlapping type on a browser with
a minimum font size, android's 8px included. Such a run is also placed more
accurately.

## v6.10.1 - 2026-08-21

Expand Down
38 changes: 28 additions & 10 deletions src/odr/internal/html/pdf_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1533,6 +1533,9 @@ class HtmlServiceImpl final : public HtmlService {

const auto [m, invisible, is_matrix, asc, scale, ox, baseline, extent,
font_pt, font_size_pt] = run_geometry(text, to_box);
// `add_position_classes` divides through the matrix, which carries
// `scale`; the uniform branch multiplies by `m.a`.
const double ascent_pt = asc * text.size * (is_matrix ? scale : 1.0);
const std::string color_suffix = color_class(text, invisible, styles);

// --- Visual layer ---------------------------------------------------
Expand Down Expand Up @@ -1564,7 +1567,7 @@ class HtmlServiceImpl final : public HtmlService {
if (new_vis_line) {
std::string line_base = "t";
add_position_classes(line_base, add_class, m, is_matrix, ox,
baseline, asc * text.size);
baseline, ascent_pt);
VisLineOut line_out;
line_out.classes = std::move(line_base);
page_out.vis_items.push_back(std::move(line_out));
Expand Down Expand Up @@ -1636,11 +1639,11 @@ class HtmlServiceImpl final : public HtmlService {
const double tz = text.horizontal_scaling / 100.0;
// In the block's frame the unit is text space: `text.width` carries
// the horizontal scaling the CSS matrix applies again.
const double local_extent = tz != 0 ? text.width / tz : 0;
const double local_extent = tz != 0 ? text.width * scale / tz : 0;
double sel_ox = is_matrix ? 0 : ox;
double sel_baseline = is_matrix ? 0 : baseline;
const double sel_extent = is_matrix ? local_extent : extent;
const double sel_font_pt = is_matrix ? text.size : font_pt;
const double sel_font_pt = is_matrix ? text.size * scale : font_pt;

bool sel_frame_kept =
sel_have_prev && !is_matrix && !sel_prev_was_matrix;
Expand Down Expand Up @@ -1695,7 +1698,7 @@ class HtmlServiceImpl final : public HtmlService {
}
std::string sel_base = "t";
add_position_classes(sel_base, add_class, m, is_matrix, ox,
baseline, asc * text.size);
baseline, ascent_pt);
sel_base += " i"; // transparent
page_out.sel_lines.push_back(SelLineOut{std::move(sel_base), {}});
sel_cur_line = static_cast<int>(page_out.sel_lines.size()) - 1;
Expand Down Expand Up @@ -2125,6 +2128,9 @@ class HtmlServiceImpl final : public HtmlService {

const auto [m, invisible, is_matrix, asc, scale, ox, baseline, extent,
font_pt, font_size_pt] = run_geometry(text, to_box);
// `add_position_classes` divides through the matrix, which carries
// `scale`; the uniform branch multiplies by `m.a`.
const double ascent_pt = asc * text.size * (is_matrix ? scale : 1.0);
const double cs_pt = round2(text.char_spacing * scale);
const double ws_pt = round2(text.word_spacing * scale);
const std::string color_suffix = color_class(text, invisible, styles);
Expand Down Expand Up @@ -2207,7 +2213,7 @@ class HtmlServiceImpl final : public HtmlService {
if (new_line) {
std::string base = "t";
add_position_classes(base, add_class, m, is_matrix, ox, baseline,
asc * text.size);
ascent_pt);
add_class(base, "f", pt_decl("font-size", font_size_pt));
const bool spacing_one_to_one =
font != 0 ||
Expand Down Expand Up @@ -2443,6 +2449,12 @@ class HtmlServiceImpl final : public HtmlService {
double font_size_pt; ///< CSS font-size in px
};

/// A matrix run's font size is the PDF's `Tf`, routinely `1`, which a
/// browser's minimum font size clamps up before the matrix multiplies it.
/// Laying out this much larger and dividing the matrix by it keeps clear of
/// the clamp. Constant, not per run: one block's runs share the space.
static constexpr double matrix_local_scale = 48;

static RunGeometry run_geometry(const pdf::TextElement &text,
const util::math::Transform2D &to_box) {
constexpr util::math::Transform2D flip_glyph =
Expand All @@ -2455,18 +2467,24 @@ class HtmlServiceImpl final : public HtmlService {
// fast path, where it would feed a negative `m.a` into the placement math.
const bool is_matrix = !(m.b == 0 && m.c == 0 && m.a == m.d && m.a > 0);
const double tz = text.horizontal_scaling / 100.0;
// off the original matrix: `extent`/`font_pt` stay page space
const double axis = tz != 0 ? std::hypot(m.a, m.b) / tz : 0;
const double scale = is_matrix ? matrix_local_scale : m.a;
const util::math::Transform2D placed =
is_matrix ? util::math::Transform2D(m.a / scale, m.b / scale,
m.c / scale, m.d / scale, m.e, m.f)
: m;
return RunGeometry{
.m = m,
.m = placed,
.invisible = invisible,
.is_matrix = is_matrix,
.asc = ascent_em(text.font),
.scale = is_matrix ? 1.0 : m.a,
.ox = m.e,
.baseline = m.f,
.scale = scale,
.ox = placed.e,
.baseline = placed.f,
.extent = text.width * axis,
.font_pt = text.size * axis,
.font_size_pt = round2(is_matrix ? text.size : m.a * text.size),
.font_size_pt = round2(text.size * scale),
};
}

Expand Down
4 changes: 2 additions & 2 deletions test/data.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ odr_test_data(
odr_test_data(
PATH "reference-output/odr-public"
URL "https://github.com/opendocument-app/OpenDocument.test.output.git"
REVISION "280cafc1747882ea600cb5ff96a68ced3af81712")
REVISION "c08282c6561ae91fd9eb3dd3ca37fa49a4bda672")

odr_test_data(
PATH "reference-output/odr-private"
URL "https://github.com/opendocument-app/OpenDocument.test-private.output.git"
REVISION "e813a20df7b0623768e8eb2afb83b80f83d43f42")
REVISION "b83c7a7fd85af624f6172fce6bdc923b60708f40")
26 changes: 24 additions & 2 deletions test/src/internal/pdf/pdf_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,7 @@ TEST(PdfFile, views_agree_on_an_image_name) {

// path -> the bytes served under it, across every view
std::map<std::string, std::string> bytes_by_path;
for (const std::string &view :
{"document.html", "page0.html", "page1.html"}) {
for (const auto *const view : {"document.html", "page0.html", "page1.html"}) {
std::ostringstream out;
for (const auto &[resource, location] : service.write_html(view, out)) {
if (resource.type() != HtmlResourceType::image) {
Expand All @@ -464,3 +463,26 @@ TEST(PdfFile, views_agree_on_an_image_name) {

EXPECT_EQ(bytes_by_path.size(), 2);
}

// A `1 Tf` run with its size in `Tm` must not come out at `font-size:1pt`,
// which a browser's minimum font size would clamp up.
TEST(PdfFile, matrix_run_font_size_clears_a_minimum_font_size) {
const std::string html =
render_html(text_mini_pdf("BT /F1 1 Tf 10 0 0 20 50 700 Tm (Hi) Tj ET"),
PdfTextMode::dual_layer);

EXPECT_FALSE(contains(html, "font-size:1pt"));
EXPECT_TRUE(contains(html, "font-size:48pt"));
// 10/48 and 20/48: the size the file asks for survives the blow-up.
EXPECT_TRUE(contains(html, "matrix(0.208333,0,0,0.416667,0,0)"));
}

// The uniform branch states the real size and needs no such treatment.
TEST(PdfFile, uniform_run_keeps_its_own_font_size) {
const std::string html =
render_html(text_mini_pdf("BT /F1 12 Tf 1 0 0 1 72 700 Tm (Hi) Tj ET"),
PdfTextMode::dual_layer);

EXPECT_TRUE(contains(html, "font-size:12pt"));
EXPECT_FALSE(contains(html, "font-size:48pt"));
}
Loading