blob: 2a71aec5042e22d79159f0f8081d816d92302f54 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "pdf/pdf_ink_text.h"
#include <iterator>
#include <string>
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/containers/to_vector.h"
#include "base/notreached.h"
namespace chrome_pdf {
namespace {
// Makes a substring of `input` containing the characters in [start, end) with
// the `location` rectangle cut so that the returned `location` covers only the
// glyphs in the range.
//
// TODO(crbug.com/510015130): check `is_horizontal`: if false the rectangle
// would need to be split on the y-axis instead of the x-axis.
InkTextInfo MakeSubstrTextInfo(const InkTextInfo& input,
float y_offset,
size_t start,
size_t end) {
CHECK_LT(start, input.glyphs.size());
CHECK_LE(end, input.glyphs.size());
CHECK_LT(start, end);
CHECK_EQ(input.glyphs.size(), input.glyph_positions.size());
const size_t count = end - start;
const float left = input.glyph_positions[start];
const float right = end < input.glyph_positions.size()
? input.glyph_positions[end]
: input.location.width();
std::vector<uint32_t> glyphs =
base::ToVector(base::span(input.glyphs).subspan(start, count));
std::vector<float> glyph_positions =
base::ToVector(base::span(input.glyph_positions).subspan(start, count),
[&left](float pos) { return pos - left; });
gfx::RectF location(/*x=*/input.location.x() + left,
/*y=*/input.location.y() + y_offset,
/*width=*/right - left,
/*height=*/input.location.height());
return InkTextInfo(input.font_id, std::move(glyphs),
std::move(glyph_positions), location, input.is_horizontal);
}
// Because PDF text objects only support 1D glyph positioning, it is necessary
// to split runs of text that have 2D glyph offsets from Harfbuzz such that each
// PDF text object contains text all on the same y-axis position.
//
// So, take in an `input` InkTextInfo, which represents a run of text all on one
// line with the same typeface, plus the y-axis `offsets` for each glyph in
// that run. Then split the `input` into multiple InkTextInfo objects based on
// the y-axis position and apply the relevant `location` rectangle adjustments
// to incorporate the y-axis offsets.
//
// It's not necessary to call this if `offsets` is all zero, it will just push a
// copy of `input` in that case.
//
// TODO(crbug.com/510015130): check `is_horizontal`: if false `offsets` would be
// interpreted as x-axis offsets instead of y-axis. The documentation above
// needs to be updated too because the axes will flip.
std::vector<InkTextInfo> Split2DOffsets(const InkTextInfo& input,
const std::vector<float>& offsets) {
CHECK(!offsets.empty());
CHECK_EQ(offsets.size(), input.glyphs.size());
CHECK_EQ(offsets.size(), input.glyph_positions.size());
std::vector<InkTextInfo> results;
size_t run_start = 0;
for (size_t i = 1; i <= offsets.size(); ++i) {
bool is_boundary = i == offsets.size() || offsets[i - 1] != offsets[i];
if (!is_boundary) {
continue;
}
results.push_back(MakeSubstrTextInfo(input, offsets[i - 1], run_start, i));
run_start = i;
}
return results;
}
// The PDFium API to set glyph positions requires that the first glyph position
// is always 0, so the first glyph position must be specified entirely in the
// `location` rectangle.
//
// This function normalizes the InkTextInfo to move any non-zero first glyph
// position into the `location` rectangle.
//
// TODO(crbug.com/510015130): check `is_horizontal`: if false `glyph_positions`
// would be interpreted as y-axis offsets instead of x-axis.
void MaybeCorrectNonZeroFirstOffset(InkTextInfo& input) {
CHECK(!input.glyph_positions.empty());
const float first_position = input.glyph_positions.front();
if (first_position == 0.0f) {
return;
}
input.location.set_x(first_position + input.location.x());
for (float& position : input.glyph_positions) {
position -= first_position;
}
// Note: The first position is now nearly guaranteed to be 0.0f or -0.0f. It's
// possible if there was a Infinity or NaN to get a different answer but now
// it's relatively safe to assume the first value is 0 and skip it in
// FPDFText_SetPositions() calls.
}
} // namespace
std::string TextTypefaceToString(TextTypeface typeface) {
switch (typeface) {
case TextTypeface::kSansSerif:
return "sans-serif";
case TextTypeface::kSerif:
return "serif";
case TextTypeface::kMonospace:
return "monospace";
}
NOTREACHED();
}
std::string TextAlignmentToString(TextAlignment alignment) {
switch (alignment) {
case TextAlignment::kLeft:
return "left";
case TextAlignment::kCenter:
return "center";
case TextAlignment::kRight:
return "right";
}
NOTREACHED();
}
InkTextBoxAttributes::InkTextBoxAttributes(gfx::RectF rect,
SkColor color,
float css_font_size,
TextTypeface typeface,
TextAlignment alignment,
int orientation,
bool is_bold,
bool is_italic,
const std::string& text)
: rect(rect),
color(color),
css_font_size(css_font_size),
typeface(typeface),
alignment(alignment),
orientation(orientation),
is_bold(is_bold),
is_italic(is_italic),
text(text) {}
InkTextBoxAttributes::InkTextBoxAttributes(InkTextBoxAttributes&&) noexcept =
default;
InkTextBoxAttributes& InkTextBoxAttributes::operator=(
InkTextBoxAttributes&&) noexcept = default;
InkTextBoxAttributes::~InkTextBoxAttributes() = default;
InkTextBox::InkTextBox(int id, InkTextBoxAttributes attributes)
: id(id), attributes(std::move(attributes)) {}
InkTextBox::InkTextBox(InkTextBox&&) noexcept = default;
InkTextBox& InkTextBox::operator=(InkTextBox&&) noexcept = default;
InkTextBox::~InkTextBox() = default;
InkTextInfo::InkTextInfo(FontId font_id,
std::vector<uint32_t> glyphs,
std::vector<float> glyph_positions,
gfx::RectF location,
bool is_horizontal)
: font_id(font_id),
glyphs(glyphs),
glyph_positions(glyph_positions),
location(location),
is_horizontal(is_horizontal) {}
InkTextInfo::InkTextInfo(InkTextInfo&&) noexcept = default;
InkTextInfo& InkTextInfo::operator=(InkTextInfo&&) noexcept = default;
InkTextInfo::~InkTextInfo() = default;
std::vector<InkTextInfo> InkTextInfo::SplitTypefaceRuns(
const std::vector<pdf::mojom::InkTextRunPtr>& text_runs,
float effective_zoom) {
std::vector<InkTextInfo> results;
for (const pdf::mojom::InkTextRunPtr& text_run : text_runs) {
float left_edge = text_run->location.x();
float prev_right_edge_advance = 0;
for (size_t i = 0; i < text_run->typeface_runs.size(); ++i) {
const pdf::mojom::InkTypefaceRunPtr& typeface_run =
text_run->typeface_runs[i];
// TODO(crbug.com/510015130): handle vertical text.
CHECK(typeface_run->is_horizontal);
const float right_edge_advance =
i + 1 < text_run->typeface_runs.size() &&
!text_run->typeface_runs[i + 1]->glyphs.empty()
? text_run->typeface_runs[i + 1]->glyphs.front()->total_advance
: text_run->location.width();
const float right_edge = right_edge_advance + text_run->location.x();
gfx::RectF run_location(left_edge, text_run->location.y(),
right_edge - left_edge,
text_run->location.height());
run_location.Scale(1.0 / effective_zoom);
left_edge = right_edge;
std::vector<uint32_t> glyphs;
// This is the total_advance + writing direction axis offsets
std::vector<float> glyph_positions;
// This is the offsets for the non-writing direction axis.
std::vector<float> glyph_offsets;
const size_t num_glyphs = typeface_run->glyphs.size();
glyphs.reserve(num_glyphs);
glyph_positions.reserve(num_glyphs);
glyph_offsets.reserve(num_glyphs);
for (const pdf::mojom::InkGlyphInfoPtr& glyph_info :
typeface_run->glyphs) {
gfx::Vector2dF position(glyph_info->offset.x() +
glyph_info->total_advance -
prev_right_edge_advance,
glyph_info->offset.y());
position.Scale(1.0 / effective_zoom);
glyph_positions.push_back(position.x());
glyph_offsets.push_back(position.y());
glyphs.push_back(glyph_info->glyph);
}
prev_right_edge_advance = right_edge_advance;
CHECK_EQ(glyphs.size(), glyph_positions.size());
if (glyphs.empty()) {
continue;
}
InkTextInfo output_info(FontId(typeface_run->typeface_id),
std::move(glyphs), std::move(glyph_positions),
run_location, typeface_run->is_horizontal);
MaybeCorrectNonZeroFirstOffset(output_info);
const bool all_zero =
std::ranges::all_of(glyph_offsets, [](float v) { return v == 0; });
if (all_zero) {
results.push_back(std::move(output_info));
} else {
std::vector<InkTextInfo> split_infos =
Split2DOffsets(output_info, glyph_offsets);
results.insert(results.end(),
std::make_move_iterator(split_infos.begin()),
std::make_move_iterator(split_infos.end()));
}
}
}
return results;
}
} // namespace chrome_pdf