813 lines
28 KiB
C++
813 lines
28 KiB
C++
#include <gtest/gtest.h>
|
|
|
|
#include <pdfengine/content_object.hpp>
|
|
#include <pdfengine/pdf_document.hpp>
|
|
|
|
#include "../src/parser/content_builder.hpp"
|
|
#include "../src/parser/lexer.hpp"
|
|
#include "../src/parser/parser.hpp"
|
|
#include "../src/qpdf/qpdf_resource_resolver.hpp"
|
|
|
|
#include <qpdf/Buffer.hh>
|
|
#include <qpdf/QPDF.hh>
|
|
#include <qpdf/QPDFObjectHandle.hh>
|
|
|
|
#include <zlib.h>
|
|
#include <jpeglib.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <csetjmp>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
using namespace pdfengine;
|
|
using namespace pdfengine::qpdf_layer;
|
|
|
|
namespace {
|
|
|
|
struct Rgba {
|
|
int r = 0;
|
|
int g = 0;
|
|
int b = 0;
|
|
int a = 255;
|
|
};
|
|
|
|
struct PdfObject {
|
|
int id = 0;
|
|
std::vector<uint8_t> body;
|
|
};
|
|
|
|
struct ImageSpec {
|
|
std::string name;
|
|
int id = 0;
|
|
int width = 0;
|
|
int height = 0;
|
|
std::string colorSpace = "/DeviceRGB";
|
|
std::string filter;
|
|
std::vector<uint8_t> streamData;
|
|
std::optional<int> smaskId;
|
|
};
|
|
|
|
void appendText(std::vector<uint8_t>& out, std::string_view text) {
|
|
out.insert(out.end(), text.begin(), text.end());
|
|
}
|
|
|
|
std::vector<uint8_t> textBody(const std::string& text) {
|
|
return std::vector<uint8_t>(text.begin(), text.end());
|
|
}
|
|
|
|
std::vector<uint8_t> makeStreamBody(const std::string& dictionaryEntries,
|
|
const std::vector<uint8_t>& streamData) {
|
|
std::vector<uint8_t> body;
|
|
appendText(body, "<< ");
|
|
appendText(body, dictionaryEntries);
|
|
appendText(body, " /Length ");
|
|
appendText(body, std::to_string(streamData.size()));
|
|
appendText(body, " >>\nstream\n");
|
|
body.insert(body.end(), streamData.begin(), streamData.end());
|
|
appendText(body, "\nendstream");
|
|
return body;
|
|
}
|
|
|
|
std::vector<uint8_t> makeImageBody(const ImageSpec& image) {
|
|
std::ostringstream dict;
|
|
dict << "/Type /XObject /Subtype /Image"
|
|
<< " /Width " << image.width
|
|
<< " /Height " << image.height;
|
|
if (!image.colorSpace.empty()) {
|
|
dict << " /ColorSpace " << image.colorSpace;
|
|
}
|
|
dict << " /BitsPerComponent 8";
|
|
if (!image.filter.empty()) {
|
|
dict << " /Filter " << image.filter;
|
|
}
|
|
if (image.smaskId) {
|
|
dict << " /SMask " << *image.smaskId << " 0 R";
|
|
}
|
|
return makeStreamBody(dict.str(), image.streamData);
|
|
}
|
|
|
|
std::vector<uint8_t> makePdf(std::vector<PdfObject> objects, int rootObjectId = 1) {
|
|
std::sort(objects.begin(), objects.end(), [](const PdfObject& a, const PdfObject& b) {
|
|
return a.id < b.id;
|
|
});
|
|
|
|
int maxId = 0;
|
|
for (const auto& object : objects) {
|
|
maxId = std::max(maxId, object.id);
|
|
}
|
|
|
|
std::vector<size_t> offsets(static_cast<size_t>(maxId) + 1, 0);
|
|
std::vector<uint8_t> pdf;
|
|
appendText(pdf, "%PDF-1.4\n%\xFF\xFF\xFF\xFF\n");
|
|
|
|
for (const auto& object : objects) {
|
|
offsets[static_cast<size_t>(object.id)] = pdf.size();
|
|
appendText(pdf, std::to_string(object.id));
|
|
appendText(pdf, " 0 obj\n");
|
|
pdf.insert(pdf.end(), object.body.begin(), object.body.end());
|
|
appendText(pdf, "\nendobj\n");
|
|
}
|
|
|
|
const size_t xrefOffset = pdf.size();
|
|
appendText(pdf, "xref\n0 ");
|
|
appendText(pdf, std::to_string(maxId + 1));
|
|
appendText(pdf, "\n0000000000 65535 f \n");
|
|
for (int id = 1; id <= maxId; ++id) {
|
|
char row[32]{};
|
|
std::snprintf(row, sizeof(row), "%010zu 00000 n \n", offsets[static_cast<size_t>(id)]);
|
|
appendText(pdf, row);
|
|
}
|
|
|
|
appendText(pdf, "trailer\n<< /Size ");
|
|
appendText(pdf, std::to_string(maxId + 1));
|
|
appendText(pdf, " /Root ");
|
|
appendText(pdf, std::to_string(rootObjectId));
|
|
appendText(pdf, " 0 R >>\nstartxref\n");
|
|
appendText(pdf, std::to_string(xrefOffset));
|
|
appendText(pdf, "\n%%EOF\n");
|
|
return pdf;
|
|
}
|
|
|
|
std::vector<uint8_t> buildSinglePagePdf(int pageWidth,
|
|
int pageHeight,
|
|
const std::string& content,
|
|
const std::vector<ImageSpec>& images) {
|
|
std::ostringstream xobjects;
|
|
for (const auto& image : images) {
|
|
if (!image.name.empty()) {
|
|
xobjects << "/" << image.name << " " << image.id << " 0 R ";
|
|
}
|
|
}
|
|
|
|
std::vector<PdfObject> objects;
|
|
objects.push_back({1, textBody("<< /Type /Catalog /Pages 2 0 R >>")});
|
|
objects.push_back({2, textBody("<< /Type /Pages /Kids [3 0 R] /Count 1 >>")});
|
|
|
|
std::ostringstream page;
|
|
page << "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 " << pageWidth << " " << pageHeight
|
|
<< "] /Resources << /XObject << " << xobjects.str()
|
|
<< ">> >> /Contents 4 0 R >>";
|
|
objects.push_back({3, textBody(page.str())});
|
|
|
|
const std::vector<uint8_t> contentBytes(content.begin(), content.end());
|
|
objects.push_back({4, makeStreamBody("", contentBytes)});
|
|
|
|
for (const auto& image : images) {
|
|
objects.push_back({image.id, makeImageBody(image)});
|
|
}
|
|
return makePdf(std::move(objects));
|
|
}
|
|
|
|
std::vector<uint8_t> flate(const std::vector<uint8_t>& raw) {
|
|
uLongf size = compressBound(static_cast<uLong>(raw.size()));
|
|
std::vector<uint8_t> compressed(size);
|
|
const int result = compress2(compressed.data(), &size, raw.data(),
|
|
static_cast<uLong>(raw.size()), Z_BEST_COMPRESSION);
|
|
EXPECT_EQ(result, Z_OK);
|
|
compressed.resize(size);
|
|
return compressed;
|
|
}
|
|
|
|
#ifdef _MSC_VER
|
|
#pragma warning(push)
|
|
#pragma warning(disable: 4324)
|
|
#endif
|
|
struct JpegErrorManager {
|
|
jpeg_error_mgr pub;
|
|
std::jmp_buf jump;
|
|
};
|
|
#ifdef _MSC_VER
|
|
#pragma warning(pop)
|
|
#endif
|
|
|
|
void jpegErrorExit(j_common_ptr cinfo) {
|
|
auto* manager = reinterpret_cast<JpegErrorManager*>(cinfo->err);
|
|
longjmp(manager->jump, 1);
|
|
}
|
|
|
|
std::vector<uint8_t> encodeJpegRgb(const std::vector<uint8_t>& rgb,
|
|
int width,
|
|
int height,
|
|
int quality = 95) {
|
|
jpeg_compress_struct cinfo{};
|
|
JpegErrorManager jerr{};
|
|
cinfo.err = jpeg_std_error(&jerr.pub);
|
|
jerr.pub.error_exit = jpegErrorExit;
|
|
|
|
unsigned char* output = nullptr;
|
|
unsigned long outputSize = 0;
|
|
|
|
#ifdef _MSC_VER
|
|
#pragma warning(push)
|
|
#pragma warning(disable: 4611)
|
|
#endif
|
|
if (setjmp(jerr.jump)) {
|
|
jpeg_destroy_compress(&cinfo);
|
|
std::free(output);
|
|
return {};
|
|
}
|
|
#ifdef _MSC_VER
|
|
#pragma warning(pop)
|
|
#endif
|
|
|
|
jpeg_create_compress(&cinfo);
|
|
jpeg_mem_dest(&cinfo, &output, &outputSize);
|
|
|
|
cinfo.image_width = static_cast<JDIMENSION>(width);
|
|
cinfo.image_height = static_cast<JDIMENSION>(height);
|
|
cinfo.input_components = 3;
|
|
cinfo.in_color_space = JCS_RGB;
|
|
|
|
jpeg_set_defaults(&cinfo);
|
|
jpeg_set_quality(&cinfo, quality, TRUE);
|
|
jpeg_start_compress(&cinfo, TRUE);
|
|
|
|
const int rowStride = width * 3;
|
|
while (cinfo.next_scanline < cinfo.image_height) {
|
|
JSAMPROW row = const_cast<JSAMPLE*>(
|
|
reinterpret_cast<const JSAMPLE*>(rgb.data() + cinfo.next_scanline * rowStride));
|
|
jpeg_write_scanlines(&cinfo, &row, 1);
|
|
}
|
|
|
|
jpeg_finish_compress(&cinfo);
|
|
std::vector<uint8_t> jpeg(output, output + outputSize);
|
|
jpeg_destroy_compress(&cinfo);
|
|
std::free(output);
|
|
return jpeg;
|
|
}
|
|
|
|
std::string decodedStream(QPDFObjectHandle contents) {
|
|
std::string decoded;
|
|
if (contents.isStream()) {
|
|
auto buffer = contents.getStreamData();
|
|
decoded.assign(reinterpret_cast<const char*>(buffer->getBuffer()), buffer->getSize());
|
|
} else if (contents.isArray()) {
|
|
for (int i = 0; i < contents.getArrayNItems(); ++i) {
|
|
auto buffer = contents.getArrayItem(i).getStreamData();
|
|
decoded.append(reinterpret_cast<const char*>(buffer->getBuffer()), buffer->getSize());
|
|
decoded.push_back('\n');
|
|
}
|
|
}
|
|
return decoded;
|
|
}
|
|
|
|
std::vector<std::unique_ptr<ContentObject>> buildPageObjects(QPDFObjectHandle page) {
|
|
Lexer lexer(decodedStream(page.getKey("/Contents")));
|
|
auto tokens = lexer.tokenize();
|
|
ContentParser parser(tokens);
|
|
auto operations = parser.parse();
|
|
|
|
QpdfResourceResolver resolver(page.getKey("/Resources"));
|
|
ContentBuilder builder(&resolver);
|
|
return builder.build(operations);
|
|
}
|
|
|
|
std::vector<std::unique_ptr<ContentObject>> buildFirstPageObjects(QPDF& qpdf,
|
|
const std::vector<uint8_t>& pdfBytes) {
|
|
qpdf.processMemoryFile("image-xobject-test", reinterpret_cast<const char*>(pdfBytes.data()), pdfBytes.size());
|
|
return buildPageObjects(qpdf.getAllPages().at(0));
|
|
}
|
|
|
|
std::vector<const ImageObject*> imageObjects(const std::vector<std::unique_ptr<ContentObject>>& objects) {
|
|
std::vector<const ImageObject*> images;
|
|
for (const auto& object : objects) {
|
|
if (object->getType() == ContentObjectType::Image) {
|
|
images.push_back(static_cast<const ImageObject*>(object.get()));
|
|
}
|
|
}
|
|
return images;
|
|
}
|
|
|
|
Rgba decodedPixel(const ImageObject& image, int x, int y) {
|
|
const auto index = (static_cast<size_t>(y) * image.width + x) * 4;
|
|
return {
|
|
image.pixelData[index + 0],
|
|
image.pixelData[index + 1],
|
|
image.pixelData[index + 2],
|
|
image.pixelData[index + 3],
|
|
};
|
|
}
|
|
|
|
Rgba averageDecoded(const ImageObject& image) {
|
|
long long r = 0;
|
|
long long g = 0;
|
|
long long b = 0;
|
|
long long a = 0;
|
|
const int count = image.width * image.height;
|
|
for (int y = 0; y < image.height; ++y) {
|
|
for (int x = 0; x < image.width; ++x) {
|
|
auto p = decodedPixel(image, x, y);
|
|
r += p.r;
|
|
g += p.g;
|
|
b += p.b;
|
|
a += p.a;
|
|
}
|
|
}
|
|
return {
|
|
static_cast<int>(r / count),
|
|
static_cast<int>(g / count),
|
|
static_cast<int>(b / count),
|
|
static_cast<int>(a / count),
|
|
};
|
|
}
|
|
|
|
Rgba pagePixel(const PageImage& image, int x, int yFromBottom) {
|
|
const int row = image.height - 1 - yFromBottom;
|
|
const auto index = (static_cast<size_t>(row) * image.width + x) * 4;
|
|
return {
|
|
image.data[index + 0],
|
|
image.data[index + 1],
|
|
image.data[index + 2],
|
|
image.data[index + 3],
|
|
};
|
|
}
|
|
|
|
Rgba averagePageRect(const PageImage& image, int x, int yFromBottom, int width, int height) {
|
|
long long r = 0;
|
|
long long g = 0;
|
|
long long b = 0;
|
|
long long a = 0;
|
|
int count = 0;
|
|
for (int yy = yFromBottom; yy < yFromBottom + height; ++yy) {
|
|
for (int xx = x; xx < x + width; ++xx) {
|
|
auto p = pagePixel(image, xx, yy);
|
|
r += p.r;
|
|
g += p.g;
|
|
b += p.b;
|
|
a += p.a;
|
|
++count;
|
|
}
|
|
}
|
|
return {
|
|
static_cast<int>(r / count),
|
|
static_cast<int>(g / count),
|
|
static_cast<int>(b / count),
|
|
static_cast<int>(a / count),
|
|
};
|
|
}
|
|
|
|
bool nearChannel(int actual, int expected, int tolerance) {
|
|
return std::abs(actual - expected) <= tolerance;
|
|
}
|
|
|
|
void expectNearColor(const Rgba& actual, const Rgba& expected, int tolerance) {
|
|
EXPECT_TRUE(nearChannel(actual.r, expected.r, tolerance))
|
|
<< "red actual=" << actual.r << " expected=" << expected.r;
|
|
EXPECT_TRUE(nearChannel(actual.g, expected.g, tolerance))
|
|
<< "green actual=" << actual.g << " expected=" << expected.g;
|
|
EXPECT_TRUE(nearChannel(actual.b, expected.b, tolerance))
|
|
<< "blue actual=" << actual.b << " expected=" << expected.b;
|
|
}
|
|
|
|
bool isWhite(const Rgba& p) {
|
|
return p.r > 245 && p.g > 245 && p.b > 245;
|
|
}
|
|
|
|
std::optional<PageImage> renderWithPdfium(const std::vector<uint8_t>& pdfBytes,
|
|
int pageIndex = 0,
|
|
int dpi = 72) {
|
|
auto document = PdfDocument::loadFromMemory(pdfBytes);
|
|
if (!document) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto page = (*document)->getPage(pageIndex);
|
|
if (!page) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
auto rendered = (*page)->renderRegionRaw(dpi, 0.0, (*page)->height());
|
|
if (!rendered) {
|
|
return std::nullopt;
|
|
}
|
|
return rendered.value();
|
|
}
|
|
|
|
void expectMatrix(const Matrix& matrix, float a, float b, float c, float d, float e, float f) {
|
|
EXPECT_FLOAT_EQ(matrix.a, a);
|
|
EXPECT_FLOAT_EQ(matrix.b, b);
|
|
EXPECT_FLOAT_EQ(matrix.c, c);
|
|
EXPECT_FLOAT_EQ(matrix.d, d);
|
|
EXPECT_FLOAT_EQ(matrix.e, e);
|
|
EXPECT_FLOAT_EQ(matrix.f, f);
|
|
}
|
|
|
|
std::vector<uint8_t> rgb(std::initializer_list<uint8_t> values) {
|
|
return std::vector<uint8_t>(values);
|
|
}
|
|
|
|
}
|
|
|
|
TEST(ImageXObjectVerification, JpegLogoAndPhotoDecodeAndMatchPdfiumRender) {
|
|
const auto logoJpeg = encodeJpegRgb(rgb({
|
|
220, 20, 20, 20, 210, 30, 25, 30, 220,
|
|
230, 210, 30, 230, 40, 200, 20, 210, 210,
|
|
}), 3, 2);
|
|
|
|
std::vector<uint8_t> photoRgb;
|
|
for (int y = 0; y < 6; ++y) {
|
|
for (int x = 0; x < 8; ++x) {
|
|
photoRgb.push_back(static_cast<uint8_t>(35 + x * 24));
|
|
photoRgb.push_back(static_cast<uint8_t>(40 + y * 28));
|
|
photoRgb.push_back(static_cast<uint8_t>(190 - x * 10 + y * 4));
|
|
}
|
|
}
|
|
const auto photoJpeg = encodeJpegRgb(photoRgb, 8, 6);
|
|
ASSERT_FALSE(logoJpeg.empty());
|
|
ASSERT_FALSE(photoJpeg.empty());
|
|
|
|
const std::string content =
|
|
"q 3 0 0 2 10 80 cm /Logo Do Q\n"
|
|
"q 8 0 0 6 30 80 cm /Photo Do Q\n";
|
|
const auto pdf = buildSinglePagePdf(80, 100, content, {
|
|
{"Logo", 5, 3, 2, "/DeviceRGB", "/DCTDecode", logoJpeg, std::nullopt},
|
|
{"Photo", 6, 8, 6, "/DeviceRGB", "/DCTDecode", photoJpeg, std::nullopt},
|
|
});
|
|
|
|
QPDF qpdf;
|
|
auto objects = buildFirstPageObjects(qpdf, pdf);
|
|
|
|
// DEBUG: Parse again just to print
|
|
{
|
|
Lexer lexer(content);
|
|
auto tokens = lexer.tokenize();
|
|
ContentParser parser(tokens);
|
|
auto operations = parser.parse();
|
|
for (const auto& op : operations) {
|
|
std::cout << "DEBUG OP: " << op.op << " operands: ";
|
|
for (const auto& operand : op.operands) {
|
|
if (operand->type == AstNodeType::Number) std::cout << operand->numberValue << " ";
|
|
else if (operand->type == AstNodeType::Name) std::cout << "/" << operand->stringValue << " ";
|
|
else std::cout << "[type " << (int)operand->type << "] ";
|
|
}
|
|
std::cout << "\n";
|
|
}
|
|
}
|
|
|
|
auto images = imageObjects(objects);
|
|
ASSERT_EQ(images.size(), 2u);
|
|
|
|
EXPECT_EQ(images[0]->name, "Logo");
|
|
EXPECT_EQ(images[0]->width, 3);
|
|
EXPECT_EQ(images[0]->height, 2);
|
|
EXPECT_EQ(images[0]->filter, "/DCTDecode");
|
|
EXPECT_EQ(images[0]->pixelData.size(), 3u * 2u * 4u);
|
|
expectMatrix(images[0]->transform, 3, 0, 0, 2, 10, 80);
|
|
|
|
EXPECT_EQ(images[1]->name, "Photo");
|
|
EXPECT_EQ(images[1]->width, 8);
|
|
EXPECT_EQ(images[1]->height, 6);
|
|
EXPECT_EQ(images[1]->filter, "/DCTDecode");
|
|
EXPECT_EQ(images[1]->pixelData.size(), 8u * 6u * 4u);
|
|
expectMatrix(images[1]->transform, 8, 0, 0, 6, 30, 80);
|
|
|
|
auto rendered = renderWithPdfium(pdf);
|
|
if (!rendered) {
|
|
GTEST_SKIP() << "PDFium render unavailable in this build";
|
|
}
|
|
|
|
expectNearColor(averagePageRect(*rendered, 10, 80, 3, 2), averageDecoded(*images[0]), 35);
|
|
expectNearColor(averagePageRect(*rendered, 30, 80, 8, 6), averageDecoded(*images[1]), 35);
|
|
EXPECT_TRUE(isWhite(pagePixel(*rendered, 9, 80)));
|
|
EXPECT_TRUE(isWhite(pagePixel(*rendered, 38, 80)));
|
|
}
|
|
|
|
TEST(ImageXObjectVerification, FlateRgbAndTransparentPngStyleSoftMaskDecodeCorrectly) {
|
|
const auto opaqueRgb = rgb({
|
|
255, 0, 0, 0, 255, 0,
|
|
0, 0, 255, 255, 255, 0,
|
|
});
|
|
const auto transparentRgb = rgb({
|
|
200, 0, 0, 0, 200, 0,
|
|
0, 0, 200, 120, 120, 120,
|
|
});
|
|
const std::vector<uint8_t> alpha = {0, 64, 128, 255};
|
|
|
|
const std::string content =
|
|
"q 2 0 0 2 10 60 cm /Png Do Q\n"
|
|
"q 2 0 0 2 20 60 cm /PngAlpha Do Q\n";
|
|
const auto pdf = buildSinglePagePdf(60, 80, content, {
|
|
{"Png", 5, 2, 2, "/DeviceRGB", "/FlateDecode", flate(opaqueRgb), std::nullopt},
|
|
{"PngAlpha", 6, 2, 2, "/DeviceRGB", "/FlateDecode", flate(transparentRgb), 7},
|
|
{"", 7, 2, 2, "/DeviceGray", "/FlateDecode", flate(alpha), std::nullopt},
|
|
});
|
|
|
|
QPDF qpdf;
|
|
auto objects = buildFirstPageObjects(qpdf, pdf);
|
|
auto images = imageObjects(objects);
|
|
ASSERT_EQ(images.size(), 2u);
|
|
|
|
EXPECT_EQ(images[0]->pixelData.size(), 2u * 2u * 4u);
|
|
expectNearColor(decodedPixel(*images[0], 0, 0), {255, 0, 0, 255}, 0);
|
|
expectNearColor(decodedPixel(*images[0], 1, 0), {0, 255, 0, 255}, 0);
|
|
expectNearColor(decodedPixel(*images[0], 0, 1), {0, 0, 255, 255}, 0);
|
|
expectNearColor(decodedPixel(*images[0], 1, 1), {255, 255, 0, 255}, 0);
|
|
|
|
EXPECT_TRUE(images[1]->hasSoftMask);
|
|
EXPECT_EQ(decodedPixel(*images[1], 0, 0).a, 0);
|
|
EXPECT_EQ(decodedPixel(*images[1], 1, 0).a, 64);
|
|
EXPECT_EQ(decodedPixel(*images[1], 0, 1).a, 128);
|
|
EXPECT_EQ(decodedPixel(*images[1], 1, 1).a, 255);
|
|
}
|
|
|
|
TEST(ImageXObjectVerification, DeviceGrayMapsBlackAndWhiteExactly) {
|
|
const std::vector<uint8_t> gray = {0, 255};
|
|
const auto pdf = buildSinglePagePdf(20, 20, "q 2 0 0 1 5 5 cm /Scan Do Q\n", {
|
|
{"Scan", 5, 2, 1, "/DeviceGray", "/FlateDecode", flate(gray), std::nullopt},
|
|
});
|
|
|
|
QPDF qpdf;
|
|
auto objects = buildFirstPageObjects(qpdf, pdf);
|
|
auto images = imageObjects(objects);
|
|
ASSERT_EQ(images.size(), 1u);
|
|
EXPECT_EQ(decodedPixel(*images[0], 0, 0).r, 0);
|
|
EXPECT_EQ(decodedPixel(*images[0], 0, 0).g, 0);
|
|
EXPECT_EQ(decodedPixel(*images[0], 0, 0).b, 0);
|
|
EXPECT_EQ(decodedPixel(*images[0], 1, 0).r, 255);
|
|
EXPECT_EQ(decodedPixel(*images[0], 1, 0).g, 255);
|
|
EXPECT_EQ(decodedPixel(*images[0], 1, 0).b, 255);
|
|
}
|
|
|
|
TEST(ImageXObjectVerification, DeviceCmykConversionMatchesPdfiumRender) {
|
|
const std::vector<uint8_t> cmyk = {
|
|
255, 0, 0, 0, 0, 255, 0, 0,
|
|
0, 0, 255, 0, 0, 0, 0, 255,
|
|
};
|
|
const std::string content = "q 20 0 0 20 10 10 cm /Print Do Q\n";
|
|
const auto pdf = buildSinglePagePdf(50, 50, content, {
|
|
{"Print", 5, 2, 2, "/DeviceCMYK", "/FlateDecode", flate(cmyk), std::nullopt},
|
|
});
|
|
|
|
QPDF qpdf;
|
|
auto objects = buildFirstPageObjects(qpdf, pdf);
|
|
auto images = imageObjects(objects);
|
|
ASSERT_EQ(images.size(), 1u);
|
|
|
|
expectNearColor(decodedPixel(*images[0], 0, 0), {0, 174, 239, 255}, 1);
|
|
expectNearColor(decodedPixel(*images[0], 1, 0), {237, 0, 140, 255}, 1);
|
|
expectNearColor(decodedPixel(*images[0], 0, 1), {255, 241, 0, 255}, 1);
|
|
expectNearColor(decodedPixel(*images[0], 1, 1), {35, 31, 32, 255}, 1);
|
|
|
|
auto rendered = renderWithPdfium(pdf);
|
|
if (!rendered) {
|
|
GTEST_SKIP() << "PDFium render unavailable in this build";
|
|
}
|
|
|
|
expectNearColor(averagePageRect(*rendered, 10, 20, 10, 10), decodedPixel(*images[0], 0, 0), 12);
|
|
expectNearColor(averagePageRect(*rendered, 20, 20, 10, 10), decodedPixel(*images[0], 1, 0), 12);
|
|
expectNearColor(averagePageRect(*rendered, 10, 10, 10, 10), decodedPixel(*images[0], 0, 1), 12);
|
|
expectNearColor(averagePageRect(*rendered, 20, 10, 10, 10), decodedPixel(*images[0], 1, 1), 12);
|
|
}
|
|
|
|
TEST(ImageXObjectVerification, MatrixTransformTranslationScalingRotationAndFlipping) {
|
|
const auto data = flate(rgb({
|
|
255, 0, 0, 0, 255, 0,
|
|
0, 0, 255, 0, 0, 0,
|
|
}));
|
|
const std::string content =
|
|
"q 100 0 0 100 50 50 cm /Scale Do Q\n"
|
|
"q 0 40 -40 0 200 40 cm /Rotate Do Q\n"
|
|
"q 40 0 0 -40 230 120 cm /Flip Do Q\n";
|
|
const auto pdf = buildSinglePagePdf(280, 180, content, {
|
|
{"Scale", 5, 2, 2, "/DeviceRGB", "/FlateDecode", data, std::nullopt},
|
|
{"Rotate", 6, 2, 2, "/DeviceRGB", "/FlateDecode", data, std::nullopt},
|
|
{"Flip", 7, 2, 2, "/DeviceRGB", "/FlateDecode", data, std::nullopt},
|
|
});
|
|
|
|
QPDF qpdf;
|
|
qpdf.processMemoryFile("image-xobject-test", reinterpret_cast<const char*>(pdf.data()), pdf.size());
|
|
auto page = qpdf.getAllPages().at(0);
|
|
const std::string decodedContent = decodedStream(page.getKey("/Contents"));
|
|
|
|
Lexer lexer(decodedContent);
|
|
auto tokens = lexer.tokenize();
|
|
ContentParser parser(tokens);
|
|
auto operations = parser.parse();
|
|
size_t doOps = 0;
|
|
std::vector<std::string> doNames;
|
|
for (const auto& operation : operations) {
|
|
if (operation.op == "Do") {
|
|
++doOps;
|
|
if (!operation.operands.empty() && operation.operands.back()->type == AstNodeType::Name) {
|
|
doNames.push_back(operation.operands.back()->stringValue);
|
|
}
|
|
}
|
|
}
|
|
EXPECT_EQ(doOps, 3u);
|
|
EXPECT_EQ(doNames, (std::vector<std::string>{"Scale", "Rotate", "Flip"}));
|
|
|
|
QpdfResourceResolver resolver(page.getKey("/Resources"));
|
|
EXPECT_EQ(resolver.resolveXObject("Scale").type, XObjectType::Image);
|
|
EXPECT_EQ(resolver.resolveXObject("Rotate").type, XObjectType::Image);
|
|
EXPECT_EQ(resolver.resolveXObject("Flip").type, XObjectType::Image);
|
|
|
|
ContentBuilder builder(&resolver);
|
|
auto objects = builder.build(operations);
|
|
auto images = imageObjects(objects);
|
|
ASSERT_EQ(images.size(), 3u)
|
|
<< "content objects=" << objects.size()
|
|
<< " decoded=[" << decodedContent << "]"
|
|
<< " resources=[" << page.getKey("/Resources").unparse() << "]";
|
|
|
|
EXPECT_EQ(images[0]->name, "Scale");
|
|
EXPECT_EQ(images[1]->name, "Rotate");
|
|
EXPECT_EQ(images[2]->name, "Flip");
|
|
expectMatrix(images[0]->transform, 100, 0, 0, 100, 50, 50);
|
|
expectMatrix(images[1]->transform, 0, 40, -40, 0, 200, 40);
|
|
expectMatrix(images[2]->transform, 40, 0, 0, -40, 230, 120);
|
|
|
|
auto rendered = renderWithPdfium(pdf);
|
|
if (!rendered) {
|
|
GTEST_SKIP() << "PDFium render unavailable in this build";
|
|
}
|
|
|
|
EXPECT_FALSE(isWhite(pagePixel(*rendered, 75, 75)));
|
|
EXPECT_FALSE(isWhite(pagePixel(*rendered, 185, 55)));
|
|
EXPECT_FALSE(isWhite(pagePixel(*rendered, 245, 105)));
|
|
EXPECT_TRUE(isWhite(pagePixel(*rendered, 49, 50)));
|
|
EXPECT_TRUE(isWhite(pagePixel(*rendered, 201, 80)));
|
|
}
|
|
|
|
TEST(ImageXObjectVerification, MultipleImagesPreserveOrderAndPlacement) {
|
|
std::vector<ImageSpec> images;
|
|
std::ostringstream content;
|
|
const std::array<Rgba, 8> colors = {{
|
|
{230, 20, 20, 255}, {20, 230, 20, 255}, {20, 20, 230, 255}, {230, 230, 20, 255},
|
|
{230, 20, 230, 255}, {20, 230, 230, 255}, {120, 80, 240, 255}, {20, 20, 20, 255},
|
|
}};
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
const int x = 5 + i * 10;
|
|
content << "q 6 0 0 6 " << x << " 20 cm /Im" << i << " Do Q\n";
|
|
images.push_back({
|
|
"Im" + std::to_string(i),
|
|
5 + i,
|
|
1,
|
|
1,
|
|
"/DeviceRGB",
|
|
"/FlateDecode",
|
|
flate(rgb({
|
|
static_cast<uint8_t>(colors[i].r),
|
|
static_cast<uint8_t>(colors[i].g),
|
|
static_cast<uint8_t>(colors[i].b),
|
|
})),
|
|
std::nullopt,
|
|
});
|
|
}
|
|
|
|
const auto pdf = buildSinglePagePdf(100, 50, content.str(), images);
|
|
QPDF qpdf;
|
|
auto objects = buildFirstPageObjects(qpdf, pdf);
|
|
auto parsedImages = imageObjects(objects);
|
|
ASSERT_EQ(parsedImages.size(), 8u);
|
|
|
|
auto rendered = renderWithPdfium(pdf);
|
|
if (!rendered) {
|
|
GTEST_SKIP() << "PDFium render unavailable in this build";
|
|
}
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
EXPECT_EQ(parsedImages[i]->name, "Im" + std::to_string(i));
|
|
expectMatrix(parsedImages[i]->transform, 6, 0, 0, 6, static_cast<float>(5 + i * 10), 20);
|
|
expectNearColor(pagePixel(*rendered, 8 + i * 10, 23), colors[i], 2);
|
|
EXPECT_TRUE(isWhite(pagePixel(*rendered, 12 + i * 10, 23)));
|
|
}
|
|
}
|
|
|
|
TEST(ImageXObjectVerification, LoadRenderCloseStress100Pages1000ImageDraws) {
|
|
constexpr int kPages = 100;
|
|
constexpr int kImagesPerPage = 10;
|
|
|
|
std::vector<PdfObject> objects;
|
|
objects.push_back({1, textBody("<< /Type /Catalog /Pages 2 0 R >>")});
|
|
|
|
int nextId = 3;
|
|
std::vector<int> pageIds;
|
|
|
|
for (int pageIndex = 0; pageIndex < kPages; ++pageIndex) {
|
|
const int pageId = nextId++;
|
|
const int contentId = nextId++;
|
|
pageIds.push_back(pageId);
|
|
|
|
std::ostringstream xobjects;
|
|
std::ostringstream content;
|
|
for (int imageIndex = 0; imageIndex < kImagesPerPage; ++imageIndex) {
|
|
const int imageId = nextId++;
|
|
const std::string name = "Im" + std::to_string(imageIndex);
|
|
xobjects << "/" << name << " " << imageId << " 0 R ";
|
|
content << "q 1 0 0 1 " << (imageIndex % 5) * 3 << " "
|
|
<< (imageIndex / 5) * 3 << " cm /" << name << " Do Q\n";
|
|
|
|
const uint8_t shade = static_cast<uint8_t>((pageIndex + imageIndex) % 255);
|
|
objects.push_back({imageId, makeImageBody({
|
|
"",
|
|
imageId,
|
|
1,
|
|
1,
|
|
"/DeviceRGB",
|
|
"/FlateDecode",
|
|
flate({shade, static_cast<uint8_t>(255 - shade), 90}),
|
|
std::nullopt,
|
|
})});
|
|
}
|
|
|
|
objects.push_back({contentId, makeStreamBody("", textBody(content.str()))});
|
|
|
|
std::ostringstream page;
|
|
page << "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 20 20]"
|
|
<< " /Resources << /XObject << " << xobjects.str() << ">> >>"
|
|
<< " /Contents " << contentId << " 0 R >>";
|
|
objects.push_back({pageId, textBody(page.str())});
|
|
}
|
|
|
|
std::ostringstream kids;
|
|
for (int pageId : pageIds) {
|
|
kids << pageId << " 0 R ";
|
|
}
|
|
objects.push_back({2, textBody("<< /Type /Pages /Kids [" + kids.str() +
|
|
"] /Count " + std::to_string(kPages) + " >>")});
|
|
|
|
const auto pdf = makePdf(std::move(objects));
|
|
|
|
auto verifyCustomDecode = [&](std::string_view phase) {
|
|
QPDF qpdf;
|
|
qpdf.processMemoryFile("image-xobject-stress", reinterpret_cast<const char*>(pdf.data()), pdf.size());
|
|
auto pages = qpdf.getAllPages();
|
|
ASSERT_EQ(pages.size(), static_cast<size_t>(kPages)) << phase;
|
|
|
|
size_t decodedImages = 0;
|
|
for (size_t pageIndex = 0; pageIndex < pages.size(); ++pageIndex) {
|
|
const auto& page = pages[pageIndex];
|
|
const std::string content = decodedStream(page.getKey("/Contents"));
|
|
Lexer lexer(content);
|
|
auto tokens = lexer.tokenize();
|
|
ContentParser parser(tokens);
|
|
auto operations = parser.parse();
|
|
size_t doOps = 0;
|
|
std::vector<std::string> doNames;
|
|
for (const auto& operation : operations) {
|
|
if (operation.op == "Do") {
|
|
++doOps;
|
|
if (!operation.operands.empty() && operation.operands.back()->type == AstNodeType::Name) {
|
|
doNames.push_back(operation.operands.back()->stringValue);
|
|
}
|
|
}
|
|
}
|
|
QpdfResourceResolver resolver(page.getKey("/Resources"));
|
|
std::vector<std::string> unresolved;
|
|
for (int imageIndex = 0; imageIndex < kImagesPerPage; ++imageIndex) {
|
|
const std::string name = "Im" + std::to_string(imageIndex);
|
|
if (resolver.resolveXObject(name).type != XObjectType::Image) {
|
|
unresolved.push_back(name);
|
|
}
|
|
}
|
|
auto pageObjects = buildPageObjects(page);
|
|
auto parsedImages = imageObjects(pageObjects);
|
|
std::vector<std::string> emittedNames;
|
|
for (const auto* image : parsedImages) {
|
|
emittedNames.push_back(image->name);
|
|
}
|
|
ASSERT_EQ(parsedImages.size(), static_cast<size_t>(kImagesPerPage))
|
|
<< phase << " page=" << pageIndex
|
|
<< " doOps=" << doOps
|
|
<< " doNames=" << ::testing::PrintToString(doNames)
|
|
<< " unresolved=" << ::testing::PrintToString(unresolved)
|
|
<< " emitted=" << ::testing::PrintToString(emittedNames)
|
|
<< " decoded=[" << content << "]"
|
|
<< " resources=[" << page.getKey("/Resources").unparse() << "]";
|
|
for (const auto* image : parsedImages) {
|
|
EXPECT_EQ(image->pixelData.size(), 4u) << phase << " page=" << pageIndex;
|
|
}
|
|
decodedImages += parsedImages.size();
|
|
}
|
|
EXPECT_EQ(decodedImages, static_cast<size_t>(kPages * kImagesPerPage)) << phase;
|
|
};
|
|
|
|
verifyCustomDecode("before-pdfium-render");
|
|
|
|
auto document = PdfDocument::loadFromMemory(pdf);
|
|
if (!document) {
|
|
GTEST_SKIP() << "PDFium load unavailable in this build";
|
|
}
|
|
ASSERT_EQ((*document)->pageCount(), kPages);
|
|
for (int i = 0; i < kPages; ++i) {
|
|
auto page = (*document)->getPage(i);
|
|
ASSERT_TRUE(page);
|
|
auto rendered = (*page)->renderRegionRaw(72, 0.0, (*page)->height());
|
|
ASSERT_TRUE(rendered);
|
|
}
|
|
document = {};
|
|
|
|
verifyCustomDecode("after-pdfium-render");
|
|
}
|