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 <[email protected]> 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 <[email protected]>. 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 <[email protected]>. 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, <[email protected]>. Work sponsored by Technische Universität Dresden // Copyright (C) 2010 William Bader <[email protected]> // Copyright (C) 2021 Mahmoud Khalil <[email protected]> +// Copyright (C) 2021 Georgiy Sgibnev <[email protected]>. 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 <[email protected]> // Copyright (C) 2018 Marek Kasik <[email protected]> // Copyright (C) 2021 Mahmoud Khalil <[email protected]> +// Copyright (C) 2021 Georgiy Sgibnev <[email protected]>. 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 <[email protected]>. 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; +}
