feat: implemented text extraction with bounding boxes

This commit is contained in:
Furqan-14
2026-05-25 14:47:38 +05:30
parent 5ce96ebd5d
commit f961c579bc
8 changed files with 165 additions and 3 deletions
+11
View File
@@ -44,6 +44,15 @@ struct DevicePoint {
int y;
};
struct GlyphBounds {
std::string text;
double x;
double y;
double w;
double h;
double fontSize;
};
class PdfPage {
public:
virtual ~PdfPage() = default;
@@ -55,6 +64,8 @@ public:
[[nodiscard]] virtual std::expected<std::string, EngineError> extractText() const = 0;
[[nodiscard]] virtual std::expected<std::vector<GlyphBounds>, EngineError> extractTextWithBounds() const = 0;
[[nodiscard]] virtual DevicePoint pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept = 0;
[[nodiscard]] virtual Point2D deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept = 0;
};
+97
View File
@@ -33,6 +33,28 @@ std::string utf16le_to_utf8(const char16_t* utf16, size_t length) {
return utf8;
}
std::string code_point_to_utf8(unsigned int cp) {
std::string utf8;
if (cp == 0) return "";
if (cp < 0x80) {
utf8 += static_cast<char>(cp);
} else if (cp < 0x800) {
utf8 += static_cast<char>(0xC0 | (cp >> 6));
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
} else if (cp < 0x10000) {
utf8 += static_cast<char>(0xE0 | (cp >> 12));
utf8 += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
} else if (cp < 0x200000) {
utf8 += static_cast<char>(0xF0 | (cp >> 18));
utf8 += static_cast<char>(0x80 | ((cp >> 12) & 0x3F));
utf8 += static_cast<char>(0x80 | ((cp >> 6) & 0x3F));
utf8 += static_cast<char>(0x80 | (cp & 0x3F));
}
return utf8;
}
#ifdef PDFENGINE_WITH_PDFIUM
struct VectorWriter : public FPDF_FILEWRITE {
std::vector<uint8_t> buffer;
@@ -301,6 +323,81 @@ std::expected<std::string, EngineError> PdfiumPage::extractText() const {
#endif
}
std::expected<std::vector<GlyphBounds>, EngineError> PdfiumPage::extractTextWithBounds() const {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) {
return std::unexpected(EngineError::Unknown);
}
ensureTextPageLoaded();
if (!textPage_) {
return std::unexpected(EngineError::Unknown);
}
int charCount = FPDFText_CountChars(textPage_);
std::vector<GlyphBounds> result;
if (charCount <= 0) {
return result;
}
result.reserve(charCount);
for (int i = 0; i < charCount; ++i) {
unsigned int codeUnit = FPDFText_GetUnicode(textPage_, i);
unsigned int cp = codeUnit;
double left = 0, right = 0, bottom = 0, top = 0;
double fontSize = FPDFText_GetFontSize(textPage_, i);
if (codeUnit >= 0xD800 && codeUnit <= 0xDBFF && i + 1 < charCount) {
unsigned int nextUnit = FPDFText_GetUnicode(textPage_, i + 1);
if (nextUnit >= 0xDC00 && nextUnit <= 0xDFFF) {
cp = 0x10000 + ((codeUnit - 0xD800) << 10) + (nextUnit - 0xDC00);
double l1 = 0, r1 = 0, b1 = 0, t1 = 0;
FPDFText_GetCharBox(textPage_, i, &l1, &r1, &b1, &t1);
double l2 = 0, r2 = 0, b2 = 0, t2 = 0;
FPDFText_GetCharBox(textPage_, i + 1, &l2, &r2, &b2, &t2);
left = (std::min)(l1, l2);
right = (std::max)(r1, r2);
bottom = (std::min)(b1, b2);
top = (std::max)(t1, t2);
++i;
} else {
FPDFText_GetCharBox(textPage_, i, &left, &right, &bottom, &top);
}
} else {
FPDFText_GetCharBox(textPage_, i, &left, &right, &bottom, &top);
}
std::string utf8_char = code_point_to_utf8(cp);
if (utf8_char.empty()) {
continue;
}
double x = (std::min)(left, right);
double y = (std::min)(bottom, top);
double w = std::abs(right - left);
double h = std::abs(top - bottom);
GlyphBounds gb;
gb.text = std::move(utf8_char);
gb.x = x;
gb.y = y;
gb.w = w;
gb.h = h;
gb.fontSize = fontSize;
result.push_back(gb);
}
return result;
#else
return std::unexpected(EngineError::Unknown);
#endif
}
DevicePoint PdfiumPage::pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept {
#ifdef PDFENGINE_WITH_PDFIUM
if (!page_) return {0, 0};
+1
View File
@@ -39,6 +39,7 @@ public:
std::expected<PageImage, EngineError> render(int dpi = 96) const override;
std::expected<std::string, EngineError> extractText() const override;
std::expected<std::vector<GlyphBounds>, EngineError> extractTextWithBounds() const override;
DevicePoint pageToDevice(const Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
Point2D deviceToPage(const DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override;
+31
View File
@@ -343,4 +343,35 @@ TEST(TextExtractionTest, ExtractUtf8ExtendedText) {
EXPECT_FALSE(text.empty());
}
TEST(TextExtractionTest, ExtractTextWithBounds) {
SKIP_IF_NO_PDFIUM();
auto path = getCorpusPath("basic", "hello_world.pdf");
if (!std::filesystem::exists(path)) {
GTEST_SKIP() << "hello_world.pdf not found in corpus.";
}
auto docRes = PdfDocument::loadFromFile(path.string());
ASSERT_TRUE(docRes.has_value());
auto pageRes = (*docRes)->getPage(0);
ASSERT_TRUE(pageRes.has_value());
auto boundsRes = (*pageRes)->extractTextWithBounds();
ASSERT_TRUE(boundsRes.has_value());
const auto& glyphs = *boundsRes;
ASSERT_FALSE(glyphs.empty());
bool found_hello = false;
for (const auto& glyph : glyphs) {
EXPECT_FALSE(glyph.text.empty());
EXPECT_GT(glyph.w, 0.0);
EXPECT_GT(glyph.h, 0.0);
EXPECT_GT(glyph.fontSize, 0.0);
if (glyph.text == "H" || glyph.text == "e" || glyph.text == "l" || glyph.text == "o") {
found_hello = true;
}
}
EXPECT_TRUE(found_hello);
}
}