diff --git a/PdfForQtLib/sources/pdfcolorspaces.cpp b/PdfForQtLib/sources/pdfcolorspaces.cpp index c155d2e..fa985f4 100644 --- a/PdfForQtLib/sources/pdfcolorspaces.cpp +++ b/PdfForQtLib/sources/pdfcolorspaces.cpp @@ -103,13 +103,20 @@ QImage PDFAbstractColorSpace::getImage(const PDFImageData& imageData) const throw PDFParserException(PDFTranslationContext::tr("Invalid colors for color space. Color space has %1 colors. Provided color count is %4.").arg(getColorComponentCount()).arg(componentCount)); } + const std::vector& decode = imageData.getDecode(); + if (!decode.empty() && decode.size() != componentCount * 2) + { + throw PDFParserException(PDFTranslationContext::tr("Invalid size of the decoded array. Expected %1, actual %2.").arg(componentCount * 2).arg(decode.size())); + } + QDataStream stream(const_cast(&imageData.getData()), QIODevice::ReadOnly); PDFBitReader reader(&stream, imageData.getBitsPerComponent()); PDFColor color; color.resize(componentCount); - const double coefficient = 1.0 / reader.max(); + const double max = reader.max(); + const double coefficient = 1.0 / max; for (unsigned int i = 0, rowCount = imageData.getHeight(); i < rowCount; ++i) { reader.seek(i * imageData.getStride()); @@ -119,8 +126,17 @@ QImage PDFAbstractColorSpace::getImage(const PDFImageData& imageData) const { for (unsigned int k = 0; k < componentCount; ++k) { - PDFBitReader::Value value = reader.read(); - color[k] = value * coefficient; + PDFReal value = reader.read(); + + // Interpolate value, if it is not empty + if (!decode.empty()) + { + color[k] = interpolate(value, 0.0, max, decode[2 * k], decode[2 * k + 1]); + } + else + { + color[k] = value * coefficient; + } } QColor transformedColor = getColor(color); @@ -147,19 +163,26 @@ QImage PDFAbstractColorSpace::getImage(const PDFImageData& imageData) const } Q_ASSERT(componentCount > 0); - std::vector colorKeyMask = imageData.getColorKeyMask(); + const std::vector& colorKeyMask = imageData.getColorKeyMask(); if (colorKeyMask.size() / 2 != componentCount) { throw PDFParserException(PDFTranslationContext::tr("Invalid number of color components in color key mask. Expected %1, provided %2.").arg(2 * componentCount).arg(colorKeyMask.size())); } + const std::vector& decode = imageData.getDecode(); + if (!decode.empty() && decode.size() != componentCount * 2) + { + throw PDFParserException(PDFTranslationContext::tr("Invalid size of the decoded array. Expected %1, actual %2.").arg(componentCount * 2).arg(decode.size())); + } + QDataStream stream(const_cast(&imageData.getData()), QIODevice::ReadOnly); PDFBitReader reader(&stream, imageData.getBitsPerComponent()); PDFColor color; color.resize(componentCount); - const double coefficient = 1.0 / reader.max(); + const double max = reader.max(); + const double coefficient = 1.0 / max; for (unsigned int i = 0, rowCount = imageData.getHeight(); i < rowCount; ++i) { reader.seek(i * imageData.getStride()); @@ -173,11 +196,20 @@ QImage PDFAbstractColorSpace::getImage(const PDFImageData& imageData) const for (unsigned int k = 0; k < componentCount; ++k) { PDFBitReader::Value value = reader.read(); - color[k] = value * coefficient; + + // Interpolate value, if it is not empty + if (!decode.empty()) + { + color[k] = interpolate(value, 0.0, max, decode[2 * k], decode[2 * k + 1]); + } + else + { + color[k] = value * coefficient; + } Q_ASSERT(2 * k + 1 < colorKeyMask.size()); - if (static_cast(value) >= colorKeyMask[2 * k] && - static_cast(value) <= colorKeyMask[2 * k + 1]) + if (static_cast::type::value_type>(value) >= colorKeyMask[2 * k] && + static_cast::type::value_type>(value) <= colorKeyMask[2 * k + 1]) { ++maskedColors; } diff --git a/PdfForQtLib/sources/pdfcolorspaces.h b/PdfForQtLib/sources/pdfcolorspaces.h index 6908bdd..d71346e 100644 --- a/PdfForQtLib/sources/pdfcolorspaces.h +++ b/PdfForQtLib/sources/pdfcolorspaces.h @@ -80,8 +80,9 @@ public: enum class MaskingType { None, - ColorKeyMasking, - ImageMasking + ColorKeyMasking, ///< Masking by color key + Image, ///< Masking by image with alpha mask + ImageMask, ///< Masking by 1-bit image (see "ImageMask" entry in image's dictionary), current color from the graphic state is used to paint an image }; explicit PDFImageData() : @@ -102,7 +103,8 @@ public: unsigned int stride, MaskingType maskingType, QByteArray data, - std::vector&& colorKeyMask) : + std::vector&& colorKeyMask, + std::vector&& decode) : m_components(components), m_bitsPerComponent(bitsPerComponent), m_width(width), @@ -110,7 +112,8 @@ public: m_stride(stride), m_maskingType(maskingType), m_data(qMove(data)), - m_colorKeyMask(qMove(colorKeyMask)) + m_colorKeyMask(qMove(colorKeyMask)), + m_decode(qMove(decode)) { } @@ -122,7 +125,8 @@ public: unsigned int getStride() const { return m_stride; } MaskingType getMaskingType() const { return m_maskingType; } const QByteArray& getData() const { return m_data; } - std::vector getColorKeyMask() const { return m_colorKeyMask; } + const std::vector& getColorKeyMask() const { return m_colorKeyMask; } + const std::vector& getDecode() const { return m_decode; } /// Returns number of color channels unsigned int getColorChannels() const { return m_components; } @@ -149,6 +153,11 @@ private: /// If it is not empty, then it should contain 2 x number of color components, /// consisting of [ min_0, max_0, min_1, max_1, ... , min_n, max_n ]. std::vector m_colorKeyMask; + + /// Decode array. If it is empty, then no decoding is performed. If it is nonempty, + /// then contains n pairs of numbers, where n is number of color components. If ImageMask + /// in the image dictionary is true, then decode array should be [0 1] or [1 0]. + std::vector m_decode; }; using PDFColor3 = std::array; diff --git a/PdfForQtLib/sources/pdffunction.cpp b/PdfForQtLib/sources/pdffunction.cpp index 06dfa92..3921625 100644 --- a/PdfForQtLib/sources/pdffunction.cpp +++ b/PdfForQtLib/sources/pdffunction.cpp @@ -20,6 +20,7 @@ #include "pdfparser.h" #include "pdfdocument.h" #include "pdfexception.h" +#include "pdfutils.h" #include #include diff --git a/PdfForQtLib/sources/pdffunction.h b/PdfForQtLib/sources/pdffunction.h index 8e3f809..4707ba3 100644 --- a/PdfForQtLib/sources/pdffunction.h +++ b/PdfForQtLib/sources/pdffunction.h @@ -110,17 +110,6 @@ protected: /// \param value Value to be clamped inline PDFReal clampOutput(size_t index, PDFReal value) const { return qBound(m_range[2 * index], value, m_range[2 * index + 1]); } - /// Performs linear mapping of value x in interval [x_min, x_max] to the interval [y_min, y_max]. - /// \param x Value to be linearly remapped from interval [x_min, x_max] to the interval [y_min, y_max]. - /// \param x_min Start of the input interval - /// \param x_max End of the input interval - /// \param y_min Start of the output interval - /// \param y_max End of the output interval - static inline constexpr PDFReal interpolate(PDFReal x, PDFReal x_min, PDFReal x_max, PDFReal y_min, PDFReal y_max) - { - return y_min + (x - x_min) * (y_max - y_min) / (x_max - x_min); - } - /// Performs linear interpolation between c0 and c1 using x (in range [0.0, 1.0]). If x is not of this range, /// then the function succeeds, and returns value outside of interval [c0, c1]. /// \param x Value to be interpolated diff --git a/PdfForQtLib/sources/pdfimage.cpp b/PdfForQtLib/sources/pdfimage.cpp index 3b44ae0..bb3768c 100644 --- a/PdfForQtLib/sources/pdfimage.cpp +++ b/PdfForQtLib/sources/pdfimage.cpp @@ -19,6 +19,7 @@ #include "pdfdocument.h" #include "pdfconstants.h" #include "pdfexception.h" +#include "pdfutils.h" #include #include @@ -57,12 +58,10 @@ PDFImage PDFImage::createImage(const PDFDocument* document, const PDFStream* str throw PDFParserException(PDFTranslationContext::tr("Image has not data.")); } - // TODO: Implement ImageMask - // TODO: Implement Decode // TODO: Implement SMask // TODO: Implement SMaskInData - for (const char* notImplementedKey : { "ImageMask", "Decode", "SMask", "SMaskInData" }) + for (const char* notImplementedKey : { "SMask", "SMaskInData" }) { if (dictionary->hasKey(notImplementedKey)) { @@ -72,6 +71,8 @@ PDFImage PDFImage::createImage(const PDFDocument* document, const PDFStream* str PDFImageData::MaskingType maskingType = PDFImageData::MaskingType::None; std::vector mask; + std::vector decode = loader.readNumberArrayFromDictionary(dictionary, "Decode"); + bool imageMask = loader.readBooleanFromDictionary(dictionary, "ImageMask", false); // Fill Mask if (dictionary->hasKey("Mask")) @@ -85,11 +86,16 @@ PDFImage PDFImage::createImage(const PDFDocument* document, const PDFStream* str else if (object.isStream()) { // TODO: Implement Mask Image - maskingType = PDFImageData::MaskingType::ImageMasking; + maskingType = PDFImageData::MaskingType::Image; throw PDFRendererException(RenderErrorType::NotImplemented, PDFTranslationContext::tr("Mask image is not implemented.")); } } + if (imageMask) + { + maskingType = PDFImageData::MaskingType::ImageMask; + } + // Retrieve filters PDFObject filters; if (dictionary->hasKey(PDF_STREAM_DICT_FILTER)) @@ -245,7 +251,7 @@ PDFImage PDFImage::createImage(const PDFDocument* document, const PDFStream* str } jpeg_finish_decompress(&codec); - image.m_imageData = PDFImageData(components, bitsPerComponent, width, height, rowStride, maskingType, qMove(buffer), qMove(mask)); + image.m_imageData = PDFImageData(components, bitsPerComponent, width, height, rowStride, maskingType, qMove(buffer), qMove(mask), qMove(decode)); } jpeg_destroy_decompress(&codec); @@ -411,7 +417,7 @@ PDFImage PDFImage::createImage(const PDFDocument* document, const PDFStream* str } } - image.m_imageData = PDFImageData(components, bitsPerComponent, width, height, stride, maskingType, qMove(imageDataBuffer), qMove(mask)); + image.m_imageData = PDFImageData(components, bitsPerComponent, width, height, stride, maskingType, qMove(imageDataBuffer), qMove(mask), qMove(decode)); valid = image.m_imageData.isValid(); } else @@ -477,7 +483,32 @@ PDFImage PDFImage::createImage(const PDFDocument* document, const PDFStream* str const unsigned int stride = (components * bitsPerComponent * width + 7) / 8; QByteArray imageDataBuffer = document->getDecodedStream(stream); - image.m_imageData = PDFImageData(components, bitsPerComponent, width, height, stride, maskingType, qMove(imageDataBuffer), qMove(mask)); + image.m_imageData = PDFImageData(components, bitsPerComponent, width, height, stride, maskingType, qMove(imageDataBuffer), qMove(mask), qMove(decode)); + } + else if (imageMask) + { + // We intentionally have 8 bits in the following code, because if ImageMask is set to true, then "BitsPerComponent" + // should have always value of 1. + const unsigned int bitsPerComponent = static_cast(loader.readIntegerFromDictionary(dictionary, "BitsPerComponent", 8)); + + if (bitsPerComponent != 1) + { + throw PDFRendererException(RenderErrorType::Error, PDFTranslationContext::tr("Invalid number bits of image mask (should be 1 bit instead of %1 bits).").arg(bitsPerComponent)); + } + + const unsigned int width = static_cast(loader.readIntegerFromDictionary(dictionary, "Width", 0)); + const unsigned int height = static_cast(loader.readIntegerFromDictionary(dictionary, "Height", 0)); + + if (width == 0 || height == 0) + { + throw PDFRendererException(RenderErrorType::Error, PDFTranslationContext::tr("Invalid size of image (%1x%2)").arg(width).arg(height)); + } + + // Calculate stride + const unsigned int stride = (width + 7) / 8; + + QByteArray imageDataBuffer = document->getDecodedStream(stream); + image.m_imageData = PDFImageData(1, bitsPerComponent, width, height, stride, maskingType, qMove(imageDataBuffer), qMove(mask), qMove(decode)); } return image; @@ -489,6 +520,39 @@ QImage PDFImage::getImage() const { return m_colorSpace->getImage(m_imageData); } + else if (m_imageData.getMaskingType() == PDFImageData::MaskingType::ImageMask) + { + if (m_imageData.getBitsPerComponent() != 1) + { + throw PDFRendererException(RenderErrorType::Error, PDFTranslationContext::tr("Invalid number bits of image mask (should be 1 bit instead of %1 bits).").arg(m_imageData.getBitsPerComponent())); + } + + if (m_imageData.getWidth() == 0 || m_imageData.getHeight() == 0) + { + throw PDFRendererException(RenderErrorType::Error, PDFTranslationContext::tr("Invalid size of image (%1x%2)").arg(m_imageData.getWidth()).arg(m_imageData.getHeight())); + } + + QImage image(m_imageData.getWidth(), m_imageData.getHeight(), QImage::Format_Alpha8); + image.fill(QColor(Qt::transparent)); + + const bool flip01 = !m_imageData.getDecode().empty() && qFuzzyCompare(m_imageData.getDecode().front(), 1.0); + QDataStream stream(const_cast(&m_imageData.getData()), QIODevice::ReadOnly); + PDFBitReader reader(&stream, m_imageData.getBitsPerComponent()); + + for (unsigned int i = 0, rowCount = m_imageData.getHeight(); i < rowCount; ++i) + { + reader.seek(i * m_imageData.getStride()); + unsigned char* outputLine = image.scanLine(i); + + for (unsigned int j = 0; j < m_imageData.getWidth(); ++j) + { + const bool transparent = flip01 != static_cast(reader.read()); + *outputLine++ = transparent ? 0x00 : 0xFF; + } + } + + return image; + } return QImage(); } diff --git a/PdfForQtLib/sources/pdfutils.h b/PdfForQtLib/sources/pdfutils.h index 19af013..9955a1b 100644 --- a/PdfForQtLib/sources/pdfutils.h +++ b/PdfForQtLib/sources/pdfutils.h @@ -99,6 +99,17 @@ private: Value m_bitsInBuffer; }; +/// Performs linear mapping of value x in interval [x_min, x_max] to the interval [y_min, y_max]. +/// \param x Value to be linearly remapped from interval [x_min, x_max] to the interval [y_min, y_max]. +/// \param x_min Start of the input interval +/// \param x_max End of the input interval +/// \param y_min Start of the output interval +/// \param y_max End of the output interval +static inline constexpr PDFReal interpolate(PDFReal x, PDFReal x_min, PDFReal x_max, PDFReal y_min, PDFReal y_max) +{ + return y_min + (x - x_min) * (y_max - y_min) / (x_max - x_min); +} + } // namespace pdf #endif // PDFUTILS_H