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
74 changes: 56 additions & 18 deletions core/fpdftext/cpdf_textpage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,48 @@ float GetFontSize(const CPDF_TextObject* text_object) {
return has_font ? text_object->GetFontSize() : kDefaultFontSize;
}

// EmbedPDF(bug-fix): the top and bottom of a character's loose box, in glyph
// units from its origin: the font's ascent and descent, bounded by /FontBBox
// (upstream ff5e04acc, 2558d5d97). A Type3 font takes /FontBBox itself, as the
// loose box did before ff5e04acc. A Type3 font declares no ascent or descent;
// CPDF_Font::CheckFontMetrics() takes the glyphs of char codes 65 and 103 ('A'
// and 'g'), but a Type3 font's codes are its own (Chrome and Skia number their
// glyphs), so those are any two glyphs, and the boxes of one line get uneven
// heights. /FontBBox may be given in either corner order.
std::pair<int, int> LooseAscentDescent(const CPDF_Font& font) {
const FX_RECT& font_bbox = font.GetFontBBox();
if (font.IsType3Font() && font_bbox.top != font_bbox.bottom) {
return {std::max(font_bbox.top, font_bbox.bottom),
std::min(font_bbox.top, font_bbox.bottom)};
}
int ascent = font.GetTypeAscent();
int descent = font.GetTypeDescent();
if (font_bbox.top > font_bbox.bottom) {
ascent = std::min(ascent, font_bbox.top);
descent = std::max(descent, font_bbox.bottom);
}
return {ascent, descent};
}

// EmbedPDF(bug-fix): whether `text_obj` draws only spaces. A space has no ink,
// so its object has no width, but it separates words: a producer that draws
// each character as its own object (Chrome and Skia do) draws every space that
// way. Two spaces in a row collapse in CloseTempLine().
bool DrawsOnlySpaces(const CPDF_TextObject* text_obj) {
RetainPtr<CPDF_Font> font = text_obj->GetFont();
if (!font || text_obj->CharCount() == 0) {
return false;
}
for (size_t i = 0; i < text_obj->CharCount(); ++i) {
const uint32_t char_code = text_obj->GetCharCode(i);
if (char_code == CPDF_Font::kInvalidCharCode ||
font->UnicodeFromCharCode(char_code) != L" ") {
return false;
}
}
return true;
}

// EmbedPDF(text-geometry): a character's loose and tight boxes in its text
// object's space. The tight box is upstream's char box, moved here from
// ProcessTextObjectItems() so the text page and EPDFText_GetCharGeometry
Expand Down Expand Up @@ -321,15 +363,8 @@ std::optional<CPDF_TextPage::CharLocalGeometry> GetCharLocalBoxes(
local_origin.x + offset_x, local_origin.y + offset_y + height,
local_origin.x + offset_x + font_size, local_origin.y + offset_y);
} else {
// GetLooseBounds()'s ascent and descent from the glyph's origin, bounded
// by /FontBBox (upstream ff5e04acc, 2558d5d97).
int ascent = font->GetTypeAscent();
int descent = font->GetTypeDescent();
const FX_RECT& font_bbox = font->GetFontBBox();
if (font_bbox.top > font_bbox.bottom) {
ascent = std::min(ascent, font_bbox.top);
descent = std::max(descent, font_bbox.bottom);
}
// GetLooseBounds()'s ascent and descent from the glyph's origin.
const auto [ascent, descent] = LooseAscentDescent(*font);
if (ascent != descent) {
const float width = text_object->GetCharWidth(char_code);
loose_box = CFX_FloatRect(
Expand Down Expand Up @@ -373,13 +408,8 @@ CFX_FloatRect GetLooseBounds(const CPDF_TextPage::CharInfo& charinfo) {
return char_box;
}

int ascent = font->GetTypeAscent();
int descent = font->GetTypeDescent();
const FX_RECT& font_bbox = font->GetFontBBox();
if (font_bbox.top > font_bbox.bottom) {
ascent = std::min(ascent, font_bbox.top);
descent = std::max(descent, font_bbox.bottom);
}
// EmbedPDF(bug-fix): /FontBBox for a Type3 font.
const auto [ascent, descent] = LooseAscentDescent(*font);
if (ascent != descent) {
// Compute `left` and `right` based on the individual character's `width`.
float width = text_object->GetCharWidth(charinfo.char_code());
Expand Down Expand Up @@ -1022,7 +1052,11 @@ void CPDF_TextPage::ProcessTextObject(
const CFX_Matrix& form_matrix,
const CPDF_PageObjectHolder* obj_list,
CPDF_PageObjectHolder::const_iterator obj_iter) {
if (fabs(text_obj->GetRect().Width()) < kSizeEpsilon) {
// EmbedPDF(bug-fix): a space-only object joins its line though it has no
// width, which upstream 49042621c drops it for. Where it doesn't separate
// text, ProcessTransformedTextObjects() drops it after all.
if (fabs(text_obj->GetRect().Width()) < kSizeEpsilon &&
!DrawsOnlySpaces(text_obj)) {
return;
}

Expand Down Expand Up @@ -1223,7 +1257,11 @@ void CPDF_TextPage::ReverseTempTextBufs(size_t char_list_index,
void CPDF_TextPage::ProcessTransformedTextObjects() {
for (const TransformedTextObject& obj : text_objects_) {
CPDF_TextObject* const text_obj = obj.text_obj_;
if (fabs(text_obj->GetRect().Width()) < kSizeEpsilon) {
// EmbedPDF(bug-fix): a space-only object stays where it separates text
// on its line; at either end of the line it goes, as upstream drops it.
if (fabs(text_obj->GetRect().Width()) < kSizeEpsilon &&
(!DrawsOnlySpaces(text_obj) || &obj == &text_objects_.front() ||
&obj == &text_objects_.back())) {
continue;
}

Expand Down
53 changes: 52 additions & 1 deletion fpdfsdk/epdf_text_embeddertest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ TEST_F(EPDFTextEmbedderTest, GetCharGeometry) {

// A text object with no width is left out of the text page (upstream
// 49042621c, Feb 2026), so a zero font size gives no characters to measure.
// An empty character box is covered by FPDFTextEmbedderTest.Bug399689604.
// Only a space between words stays (ChromeType3TextKeepsSpaces). An empty
// character box is covered by FPDFTextEmbedderTest.Bug399689604.
TEST_F(EPDFTextEmbedderTest, ZeroFontSizeTextHasNoChars) {
ASSERT_TRUE(OpenDocument("font_size_zero.pdf"));
ScopedPage page = LoadScopedPage(0);
Expand All @@ -110,6 +111,56 @@ TEST_F(EPDFTextEmbedderTest, ZeroFontSizeTextHasNoChars) {
EXPECT_EQ(0, FPDFText_CountChars(text_page.get()));
}

// Chrome and Skia print each glyph as its own text object, so a space is an
// object without ink, and so without width. It still separates the words.
TEST_F(EPDFTextEmbedderTest, ChromeType3TextKeepsSpaces) {
ASSERT_TRUE(OpenDocument("embedpdf_chrome_type3_text.pdf"));
ScopedPage page = LoadScopedPage(0);
ASSERT_TRUE(page);

ScopedFPDFTextPage text_page(FPDFText_LoadPage(page.get()));
ASSERT_TRUE(text_page);

static constexpr char kExpected[] = "to pixels";
static constexpr int kExpectedSize = std::size(kExpected);
ASSERT_EQ(kExpectedSize - 1, FPDFText_CountChars(text_page.get()));
unsigned short buffer[16] = {};
ASSERT_EQ(kExpectedSize,
FPDFText_GetText(text_page.get(), 0, std::size(buffer), buffer));
EXPECT_THAT(pdfium::span(buffer).first<kExpectedSize>(),
ElementsAreArray(kExpected));
}

// A Type3 font has no ascent or descent, so its characters' loose boxes take
// the top and bottom of /FontBBox: one line, one height, whatever each glyph's
// ink. The space between the words has the same box.
TEST_F(EPDFTextEmbedderTest, ChromeType3TextLooseBoxesShareTheLine) {
ASSERT_TRUE(OpenDocument("embedpdf_chrome_type3_text.pdf"));
ScopedPage page = LoadScopedPage(0);
ASSERT_TRUE(page);

ScopedFPDFTextPage text_page(FPDFText_LoadPage(page.get()));
ASSERT_TRUE(text_page);

// 20pt text on a baseline at y = 50; /FontBBox runs from 200 below the
// baseline to 750 above it, in thousandths of the font size.
static constexpr float kTop = 65.0f;
static constexpr float kBottom = 46.0f;
const int char_count = FPDFText_CountChars(text_page.get());
ASSERT_GT(char_count, 0);
for (int i = 0; i < char_count; ++i) {
FS_RECTF loose;
ASSERT_TRUE(FPDFText_GetLooseCharBox(text_page.get(), i, &loose));
EXPECT_FLOAT_EQ(kTop, loose.top) << i;
EXPECT_FLOAT_EQ(kBottom, loose.bottom) << i;

EPDF_CHAR_GEOMETRY geometry = {};
ASSERT_TRUE(EPDFText_GetCharGeometry(text_page.get(), i, &geometry));
EXPECT_FLOAT_EQ(kTop, geometry.loose_box.top) << i;
EXPECT_FLOAT_EQ(kBottom, geometry.loose_box.bottom) << i;
}
}

TEST_F(EPDFTextEmbedderTest, GetRotatedCharGeometry) {
ASSERT_TRUE(OpenDocument("rotated_text.pdf"));
ScopedPage page = LoadScopedPage(0);
Expand Down
10 changes: 5 additions & 5 deletions fpdfsdk/fpdf_text_embeddertest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2464,15 +2464,15 @@ TEST_F(FPDFTextEmbedderTest, Bug444176962) {
ScopedFPDFTextPage textpage(FPDFText_LoadPage(page.get()));
ASSERT_TRUE(textpage);

// TODO(crbug.com/444176962): FPDFText_GetText() should return 10.
// kMissingSpaceResult should be replaced with kResult as "local act"
// EmbedPDF(bug-fix): the space drawn as its own object between the words
// stays (crbug.com/444176962), so FPDFText_GetText() returns 10.
unsigned short buffer[128] = {};
static constexpr char kMissingSpaceResult[] = "localact";
static constexpr int kExpectedSize = std::size(kMissingSpaceResult);
static constexpr char kResult[] = "local act";
static constexpr int kExpectedSize = std::size(kResult);
ASSERT_EQ(kExpectedSize,
FPDFText_GetText(textpage.get(), 0, std::size(buffer), buffer));
EXPECT_THAT(pdfium::span(buffer).first<kExpectedSize>(),
ElementsAreArray(kMissingSpaceResult));
ElementsAreArray(kResult));
}

TEST_F(FPDFTextEmbedderTest, Bug491161396) {
Expand Down
Loading