[poppler] CMakeLists.txt poppler/Annot.cc poppler/Annot.h poppler/Form.cc poppler/Form.h poppler/ImageEmbeddingUtils.cc poppler/ImageEmbeddingUtils.h poppler/PDFDoc.cc poppler/PDFDoc.h poppler/Stream.cc poppler/Stream.h poppler/XRef.cc poppler/XRef.h test/CMakeLists.txt test/image-embedding.cc

GitLab Mirror gitlab-mirror at kemper.freedesktop.org
Mon Nov 22 13:47:13 UTC 2021


 CMakeLists.txt                 |    1 
 poppler/Annot.cc               |   29 ++
 poppler/Annot.h                |    3 
 poppler/Form.cc                |   10 +
 poppler/Form.h                 |    5 
 poppler/ImageEmbeddingUtils.cc |  407 +++++++++++++++++++++++++++++++++++++++++
 poppler/ImageEmbeddingUtils.h  |   36 +++
 poppler/PDFDoc.cc              |   20 +-
 poppler/PDFDoc.h               |    5 
 poppler/Stream.cc              |   10 +
 poppler/Stream.h               |    8 
 poppler/XRef.cc                |   15 +
 poppler/XRef.h                 |    7 
 test/CMakeLists.txt            |   67 ++++++
 test/image-embedding.cc        |  106 ++++++++++
 15 files changed, 723 insertions(+), 6 deletions(-)

New commits:
commit d1070d73747d3c8771175c43e214f84537b65037
Author: Georgiy Sgibnev <georgiy at lab50.net>
Date:   Fri Jul 23 13:45:44 2021 +0300

    Image embedding API

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7e959305..88d6392c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -419,6 +419,7 @@ set(poppler_SRCS
   poppler/GfxState.cc
   poppler/GlobalParams.cc
   poppler/Hints.cc
+  poppler/ImageEmbeddingUtils.cc
   poppler/JArithmeticDecoder.cc
   poppler/JBIG2Stream.cc
   poppler/JSInfo.cc
diff --git a/poppler/Annot.cc b/poppler/Annot.cc
index 9f2289b5..6d1a2aea 100644
--- a/poppler/Annot.cc
+++ b/poppler/Annot.cc
@@ -4934,7 +4934,7 @@ bool AnnotAppearanceBuilder::drawSignatureFieldText(const FormFieldSignature *fi
 
     const GooString &leftText = field->getCustomAppearanceLeftContent();
     if (leftText.toStr().empty()) {
-        drawSignatureFieldText(contents, DefaultAppearance(_da), border, rect, xref, resourcesDict, 0, false /* don't center vertically */, false /* don't center horizontally */);
+        drawSignatureFieldText(contents, DefaultAppearance(_da), border, rect, xref, resourcesDict, 0, false /* don't center vertically */, false /* don't center horizontally */, field->getImageResource());
     } else {
         DefaultAppearance daLeft(_da);
         daLeft.setFontPtSize(field->getCustomAppearanceLeftFontSize());
@@ -4949,8 +4949,20 @@ bool AnnotAppearanceBuilder::drawSignatureFieldText(const FormFieldSignature *fi
     return true;
 }
 
+// Helper function for AnnotAppearanceBuilder::drawSignatureFieldText(). Registers a resource.
+// Argument resourceType should be "XObject" or "Font".
+static void registerResourceForWidget(const char *resourceType, Dict *resourcesDict, const char *resourceId, const Ref resourceRef, XRef *xref)
+{
+    Object childDictionaryObj = resourcesDict->lookup(resourceType);
+    if (!childDictionaryObj.isDict()) {
+        childDictionaryObj = Object(new Dict(xref));
+        resourcesDict->add(resourceType, childDictionaryObj.copy());
+    }
+    childDictionaryObj.dictSet(resourceId, Object(resourceRef));
+}
+
 void AnnotAppearanceBuilder::drawSignatureFieldText(const GooString &text, const DefaultAppearance &da, const AnnotBorder *border, const PDFRectangle *rect, XRef *xref, Dict *resourcesDict, double leftMargin, bool centerVertically,
-                                                    bool centerHorizontally)
+                                                    bool centerHorizontally, const Ref imageResourceRef)
 {
     double borderWidth = 0;
     append("q\n");
@@ -4967,6 +4979,19 @@ void AnnotAppearanceBuilder::drawSignatureFieldText(const GooString &text, const
     const double textmargin = borderWidth * 2;
     const double textwidth = width - 2 * textmargin;
 
+    // Print a background image.
+    if (imageResourceRef != Ref::INVALID()) {
+        static const char *imageResourceId = "SigImg";
+        registerResourceForWidget("XObject", resourcesDict, imageResourceId, imageResourceRef, xref);
+
+        Matrix matrix = { 1.0, 0.0, 0.0, 1.0, 0.0, 0.0 };
+        matrix.scale(width, height);
+        static const char *IMG_TMPL = "\nq {0:.1g} {1:.1g} {2:.1g} {3:.1g} {4:.1g} {5:.1g} cm /{6:s} Do Q\n";
+        const GooString *imgBuffer = GooString::format(IMG_TMPL, matrix.m[0], matrix.m[1], matrix.m[2], matrix.m[3], matrix.m[4], matrix.m[5], imageResourceId);
+        append(imgBuffer->c_str());
+        delete imgBuffer;
+    }
+
     // create a Helvetica fake font
     GfxFont *font = createAnnotDrawFont(xref, resourcesDict, da.getFontName().getName());
 
diff --git a/poppler/Annot.h b/poppler/Annot.h
index 745618db..22621c1e 100644
--- a/poppler/Annot.h
+++ b/poppler/Annot.h
@@ -597,7 +597,8 @@ private:
                              XRef *xref, Dict *resourcesDict);
     bool drawSignatureFieldText(const FormFieldSignature *field, const Form *form, const GfxResources *resources, const GooString *da, const AnnotBorder *border, const AnnotAppearanceCharacs *appearCharacs, const PDFRectangle *rect,
                                 XRef *xref, Dict *resourcesDict);
-    void drawSignatureFieldText(const GooString &text, const DefaultAppearance &da, const AnnotBorder *border, const PDFRectangle *rect, XRef *xref, Dict *resourcesDict, double leftMargin, bool centerVertically, bool centerHorizontally);
+    void drawSignatureFieldText(const GooString &text, const DefaultAppearance &da, const AnnotBorder *border, const PDFRectangle *rect, XRef *xref, Dict *resourcesDict, double leftMargin, bool centerVertically, bool centerHorizontally,
+                                const Ref imageResourceRef = Ref::INVALID());
     bool drawText(const GooString *text, const GooString *da, const GfxResources *resources, const AnnotBorder *border, const AnnotAppearanceCharacs *appearCharacs, const PDFRectangle *rect, bool multiline, int comb, int quadding,
                   bool txField, bool forceZapfDingbats, XRef *xref, bool password, Dict *resourcesDict, const char *defaultFallback = "Helvetica");
     void drawArrowPath(double x, double y, const Matrix &m, int orientation = 1);
diff --git a/poppler/Form.cc b/poppler/Form.cc
index fe39d3cb..46ed2cc9 100644
--- a/poppler/Form.cc
+++ b/poppler/Form.cc
@@ -2032,6 +2032,16 @@ void FormFieldSignature::setCustomAppearanceLeftFontSize(double size)
     customAppearanceLeftFontSize = size;
 }
 
+Ref FormFieldSignature::getImageResource() const
+{
+    return imageResource;
+}
+
+void FormFieldSignature::setImageResource(const Ref imageResourceA)
+{
+    imageResource = imageResourceA;
+}
+
 void FormFieldSignature::setCertificateInfo(std::unique_ptr<X509CertificateInfo> &certInfo)
 {
     certificate_info.swap(certInfo);
diff --git a/poppler/Form.h b/poppler/Form.h
index d88f0a0a..61174e83 100644
--- a/poppler/Form.h
+++ b/poppler/Form.h
@@ -624,6 +624,10 @@ public:
     double getCustomAppearanceLeftFontSize() const;
     void setCustomAppearanceLeftFontSize(double size);
 
+    // Background image (ref to an object of type XObject). Invalid ref if not required.
+    Ref getImageResource() const;
+    void setImageResource(const Ref imageResourceA);
+
     void setCertificateInfo(std::unique_ptr<X509CertificateInfo> &);
 
     FormWidget *getCreateWidget();
@@ -639,6 +643,7 @@ private:
     GooString customAppearanceContent;
     GooString customAppearanceLeftContent;
     double customAppearanceLeftFontSize = 20;
+    Ref imageResource = Ref::INVALID();
     std::unique_ptr<X509CertificateInfo> certificate_info;
 
     void print(int indent) override;
diff --git a/poppler/ImageEmbeddingUtils.cc b/poppler/ImageEmbeddingUtils.cc
new file mode 100644
index 00000000..a3dd5868
--- /dev/null
+++ b/poppler/ImageEmbeddingUtils.cc
@@ -0,0 +1,407 @@
+//========================================================================
+//
+// ImageEmbeddingUtils.cc
+//
+// Copyright (C) 2021 Georgiy Sgibnev <georgiy at sgibnev.com>. Work sponsored by lab50.net.
+//
+// This file is licensed under the GPLv2 or later
+//
+//========================================================================
+
+#include <config.h>
+
+#include <memory>
+#ifdef ENABLE_LIBJPEG
+extern "C" {
+#    include <jpeglib.h>
+}
+#endif
+#ifdef ENABLE_LIBPNG
+#    include <png.h>
+#endif
+
+#include "ImageEmbeddingUtils.h"
+#include "goo/gmem.h"
+#include "Object.h"
+#include "Array.h"
+#include "Error.h"
+#include "PDFDoc.h"
+
+namespace ImageEmbeddingUtils {
+
+static const uint8_t PNG_MAGIC_NUM[] = { 0x89, 0x50, 0x4e, 0x47 };
+static const uint8_t JPEG_MAGIC_NUM[] = { 0xff, 0xd8, 0xff };
+static const uint8_t JPEG2000_MAGIC_NUM[] = { 0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20 };
+static const Goffset MAX_MAGIC_NUM_SIZE = sizeof(JPEG2000_MAGIC_NUM);
+
+static bool checkMagicNum(const uint8_t *fileContent, const uint8_t *magicNum, const uint8_t size)
+{
+    return (memcmp(fileContent, magicNum, size) == 0);
+}
+
+// Transforms an image to XObject.
+class ImageEmbedder
+{
+protected:
+    static constexpr const char *DEVICE_GRAY = "DeviceGray";
+    static constexpr const char *DEVICE_RGB = "DeviceRGB";
+
+    int m_width;
+    int m_height;
+
+    ImageEmbedder(const int width, const int height) : m_width(width), m_height(height) { }
+
+    // Creates an object of type XObject. You own the returned ptr.
+    static Dict *createImageDict(XRef *xref, const char *colorSpace, const int width, const int height, const int bitsPerComponent)
+    {
+        Dict *imageDict = new Dict(xref);
+        imageDict->add("Type", Object(objName, "XObject"));
+        imageDict->add("Subtype", Object(objName, "Image"));
+        imageDict->add("ColorSpace", Object(objName, colorSpace));
+        imageDict->add("Width", Object(width));
+        imageDict->add("Height", Object(height));
+        imageDict->add("BitsPerComponent", Object(bitsPerComponent));
+        return imageDict;
+    }
+
+public:
+    ImageEmbedder() = delete;
+    ImageEmbedder(const ImageEmbedder &) = delete;
+    ImageEmbedder &operator=(const ImageEmbedder &) = delete;
+    virtual ~ImageEmbedder();
+
+    // Call it only once.
+    // Returns ref to a new object or Ref::INVALID.
+    virtual Ref embedImage(XRef *xref) = 0;
+};
+
+ImageEmbedder::~ImageEmbedder() { }
+
+#ifdef ENABLE_LIBPNG
+// Transforms a PNG image to XObject.
+class PngEmbedder : public ImageEmbedder
+{
+    // LibpngInputStream is a simple replacement for GInputStream.
+    // Used with png_set_read_fn().
+    class LibpngInputStream
+    {
+        uint8_t *m_fileContent;
+        uint8_t *m_iterator;
+        png_size_t m_remainingSize;
+
+        void read(png_bytep out, const png_size_t size)
+        {
+            const png_size_t fixedSize = (m_remainingSize >= size) ? size : m_remainingSize;
+            memcpy(out, m_iterator, fixedSize);
+            m_iterator += fixedSize;
+            m_remainingSize -= fixedSize;
+        }
+
+    public:
+        // LibpngInputStream takes ownership over the buffer.
+        LibpngInputStream(uint8_t *fileContent, const Goffset size) : m_fileContent(fileContent), m_iterator(fileContent), m_remainingSize(size) { }
+        LibpngInputStream() = delete;
+        LibpngInputStream(const LibpngInputStream &) = delete;
+        LibpngInputStream &operator=(const LibpngInputStream &) = delete;
+        ~LibpngInputStream() { gfree(m_fileContent); }
+
+        // Pass this static function to png_set_read_fn().
+        static void readCallback(png_structp png, png_bytep out, png_size_t size)
+        {
+            LibpngInputStream *stream = (LibpngInputStream *)png_get_io_ptr(png);
+            if (stream) {
+                stream->read(out, size);
+            }
+        }
+    };
+
+    png_structp m_png;
+    png_infop m_info;
+    LibpngInputStream *m_stream;
+    const png_byte m_type;
+    const bool m_hasAlpha;
+    // Number of color channels.
+    const png_byte m_n;
+    // Number of color channels excluding alpha channel. Should be 1 or 3.
+    const png_byte m_nWithoutAlpha;
+    // Shold be 8 or 16.
+    const png_byte m_bitDepth;
+    // Should be 1 or 2.
+    const png_byte m_byteDepth;
+
+    PngEmbedder(png_structp png, png_infop info, LibpngInputStream *stream)
+        : ImageEmbedder(png_get_image_width(png, info), png_get_image_height(png, info)),
+          m_png(png),
+          m_info(info),
+          m_stream(stream),
+          m_type(png_get_color_type(m_png, m_info)),
+          m_hasAlpha(m_type & PNG_COLOR_MASK_ALPHA),
+          m_n(png_get_channels(m_png, m_info)),
+          m_nWithoutAlpha(m_hasAlpha ? m_n - 1 : m_n),
+          m_bitDepth(png_get_bit_depth(m_png, m_info)),
+          m_byteDepth(m_bitDepth / 8)
+    {
+    }
+
+    // Reads pixels into mainBuffer (RGB/gray channels) and maskBuffer (alpha channel).
+    void readPixels(png_bytep mainBuffer, png_bytep maskBuffer)
+    {
+        // Read pixels from m_png.
+        const int rowSize = png_get_rowbytes(m_png, m_info);
+        png_bytepp pixels = new png_bytep[m_height];
+        for (int y = 0; y < m_height; y++) {
+            pixels[y] = new png_byte[rowSize];
+        }
+        png_read_image(m_png, pixels);
+
+        // Copy pixels into mainBuffer and maskBuffer.
+        const png_byte pixelSizeWithoutAlpha = m_nWithoutAlpha * m_byteDepth;
+        for (int y = 0; y < m_height; y++) {
+            png_bytep row = pixels[y];
+            for (int x = 0; x < m_width; x++) {
+                memcpy(mainBuffer, row, pixelSizeWithoutAlpha);
+                mainBuffer += pixelSizeWithoutAlpha;
+                row += pixelSizeWithoutAlpha;
+                if (m_hasAlpha) {
+                    memcpy(maskBuffer, row, m_byteDepth);
+                    maskBuffer += m_byteDepth;
+                    row += m_byteDepth;
+                }
+            }
+        }
+
+        // Cleanup.
+        for (int y = 0; y < m_height; y++) {
+            delete[] pixels[y];
+        }
+        delete[] pixels;
+    }
+
+    // Supportive function for create().
+    // We don't want to deal with palette images.
+    // We don't want to deal with 1/2/4-bit samples.
+    static void fixPng(png_structp png, png_infop info)
+    {
+        const png_byte colorType = png_get_color_type(png, info);
+        const png_byte bitDepth = png_get_bit_depth(png, info);
+
+        bool updateRequired = false;
+        if (colorType == PNG_COLOR_TYPE_PALETTE) {
+            png_set_palette_to_rgb(png);
+            updateRequired = true;
+        }
+        if ((colorType == PNG_COLOR_TYPE_GRAY) && (bitDepth < 8)) {
+            png_set_expand_gray_1_2_4_to_8(png);
+            updateRequired = true;
+        }
+        if (png_get_valid(png, info, PNG_INFO_tRNS)) {
+            png_set_tRNS_to_alpha(png);
+            updateRequired = true;
+        }
+        if (bitDepth < 8) {
+            png_set_packing(png);
+            updateRequired = true;
+        }
+        if (updateRequired) {
+            png_read_update_info(png, info);
+        }
+    }
+
+public:
+    PngEmbedder() = delete;
+    PngEmbedder(const PngEmbedder &) = delete;
+    PngEmbedder &operator=(const PngEmbedder &) = delete;
+    ~PngEmbedder() override
+    {
+        png_destroy_read_struct(&m_png, &m_info, nullptr);
+        delete m_stream;
+    }
+
+    Ref embedImage(XRef *xref) override;
+
+    // The function takes ownership over fileContent.
+    static std::unique_ptr<ImageEmbedder> create(uint8_t *fileContent, const Goffset fileSize)
+    {
+        png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
+        if (png == nullptr) {
+            error(errInternal, -1, "Couldn't load PNG. png_create_read_struct() failed");
+            gfree(fileContent);
+            return nullptr;
+        }
+        png_infop info = png_create_info_struct(png);
+        if (info == nullptr) {
+            error(errInternal, -1, "Couldn't load PNG. png_create_info_struct() failed");
+            png_destroy_read_struct(&png, nullptr, nullptr);
+            gfree(fileContent);
+            return nullptr;
+        }
+        if (setjmp(png_jmpbuf(png))) {
+            error(errInternal, -1, "Couldn't load PNG. Failed to set up error handling for reading PNG");
+            png_destroy_read_struct(&png, &info, nullptr);
+            gfree(fileContent);
+            return nullptr;
+        }
+
+        LibpngInputStream *stream = new LibpngInputStream(fileContent, fileSize);
+        png_set_read_fn(png, stream, LibpngInputStream::readCallback);
+        png_read_info(png, info);
+        fixPng(png, info);
+        const png_byte bitDepth = png_get_bit_depth(png, info);
+        if ((bitDepth != 8) && (bitDepth != 16)) {
+            error(errInternal, -1, "Couldn't load PNG. Fixing bit depth failed");
+            png_destroy_read_struct(&png, &info, nullptr);
+            delete stream;
+            return nullptr;
+        }
+        return std::unique_ptr<ImageEmbedder>(new PngEmbedder(png, info, stream));
+    }
+};
+
+Ref PngEmbedder::embedImage(XRef *xref)
+{
+    // Read pixels.
+    const Goffset mainBufferSize = m_width * m_height * m_nWithoutAlpha * m_byteDepth;
+    png_bytep mainBuffer = (png_bytep)gmalloc(mainBufferSize);
+    const Goffset maskBufferSize = m_width * m_height * m_byteDepth;
+    png_bytep maskBuffer = (m_hasAlpha) ? (png_bytep)gmalloc(maskBufferSize) : nullptr;
+    readPixels(mainBuffer, maskBuffer);
+
+    // Create a mask XObject and a main XObject.
+    const char *colorSpace = ((m_type == PNG_COLOR_TYPE_GRAY) || (m_type == PNG_COLOR_TYPE_GRAY_ALPHA)) ? DEVICE_GRAY : DEVICE_RGB;
+    Dict *baseImageDict = createImageDict(xref, colorSpace, m_width, m_height, m_bitDepth);
+    if (m_hasAlpha) {
+        Dict *maskImageDict = createImageDict(xref, DEVICE_GRAY, m_width, m_height, m_bitDepth);
+        Ref maskImageRef = xref->addStreamObject(maskImageDict, maskBuffer, maskBufferSize);
+        baseImageDict->add("SMask", Object(maskImageRef));
+    }
+    return xref->addStreamObject(baseImageDict, mainBuffer, mainBufferSize);
+}
+#endif
+
+#ifdef ENABLE_LIBJPEG
+
+struct JpegErrorManager
+{
+    jpeg_error_mgr pub;
+    jmp_buf setjmpBuffer;
+};
+
+// Note: an address of pub is equal to an address of a JpegErrorManager instance.
+static void jpegExitErrorHandler(j_common_ptr info)
+{
+    JpegErrorManager *errorManager = (JpegErrorManager *)info->err;
+    (*errorManager->pub.output_message)(info);
+    // Jump to the setjmp point.
+    longjmp(errorManager->setjmpBuffer, 1);
+}
+
+// Transforms a JPEG image to XObject.
+class JpegEmbedder : public ImageEmbedder
+{
+    uint8_t *m_fileContent;
+    Goffset m_fileSize;
+
+    JpegEmbedder(const int width, const int height, uint8_t *fileContent, const Goffset fileSize) : ImageEmbedder(width, height), m_fileContent(fileContent), m_fileSize(fileSize) { }
+
+public:
+    JpegEmbedder() = delete;
+    JpegEmbedder(const JpegEmbedder &) = delete;
+    JpegEmbedder &operator=(const JpegEmbedder &) = delete;
+    ~JpegEmbedder() override
+    {
+        if (m_fileContent) {
+            gfree(m_fileContent);
+        }
+    }
+
+    Ref embedImage(XRef *xref) override;
+
+    // The function takes ownership over fileContent.
+    static std::unique_ptr<ImageEmbedder> create(uint8_t *fileContent, const Goffset fileSize)
+    {
+        jpeg_decompress_struct info;
+        JpegErrorManager errorManager;
+        info.err = jpeg_std_error(&errorManager.pub);
+        errorManager.pub.error_exit = jpegExitErrorHandler;
+        if (setjmp(errorManager.setjmpBuffer)) {
+            // The setjmp point.
+            jpeg_destroy_decompress(&info);
+            error(errInternal, -1, "libjpeg failed to process the file");
+            return nullptr;
+        }
+
+        jpeg_create_decompress(&info);
+        jpeg_mem_src(&info, fileContent, fileSize);
+        jpeg_read_header(&info, TRUE);
+        jpeg_start_decompress(&info);
+        auto result = std::unique_ptr<ImageEmbedder>(new JpegEmbedder(info.output_width, info.output_height, fileContent, fileSize));
+        jpeg_abort_decompress(&info);
+        jpeg_destroy_decompress(&info);
+        return result;
+    }
+};
+
+Ref JpegEmbedder::embedImage(XRef *xref)
+{
+    if (m_fileContent == nullptr) {
+        return Ref::INVALID();
+    }
+    Dict *baseImageDict = createImageDict(xref, DEVICE_RGB, m_width, m_height, 8);
+    baseImageDict->add("Filter", Object(objName, "DCTDecode"));
+    Ref baseImageRef = xref->addStreamObject(baseImageDict, m_fileContent, m_fileSize);
+    m_fileContent = nullptr;
+    return baseImageRef;
+}
+#endif
+
+Ref embed(XRef *xref, const GooFile &imageFile)
+{
+    // Load the image file.
+    const Goffset fileSize = imageFile.size();
+    uint8_t *fileContent = (uint8_t *)gmalloc(fileSize);
+    const Goffset bytesRead = imageFile.read((char *)fileContent, fileSize, 0);
+    if ((bytesRead != fileSize) || (fileSize < MAX_MAGIC_NUM_SIZE)) {
+        gfree(fileContent);
+        error(errIO, -1, "Couldn't load the image file");
+        return Ref::INVALID();
+    }
+
+    std::unique_ptr<ImageEmbedder> embedder;
+    if (checkMagicNum(fileContent, PNG_MAGIC_NUM, sizeof(PNG_MAGIC_NUM))) {
+#ifdef ENABLE_LIBPNG
+        embedder = PngEmbedder::create(fileContent, fileSize);
+#else
+        error(errUnimplemented, -1, "PNG format is not supported");
+#endif
+    } else if (checkMagicNum(fileContent, JPEG_MAGIC_NUM, sizeof(JPEG_MAGIC_NUM))) {
+#ifdef ENABLE_LIBJPEG
+        embedder = JpegEmbedder::create(fileContent, fileSize);
+#else
+        error(errUnimplemented, -1, "JPEG format is not supported");
+#endif
+    } else if (checkMagicNum(fileContent, JPEG2000_MAGIC_NUM, sizeof(JPEG2000_MAGIC_NUM))) {
+        // TODO: implement JPEG2000 support using libopenjpeg2.
+        error(errUnimplemented, -1, "JPEG2000 format is not supported");
+        return Ref::INVALID();
+    } else {
+        error(errUnimplemented, -1, "Image format is not supported");
+        return Ref::INVALID();
+    }
+
+    if (!embedder) {
+        return Ref::INVALID();
+    }
+    return embedder->embedImage(xref);
+}
+
+Ref embed(XRef *xref, const std::string &imagePath)
+{
+    std::unique_ptr<GooFile> imageFile(GooFile::open(imagePath));
+    if (!imageFile) {
+        error(errIO, -1, "Couldn't open {0:s}", imagePath.c_str());
+        return Ref::INVALID();
+    }
+    return embed(xref, *imageFile);
+}
+
+}
diff --git a/poppler/ImageEmbeddingUtils.h b/poppler/ImageEmbeddingUtils.h
new file mode 100644
index 00000000..7fafa7f5
--- /dev/null
+++ b/poppler/ImageEmbeddingUtils.h
@@ -0,0 +1,36 @@
+//========================================================================
+//
+// ImageEmbeddingUtils.h
+//
+// Copyright (C) 2021 Georgiy Sgibnev <georgiy at sgibnev.com>. Work sponsored by lab50.net.
+//
+// This file is licensed under the GPLv2 or later
+//
+//========================================================================
+
+#ifndef IMAGE_EMBEDDING_UTILS_H
+#define IMAGE_EMBEDDING_UTILS_H
+
+#include <string>
+
+#include "poppler_private_export.h"
+
+class GooFile;
+struct Ref;
+class XRef;
+
+namespace ImageEmbeddingUtils {
+
+// Creates a new base image (an object of type XObject referred to in a resource dictionary).
+// Supported formats: PNG, JPEG.
+// Args:
+//     xref: Document's xref.
+//     imageFile: An image file to embed.
+// Returns ref to a new object or Ref::INVALID.
+Ref POPPLER_PRIVATE_EXPORT embed(XRef *xref, const GooFile &imageFile);
+
+// Same as above, but imagePath is a path to an image file.
+Ref POPPLER_PRIVATE_EXPORT embed(XRef *xref, const std::string &imagePath);
+
+}
+#endif
diff --git a/poppler/PDFDoc.cc b/poppler/PDFDoc.cc
index 86c97486..e75d6405 100644
--- a/poppler/PDFDoc.cc
+++ b/poppler/PDFDoc.cc
@@ -94,6 +94,7 @@
 #include "Hints.h"
 #include "UTF.h"
 #include "JSInfo.h"
+#include "ImageEmbeddingUtils.h"
 
 //------------------------------------------------------------------------
 
@@ -1452,6 +1453,10 @@ void PDFDoc::writeObject(Object *obj, OutStream *outStr, XRef *xRef, unsigned in
             stream->getDict()->set("Length", Object(tmp));
 
             // Remove Stream encoding
+            AutoFreeMemStream *internalStream = dynamic_cast<AutoFreeMemStream *>(stream);
+            if (internalStream && internalStream->isFilterRemovalForbidden()) {
+                removeFilter = false;
+            }
             if (removeFilter) {
                 stream->getDict()->remove("Filter");
             }
@@ -2122,9 +2127,20 @@ bool PDFDoc::hasJavascript()
 }
 
 bool PDFDoc::sign(const char *saveFilename, const char *certNickname, const char *password, GooString *partialFieldName, int page, const PDFRectangle &rect, const GooString &signatureText, const GooString &signatureTextLeft,
-                  double fontSize, std::unique_ptr<AnnotColor> &&fontColor, double borderWidth, std::unique_ptr<AnnotColor> &&borderColor, std::unique_ptr<AnnotColor> &&backgroundColor, const GooString *reason, const GooString *location)
+                  double fontSize, std::unique_ptr<AnnotColor> &&fontColor, double borderWidth, std::unique_ptr<AnnotColor> &&borderColor, std::unique_ptr<AnnotColor> &&backgroundColor, const GooString *reason, const GooString *location,
+                  const std::string &imagePath)
 {
     ::Page *destPage = getPage(page);
+    if (destPage == nullptr) {
+        return false;
+    }
+    Ref imageResourceRef = Ref::INVALID();
+    if (!imagePath.empty()) {
+        imageResourceRef = ImageEmbeddingUtils::embed(xref, imagePath);
+        if (imageResourceRef == Ref::INVALID()) {
+            return false;
+        }
+    }
 
     const DefaultAppearance da { { objName, "SigFont" }, fontSize, std::move(fontColor) };
 
@@ -2147,9 +2163,9 @@ bool PDFDoc::sign(const char *saveFilename, const char *certNickname, const char
     catalog->addFormToAcroForm(ref);
 
     std::unique_ptr<::FormFieldSignature> field = std::make_unique<::FormFieldSignature>(this, Object(annotObj.getDict()), ref, nullptr, nullptr);
-
     field->setCustomAppearanceContent(signatureText);
     field->setCustomAppearanceLeftContent(signatureTextLeft);
+    field->setImageResource(imageResourceRef);
 
     Object refObj(ref);
     AnnotWidget *signatureAnnot = new AnnotWidget(this, &annotObj, &refObj, field.get());
diff --git a/poppler/PDFDoc.h b/poppler/PDFDoc.h
index b1801aa4..cea59282 100644
--- a/poppler/PDFDoc.h
+++ b/poppler/PDFDoc.h
@@ -332,8 +332,11 @@ public:
 
     // Arguments signatureText and signatureTextLeft are UTF-16 big endian strings with BOM.
     // Arguments reason and location are UTF-16 big endian strings with BOM. An empty string and nullptr are acceptable too.
+    // Argument imagePath is a background image (a path to a file).
+    // sign() takes ownership of partialFieldName.
     bool sign(const char *saveFilename, const char *certNickname, const char *password, GooString *partialFieldName, int page, const PDFRectangle &rect, const GooString &signatureText, const GooString &signatureTextLeft, double fontSize,
-              std::unique_ptr<AnnotColor> &&fontColor, double borderWidth, std::unique_ptr<AnnotColor> &&borderColor, std::unique_ptr<AnnotColor> &&backgroundColor, const GooString *reason = nullptr, const GooString *location = nullptr);
+              std::unique_ptr<AnnotColor> &&fontColor, double borderWidth, std::unique_ptr<AnnotColor> &&borderColor, std::unique_ptr<AnnotColor> &&backgroundColor, const GooString *reason = nullptr, const GooString *location = nullptr,
+              const std::string &imagePath = "");
 
 private:
     // insert referenced objects in XRef
diff --git a/poppler/Stream.cc b/poppler/Stream.cc
index 406870e7..a9342842 100644
--- a/poppler/Stream.cc
+++ b/poppler/Stream.cc
@@ -1103,6 +1103,16 @@ AutoFreeMemStream::~AutoFreeMemStream()
     gfree(buf);
 }
 
+bool AutoFreeMemStream::isFilterRemovalForbidden() const
+{
+    return filterRemovalForbidden;
+}
+
+void AutoFreeMemStream::setFilterRemovalForbidden(bool forbidden)
+{
+    filterRemovalForbidden = forbidden;
+}
+
 //------------------------------------------------------------------------
 // EmbedStream
 //------------------------------------------------------------------------
diff --git a/poppler/Stream.h b/poppler/Stream.h
index 303435ed..b6e9b3bf 100644
--- a/poppler/Stream.h
+++ b/poppler/Stream.h
@@ -734,9 +734,17 @@ public:
 
 class AutoFreeMemStream : public BaseMemStream<char>
 {
+    bool filterRemovalForbidden;
+
 public:
+    // AutoFreeMemStream takes ownership over the buffer.
+    // The buffer should be created using gmalloc().
     AutoFreeMemStream(char *bufA, Goffset startA, Goffset lengthA, Object &&dictA) : BaseMemStream(bufA, startA, lengthA, std::move(dictA)) { }
     ~AutoFreeMemStream() override;
+
+    // A hack to deal with the strange behaviour of PDFDoc::writeObject().
+    bool isFilterRemovalForbidden() const;
+    void setFilterRemovalForbidden(bool forbidden);
 };
 
 //------------------------------------------------------------------------
diff --git a/poppler/XRef.cc b/poppler/XRef.cc
index 3e0e95fa..46433285 100644
--- a/poppler/XRef.cc
+++ b/poppler/XRef.cc
@@ -31,6 +31,7 @@
 // Copyright (C) 2020 Klarälvdalens Datakonsult AB, a KDAB Group company, <info at kdab.com>. Work sponsored by Technische Universität Dresden
 // Copyright (C) 2010 William Bader <william at newspapersystems.com>
 // Copyright (C) 2021 Mahmoud Khalil <mahmoudkhalil11 at gmail.com>
+// Copyright (C) 2021 Georgiy Sgibnev <georgiy at sgibnev.com>. Work sponsored by lab50.net.
 //
 // To see a description of the changes please see the Changelog file that
 // came with your tarball or type make ChangeLog if you are building from git
@@ -1455,6 +1456,20 @@ void XRef::removeIndirectObject(Ref r)
     setModified();
 }
 
+Ref XRef::addStreamObject(Dict *dict, char *buffer, const Goffset bufferSize)
+{
+    dict->add("Length", Object((int)bufferSize));
+    AutoFreeMemStream *stream = new AutoFreeMemStream(buffer, 0, bufferSize, Object(dict));
+    stream->setFilterRemovalForbidden(true);
+    Object streamObj((Stream *)stream);
+    return addIndirectObject(&streamObj);
+}
+
+Ref XRef::addStreamObject(Dict *dict, uint8_t *buffer, const Goffset bufferSize)
+{
+    return addStreamObject(dict, (char *)buffer, bufferSize);
+}
+
 void XRef::writeXRef(XRef::XRefWriter *writer, bool writeAllEntries)
 {
     // create free entries linked-list
diff --git a/poppler/XRef.h b/poppler/XRef.h
index 76069a1b..a40c7312 100644
--- a/poppler/XRef.h
+++ b/poppler/XRef.h
@@ -26,6 +26,7 @@
 // Copyright (C) 2018 Adam Reichold <adam.reichold at t-online.de>
 // Copyright (C) 2018 Marek Kasik <mkasik at redhat.com>
 // Copyright (C) 2021 Mahmoud Khalil <mahmoudkhalil11 at gmail.com>
+// Copyright (C) 2021 Georgiy Sgibnev <georgiy at sgibnev.com>. Work sponsored by lab50.net.
 //
 // To see a description of the changes please see the Changelog file that
 // came with your tarball or type make ChangeLog if you are building from git
@@ -208,6 +209,12 @@ public:
     void removeIndirectObject(Ref r);
     void add(int num, int gen, Goffset offs, bool used);
     void add(Ref ref, Goffset offs, bool used);
+    // Adds a stream object using AutoFreeMemStream.
+    // The function takes ownership over dict and buffer.
+    // The buffer should be created using gmalloc().
+    // Returns ref to a new object.
+    Ref addStreamObject(Dict *dict, char *buffer, const Goffset bufferSize);
+    Ref addStreamObject(Dict *dict, uint8_t *buffer, const Goffset bufferSize);
 
     // Output XRef table to stream
     void writeTableToFile(OutStream *outStr, bool writeAllEntries);
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index b9251e04..a365babb 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -55,4 +55,71 @@ set (pdf_fullrewrite_SRCS
 add_executable(pdf-fullrewrite ${pdf_fullrewrite_SRCS})
 target_link_libraries(pdf-fullrewrite poppler)
 
+# Test image embedding API.
+if(ENABLE_LIBJPEG AND ENABLE_LIBPNG)
+  set(image_embedding_SRCS
+    image-embedding.cc
+    ../utils/parseargs.cc
+  )
+  add_executable(image-embedding ${image_embedding_SRCS})
+  target_link_libraries(image-embedding poppler)
 
+  set(INPUT_PDF ${TESTDATADIR}/unittestcases/xr01.pdf)
+  set(IMG_DIR ${TESTDATADIR}/unittestcases/images)
+  set(IMAGE_EMBEDDING_PATH ${EXECUTABLE_OUTPUT_PATH}/image-embedding)
+  add_test(
+    NAME embed-png-g1
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-g1.png -depth 8 -colorspace DeviceGray
+  )
+  add_test(
+    NAME embed-png-g2
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-g2.png -depth 8 -colorspace DeviceGray
+  )
+  add_test(
+    NAME embed-png-g4
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-g4.png -depth 8 -colorspace DeviceGray
+  )
+  add_test(
+    NAME embed-png-g8
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-g8.png -depth 8 -colorspace DeviceGray
+  )
+  add_test(
+    NAME embed-png-g16
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-g16.png -depth 16 -colorspace DeviceGray
+  )
+  add_test(
+    NAME embed-png-ga8
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-ga8.png -depth 8 -colorspace DeviceGray -smask
+  )
+  add_test(
+    NAME embed-png-ga16
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-ga16.png -depth 16 -colorspace DeviceGray -smask
+  )
+  add_test(
+    NAME embed-png-palette
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-palette.png -depth 8 -colorspace DeviceRGB
+  )
+  add_test(
+    NAME embed-png-rgb8
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-rgb8.png -depth 8 -colorspace DeviceRGB
+  )
+  add_test(
+    NAME embed-png-rgb16
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-rgb16.png -depth 16 -colorspace DeviceRGB
+  )
+  add_test(
+    NAME embed-png-rgba8
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-rgba8.png -depth 8 -colorspace DeviceRGB -smask
+  )
+  add_test(
+    NAME embed-png-rgba16
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/png-rgba16.png -depth 16 -colorspace DeviceRGB -smask
+  )
+  add_test(
+    NAME embed-jpeg
+    COMMAND ${IMAGE_EMBEDDING_PATH} ${INPUT_PDF} ${IMG_DIR}/jpeg.jpg -depth 8 -colorspace DeviceRGB -filter DCTDecode
+  )
+  unset(IMAGE_EMBEDDING_PATH)
+  unset(IMG_DIR)
+  unset(INPUT_PDF)
+endif()
diff --git a/test/image-embedding.cc b/test/image-embedding.cc
new file mode 100644
index 00000000..6ffcdb2d
--- /dev/null
+++ b/test/image-embedding.cc
@@ -0,0 +1,106 @@
+//========================================================================
+//
+// image-embedding.cc
+// A test util to check ImageEmbeddingUtils::embed().
+//
+// This file is licensed under the GPLv2 or later
+//
+// Copyright (C) 2021 Georgiy Sgibnev <georgiy at sgibnev.com>. Work sponsored by lab50.net.
+//
+//========================================================================
+
+#include <config.h>
+#include <cstdio>
+#include <string>
+
+#include "utils/parseargs.h"
+#include "goo/GooString.h"
+#include "Object.h"
+#include "Dict.h"
+#include "PDFDoc.h"
+#include "PDFDocFactory.h"
+#include "ImageEmbeddingUtils.h"
+
+static int depth = 0;
+static GooString colorSpace;
+static GooString filter;
+static bool smask = false;
+static bool printHelp = false;
+
+static const ArgDesc argDesc[] = { { "-depth", argInt, &depth, 0, "XObject's property 'BitsPerComponent'" },
+                                   { "-colorspace", argGooString, &colorSpace, 0, "XObject's property 'ColorSpace'" },
+                                   { "-filter", argGooString, &filter, 0, "XObject's property 'Filter'" },
+                                   { "-smask", argFlag, &smask, 0, "SMask should exist" },
+                                   { "-h", argFlag, &printHelp, 0, "print usage information" },
+                                   { "-help", argFlag, &printHelp, 0, "print usage information" },
+                                   { "--help", argFlag, &printHelp, 0, "print usage information" },
+                                   { "-?", argFlag, &printHelp, 0, "print usage information" },
+                                   {} };
+
+int main(int argc, char *argv[])
+{
+    // Parse args.
+    const bool ok = parseArgs(argDesc, &argc, argv);
+    if (!ok || (argc != 3) || printHelp) {
+        printUsage(argv[0], "PDF-FILE IMAGE-FILE", argDesc);
+        return (printHelp) ? 0 : 1;
+    }
+    const GooString docPath(argv[1]);
+    const GooString imagePath(argv[2]);
+
+    auto doc = std::unique_ptr<PDFDoc>(PDFDocFactory().createPDFDoc(docPath, nullptr, nullptr));
+    if (!doc->isOk()) {
+        fprintf(stderr, "Error opening input PDF file.\n");
+        return 1;
+    }
+
+    // Embed an image.
+    Ref baseImageRef = ImageEmbeddingUtils::embed(doc->getXRef(), imagePath.toStr());
+    if (baseImageRef == Ref::INVALID()) {
+        fprintf(stderr, "embedImage() failed.\n");
+        return 1;
+    }
+
+    // Save updated PDF document.
+    // const GooString outputPathSuffix(".pdf");
+    // const GooString outputPath = GooString(&imagePath, &outputPathSuffix);
+    // doc->saveAs(&outputPath, writeForceRewrite);
+
+    // Check the base image.
+    Object baseImageObj = Object(baseImageRef).fetch(doc->getXRef());
+    Dict *baseImageDict = baseImageObj.streamGetDict();
+    if (std::string("XObject") != baseImageDict->lookup("Type").getName()) {
+        fprintf(stderr, "A problem with Type.\n");
+        return 1;
+    }
+    if (std::string("Image") != baseImageDict->lookup("Subtype").getName()) {
+        fprintf(stderr, "A problem with Subtype.\n");
+        return 1;
+    }
+    if (depth > 0) {
+        if (baseImageDict->lookup("BitsPerComponent").getInt() != depth) {
+            fprintf(stderr, "A problem with BitsPerComponent.\n");
+            return 1;
+        }
+    }
+    if (!colorSpace.toStr().empty()) {
+        if (colorSpace.cmp(baseImageDict->lookup("ColorSpace").getName()) != 0) {
+            fprintf(stderr, "A problem with ColorSpace.\n");
+            return 1;
+        }
+    }
+    if (!filter.toStr().empty()) {
+        if (filter.cmp(baseImageDict->lookup("Filter").getName()) != 0) {
+            fprintf(stderr, "A problem with Filter.\n");
+            return 1;
+        }
+    }
+    if (smask) {
+        Object maskObj = baseImageDict->lookup("SMask");
+        if (!maskObj.isStream()) {
+            fprintf(stderr, "A problem with SMask.\n");
+            return 1;
+        }
+    }
+    return 0;
+}


More information about the poppler mailing list