Files
pdf/engine/tests/content_builder_test.cpp
T

234 lines
8.2 KiB
C++
Raw Normal View History

2026-06-16 19:06:48 +05:30
#include <gtest/gtest.h>
#include <pdfengine/token.hpp>
#include <pdfengine/ast.hpp>
#include <pdfengine/content_object.hpp>
#include "../src/parser/lexer.hpp"
#include "../src/parser/parser.hpp"
#include "../src/parser/content_builder.hpp"
#include "../src/qpdf/qpdf_extractor.hpp"
#include <filesystem>
#ifndef TEST_CORPUS_DIR
#define TEST_CORPUS_DIR "../../corpus"
#endif
using namespace pdfengine;
2026-06-22 10:17:51 +05:30
namespace {
std::vector<std::unique_ptr<ContentObject>> 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<ContentObject>& object) {
EXPECT_EQ(object->getType(), ContentObjectType::Path);
if (object->getType() != ContentObjectType::Path) {
return nullptr;
}
return static_cast<const PathObject*>(object.get());
}
} // namespace
2026-06-16 19:06:48 +05:30
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<TextObject*>(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<TextObject*>(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<TextObject*>(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);
}
2026-06-22 10:17:51 +05:30
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);
}
2026-06-16 19:06:48 +05:30
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<TextObject*>(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);
}