#include #include #include #include "../src/parser/content_builder.hpp" #include "../src/parser/lexer.hpp" #include "../src/parser/parser.hpp" #include "../src/qpdf/qpdf_resource_resolver.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 body; }; struct ImageSpec { std::string name; int id = 0; int width = 0; int height = 0; std::string colorSpace = "/DeviceRGB"; std::string filter; std::vector streamData; std::optional smaskId; }; void appendText(std::vector& out, std::string_view text) { out.insert(out.end(), text.begin(), text.end()); } std::vector textBody(const std::string& text) { return std::vector(text.begin(), text.end()); } std::vector makeStreamBody(const std::string& dictionaryEntries, const std::vector& streamData) { std::vector 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 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 makePdf(std::vector 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 offsets(static_cast(maxId) + 1, 0); std::vector pdf; appendText(pdf, "%PDF-1.4\n%\xFF\xFF\xFF\xFF\n"); for (const auto& object : objects) { offsets[static_cast(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(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 buildSinglePagePdf(int pageWidth, int pageHeight, const std::string& content, const std::vector& images) { std::ostringstream xobjects; for (const auto& image : images) { if (!image.name.empty()) { xobjects << "/" << image.name << " " << image.id << " 0 R "; } } std::vector 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 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 flate(const std::vector& raw) { uLongf size = compressBound(static_cast(raw.size())); std::vector compressed(size); const int result = compress2(compressed.data(), &size, raw.data(), static_cast(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(cinfo->err); longjmp(manager->jump, 1); } std::vector encodeJpegRgb(const std::vector& 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(width); cinfo.image_height = static_cast(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( reinterpret_cast(rgb.data() + cinfo.next_scanline * rowStride)); jpeg_write_scanlines(&cinfo, &row, 1); } jpeg_finish_compress(&cinfo); std::vector 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(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(buffer->getBuffer()), buffer->getSize()); decoded.push_back('\n'); } } return decoded; } std::vector> 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> buildFirstPageObjects(QPDF& qpdf, const std::vector& pdfBytes) { qpdf.processMemoryFile("image-xobject-test", reinterpret_cast(pdfBytes.data()), pdfBytes.size()); return buildPageObjects(qpdf.getAllPages().at(0)); } std::vector imageObjects(const std::vector>& objects) { std::vector images; for (const auto& object : objects) { if (object->getType() == ContentObjectType::Image) { images.push_back(static_cast(object.get())); } } return images; } Rgba decodedPixel(const ImageObject& image, int x, int y) { const auto index = (static_cast(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(r / count), static_cast(g / count), static_cast(b / count), static_cast(a / count), }; } Rgba pagePixel(const PageImage& image, int x, int yFromBottom) { const int row = image.height - 1 - yFromBottom; const auto index = (static_cast(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(r / count), static_cast(g / count), static_cast(b / count), static_cast(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 renderWithPdfium(const std::vector& 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 rgb(std::initializer_list values) { return std::vector(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 photoRgb; for (int y = 0; y < 6; ++y) { for (int x = 0; x < 8; ++x) { photoRgb.push_back(static_cast(35 + x * 24)); photoRgb.push_back(static_cast(40 + y * 28)); photoRgb.push_back(static_cast(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 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 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 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(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 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{"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 images; std::ostringstream content; const std::array 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(colors[i].r), static_cast(colors[i].g), static_cast(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(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 objects; objects.push_back({1, textBody("<< /Type /Catalog /Pages 2 0 R >>")}); int nextId = 3; std::vector 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((pageIndex + imageIndex) % 255); objects.push_back({imageId, makeImageBody({ "", imageId, 1, 1, "/DeviceRGB", "/FlateDecode", flate({shade, static_cast(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(pdf.data()), pdf.size()); auto pages = qpdf.getAllPages(); ASSERT_EQ(pages.size(), static_cast(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 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 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 emittedNames; for (const auto* image : parsedImages) { emittedNames.push_back(image->name); } ASSERT_EQ(parsedImages.size(), static_cast(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(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"); }