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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:18:47 +05:30
|
|
|
}
|
2026-06-22 10:17:51 +05:30
|
|
|
|
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());
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|