blob: 0b849ac8bdf34b3848961172dbee704275032bbe [file]
// Copyright 2026 The PDFium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "core/fpdfdoc/cpvt_section.h"
#include <memory>
#include "core/fpdfapi/font/cpdf_font.h"
#include "core/fpdfapi/page/cpdf_pagemodule.h"
#include "core/fpdfapi/parser/cpdf_test_document.h"
#include "core/fpdfdoc/cpvt_fontmap.h"
#include "core/fpdfdoc/cpvt_stub_provider.h"
#include "core/fpdfdoc/cpvt_variabletext.h"
#include "core/fxcrt/cfx_bidi_resolver.h"
#include "core/fxcrt/fx_codepage.h"
#include "core/fxcrt/notreached.h"
#include "core/fxcrt/retain_ptr.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace {
constexpr uint16_t kHebrewAlef = 0x05D0;
constexpr uint16_t kHebrewBet = 0x05D1;
constexpr uint16_t kHebrewGimel = 0x05D2;
} // namespace
class CPVT_SectionTest : public testing::Test {
public:
void SetUp() override {
pdfium::InitializePageModule();
test_doc_ = std::make_unique<CPDF_TestDocument>();
font_ = CPDF_Font::GetStockFont(test_doc_.get(), "Helvetica");
font_map_ = std::make_unique<CPVT_FontMap>(test_doc_.get(), nullptr, font_,
"Helvetica");
provider_ = std::make_unique<CPVT_StubProvider>(font_map_.get());
}
void TearDown() override {
provider_.reset();
font_map_.reset();
font_.Reset();
test_doc_.reset();
pdfium::DestroyPageModule();
}
protected:
void PopulateSectionWithText(CPVT_Section& section,
const std::vector<uint32_t>& text) {
section.SetPlace(CPVT_WordPlace(0, 0, -1));
for (size_t i = 0; i < text.size(); ++i) {
section.AddWord(CPVT_WordPlace(0, 0, i),
CPVT_WordInfo(text[i], FX_Charset::kDefault, 0));
}
}
void PopulateSectionWithEnglishAndHebrew(CPVT_Section& section) {
// "A B C " (3 English words + spaces) + "H1 H2" (Alef + space + Bet)
PopulateSectionWithText(
section, {'A', ' ', 'B', ' ', 'C', ' ', kHebrewAlef, ' ', kHebrewBet});
}
void PopulateSectionWithHebrewAndEnglish(CPVT_Section& section) {
// "H1 H2 H3 " (3 Hebrew words + spaces) + "A B" (2 English words)
PopulateSectionWithText(section, {kHebrewAlef, ' ', kHebrewBet, ' ',
kHebrewGimel, ' ', 'A', ' ', 'B'});
}
void AssertEnglishAndHebrewLayout(const CPVT_Section& section,
CFX_BidiResolver::ParagraphDirection dir) {
std::vector<int> expected_visual_order;
if (dir == CFX_BidiResolver::ParagraphDirection::kAuto ||
dir == CFX_BidiResolver::ParagraphDirection::kLeftToRight) {
// `kAuto` uses the Unicode Bidirectional Algorithm, which resolves to LTR
// because the first strong character is English.
// The RTL run [w4, sp4, w5] (indices 6, 7, 8) has its words ordered from
// right to left.
expected_visual_order = {0, 1, 2, 3, 4, 5, 8, 7, 6};
} else {
expected_visual_order = {8, 7, 6, 5, 0, 1, 2, 3, 4};
}
ASSERT_EQ(static_cast<int>(expected_visual_order.size()),
section.GetWordArraySize());
for (size_t i = 0; i < expected_visual_order.size() - 1; ++i) {
EXPECT_LT(section.GetWordFromArray(expected_visual_order[i])->fWordX,
section.GetWordFromArray(expected_visual_order[i + 1])->fWordX);
}
}
void AssertHebrewAndEnglishLayout(const CPVT_Section& section,
CFX_BidiResolver::ParagraphDirection dir) {
std::vector<int> expected_visual_order;
if (dir == CFX_BidiResolver::ParagraphDirection::kLeftToRight) {
expected_visual_order = {4, 3, 2, 1, 0, 5, 6, 7, 8};
} else if (dir == CFX_BidiResolver::ParagraphDirection::kAuto ||
dir == CFX_BidiResolver::ParagraphDirection::kRightToLeft) {
// `kAuto` uses the Unicode Bidirectional Algorithm, which resolves to RTL
// because the first strong character is Hebrew.
expected_visual_order = {6, 7, 8, 5, 4, 3, 2, 1, 0};
}
ASSERT_EQ(static_cast<int>(expected_visual_order.size()),
section.GetWordArraySize());
for (size_t i = 0; i < expected_visual_order.size() - 1; ++i) {
EXPECT_LT(section.GetWordFromArray(expected_visual_order[i])->fWordX,
section.GetWordFromArray(expected_visual_order[i + 1])->fWordX);
}
}
void SetVariableTextDefaults(CPVT_VariableText& vt) {
vt.SetFontSize(10.0f);
vt.SetPlateRect(CFX_FloatRect(0, 0, 1000, 1000));
}
enum class TextContent { kEnglishAndHebrew, kHebrewAndEnglish };
void TestTextLayout(TextContent content,
CFX_BidiResolver::ParagraphDirection direction) {
CPVT_VariableText vt(provider_.get());
SetVariableTextDefaults(vt);
vt.SetTextDirection(direction);
vt.Initialize();
CPVT_Section section(&vt);
switch (content) {
case TextContent::kEnglishAndHebrew:
PopulateSectionWithEnglishAndHebrew(section);
section.Rearrange();
AssertEnglishAndHebrewLayout(section, direction);
return;
case TextContent::kHebrewAndEnglish:
PopulateSectionWithHebrewAndEnglish(section);
section.Rearrange();
AssertHebrewAndEnglishLayout(section, direction);
return;
}
NOTREACHED();
}
void PopulateSectionWithHello(CPVT_Section& section) {
section.SetPlace(CPVT_WordPlace(0, 0, -1));
section.AddWord(CPVT_WordPlace(0, 0, 0),
CPVT_WordInfo('h', FX_Charset::kDefault, 0));
section.AddWord(CPVT_WordPlace(0, 0, 1),
CPVT_WordInfo('e', FX_Charset::kDefault, 0));
section.AddWord(CPVT_WordPlace(0, 0, 2),
CPVT_WordInfo('l', FX_Charset::kDefault, 0));
section.AddWord(CPVT_WordPlace(0, 0, 3),
CPVT_WordInfo('l', FX_Charset::kDefault, 0));
section.AddWord(CPVT_WordPlace(0, 0, 4),
CPVT_WordInfo('o', FX_Charset::kDefault, 0));
CPVT_LineInfo lineinfo;
lineinfo.nBeginWordIndex = 0;
lineinfo.nEndWordIndex = 4;
section.AddLine(lineinfo);
section.ResetLinePlace();
}
std::unique_ptr<CPDF_TestDocument> test_doc_;
RetainPtr<CPDF_Font> font_;
std::unique_ptr<CPVT_FontMap> font_map_;
std::unique_ptr<CPVT_StubProvider> provider_;
};
TEST_F(CPVT_SectionTest, ClearLeftWords) {
CPVT_VariableText vt(provider_.get());
vt.Initialize();
CPVT_Section section(&vt);
PopulateSectionWithHello(section);
// Clear "hel" [0, 2] by passing in a placeholder for the previous section.
section.ClearWords(
CPVT_WordRange(CPVT_WordPlace(-1, 0, -1), CPVT_WordPlace(0, 0, 2)));
ASSERT_EQ(2, section.GetWordArraySize());
const CPVT_WordInfo* word0 = section.GetWordFromArray(0);
ASSERT_TRUE(word0);
EXPECT_EQ('l', word0->Word);
const CPVT_WordInfo* word1 = section.GetWordFromArray(1);
ASSERT_TRUE(word1);
EXPECT_EQ('o', word1->Word);
}
TEST_F(CPVT_SectionTest, ClearRightWords) {
CPVT_VariableText vt(provider_.get());
vt.Initialize();
CPVT_Section section(&vt);
PopulateSectionWithHello(section);
// Clear "llo" [2, 4] by passing in a placeholder for the next section.
section.ClearWords(
CPVT_WordRange(CPVT_WordPlace(0, 0, 1), CPVT_WordPlace(1, 0, -1)));
ASSERT_EQ(2, section.GetWordArraySize());
const CPVT_WordInfo* word0 = section.GetWordFromArray(0);
ASSERT_TRUE(word0);
EXPECT_EQ('h', word0->Word);
const CPVT_WordInfo* word1 = section.GetWordFromArray(1);
ASSERT_TRUE(word1);
EXPECT_EQ('e', word1->Word);
}
TEST_F(CPVT_SectionTest, ClearMidWords) {
CPVT_VariableText vt(provider_.get());
vt.Initialize();
CPVT_Section section(&vt);
PopulateSectionWithHello(section);
// Clear "ell" [1, 3] by passing in the word range (0, 3].
section.ClearWords(
CPVT_WordRange(CPVT_WordPlace(0, 0, 0), CPVT_WordPlace(0, 0, 3)));
ASSERT_EQ(2, section.GetWordArraySize());
const CPVT_WordInfo* word0 = section.GetWordFromArray(0);
ASSERT_TRUE(word0);
EXPECT_EQ('h', word0->Word);
const CPVT_WordInfo* word1 = section.GetWordFromArray(1);
ASSERT_TRUE(word1);
EXPECT_EQ('o', word1->Word);
}
TEST_F(CPVT_SectionTest, ClearAllWords) {
CPVT_VariableText vt(provider_.get());
vt.Initialize();
CPVT_Section section(&vt);
PopulateSectionWithHello(section);
// Clear all words by passing in placeholders for the previous and next
// sections.
section.ClearWords(
CPVT_WordRange(CPVT_WordPlace(-1, 0, -1), CPVT_WordPlace(1, 0, -1)));
EXPECT_EQ(0, section.GetWordArraySize());
}
TEST_F(CPVT_SectionTest, OutputLines_EnglishAndHebrew_AutoDirection) {
TestTextLayout(TextContent::kEnglishAndHebrew,
CFX_BidiResolver::ParagraphDirection::kAuto);
}
TEST_F(CPVT_SectionTest, OutputLines_EnglishAndHebrew_ExplicitLeftToRight) {
TestTextLayout(TextContent::kEnglishAndHebrew,
CFX_BidiResolver::ParagraphDirection::kLeftToRight);
}
TEST_F(CPVT_SectionTest, OutputLines_EnglishAndHebrew_ExplicitRightToLeft) {
TestTextLayout(TextContent::kEnglishAndHebrew,
CFX_BidiResolver::ParagraphDirection::kRightToLeft);
}
TEST_F(CPVT_SectionTest, OutputLines_HebrewAndEnglish_AutoDirection) {
TestTextLayout(TextContent::kHebrewAndEnglish,
CFX_BidiResolver::ParagraphDirection::kAuto);
}
TEST_F(CPVT_SectionTest, OutputLines_HebrewAndEnglish_ExplicitLeftToRight) {
TestTextLayout(TextContent::kHebrewAndEnglish,
CFX_BidiResolver::ParagraphDirection::kLeftToRight);
}
TEST_F(CPVT_SectionTest, OutputLines_HebrewAndEnglish_ExplicitRightToLeft) {
TestTextLayout(TextContent::kHebrewAndEnglish,
CFX_BidiResolver::ParagraphDirection::kRightToLeft);
}
TEST_F(CPVT_SectionTest, OutputLines_Multiline_EnglishAndHebrew) {
CPVT_VariableText vt(provider_.get());
vt.SetFontSize(10.0f);
// Narrow width to force word wrap (stub font chars are 0.1 wide)
vt.SetPlateRect(CFX_FloatRect(0, 0, 0.45f, 1000));
vt.SetAutoReturn(true);
vt.Initialize();
CPVT_Section section(&vt);
PopulateSectionWithEnglishAndHebrew(section);
section.Rearrange();
// With SetAutoReturn(true) and a constrained plate width, the text wraps
// into 3 lines:
// Line 1: "A B "
// Line 2: "C H1 "
// Line 3: "H2"
// The Unicode Bidirectional Algorithm resolves the paragraph direction to LTR
// (based on the first strong character 'A').
// When resolving visual runs for physical line 2, the algorithm maintains
// the paragraph's overall LTR context. The spaces are resolved as LTR, and
// the X coordinates correctly progress LTR (X increases).
// Line 1: LTR
for (int i = 0; i < 3; ++i) {
EXPECT_LT(section.GetWordFromArray(i)->fWordX,
section.GetWordFromArray(i + 1)->fWordX);
}
// Line 2: Correctly resolved as LTR using paragraph context
for (int i = 4; i < 7; ++i) {
EXPECT_LT(section.GetWordFromArray(i)->fWordX,
section.GetWordFromArray(i + 1)->fWordX);
}
}