Files
pdf/engine/tests/image_xobject_test.cpp
T
2026-06-19 17:08:36 +05:30

754 lines
26 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);
}
} // namespace
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);
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 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 = {};
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));
size_t decodedImages = 0;
for (const auto& page : pages) {
auto pageObjects = buildPageObjects(page);
auto parsedImages = imageObjects(pageObjects);
ASSERT_EQ(parsedImages.size(), static_cast<size_t>(kImagesPerPage));
for (const auto* image : parsedImages) {
EXPECT_EQ(image->pixelData.size(), 4u);
}
decodedImages += parsedImages.size();
}
EXPECT_EQ(decodedImages, static_cast<size_t>(kPages * kImagesPerPage));
}