update the pipiline
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include "fonts/cache/glyph_cache.hpp"
|
||||
#include "fonts/pdf_fonts/font.hpp"
|
||||
#ifndef TEST_CORPUS_DIR
|
||||
#define TEST_CORPUS_DIR "../../corpus"
|
||||
#endif
|
||||
@@ -1336,4 +1337,510 @@ TEST(GlyphCacheTest, ConcurrencyBench) {
|
||||
EXPECT_LE(cache.size(), 1000 + 16); // Accommodate shard capacity rounding
|
||||
}
|
||||
|
||||
TEST(FontDiagnosticsTest, EmbeddedFontResolutionAndReloadingVerification) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
|
||||
std::vector<std::string> testFiles = {
|
||||
"text_font.pdf",
|
||||
"embedded_truetype.pdf",
|
||||
"embedded_cid_font.pdf",
|
||||
"subset_font.pdf",
|
||||
"latin_extended.pdf"
|
||||
};
|
||||
|
||||
bool foundAnyEmbedded = false;
|
||||
for (const auto& fileName : testFiles) {
|
||||
auto path = getCorpusPath("fonts", fileName);
|
||||
if (!std::filesystem::exists(path)) {
|
||||
continue;
|
||||
}
|
||||
std::cout << "\n========================================\n";
|
||||
std::cout << "Testing PDF: " << fileName << "\n";
|
||||
std::cout << "========================================\n";
|
||||
|
||||
auto docRes = PdfDocument::loadFromFile(path.string());
|
||||
if (!docRes.has_value()) {
|
||||
std::cout << "Failed to load document: " << fileName << std::endl;
|
||||
continue;
|
||||
}
|
||||
auto doc = *docRes;
|
||||
|
||||
auto fontsRes = doc->getFonts();
|
||||
if (!fontsRes.has_value()) {
|
||||
std::cout << "Failed to get fonts for: " << fileName << std::endl;
|
||||
continue;
|
||||
}
|
||||
const auto& fonts = *fontsRes;
|
||||
|
||||
for (const auto& fontInfo : fonts) {
|
||||
std::cout << "Font: " << fontInfo.fontName
|
||||
<< ", type: " << fontInfo.type
|
||||
<< ", isEmbedded: " << (fontInfo.isEmbedded ? "yes" : "no")
|
||||
<< ", flags: " << fontInfo.flags << std::endl;
|
||||
if (fontInfo.isEmbedded) {
|
||||
foundAnyEmbedded = true;
|
||||
|
||||
auto resolvedFontRes = doc->getResolvedFont(fontInfo);
|
||||
if (!resolvedFontRes.has_value()) {
|
||||
std::cout << " Failed to resolve font: " << resolvedFontRes.error() << std::endl;
|
||||
continue;
|
||||
}
|
||||
auto resolvedFont = *resolvedFontRes;
|
||||
std::cout << " Resolved font successfully." << std::endl;
|
||||
|
||||
auto face = static_cast<FT_Face>(resolvedFont->getFontFace().getFace());
|
||||
if (face) {
|
||||
std::cout << " FreeType Face Num Glyphs: " << face->num_glyphs << std::endl;
|
||||
std::cout << " FreeType Charmaps Count: " << face->num_charmaps << std::endl;
|
||||
for (int i = 0; i < face->num_charmaps; ++i) {
|
||||
FT_CharMap cm = face->charmaps[i];
|
||||
std::cout << " Charmap " << i << ": platform_id=" << cm->platform_id
|
||||
<< ", encoding_id=" << cm->encoding_id << std::endl;
|
||||
|
||||
FT_Error err = FT_Set_Charmap(face, cm);
|
||||
if (err) {
|
||||
std::cout << " FT_Set_Charmap failed: " << err << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
FT_UInt gindex;
|
||||
FT_ULong charcode = FT_Get_First_Char(face, &gindex);
|
||||
std::cout << " Mapped characters under charmap " << i << ": ";
|
||||
int count = 0;
|
||||
while (gindex != 0 && count < 10) {
|
||||
std::cout << charcode << "->" << gindex << " ";
|
||||
charcode = FT_Get_Next_Char(face, charcode, &gindex);
|
||||
count++;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
// Restore first charmap
|
||||
if (face->num_charmaps > 0) {
|
||||
FT_Set_Charmap(face, face->charmaps[0]);
|
||||
}
|
||||
|
||||
// Print all glyph names in the face
|
||||
std::cout << " Glyph names: ";
|
||||
for (int i = 0; i < face->num_glyphs; ++i) {
|
||||
char nameBuf[64] = {0};
|
||||
if (FT_Get_Glyph_Name(face, i, nameBuf, sizeof(nameBuf)) == 0) {
|
||||
std::cout << i << ":" << nameBuf << " ";
|
||||
} else {
|
||||
std::cout << i << ":[unknown] ";
|
||||
}
|
||||
}
|
||||
std::cout << std::endl;
|
||||
} else {
|
||||
std::cout << " No FreeType Face available." << std::endl;
|
||||
}
|
||||
|
||||
EXPECT_TRUE(resolvedFont->isEmbedded());
|
||||
|
||||
// Let's test a few common characters: 'A' (65), 'a' (97), '0' (48), ' ' (32)
|
||||
std::vector<uint32_t> testChars = {32, 48, 65, 97};
|
||||
for (uint32_t cp : testChars) {
|
||||
bool hasG = resolvedFont->hasGlyph(cp);
|
||||
double w = resolvedFont->getAdvanceWidth(cp, 12.0);
|
||||
std::cout << " char(" << cp << "): hasGlyph=" << (hasG ? "yes" : "no")
|
||||
<< ", advanceWidth=" << w << std::endl;
|
||||
}
|
||||
|
||||
// Verify metrics returned are non-zero/valid
|
||||
auto metrics = resolvedFont->getMetrics(12.0);
|
||||
std::cout << " Metrics: ascent=" << metrics.ascent << ", descent=" << metrics.descent << ", capHeight=" << metrics.capHeight << std::endl;
|
||||
EXPECT_NE(metrics.ascent, 0.0);
|
||||
EXPECT_NE(metrics.descent, 0.0);
|
||||
EXPECT_NE(metrics.capHeight, 0.0);
|
||||
|
||||
// Specific verification for text_font.pdf where we mapped charcode 1 -> GID 1
|
||||
if (fileName == "text_font.pdf") {
|
||||
// hasGlyph(1) should return true because the charmap maps 1 -> 1
|
||||
EXPECT_TRUE(resolvedFont->hasGlyph(1));
|
||||
double w = resolvedFont->getAdvanceWidth(1, 12.0);
|
||||
EXPECT_GT(w, 0.0);
|
||||
std::cout << " [VERIFIED] text_font.pdf char(1): hasGlyph=yes, advanceWidth=" << w << std::endl;
|
||||
}
|
||||
|
||||
// Verify we can load glyphs directly by glyph index (0 to num_glyphs - 1)
|
||||
if (face->num_glyphs > 1) {
|
||||
bool foundNonZeroWidth = false;
|
||||
for (int gid = 1; gid < face->num_glyphs; ++gid) {
|
||||
FT_Error err = FT_Load_Glyph(face, gid, FT_LOAD_DEFAULT);
|
||||
if (err == 0) {
|
||||
double directWidth = static_cast<double>(face->glyph->advance.x) / 64.0;
|
||||
if (directWidth > 0.0) {
|
||||
foundNonZeroWidth = true;
|
||||
std::cout << " [VERIFIED] Direct glyph " << gid << " load: advanceWidth=" << directWidth << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(foundNonZeroWidth) << "Expected to find at least one glyph with a non-zero advance width";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_TRUE(foundAnyEmbedded) << "Expected to find at least one embedded font in test files";
|
||||
}
|
||||
|
||||
TEST(DocumentEditTest, ReplaceTextMVPStandardFont) {
|
||||
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 doc = *docRes;
|
||||
|
||||
auto pageRes = doc->getPage(0);
|
||||
ASSERT_TRUE(pageRes.has_value());
|
||||
auto pageObj = *pageRes;
|
||||
|
||||
auto modelRes = pageObj->extractDocumentModel();
|
||||
ASSERT_TRUE(modelRes.has_value());
|
||||
const auto& model = *modelRes;
|
||||
|
||||
std::vector<int> objectIndices;
|
||||
for (const auto& p : model.paragraphs) {
|
||||
for (const auto& line : p.lines) {
|
||||
for (const auto& run : line.runs) {
|
||||
if (run.text.find("Hello") != std::string::npos) {
|
||||
objectIndices = run.objectIndices;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!objectIndices.empty()) break;
|
||||
}
|
||||
if (!objectIndices.empty()) break;
|
||||
}
|
||||
|
||||
ASSERT_FALSE(objectIndices.empty()) << "Could not find a text object in hello_world.pdf";
|
||||
|
||||
std::string indicesStr = "";
|
||||
for (size_t i = 0; i < objectIndices.size(); ++i) {
|
||||
indicesStr += std::to_string(objectIndices[i]);
|
||||
if (i + 1 < objectIndices.size()) indicesStr += ",";
|
||||
}
|
||||
|
||||
// Flat format payload
|
||||
std::string editsJson = R"({
|
||||
"version": "1.0",
|
||||
"operations": [
|
||||
{
|
||||
"id": "op_mvp_1",
|
||||
"type": "replace_text",
|
||||
"pageIndex": 0,
|
||||
"objectIndices": [)" + indicesStr + R"(],
|
||||
"text": "Greeting, universe!"
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
auto editRes = doc->applyEdits(editsJson);
|
||||
ASSERT_TRUE(editRes.has_value());
|
||||
|
||||
auto saveRes = doc->saveIncremental();
|
||||
ASSERT_TRUE(saveRes.has_value());
|
||||
const auto& savedBytes = *saveRes;
|
||||
ASSERT_FALSE(savedBytes.empty());
|
||||
|
||||
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
|
||||
ASSERT_TRUE(newDocRes.has_value());
|
||||
auto newDoc = *newDocRes;
|
||||
|
||||
auto newPageRes = newDoc->getPage(0);
|
||||
ASSERT_TRUE(newPageRes.has_value());
|
||||
auto newPage = *newPageRes;
|
||||
|
||||
auto textRes = newPage->extractText();
|
||||
ASSERT_TRUE(textRes.has_value());
|
||||
EXPECT_NE(textRes->find("Greeting, universe!"), std::string::npos);
|
||||
EXPECT_EQ(textRes->find("Hello"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(DocumentEditTest, ReplaceTextRuntimeFontEngine) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto path = getCorpusPath("fonts", "latin_extended.pdf");
|
||||
if (!std::filesystem::exists(path)) {
|
||||
GTEST_SKIP() << "latin_extended.pdf not found in corpus.";
|
||||
}
|
||||
|
||||
auto docRes = PdfDocument::loadFromFile(path.string());
|
||||
ASSERT_TRUE(docRes.has_value());
|
||||
auto doc = *docRes;
|
||||
|
||||
auto pageRes = doc->getPage(0);
|
||||
ASSERT_TRUE(pageRes.has_value());
|
||||
auto pageObj = *pageRes;
|
||||
|
||||
auto modelRes = pageObj->extractDocumentModel();
|
||||
ASSERT_TRUE(modelRes.has_value());
|
||||
const auto& model = *modelRes;
|
||||
|
||||
std::vector<int> objectIndices;
|
||||
std::string originalFontId = "";
|
||||
for (const auto& p : model.paragraphs) {
|
||||
for (const auto& line : p.lines) {
|
||||
for (const auto& run : line.runs) {
|
||||
if (run.fontName.find("Roboto-Regular") != std::string::npos) {
|
||||
objectIndices = run.objectIndices;
|
||||
originalFontId = run.internalFontId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!objectIndices.empty()) break;
|
||||
}
|
||||
if (!objectIndices.empty()) break;
|
||||
}
|
||||
|
||||
ASSERT_FALSE(objectIndices.empty()) << "Could not find target text run in latin_extended.pdf";
|
||||
|
||||
std::string indicesStr = "";
|
||||
for (size_t i = 0; i < objectIndices.size(); ++i) {
|
||||
indicesStr += std::to_string(objectIndices[i]);
|
||||
if (i + 1 < objectIndices.size()) indicesStr += ",";
|
||||
}
|
||||
|
||||
// JSON payload including internalFontId to resolve
|
||||
std::string editsJson = R"({
|
||||
"version": "1.0",
|
||||
"operations": [
|
||||
{
|
||||
"id": "op_engine_1",
|
||||
"type": "replace_text",
|
||||
"pageIndex": 0,
|
||||
"objectIndices": [)" + indicesStr + R"(],
|
||||
"text": "Font Engine Active!",
|
||||
"internalFontId": ")" + originalFontId + R"("
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
auto editRes = doc->applyEdits(editsJson);
|
||||
ASSERT_TRUE(editRes.has_value());
|
||||
|
||||
auto saveRes = doc->saveIncremental();
|
||||
ASSERT_TRUE(saveRes.has_value());
|
||||
const auto& savedBytes = *saveRes;
|
||||
ASSERT_FALSE(savedBytes.empty());
|
||||
|
||||
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
|
||||
ASSERT_TRUE(newDocRes.has_value());
|
||||
auto newDoc = *newDocRes;
|
||||
|
||||
auto newPageRes = newDoc->getPage(0);
|
||||
ASSERT_TRUE(newPageRes.has_value());
|
||||
auto newPage = *newPageRes;
|
||||
|
||||
auto textRes = newPage->extractText();
|
||||
ASSERT_TRUE(textRes.has_value());
|
||||
EXPECT_NE(textRes->find("Font Engine Active!"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(DocumentEditTest, ReplaceTextFontReuseAndEmbedding) {
|
||||
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 doc = *docRes;
|
||||
|
||||
auto pageRes = doc->getPage(0);
|
||||
ASSERT_TRUE(pageRes.has_value());
|
||||
auto pageObj = *pageRes;
|
||||
|
||||
auto modelRes = pageObj->extractDocumentModel();
|
||||
ASSERT_TRUE(modelRes.has_value());
|
||||
const auto& model = *modelRes;
|
||||
|
||||
// Find the first text run
|
||||
std::vector<int> objectIndices;
|
||||
std::string originalFontId = "";
|
||||
for (const auto& p : model.paragraphs) {
|
||||
for (const auto& line : p.lines) {
|
||||
for (const auto& run : line.runs) {
|
||||
if (!run.objectIndices.empty()) {
|
||||
objectIndices = run.objectIndices;
|
||||
originalFontId = run.internalFontId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!objectIndices.empty()) break;
|
||||
}
|
||||
if (!objectIndices.empty()) break;
|
||||
}
|
||||
|
||||
ASSERT_FALSE(objectIndices.empty()) << "Could not find a text run in hello_world.pdf";
|
||||
|
||||
std::string indicesStr = "";
|
||||
for (size_t i = 0; i < objectIndices.size(); ++i) {
|
||||
indicesStr += std::to_string(objectIndices[i]);
|
||||
if (i + 1 < objectIndices.size()) indicesStr += ",";
|
||||
}
|
||||
|
||||
// JSON payload containing replacement using system font embedding
|
||||
std::string editsJson = R"({
|
||||
"version": "1.0",
|
||||
"operations": [
|
||||
{
|
||||
"id": "op_reuse_1",
|
||||
"type": "replace_text",
|
||||
"pageIndex": 0,
|
||||
"objectIndices": [)" + indicesStr + R"(],
|
||||
"text": "Embedded Arial",
|
||||
"internalFontId": ")" + originalFontId + R"("
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
auto editRes = doc->applyEdits(editsJson);
|
||||
ASSERT_TRUE(editRes.has_value());
|
||||
|
||||
// Save and reload
|
||||
auto saveRes = doc->saveIncremental();
|
||||
ASSERT_TRUE(saveRes.has_value());
|
||||
const auto& savedBytes = *saveRes;
|
||||
ASSERT_FALSE(savedBytes.empty());
|
||||
|
||||
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
|
||||
ASSERT_TRUE(newDocRes.has_value());
|
||||
auto newDoc = *newDocRes;
|
||||
|
||||
// Get page fonts to verify that Arial was successfully embedded in the new document
|
||||
auto fontsRes = newDoc->getFonts(0, 0);
|
||||
ASSERT_TRUE(fontsRes.has_value());
|
||||
|
||||
bool foundEmbeddedArial = false;
|
||||
for (const auto& f : *fontsRes) {
|
||||
if (f.isEmbedded && (f.fontName.find("Arial") != std::string::npos || f.fontName.find("LiberationSans") != std::string::npos)) {
|
||||
foundEmbeddedArial = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Font Embedding Test: foundEmbeddedArial = " << foundEmbeddedArial << std::endl;
|
||||
}
|
||||
|
||||
TEST(DocumentEditTest, ReplaceTextHarfBuzzShapingAndReflow) {
|
||||
SKIP_IF_NO_PDFIUM();
|
||||
auto path = getCorpusPath("fonts", "latin_extended.pdf");
|
||||
if (!std::filesystem::exists(path)) {
|
||||
GTEST_SKIP() << "latin_extended.pdf not found in corpus.";
|
||||
}
|
||||
|
||||
auto docRes = PdfDocument::loadFromFile(path.string());
|
||||
ASSERT_TRUE(docRes.has_value());
|
||||
auto doc = *docRes;
|
||||
|
||||
auto pageRes = doc->getPage(0);
|
||||
ASSERT_TRUE(pageRes.has_value());
|
||||
auto pageObj = *pageRes;
|
||||
|
||||
auto modelRes = pageObj->extractDocumentModel();
|
||||
ASSERT_TRUE(modelRes.has_value());
|
||||
const auto& model = *modelRes;
|
||||
|
||||
// Find a line that has at least 2 runs, where the first run uses Roboto-Regular
|
||||
std::vector<int> targetIndices;
|
||||
std::string originalFontId = "";
|
||||
std::string runBText = "";
|
||||
double runBOrigX = 0.0;
|
||||
double runBOrigY = 0.0;
|
||||
|
||||
for (const auto& p : model.paragraphs) {
|
||||
for (const auto& line : p.lines) {
|
||||
if (line.runs.size() >= 2) {
|
||||
const auto& runA = line.runs[0];
|
||||
const auto& runB = line.runs[1];
|
||||
if (runA.fontName.find("Roboto-Regular") != std::string::npos &&
|
||||
!runA.objectIndices.empty() &&
|
||||
runB.x > runA.x) {
|
||||
targetIndices = runA.objectIndices;
|
||||
originalFontId = runA.internalFontId;
|
||||
runBText = runB.text;
|
||||
runBOrigX = runB.x;
|
||||
runBOrigY = runB.y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetIndices.empty()) break;
|
||||
}
|
||||
|
||||
if (targetIndices.empty()) {
|
||||
GTEST_SKIP() << "Could not find a suitable line with multiple runs to test reflow.";
|
||||
}
|
||||
|
||||
std::string indicesStr = "";
|
||||
for (size_t i = 0; i < targetIndices.size(); ++i) {
|
||||
indicesStr += std::to_string(targetIndices[i]);
|
||||
if (i + 1 < targetIndices.size()) indicesStr += ",";
|
||||
}
|
||||
|
||||
// JSON payload: replacing runA with a very long text to trigger significant shift
|
||||
std::string editsJson = R"({
|
||||
"version": "1.0",
|
||||
"operations": [
|
||||
{
|
||||
"id": "op_reflow_1",
|
||||
"type": "replace_text",
|
||||
"pageIndex": 0,
|
||||
"objectIndices": [)" + indicesStr + R"(],
|
||||
"text": "This is an extremely long replacement text to force the Reflow Engine to shift subsequent runs!",
|
||||
"internalFontId": ")" + originalFontId + R"("
|
||||
}
|
||||
]
|
||||
})";
|
||||
|
||||
auto editRes = doc->applyEdits(editsJson);
|
||||
ASSERT_TRUE(editRes.has_value());
|
||||
|
||||
// Save and reload
|
||||
auto saveRes = doc->saveIncremental();
|
||||
ASSERT_TRUE(saveRes.has_value());
|
||||
const auto& savedBytes = *saveRes;
|
||||
ASSERT_FALSE(savedBytes.empty());
|
||||
|
||||
auto newDocRes = PdfDocument::loadFromMemory(savedBytes);
|
||||
ASSERT_TRUE(newDocRes.has_value());
|
||||
auto newDoc = *newDocRes;
|
||||
|
||||
auto newPageRes = newDoc->getPage(0);
|
||||
ASSERT_TRUE(newPageRes.has_value());
|
||||
auto newPage = *newPageRes;
|
||||
|
||||
auto newModelRes = newPage->extractDocumentModel();
|
||||
ASSERT_TRUE(newModelRes.has_value());
|
||||
const auto& newModel = *newModelRes;
|
||||
|
||||
// Find runB in the new document model and verify its X coordinate has shifted to the right
|
||||
bool foundRunB = false;
|
||||
double runBNewX = 0.0;
|
||||
for (const auto& p : newModel.paragraphs) {
|
||||
for (const auto& line : p.lines) {
|
||||
for (const auto& run : line.runs) {
|
||||
if (run.text == runBText && std::abs(run.y - runBOrigY) < 5.0) {
|
||||
foundRunB = true;
|
||||
runBNewX = run.x;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundRunB) break;
|
||||
}
|
||||
if (foundRunB) break;
|
||||
}
|
||||
|
||||
ASSERT_TRUE(foundRunB) << "Could not find the subsequent text run '" << runBText << "' in the reflowed document.";
|
||||
EXPECT_GT(runBNewX, runBOrigX + 10.0) << "The subsequent text run did not shift to the right by at least 10 points.";
|
||||
|
||||
std::cout << "Reflow Engine verified: '" << runBText << "' shifted from X=" << runBOrigX << " to X=" << runBNewX << std::endl;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user