#include #include #include #include #include "../src/parser/lexer.hpp" #include "../src/parser/parser.hpp" #include "../src/parser/content_builder.hpp" #include "../src/qpdf/qpdf_extractor.hpp" #include #ifndef TEST_CORPUS_DIR #define TEST_CORPUS_DIR "../../corpus" #endif using namespace pdfengine; namespace { std::vector> buildObjects(const std::string& content) { Lexer lexer(content); auto tokens = lexer.tokenize(); ContentParser parser(tokens); ContentBuilder builder; return builder.build(parser.parse()); } const PathObject* requirePathObject(const std::unique_ptr& object) { EXPECT_EQ(object->getType(), ContentObjectType::Path); if (object->getType() != ContentObjectType::Path) { return nullptr; } return static_cast(object.get()); } } // namespace TEST(ContentBuilderTest, SimpleTextState) { Lexer lexer("10 20 Td /F1 12 Tf (Hello) Tj"); auto tokens = lexer.tokenize(); ContentParser parser(tokens); ContentBuilder builder; auto ops = parser.parse(); auto objects = builder.build(ops); ASSERT_EQ(objects.size(), 1); EXPECT_EQ(objects[0]->getType(), ContentObjectType::Text); auto* textObj = static_cast(objects[0].get()); EXPECT_EQ(textObj->text, "Hello"); EXPECT_EQ(textObj->fontName, "F1"); EXPECT_DOUBLE_EQ(textObj->fontSize, 12.0); EXPECT_DOUBLE_EQ(textObj->tm[4], 10.0); EXPECT_DOUBLE_EQ(textObj->tm[5], 20.0); } TEST(ContentBuilderTest, KerningArrayTJ) { Lexer lexer("[ (He) 120 (llo) -600 (World) ] TJ"); auto tokens = lexer.tokenize(); ContentParser parser(tokens); ContentBuilder builder; auto ops = parser.parse(); auto objects = builder.build(ops); ASSERT_EQ(objects.size(), 1); EXPECT_EQ(objects[0]->getType(), ContentObjectType::Text); auto* textObj = static_cast(objects[0].get()); // -600 is less than -500, so it inserts a space EXPECT_EQ(textObj->text, "Hello World"); } TEST(ContentBuilderTest, RotatedTextMatrix) { Lexer lexer("0 1 -1 0 100 200 Tm (Rotated) Tj"); auto tokens = lexer.tokenize(); ContentParser parser(tokens); ContentBuilder builder; auto ops = parser.parse(); auto objects = builder.build(ops); ASSERT_EQ(objects.size(), 1); EXPECT_EQ(objects[0]->getType(), ContentObjectType::Text); auto* textObj = static_cast(objects[0].get()); EXPECT_EQ(textObj->text, "Rotated"); EXPECT_DOUBLE_EQ(textObj->tm[0], 0.0); EXPECT_DOUBLE_EQ(textObj->tm[1], 1.0); EXPECT_DOUBLE_EQ(textObj->tm[2], -1.0); EXPECT_DOUBLE_EQ(textObj->tm[3], 0.0); EXPECT_DOUBLE_EQ(textObj->tm[4], 100.0); EXPECT_DOUBLE_EQ(textObj->tm[5], 200.0); } TEST(ContentBuilderTest, StrokePathFromMoveAndLine) { auto objects = buildObjects("10 20 m 30 40 l S"); ASSERT_EQ(objects.size(), 1); const auto* pathObj = requirePathObject(objects[0]); ASSERT_NE(pathObj, nullptr); EXPECT_EQ(pathObj->paintOp, PathPaintOp::Stroke); const auto& segments = pathObj->path.segments(); ASSERT_EQ(segments.size(), 2); EXPECT_EQ(segments[0].verb, Path::Verb::MoveTo); EXPECT_FLOAT_EQ(segments[0].points[0].x, 10.0f); EXPECT_FLOAT_EQ(segments[0].points[0].y, 20.0f); EXPECT_EQ(segments[1].verb, Path::Verb::LineTo); EXPECT_FLOAT_EQ(segments[1].points[0].x, 30.0f); EXPECT_FLOAT_EQ(segments[1].points[0].y, 40.0f); } TEST(ContentBuilderTest, FillRectanglePath) { auto objects = buildObjects("5 6 7 8 re f"); ASSERT_EQ(objects.size(), 1); const auto* pathObj = requirePathObject(objects[0]); ASSERT_NE(pathObj, nullptr); EXPECT_EQ(pathObj->paintOp, PathPaintOp::Fill); const auto& segments = pathObj->path.segments(); ASSERT_EQ(segments.size(), 5); EXPECT_EQ(segments[0].verb, Path::Verb::MoveTo); EXPECT_FLOAT_EQ(segments[0].points[0].x, 5.0f); EXPECT_FLOAT_EQ(segments[0].points[0].y, 6.0f); EXPECT_EQ(segments[1].verb, Path::Verb::LineTo); EXPECT_FLOAT_EQ(segments[1].points[0].x, 12.0f); EXPECT_FLOAT_EQ(segments[1].points[0].y, 6.0f); EXPECT_EQ(segments[2].verb, Path::Verb::LineTo); EXPECT_FLOAT_EQ(segments[2].points[0].x, 12.0f); EXPECT_FLOAT_EQ(segments[2].points[0].y, 14.0f); EXPECT_EQ(segments[3].verb, Path::Verb::LineTo); EXPECT_FLOAT_EQ(segments[3].points[0].x, 5.0f); EXPECT_FLOAT_EQ(segments[3].points[0].y, 14.0f); EXPECT_EQ(segments[4].verb, Path::Verb::Close); } TEST(ContentBuilderTest, FillStrokeCubicPath) { auto objects = buildObjects("1 2 m 3 4 5 6 7 8 c B"); ASSERT_EQ(objects.size(), 1); const auto* pathObj = requirePathObject(objects[0]); ASSERT_NE(pathObj, nullptr); EXPECT_EQ(pathObj->paintOp, PathPaintOp::FillStroke); const auto& segments = pathObj->path.segments(); ASSERT_EQ(segments.size(), 2); EXPECT_EQ(segments[0].verb, Path::Verb::MoveTo); EXPECT_EQ(segments[1].verb, Path::Verb::CubicBezierTo); EXPECT_FLOAT_EQ(segments[1].points[0].x, 3.0f); EXPECT_FLOAT_EQ(segments[1].points[0].y, 4.0f); EXPECT_FLOAT_EQ(segments[1].points[1].x, 5.0f); EXPECT_FLOAT_EQ(segments[1].points[1].y, 6.0f); EXPECT_FLOAT_EQ(segments[1].points[2].x, 7.0f); EXPECT_FLOAT_EQ(segments[1].points[2].y, 8.0f); } TEST(ContentBuilderTest, PathPaintClearsCurrentPath) { auto objects = buildObjects("0 0 m 10 10 l S 20 20 m 30 30 l f"); ASSERT_EQ(objects.size(), 2); const auto* stroke = requirePathObject(objects[0]); const auto* fill = requirePathObject(objects[1]); ASSERT_NE(stroke, nullptr); ASSERT_NE(fill, nullptr); EXPECT_EQ(stroke->paintOp, PathPaintOp::Stroke); EXPECT_EQ(fill->paintOp, PathPaintOp::Fill); ASSERT_EQ(stroke->path.segments().size(), 2); ASSERT_EQ(fill->path.segments().size(), 2); EXPECT_FLOAT_EQ(fill->path.segments()[0].points[0].x, 20.0f); EXPECT_FLOAT_EQ(fill->path.segments()[0].points[0].y, 20.0f); } TEST(ContentBuilderTest, PathCapturesCurrentTransform) { auto objects = buildObjects("q 2 0 0 3 10 20 cm 1 2 3 4 re f Q"); ASSERT_EQ(objects.size(), 1); const auto* pathObj = requirePathObject(objects[0]); ASSERT_NE(pathObj, nullptr); EXPECT_FLOAT_EQ(pathObj->transform.a, 2.0f); EXPECT_FLOAT_EQ(pathObj->transform.b, 0.0f); EXPECT_FLOAT_EQ(pathObj->transform.c, 0.0f); EXPECT_FLOAT_EQ(pathObj->transform.d, 3.0f); EXPECT_FLOAT_EQ(pathObj->transform.e, 10.0f); EXPECT_FLOAT_EQ(pathObj->transform.f, 20.0f); } TEST(ContentBuilderTest, IntegrationHelloWorld) { pdfengine::qpdf_layer::QpdfExtractor extractor; std::filesystem::path path = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world.pdf"; auto stream = extractor.extractPageStream(path.string(), 0); ASSERT_TRUE(stream.has_value()); Lexer lexer(stream->decodedContent); auto tokens = lexer.tokenize(); ContentParser parser(tokens); ContentBuilder builder; auto ops = parser.parse(); auto objects = builder.build(ops); // hello_world.pdf has two text lines: "Hello, world!" and "Goodbye, world!" int textObjectCount = 0; bool foundHello = false; bool foundGoodbye = false; for (const auto& obj : objects) { if (obj->getType() == ContentObjectType::Text) { textObjectCount++; auto* textObj = static_cast(obj.get()); if (textObj->text == "Hello, world!") { foundHello = true; EXPECT_EQ(textObj->fontName, "F1"); EXPECT_DOUBLE_EQ(textObj->fontSize, 12.0); EXPECT_DOUBLE_EQ(textObj->tm[4], 20.0); EXPECT_DOUBLE_EQ(textObj->tm[5], 50.0); } else if (textObj->text == "Goodbye, world!") { foundGoodbye = true; EXPECT_EQ(textObj->fontName, "F2"); EXPECT_DOUBLE_EQ(textObj->fontSize, 16.0); EXPECT_DOUBLE_EQ(textObj->tm[4], 20.0); EXPECT_DOUBLE_EQ(textObj->tm[5], 100.0); } } } EXPECT_GE(textObjectCount, 2); EXPECT_TRUE(foundHello); EXPECT_TRUE(foundGoodbye); }