1429 lines
49 KiB
C++
1429 lines
49 KiB
C++
#include "fonts/face/font_face.hpp"
|
|
#include "fonts/shaping/hb_shaper.hpp"
|
|
#include "fonts/cache/glyph_cache.hpp"
|
|
#include "fonts/pdf_fonts/font.hpp"
|
|
#include "fonts/pdf_fonts/types/truetype_font.hpp"
|
|
#include "fonts/pdf_fonts/types/type1_font.hpp"
|
|
#include "fonts/pdf_fonts/types/cid_font.hpp"
|
|
#include "fonts/pdf_fonts/font_loader.hpp"
|
|
#include "fonts/pdf_fonts/encoding/encoding.hpp"
|
|
#include "fonts/pdf_fonts/encoding/cjk_collection_db.hpp"
|
|
#include "fonts/pdf_fonts/font_fallback.hpp"
|
|
#include "fonts/pdf_fonts/font_subset.hpp"
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
// Case-insensitive substring check. System font filenames differ in case
|
|
// across platforms (e.g. macOS ships "Times.ttc", Windows "times.ttf"), so the
|
|
// font-fallback assertions match without regard to case.
|
|
bool containsCI(const std::string& haystack, const std::string& needle) {
|
|
auto it = std::search(
|
|
haystack.begin(), haystack.end(), needle.begin(), needle.end(),
|
|
[](char a, char b) {
|
|
return std::tolower(static_cast<unsigned char>(a)) ==
|
|
std::tolower(static_cast<unsigned char>(b));
|
|
});
|
|
return it != haystack.end();
|
|
}
|
|
|
|
bool saveGlyphAsPGM(const pdfengine::fonts::GlyphBitmap& bitmap, const std::string& filename) {
|
|
if (bitmap.width == 0 || bitmap.height == 0 || bitmap.pixels.empty()) {
|
|
return false;
|
|
}
|
|
std::ofstream out(filename, std::ios::binary);
|
|
if (!out) {
|
|
return false;
|
|
}
|
|
out << "P5\n" << bitmap.width << " " << bitmap.height << "\n255\n";
|
|
out.write(reinterpret_cast<const char*>(bitmap.pixels.data()), bitmap.pixels.size());
|
|
return true;
|
|
}
|
|
|
|
std::string getSystemFontPath() {
|
|
#if defined(_WIN32)
|
|
// Common Windows fonts
|
|
std::vector<std::string> paths = {
|
|
"C:\\Windows\\Fonts\\arial.ttf",
|
|
"C:\\Windows\\Fonts\\consola.ttf",
|
|
"C:\\Windows\\Fonts\\tahoma.ttf"
|
|
};
|
|
#elif defined(__APPLE__)
|
|
// Common macOS fonts
|
|
std::vector<std::string> paths = {
|
|
"/Library/Fonts/Arial.ttf",
|
|
"/System/Library/Fonts/Geneva.ttf",
|
|
"/System/Library/Fonts/Helvetica.ttc",
|
|
"/System/Library/Fonts/Supplemental/Arial.ttf"
|
|
};
|
|
#else
|
|
// Common Linux fonts
|
|
std::vector<std::string> paths = {
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
|
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
|
"/usr/share/fonts/truetype/freefont/FreeSans.ttf"
|
|
};
|
|
#endif
|
|
|
|
for (const auto& path : paths) {
|
|
if (std::filesystem::exists(path)) {
|
|
return path;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace pdfengine::fonts {
|
|
|
|
TEST(FontTest, FontFaceInitialization) {
|
|
FontFace face;
|
|
EXPECT_EQ(face.getFace(), nullptr);
|
|
}
|
|
|
|
TEST(FontTest, FontFaceLoadNonExistentFile) {
|
|
FontFace face;
|
|
EXPECT_FALSE(face.loadFromFile("this_file_does_not_exist_12345.ttf"));
|
|
EXPECT_EQ(face.getFace(), nullptr);
|
|
}
|
|
|
|
TEST(FontTest, FontFaceMoveSemantics) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run move semantics test.";
|
|
}
|
|
|
|
FontFace face1;
|
|
ASSERT_TRUE(face1.loadFromFile(fontPath));
|
|
FT_Face rawFace = face1.getFace();
|
|
ASSERT_NE(rawFace, nullptr);
|
|
|
|
// Move construction
|
|
FontFace face2(std::move(face1));
|
|
EXPECT_EQ(face1.getFace(), nullptr);
|
|
EXPECT_EQ(face2.getFace(), rawFace);
|
|
|
|
// Move assignment
|
|
FontFace face3;
|
|
face3 = std::move(face2);
|
|
EXPECT_EQ(face2.getFace(), nullptr);
|
|
EXPECT_EQ(face3.getFace(), rawFace);
|
|
}
|
|
|
|
TEST(FontTest, HbShaperEmptyInput) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run empty input shaper test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
|
|
HbShaper shaper;
|
|
auto glyphs = shaper.shapeRun("", face, 16);
|
|
EXPECT_TRUE(glyphs.empty());
|
|
}
|
|
|
|
TEST(FontTest, HbShaperNullFace) {
|
|
FontFace face; // Null face
|
|
HbShaper shaper;
|
|
auto glyphs = shaper.shapeRun("Hello", face, 16);
|
|
EXPECT_TRUE(glyphs.empty());
|
|
}
|
|
|
|
TEST(FontTest, HbShaperShapeTextSuccess) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
std::cout << "[ WARNING ] Skipping shape success test: no system font found." << std::endl;
|
|
GTEST_SKIP() << "No system font found to run text shaping test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
ASSERT_NE(face.getFace(), nullptr);
|
|
|
|
HbShaper shaper;
|
|
std::string testText = "Hello World!";
|
|
auto glyphs = shaper.shapeRun(testText, face, 16);
|
|
|
|
// Validate that some glyphs were shaped.
|
|
// Note that the number of glyphs doesn't strictly have to match testText.length() (e.g. ligatures),
|
|
// but for simple English it's usually 1:1.
|
|
EXPECT_FALSE(glyphs.empty());
|
|
|
|
for (const auto& g : glyphs) {
|
|
// Glyph index should be non-zero for valid glyphs (0 is usually .notdef)
|
|
// Note: some fonts might not map all characters, but Arial/DejaVu/Consolas should map ASCII.
|
|
EXPECT_GT(g.advanceX, 0.0);
|
|
}
|
|
}
|
|
|
|
TEST(FontTest, FontFaceRenderGlyphNullFace) {
|
|
FontFace face; // Null face
|
|
auto glyph = face.renderGlyph(0, 16);
|
|
EXPECT_FALSE(glyph.has_value());
|
|
}
|
|
|
|
TEST(FontTest, FontFaceRenderGlyphSuccess) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run render glyph success test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
ASSERT_NE(face.getFace(), nullptr);
|
|
|
|
// Get the glyph index for character 'A'.
|
|
unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'A');
|
|
ASSERT_GT(glyphIndex, 0u); // Ensure it's not the undefined glyph
|
|
|
|
// Render it at 24px.
|
|
auto glyphOpt = face.renderGlyph(glyphIndex, 24);
|
|
ASSERT_TRUE(glyphOpt.has_value());
|
|
|
|
const auto& glyph = *glyphOpt;
|
|
EXPECT_GT(glyph.width, 0);
|
|
EXPECT_GT(glyph.height, 0);
|
|
EXPECT_EQ(glyph.pixels.size(), static_cast<size_t>(glyph.width * glyph.height));
|
|
EXPECT_GT(glyph.advance, 0.0);
|
|
}
|
|
|
|
TEST(FontTest, GlyphCacheBasicGetInsert) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run cache test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
|
|
GlyphCache cache(10);
|
|
EXPECT_EQ(cache.capacity(), 10u);
|
|
EXPECT_EQ(cache.size(), 0u);
|
|
|
|
// Initial check: cache miss
|
|
auto miss = cache.get(face, 12, 16);
|
|
EXPECT_FALSE(miss.has_value());
|
|
|
|
// Create a dummy GlyphBitmap
|
|
GlyphBitmap bitmap;
|
|
bitmap.width = 10;
|
|
bitmap.height = 12;
|
|
bitmap.pixels = std::vector<unsigned char>(120, 255);
|
|
bitmap.advance = 8.5;
|
|
|
|
// Insert
|
|
cache.insert(face, 12, 16, bitmap);
|
|
EXPECT_EQ(cache.size(), 1u);
|
|
|
|
// Cache hit
|
|
auto hit = cache.get(face, 12, 16);
|
|
ASSERT_TRUE(hit.has_value());
|
|
EXPECT_EQ(hit->width, 10);
|
|
EXPECT_EQ(hit->height, 12);
|
|
EXPECT_EQ(hit->advance, 8.5);
|
|
EXPECT_EQ(hit->pixels.size(), 120u);
|
|
}
|
|
|
|
TEST(FontTest, GlyphCacheEvictionPolicy) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run cache eviction test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
|
|
// Capacity 2
|
|
GlyphCache cache(2);
|
|
|
|
GlyphBitmap bmp1{ .width = 1 };
|
|
GlyphBitmap bmp2{ .width = 2 };
|
|
GlyphBitmap bmp3{ .width = 3 };
|
|
|
|
cache.insert(face, 1, 16, bmp1);
|
|
cache.insert(face, 2, 16, bmp2);
|
|
EXPECT_EQ(cache.size(), 2u);
|
|
|
|
// Insert third one: should evict the oldest (1, 16)
|
|
cache.insert(face, 3, 16, bmp3);
|
|
EXPECT_EQ(cache.size(), 2u);
|
|
|
|
EXPECT_FALSE(cache.get(face, 1, 16).has_value());
|
|
EXPECT_TRUE(cache.get(face, 2, 16).has_value());
|
|
EXPECT_TRUE(cache.get(face, 3, 16).has_value());
|
|
}
|
|
|
|
TEST(FontTest, GlyphCacheLRUPolicy) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run cache LRU test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
|
|
GlyphCache cache(2);
|
|
|
|
GlyphBitmap bmp1{ .width = 1 };
|
|
GlyphBitmap bmp2{ .width = 2 };
|
|
GlyphBitmap bmp3{ .width = 3 };
|
|
|
|
cache.insert(face, 1, 16, bmp1);
|
|
cache.insert(face, 2, 16, bmp2);
|
|
|
|
// Access 1 to make it most recently used
|
|
auto hit = cache.get(face, 1, 16);
|
|
ASSERT_TRUE(hit.has_value());
|
|
|
|
// Insert 3: since 2 is the oldest (least recently used), 2 should be evicted and 1 should remain
|
|
cache.insert(face, 3, 16, bmp3);
|
|
EXPECT_EQ(cache.size(), 2u);
|
|
|
|
EXPECT_TRUE(cache.get(face, 1, 16).has_value());
|
|
EXPECT_FALSE(cache.get(face, 2, 16).has_value());
|
|
EXPECT_TRUE(cache.get(face, 3, 16).has_value());
|
|
}
|
|
|
|
TEST(FontTest, FontPipelineIntegration) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run pipeline integration test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
|
|
HbShaper shaper;
|
|
GlyphCache cache(100);
|
|
|
|
std::vector<std::string> testTexts = {"Hello World", "office", "سلام"};
|
|
unsigned int fontSize = 16;
|
|
|
|
for (const auto& text : testTexts) {
|
|
auto shapedGlyphs = shaper.shapeRun(text, face, fontSize);
|
|
EXPECT_FALSE(shapedGlyphs.empty());
|
|
|
|
for (const auto& sg : shapedGlyphs) {
|
|
auto cachedBmp = cache.get(face, sg.glyphIndex, fontSize);
|
|
if (!cachedBmp.has_value()) {
|
|
auto renderedOpt = face.renderGlyph(sg.glyphIndex, fontSize);
|
|
ASSERT_TRUE(renderedOpt.has_value());
|
|
cache.insert(face, sg.glyphIndex, fontSize, *renderedOpt);
|
|
EXPECT_EQ(renderedOpt->pixels.size(), static_cast<size_t>(renderedOpt->width * renderedOpt->height));
|
|
}
|
|
|
|
auto hitBmp = cache.get(face, sg.glyphIndex, fontSize);
|
|
ASSERT_TRUE(hitBmp.has_value());
|
|
EXPECT_EQ(hitBmp->pixels.size(), static_cast<size_t>(hitBmp->width * hitBmp->height));
|
|
EXPECT_GE(hitBmp->advance, 0.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(FontTest, UnicodeAndRtlShaping) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run Unicode and RTL shaping test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
|
|
HbShaper shaper;
|
|
unsigned int fontSize = 16;
|
|
|
|
// Test A: Arabic (RTL) - "سلام"
|
|
{
|
|
std::string arabicText = "سلام";
|
|
auto glyphs = shaper.shapeRun(arabicText, face, fontSize);
|
|
EXPECT_FALSE(glyphs.empty());
|
|
for (const auto& g : glyphs) {
|
|
// Validate that shaping executed and returned valid layout metrics
|
|
EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.offsetX != 0.0 || g.offsetY != 0.0 || g.glyphIndex != 999999u);
|
|
}
|
|
}
|
|
|
|
// Test B: Hindi - "नमस्ते"
|
|
{
|
|
std::string hindiText = "नमस्ते";
|
|
auto glyphs = shaper.shapeRun(hindiText, face, fontSize);
|
|
EXPECT_FALSE(glyphs.empty());
|
|
for (const auto& g : glyphs) {
|
|
EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.glyphIndex != 999999u);
|
|
}
|
|
}
|
|
|
|
// Test C: Ligatures - "office"
|
|
{
|
|
std::string ligatureText = "office";
|
|
auto glyphs = shaper.shapeRun(ligatureText, face, fontSize);
|
|
EXPECT_FALSE(glyphs.empty());
|
|
EXPECT_LE(glyphs.size(), ligatureText.length());
|
|
for (const auto& g : glyphs) {
|
|
EXPECT_TRUE(g.advanceX != 0.0 || g.advanceY != 0.0 || g.glyphIndex != 999999u);
|
|
}
|
|
}
|
|
}
|
|
|
|
TEST(FontTest, CachePerformanceTest) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run cache performance test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
|
|
HbShaper shaper;
|
|
GlyphCache cache(100);
|
|
|
|
std::string text = "Hello Hello Hello Hello";
|
|
unsigned int fontSize = 16;
|
|
|
|
{
|
|
auto glyphs = shaper.shapeRun(text, face, fontSize);
|
|
for (const auto& g : glyphs) {
|
|
auto cached = cache.get(face, g.glyphIndex, fontSize);
|
|
if (!cached.has_value()) {
|
|
auto rendered = face.renderGlyph(g.glyphIndex, fontSize);
|
|
if (rendered.has_value()) {
|
|
cache.insert(face, g.glyphIndex, fontSize, *rendered);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
cache.resetStats();
|
|
|
|
for (int i = 0; i < 1000; ++i) {
|
|
auto glyphs = shaper.shapeRun(text, face, fontSize);
|
|
for (const auto& g : glyphs) {
|
|
auto cached = cache.get(face, g.glyphIndex, fontSize);
|
|
if (!cached.has_value()) {
|
|
auto rendered = face.renderGlyph(g.glyphIndex, fontSize);
|
|
if (rendered.has_value()) {
|
|
cache.insert(face, g.glyphIndex, fontSize, *rendered);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
double hitRateVal = cache.hitRate();
|
|
std::cout << "[ INFO ] Cache Hit Rate for repetitive text: " << (hitRateVal * 100.0) << "%" << std::endl;
|
|
EXPECT_GT(hitRateVal, 0.90);
|
|
}
|
|
|
|
TEST(FontTest, EngineStressTest) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run engine stress test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
|
|
HbShaper shaper;
|
|
GlyphCache cache(32);
|
|
|
|
std::string base = "The quick brown fox jumps over the lazy dog. 1234567890!@#$%^&*() ";
|
|
std::string longText;
|
|
longText.reserve(10000);
|
|
while (longText.length() < 10000) {
|
|
longText += base;
|
|
}
|
|
|
|
unsigned int fontSize = 16;
|
|
|
|
auto glyphs = shaper.shapeRun(longText, face, fontSize);
|
|
EXPECT_FALSE(glyphs.empty());
|
|
|
|
for (const auto& g : glyphs) {
|
|
auto cached = cache.get(face, g.glyphIndex, fontSize);
|
|
if (!cached.has_value()) {
|
|
auto rendered = face.renderGlyph(g.glyphIndex, fontSize);
|
|
if (rendered.has_value()) {
|
|
cache.insert(face, g.glyphIndex, fontSize, *rendered);
|
|
}
|
|
}
|
|
}
|
|
|
|
EXPECT_LE(cache.size(), cache.capacity());
|
|
EXPECT_GT(cache.size(), 0u);
|
|
}
|
|
|
|
TEST(FontTest, VisualBitmapDebugging) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run visual bitmap debugging test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
|
|
unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'A');
|
|
ASSERT_GT(glyphIndex, 0u);
|
|
|
|
auto renderedOpt = face.renderGlyph(glyphIndex, 48);
|
|
ASSERT_TRUE(renderedOpt.has_value());
|
|
|
|
std::string filename = "A.pgm";
|
|
std::filesystem::remove(filename);
|
|
|
|
ASSERT_TRUE(saveGlyphAsPGM(*renderedOpt, filename));
|
|
EXPECT_TRUE(std::filesystem::exists(filename));
|
|
EXPECT_GT(std::filesystem::file_size(filename), 0u);
|
|
}
|
|
|
|
TEST(FontTest, MetricsValidation) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run metrics validation test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
FT_Face ftFace = face.getFace();
|
|
ASSERT_NE(ftFace, nullptr);
|
|
|
|
unsigned int glyphIndex = FT_Get_Char_Index(ftFace, 'B');
|
|
ASSERT_GT(glyphIndex, 0u);
|
|
|
|
unsigned int fontSize = 24;
|
|
auto renderedOpt = face.renderGlyph(glyphIndex, fontSize);
|
|
ASSERT_TRUE(renderedOpt.has_value());
|
|
|
|
ASSERT_EQ(FT_Set_Pixel_Sizes(ftFace, 0, fontSize), 0);
|
|
ASSERT_EQ(FT_Load_Glyph(ftFace, glyphIndex, FT_LOAD_RENDER), 0);
|
|
|
|
FT_GlyphSlot slot = ftFace->glyph;
|
|
EXPECT_EQ(renderedOpt->width, static_cast<int>(slot->bitmap.width));
|
|
EXPECT_EQ(renderedOpt->height, static_cast<int>(slot->bitmap.rows));
|
|
EXPECT_EQ(renderedOpt->bearingX, slot->bitmap_left);
|
|
EXPECT_EQ(renderedOpt->bearingY, slot->bitmap_top);
|
|
EXPECT_DOUBLE_EQ(renderedOpt->advance, static_cast<double>(slot->advance.x) / 64.0);
|
|
}
|
|
|
|
TEST(FontTest, CacheRecencyStress) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run cache recency stress test.";
|
|
}
|
|
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromFile(fontPath));
|
|
|
|
GlyphCache cache(5);
|
|
|
|
std::vector<GlyphBitmap> bmps;
|
|
for (int i = 0; i < 10; ++i) {
|
|
GlyphBitmap b;
|
|
b.width = i;
|
|
bmps.push_back(b);
|
|
}
|
|
|
|
for (unsigned int i = 0; i < 5; ++i) {
|
|
cache.insert(face, i, 16, bmps[i]);
|
|
}
|
|
EXPECT_EQ(cache.size(), 5u);
|
|
|
|
ASSERT_TRUE(cache.get(face, 0, 16).has_value());
|
|
ASSERT_TRUE(cache.get(face, 2, 16).has_value());
|
|
|
|
cache.insert(face, 5, 16, bmps[5]);
|
|
// 1 should be evicted because it was the oldest
|
|
EXPECT_FALSE(cache.get(face, 1, 16).has_value());
|
|
// 5 was just inserted, should be at the front
|
|
EXPECT_TRUE(cache.get(face, 5, 16).has_value());
|
|
|
|
// Access 3 to promote it to the front
|
|
ASSERT_TRUE(cache.get(face, 3, 16).has_value());
|
|
|
|
// Insert 6. With cache.get lookups, 4 is now the oldest (since 3, 5, 2, 0 have been looked up recently)
|
|
cache.insert(face, 6, 16, bmps[6]);
|
|
// 4 should be evicted
|
|
EXPECT_FALSE(cache.get(face, 4, 16).has_value());
|
|
|
|
// The rest should remain
|
|
EXPECT_TRUE(cache.get(face, 0, 16).has_value());
|
|
EXPECT_TRUE(cache.get(face, 2, 16).has_value());
|
|
EXPECT_TRUE(cache.get(face, 3, 16).has_value());
|
|
EXPECT_TRUE(cache.get(face, 5, 16).has_value());
|
|
EXPECT_TRUE(cache.get(face, 6, 16).has_value());
|
|
}
|
|
|
|
TEST(FontLoaderTest, FontFaceLoadFromMemorySuccess) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run load from memory success test.";
|
|
}
|
|
|
|
// Read the entire file into a buffer
|
|
std::ifstream file(fontPath, std::ios::binary | std::ios::ate);
|
|
ASSERT_TRUE(file.is_open());
|
|
std::streamsize size = file.tellg();
|
|
file.seekg(0, std::ios::beg);
|
|
|
|
std::vector<uint8_t> buffer(size);
|
|
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
|
|
|
|
// Load from memory
|
|
FontFace face;
|
|
ASSERT_TRUE(face.loadFromMemory(buffer));
|
|
ASSERT_NE(face.getFace(), nullptr);
|
|
|
|
// Validate that glyph rendering and metrics are valid
|
|
unsigned int glyphIndex = FT_Get_Char_Index(face.getFace(), 'M');
|
|
ASSERT_GT(glyphIndex, 0u);
|
|
|
|
auto glyph = face.renderGlyph(glyphIndex, 16);
|
|
ASSERT_TRUE(glyph.has_value());
|
|
EXPECT_GT(glyph->width, 0);
|
|
EXPECT_GT(glyph->height, 0);
|
|
EXPECT_GT(glyph->advance, 0.0);
|
|
}
|
|
|
|
TEST(FontLoaderTest, FontFaceLoadFromMemoryInvalid) {
|
|
FontFace face;
|
|
// Empty vector
|
|
std::vector<uint8_t> emptyData;
|
|
EXPECT_FALSE(face.loadFromMemory(emptyData));
|
|
EXPECT_EQ(face.getFace(), nullptr);
|
|
|
|
// Corrupt garbage data
|
|
std::vector<uint8_t> corruptData = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
|
|
EXPECT_FALSE(face.loadFromMemory(corruptData));
|
|
EXPECT_EQ(face.getFace(), nullptr);
|
|
}
|
|
|
|
TEST(FontLoaderTest, FontLoaderTrueTypeSuccess) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run PDF font loader test.";
|
|
}
|
|
|
|
std::ifstream file(fontPath, std::ios::binary | std::ios::ate);
|
|
ASSERT_TRUE(file.is_open());
|
|
std::streamsize size = file.tellg();
|
|
file.seekg(0, std::ios::beg);
|
|
std::vector<uint8_t> buffer(size);
|
|
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
|
|
|
|
// Use factory loader
|
|
auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory("Arial", buffer);
|
|
ASSERT_NE(pdfFont, nullptr);
|
|
EXPECT_EQ(pdfFont->getBaseFont(), "Arial");
|
|
EXPECT_EQ(pdfFont->getType(), pdfengine::fonts::pdf_fonts::FontType::TrueType);
|
|
EXPECT_TRUE(pdfFont->isEmbedded());
|
|
|
|
// Shaping via the loaded FontFace
|
|
HbShaper shaper;
|
|
auto glyphs = shaper.shapeRun("Test Memory Load", pdfFont->getFontFace(), 16);
|
|
EXPECT_FALSE(glyphs.empty());
|
|
}
|
|
|
|
TEST(FontDescriptorTest, DescriptorDefaultValues) {
|
|
pdfengine::fonts::pdf_fonts::FontDescriptor desc;
|
|
EXPECT_EQ(desc.getFontName(), "");
|
|
EXPECT_EQ(desc.getFlags(), 0);
|
|
EXPECT_EQ(desc.getItalicAngle(), 0.0);
|
|
EXPECT_EQ(desc.getAscent(), 0.0);
|
|
EXPECT_EQ(desc.getDescent(), 0.0);
|
|
EXPECT_EQ(desc.getCapHeight(), 0.0);
|
|
EXPECT_EQ(desc.getStemV(), 0.0);
|
|
|
|
pdfengine::fonts::pdf_fonts::FontBBox bbox = desc.getFontBBox();
|
|
EXPECT_EQ(bbox.llx, 0);
|
|
EXPECT_EQ(bbox.lly, 0);
|
|
EXPECT_EQ(bbox.urx, 0);
|
|
EXPECT_EQ(bbox.ury, 0);
|
|
|
|
EXPECT_FALSE(desc.isFixedPitch());
|
|
EXPECT_FALSE(desc.isSerif());
|
|
EXPECT_FALSE(desc.isSymbolic());
|
|
EXPECT_FALSE(desc.isItalic());
|
|
}
|
|
|
|
TEST(FontDescriptorTest, DescriptorParsingSuccess) {
|
|
std::string dict =
|
|
"<< /Type /FontDescriptor\n"
|
|
" /FontName /ArialMT\n"
|
|
" /Flags 32\n"
|
|
" /FontBBox [-166 -225 1000 931]\n"
|
|
" /ItalicAngle 0\n"
|
|
" /Ascent 905\n"
|
|
" /Descent -211\n"
|
|
" /CapHeight 728\n"
|
|
" /StemV 94\n"
|
|
">>";
|
|
|
|
pdfengine::fonts::pdf_fonts::FontDescriptor desc;
|
|
ASSERT_TRUE(desc.parseFromDictionaryString(dict));
|
|
|
|
EXPECT_EQ(desc.getFontName(), "ArialMT");
|
|
EXPECT_EQ(desc.getFlags(), 32);
|
|
|
|
pdfengine::fonts::pdf_fonts::FontBBox bbox = desc.getFontBBox();
|
|
EXPECT_EQ(bbox.llx, -166);
|
|
EXPECT_EQ(bbox.lly, -225);
|
|
EXPECT_EQ(bbox.urx, 1000);
|
|
EXPECT_EQ(bbox.ury, 931);
|
|
|
|
EXPECT_DOUBLE_EQ(desc.getItalicAngle(), 0.0);
|
|
EXPECT_DOUBLE_EQ(desc.getAscent(), 905.0);
|
|
EXPECT_DOUBLE_EQ(desc.getDescent(), -211.0);
|
|
EXPECT_DOUBLE_EQ(desc.getCapHeight(), 728.0);
|
|
EXPECT_DOUBLE_EQ(desc.getStemV(), 94.0);
|
|
|
|
// Check flags
|
|
EXPECT_FALSE(desc.isFixedPitch());
|
|
EXPECT_TRUE(desc.isNonsymbolic()); // 32
|
|
EXPECT_FALSE(desc.isItalic());
|
|
}
|
|
|
|
TEST(FontDescriptorTest, DescriptorParsingMalformed) {
|
|
pdfengine::fonts::pdf_fonts::FontDescriptor desc;
|
|
|
|
// Missing <<
|
|
EXPECT_FALSE(desc.parseFromDictionaryString("/Flags 32 >>"));
|
|
|
|
// Unmatched >>
|
|
EXPECT_FALSE(desc.parseFromDictionaryString("<< /Flags 32"));
|
|
|
|
// Malformed BBox array (missing urx, ury)
|
|
EXPECT_FALSE(desc.parseFromDictionaryString("<< /FontBBox [-166 -225] >>"));
|
|
|
|
// Malformed double conversion
|
|
EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent abc >>"));
|
|
|
|
// Key without value
|
|
EXPECT_FALSE(desc.parseFromDictionaryString("<< /Ascent >>"));
|
|
}
|
|
|
|
TEST(FontDescriptorTest, FontLoaderWithDescriptor) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run PDF font loader test.";
|
|
}
|
|
|
|
std::ifstream file(fontPath, std::ios::binary | std::ios::ate);
|
|
ASSERT_TRUE(file.is_open());
|
|
std::streamsize size = file.tellg();
|
|
file.seekg(0, std::ios::beg);
|
|
std::vector<uint8_t> buffer(size);
|
|
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
|
|
|
|
// Create descriptor
|
|
auto descriptor = std::make_unique<pdfengine::fonts::pdf_fonts::FontDescriptor>();
|
|
descriptor->setFontName("Arial-BoldMT");
|
|
descriptor->setFlags(96); // Nonsymbolic (32) | Italic (64)
|
|
descriptor->setAscent(905.0);
|
|
descriptor->setDescent(-211.0);
|
|
|
|
// Load with descriptor
|
|
auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory("Arial-Bold", buffer, std::move(descriptor));
|
|
ASSERT_NE(pdfFont, nullptr);
|
|
EXPECT_EQ(pdfFont->getBaseFont(), "Arial-Bold");
|
|
EXPECT_TRUE(pdfFont->isEmbedded());
|
|
|
|
const auto* retrievedDesc = pdfFont->getDescriptor();
|
|
ASSERT_NE(retrievedDesc, nullptr);
|
|
EXPECT_EQ(retrievedDesc->getFontName(), "Arial-BoldMT");
|
|
EXPECT_EQ(retrievedDesc->getFlags(), 96);
|
|
EXPECT_TRUE(retrievedDesc->isItalic());
|
|
EXPECT_TRUE(retrievedDesc->isNonsymbolic());
|
|
EXPECT_DOUBLE_EQ(retrievedDesc->getAscent(), 905.0);
|
|
EXPECT_DOUBLE_EQ(retrievedDesc->getDescent(), -211.0);
|
|
}
|
|
|
|
TEST(EncodingTest, PredefinedEncodingTest) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
// WinAnsiEncoding
|
|
PredefinedEncoding winAnsi(SimpleEncodingType::WinAnsi);
|
|
EXPECT_EQ(winAnsi.getType(), SimpleEncodingType::WinAnsi);
|
|
EXPECT_EQ(winAnsi.decode(65), 65); // 'A'
|
|
EXPECT_EQ(winAnsi.decode(128), 0x20AC); // Euro symbol exception
|
|
EXPECT_EQ(winAnsi.decode(169), 169); // copyright symbol (standard ISO-8859-1)
|
|
EXPECT_EQ(winAnsi.decode(300), 0); // Out of bounds
|
|
|
|
// MacRomanEncoding
|
|
PredefinedEncoding macRoman(SimpleEncodingType::MacRoman);
|
|
EXPECT_EQ(macRoman.getType(), SimpleEncodingType::MacRoman);
|
|
EXPECT_EQ(macRoman.decode(65), 65); // 'A'
|
|
EXPECT_EQ(macRoman.decode(128), 0x00C4); // High page lookup exception (A-dieresis)
|
|
EXPECT_EQ(macRoman.decode(300), 0); // Out of bounds
|
|
|
|
// Identity encoding (pass-through)
|
|
PredefinedEncoding identity(SimpleEncodingType::Identity);
|
|
EXPECT_EQ(identity.getType(), SimpleEncodingType::Identity);
|
|
EXPECT_EQ(identity.decode(65), 65);
|
|
EXPECT_EQ(identity.decode(128), 128);
|
|
EXPECT_EQ(identity.decode(1000), 1000); // Beyond 255 pass-through
|
|
}
|
|
|
|
TEST(EncodingTest, CustomEncodingWithDifferences) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto baseEncoding = std::make_unique<PredefinedEncoding>(SimpleEncodingType::WinAnsi);
|
|
CustomEncoding custom(std::move(baseEncoding));
|
|
|
|
// Fallback to base
|
|
EXPECT_EQ(custom.decode(65), 65);
|
|
|
|
// Standard glyph name difference mapping
|
|
custom.addDifference(120, "quotesingle");
|
|
EXPECT_EQ(custom.decode(120), 0x0027);
|
|
|
|
// Unicode-like glyph name (uniXXXX) difference mapping
|
|
custom.addDifference(121, "uni0041");
|
|
EXPECT_EQ(custom.decode(121), 0x0041); // 'A'
|
|
|
|
// uXXXX representation
|
|
custom.addDifference(122, "u0042");
|
|
EXPECT_EQ(custom.decode(122), 0x0042); // 'B'
|
|
|
|
// Non-existent or unresolved glyph name
|
|
custom.addDifference(123, "nonexistentglyphname123");
|
|
EXPECT_EQ(custom.decode(123), 123); // Falls back to base (WinAnsi maps 123 to 123 '{')
|
|
}
|
|
|
|
TEST(EncodingTest, ToUnicodeCMapbfchar) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
ToUnicodeCMap cmap;
|
|
|
|
std::string cmapStream =
|
|
"/CIDInit /ProcSet findresource begin\n"
|
|
"12 dict begin\n"
|
|
"begincmap\n"
|
|
"/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n"
|
|
"/CMapName /Custom-ToUnicode def\n"
|
|
"1 begincodespacerange\n"
|
|
"<0000> <FFFF>\n"
|
|
"endcodespacerange\n"
|
|
"2 beginbfchar\n"
|
|
"<0001> <0041>\n"
|
|
"<0002> <0042>\n"
|
|
"endbfchar\n"
|
|
"endcmap\n"
|
|
"CMapName currentdict /CMap defineresource pop\n"
|
|
"end\n"
|
|
"end\n";
|
|
|
|
ASSERT_TRUE(cmap.parseCMapStream(cmapStream));
|
|
EXPECT_EQ(cmap.decode(1), 0x0041); // 'A'
|
|
EXPECT_EQ(cmap.decode(2), 0x0042); // 'B'
|
|
EXPECT_EQ(cmap.decode(3), 0); // Missing
|
|
}
|
|
|
|
TEST(EncodingTest, ToUnicodeCMapbfrange) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
ToUnicodeCMap cmap;
|
|
|
|
std::string cmapStream =
|
|
"begincmap\n"
|
|
"2 beginbfrange\n"
|
|
"<0001> <0005> <0041>\n" // Sequential base mapping (<0001> to <0005> starting at <0041>)
|
|
"<0010> <0012> [<0061> <0062> <0063>]\n" // Array mapping
|
|
"endbfrange\n"
|
|
"endcmap\n";
|
|
|
|
ASSERT_TRUE(cmap.parseCMapStream(cmapStream));
|
|
|
|
// Assert sequential
|
|
EXPECT_EQ(cmap.decode(1), 0x0041); // 'A'
|
|
EXPECT_EQ(cmap.decode(3), 0x0043); // 'C'
|
|
EXPECT_EQ(cmap.decode(5), 0x0045); // 'E'
|
|
|
|
// Assert array
|
|
EXPECT_EQ(cmap.decode(0x10), 0x0061); // 'a'
|
|
EXPECT_EQ(cmap.decode(0x11), 0x0062); // 'b'
|
|
EXPECT_EQ(cmap.decode(0x12), 0x0063); // 'c'
|
|
}
|
|
|
|
TEST(EncodingTest, ToUnicodeMalformedCMap) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
ToUnicodeCMap cmap;
|
|
|
|
// Completely invalid/garbage content
|
|
std::string garbageStream = "This is a garbage string with no valid CMap elements";
|
|
EXPECT_FALSE(cmap.parseCMapStream(garbageStream));
|
|
|
|
// Partially valid CMap - should recover parsed entries
|
|
std::string partialStream =
|
|
"begincmap\n"
|
|
"beginbfchar\n"
|
|
"<0001> <0041>\n" // Valid
|
|
"<0002> /invalid\n" // Invalid/missing dest (non-hex)
|
|
"<0003> <0043>\n" // Valid
|
|
"endbfchar\n"
|
|
"endcmap\n";
|
|
|
|
EXPECT_TRUE(cmap.parseCMapStream(partialStream));
|
|
EXPECT_EQ(cmap.decode(1), 0x0041);
|
|
EXPECT_EQ(cmap.decode(2), 0);
|
|
EXPECT_EQ(cmap.decode(3), 0x0043);
|
|
}
|
|
|
|
TEST(EncodingTest, FontLoaderWithEncoding) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run PDF font loader with encoding test.";
|
|
}
|
|
|
|
std::ifstream file(fontPath, std::ios::binary | std::ios::ate);
|
|
ASSERT_TRUE(file.is_open());
|
|
std::streamsize size = file.tellg();
|
|
file.seekg(0, std::ios::beg);
|
|
std::vector<uint8_t> buffer(size);
|
|
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
|
|
|
|
// Create predefined encoding (WinAnsi)
|
|
auto encoding = std::make_unique<pdfengine::fonts::pdf_fonts::PredefinedEncoding>(
|
|
pdfengine::fonts::pdf_fonts::SimpleEncodingType::WinAnsi
|
|
);
|
|
|
|
// Load font with encoding
|
|
auto pdfFont = pdfengine::fonts::pdf_fonts::FontLoader::loadTrueTypeFromMemory(
|
|
"Arial-With-Encoding", buffer, nullptr, std::move(encoding)
|
|
);
|
|
|
|
ASSERT_NE(pdfFont, nullptr);
|
|
EXPECT_EQ(pdfFont->getBaseFont(), "Arial-With-Encoding");
|
|
|
|
const auto* retrievedEncoding = pdfFont->getEncoding();
|
|
ASSERT_NE(retrievedEncoding, nullptr);
|
|
|
|
// Verify it translates Euro symbol exception properly
|
|
EXPECT_EQ(retrievedEncoding->decode(128), 0x20AC);
|
|
}
|
|
|
|
TEST(Type1FontTest, EmbeddedType1FontLoading) {
|
|
std::string fontPath = getSystemFontPath();
|
|
if (fontPath.empty()) {
|
|
GTEST_SKIP() << "No system font found to run embedded Type1 test.";
|
|
}
|
|
|
|
std::ifstream file(fontPath, std::ios::binary | std::ios::ate);
|
|
ASSERT_TRUE(file.is_open());
|
|
std::streamsize size = file.tellg();
|
|
file.seekg(0, std::ios::beg);
|
|
std::vector<uint8_t> buffer(size);
|
|
ASSERT_TRUE(file.read(reinterpret_cast<char*>(buffer.data()), size));
|
|
|
|
auto font = pdfengine::fonts::pdf_fonts::FontLoader::loadType1FromMemory("LegacyType1", buffer);
|
|
ASSERT_NE(font, nullptr);
|
|
EXPECT_EQ(font->getBaseFont(), "LegacyType1");
|
|
EXPECT_EQ(font->getType(), pdfengine::fonts::pdf_fonts::FontType::Type1);
|
|
EXPECT_TRUE(font->isEmbedded());
|
|
}
|
|
|
|
TEST(Type1FontTest, NonEmbeddedType1SystemFallback) {
|
|
auto fontHelv = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("Helvetica");
|
|
ASSERT_NE(fontHelv, nullptr);
|
|
EXPECT_EQ(fontHelv->getBaseFont(), "Helvetica");
|
|
EXPECT_EQ(fontHelv->getType(), pdfengine::fonts::pdf_fonts::FontType::Type1);
|
|
EXPECT_FALSE(fontHelv->isEmbedded());
|
|
|
|
auto fontTimes = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("Times-BoldItalic");
|
|
ASSERT_NE(fontTimes, nullptr);
|
|
EXPECT_EQ(fontTimes->getBaseFont(), "Times-BoldItalic");
|
|
EXPECT_FALSE(fontTimes->isEmbedded());
|
|
|
|
auto fontCourier = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("Courier-Oblique");
|
|
ASSERT_NE(fontCourier, nullptr);
|
|
EXPECT_EQ(fontCourier->getBaseFont(), "Courier-Oblique");
|
|
EXPECT_FALSE(fontCourier->isEmbedded());
|
|
|
|
auto fontUnknown = pdfengine::fonts::pdf_fonts::FontLoader::loadType1SystemFallback("UnknownLegacyFont");
|
|
ASSERT_NE(fontUnknown, nullptr);
|
|
EXPECT_EQ(fontUnknown->getBaseFont(), "UnknownLegacyFont");
|
|
EXPECT_FALSE(fontUnknown->isEmbedded());
|
|
}
|
|
|
|
TEST(CIDFontTest, CompositeFontInitializationAndTypes) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto desc = std::make_unique<FontDescriptor>();
|
|
desc->setFontName("SimSun-Descriptor");
|
|
|
|
// Test CIDFontType0
|
|
CIDFont font0("SimSun", FontType::CIDFontType0, false, std::move(desc));
|
|
EXPECT_EQ(font0.getBaseFont(), "SimSun");
|
|
EXPECT_EQ(font0.getType(), FontType::CIDFontType0);
|
|
EXPECT_FALSE(font0.isEmbedded());
|
|
EXPECT_EQ(font0.getDescriptor()->getFontName(), "SimSun-Descriptor");
|
|
|
|
// Test CIDFontType2
|
|
CIDFont font2("MS-Gothic", FontType::CIDFontType2, true);
|
|
EXPECT_EQ(font2.getBaseFont(), "MS-Gothic");
|
|
EXPECT_EQ(font2.getType(), FontType::CIDFontType2);
|
|
EXPECT_TRUE(font2.isEmbedded());
|
|
}
|
|
|
|
TEST(CIDFontTest, CIDToGIDTranslations) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
CIDFont font("SimSun", FontType::CIDFontType2, false);
|
|
|
|
// By default, it should be an identity mapping
|
|
EXPECT_TRUE(font.isIdentityMap());
|
|
EXPECT_EQ(font.mapCIDToGID(100), 100);
|
|
EXPECT_EQ(font.mapCIDToGID(5000), 5000);
|
|
|
|
// Set custom mapping
|
|
std::unordered_map<uint32_t, uint32_t> customMap = {
|
|
{10, 100},
|
|
{20, 200},
|
|
{30, 300}
|
|
};
|
|
font.setCIDToGIDMap(customMap);
|
|
EXPECT_FALSE(font.isIdentityMap());
|
|
EXPECT_EQ(font.mapCIDToGID(10), 100);
|
|
EXPECT_EQ(font.mapCIDToGID(20), 200);
|
|
EXPECT_EQ(font.mapCIDToGID(30), 300);
|
|
EXPECT_EQ(font.mapCIDToGID(40), 0); // Undefined / missing
|
|
|
|
// Set back to identity
|
|
font.setIdentityCIDToGIDMap();
|
|
EXPECT_TRUE(font.isIdentityMap());
|
|
EXPECT_EQ(font.mapCIDToGID(10), 10);
|
|
}
|
|
|
|
TEST(CIDFontTest, NonEmbeddedCIDFontSystemFallback) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
// Load Chinese Simplified CJK fallback
|
|
auto fontSimSun = FontLoader::loadCIDFontSystemFallback("SimSun", FontType::CIDFontType2);
|
|
ASSERT_NE(fontSimSun, nullptr);
|
|
EXPECT_EQ(fontSimSun->getBaseFont(), "SimSun");
|
|
EXPECT_EQ(fontSimSun->getType(), FontType::CIDFontType2);
|
|
EXPECT_FALSE(fontSimSun->isEmbedded());
|
|
|
|
// Load Japanese CJK fallback
|
|
auto fontGothic = FontLoader::loadCIDFontSystemFallback("HeiseiMin-W3", FontType::CIDFontType0);
|
|
ASSERT_NE(fontGothic, nullptr);
|
|
EXPECT_EQ(fontGothic->getBaseFont(), "HeiseiMin-W3");
|
|
EXPECT_EQ(fontGothic->getType(), FontType::CIDFontType0);
|
|
EXPECT_FALSE(fontGothic->isEmbedded());
|
|
|
|
// Verify it resolved to a valid system font that can render and shape text
|
|
// E.g. we can shape a basic CJK run with SimSun or MS Gothic (like Japanese characters)
|
|
HbShaper shaper;
|
|
auto glyphs = shaper.shapeRun("日本語漢字", fontGothic->getFontFace(), 16);
|
|
EXPECT_FALSE(glyphs.empty());
|
|
}
|
|
|
|
TEST(FontFallbackTest, SingletonInstanceIsUnique) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto& instance1 = FontFallback::getInstance();
|
|
auto& instance2 = FontFallback::getInstance();
|
|
EXPECT_EQ(&instance1, &instance2);
|
|
}
|
|
|
|
TEST(FontFallbackTest, StandardFontFallbacks) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto& fallback = FontFallback::getInstance();
|
|
|
|
// Helvetica resolves to its sans-serif substitute for the host platform.
|
|
std::string path1 = fallback.getFallbackFontPath("Helvetica");
|
|
EXPECT_FALSE(path1.empty());
|
|
EXPECT_TRUE(std::filesystem::exists(path1));
|
|
#if defined(_WIN32)
|
|
EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "liberationsans"));
|
|
#elif defined(__APPLE__)
|
|
// Arial may not be installed; the resolver then falls back to Helvetica.
|
|
EXPECT_TRUE(containsCI(path1, "arial") || containsCI(path1, "helvetica") ||
|
|
containsCI(path1, "liberationsans"));
|
|
#else
|
|
EXPECT_TRUE(containsCI(path1, "liberationsans") || containsCI(path1, "dejavusans"));
|
|
#endif
|
|
|
|
// Times resolves to its serif substitute for the host platform.
|
|
std::string path2 = fallback.getFallbackFontPath("Times-Roman");
|
|
EXPECT_FALSE(path2.empty());
|
|
EXPECT_TRUE(std::filesystem::exists(path2));
|
|
#if defined(_WIN32)
|
|
EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif"));
|
|
#elif defined(__APPLE__)
|
|
EXPECT_TRUE(containsCI(path2, "times") || containsCI(path2, "liberationserif"));
|
|
#else
|
|
EXPECT_TRUE(containsCI(path2, "liberationserif") || containsCI(path2, "dejavuserif"));
|
|
#endif
|
|
}
|
|
|
|
TEST(FontFallbackTest, StyleModifierResolutions) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto& fallback = FontFallback::getInstance();
|
|
|
|
// Bold Helvetica should map to a bold sans-serif substitute.
|
|
std::string pathBold = fallback.getFallbackFontPath("Helvetica", true, false);
|
|
EXPECT_FALSE(pathBold.empty());
|
|
EXPECT_TRUE(std::filesystem::exists(pathBold));
|
|
#if defined(_WIN32)
|
|
// Windows ships the styled variants, so assert the exact bold face.
|
|
EXPECT_TRUE(containsCI(pathBold, "arialbd") || containsCI(pathBold, "liberationsans-bold"));
|
|
#endif
|
|
|
|
// Bold-italic Times should map to a bold-italic serif substitute.
|
|
std::string pathBoldItalic = fallback.getFallbackFontPath("Times", true, true);
|
|
EXPECT_FALSE(pathBoldItalic.empty());
|
|
EXPECT_TRUE(std::filesystem::exists(pathBoldItalic));
|
|
#if defined(_WIN32)
|
|
EXPECT_TRUE(containsCI(pathBoldItalic, "timesbi") ||
|
|
containsCI(pathBoldItalic, "liberationserif-bolditalic"));
|
|
#endif
|
|
}
|
|
|
|
TEST(FontFallbackTest, CustomFallbackRegistration) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto& fallback = FontFallback::getInstance();
|
|
fallback.resetToDefaults();
|
|
|
|
// Lookup the default substitute path for Helvetica.
|
|
std::string standardPath = fallback.getFallbackFontPath("Helvetica");
|
|
|
|
// The resolver only returns an override whose file actually exists on disk,
|
|
// so register a real temp file rather than a hardcoded OS-specific path.
|
|
std::filesystem::path overrideFont =
|
|
std::filesystem::temp_directory_path() / "pdfengine_custom_fallback.ttf";
|
|
{ std::ofstream(overrideFont) << "stub-font"; }
|
|
|
|
fallback.registerFallback("helvetica", overrideFont.string());
|
|
|
|
std::string overridenPath = fallback.getFallbackFontPath("Helvetica");
|
|
EXPECT_EQ(overridenPath, overrideFont.string());
|
|
|
|
// Reset back to defaults and confirm the original substitute returns.
|
|
fallback.resetToDefaults();
|
|
std::string restoredPath = fallback.getFallbackFontPath("Helvetica");
|
|
EXPECT_EQ(restoredPath, standardPath);
|
|
|
|
std::filesystem::remove(overrideFont);
|
|
}
|
|
|
|
TEST(FontSubsetTest, SubsetTagParsingAndStripping) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
std::string subsetName = "KTJHQO+Arial";
|
|
|
|
EXPECT_TRUE(FontSubset::hasSubsetPrefix(subsetName));
|
|
EXPECT_EQ(FontSubset::getSubsetPrefix(subsetName), "KTJHQO");
|
|
EXPECT_EQ(FontSubset::stripSubsetPrefix(subsetName), "Arial");
|
|
|
|
// Standard naming (no prefix)
|
|
std::string normalName = "Arial";
|
|
EXPECT_FALSE(FontSubset::hasSubsetPrefix(normalName));
|
|
EXPECT_EQ(FontSubset::getSubsetPrefix(normalName), "");
|
|
EXPECT_EQ(FontSubset::stripSubsetPrefix(normalName), "Arial");
|
|
}
|
|
|
|
TEST(FontSubsetTest, PrefixFormatValidation) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
// Prefixes must be exactly 6 UPPERCASE letters followed by '+'
|
|
EXPECT_TRUE(FontSubset::hasSubsetPrefix("ABCDEF+Helvetica"));
|
|
|
|
// Lowercase should fail
|
|
EXPECT_FALSE(FontSubset::hasSubsetPrefix("abcDEF+Helvetica"));
|
|
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCdef+Helvetica"));
|
|
|
|
// Numbers/Special should fail
|
|
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABC123+Helvetica"));
|
|
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE_+Helvetica"));
|
|
|
|
// Length must be exactly 6 characters
|
|
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDE+Helvetica")); // 5 chars
|
|
EXPECT_FALSE(FontSubset::hasSubsetPrefix("ABCDEFG+Helvetica")); // 7 chars
|
|
}
|
|
|
|
TEST(FontSubsetTest, GIDRemappingTranslations) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
FontSubset subset("KTJHQO+Arial");
|
|
EXPECT_TRUE(subset.isSubset());
|
|
EXPECT_EQ(subset.getFullFontName(), "KTJHQO+Arial");
|
|
EXPECT_EQ(subset.getBaseFontName(), "Arial");
|
|
EXPECT_EQ(subset.getPrefix(), "KTJHQO");
|
|
|
|
// Default pass-through lookup
|
|
EXPECT_EQ(subset.mapSubsetToOriginal(5), 5);
|
|
EXPECT_FALSE(subset.hasGlyphMapping(5));
|
|
|
|
// Register glyph ID translations
|
|
subset.addGlyphMapping(1, 41); // 'A'
|
|
subset.addGlyphMapping(2, 42); // 'B'
|
|
subset.addGlyphMapping(3, 43); // 'C'
|
|
|
|
EXPECT_EQ(subset.getMappingCount(), 3u);
|
|
EXPECT_TRUE(subset.hasGlyphMapping(1));
|
|
EXPECT_TRUE(subset.hasGlyphMapping(2));
|
|
EXPECT_EQ(subset.mapSubsetToOriginal(1), 41);
|
|
EXPECT_EQ(subset.mapSubsetToOriginal(2), 42);
|
|
EXPECT_EQ(subset.mapSubsetToOriginal(3), 43);
|
|
EXPECT_EQ(subset.mapSubsetToOriginal(4), 4); // Falls back to standard GID
|
|
}
|
|
|
|
TEST(FontSubsetTest, CoreFontSubsettingIntegration) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
// Test TrueType Font integration
|
|
TrueTypeFont fontTT("KTJHQO+Arial", false);
|
|
const auto* ttSubset = fontTT.getSubsetInfo();
|
|
ASSERT_NE(ttSubset, nullptr);
|
|
EXPECT_TRUE(ttSubset->isSubset());
|
|
EXPECT_EQ(ttSubset->getBaseFontName(), "Arial");
|
|
EXPECT_EQ(ttSubset->getPrefix(), "KTJHQO");
|
|
|
|
// Test Type1 Font integration
|
|
Type1Font fontT1("SUBSET+Courier", false);
|
|
const auto* t1Subset = fontT1.getSubsetInfo();
|
|
ASSERT_NE(t1Subset, nullptr);
|
|
EXPECT_TRUE(t1Subset->isSubset());
|
|
EXPECT_EQ(t1Subset->getBaseFontName(), "Courier");
|
|
EXPECT_EQ(t1Subset->getPrefix(), "SUBSET");
|
|
|
|
// Test CID Font integration
|
|
CIDFont fontCID("CJKTAG+SimSun", FontType::CIDFontType2, false);
|
|
const auto* cidSubset = fontCID.getSubsetInfo();
|
|
ASSERT_NE(cidSubset, nullptr);
|
|
EXPECT_TRUE(cidSubset->isSubset());
|
|
EXPECT_EQ(cidSubset->getBaseFontName(), "SimSun");
|
|
EXPECT_EQ(cidSubset->getPrefix(), "CJKTAG");
|
|
}
|
|
|
|
TEST(TextExtractionLayerTest, SimpleCharacterDecoding) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto font = FontLoader::loadType1SystemFallback("Helvetica");
|
|
ASSERT_NE(font, nullptr);
|
|
|
|
EXPECT_EQ(font->decodeToUnicode(65), 65u);
|
|
EXPECT_EQ(font->decodeToUnicode(97), 97u);
|
|
EXPECT_EQ(font->decodeToUnicode(48), 48u);
|
|
}
|
|
|
|
TEST(TextExtractionLayerTest, EncodingAndToUnicodeDecoding) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto baseEncoding = std::make_unique<PredefinedEncoding>(SimpleEncodingType::WinAnsi);
|
|
auto customEnc = std::make_unique<CustomEncoding>(std::move(baseEncoding));
|
|
customEnc->addDifference(128, "euro");
|
|
|
|
auto font = FontLoader::loadType1SystemFallback("Helvetica", nullptr, std::move(customEnc));
|
|
ASSERT_NE(font, nullptr);
|
|
|
|
EXPECT_EQ(font->decodeToUnicode(128), 0x20ACu);
|
|
|
|
auto cmap = std::make_unique<ToUnicodeCMap>();
|
|
cmap->addMapping(1, 0x0041);
|
|
cmap->addMapping(2, 0x0042);
|
|
cmap->addMapping(3, 0x20AC);
|
|
|
|
auto fontCMap = FontLoader::loadType1SystemFallback("Helvetica", nullptr, std::move(cmap));
|
|
ASSERT_NE(fontCMap, nullptr);
|
|
EXPECT_EQ(fontCMap->decodeToUnicode(1), 0x0041u);
|
|
EXPECT_EQ(fontCMap->decodeToUnicode(2), 0x0042u);
|
|
EXPECT_EQ(fontCMap->decodeToUnicode(3), 0x20ACu);
|
|
}
|
|
|
|
TEST(TextExtractionLayerTest, SubsetFontTextExtraction) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto font = FontLoader::loadType1SystemFallback("KTJHQO+Helvetica");
|
|
ASSERT_NE(font, nullptr);
|
|
|
|
const auto* subsetConst = font->getSubsetInfo();
|
|
ASSERT_NE(subsetConst, nullptr);
|
|
auto* subset = const_cast<FontSubset*>(subsetConst);
|
|
|
|
FT_Face face = font->getFontFace().getFace();
|
|
ASSERT_NE(face, nullptr);
|
|
FT_UInt originalGid = FT_Get_Char_Index(face, 'A');
|
|
ASSERT_GT(originalGid, 0u);
|
|
|
|
subset->addGlyphMapping(5, originalGid);
|
|
|
|
EXPECT_EQ(font->decodeToUnicode(5), 65u);
|
|
}
|
|
|
|
TEST(TextExtractionLayerTest, CIDFontTextExtraction) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto font = FontLoader::loadCIDFontSystemFallback("SimSun", FontType::CIDFontType2);
|
|
ASSERT_NE(font, nullptr);
|
|
|
|
CIDFont* cidFont = dynamic_cast<CIDFont*>(font.get());
|
|
ASSERT_NE(cidFont, nullptr);
|
|
|
|
FT_Face face = cidFont->getFontFace().getFace();
|
|
ASSERT_NE(face, nullptr);
|
|
FT_UInt gid65 = 65;
|
|
FT_ULong expectedChar65 = 0;
|
|
|
|
FT_UInt gindex;
|
|
FT_ULong charcode = FT_Get_First_Char(face, &gindex);
|
|
while (gindex != 0) {
|
|
if (gindex == gid65) {
|
|
expectedChar65 = charcode;
|
|
break;
|
|
}
|
|
charcode = FT_Get_Next_Char(face, charcode, &gindex);
|
|
}
|
|
|
|
if (expectedChar65 != 0) {
|
|
EXPECT_EQ(cidFont->decodeToUnicode(65), expectedChar65);
|
|
} else {
|
|
EXPECT_EQ(cidFont->decodeToUnicode(65), 65u);
|
|
}
|
|
|
|
FT_UInt gidA = FT_Get_Char_Index(face, 'A');
|
|
if (gidA > 0) {
|
|
std::unordered_map<uint32_t, uint32_t> cidToGid = {
|
|
{500, gidA}
|
|
};
|
|
cidFont->setCIDToGIDMap(cidToGid);
|
|
|
|
EXPECT_EQ(cidFont->decodeToUnicode(500), 65u);
|
|
}
|
|
}
|
|
|
|
TEST(TextExtractionLayerTest, StringUtf8Conversion) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto font = FontLoader::loadType1SystemFallback("Helvetica");
|
|
ASSERT_NE(font, nullptr);
|
|
|
|
std::vector<uint32_t> codes = {65, 66, 67};
|
|
EXPECT_EQ(font->decodeStringToUnicode(codes), "ABC");
|
|
|
|
auto cmap = std::make_unique<ToUnicodeCMap>();
|
|
cmap->addMapping(10, 0x65E5);
|
|
cmap->addMapping(11, 0x672C);
|
|
cmap->addMapping(12, 0x8A9E);
|
|
|
|
auto fontCMap = FontLoader::loadType1SystemFallback("Helvetica", nullptr, std::move(cmap));
|
|
ASSERT_NE(fontCMap, nullptr);
|
|
|
|
std::vector<uint32_t> cjkCodes = {10, 11, 12};
|
|
std::string decodedCjk = fontCMap->decodeStringToUnicode(cjkCodes);
|
|
EXPECT_EQ(decodedCjk, "日本語");
|
|
}
|
|
|
|
TEST(FontSubstitutionAndWidthsTest, WidthMatchingAndSubstitutionVerification) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
// Load non-embedded Helvetica font, which triggers substitution
|
|
auto font = FontLoader::loadType1SystemFallback("Helvetica");
|
|
ASSERT_NE(font, nullptr);
|
|
|
|
// Confirm that the font does not have widths set yet
|
|
EXPECT_FALSE(font->hasWidths());
|
|
|
|
// Original widths for character codes 65 to 68 ('A' to 'D') from PDF /Widths array
|
|
// E.g., 'A'=600, 'B'=500, 'C'=550, 'D'=400
|
|
std::vector<double> pdfWidths = { 600.0, 500.0, 550.0, 400.0 };
|
|
font->setWidths(65, 68, pdfWidths);
|
|
|
|
EXPECT_TRUE(font->hasWidths());
|
|
|
|
// Font size context: 12.0
|
|
double fontSize = 12.0;
|
|
|
|
// Expected widths = (W / 1000.0) * fontSize
|
|
double expectedWidthA = (600.0 / 1000.0) * fontSize; // 7.2
|
|
double expectedWidthB = (500.0 / 1000.0) * fontSize; // 6.0
|
|
double expectedWidthC = (550.0 / 1000.0) * fontSize; // 6.6
|
|
double expectedWidthD = (400.0 / 1000.0) * fontSize; // 4.8
|
|
|
|
// Verify mapped widths match original PDF widths with 0% error (well under 5%)
|
|
EXPECT_NEAR(font->getCharWidth(65, fontSize), expectedWidthA, 1e-5);
|
|
EXPECT_NEAR(font->getCharWidth(66, fontSize), expectedWidthB, 1e-5);
|
|
EXPECT_NEAR(font->getCharWidth(67, fontSize), expectedWidthC, 1e-5);
|
|
EXPECT_NEAR(font->getCharWidth(68, fontSize), expectedWidthD, 1e-5);
|
|
|
|
// Verify out-of-range character falls back safely (returns 0.0 or descriptor missing width)
|
|
EXPECT_EQ(font->getCharWidth(999, fontSize), 0.0);
|
|
}
|
|
|
|
TEST(CIDAdvancedMappingTest, IdentityVSupport) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
PredefinedEncoding identityV(SimpleEncodingType::Identity_V);
|
|
EXPECT_EQ(identityV.getType(), SimpleEncodingType::Identity_V);
|
|
EXPECT_EQ(identityV.decode(65), 65);
|
|
EXPECT_EQ(identityV.decode(1000), 1000);
|
|
}
|
|
|
|
TEST(CIDAdvancedMappingTest, VerticalMetricsResolution) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
auto font = FontLoader::loadType1SystemFallback("Helvetica");
|
|
ASSERT_NE(font, nullptr);
|
|
|
|
// Default vertical advance metrics: 1.0em = font size context
|
|
double fontSize = 12.0;
|
|
EXPECT_EQ(font->isVertical(), false);
|
|
EXPECT_EQ(font->getCharHeight(65, fontSize), fontSize);
|
|
|
|
// Turn vertical metrics ON and verify custom heights
|
|
font->setVertical(true);
|
|
EXPECT_EQ(font->isVertical(), true);
|
|
|
|
std::vector<double> verticalAdvances = { 1000.0, 800.0, 900.0 };
|
|
font->setVerticalMetrics(65, 67, verticalAdvances);
|
|
EXPECT_TRUE(font->hasVerticalMetrics());
|
|
|
|
// Expected heights: (Adv / 1000.0) * fontSize
|
|
EXPECT_NEAR(font->getCharHeight(65, fontSize), 12.0, 1e-5); // (1000/1000) * 12
|
|
EXPECT_NEAR(font->getCharHeight(66, fontSize), 9.6, 1e-5); // (800/1000) * 12
|
|
EXPECT_NEAR(font->getCharHeight(67, fontSize), 10.8, 1e-5); // (900/1000) * 12
|
|
EXPECT_NEAR(font->getCharHeight(999, fontSize), 12.0, 1e-5); // Fallback to 12.0
|
|
}
|
|
|
|
TEST(CIDAdvancedMappingTest, CjkCollectionResolutionDB) {
|
|
using namespace pdfengine::fonts::pdf_fonts;
|
|
|
|
// Standard Adobe-Japan1 Hiragana CIDs
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1010), 0x3041); // Hiragana 'ぁ'
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1092), 0x3093); // Hiragana 'ん'
|
|
|
|
// Standard Adobe-Japan1 Katakana CIDs
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1125), 0x30A1); // Katakana 'ァ'
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1205), 0x30F6); // Katakana 'ヶ'
|
|
|
|
// Core Kanji
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-Japan1", 1206), 0x4E00); // Kanji '一'
|
|
|
|
// GB1 Chinese simplified ideographic marks
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 1), 0x3000); // space
|
|
EXPECT_EQ(CjkCollectionDB::resolveCID("Adobe-GB1", 2), 0x3001); // comma
|
|
}
|
|
|
|
TEST(CIDAdvancedMappingTest, HarfBuzzVerticalShapingSignature) {
|
|
using namespace pdfengine::fonts;
|
|
|
|
// Validate WritingMode configurations
|
|
EXPECT_EQ(static_cast<int>(HbShaper::WritingMode::Horizontal), 0);
|
|
EXPECT_EQ(static_cast<int>(HbShaper::WritingMode::Vertical), 1);
|
|
}
|
|
|
|
} // namespace pdfengine::fonts
|
|
|