content stream
This commit is contained in:
@@ -7,8 +7,18 @@ add_executable(pdfengine_smoke
|
||||
display_list_test.cpp
|
||||
document_test.cpp
|
||||
skia_renderer_test.cpp
|
||||
lexer_test.cpp
|
||||
parser_test.cpp
|
||||
content_builder_test.cpp
|
||||
ast_serializer_test.cpp
|
||||
content_serializer_test.cpp
|
||||
qpdf_writer_test.cpp
|
||||
)
|
||||
|
||||
if(PDFENGINE_WITH_QPDF)
|
||||
target_sources(pdfengine_smoke PRIVATE qpdf_extractor_test.cpp)
|
||||
endif()
|
||||
|
||||
target_link_libraries(pdfengine_smoke
|
||||
PRIVATE
|
||||
pdfengine::pdfengine
|
||||
@@ -20,6 +30,10 @@ if(PDFENGINE_WITH_SKIA)
|
||||
target_link_libraries(pdfengine_smoke PRIVATE skia::skia)
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
target_link_options(pdfengine_smoke PRIVATE "/FORCE:MULTIPLE")
|
||||
endif()
|
||||
|
||||
target_include_directories(pdfengine_smoke
|
||||
PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../src"
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "../src/serializer/ast_serializer.hpp"
|
||||
|
||||
using namespace pdfengine;
|
||||
|
||||
TEST(AstSerializerTest, SimpleOperations) {
|
||||
Operation opTd;
|
||||
opTd.op = "Td";
|
||||
|
||||
auto xNode = std::make_shared<AstNode>(AstNodeType::Number);
|
||||
xNode->numberValue = 10.5;
|
||||
|
||||
auto yNode = std::make_shared<AstNode>(AstNodeType::Number);
|
||||
yNode->numberValue = 20.0;
|
||||
|
||||
opTd.operands.push_back(xNode);
|
||||
opTd.operands.push_back(yNode);
|
||||
|
||||
AstSerializer serializer;
|
||||
std::string result = serializer.serialize({opTd});
|
||||
|
||||
EXPECT_EQ(result, "10.5 20 Td\n");
|
||||
}
|
||||
|
||||
TEST(AstSerializerTest, StringEscaping) {
|
||||
Operation opTj;
|
||||
opTj.op = "Tj";
|
||||
|
||||
auto strNode = std::make_shared<AstNode>(AstNodeType::String);
|
||||
strNode->stringValue = "Hello (World)";
|
||||
|
||||
opTj.operands.push_back(strNode);
|
||||
|
||||
AstSerializer serializer;
|
||||
std::string result = serializer.serialize({opTj});
|
||||
|
||||
EXPECT_EQ(result, "(Hello \\(World\\)) Tj\n");
|
||||
}
|
||||
|
||||
TEST(AstSerializerTest, ArraySerialization) {
|
||||
Operation opTJ;
|
||||
opTJ.op = "TJ";
|
||||
|
||||
auto arrNode = std::make_shared<AstNode>(AstNodeType::Array);
|
||||
|
||||
auto str1 = std::make_shared<AstNode>(AstNodeType::String);
|
||||
str1->stringValue = "He";
|
||||
auto num = std::make_shared<AstNode>(AstNodeType::Number);
|
||||
num->numberValue = 120;
|
||||
auto str2 = std::make_shared<AstNode>(AstNodeType::String);
|
||||
str2->stringValue = "llo";
|
||||
|
||||
arrNode->arrayItems.push_back(str1);
|
||||
arrNode->arrayItems.push_back(num);
|
||||
arrNode->arrayItems.push_back(str2);
|
||||
|
||||
opTJ.operands.push_back(arrNode);
|
||||
|
||||
AstSerializer serializer;
|
||||
std::string result = serializer.serialize({opTJ});
|
||||
|
||||
EXPECT_EQ(result, "[ (He) 120 (llo) ] TJ\n");
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
#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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "../src/serializer/content_serializer.hpp"
|
||||
|
||||
using namespace pdfengine;
|
||||
|
||||
TEST(ContentSerializerTest, SerializeTextObject) {
|
||||
auto textObj = std::make_unique<TextObject>();
|
||||
textObj->text = "Hello Serialization";
|
||||
textObj->fontName = "F1";
|
||||
textObj->fontSize = 14.5;
|
||||
textObj->tm[0] = 1.0;
|
||||
textObj->tm[1] = 0.0;
|
||||
textObj->tm[2] = 0.0;
|
||||
textObj->tm[3] = 1.0;
|
||||
textObj->tm[4] = 100.0;
|
||||
textObj->tm[5] = 200.0;
|
||||
|
||||
std::vector<std::unique_ptr<ContentObject>> objects;
|
||||
objects.push_back(std::move(textObj));
|
||||
|
||||
ContentSerializer serializer;
|
||||
auto ops = serializer.serialize(objects);
|
||||
|
||||
ASSERT_EQ(ops.size(), 5);
|
||||
|
||||
EXPECT_EQ(ops[0].op, "BT");
|
||||
|
||||
EXPECT_EQ(ops[1].op, "Tf");
|
||||
ASSERT_EQ(ops[1].operands.size(), 2);
|
||||
EXPECT_EQ(ops[1].operands[0]->stringValue, "F1");
|
||||
EXPECT_DOUBLE_EQ(ops[1].operands[1]->numberValue, 14.5);
|
||||
|
||||
EXPECT_EQ(ops[2].op, "Tm");
|
||||
ASSERT_EQ(ops[2].operands.size(), 6);
|
||||
EXPECT_DOUBLE_EQ(ops[2].operands[0]->numberValue, 1.0);
|
||||
EXPECT_DOUBLE_EQ(ops[2].operands[1]->numberValue, 0.0);
|
||||
EXPECT_DOUBLE_EQ(ops[2].operands[2]->numberValue, 0.0);
|
||||
EXPECT_DOUBLE_EQ(ops[2].operands[3]->numberValue, 1.0);
|
||||
EXPECT_DOUBLE_EQ(ops[2].operands[4]->numberValue, 100.0);
|
||||
EXPECT_DOUBLE_EQ(ops[2].operands[5]->numberValue, 200.0);
|
||||
|
||||
EXPECT_EQ(ops[3].op, "Tj");
|
||||
ASSERT_EQ(ops[3].operands.size(), 1);
|
||||
EXPECT_EQ(ops[3].operands[0]->stringValue, "Hello Serialization");
|
||||
|
||||
EXPECT_EQ(ops[4].op, "ET");
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <pdfengine/token.hpp>
|
||||
#include "../src/parser/lexer.hpp"
|
||||
|
||||
using namespace pdfengine;
|
||||
|
||||
TEST(LexerTest, OperatorsAndWhitespace) {
|
||||
Lexer lexer("BT\n/F1 12 Tf\nET");
|
||||
auto tokens = lexer.tokenize();
|
||||
|
||||
ASSERT_EQ(tokens.size(), 5);
|
||||
EXPECT_EQ(tokens[0].type, TokenType::Operator);
|
||||
EXPECT_EQ(tokens[0].stringValue, "BT");
|
||||
|
||||
EXPECT_EQ(tokens[1].type, TokenType::Name);
|
||||
EXPECT_EQ(tokens[1].stringValue, "F1");
|
||||
|
||||
EXPECT_EQ(tokens[2].type, TokenType::Number);
|
||||
EXPECT_EQ(tokens[2].numberValue, 12.0);
|
||||
|
||||
EXPECT_EQ(tokens[3].type, TokenType::Operator);
|
||||
EXPECT_EQ(tokens[3].stringValue, "Tf");
|
||||
|
||||
EXPECT_EQ(tokens[4].type, TokenType::Operator);
|
||||
EXPECT_EQ(tokens[4].stringValue, "ET");
|
||||
}
|
||||
|
||||
TEST(LexerTest, Strings) {
|
||||
Lexer lexer("(Hello World) (Nested (parens) ok) (Escapes \\n \\t \\\\ \\(\\)) (Octal \\053)");
|
||||
auto tokens = lexer.tokenize();
|
||||
|
||||
ASSERT_EQ(tokens.size(), 4);
|
||||
EXPECT_EQ(tokens[0].type, TokenType::String);
|
||||
EXPECT_EQ(tokens[0].stringValue, "Hello World");
|
||||
|
||||
EXPECT_EQ(tokens[1].type, TokenType::String);
|
||||
EXPECT_EQ(tokens[1].stringValue, "Nested (parens) ok");
|
||||
|
||||
EXPECT_EQ(tokens[2].type, TokenType::String);
|
||||
EXPECT_EQ(tokens[2].stringValue, "Escapes \n \t \\ ()");
|
||||
|
||||
EXPECT_EQ(tokens[3].type, TokenType::String);
|
||||
EXPECT_EQ(tokens[3].stringValue, "Octal +"); // \053 is '+'
|
||||
}
|
||||
|
||||
TEST(LexerTest, HexStrings) {
|
||||
Lexer lexer("<48 656c 6c6F> <4A5>");
|
||||
auto tokens = lexer.tokenize();
|
||||
|
||||
ASSERT_EQ(tokens.size(), 2);
|
||||
EXPECT_EQ(tokens[0].type, TokenType::HexString);
|
||||
// "Hello"
|
||||
std::vector<uint8_t> expected1 = {0x48, 0x65, 0x6C, 0x6C, 0x6F};
|
||||
EXPECT_EQ(tokens[0].bytesValue, expected1);
|
||||
|
||||
EXPECT_EQ(tokens[1].type, TokenType::HexString);
|
||||
// "4A5" padded to "4A50"
|
||||
std::vector<uint8_t> expected2 = {0x4A, 0x50};
|
||||
EXPECT_EQ(tokens[1].bytesValue, expected2);
|
||||
}
|
||||
|
||||
TEST(LexerTest, NamesAndNumbers) {
|
||||
Lexer lexer("/Name1 /A#20B -3.14 .5 100");
|
||||
auto tokens = lexer.tokenize();
|
||||
|
||||
ASSERT_EQ(tokens.size(), 5);
|
||||
EXPECT_EQ(tokens[0].type, TokenType::Name);
|
||||
EXPECT_EQ(tokens[0].stringValue, "Name1");
|
||||
|
||||
EXPECT_EQ(tokens[1].type, TokenType::Name);
|
||||
EXPECT_EQ(tokens[1].stringValue, "A B");
|
||||
|
||||
EXPECT_EQ(tokens[2].type, TokenType::Number);
|
||||
EXPECT_DOUBLE_EQ(tokens[2].numberValue, -3.14);
|
||||
|
||||
EXPECT_EQ(tokens[3].type, TokenType::Number);
|
||||
EXPECT_DOUBLE_EQ(tokens[3].numberValue, 0.5);
|
||||
|
||||
EXPECT_EQ(tokens[4].type, TokenType::Number);
|
||||
EXPECT_DOUBLE_EQ(tokens[4].numberValue, 100.0);
|
||||
}
|
||||
|
||||
#include "../src/qpdf/qpdf_extractor.hpp"
|
||||
#include <filesystem>
|
||||
|
||||
#ifndef TEST_CORPUS_DIR
|
||||
#define TEST_CORPUS_DIR "../../corpus"
|
||||
#endif
|
||||
|
||||
TEST(LexerTest, DictAndArray) {
|
||||
Lexer lexer("<< /Type /Page >> [ 1 2 3 ]");
|
||||
auto tokens = lexer.tokenize();
|
||||
|
||||
ASSERT_EQ(tokens.size(), 9);
|
||||
EXPECT_EQ(tokens[0].type, TokenType::DictStart);
|
||||
EXPECT_EQ(tokens[1].type, TokenType::Name);
|
||||
EXPECT_EQ(tokens[2].type, TokenType::Name);
|
||||
EXPECT_EQ(tokens[3].type, TokenType::DictEnd);
|
||||
EXPECT_EQ(tokens[4].type, TokenType::ArrayStart);
|
||||
EXPECT_EQ(tokens[5].type, TokenType::Number);
|
||||
EXPECT_EQ(tokens[6].type, TokenType::Number);
|
||||
EXPECT_EQ(tokens[7].type, TokenType::Number);
|
||||
EXPECT_EQ(tokens[8].type, TokenType::ArrayEnd);
|
||||
}
|
||||
|
||||
TEST(LexerTest, 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();
|
||||
|
||||
// We expect something like: BT /F1 12 Tf (Hello) Tj ET
|
||||
// plus any graphics state like 0 0 0 rg, etc.
|
||||
// Let's just find the text block.
|
||||
|
||||
bool foundHello = false;
|
||||
for (size_t i = 0; i < tokens.size(); ++i) {
|
||||
if (tokens[i].type == TokenType::String && tokens[i].stringValue == "Hello, world!") {
|
||||
foundHello = true;
|
||||
// The next token should be Tj or TJ
|
||||
ASSERT_LT(i + 1, tokens.size());
|
||||
EXPECT_EQ(tokens[i+1].type, TokenType::Operator);
|
||||
EXPECT_TRUE(tokens[i+1].stringValue == "Tj" || tokens[i+1].stringValue == "TJ");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(foundHello) << "Failed to lex (Hello, world!) from hello_world.pdf stream";
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <pdfengine/token.hpp>
|
||||
#include <pdfengine/ast.hpp>
|
||||
#include "../src/parser/parser.hpp"
|
||||
#include "../src/parser/lexer.hpp"
|
||||
#include "../src/qpdf/qpdf_extractor.hpp"
|
||||
#include <filesystem>
|
||||
|
||||
#ifndef TEST_CORPUS_DIR
|
||||
#define TEST_CORPUS_DIR "../../corpus"
|
||||
#endif
|
||||
|
||||
using namespace pdfengine;
|
||||
|
||||
TEST(ParserTest, SimpleOperation) {
|
||||
Lexer lexer("10 20 Td");
|
||||
auto tokens = lexer.tokenize();
|
||||
|
||||
ContentParser parser(tokens);
|
||||
auto ops = parser.parse();
|
||||
|
||||
ASSERT_EQ(ops.size(), 1);
|
||||
EXPECT_EQ(ops[0].op, "Td");
|
||||
ASSERT_EQ(ops[0].operands.size(), 2);
|
||||
|
||||
EXPECT_EQ(ops[0].operands[0]->type, AstNodeType::Number);
|
||||
EXPECT_DOUBLE_EQ(ops[0].operands[0]->numberValue, 10.0);
|
||||
|
||||
EXPECT_EQ(ops[0].operands[1]->type, AstNodeType::Number);
|
||||
EXPECT_DOUBLE_EQ(ops[0].operands[1]->numberValue, 20.0);
|
||||
}
|
||||
|
||||
TEST(ParserTest, ArraysAndDicts) {
|
||||
Lexer lexer("<< /Type /Page >> [ 1 2 ] (Text) Tj");
|
||||
auto tokens = lexer.tokenize();
|
||||
|
||||
ContentParser parser(tokens);
|
||||
auto ops = parser.parse();
|
||||
|
||||
// The dictionary and array and string are ALL pushed onto the operand stack
|
||||
// until the operator 'Tj' is encountered.
|
||||
// Tj will consume all of them.
|
||||
ASSERT_EQ(ops.size(), 1);
|
||||
EXPECT_EQ(ops[0].op, "Tj");
|
||||
ASSERT_EQ(ops[0].operands.size(), 3);
|
||||
|
||||
// First operand: Dict
|
||||
auto dictNode = ops[0].operands[0];
|
||||
EXPECT_EQ(dictNode->type, AstNodeType::Dictionary);
|
||||
ASSERT_TRUE(dictNode->dictItems.find("Type") != dictNode->dictItems.end());
|
||||
EXPECT_EQ(dictNode->dictItems["Type"]->stringValue, "Page");
|
||||
|
||||
// Second operand: Array
|
||||
auto arrayNode = ops[0].operands[1];
|
||||
EXPECT_EQ(arrayNode->type, AstNodeType::Array);
|
||||
ASSERT_EQ(arrayNode->arrayItems.size(), 2);
|
||||
EXPECT_DOUBLE_EQ(arrayNode->arrayItems[0]->numberValue, 1.0);
|
||||
EXPECT_DOUBLE_EQ(arrayNode->arrayItems[1]->numberValue, 2.0);
|
||||
|
||||
// Third operand: String
|
||||
auto strNode = ops[0].operands[2];
|
||||
EXPECT_EQ(strNode->type, AstNodeType::String);
|
||||
EXPECT_EQ(strNode->stringValue, "Text");
|
||||
}
|
||||
|
||||
TEST(ParserTest, MultipleOperations) {
|
||||
Lexer lexer("BT /F1 12 Tf (Hello) Tj ET");
|
||||
auto tokens = lexer.tokenize();
|
||||
|
||||
ContentParser parser(tokens);
|
||||
auto ops = parser.parse();
|
||||
|
||||
ASSERT_EQ(ops.size(), 4);
|
||||
|
||||
EXPECT_EQ(ops[0].op, "BT");
|
||||
EXPECT_TRUE(ops[0].operands.empty());
|
||||
|
||||
EXPECT_EQ(ops[1].op, "Tf");
|
||||
ASSERT_EQ(ops[1].operands.size(), 2);
|
||||
EXPECT_EQ(ops[1].operands[0]->stringValue, "F1");
|
||||
EXPECT_DOUBLE_EQ(ops[1].operands[1]->numberValue, 12.0);
|
||||
|
||||
EXPECT_EQ(ops[2].op, "Tj");
|
||||
ASSERT_EQ(ops[2].operands.size(), 1);
|
||||
EXPECT_EQ(ops[2].operands[0]->stringValue, "Hello");
|
||||
|
||||
EXPECT_EQ(ops[3].op, "ET");
|
||||
EXPECT_TRUE(ops[3].operands.empty());
|
||||
}
|
||||
|
||||
TEST(ParserTest, 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);
|
||||
auto ops = parser.parse();
|
||||
|
||||
bool foundTj = false;
|
||||
for (const auto& op : ops) {
|
||||
if (op.op == "Tj" || op.op == "TJ") {
|
||||
ASSERT_EQ(op.operands.size(), 1);
|
||||
if (op.operands[0]->type == AstNodeType::String &&
|
||||
op.operands[0]->stringValue == "Hello, world!") {
|
||||
foundTj = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(foundTj) << "Failed to parse (Hello, world!) Tj operation from hello_world.pdf stream";
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <pdfengine/content_stream.hpp>
|
||||
#include "../src/qpdf/qpdf_extractor.hpp"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
using namespace pdfengine;
|
||||
using namespace pdfengine::qpdf_layer;
|
||||
|
||||
#ifndef TEST_CORPUS_DIR
|
||||
#define TEST_CORPUS_DIR "../../corpus"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
std::filesystem::path getCorpusPath(const std::string& subfolder, const std::string& filename) {
|
||||
return std::filesystem::path(TEST_CORPUS_DIR) / subfolder / filename;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> readFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||
if (!file.is_open()) return {};
|
||||
std::streamsize size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
std::vector<uint8_t> buffer(size);
|
||||
if (file.read(reinterpret_cast<char*>(buffer.data()), size)) {
|
||||
return buffer;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
class QpdfExtractorTest : public ::testing::Test {
|
||||
protected:
|
||||
QpdfExtractor extractor;
|
||||
};
|
||||
|
||||
TEST_F(QpdfExtractorTest, ExtractFromMemory) {
|
||||
auto path = getCorpusPath("basic", "hello_world.pdf");
|
||||
auto stream = extractor.extractPageStream(path.string(), 0);
|
||||
ASSERT_TRUE(stream.has_value());
|
||||
EXPECT_EQ(stream->pageIndex, 0);
|
||||
EXPECT_FALSE(stream->compressed); // Or true depending on qpdf, but we only verify success here
|
||||
|
||||
StreamVerification v = verifyContentStream(stream.value());
|
||||
EXPECT_TRUE(v.hasBT);
|
||||
EXPECT_TRUE(v.hasET);
|
||||
EXPECT_TRUE(v.hasTf);
|
||||
EXPECT_TRUE(v.hasTj);
|
||||
}
|
||||
|
||||
TEST_F(QpdfExtractorTest, PageOutOfBounds) {
|
||||
auto path = getCorpusPath("basic", "hello_world.pdf");
|
||||
auto data = readFile(path);
|
||||
ASSERT_FALSE(data.empty());
|
||||
|
||||
auto stream = extractor.extractPageStreamFromMemory(data, 1);
|
||||
ASSERT_FALSE(stream.has_value());
|
||||
EXPECT_EQ(stream.error(), QpdfError::PageOutOfBounds);
|
||||
|
||||
stream = extractor.extractPageStreamFromMemory(data, -1);
|
||||
ASSERT_FALSE(stream.has_value());
|
||||
EXPECT_EQ(stream.error(), QpdfError::PageOutOfBounds);
|
||||
}
|
||||
|
||||
TEST_F(QpdfExtractorTest, CorruptPdf) {
|
||||
std::vector<uint8_t> data = {0x00, 0x01, 0x02};
|
||||
auto stream = extractor.extractPageStreamFromMemory(data, 0);
|
||||
ASSERT_FALSE(stream.has_value());
|
||||
EXPECT_EQ(stream.error(), QpdfError::InvalidFormat);
|
||||
}
|
||||
|
||||
TEST_F(QpdfExtractorTest, EmptyContents) {
|
||||
// about_blank.pdf usually has an empty page or no text
|
||||
auto path = getCorpusPath("basic", "about_blank.pdf");
|
||||
auto stream = extractor.extractPageStream(path.string(), 0);
|
||||
ASSERT_TRUE(stream.has_value());
|
||||
EXPECT_EQ(stream->pageIndex, 0);
|
||||
|
||||
StreamVerification v = verifyContentStream(stream.value());
|
||||
EXPECT_FALSE(v.hasTj);
|
||||
}
|
||||
|
||||
TEST_F(QpdfExtractorTest, VerifyNoText) {
|
||||
// black.pdf or rectangles.pdf has no text, just graphics
|
||||
auto path = getCorpusPath("basic", "black.pdf");
|
||||
auto stream = extractor.extractPageStream(path.string(), 0);
|
||||
ASSERT_TRUE(stream.has_value());
|
||||
|
||||
StreamVerification v = verifyContentStream(stream.value());
|
||||
EXPECT_FALSE(v.hasBT);
|
||||
EXPECT_FALSE(v.hasET);
|
||||
EXPECT_FALSE(v.hasTf);
|
||||
EXPECT_FALSE(v.hasTj);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "../src/qpdf/qpdf_extractor.hpp"
|
||||
#include "../src/qpdf/qpdf_writer.hpp"
|
||||
#include "../src/parser/lexer.hpp"
|
||||
#include "../src/parser/parser.hpp"
|
||||
#include "../src/parser/content_builder.hpp"
|
||||
#include "../src/serializer/content_serializer.hpp"
|
||||
#include "../src/serializer/ast_serializer.hpp"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#ifndef TEST_CORPUS_DIR
|
||||
#define TEST_CORPUS_DIR "../../corpus"
|
||||
#endif
|
||||
|
||||
using namespace pdfengine;
|
||||
using namespace pdfengine::qpdf_layer;
|
||||
|
||||
TEST(QpdfWriterTest, IntegrationReadModifyWrite) {
|
||||
std::filesystem::path sourcePath = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world.pdf";
|
||||
std::filesystem::path destPath = std::filesystem::path(TEST_CORPUS_DIR) / "basic" / "hello_world_modified.pdf";
|
||||
|
||||
// 1. Extract
|
||||
QpdfExtractor extractor;
|
||||
auto stream = extractor.extractPageStream(sourcePath.string(), 0);
|
||||
ASSERT_TRUE(stream.has_value());
|
||||
|
||||
// 2. Lex, Parse, Build
|
||||
Lexer lexer(stream->decodedContent);
|
||||
auto tokens = lexer.tokenize();
|
||||
ContentParser parser(tokens);
|
||||
auto ops = parser.parse();
|
||||
ContentBuilder builder;
|
||||
auto objects = builder.build(ops);
|
||||
|
||||
// 3. Modify Text
|
||||
bool foundAndModified = false;
|
||||
for (auto& obj : objects) {
|
||||
if (obj->getType() == ContentObjectType::Text) {
|
||||
auto* textObj = static_cast<TextObject*>(obj.get());
|
||||
if (textObj->text == "Hello, world!") {
|
||||
textObj->text = "Hello, PDF Editor!";
|
||||
foundAndModified = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(foundAndModified) << "Could not find 'Hello, world!' to modify";
|
||||
|
||||
// 4. Serialize back to Ops
|
||||
ContentSerializer contentSerializer;
|
||||
auto newOps = contentSerializer.serialize(objects);
|
||||
|
||||
// 5. Serialize to raw bytes
|
||||
AstSerializer astSerializer;
|
||||
std::string newRawStream = astSerializer.serialize(newOps);
|
||||
|
||||
// 6. Write and Save PDF
|
||||
QpdfWriter writer;
|
||||
auto writeRes = writer.replacePageStreamAndSave(sourcePath.string(), destPath.string(), 0, newRawStream);
|
||||
ASSERT_TRUE(writeRes.has_value()) << writeRes.error();
|
||||
|
||||
// 7. Re-open and verify modification
|
||||
auto verifyStream = extractor.extractPageStream(destPath.string(), 0);
|
||||
ASSERT_TRUE(verifyStream.has_value());
|
||||
|
||||
Lexer verifyLexer(verifyStream->decodedContent);
|
||||
auto verifyTokens = verifyLexer.tokenize();
|
||||
ContentParser verifyParser(verifyTokens);
|
||||
ContentBuilder verifyBuilder;
|
||||
auto verifyObjects = verifyBuilder.build(verifyParser.parse());
|
||||
|
||||
bool verifiedModification = false;
|
||||
for (auto& obj : verifyObjects) {
|
||||
if (obj->getType() == ContentObjectType::Text) {
|
||||
auto* textObj = static_cast<TextObject*>(obj.get());
|
||||
if (textObj->text == "Hello, PDF Editor!") {
|
||||
verifiedModification = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(verifiedModification) << "Modified string was not successfully saved and reloaded!";
|
||||
|
||||
// Cleanup
|
||||
std::filesystem::remove(destPath);
|
||||
}
|
||||
Reference in New Issue
Block a user