feat: implemented text extraction with bounding boxes
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user